AttachmentsModal.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import React, { useRef, useState } from 'react';
  2. import { StyleSheet, TouchableOpacity, View, Text } from 'react-native';
  3. import ActionSheet, { Route, SheetManager, useSheetRouter } from 'react-native-actions-sheet';
  4. import { getFontSize } from 'src/utils';
  5. import { Colors } from 'src/theme';
  6. import { WarningProps } from '../types';
  7. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  8. import { usePostReportConversationMutation } from '@api/chat';
  9. import * as ImagePicker from 'expo-image-picker';
  10. import * as DocumentPicker from '@react-native-documents/picker';
  11. import { MaterialCommunityIcons } from '@expo/vector-icons';
  12. import RouteB from './RouteB';
  13. import MegaphoneIcon from 'assets/icons/messages/megaphone.svg';
  14. import LocationIcon from 'assets/icons/messages/location.svg';
  15. import CameraIcon from 'assets/icons/messages/camera.svg';
  16. import ImagesIcon from 'assets/icons/messages/images.svg';
  17. // import PollIcon from 'assets/icons/messages/poll.svg';
  18. import { storage, StoreType } from 'src/storage';
  19. // import RouteC from './RouteC';
  20. const AttachmentsModal = () => {
  21. const insets = useSafeAreaInsets();
  22. const token = storage.get('token', StoreType.STRING) as string;
  23. const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState<WarningProps | null>(null);
  24. const { mutateAsync: reportUser } = usePostReportConversationMutation();
  25. const [data, setData] = useState<any | null>(null);
  26. const chatDataRef = useRef<any>(null);
  27. const handleSheetOpen = (payload: any) => {
  28. chatDataRef.current = payload;
  29. setData(payload);
  30. };
  31. const handleReport = async () => {
  32. const chatData = chatDataRef.current;
  33. if (!chatData) return;
  34. setShouldOpenWarningModal({
  35. title: `Report ${chatData.name}`,
  36. buttonTitle: 'Report',
  37. message: `Are you sure you want to report ${chatData.name}?\nIf you proceed, the chat history with ${chatData.name} will become visible to NomadMania admins for investigation.`,
  38. action: async () => {
  39. await reportUser({
  40. token,
  41. reported_user_id: chatData.uid
  42. });
  43. }
  44. });
  45. setTimeout(() => {
  46. SheetManager.hide('chat-attachments');
  47. setShouldOpenWarningModal(null);
  48. }, 300);
  49. };
  50. const handleOpenGallery = async () => {
  51. const chatData = chatDataRef.current;
  52. if (!chatData) return;
  53. try {
  54. const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
  55. if (!perm.granted) {
  56. console.warn('Permission for gallery not granted');
  57. return;
  58. }
  59. const result = await ImagePicker.launchImageLibraryAsync({
  60. mediaTypes: ImagePicker.MediaTypeOptions.All,
  61. allowsMultipleSelection: true,
  62. quality: 1,
  63. selectionLimit: 4
  64. });
  65. if (!result.canceled && result.assets) {
  66. const files = result.assets.map((asset) => ({
  67. uri: asset.uri,
  68. type: asset.type === 'video' ? 'video' : 'image'
  69. }));
  70. chatData.onSendMedia(files);
  71. }
  72. SheetManager.hide('chat-attachments');
  73. } catch (err) {
  74. console.warn('Gallery error: ', err);
  75. }
  76. };
  77. const handleOpenCamera = async () => {
  78. const chatData = chatDataRef.current;
  79. if (!chatData) return;
  80. try {
  81. const perm = await ImagePicker.requestCameraPermissionsAsync();
  82. if (!perm.granted) {
  83. console.warn('Permission for camera not granted');
  84. return;
  85. }
  86. const result = await ImagePicker.launchCameraAsync({
  87. mediaTypes: ImagePicker.MediaTypeOptions.Images,
  88. quality: 1
  89. });
  90. if (!result.canceled && result.assets) {
  91. const files = result.assets.map((asset) => ({
  92. uri: asset.uri,
  93. type: asset.type === 'video' ? 'video' : 'image'
  94. }));
  95. chatData.onSendMedia(files);
  96. }
  97. SheetManager.hide('chat-attachments');
  98. } catch (err) {
  99. console.warn('Camera error: ', err);
  100. }
  101. };
  102. const handleShareLiveLocation = () => {
  103. const chatData = chatDataRef.current;
  104. if (!chatData) return;
  105. chatData.onShareLiveLocation();
  106. SheetManager.hide('chat-attachments');
  107. };
  108. const handleSendFile = async () => {
  109. const chatData = chatDataRef.current;
  110. if (!chatData) return;
  111. try {
  112. const res = await DocumentPicker.pick({
  113. type: [DocumentPicker.types.allFiles],
  114. allowMultiSelection: false
  115. });
  116. let file = {
  117. uri: res[0].uri,
  118. name: res[0].name,
  119. type: res[0].type
  120. };
  121. if ((file.name && !file.name.includes('.')) || !file.type) {
  122. file = {
  123. ...file,
  124. type: file.type || 'application/octet-stream'
  125. };
  126. }
  127. if (chatData.onSendFile) {
  128. chatData.onSendFile([file]);
  129. }
  130. } catch (err) {
  131. console.warn('DocumentPicker error:', err);
  132. }
  133. SheetManager.hide('chat-attachments');
  134. };
  135. const RouteA = () => {
  136. const router = useSheetRouter('chat-attachments');
  137. return (
  138. <View
  139. style={[
  140. styles.container,
  141. { paddingBottom: 8 + insets.bottom, backgroundColor: Colors.FILL_LIGHT }
  142. ]}
  143. >
  144. <View style={styles.optionRow}>
  145. <TouchableOpacity style={styles.optionItem} onPress={handleOpenGallery}>
  146. <ImagesIcon height={36} />
  147. <Text style={styles.optionLabel}>Gallery</Text>
  148. </TouchableOpacity>
  149. <TouchableOpacity style={styles.optionItem} onPress={handleOpenCamera}>
  150. <CameraIcon height={36} fill={Colors.ORANGE} />
  151. <Text style={styles.optionLabel}>Camera</Text>
  152. </TouchableOpacity>
  153. <TouchableOpacity style={styles.optionItem} onPress={handleSendFile}>
  154. <MaterialCommunityIcons name="file" size={36} color={Colors.ORANGE} />
  155. <Text style={styles.optionLabel}>File</Text>
  156. </TouchableOpacity>
  157. <TouchableOpacity
  158. style={styles.optionItem}
  159. onPress={() => {
  160. router?.navigate('route-b');
  161. }}
  162. >
  163. <LocationIcon height={36} />
  164. <Text style={styles.optionLabel}>Location</Text>
  165. </TouchableOpacity>
  166. {/* <TouchableOpacity style={styles.optionItem} onPress={handleShareLiveLocation}>
  167. <MaterialCommunityIcons name="navigation" size={36} color={Colors.ORANGE} />
  168. <Text style={styles.optionLabel}>Live</Text>
  169. </TouchableOpacity> */}
  170. {/* {chatDataRef.current?.isGroup ? (
  171. <TouchableOpacity
  172. style={styles.optionItem}
  173. onPress={() => {
  174. router?.navigate('route-c');
  175. }}
  176. >
  177. <PollIcon height={36} />
  178. <Text style={styles.optionLabel}>Poll</Text>
  179. </TouchableOpacity>
  180. ) : null} */}
  181. {!chatDataRef.current?.isGroup ? (
  182. <TouchableOpacity style={styles.optionItem} onPress={handleReport}>
  183. <MegaphoneIcon fill={Colors.RED} width={36} height={36} />
  184. <Text style={styles.optionLabel}>Report</Text>
  185. </TouchableOpacity>
  186. ) : (
  187. <View style={styles.optionItem}></View>
  188. )}
  189. <View style={styles.optionItem}></View>
  190. </View>
  191. </View>
  192. );
  193. };
  194. const routes: Route[] = [
  195. {
  196. name: 'route-a',
  197. component: RouteA
  198. },
  199. {
  200. name: 'route-b',
  201. component: RouteB,
  202. params: { onSendLocation: data?.onSendLocation, insetsBottom: insets.bottom } as any
  203. }
  204. // {
  205. // name: 'route-c',
  206. // component: RouteC,
  207. // params: { onSendPoll: data?.onSendLocation, insetsBottom: insets.bottom } as any
  208. // }
  209. ];
  210. return (
  211. <ActionSheet
  212. id="chat-attachments"
  213. containerStyle={{
  214. backgroundColor: Colors.FILL_LIGHT
  215. }}
  216. enableRouterBackNavigation={true}
  217. closeOnTouchBackdrop={true}
  218. // keyboardHandlerEnabled={false}
  219. routes={routes}
  220. initialRoute="route-a"
  221. defaultOverlayOpacity={0.05}
  222. indicatorStyle={{ backgroundColor: Colors.WHITE }}
  223. onBeforeShow={(sheetRef) => {
  224. const payload = sheetRef || null;
  225. handleSheetOpen(payload);
  226. }}
  227. onClose={() => {
  228. if (shouldOpenWarningModal) {
  229. chatDataRef.current?.setModalInfo({
  230. visible: true,
  231. type: 'delete',
  232. title: shouldOpenWarningModal.title,
  233. buttonTitle: shouldOpenWarningModal.buttonTitle,
  234. message: shouldOpenWarningModal.message,
  235. action: shouldOpenWarningModal.action
  236. });
  237. }
  238. }}
  239. />
  240. );
  241. };
  242. const styles = StyleSheet.create({
  243. option: {
  244. flexDirection: 'row',
  245. alignItems: 'center',
  246. justifyContent: 'space-between'
  247. },
  248. optionText: {
  249. fontSize: getFontSize(12),
  250. fontWeight: '600',
  251. color: Colors.DARK_BLUE
  252. },
  253. dangerOption: {
  254. paddingVertical: 10,
  255. borderBottomWidth: 1,
  256. borderBlockColor: Colors.WHITE
  257. },
  258. dangerText: {
  259. color: Colors.RED
  260. },
  261. container: {
  262. backgroundColor: Colors.WHITE
  263. },
  264. optionRow: {
  265. flexDirection: 'row',
  266. justifyContent: 'space-between',
  267. paddingHorizontal: '5%',
  268. marginVertical: 20,
  269. flexWrap: 'wrap'
  270. },
  271. optionItem: {
  272. width: '30%',
  273. paddingVertical: 8,
  274. marginBottom: 12,
  275. alignItems: 'center'
  276. },
  277. optionLabel: {
  278. marginTop: 6,
  279. fontSize: 12,
  280. color: Colors.DARK_BLUE,
  281. fontWeight: '700'
  282. }
  283. });
  284. export default AttachmentsModal;