index.tsx 65 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163
  1. import React, { useState, useCallback, useEffect, useRef } from 'react';
  2. import {
  3. View,
  4. TouchableOpacity,
  5. Image,
  6. Text,
  7. FlatList,
  8. Dimensions,
  9. Alert,
  10. ScrollView,
  11. Linking,
  12. ActivityIndicator,
  13. AppState,
  14. AppStateStatus,
  15. TextInput,
  16. Platform
  17. } from 'react-native';
  18. import {
  19. GiftedChat,
  20. Bubble,
  21. InputToolbar,
  22. IMessage,
  23. Send,
  24. BubbleProps,
  25. Composer,
  26. TimeProps,
  27. MessageProps,
  28. Actions
  29. } from 'react-native-gifted-chat';
  30. import { MaterialCommunityIcons } from '@expo/vector-icons';
  31. import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler';
  32. import { AvatarWithInitials, Header, WarningModal } from 'src/components';
  33. import { Colors } from 'src/theme';
  34. import { useFocusEffect, useNavigation } from '@react-navigation/native';
  35. import { Audio } from 'expo-av';
  36. import ChatMessageBox from '../Components/ChatMessageBox';
  37. import ReplyMessageBar from '../Components/ReplyMessageBar';
  38. import { useSharedValue, withTiming } from 'react-native-reanimated';
  39. import { BlurView } from 'expo-blur';
  40. import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
  41. import Clipboard from '@react-native-clipboard/clipboard';
  42. import { trigger } from 'react-native-haptic-feedback';
  43. import ReactModal from 'react-native-modal';
  44. import { storage, StoreType } from 'src/storage';
  45. import {
  46. usePostDeleteMessageMutation,
  47. usePostGetChatWithQuery,
  48. usePostMessagesReadMutation,
  49. usePostReactToMessageMutation,
  50. usePostSendMessageMutation,
  51. usePostEditMessageMutation,
  52. usePostCanSendMessageQuery
  53. } from '@api/chat';
  54. import { CustomMessage, Message, Reaction } from '../types';
  55. import { API_HOST, APP_VERSION, WEBSOCKET_URL } from 'src/constants';
  56. import ReactionBar from '../Components/ReactionBar';
  57. import OptionsMenu from '../Components/OptionsMenu';
  58. import EmojiSelectorModal from '../Components/EmojiSelectorModal';
  59. import { styles } from './styles';
  60. import SendIcon from 'assets/icons/messages/send.svg';
  61. import { SheetManager } from 'react-native-actions-sheet';
  62. import { NAVIGATION_PAGES } from 'src/types';
  63. import { usePushNotification } from 'src/contexts/PushNotificationContext';
  64. import ReactionsListModal from '../Components/ReactionsListModal';
  65. import { dismissChatNotifications, isMessageEdited } from '../utils';
  66. import { useMessagesStore } from 'src/stores/unreadMessagesStore';
  67. import FileViewer from 'react-native-file-viewer';
  68. import * as FileSystem from 'expo-file-system';
  69. import ImageView from 'better-react-native-image-viewing';
  70. import * as MediaLibrary from 'expo-media-library';
  71. import BanIcon from 'assets/icons/messages/ban.svg';
  72. import AttachmentsModal from '../Components/AttachmentsModal';
  73. import RenderMessageVideo from '../Components/renderMessageVideo';
  74. import MessageLocation from '../Components/MessageLocation';
  75. import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
  76. const options = {
  77. enableVibrateFallback: true,
  78. ignoreAndroidSystemSettings: false
  79. };
  80. const reactionEmojis = ['👍', '❤️', '😂', '😮', '😭'];
  81. const ChatScreen = ({ route }: { route: any }) => {
  82. const token = storage.get('token', StoreType.STRING) as string;
  83. const {
  84. id,
  85. name,
  86. avatar,
  87. userType = 'normal'
  88. }: {
  89. id: number;
  90. name: string;
  91. avatar: string | null;
  92. userType: 'normal' | 'not_exist' | 'blocked';
  93. } = route.params;
  94. const userName =
  95. userType === 'blocked'
  96. ? 'Account is blocked'
  97. : userType === 'not_exist'
  98. ? 'Account does not exist'
  99. : name;
  100. const currentUserId = storage.get('uid', StoreType.STRING) as number;
  101. const insets = useSafeAreaInsets();
  102. const [messages, setMessages] = useState<CustomMessage[] | null>();
  103. const navigation = useNavigation();
  104. const [prevThenMessageId, setPrevThenMessageId] = useState<number>(-1);
  105. const { data: canSendMessage } = usePostCanSendMessageQuery(token, id, true);
  106. const {
  107. data: chatData,
  108. refetch,
  109. isFetching
  110. } = usePostGetChatWithQuery(token, id, 50, prevThenMessageId, true);
  111. const { mutateAsync: sendMessage } = usePostSendMessageMutation();
  112. const swipeableRowRef = useRef<Swipeable | null>(null);
  113. const messageContainerRef = useRef<FlatList<IMessage> | null>(null);
  114. const [selectedMedia, setSelectedMedia] = useState<any>(null);
  115. const [replyMessage, setReplyMessage] = useState<CustomMessage | null>(null);
  116. const [modalInfo, setModalInfo] = useState<any>({
  117. visible: false,
  118. type: 'confirm',
  119. message: '',
  120. action: () => {},
  121. buttonTitle: '',
  122. title: ''
  123. });
  124. const [selectedMessage, setSelectedMessage] = useState<BubbleProps<CustomMessage> | null>(null);
  125. const [emojiSelectorVisible, setEmojiSelectorVisible] = useState(false);
  126. const [messagePosition, setMessagePosition] = useState<{
  127. x: number;
  128. y: number;
  129. width: number;
  130. height: number;
  131. isMine: boolean;
  132. } | null>(null);
  133. const [isModalVisible, setIsModalVisible] = useState(false);
  134. const [unreadMessageIndex, setUnreadMessageIndex] = useState<number | null>(null);
  135. const { mutateAsync: markMessagesAsRead } = usePostMessagesReadMutation();
  136. const { mutateAsync: deleteMessage } = usePostDeleteMessageMutation();
  137. const { mutateAsync: reactToMessage } = usePostReactToMessageMutation();
  138. const { mutateAsync: editMessage } = usePostEditMessageMutation();
  139. const [highlightedMessageId, setHighlightedMessageId] = useState<number | null>(null);
  140. const [isRerendering, setIsRerendering] = useState<boolean>(false);
  141. const [isTyping, setIsTyping] = useState<boolean>(false);
  142. const messageRefs = useRef<{ [key: string]: any }>({});
  143. const flatList = useRef<FlatList | null>(null);
  144. const scrollY = useSharedValue(0);
  145. const { isSubscribed } = usePushNotification();
  146. const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
  147. const [hasMoreMessages, setHasMoreMessages] = useState(true);
  148. const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
  149. const [text, setText] = useState('');
  150. const [editingMessage, setEditingMessage] = useState<CustomMessage | null>(null);
  151. const appState = useRef(AppState.currentState);
  152. const textInputRef = useRef<TextInput>(null);
  153. const socket = useRef<WebSocket | null>(null);
  154. const closeModal = () => {
  155. setModalInfo({ ...modalInfo, visible: false });
  156. };
  157. useEffect(() => {
  158. if (!Audio || !Audio.setAudioModeAsync) {
  159. return;
  160. }
  161. Audio.setAudioModeAsync({
  162. allowsRecordingIOS: false,
  163. staysActiveInBackground: false,
  164. playsInSilentModeIOS: true,
  165. shouldDuckAndroid: true,
  166. playThroughEarpieceAndroid: false
  167. });
  168. }, []);
  169. useEffect(() => {
  170. if (canSendMessage && canSendMessage.need_authentication_or_friend === 1) {
  171. setModalInfo({
  172. visible: true,
  173. type: 'success',
  174. message: (
  175. <Text>
  176. Only legit NomadManias and friends can send messages.{'\n'}Befriend this traveller first
  177. or{' '}
  178. <Text
  179. style={{
  180. color: Colors.ORANGE,
  181. textDecorationLine: 'underline',
  182. textDecorationColor: Colors.ORANGE
  183. }}
  184. onPress={() =>
  185. Linking.openURL('https://nomadmania.com/badges/').catch((err) =>
  186. console.error('Failed to open auth URL:', err)
  187. )
  188. }
  189. >
  190. become Legit
  191. </Text>
  192. !
  193. </Text>
  194. ),
  195. action: () => {},
  196. buttonTitle: 'OK',
  197. title: 'Oops!'
  198. });
  199. }
  200. }, [canSendMessage]);
  201. const onSendMedia = useCallback(
  202. async (files: { uri: string; type: 'image' | 'video' }[]) => {
  203. for (const file of files) {
  204. const tempMessage: CustomMessage = {
  205. _id: Date.now() + Math.random(),
  206. text: '',
  207. createdAt: new Date(),
  208. user: { _id: +currentUserId, name: 'Me' },
  209. reactions: {},
  210. deleted: false,
  211. attachment: {
  212. id: -1,
  213. filename: file.type,
  214. filetype: file.type,
  215. attachment_link: file.uri,
  216. attachment_full_url: file.uri
  217. },
  218. pending: true,
  219. isSending: true,
  220. edited: false,
  221. image: file.type === 'image' ? file.uri : undefined,
  222. video: file.type === 'video' ? file.uri : undefined
  223. };
  224. if (replyMessage) {
  225. tempMessage.replyMessage = {
  226. text: replyMessage.text,
  227. id: replyMessage._id,
  228. name: replyMessage.user._id === id ? userName : 'Me'
  229. };
  230. }
  231. setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [tempMessage]));
  232. const messageData = {
  233. token,
  234. to_uid: id,
  235. text: '',
  236. reply_to_id: replyMessage ? (replyMessage._id as number) : -1,
  237. attachment: {
  238. uri: file.uri,
  239. type: file.type,
  240. name: file.uri.split('/').pop()
  241. }
  242. };
  243. const res = await sendMessage(messageData, {
  244. onSuccess: (res) => {
  245. const { attachment, message_id } = res;
  246. const newMessage = {
  247. _id: message_id,
  248. text: '',
  249. attachment,
  250. replyMessage: { ...tempMessage.replyMessage, sender: replyMessage?.user?._id },
  251. image: file.type === 'image' ? API_HOST + attachment.attachment_full_url : undefined,
  252. video: file.type === 'video' ? file.uri : undefined
  253. };
  254. setMessages((previousMessages) =>
  255. (previousMessages ?? []).map((msg) =>
  256. msg._id === tempMessage._id
  257. ? {
  258. ...msg,
  259. _id: res.message_id,
  260. isSending: false
  261. }
  262. : msg
  263. )
  264. );
  265. sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
  266. }
  267. });
  268. clearReplyMessage();
  269. }
  270. },
  271. [replyMessage]
  272. );
  273. const onSendLocation = useCallback(
  274. async (coords: { latitude: number; longitude: number }) => {
  275. const tempMessage: CustomMessage = {
  276. _id: Date.now() + Math.random(),
  277. text: '',
  278. createdAt: new Date(),
  279. user: { _id: +currentUserId, name: 'Me' },
  280. pending: true,
  281. deleted: false,
  282. edited: false,
  283. reactions: {},
  284. attachment: {
  285. id: -1,
  286. filename: 'location.json',
  287. filetype: 'nomadmania/location',
  288. lat: coords.latitude,
  289. lng: coords.longitude
  290. }
  291. };
  292. if (replyMessage) {
  293. tempMessage.replyMessage = {
  294. text: replyMessage.text,
  295. id: replyMessage._id,
  296. name: replyMessage.user._id === id ? userName : 'Me'
  297. };
  298. }
  299. setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [tempMessage]));
  300. const locationData = JSON.stringify({ lat: coords.latitude, lng: coords.longitude });
  301. const fileUri = FileSystem.documentDirectory + 'location.json';
  302. await FileSystem.writeAsStringAsync(fileUri, locationData);
  303. const locationFile = {
  304. uri: fileUri,
  305. type: 'application/json',
  306. name: 'location.json'
  307. };
  308. const messageData = {
  309. token,
  310. to_uid: id,
  311. text: tempMessage.text,
  312. reply_to_id: replyMessage ? (replyMessage._id as number) : -1,
  313. attachment: locationFile
  314. };
  315. sendMessage(messageData, {
  316. onSuccess: async (res) => {
  317. const { attachment, message_id } = res;
  318. const newMessage = {
  319. _id: message_id,
  320. text: '',
  321. attachment,
  322. replyMessage: { ...tempMessage.replyMessage, sender: replyMessage?.user?._id }
  323. };
  324. setMessages((previousMessages) =>
  325. (previousMessages ?? []).map((msg) =>
  326. msg._id === tempMessage._id ? { ...msg, _id: res.message_id } : msg
  327. )
  328. );
  329. sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
  330. await FileSystem.deleteAsync(fileUri);
  331. },
  332. onError: async (err) => {
  333. await FileSystem.deleteAsync(fileUri);
  334. }
  335. });
  336. clearReplyMessage();
  337. },
  338. [replyMessage]
  339. );
  340. const onSendFile = useCallback(
  341. (files: { uri: string; type: string; name?: string }[]) => {
  342. const newMsgs = files.map((file) => {
  343. const msg: CustomMessage = {
  344. _id: Date.now() + Math.random(),
  345. text: '',
  346. createdAt: new Date(),
  347. user: { _id: +currentUserId, name: 'Me' },
  348. deleted: false,
  349. reactions: {},
  350. isSending: true,
  351. edited: false,
  352. attachment: {
  353. id: -1,
  354. filename: file.name ?? 'File',
  355. filetype: file.type,
  356. attachment_link: file.uri
  357. }
  358. };
  359. if (replyMessage) {
  360. msg.replyMessage = {
  361. text: replyMessage.text,
  362. id: replyMessage._id,
  363. name: replyMessage.user._id === id ? userName : 'Me'
  364. };
  365. }
  366. if (file.type.includes('image')) {
  367. msg.image = file.uri;
  368. } else if (file.type.includes('video')) {
  369. msg.video = file.uri;
  370. }
  371. setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [msg]));
  372. const messageData = {
  373. token,
  374. to_uid: id,
  375. text: '',
  376. reply_to_id: replyMessage ? (replyMessage._id as number) : -1,
  377. attachment: {
  378. uri: file.uri,
  379. type: file.type,
  380. name: file.name || file.uri.split('/').pop()
  381. }
  382. };
  383. sendMessage(messageData, {
  384. onSuccess: (res) => {
  385. const { attachment, message_id } = res;
  386. const newMessage = {
  387. _id: message_id,
  388. text: '',
  389. attachment,
  390. replyMessage: { ...msg.replyMessage, sender: replyMessage?.user?._id },
  391. image: file.type === 'image' ? API_HOST + attachment.attachment_full_url : undefined,
  392. video: file.type === 'video' ? file.uri : undefined
  393. };
  394. setMessages((previousMessages) =>
  395. (previousMessages ?? []).map((prevMsg) =>
  396. prevMsg._id === msg._id
  397. ? {
  398. ...prevMsg,
  399. _id: res.message_id,
  400. attachment: res.attachment,
  401. isSending: false,
  402. image:
  403. res.attachment?.attachment_small_url && file.type?.startsWith('image')
  404. ? API_HOST + res.attachment.attachment_small_url
  405. : undefined,
  406. video:
  407. res.attachment?.attachment_link && file.type?.startsWith('video')
  408. ? API_HOST + res.attachment.attachment_link
  409. : undefined
  410. }
  411. : prevMsg
  412. )
  413. );
  414. sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
  415. }
  416. });
  417. return msg;
  418. });
  419. clearReplyMessage();
  420. },
  421. [replyMessage]
  422. );
  423. async function openFileInApp(uri: string, fileName: string) {
  424. try {
  425. const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
  426. if (!dirExist.exists) {
  427. await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
  428. }
  429. const fileUri = `${CACHED_ATTACHMENTS_DIR}${fileName}`;
  430. const fileExists = await FileSystem.getInfoAsync(fileUri);
  431. if (fileExists.exists && fileExists.size > 1024) {
  432. await FileViewer.open(fileUri, {
  433. showOpenWithDialog: true,
  434. showAppsSuggestions: true
  435. });
  436. return;
  437. }
  438. const { uri: localUri } = await FileSystem.downloadAsync(uri, fileUri, {
  439. headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
  440. });
  441. await FileViewer.open(localUri, {
  442. showOpenWithDialog: true,
  443. showAppsSuggestions: true
  444. });
  445. } catch (err) {
  446. console.warn('openFileInApp error:', err);
  447. Alert.alert('Cannot open file', 'No application found to open this file.');
  448. }
  449. }
  450. async function downloadFileToDevice(currentMessage: CustomMessage) {
  451. if (!currentMessage.image && !currentMessage.video) {
  452. return;
  453. }
  454. const fileUrl = currentMessage.video
  455. ? currentMessage.video
  456. : API_HOST + currentMessage.attachment?.attachment_full_url;
  457. const fileType = currentMessage.attachment?.filetype || '';
  458. const fileExt = fileType.split('/').pop() || '';
  459. const fileName = currentMessage.attachment?.filename?.split('.')[0] || 'file';
  460. const fileUri = `${FileSystem.cacheDirectory}${fileName}.${fileExt}`;
  461. try {
  462. const { status } = await MediaLibrary.requestPermissionsAsync();
  463. if (status !== 'granted') {
  464. return;
  465. }
  466. const downloadOptions = currentMessage.video
  467. ? { headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS } }
  468. : undefined;
  469. const { uri } = await FileSystem.downloadAsync(fileUrl, fileUri, downloadOptions);
  470. await MediaLibrary.createAssetAsync(uri);
  471. Alert.alert(
  472. 'Success',
  473. `${fileType.startsWith('video') ? 'Video' : 'Image'} saved to gallery.`
  474. );
  475. } catch (error) {
  476. Alert.alert('Error', 'Failed to download the file.');
  477. }
  478. }
  479. const renderMessageFile = (props: BubbleProps<CustomMessage>) => {
  480. const { currentMessage } = props;
  481. const leftMessage = currentMessage?.user?._id !== +currentUserId;
  482. if (!currentMessage?.attachment) return null;
  483. const { attachment_link, filename } = currentMessage.attachment;
  484. const fileName = filename ?? 'Attachment';
  485. const uri = API_HOST + attachment_link;
  486. return (
  487. <TouchableOpacity
  488. style={[
  489. styles.fileContainer,
  490. { backgroundColor: leftMessage ? 'rgba(15, 63, 79, 0.2)' : 'rgba(244, 244, 244, 0.2)' }
  491. ]}
  492. onPress={() => {
  493. openFileInApp(uri, fileName);
  494. }}
  495. onLongPress={() => handleLongPress(currentMessage, props)}
  496. disabled={currentMessage?.isSending}
  497. >
  498. {currentMessage?.isSending ? (
  499. <ActivityIndicator
  500. size="small"
  501. color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
  502. />
  503. ) : (
  504. <MaterialCommunityIcons
  505. name="file"
  506. size={32}
  507. color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
  508. />
  509. )}
  510. <Text
  511. style={[
  512. styles.fileNameText,
  513. { color: leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT }
  514. ]}
  515. >
  516. {fileName}
  517. </Text>
  518. </TouchableOpacity>
  519. );
  520. };
  521. const renderMessageLocation = (props: BubbleProps<CustomMessage>) => {
  522. const { currentMessage } = props;
  523. if (!currentMessage?.attachment) return null;
  524. const { lat, lng } = currentMessage.attachment;
  525. if (!lat || !lng) return null;
  526. return (
  527. <View
  528. style={[
  529. {
  530. alignItems: 'center',
  531. borderRadius: 8,
  532. marginVertical: 6,
  533. marginHorizontal: 6,
  534. width: 220
  535. }
  536. ]}
  537. >
  538. <MessageLocation props={props} lat={lat} lng={lng} onLongPress={handleLongPress} />
  539. </View>
  540. );
  541. };
  542. const onShareLiveLocation = useCallback(() => {
  543. const liveMsg: IMessage = {
  544. _id: 'live-loc-' + Date.now(),
  545. text: 'Sharing live location...',
  546. createdAt: new Date(),
  547. user: { _id: +currentUserId, name: 'Me' },
  548. system: false
  549. };
  550. // setMessages((prev) => GiftedChat.append(prev, [liveMsg]));
  551. }, []);
  552. useEffect(() => {
  553. let unsubscribe: any;
  554. const setupNotificationHandler = async () => {
  555. unsubscribe = await dismissChatNotifications(id, isSubscribed, setModalInfo, navigation);
  556. };
  557. setupNotificationHandler();
  558. return () => {
  559. if (unsubscribe) unsubscribe();
  560. updateUnreadMessagesCount();
  561. };
  562. }, [id]);
  563. useEffect(() => {
  564. socket.current = new WebSocket(WEBSOCKET_URL);
  565. socket.current.onopen = () => {
  566. socket.current?.send(JSON.stringify({ token }));
  567. };
  568. socket.current.onmessage = (event) => {
  569. const data = JSON.parse(event.data);
  570. handleWebSocketMessage(data);
  571. };
  572. socket.current.onclose = () => {
  573. console.log('WebSocket connection closed chat screen');
  574. };
  575. return () => {
  576. if (socket.current) {
  577. socket.current.close();
  578. socket.current = null;
  579. }
  580. };
  581. }, [token]);
  582. useEffect(() => {
  583. const handleAppStateChange = async (nextAppState: AppStateStatus) => {
  584. const prevState = appState.current;
  585. appState.current = nextAppState;
  586. if (prevState.match(/inactive|background/) && nextAppState === 'active') {
  587. refetch();
  588. if (!socket.current || socket.current.readyState === WebSocket.CLOSED) {
  589. socket.current = new WebSocket(WEBSOCKET_URL);
  590. socket.current.onopen = () => {
  591. socket.current?.send(JSON.stringify({ token }));
  592. };
  593. socket.current.onmessage = (event) => {
  594. const data = JSON.parse(event.data);
  595. handleWebSocketMessage(data);
  596. };
  597. }
  598. await dismissChatNotifications(id, isSubscribed, setModalInfo, navigation);
  599. }
  600. };
  601. const subscription = AppState.addEventListener('change', handleAppStateChange);
  602. return () => {
  603. subscription.remove();
  604. if (socket.current) {
  605. socket.current.close();
  606. socket.current = null;
  607. }
  608. };
  609. }, [token]);
  610. const handleWebSocketMessage = (data: any) => {
  611. switch (data.action) {
  612. case 'new_message':
  613. if (data.conversation_with === id && data.message) {
  614. const newMessage = mapApiMessageToGiftedMessage(data.message);
  615. setMessages((previousMessages) => {
  616. const messageExists =
  617. previousMessages && previousMessages.some((msg) => msg._id === newMessage._id);
  618. if (!messageExists) {
  619. return GiftedChat.append(previousMessages ?? [], [newMessage]);
  620. }
  621. return previousMessages;
  622. });
  623. }
  624. break;
  625. case 'new_reaction':
  626. if (data.conversation_with === id && data.reaction) {
  627. updateMessageWithReaction(data.reaction);
  628. }
  629. break;
  630. case 'unreact':
  631. if (data.conversation_with === id && data.unreacted_message_id) {
  632. removeReactionFromMessage(data.unreacted_message_id);
  633. }
  634. break;
  635. case 'delete_message':
  636. if (data.conversation_with === id && data.deleted_message_id) {
  637. removeDeletedMessage(data.deleted_message_id);
  638. }
  639. break;
  640. case 'is_typing':
  641. if (data.conversation_with === id) {
  642. setIsTyping(true);
  643. }
  644. break;
  645. case 'stopped_typing':
  646. if (data.conversation_with === id) {
  647. setIsTyping(false);
  648. }
  649. break;
  650. case 'messages_read':
  651. if (data.conversation_with === id && data.read_messages_ids) {
  652. setMessages(
  653. (prevMessages) =>
  654. prevMessages?.map((msg) => {
  655. if (data.read_messages_ids.includes(msg._id)) {
  656. return { ...msg, received: true };
  657. }
  658. return msg;
  659. }) ?? []
  660. );
  661. }
  662. break;
  663. case 'messages_received':
  664. if (data.conversation_with === id && data.received_messages_ids) {
  665. setMessages(
  666. (prevMessages) =>
  667. prevMessages?.map((msg) => {
  668. if (data.received_messages_ids.includes(msg._id)) {
  669. return { ...msg, sent: true };
  670. }
  671. return msg;
  672. }) ?? []
  673. );
  674. }
  675. break;
  676. case 'edited_message':
  677. if (data.conversation_with === id && data.message) {
  678. setMessages(
  679. (prevMessages) =>
  680. prevMessages?.map((msg) => {
  681. if (msg._id === data.message.id) {
  682. return { ...msg, text: data.message.text, edited: true };
  683. }
  684. return msg;
  685. }) ?? []
  686. );
  687. }
  688. break;
  689. default:
  690. break;
  691. }
  692. };
  693. const updateMessageWithReaction = (reactionData: any) => {
  694. setMessages(
  695. (prevMessages) =>
  696. prevMessages?.map((msg) => {
  697. if (msg._id === reactionData.message_id) {
  698. const updatedReactions = [
  699. ...(Array.isArray(msg.reactions)
  700. ? msg.reactions?.filter((r: any) => r.uid !== reactionData.uid)
  701. : []),
  702. reactionData
  703. ];
  704. return { ...msg, reactions: updatedReactions };
  705. }
  706. return msg;
  707. }) ?? []
  708. );
  709. };
  710. const removeReactionFromMessage = (messageId: number) => {
  711. setMessages(
  712. (prevMessages) =>
  713. prevMessages?.map((msg) => {
  714. if (msg._id === messageId) {
  715. const updatedReactions = Array.isArray(msg.reactions)
  716. ? msg.reactions?.filter((r: any) => r.uid !== id)
  717. : [];
  718. return { ...msg, reactions: updatedReactions };
  719. }
  720. return msg;
  721. }) ?? []
  722. );
  723. };
  724. const removeDeletedMessage = (messageId: number) => {
  725. setMessages(
  726. (prevMessages) =>
  727. prevMessages?.map((msg) => {
  728. if (msg._id === messageId) {
  729. return {
  730. ...msg,
  731. deleted: true,
  732. text: 'This message was deleted',
  733. pending: false,
  734. sent: false,
  735. received: false
  736. };
  737. }
  738. return msg;
  739. }) ?? []
  740. );
  741. };
  742. useEffect(() => {
  743. const pingInterval = setInterval(() => {
  744. if (socket.current && socket.current.readyState === WebSocket.OPEN) {
  745. socket.current.send(JSON.stringify({ action: 'ping', conversation_with: id }));
  746. } else {
  747. socket.current = new WebSocket(WEBSOCKET_URL);
  748. socket.current.onopen = () => {
  749. socket.current?.send(JSON.stringify({ token }));
  750. };
  751. socket.current.onmessage = (event) => {
  752. const data = JSON.parse(event.data);
  753. handleWebSocketMessage(data);
  754. };
  755. return () => {
  756. if (socket.current) {
  757. socket.current.close();
  758. socket.current = null;
  759. }
  760. };
  761. }
  762. }, 50000);
  763. return () => clearInterval(pingInterval);
  764. }, []);
  765. const sendWebSocketMessage = (
  766. action: string,
  767. message: CustomMessage | null = null,
  768. reaction: string | null = null,
  769. readMessagesIds: number[] | null = null
  770. ) => {
  771. if (socket.current && socket.current.readyState === WebSocket.OPEN) {
  772. const data: any = {
  773. action,
  774. conversation_with: id
  775. };
  776. if (action === 'new_message' && message) {
  777. data.message = {
  778. id: message._id,
  779. text: message.text,
  780. sender: +currentUserId,
  781. sent_datetime: new Date().toISOString().replace('T', ' ').substring(0, 19),
  782. reply_to_id: message.replyMessage?.id ?? -1,
  783. reply_to: message.replyMessage ?? null,
  784. reactions: message.reactions ?? '{}',
  785. status: 2,
  786. attachement: message.attachment ? message.attachment : -1
  787. };
  788. }
  789. if (action === 'new_reaction' && message && reaction) {
  790. data.reaction = {
  791. message_id: message._id,
  792. reaction,
  793. uid: +currentUserId,
  794. datetime: new Date().toISOString()
  795. };
  796. }
  797. if (action === 'unreact' && message) {
  798. data.message_id = message._id;
  799. }
  800. if (action === 'delete_message' && message) {
  801. data.message_id = message._id;
  802. }
  803. if (action === 'messages_read' && readMessagesIds) {
  804. data.messages_ids = readMessagesIds;
  805. }
  806. if (action === 'edited_message' && message) {
  807. data.message = {
  808. id: message._id,
  809. text: message.text
  810. };
  811. }
  812. socket.current.send(JSON.stringify(data));
  813. }
  814. };
  815. const handleTyping = (isTyping: boolean) => {
  816. if (isTyping) {
  817. sendWebSocketMessage('is_typing');
  818. } else {
  819. sendWebSocketMessage('stopped_typing');
  820. }
  821. };
  822. const mapApiMessageToGiftedMessage = (message: Message): CustomMessage => {
  823. return {
  824. _id: message.id,
  825. text: message.text,
  826. createdAt: new Date(message.sent_datetime + 'Z'),
  827. user: {
  828. _id: message.sender,
  829. name: message.sender === id ? userName : 'Me'
  830. },
  831. replyMessage:
  832. message.reply_to_id !== -1
  833. ? {
  834. text: message.reply_to.text,
  835. id: message.reply_to.id,
  836. name: message.reply_to.sender === id ? userName : 'Me'
  837. }
  838. : null,
  839. reactions: JSON.parse(message.reactions || '{}'),
  840. attachment: message.attachement !== -1 ? message.attachement : null,
  841. pending: message.status === 1,
  842. sent: message.status === 2,
  843. received: message.status === 3,
  844. deleted: message.status === 4,
  845. edited: isMessageEdited(message.edits),
  846. isSending: false,
  847. video:
  848. message.attachement !== -1 && message.attachement?.filetype?.startsWith('video')
  849. ? API_HOST + message.attachement?.attachment_link
  850. : null,
  851. image:
  852. message.attachement !== -1 && message.attachement?.filetype?.startsWith('image')
  853. ? API_HOST + message.attachement?.attachment_small_url
  854. : null
  855. };
  856. };
  857. useFocusEffect(
  858. useCallback(() => {
  859. refetch();
  860. }, [])
  861. );
  862. useFocusEffect(
  863. useCallback(() => {
  864. if (chatData?.messages) {
  865. const mappedMessages = chatData.messages.map(mapApiMessageToGiftedMessage);
  866. if (unreadMessageIndex === null && !isFetching) {
  867. const firstUnreadIndex = mappedMessages.findLastIndex(
  868. (msg) => !msg.received && !msg?.deleted && msg.user._id === id
  869. );
  870. if (firstUnreadIndex !== -1) {
  871. setUnreadMessageIndex(firstUnreadIndex);
  872. const unreadMarker: any = {
  873. _id: 'unreadMarker',
  874. text: 'Unread messages',
  875. system: true
  876. };
  877. mappedMessages.splice(firstUnreadIndex + 1, 0, unreadMarker);
  878. setTimeout(() => {
  879. if (flatList.current) {
  880. flatList.current.scrollToIndex({
  881. index: firstUnreadIndex,
  882. animated: true,
  883. viewPosition: 0.5
  884. });
  885. }
  886. }, 500);
  887. } else {
  888. setUnreadMessageIndex(0);
  889. }
  890. }
  891. setMessages((previousMessages) => {
  892. const newMessages = mappedMessages.filter(
  893. (newMsg) => !previousMessages?.some((oldMsg) => oldMsg._id === newMsg._id)
  894. );
  895. return prevThenMessageId !== -1 && previousMessages
  896. ? GiftedChat.prepend(previousMessages, newMessages)
  897. : mappedMessages;
  898. });
  899. if (mappedMessages.length < 50) {
  900. setHasMoreMessages(false);
  901. }
  902. if (
  903. mappedMessages.length === 0 &&
  904. !modalInfo.visible &&
  905. !canSendMessage?.need_authentication_or_friend
  906. ) {
  907. setTimeout(() => {
  908. textInputRef.current?.focus();
  909. }, 500);
  910. }
  911. setIsLoadingEarlier(false);
  912. }
  913. }, [chatData])
  914. );
  915. useEffect(() => {
  916. if (
  917. messages?.length === 0 &&
  918. !modalInfo.visible &&
  919. !canSendMessage?.need_authentication_or_friend
  920. ) {
  921. setTimeout(() => {
  922. textInputRef.current?.focus();
  923. }, 500);
  924. }
  925. }, [modalInfo]);
  926. const loadEarlierMessages = async () => {
  927. if (!hasMoreMessages || isLoadingEarlier || !messages) return;
  928. setIsLoadingEarlier(true);
  929. const previousMessageId = messages[messages.length - 1]._id;
  930. setPrevThenMessageId(previousMessageId);
  931. };
  932. const sentToServer = useRef<Set<number>>(new Set());
  933. const handleViewableItemsChanged = ({ viewableItems }: { viewableItems: any[] }) => {
  934. const newViewableUnreadMessages = viewableItems
  935. .filter(
  936. (item) =>
  937. !item.item.received &&
  938. !item.item.deleted &&
  939. !item.item.system &&
  940. item.item.user._id === id &&
  941. !sentToServer.current.has(item.item._id)
  942. )
  943. .map((item) => item.item._id);
  944. if (newViewableUnreadMessages.length > 0) {
  945. markMessagesAsRead(
  946. {
  947. token,
  948. from_user: id,
  949. messages_id: newViewableUnreadMessages
  950. },
  951. {
  952. onSuccess: () => {
  953. newViewableUnreadMessages.forEach((id) => sentToServer.current.add(id));
  954. sendWebSocketMessage('messages_read', null, null, newViewableUnreadMessages);
  955. }
  956. }
  957. );
  958. }
  959. };
  960. const renderSystemMessage = (props: any) => {
  961. if (props.currentMessage._id === 'unreadMarker') {
  962. return (
  963. <View style={styles.unreadMessagesContainer}>
  964. <Text style={styles.unreadMessagesText}>{props.currentMessage.text}</Text>
  965. </View>
  966. );
  967. }
  968. return null;
  969. };
  970. const clearReplyMessage = () => setReplyMessage(null);
  971. const clearEditMessage = () => {
  972. setEditingMessage(null);
  973. setText('');
  974. };
  975. const onInputTextChanged = (value: string) => {
  976. handleTyping(value.length > 0);
  977. setText(value);
  978. };
  979. const handleLongPress = (message: CustomMessage, props: BubbleProps<CustomMessage>) => {
  980. const messageRef = messageRefs.current[message._id];
  981. setSelectedMessage(props);
  982. trigger('impactMedium', options);
  983. const isMine = message.user._id === +currentUserId;
  984. if (messageRef) {
  985. messageRef.measureInWindow((x: number, y: number, width: number, height: number) => {
  986. const screenHeight = Dimensions.get('window').height;
  987. const spaceAbove = y - insets.top;
  988. const spaceBelow = screenHeight - (y + height) - insets.bottom * 2;
  989. let finalY = y;
  990. scrollY.value = 0;
  991. if (isNaN(y) || isNaN(height)) {
  992. console.error("Invalid measurement values for 'y' or 'height'", { y, height });
  993. return;
  994. }
  995. if (spaceBelow < 180) {
  996. const extraShift = 180 - spaceBelow;
  997. finalY -= extraShift;
  998. }
  999. if (spaceAbove < 50) {
  1000. const extraShift = 50 - spaceAbove;
  1001. finalY += extraShift;
  1002. }
  1003. if (spaceBelow < 180 || spaceAbove < 50) {
  1004. const targetY = screenHeight / 2 - height / 2;
  1005. scrollY.value = withTiming(finalY - finalY);
  1006. }
  1007. if (height > Dimensions.get('window').height - 200) {
  1008. finalY = 100;
  1009. }
  1010. finalY = isNaN(finalY) ? 0 : finalY;
  1011. setMessagePosition({ x, y: finalY, width, height, isMine });
  1012. setIsModalVisible(true);
  1013. });
  1014. }
  1015. };
  1016. const openEmojiSelector = () => {
  1017. SheetManager.show('emoji-selector');
  1018. trigger('impactLight', options);
  1019. };
  1020. const closeEmojiSelector = () => {
  1021. SheetManager.hide('emoji-selector');
  1022. };
  1023. const handleReactionPress = (emoji: string, messageId: number) => {
  1024. addReaction(messageId, emoji);
  1025. };
  1026. const handleDeleteMessage = (messageId: number) => {
  1027. deleteMessage(
  1028. {
  1029. token,
  1030. message_id: messageId,
  1031. conversation_with_user: id
  1032. },
  1033. {
  1034. onSuccess: () => {
  1035. setMessages(
  1036. (prevMessages) =>
  1037. prevMessages?.map((msg) => {
  1038. if (msg._id === messageId) {
  1039. return {
  1040. ...msg,
  1041. deleted: true,
  1042. text: 'This message was deleted',
  1043. pending: false,
  1044. sent: false,
  1045. received: false,
  1046. attachment: null,
  1047. image: undefined,
  1048. video: undefined
  1049. };
  1050. }
  1051. return msg;
  1052. }) ?? []
  1053. );
  1054. const messageToDelete = messages?.find((msg) => msg._id === messageId);
  1055. if (messageToDelete) {
  1056. sendWebSocketMessage('delete_message', messageToDelete, null, null);
  1057. }
  1058. }
  1059. }
  1060. );
  1061. };
  1062. const handleOptionPress = (option: string) => {
  1063. if (!selectedMessage) return;
  1064. switch (option) {
  1065. case 'reply':
  1066. setReplyMessage(selectedMessage.currentMessage);
  1067. setIsModalVisible(false);
  1068. break;
  1069. case 'copy':
  1070. Clipboard.setString(selectedMessage?.currentMessage?.text ?? '');
  1071. setIsModalVisible(false);
  1072. Alert.alert('Copied');
  1073. break;
  1074. case 'delete':
  1075. handleDeleteMessage(selectedMessage.currentMessage?._id);
  1076. setIsModalVisible(false);
  1077. break;
  1078. case 'download':
  1079. downloadFileToDevice(selectedMessage.currentMessage);
  1080. setIsModalVisible(false);
  1081. break;
  1082. case 'edit':
  1083. handleEditMessage(selectedMessage.currentMessage);
  1084. setIsModalVisible(false);
  1085. break;
  1086. default:
  1087. break;
  1088. }
  1089. closeEmojiSelector();
  1090. };
  1091. const openReactionList = (
  1092. reactions: { uid: number; name: string; reaction: string }[],
  1093. messageId: number
  1094. ) => {
  1095. SheetManager.show('reactions-list-modal', {
  1096. payload: {
  1097. users: reactions,
  1098. currentUserId: +currentUserId,
  1099. token,
  1100. messageId,
  1101. conversation_with_user: id,
  1102. setMessages,
  1103. sendWebSocketMessage
  1104. } as any
  1105. });
  1106. };
  1107. const renderTimeContainer = (time: TimeProps<CustomMessage>) => {
  1108. const createdAt = new Date(time.currentMessage.createdAt);
  1109. const formattedTime = createdAt.toLocaleTimeString([], {
  1110. hour: '2-digit',
  1111. minute: '2-digit',
  1112. hour12: true
  1113. });
  1114. const hasReactions =
  1115. time.currentMessage.reactions &&
  1116. Array.isArray(time.currentMessage.reactions) &&
  1117. time.currentMessage.reactions.length > 0;
  1118. return (
  1119. <View
  1120. style={[
  1121. styles.bottomContainer,
  1122. {
  1123. justifyContent: hasReactions ? 'space-between' : 'flex-end'
  1124. }
  1125. ]}
  1126. >
  1127. {hasReactions && (
  1128. <TouchableOpacity
  1129. style={[
  1130. styles.bottomCustomContainer,
  1131. {
  1132. backgroundColor:
  1133. time.position === 'left' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.2)'
  1134. }
  1135. ]}
  1136. onPress={() =>
  1137. Array.isArray(time.currentMessage.reactions) &&
  1138. openReactionList(
  1139. time.currentMessage.reactions.map((reaction) => ({
  1140. ...reaction,
  1141. name: reaction.uid === id ? userName : 'Me'
  1142. })),
  1143. time.currentMessage._id
  1144. )
  1145. }
  1146. >
  1147. {Object.entries(
  1148. (Array.isArray(time.currentMessage.reactions)
  1149. ? time.currentMessage.reactions
  1150. : []
  1151. ).reduce(
  1152. (acc: Record<string, { count: number }>, { reaction }: { reaction: string }) => {
  1153. if (!acc[reaction]) {
  1154. acc[reaction] = { count: 0 };
  1155. }
  1156. acc[reaction].count += 1;
  1157. return acc;
  1158. },
  1159. {}
  1160. )
  1161. ).map(([emoji, { count }]: any) => {
  1162. return (
  1163. <View key={emoji}>
  1164. <Text style={{}}>
  1165. {emoji}
  1166. {(count as number) > 1 ? ` ${count}` : ''}
  1167. </Text>
  1168. </View>
  1169. );
  1170. })}
  1171. </TouchableOpacity>
  1172. )}
  1173. <View style={styles.timeContainer}>
  1174. {time.currentMessage.edited && <Text style={styles.timeText}>Edited</Text>}
  1175. <Text style={[styles.timeText, time.currentMessage.edited ? { paddingLeft: 0 } : {}]}>
  1176. {formattedTime}
  1177. </Text>
  1178. {renderTicks(time.currentMessage)}
  1179. </View>
  1180. </View>
  1181. );
  1182. };
  1183. const renderSelectedMessage = () =>
  1184. selectedMessage && (
  1185. <View
  1186. style={{
  1187. maxHeight: '80%',
  1188. width: messagePosition?.width,
  1189. position: 'absolute',
  1190. top: messagePosition?.y,
  1191. left: messagePosition?.x
  1192. }}
  1193. >
  1194. <ScrollView>
  1195. <Bubble
  1196. {...selectedMessage}
  1197. wrapperStyle={{
  1198. right: { backgroundColor: Colors.DARK_BLUE },
  1199. left: { backgroundColor: Colors.FILL_LIGHT }
  1200. }}
  1201. textStyle={{
  1202. right: { color: Colors.WHITE },
  1203. left: { color: Colors.DARK_BLUE }
  1204. }}
  1205. renderTicks={() => null}
  1206. renderTime={renderTimeContainer}
  1207. renderCustomView={() =>
  1208. selectedMessage.currentMessage.attachment?.filetype === 'nomadmania/location'
  1209. ? renderMessageLocation(selectedMessage)
  1210. : selectedMessage.currentMessage.attachment &&
  1211. !selectedMessage.currentMessage.image &&
  1212. !selectedMessage.currentMessage.video
  1213. ? renderMessageFile(selectedMessage)
  1214. : renderReplyMessageView(selectedMessage)
  1215. }
  1216. />
  1217. </ScrollView>
  1218. </View>
  1219. );
  1220. const handleBackgroundPress = () => {
  1221. setIsModalVisible(false);
  1222. setSelectedMessage(null);
  1223. closeEmojiSelector();
  1224. };
  1225. useFocusEffect(
  1226. useCallback(() => {
  1227. navigation?.getParent()?.setOptions({
  1228. tabBarStyle: {
  1229. display: 'none'
  1230. }
  1231. });
  1232. }, [navigation])
  1233. );
  1234. const handleEditMessage = (message: CustomMessage) => {
  1235. setReplyMessage(null);
  1236. setEditingMessage(message);
  1237. setText(message.text);
  1238. textInputRef.current?.focus();
  1239. };
  1240. const onSend = useCallback(
  1241. (newMessages: CustomMessage[] = []) => {
  1242. if (editingMessage) {
  1243. if (editingMessage.text !== newMessages[0].text) {
  1244. setMessages((prevMessages) =>
  1245. (prevMessages ?? []).map((msg) =>
  1246. msg._id === editingMessage._id ? { ...msg, text: newMessages[0].text } : msg
  1247. )
  1248. );
  1249. editMessage(
  1250. {
  1251. token,
  1252. to_uid: id,
  1253. message_id: editingMessage._id,
  1254. text: newMessages[0].text
  1255. },
  1256. {
  1257. onSuccess: (res) => {
  1258. const editedMessage = {
  1259. _id: editingMessage._id,
  1260. text: newMessages[0].text
  1261. };
  1262. setMessages((previousMessages) =>
  1263. (previousMessages ?? []).map((msg) =>
  1264. msg._id === editingMessage._id
  1265. ? { ...msg, isSending: false, edited: true }
  1266. : msg
  1267. )
  1268. );
  1269. sendWebSocketMessage('edited_message', editedMessage as unknown as CustomMessage);
  1270. }
  1271. }
  1272. );
  1273. }
  1274. clearEditMessage();
  1275. clearReplyMessage();
  1276. return;
  1277. }
  1278. if (replyMessage) {
  1279. newMessages[0].replyMessage = {
  1280. text: replyMessage.text,
  1281. id: replyMessage._id,
  1282. name: replyMessage.user._id === id ? userName : 'Me'
  1283. };
  1284. }
  1285. const message = { ...newMessages[0], pending: true, isSending: true };
  1286. setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [message]));
  1287. sendMessage(
  1288. {
  1289. token,
  1290. to_uid: id,
  1291. text: message.text,
  1292. reply_to_id: replyMessage ? (replyMessage._id as number) : -1
  1293. },
  1294. {
  1295. onSuccess: (res) => {
  1296. if (res.need_authentication_or_friend === 1) {
  1297. setModalInfo({
  1298. visible: true,
  1299. type: 'success',
  1300. message: (
  1301. <Text>
  1302. Only legit NomadManias and friends can send messages.{'\n'}Befriend this
  1303. traveller first or{' '}
  1304. <Text
  1305. style={{
  1306. color: Colors.ORANGE,
  1307. textDecorationLine: 'underline',
  1308. textDecorationColor: Colors.ORANGE
  1309. }}
  1310. onPress={() =>
  1311. Linking.openURL('https://nomadmania.com/badges/').catch((err) =>
  1312. console.error('Failed to open auth URL:', err)
  1313. )
  1314. }
  1315. >
  1316. become Legit
  1317. </Text>
  1318. !
  1319. </Text>
  1320. ),
  1321. action: () => {},
  1322. buttonTitle: 'OK',
  1323. title: 'Oops!'
  1324. });
  1325. setMessages((messages ?? []).filter((msg) => msg._id === message._id));
  1326. return;
  1327. }
  1328. const newMessage = {
  1329. _id: res.message_id,
  1330. text: message.text,
  1331. replyMessage: { ...message.replyMessage, sender: replyMessage?.user?._id }
  1332. };
  1333. setMessages((previousMessages) =>
  1334. (previousMessages ?? []).map((msg) =>
  1335. msg._id === message._id ? { ...msg, _id: res.message_id, isSending: false } : msg
  1336. )
  1337. );
  1338. sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
  1339. }
  1340. }
  1341. );
  1342. clearReplyMessage();
  1343. },
  1344. [replyMessage, editingMessage]
  1345. );
  1346. const addReaction = (messageId: number, reaction: string) => {
  1347. if (!messages) return;
  1348. const updatedMessages = messages.map((msg: any) => {
  1349. if (msg._id === messageId) {
  1350. const updatedReactions: Reaction[] = [
  1351. ...(Array.isArray(msg.reactions)
  1352. ? msg.reactions?.filter((r: Reaction) => r.uid !== +currentUserId)
  1353. : []),
  1354. { datetime: new Date().toISOString(), reaction: reaction, uid: +currentUserId }
  1355. ];
  1356. return {
  1357. ...msg,
  1358. reactions: updatedReactions
  1359. };
  1360. }
  1361. return msg;
  1362. });
  1363. setMessages(updatedMessages);
  1364. reactToMessage(
  1365. { token, message_id: messageId, reaction: reaction, conversation_with_user: id },
  1366. {
  1367. onSuccess: () => {
  1368. const message = messages.find((msg) => msg._id === messageId);
  1369. if (message) {
  1370. sendWebSocketMessage('new_reaction', message, reaction);
  1371. }
  1372. }
  1373. }
  1374. );
  1375. setIsModalVisible(false);
  1376. };
  1377. const updateRowRef = useCallback(
  1378. (ref: any) => {
  1379. if (
  1380. ref &&
  1381. replyMessage &&
  1382. ref.props.children.props.currentMessage?._id === replyMessage._id
  1383. ) {
  1384. swipeableRowRef.current = ref;
  1385. }
  1386. },
  1387. [replyMessage]
  1388. );
  1389. const renderReplyMessageView = (props: BubbleProps<CustomMessage>) => {
  1390. if (!props.currentMessage) {
  1391. return null;
  1392. }
  1393. const { currentMessage } = props;
  1394. if (!currentMessage || !currentMessage?.replyMessage) {
  1395. return null;
  1396. }
  1397. return (
  1398. <TouchableOpacity
  1399. style={[
  1400. styles.replyMessageContainer,
  1401. {
  1402. backgroundColor:
  1403. currentMessage.user._id === id ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.2)',
  1404. borderColor: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE
  1405. }
  1406. ]}
  1407. onPress={() => {
  1408. if (currentMessage?.replyMessage?.id) {
  1409. scrollToMessage(currentMessage.replyMessage.id);
  1410. }
  1411. }}
  1412. >
  1413. <View style={styles.replyContent}>
  1414. <Text
  1415. style={[
  1416. styles.replyAuthorName,
  1417. { color: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE }
  1418. ]}
  1419. >
  1420. {currentMessage.replyMessage.name}
  1421. </Text>
  1422. <Text
  1423. numberOfLines={1}
  1424. style={[
  1425. styles.replyMessageText,
  1426. { color: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE }
  1427. ]}
  1428. >
  1429. {currentMessage.replyMessage.text}
  1430. </Text>
  1431. </View>
  1432. </TouchableOpacity>
  1433. );
  1434. };
  1435. const scrollToMessage = (messageId: number) => {
  1436. if (!messages) return;
  1437. const messageIndex = messages.findIndex((message) => message._id === messageId);
  1438. if (messageIndex !== -1 && flatList.current) {
  1439. flatList.current.scrollToIndex({
  1440. index: messageIndex,
  1441. animated: true,
  1442. viewPosition: 0.5
  1443. });
  1444. setHighlightedMessageId(messageId);
  1445. }
  1446. };
  1447. useEffect(() => {
  1448. if (highlightedMessageId && isRerendering) {
  1449. setTimeout(() => {
  1450. setHighlightedMessageId(null);
  1451. setIsRerendering(false);
  1452. }, 1500);
  1453. }
  1454. }, [highlightedMessageId, isRerendering]);
  1455. useEffect(() => {
  1456. if (replyMessage && swipeableRowRef.current) {
  1457. swipeableRowRef.current.close();
  1458. swipeableRowRef.current = null;
  1459. }
  1460. }, [replyMessage]);
  1461. const handleOpenImage = async (uri: string, fileName: string) => {
  1462. const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
  1463. if (!dirExist.exists) {
  1464. await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
  1465. }
  1466. const fileUri = `${CACHED_ATTACHMENTS_DIR}${fileName}`;
  1467. const fileExists = await FileSystem.getInfoAsync(fileUri);
  1468. if (fileExists.exists && fileExists.size > 1024) {
  1469. setSelectedMedia(fileUri);
  1470. return;
  1471. }
  1472. setSelectedMedia(uri);
  1473. const { uri: localUri } = await FileSystem.downloadAsync(uri, fileUri, {
  1474. headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
  1475. });
  1476. };
  1477. const renderMessageImage = (props: any) => {
  1478. const { currentMessage } = props;
  1479. const leftMessage = currentMessage?.user?._id !== +currentUserId;
  1480. return (
  1481. <TouchableOpacity
  1482. onPress={() => {
  1483. if (!currentMessage.attachment.attachment_full_url?.startsWith('/')) {
  1484. setSelectedMedia(currentMessage.attachment.attachment_full_url);
  1485. return;
  1486. }
  1487. handleOpenImage(
  1488. API_HOST + currentMessage.attachment.attachment_full_url,
  1489. currentMessage.attachment?.filename
  1490. );
  1491. }}
  1492. onLongPress={() => handleLongPress(currentMessage, props)}
  1493. style={styles.imageContainer}
  1494. disabled={currentMessage.isSending}
  1495. >
  1496. <Image
  1497. key={currentMessage.image}
  1498. source={{
  1499. uri: currentMessage.image,
  1500. headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
  1501. }}
  1502. style={styles.chatImage}
  1503. resizeMode="cover"
  1504. />
  1505. {currentMessage.isSending && (
  1506. <View
  1507. style={{
  1508. position: 'absolute',
  1509. top: 0,
  1510. left: 0,
  1511. right: 0,
  1512. bottom: 0,
  1513. justifyContent: 'center',
  1514. alignItems: 'center'
  1515. }}
  1516. >
  1517. <ActivityIndicator
  1518. size="large"
  1519. color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
  1520. />
  1521. </View>
  1522. )}
  1523. </TouchableOpacity>
  1524. );
  1525. };
  1526. const renderTicks = (message: CustomMessage) => {
  1527. if (message.user._id === id) return null;
  1528. if (message.isSending) {
  1529. return (
  1530. <View>
  1531. <ActivityIndicator
  1532. size={16}
  1533. color={Colors.LIGHT_GRAY}
  1534. style={{ transform: 'scale(0.8)' }}
  1535. />
  1536. </View>
  1537. );
  1538. }
  1539. return message.received ? (
  1540. <View>
  1541. <MaterialCommunityIcons name="check-all" size={16} color={Colors.WHITE} />
  1542. </View>
  1543. ) : message.sent ? (
  1544. <View>
  1545. <MaterialCommunityIcons name="check" size={16} color={Colors.WHITE} />
  1546. </View>
  1547. ) : message.pending ? (
  1548. <View>
  1549. <MaterialCommunityIcons name="check" size={16} color={Colors.LIGHT_GRAY} />
  1550. </View>
  1551. ) : null;
  1552. };
  1553. const renderBubble = (props: BubbleProps<CustomMessage>) => {
  1554. const { currentMessage } = props;
  1555. if (currentMessage.deleted) {
  1556. const text = currentMessage.text.length
  1557. ? props.currentMessage.text
  1558. : 'This message was deleted';
  1559. return (
  1560. <View>
  1561. <Bubble
  1562. {...props}
  1563. renderTime={() => null}
  1564. currentMessage={{
  1565. ...props.currentMessage,
  1566. text: text
  1567. }}
  1568. renderMessageText={() => (
  1569. <View style={{ paddingHorizontal: 12, paddingVertical: 6 }}>
  1570. <Text style={{ color: Colors.LIGHT_GRAY, fontStyle: 'italic', fontSize: 12 }}>
  1571. {text}
  1572. </Text>
  1573. </View>
  1574. )}
  1575. wrapperStyle={{
  1576. right: {
  1577. backgroundColor: Colors.DARK_BLUE
  1578. },
  1579. left: {
  1580. backgroundColor: Colors.FILL_LIGHT
  1581. }
  1582. }}
  1583. textStyle={{
  1584. left: {
  1585. color: Colors.DARK_BLUE
  1586. },
  1587. right: {
  1588. color: Colors.WHITE
  1589. }
  1590. }}
  1591. />
  1592. </View>
  1593. );
  1594. }
  1595. const isHighlighted = currentMessage._id === highlightedMessageId;
  1596. const backgroundColor = isHighlighted
  1597. ? Colors.ORANGE
  1598. : currentMessage.user._id === +currentUserId
  1599. ? Colors.DARK_BLUE
  1600. : Colors.FILL_LIGHT;
  1601. return (
  1602. <View
  1603. key={`${currentMessage._id}-${isHighlighted ? 'highlighted' : 'normal'}`}
  1604. ref={(ref) => {
  1605. if (ref && currentMessage) {
  1606. messageRefs.current[currentMessage._id] = ref;
  1607. }
  1608. }}
  1609. collapsable={false}
  1610. >
  1611. <Bubble
  1612. {...props}
  1613. wrapperStyle={{
  1614. right: {
  1615. backgroundColor: backgroundColor
  1616. },
  1617. left: {
  1618. backgroundColor: backgroundColor
  1619. }
  1620. }}
  1621. textStyle={{
  1622. left: {
  1623. color: Colors.DARK_BLUE
  1624. },
  1625. right: {
  1626. color: Colors.FILL_LIGHT
  1627. }
  1628. }}
  1629. onLongPress={() => handleLongPress(currentMessage, props)}
  1630. renderTicks={() => null}
  1631. renderTime={renderTimeContainer}
  1632. renderCustomView={() =>
  1633. currentMessage.attachment?.filetype === 'nomadmania/location'
  1634. ? renderMessageLocation(props)
  1635. : currentMessage.attachment && !currentMessage.image && !currentMessage.video
  1636. ? renderMessageFile(props)
  1637. : renderReplyMessageView(props)
  1638. }
  1639. />
  1640. </View>
  1641. );
  1642. };
  1643. const openAttachmentsModal = () => {
  1644. SheetManager.show('chat-attachments', {
  1645. payload: {
  1646. name: userName,
  1647. uid: id,
  1648. setModalInfo,
  1649. closeOptions: () => {},
  1650. onSendMedia,
  1651. onSendLocation,
  1652. onShareLiveLocation,
  1653. onSendFile
  1654. } as any
  1655. });
  1656. };
  1657. const renderInputToolbar = (props: any) => (
  1658. <InputToolbar
  1659. {...props}
  1660. renderActions={() =>
  1661. userType === 'normal' && !editingMessage ? (
  1662. <Actions
  1663. icon={() => <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />}
  1664. onPressActionButton={openAttachmentsModal}
  1665. />
  1666. ) : null
  1667. }
  1668. containerStyle={{
  1669. backgroundColor: Colors.FILL_LIGHT
  1670. }}
  1671. />
  1672. );
  1673. const renderScrollToBottom = () => {
  1674. return (
  1675. <TouchableOpacity
  1676. style={styles.scrollToBottom}
  1677. onPress={() => {
  1678. if (flatList.current) {
  1679. flatList.current.scrollToIndex({ index: 0, animated: true });
  1680. }
  1681. }}
  1682. >
  1683. <MaterialCommunityIcons name="chevron-down" size={24} color={Colors.WHITE} />
  1684. </TouchableOpacity>
  1685. );
  1686. };
  1687. const shouldUpdateMessage = (
  1688. props: MessageProps<IMessage>,
  1689. nextProps: MessageProps<IMessage>
  1690. ) => {
  1691. setIsRerendering(true);
  1692. const currentId = nextProps.currentMessage._id;
  1693. return currentId === highlightedMessageId;
  1694. };
  1695. return (
  1696. <SafeAreaView
  1697. edges={['top']}
  1698. style={{
  1699. height: '100%'
  1700. }}
  1701. >
  1702. <View style={{ paddingHorizontal: '5%' }}>
  1703. <Header
  1704. label={userName}
  1705. textColor={userType !== 'normal' ? Colors.RED : Colors.DARK_BLUE}
  1706. rightElement={
  1707. <TouchableOpacity
  1708. onPress={() =>
  1709. navigation.navigate(
  1710. ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: id }] as never)
  1711. )
  1712. }
  1713. disabled={userType !== 'normal'}
  1714. >
  1715. {avatar && userType === 'normal' ? (
  1716. <Image source={{ uri: API_HOST + avatar }} style={styles.avatar} />
  1717. ) : userType === 'normal' ? (
  1718. <AvatarWithInitials
  1719. text={
  1720. name
  1721. .split(/ (.+)/)
  1722. .map((n) => n[0])
  1723. .join('') ?? ''
  1724. }
  1725. flag={API_HOST + 'flag.png'}
  1726. size={30}
  1727. fontSize={12}
  1728. />
  1729. ) : (
  1730. <BanIcon fill={Colors.RED} width={30} height={30} />
  1731. )}
  1732. </TouchableOpacity>
  1733. }
  1734. />
  1735. </View>
  1736. <GestureHandlerRootView style={styles.container}>
  1737. {messages ? (
  1738. <GiftedChat
  1739. messages={messages as CustomMessage[]}
  1740. text={text}
  1741. listViewProps={{
  1742. ref: flatList,
  1743. showsVerticalScrollIndicator: false,
  1744. initialNumToRender: 30,
  1745. onViewableItemsChanged: handleViewableItemsChanged,
  1746. viewabilityConfig: { itemVisiblePercentThreshold: 50 },
  1747. onScrollToIndexFailed: (info: any) => {
  1748. const wait = new Promise((resolve) => setTimeout(resolve, 300));
  1749. wait.then(() => {
  1750. flatList.current?.scrollToIndex({
  1751. index: info.index,
  1752. animated: true,
  1753. viewPosition: 0.5
  1754. });
  1755. });
  1756. }
  1757. }}
  1758. renderSystemMessage={renderSystemMessage}
  1759. onSend={(newMessages: CustomMessage[]) => onSend(newMessages)}
  1760. user={{ _id: +currentUserId, name: 'Me' }}
  1761. renderBubble={renderBubble}
  1762. renderMessageImage={renderMessageImage}
  1763. renderInputToolbar={renderInputToolbar}
  1764. renderCustomView={renderReplyMessageView}
  1765. isCustomViewBottom={false}
  1766. messageContainerRef={messageContainerRef}
  1767. minComposerHeight={34}
  1768. onInputTextChanged={onInputTextChanged}
  1769. textInputRef={textInputRef}
  1770. isTyping={isTyping}
  1771. renderSend={(props) =>
  1772. editingMessage ? (
  1773. <View style={[styles.sendBtn, { paddingHorizontal: 8 }]}>
  1774. {props.text?.trim() && (
  1775. <Send
  1776. {...props}
  1777. containerStyle={{
  1778. justifyContent: 'center'
  1779. }}
  1780. >
  1781. <View style={styles.editBtn}>
  1782. <MaterialCommunityIcons name="check" size={22} color={Colors.WHITE} />
  1783. </View>
  1784. </Send>
  1785. )}
  1786. {!props.text?.trim() && (
  1787. <View style={[styles.editBtn, { backgroundColor: Colors.LIGHT_GRAY }]}>
  1788. <MaterialCommunityIcons name="check" size={22} color={Colors.WHITE} />
  1789. </View>
  1790. )}
  1791. </View>
  1792. ) : (
  1793. <View style={styles.sendBtn}>
  1794. {props.text?.trim() && (
  1795. <Send
  1796. {...props}
  1797. containerStyle={{
  1798. justifyContent: 'center'
  1799. }}
  1800. >
  1801. <SendIcon fill={Colors.DARK_BLUE} />
  1802. </Send>
  1803. )}
  1804. {!props.text?.trim() && <SendIcon fill={Colors.LIGHT_GRAY} />}
  1805. </View>
  1806. )
  1807. }
  1808. renderMessageVideo={(props) => (
  1809. <RenderMessageVideo
  1810. props={props}
  1811. token={token}
  1812. currentUserId={+currentUserId}
  1813. onLongPress={handleLongPress}
  1814. />
  1815. )}
  1816. textInputProps={{
  1817. ...styles.composer,
  1818. selectionColor: Colors.LIGHT_GRAY
  1819. }}
  1820. placeholder=""
  1821. renderMessage={(props) => (
  1822. <ChatMessageBox
  1823. {...(props as MessageProps<CustomMessage>)}
  1824. updateRowRef={updateRowRef}
  1825. setReplyOnSwipeOpen={setReplyMessage}
  1826. />
  1827. )}
  1828. renderChatFooter={() => (
  1829. <ReplyMessageBar
  1830. clearReply={clearReplyMessage}
  1831. clearEditMessage={clearEditMessage}
  1832. message={replyMessage}
  1833. editingMessage={editingMessage}
  1834. />
  1835. )}
  1836. renderAvatar={null}
  1837. maxComposerHeight={100}
  1838. renderComposer={(props) => <Composer {...props} />}
  1839. keyboardShouldPersistTaps="handled"
  1840. renderChatEmpty={() => (
  1841. <View style={styles.emptyChat}>
  1842. <Text
  1843. style={styles.emptyChatText}
  1844. >{`No messages yet.\nFeel free to start the conversation.`}</Text>
  1845. </View>
  1846. )}
  1847. shouldUpdateMessage={shouldUpdateMessage}
  1848. scrollToBottom={true}
  1849. scrollToBottomComponent={renderScrollToBottom}
  1850. scrollToBottomStyle={{ backgroundColor: 'transparent' }}
  1851. parsePatterns={(linkStyle) => [
  1852. {
  1853. type: 'url',
  1854. style: { color: Colors.ORANGE, textDecorationLine: 'underline' },
  1855. onPress: (url: string) => Linking.openURL(url),
  1856. onLongPress: (url: string) => {
  1857. Clipboard.setString(url ?? '');
  1858. Alert.alert('Link copied');
  1859. }
  1860. }
  1861. ]}
  1862. infiniteScroll={true}
  1863. loadEarlier={hasMoreMessages}
  1864. isLoadingEarlier={isLoadingEarlier}
  1865. onLoadEarlier={loadEarlierMessages}
  1866. renderLoadEarlier={() => (
  1867. <View style={{ paddingVertical: 20 }}>
  1868. <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
  1869. </View>
  1870. )}
  1871. />
  1872. ) : (
  1873. <ActivityIndicator size="large" color={Colors.DARK_BLUE} />
  1874. )}
  1875. <ImageView
  1876. images={[
  1877. {
  1878. uri: selectedMedia,
  1879. cache: 'force-cache',
  1880. headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
  1881. }
  1882. ]}
  1883. imageIndex={0}
  1884. visible={!!selectedMedia}
  1885. onRequestClose={() => setSelectedMedia(null)}
  1886. backgroundColor={Colors.DARK_BLUE}
  1887. />
  1888. <ReactModal
  1889. isVisible={isModalVisible}
  1890. onBackdropPress={handleBackgroundPress}
  1891. style={styles.reactModalContainer}
  1892. animationIn="fadeIn"
  1893. animationOut="fadeOut"
  1894. useNativeDriver
  1895. backdropColor="transparent"
  1896. >
  1897. <BlurView
  1898. intensity={80}
  1899. style={styles.modalBackground}
  1900. experimentalBlurMethod="dimezisBlurView"
  1901. >
  1902. <TouchableOpacity
  1903. style={styles.modalBackground}
  1904. activeOpacity={1}
  1905. onPress={handleBackgroundPress}
  1906. >
  1907. <ReactionBar
  1908. messagePosition={messagePosition}
  1909. selectedMessage={selectedMessage}
  1910. reactionEmojis={reactionEmojis}
  1911. handleReactionPress={handleReactionPress}
  1912. openEmojiSelector={openEmojiSelector}
  1913. />
  1914. {renderSelectedMessage()}
  1915. <OptionsMenu
  1916. selectedMessage={selectedMessage}
  1917. handleOptionPress={handleOptionPress}
  1918. messagePosition={messagePosition}
  1919. />
  1920. <EmojiSelectorModal
  1921. visible={emojiSelectorVisible}
  1922. selectedMessage={selectedMessage}
  1923. addReaction={addReaction}
  1924. closeEmojiSelector={closeEmojiSelector}
  1925. />
  1926. </TouchableOpacity>
  1927. </BlurView>
  1928. </ReactModal>
  1929. <WarningModal
  1930. isVisible={modalInfo.visible}
  1931. onClose={closeModal}
  1932. type={modalInfo.type}
  1933. message={modalInfo.message}
  1934. buttonTitle={modalInfo.buttonTitle}
  1935. title={modalInfo.title}
  1936. action={() => {
  1937. modalInfo.action();
  1938. closeModal();
  1939. }}
  1940. />
  1941. <AttachmentsModal />
  1942. <ReactionsListModal />
  1943. </GestureHandlerRootView>
  1944. <View
  1945. style={{
  1946. height: insets.bottom,
  1947. backgroundColor: Colors.FILL_LIGHT
  1948. }}
  1949. />
  1950. </SafeAreaView>
  1951. );
  1952. };
  1953. export default ChatScreen;