AttachmentsModal.tsx 8.5 KB

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