EditGroupModal.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. import React, { useState } from 'react';
  2. import {
  3. View,
  4. StyleSheet,
  5. ScrollView,
  6. TouchableOpacity,
  7. ActivityIndicator,
  8. Text,
  9. Image
  10. } from 'react-native';
  11. import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
  12. import * as yup from 'yup';
  13. import * as ImagePicker from 'expo-image-picker';
  14. import { chatStyles } from './styles';
  15. import { Colors } from 'src/theme';
  16. import { AvatarWithInitials, Input } from 'src/components';
  17. import Checkbox from 'expo-checkbox';
  18. import CameraIcon from 'assets/icons/messages/camera.svg';
  19. import { API_HOST } from 'src/constants';
  20. import { FlashList } from '@shopify/flash-list';
  21. import { Formik } from 'formik';
  22. import { useNavigation } from '@react-navigation/native';
  23. import { usePostUpdateGroupSettingsMutation, usePostRemoveFromGroupMutation } from '@api/chat';
  24. import { getFontSize } from 'src/utils';
  25. import CheckSvg from 'assets/icons/travels-screens/circle-check.svg';
  26. import { StoreType, storage } from 'src/storage';
  27. const SettingsSchema = yup.object({
  28. name: yup
  29. .string()
  30. .required('name is required')
  31. .min(3, 'group name should be at least 3 characters'),
  32. description: yup.string().optional().max(8000, 'description should not exceed 8000 characters')
  33. });
  34. const SearchModal = () => {
  35. const currentUserId = storage.get('uid', StoreType.STRING) as number;
  36. const [isSubmitting, setIsSubmitting] = useState(false);
  37. const [data, setData] = useState<any>(null);
  38. const [image, setImage] = useState<ImagePicker.ImagePickerAsset | null>(null);
  39. const { mutateAsync: editGroup } = usePostUpdateGroupSettingsMutation();
  40. const { mutateAsync: removeFromGroup } = usePostRemoveFromGroupMutation();
  41. const [canEdit, setCanEdit] = useState(false);
  42. const [canSend, setCanSend] = useState(false);
  43. const [canAdd, setCanAdd] = useState(false);
  44. const [canSee, setCanSee] = useState(false);
  45. const [filteredUsers, setFilteredUsers] = useState<any[]>([]);
  46. const handleSheetOpen = (payload: any) => {
  47. setData(payload);
  48. setCanEdit(payload?.settings.members_can_edit_settings === 1);
  49. setCanSend(payload?.settings.members_can_send_messages === 1);
  50. setCanAdd(payload?.settings.members_can_add_new_members === 1);
  51. setCanSee(payload?.settings.members_can_see_members === 1);
  52. setFilteredUsers(payload?.members ?? []);
  53. };
  54. const pickImage = async () => {
  55. let result = await ImagePicker.launchImageLibraryAsync({
  56. mediaTypes: ImagePicker.MediaTypeOptions.Images,
  57. allowsEditing: true,
  58. aspect: [4, 4],
  59. quality: 1
  60. });
  61. if (!result.canceled) {
  62. setImage(result.assets[0]);
  63. }
  64. };
  65. const toggleUserSelection = (user: any) => {
  66. const isSelected = filteredUsers.some((selected) => selected.uid === user.uid);
  67. if (isSelected) {
  68. setFilteredUsers((prev) => prev.filter((selected) => selected.uid !== user.uid));
  69. } else {
  70. setFilteredUsers((prev) => [...prev, user]);
  71. }
  72. };
  73. const renderUserItem = ({ item }: { item: any }) => {
  74. const isSelected = filteredUsers.some((selected) => selected.uid === item.uid);
  75. return (
  76. <TouchableOpacity
  77. style={styles.userItem}
  78. disabled={item.uid === +currentUserId}
  79. onPress={() => toggleUserSelection(item)}
  80. >
  81. {item.avatar ? (
  82. <Image source={{ uri: API_HOST + item.avatar }} style={styles.avatar} />
  83. ) : (
  84. <AvatarWithInitials
  85. text={item.name?.split(' ')[0][0] + (item.name?.split(' ')[1][0] ?? '')}
  86. flag={API_HOST + item?.flag1}
  87. size={36}
  88. fontSize={16}
  89. borderColor={Colors.LIGHT_GRAY}
  90. borderWidth={1}
  91. />
  92. )}
  93. <View style={styles.userDetails}>
  94. <Text style={styles.userName}>{item.name}</Text>
  95. </View>
  96. {item.admin === 1 && (
  97. <Text
  98. style={{
  99. fontSize: getFontSize(10),
  100. fontWeight: '600',
  101. color: Colors.LIGHT_GRAY
  102. }}
  103. >
  104. Admin
  105. </Text>
  106. )}
  107. {item.uid !== +currentUserId && (
  108. <View style={styles.unselectedCircle}>
  109. {isSelected && <CheckSvg fill={Colors.DARK_BLUE} height={20} width={20} />}
  110. </View>
  111. )}
  112. </TouchableOpacity>
  113. );
  114. };
  115. return (
  116. <ActionSheet
  117. id="edit-group-modal"
  118. containerStyle={styles.sheetContainer}
  119. defaultOverlayOpacity={0.5}
  120. closeOnTouchBackdrop={false}
  121. keyboardHandlerEnabled={false}
  122. onBeforeShow={(sheetRef) => {
  123. const payload = sheetRef || null;
  124. handleSheetOpen(payload);
  125. }}
  126. onClose={() => {
  127. setImage(null);
  128. data && data.refetch();
  129. }}
  130. >
  131. <Formik
  132. validationSchema={SettingsSchema}
  133. initialValues={{
  134. name: data?.settings?.name ?? '',
  135. description: data?.settings?.description ?? ''
  136. }}
  137. onSubmit={async (values) => {
  138. if (!data) return;
  139. setIsSubmitting(true);
  140. const removedUsers = data?.members
  141. ?.filter((member: any) => !filteredUsers.some((user) => user.uid === member.uid))
  142. ?.map((member: any) => member.uid);
  143. const groupData: any = {
  144. token: data.token,
  145. group_token: data.groupToken,
  146. name: values.name,
  147. description: values.description,
  148. members_can_edit_settings: canEdit ? 1 : 0,
  149. members_can_send_messages: canSend ? 1 : 0,
  150. members_can_add_new_members: canAdd ? 1 : 0,
  151. members_can_see_members: canSee ? 1 : 0
  152. };
  153. if (image && image.uri) {
  154. groupData.group_avatar = {
  155. type: image.type || 'image',
  156. uri: image.uri,
  157. name: image.uri.split('/').pop()!
  158. };
  159. }
  160. if (removedUsers.length > 0) {
  161. await Promise.all(
  162. removedUsers.map(
  163. async (userId: number) =>
  164. await removeFromGroup(
  165. {
  166. token: data.token,
  167. group_token: data.groupToken,
  168. uid: userId
  169. },
  170. {
  171. onSuccess: () => {
  172. data && data.refetchMembers();
  173. }
  174. }
  175. )
  176. )
  177. );
  178. }
  179. await editGroup(groupData, {
  180. onSuccess: () => {
  181. setIsSubmitting(false);
  182. setTimeout(() => {
  183. data && data.setCacheKey(Date.now());
  184. }, 2500);
  185. SheetManager.hide('edit-group-modal');
  186. },
  187. onError: () => {
  188. setIsSubmitting(false);
  189. }
  190. });
  191. }}
  192. >
  193. {(props) => {
  194. return (
  195. <View style={chatStyles.container}>
  196. <View style={chatStyles.header}>
  197. <TouchableOpacity
  198. onPress={() => SheetManager.hide('edit-group-modal')}
  199. style={styles.backButton}
  200. >
  201. <Text style={chatStyles.headerText}>Back</Text>
  202. </TouchableOpacity>
  203. {isSubmitting ? (
  204. <ActivityIndicator size="small" color={Colors.DARK_BLUE} style={styles.loader} />
  205. ) : (
  206. <TouchableOpacity onPress={() => props.handleSubmit()} style={styles.saveButton}>
  207. <Text style={chatStyles.headerText}>Save</Text>
  208. </TouchableOpacity>
  209. )}
  210. </View>
  211. <ScrollView
  212. showsVerticalScrollIndicator={false}
  213. style={{ flex: 1 }}
  214. contentContainerStyle={{ gap: 16 }}
  215. >
  216. <View style={styles.photoContainer}>
  217. <TouchableOpacity onPress={pickImage} style={chatStyles.photoContainer}>
  218. {image || data?.settings?.avatar ? (
  219. <>
  220. <Image
  221. source={{
  222. uri: image
  223. ? image.uri
  224. : `${API_HOST}${data?.settings?.avatar}?cacheBust=${data?.cacheKey}`
  225. }}
  226. style={styles.groupPhotoImage}
  227. />
  228. <Text style={chatStyles.photoText}>Change photo</Text>
  229. </>
  230. ) : (
  231. <>
  232. <View
  233. style={[chatStyles.groupPhoto, { backgroundColor: Colors.FILL_LIGHT }]}
  234. >
  235. <CameraIcon width={36} height={36} fill={Colors.LIGHT_GRAY} />
  236. </View>
  237. <Text style={chatStyles.photoText}>Add photo</Text>
  238. </>
  239. )}
  240. </TouchableOpacity>
  241. </View>
  242. <Input
  243. placeholder="Add group name"
  244. value={props.values.name}
  245. inputMode="text"
  246. onChange={props.handleChange('name')}
  247. onBlur={props.handleBlur('name')}
  248. header="Group name"
  249. formikError={props.touched.name && (props.errors.name as string)}
  250. />
  251. <Input
  252. placeholder="Add group description"
  253. value={props.values.description}
  254. onChange={props.handleChange('description')}
  255. onBlur={props.handleBlur('description')}
  256. header="Description"
  257. multiline
  258. height={58}
  259. formikError={props.touched.description && (props.errors.description as string)}
  260. />
  261. <View>
  262. <Text style={chatStyles.title}>Members can</Text>
  263. <View style={chatStyles.optionsContainer}>
  264. <TouchableOpacity
  265. style={chatStyles.option}
  266. onPress={() => setCanEdit(!canEdit)}
  267. >
  268. <Text style={chatStyles.optionText}>Edit group settings</Text>
  269. <Checkbox
  270. value={canEdit}
  271. color={Colors.DARK_BLUE}
  272. style={{ backgroundColor: Colors.WHITE, borderRadius: 4 }}
  273. onValueChange={() => setCanEdit(!canEdit)}
  274. />
  275. </TouchableOpacity>
  276. <TouchableOpacity
  277. style={chatStyles.option}
  278. onPress={() => setCanSend(!canSend)}
  279. >
  280. <Text style={chatStyles.optionText}>Send messages</Text>
  281. <Checkbox
  282. value={canSend}
  283. color={Colors.DARK_BLUE}
  284. style={{ backgroundColor: Colors.WHITE, borderRadius: 4 }}
  285. onValueChange={() => setCanSend(!canSend)}
  286. />
  287. </TouchableOpacity>
  288. <TouchableOpacity style={chatStyles.option} onPress={() => setCanAdd(!canAdd)}>
  289. <Text style={chatStyles.optionText}>Add new members</Text>
  290. <Checkbox
  291. value={canAdd}
  292. color={Colors.DARK_BLUE}
  293. style={{ backgroundColor: Colors.WHITE, borderRadius: 4 }}
  294. onValueChange={() => setCanAdd(!canAdd)}
  295. />
  296. </TouchableOpacity>
  297. <TouchableOpacity style={chatStyles.option} onPress={() => setCanSee(!canSee)}>
  298. <Text style={chatStyles.optionText}>See members</Text>
  299. <Checkbox
  300. value={canSee}
  301. color={Colors.DARK_BLUE}
  302. style={{ backgroundColor: Colors.WHITE, borderRadius: 4 }}
  303. onValueChange={() => setCanSee(!canSee)}
  304. />
  305. </TouchableOpacity>
  306. </View>
  307. </View>
  308. {data?.settings?.admin === 1 ? (
  309. <View>
  310. <Text style={chatStyles.title}>Members: {data?.members?.length}</Text>
  311. {data?.members?.length > 0 && (
  312. <FlashList
  313. viewabilityConfig={{
  314. waitForInteraction: true,
  315. itemVisiblePercentThreshold: 50,
  316. minimumViewTime: 1000
  317. }}
  318. data={data?.members || []}
  319. renderItem={renderUserItem}
  320. keyExtractor={(item) => item.uid.toString()}
  321. estimatedItemSize={100}
  322. extraData={filteredUsers}
  323. showsVerticalScrollIndicator={false}
  324. contentContainerStyle={{ paddingBottom: 16 }}
  325. />
  326. )}
  327. </View>
  328. ) : null}
  329. </ScrollView>
  330. </View>
  331. );
  332. }}
  333. </Formik>
  334. </ActionSheet>
  335. );
  336. };
  337. const styles = StyleSheet.create({
  338. sheetContainer: {
  339. height: '95%',
  340. borderTopLeftRadius: 15,
  341. borderTopRightRadius: 15,
  342. paddingHorizontal: 16
  343. },
  344. backButton: {
  345. paddingVertical: 16,
  346. paddingHorizontal: 6
  347. },
  348. saveButton: {
  349. paddingVertical: 16,
  350. paddingHorizontal: 6
  351. },
  352. loader: {
  353. padding: 10
  354. },
  355. groupPhotoImage: {
  356. width: 80,
  357. height: 80,
  358. borderRadius: 40,
  359. borderWidth: 1,
  360. borderColor: Colors.FILL_LIGHT
  361. },
  362. errorBorder: {
  363. borderColor: Colors.RED,
  364. borderWidth: 1
  365. },
  366. photoContainer: {
  367. alignItems: 'center',
  368. gap: 8
  369. },
  370. userDetails: {
  371. flex: 1
  372. },
  373. userName: {
  374. color: Colors.DARK_BLUE,
  375. fontSize: getFontSize(14),
  376. fontFamily: 'montserrat-700'
  377. },
  378. userSubtitle: {
  379. color: Colors.DARK_BLUE,
  380. fontSize: 14,
  381. fontFamily: 'montserrat-500'
  382. },
  383. userItem: {
  384. flexDirection: 'row',
  385. alignItems: 'center',
  386. paddingVertical: 8,
  387. paddingHorizontal: 12,
  388. backgroundColor: Colors.FILL_LIGHT,
  389. gap: 8,
  390. borderRadius: 8,
  391. marginBottom: 6
  392. },
  393. avatar: {
  394. width: 36,
  395. height: 36,
  396. borderRadius: 18,
  397. borderWidth: 1,
  398. borderColor: Colors.LIGHT_GRAY
  399. },
  400. unselectedCircle: {
  401. width: 20,
  402. height: 20,
  403. borderRadius: 10,
  404. borderWidth: 1,
  405. borderColor: Colors.LIGHT_GRAY,
  406. justifyContent: 'center',
  407. alignItems: 'center'
  408. }
  409. });
  410. export default SearchModal;