index.tsx 56 KB

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