index.tsx 75 KB

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