index.tsx 75 KB

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