RouteAddGroup.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import React, { useEffect, useState } from 'react';
  2. import {
  3. View,
  4. Text,
  5. TouchableOpacity,
  6. StyleSheet,
  7. Image,
  8. ActivityIndicator,
  9. ScrollView
  10. } from 'react-native';
  11. import { useNavigation } from '@react-navigation/native';
  12. import { Colors } from 'src/theme';
  13. import { Input } from 'src/components';
  14. import { storage, StoreType } from 'src/storage';
  15. import { PostCreateGroup, usePostCreateGroupMutation } from '@api/chat';
  16. import { API_HOST } from 'src/constants';
  17. import { AvatarWithInitials } from 'src/components';
  18. import * as ImagePicker from 'expo-image-picker';
  19. import * as yup from 'yup';
  20. import { FlashList } from '@shopify/flash-list';
  21. import { useSheetRouter } from 'react-native-actions-sheet';
  22. import { getFontSize } from 'src/utils';
  23. import CloseIcon from 'assets/icons/close.svg';
  24. import CameraIcon from 'assets/icons/messages/camera.svg';
  25. import { Formik } from 'formik';
  26. import { useGroupChatStore } from 'src/stores/groupChatStore';
  27. import { NAVIGATION_PAGES } from 'src/types';
  28. const ProfileSchema = yup.object({
  29. name: yup
  30. .string()
  31. .required('name is required')
  32. .min(3, 'group name should be at least 3 characters'),
  33. description: yup.string().optional().max(8000, 'description should not exceed 8000 characters'),
  34. users: yup.array().min(2, 'select at least 2 members').required('members are required')
  35. });
  36. const RouteAddGroup = () => {
  37. const router = useSheetRouter('search-modal');
  38. const token = storage.get('token', StoreType.STRING) as string;
  39. const navigation = useNavigation();
  40. const {
  41. selectedUsers,
  42. removeUser,
  43. image,
  44. setImage,
  45. groupName,
  46. setGroupName,
  47. description,
  48. setDescription,
  49. clearStore
  50. } = useGroupChatStore();
  51. const [isSubmitting, setIsSubmitting] = useState(false);
  52. const { mutate: createGroup } = usePostCreateGroupMutation();
  53. const pickImage = async () => {
  54. let result = await ImagePicker.launchImageLibraryAsync({
  55. mediaTypes: ImagePicker.MediaTypeOptions.Images,
  56. allowsEditing: true,
  57. aspect: [4, 3],
  58. quality: 1
  59. });
  60. if (!result.canceled) {
  61. setImage(result.assets[0]);
  62. }
  63. };
  64. const renderUserItem = ({ item }: { item: any }) => {
  65. return (
  66. <View style={styles.selectedUserContainer}>
  67. <View style={styles.userContainer}>
  68. <TouchableOpacity onPress={() => removeUser(item.user_id)} style={styles.removeIcon}>
  69. <CloseIcon width={10} height={10} fill={Colors.WHITE} />
  70. </TouchableOpacity>
  71. {item.avatar ? (
  72. <Image source={{ uri: API_HOST + item.avatar }} style={styles.selectedAvatar} />
  73. ) : (
  74. <AvatarWithInitials
  75. text={`${item.first_name[0] ?? ''}${item.last_name[0] ?? ''}`}
  76. flag={API_HOST + item.flag1}
  77. size={60}
  78. fontSize={21}
  79. borderColor={Colors.LIGHT_GRAY}
  80. borderWidth={1}
  81. />
  82. )}
  83. </View>
  84. </View>
  85. );
  86. };
  87. return (
  88. <Formik
  89. validationSchema={ProfileSchema}
  90. initialValues={{
  91. name: groupName,
  92. description,
  93. users: selectedUsers
  94. }}
  95. onSubmit={async (values) => {
  96. setIsSubmitting(true);
  97. const groupData: PostCreateGroup = {
  98. token,
  99. name: values.name,
  100. description: values.description,
  101. users: selectedUsers.map((user) => +user.user_id),
  102. admins: []
  103. };
  104. if (image && image.uri) {
  105. groupData.group_avatar = {
  106. type: image.type || 'image',
  107. uri: image.uri,
  108. name: image.uri.split('/').pop()!
  109. };
  110. }
  111. await createGroup(groupData, {
  112. onSuccess: (res) => {
  113. console.log('res', res);
  114. setIsSubmitting(false);
  115. navigation.navigate(
  116. ...([
  117. NAVIGATION_PAGES.CHAT,
  118. {
  119. id: 8948,
  120. name: 'Test Name',
  121. avatar: null,
  122. userType: 'normal'
  123. }
  124. ] as never)
  125. );
  126. router?.close();
  127. },
  128. onError: (err) => {
  129. console.log('err', err);
  130. setIsSubmitting(false);
  131. }
  132. });
  133. }}
  134. >
  135. {(props) => {
  136. useEffect(() => {
  137. props.setFieldValue('users', selectedUsers);
  138. }, [selectedUsers]);
  139. return (
  140. <View style={styles.container}>
  141. <View style={styles.header}>
  142. <TouchableOpacity
  143. onPress={() => {
  144. router?.goBack();
  145. }}
  146. style={{
  147. paddingTop: 16,
  148. paddingBottom: 6,
  149. paddingHorizontal: 6
  150. }}
  151. >
  152. <Text style={styles.headerText}>Back</Text>
  153. </TouchableOpacity>
  154. {isSubmitting ? (
  155. <View
  156. style={{
  157. paddingTop: 10,
  158. paddingHorizontal: 10
  159. }}
  160. >
  161. <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
  162. </View>
  163. ) : (
  164. <TouchableOpacity
  165. style={{
  166. paddingTop: 16,
  167. paddingBottom: 6,
  168. paddingHorizontal: 6
  169. }}
  170. onPress={() => props.handleSubmit()}
  171. >
  172. <Text style={styles.headerText}>Save</Text>
  173. </TouchableOpacity>
  174. )}
  175. </View>
  176. <ScrollView
  177. showsVerticalScrollIndicator={false}
  178. style={{ flex: 1 }}
  179. contentContainerStyle={{ gap: 16 }}
  180. >
  181. <View style={styles.photoContainer}>
  182. <TouchableOpacity style={styles.photoContainer} onPress={pickImage}>
  183. {!image && (
  184. <>
  185. <View style={[styles.groupPhoto, { backgroundColor: Colors.FILL_LIGHT }]}>
  186. <CameraIcon width={36} height={36} fill={Colors.LIGHT_GRAY} />
  187. </View>
  188. <Text style={styles.photoText}>Add photo</Text>
  189. </>
  190. )}
  191. {image && (
  192. <>
  193. <Image
  194. source={{ uri: image.uri }}
  195. style={{
  196. width: 80,
  197. height: 80,
  198. borderRadius: 40,
  199. borderWidth: 1,
  200. borderColor: Colors.FILL_LIGHT
  201. }}
  202. />
  203. <Text style={styles.photoText}>Change photo</Text>
  204. </>
  205. )}
  206. </TouchableOpacity>
  207. </View>
  208. <Input
  209. placeholder="Add group name"
  210. value={props.values.name}
  211. inputMode={'text'}
  212. onChange={(text) => {
  213. props.handleChange('name')(text);
  214. setGroupName(text);
  215. }}
  216. onBlur={props.handleBlur('name')}
  217. header="Group name"
  218. formikError={props.touched.name && props.errors.name}
  219. />
  220. <Input
  221. placeholder="Add group description"
  222. value={props.values.description}
  223. onChange={(text) => {
  224. props.handleChange('description')(text);
  225. setDescription(text);
  226. }}
  227. onBlur={props.handleBlur('description')}
  228. header="Description"
  229. multiline
  230. height={58}
  231. formikError={props.touched.description && props.errors.description}
  232. />
  233. {selectedUsers.length > 0 ? (
  234. <View
  235. style={[
  236. styles.usersRow,
  237. props.errors.users ? { borderColor: Colors.RED, borderWidth: 1 } : {}
  238. ]}
  239. >
  240. <FlashList
  241. viewabilityConfig={{
  242. waitForInteraction: true,
  243. itemVisiblePercentThreshold: 50,
  244. minimumViewTime: 1000
  245. }}
  246. scrollEnabled={false}
  247. data={selectedUsers}
  248. renderItem={renderUserItem}
  249. keyExtractor={(item) => item.user_id.toString()}
  250. estimatedItemSize={100}
  251. extraData={selectedUsers}
  252. showsVerticalScrollIndicator={false}
  253. contentContainerStyle={styles.selectedUsersList}
  254. numColumns={4}
  255. />
  256. </View>
  257. ) : null}
  258. {props.errors.users && (
  259. <Text
  260. style={[styles.textError, selectedUsers.length > 0 ? { marginTop: -32 } : {}]}
  261. >
  262. select at least 2 members
  263. </Text>
  264. )}
  265. </ScrollView>
  266. </View>
  267. );
  268. }}
  269. </Formik>
  270. );
  271. };
  272. const styles = StyleSheet.create({
  273. container: {
  274. gap: 16,
  275. height: '100%',
  276. backgroundColor: Colors.WHITE
  277. },
  278. header: {
  279. flexDirection: 'row',
  280. justifyContent: 'space-between',
  281. alignItems: 'center'
  282. },
  283. headerText: {
  284. color: Colors.DARK_BLUE,
  285. fontSize: getFontSize(14),
  286. fontWeight: '700'
  287. },
  288. photoContainer: {
  289. alignItems: 'center',
  290. gap: 8
  291. },
  292. groupPhoto: {
  293. width: 80,
  294. height: 80,
  295. borderRadius: 40,
  296. alignItems: 'center',
  297. justifyContent: 'center'
  298. },
  299. photoText: {
  300. color: Colors.DARK_BLUE,
  301. fontSize: 12,
  302. fontWeight: '700'
  303. },
  304. input: {
  305. marginBottom: 12
  306. },
  307. userItem: {
  308. flexDirection: 'row',
  309. alignItems: 'center',
  310. paddingVertical: 8,
  311. paddingHorizontal: 12,
  312. backgroundColor: Colors.FILL_LIGHT,
  313. gap: 8,
  314. borderRadius: 8,
  315. marginBottom: 6
  316. },
  317. avatar: {
  318. width: 36,
  319. height: 36,
  320. borderRadius: 18,
  321. borderWidth: 1,
  322. borderColor: Colors.LIGHT_GRAY
  323. },
  324. userName: {
  325. color: Colors.DARK_BLUE,
  326. fontSize: getFontSize(14),
  327. fontFamily: 'montserrat-700'
  328. },
  329. userSubtitle: {
  330. color: Colors.DARK_BLUE,
  331. fontSize: 14,
  332. fontFamily: 'montserrat-500'
  333. },
  334. userNM: {
  335. color: Colors.DARK_BLUE,
  336. fontSize: 14,
  337. fontFamily: 'montserrat-700',
  338. marginRight: 12
  339. },
  340. unselectedCircle: {
  341. width: 20,
  342. height: 20,
  343. borderRadius: 10,
  344. borderWidth: 1,
  345. borderColor: Colors.LIGHT_GRAY,
  346. justifyContent: 'center',
  347. alignItems: 'center'
  348. },
  349. selectedUsersList: {
  350. paddingTop: 12
  351. },
  352. selectedUserContainer: {
  353. position: 'relative',
  354. width: '100%',
  355. alignItems: 'center',
  356. paddingBottom: 12
  357. },
  358. userContainer: {},
  359. selectedAvatar: {
  360. width: 60,
  361. height: 60,
  362. borderRadius: 30,
  363. borderWidth: 1,
  364. borderColor: Colors.LIGHT_GRAY
  365. },
  366. removeIcon: {
  367. position: 'absolute',
  368. top: -4,
  369. right: -4,
  370. width: 22,
  371. height: 22,
  372. borderRadius: 11,
  373. borderWidth: 1,
  374. borderColor: Colors.WHITE,
  375. backgroundColor: Colors.RED,
  376. justifyContent: 'center',
  377. alignItems: 'center',
  378. zIndex: 1
  379. },
  380. usersRow: {
  381. flex: 1,
  382. backgroundColor: Colors.FILL_LIGHT,
  383. borderRadius: 8,
  384. marginBottom: 24
  385. },
  386. textError: {
  387. color: Colors.RED,
  388. fontSize: getFontSize(12),
  389. fontFamily: 'redhat-600',
  390. marginTop: 5
  391. }
  392. });
  393. export default RouteAddGroup;