index.tsx 53 KB

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