From d0c7a7fe1f9903bdabcd16efc99f3034b160531b Mon Sep 17 00:00:00 2001 From: Melvin Date: Tue, 29 Apr 2025 14:31:51 +0200 Subject: [PATCH 01/15] feat: rewrite caching system, use onScroll instead of onEndReached --- src/CuteChat.tsx | 185 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 143 insertions(+), 42 deletions(-) diff --git a/src/CuteChat.tsx b/src/CuteChat.tsx index 8203bbc..5b82dce 100644 --- a/src/CuteChat.tsx +++ b/src/CuteChat.tsx @@ -2,8 +2,14 @@ import firestore, { FirebaseFirestoreTypes as FirebaseFirestore, firebase, } from '@react-native-firebase/firestore'; -import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react'; -import { Alert } from 'react-native'; +import React, { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from 'react'; +import { Alert, NativeScrollEvent } from 'react-native'; import type { IMessage } from 'react-native-gifted-chat'; import { GiftedChat, GiftedChatProps } from 'react-native-gifted-chat'; @@ -24,6 +30,13 @@ interface User { type CuteChatProps = Omit & CustomCuteChatProps; +type SnapshotChange = { + type: FirebaseFirestore.DocumentChangeType; + message: IMessage; +}; + +const messageBatch = 20; + export function CuteChat(props: CuteChatProps) { const [messages, setMessages] = useState([]); const [lastMessageDoc, setLastMessageDoc] = @@ -31,6 +44,7 @@ export function CuteChat(props: CuteChatProps) { const { chatId, user } = props; const memoizedUser = useMemo(() => ({ _id: user.id, ...user }), [user]); const [initializing, setInitializing] = useState(true); + const startDate = useMemo(() => new Date(), []); const setIsLoading = useMemo( () => props.setIsLoading || (() => {}), @@ -104,6 +118,62 @@ export function CuteChat(props: CuteChatProps) { [chatId] ); + const prepareSnapshot = async ( + snapshot: FirebaseFirestore.QuerySnapshot + ): Promise => { + console.log('Preparing snapshot'); + return Promise.all( + snapshot.docChanges().map(async (change) => ({ + type: change.type, + message: await docToMessage(change.doc), + })) + ); + }; + + const appendSnapshot = ( + currentMessages: IMessage[], + snapshotChanges: SnapshotChange[] + ): IMessage[] => { + console.log('Appending snapshot...'); + const newMessages = [...currentMessages]; + + for (const change of snapshotChanges) { + switch (change.type) { + case 'removed': + console.log('Message remove id:', change.message._id); + const deleteIndex = newMessages.findIndex( + (m) => change.message._id == m._id + ); + if (deleteIndex == -1) { + console.log('Message does not exist in currentMessage'); + break; + } + newMessages.splice(deleteIndex, 1); + console.log('Message removed'); + break; + case 'added': + case 'modified': + console.log('Message modified or added id:', change.message._id); + const modifiedIndex = newMessages.findIndex( + (m) => change.message._id == m._id + ); + if (modifiedIndex == -1) { + console.log('Message added'); + newMessages.push(change.message); + } else { + console.log('Message updated'); + newMessages[modifiedIndex] = change.message; + } + break; + } + } + + return newMessages.sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); + }; + const markMessagesAsRead = useCallback( async (newMessages: IMessage[]) => { const unreadMessages = newMessages.filter( @@ -152,9 +222,10 @@ export function CuteChat(props: CuteChatProps) { setIsLoading(true); const messagesRef = firestore().collection(`chats/${chatId}/messages`); - const unsubscribe = messagesRef + const unsubscribeOldMessages = messagesRef .orderBy('createdAt', 'desc') - .limit(20) + .startAfter(startDate.toISOString()) + .limit(messageBatch) .onSnapshot( async (snapshot: FirebaseFirestore.QuerySnapshot) => { if (snapshot.empty) { @@ -168,27 +239,35 @@ export function CuteChat(props: CuteChatProps) { } if (!snapshot.empty) { - setLastMessageDoc( - snapshot.docs[ - snapshot.docs.length - 1 - ] as FirebaseFirestore.QueryDocumentSnapshot - ); - - const newMessagesPromises = snapshot.docs.map(docToMessage); - const newMessages = await Promise.all(newMessagesPromises); + const snapshotChanges = await prepareSnapshot(snapshot); + setMessages((old) => appendSnapshot(old, snapshotChanges)); - setMessages(newMessages); setIsLoading(false); setInitializing(false); + } + }, + (error: Error) => console.error('Error fetching documents: ', error) + ); - markMessagesAsRead(newMessages); + const unsubscribeNewMessages = messagesRef + .orderBy('createdAt', 'asc') + .startAfter(startDate.toISOString()) + .onSnapshot( + async (snapshot: FirebaseFirestore.QuerySnapshot) => { + if (!snapshot.empty) { + console.log('New messages'); + const snapshotChanges = await prepareSnapshot(snapshot); + setMessages((old) => appendSnapshot(old, snapshotChanges)); } }, (error: Error) => console.error('Error fetching documents: ', error) ); - // Clean up function - return () => unsubscribe(); - }, [chatId, docToMessage, markMessagesAsRead, setIsLoading]); + + return () => { + unsubscribeOldMessages(); + unsubscribeNewMessages(); + }; + }, [chatId, docToMessage, markMessagesAsRead, setIsLoading, startDate]); // Handle outgoing messages const onSend = async (newMessages: IMessage[] = []) => { @@ -257,36 +336,19 @@ export function CuteChat(props: CuteChatProps) { setIsLoading(true); try { + console.log('Fetching more messages...'); const messagesRef = firestore().collection(`chats/${chatId}/messages`); messagesRef .orderBy('createdAt', 'desc') .startAfter(lastMessageDoc) - .limit(20) + .limit(messageBatch) .onSnapshot(async (snapshot) => { + console.log('Messages received'); if (!snapshot.empty) { - setLastMessageDoc( - snapshot.docs[ - snapshot.docs.length - 1 - ] as FirebaseFirestore.QueryDocumentSnapshot - ); - - const newMessages = await Promise.all( - snapshot.docs.map(docToMessage) - ); - - setMessages((previousMessages) => { - const newMessagesFiltered = newMessages.filter( - (newMessage) => - !previousMessages.some( - (previousMessage) => previousMessage._id === newMessage._id - ) - ); - return GiftedChat.prepend(previousMessages, newMessagesFiltered); - }); - - markMessagesAsRead(newMessages); + const snapshotChanges = await prepareSnapshot(snapshot); + setMessages((old) => appendSnapshot(old, snapshotChanges)); setIsLoading(false); - } + } else console.log('Snapshot empty'); }); } catch (error) { console.error('Error fetching more messages: ', error); @@ -300,6 +362,30 @@ export function CuteChat(props: CuteChatProps) { initializing, ]); + useEffect(() => { + if (!messages.length) { + setLastMessageDoc(null); + return; + } + + try { + const lastMessage = messages[messages.length - 1]; + console.log('Las message: ', lastMessage); + const lastMessageRef = firestore().doc( + `chats/${chatId}/messages/${lastMessage._id}` + ); + const unsubscribe = lastMessageRef.onSnapshot(async (snapshot) => { + setLastMessageDoc(snapshot); + }); + + return () => unsubscribe(); + } catch (error) { + console.error('Failed to set lastMessageDoc:', error); + } + }, [messages]); + + console.log('Amount of msgs:', messages.length); + return ( { + if (isCloseToBottom(nativeEvent)) fetchMoreMessages(); + }, + scrollEventThrottle: 500, }} /> ); } + +function isCloseToBottom({ + layoutMeasurement, + contentOffset, + contentSize, +}: NativeScrollEvent) { + const paddingToBottom = 500; + + return ( + layoutMeasurement.height + contentOffset.y >= + contentSize.height - paddingToBottom + ); +} From 93ea0885eab5caff748c99848ff0785f8b8f689c Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 09:34:15 +0200 Subject: [PATCH 02/15] fix: lint warnings --- src/CuteChat.tsx | 65 +++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/src/CuteChat.tsx b/src/CuteChat.tsx index 5b82dce..fde5e75 100644 --- a/src/CuteChat.tsx +++ b/src/CuteChat.tsx @@ -118,17 +118,20 @@ export function CuteChat(props: CuteChatProps) { [chatId] ); - const prepareSnapshot = async ( - snapshot: FirebaseFirestore.QuerySnapshot - ): Promise => { - console.log('Preparing snapshot'); - return Promise.all( - snapshot.docChanges().map(async (change) => ({ - type: change.type, - message: await docToMessage(change.doc), - })) - ); - }; + const prepareSnapshot = useCallback( + async ( + snapshot: FirebaseFirestore.QuerySnapshot + ): Promise => { + console.log('Preparing snapshot'); + return Promise.all( + snapshot.docChanges().map(async (change) => ({ + type: change.type, + message: await docToMessage(change.doc), + })) + ); + }, + [docToMessage] + ); const appendSnapshot = ( currentMessages: IMessage[], @@ -142,9 +145,9 @@ export function CuteChat(props: CuteChatProps) { case 'removed': console.log('Message remove id:', change.message._id); const deleteIndex = newMessages.findIndex( - (m) => change.message._id == m._id + (m) => change.message._id === m._id ); - if (deleteIndex == -1) { + if (deleteIndex === -1) { console.log('Message does not exist in currentMessage'); break; } @@ -155,9 +158,9 @@ export function CuteChat(props: CuteChatProps) { case 'modified': console.log('Message modified or added id:', change.message._id); const modifiedIndex = newMessages.findIndex( - (m) => change.message._id == m._id + (m) => change.message._id === m._id ); - if (modifiedIndex == -1) { + if (modifiedIndex === -1) { console.log('Message added'); newMessages.push(change.message); } else { @@ -267,7 +270,14 @@ export function CuteChat(props: CuteChatProps) { unsubscribeOldMessages(); unsubscribeNewMessages(); }; - }, [chatId, docToMessage, markMessagesAsRead, setIsLoading, startDate]); + }, [ + chatId, + docToMessage, + markMessagesAsRead, + setIsLoading, + startDate, + prepareSnapshot, + ]); // Handle outgoing messages const onSend = async (newMessages: IMessage[] = []) => { @@ -329,6 +339,7 @@ export function CuteChat(props: CuteChatProps) { } }; + // Function to fetch more messages const fetchMoreMessages = useCallback(async () => { if (initializing) { return; @@ -353,15 +364,9 @@ export function CuteChat(props: CuteChatProps) { } catch (error) { console.error('Error fetching more messages: ', error); } - }, [ - chatId, - lastMessageDoc, - docToMessage, - markMessagesAsRead, - setIsLoading, - initializing, - ]); + }, [chatId, lastMessageDoc, setIsLoading, initializing, prepareSnapshot]); + // Keep `lastMessageDoc` up to date based on `messages` useEffect(() => { if (!messages.length) { setLastMessageDoc(null); @@ -370,10 +375,17 @@ export function CuteChat(props: CuteChatProps) { try { const lastMessage = messages[messages.length - 1]; - console.log('Las message: ', lastMessage); + + if (!lastMessage) { + console.log('No last message. Skipping setting last message.'); + return; + } + + console.log('Last message: ', lastMessage); const lastMessageRef = firestore().doc( `chats/${chatId}/messages/${lastMessage._id}` ); + const unsubscribe = lastMessageRef.onSnapshot(async (snapshot) => { setLastMessageDoc(snapshot); }); @@ -381,8 +393,9 @@ export function CuteChat(props: CuteChatProps) { return () => unsubscribe(); } catch (error) { console.error('Failed to set lastMessageDoc:', error); + return; } - }, [messages]); + }, [messages, chatId]); console.log('Amount of msgs:', messages.length); From 48998b2d6d928da98d02eb08fb33b71673c795ab Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 09:36:17 +0200 Subject: [PATCH 03/15] fix: add clarifying comment to initial useEffect --- src/CuteChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CuteChat.tsx b/src/CuteChat.tsx index fde5e75..dce0080 100644 --- a/src/CuteChat.tsx +++ b/src/CuteChat.tsx @@ -220,7 +220,7 @@ export function CuteChat(props: CuteChatProps) { [chatId, memoizedUser._id] ); - // Fetch initial messages + // Fetch initial messages and subscribe to potential future messages useLayoutEffect(() => { setIsLoading(true); const messagesRef = firestore().collection(`chats/${chatId}/messages`); From dd6f193723624728cf4900d7228029e7e9cb4c76 Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 10:32:10 +0200 Subject: [PATCH 04/15] feat: do not fetch more if still loading previous messages --- src/CuteChat.tsx | 48 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/CuteChat.tsx b/src/CuteChat.tsx index dce0080..6a723bd 100644 --- a/src/CuteChat.tsx +++ b/src/CuteChat.tsx @@ -41,14 +41,18 @@ export function CuteChat(props: CuteChatProps) { const [messages, setMessages] = useState([]); const [lastMessageDoc, setLastMessageDoc] = useState(null); - const { chatId, user } = props; + const { chatId, user, setIsLoading } = props; const memoizedUser = useMemo(() => ({ _id: user.id, ...user }), [user]); const [initializing, setInitializing] = useState(true); const startDate = useMemo(() => new Date(), []); + const [loading, setLoading] = useState(false); - const setIsLoading = useMemo( - () => props.setIsLoading || (() => {}), - [props.setIsLoading] + const setIsLoadingBool = useCallback( + (isLoading: boolean) => { + setIsLoading?.(isLoading); + setLoading(isLoading); + }, + [setIsLoading] ); // Utility function to convert a Firestore document to a Gifted Chat message @@ -222,7 +226,7 @@ export function CuteChat(props: CuteChatProps) { // Fetch initial messages and subscribe to potential future messages useLayoutEffect(() => { - setIsLoading(true); + setIsLoadingBool(true); const messagesRef = firestore().collection(`chats/${chatId}/messages`); const unsubscribeOldMessages = messagesRef @@ -235,7 +239,7 @@ export function CuteChat(props: CuteChatProps) { setLastMessageDoc(null); setMessages([]); - setIsLoading(false); + setIsLoadingBool(false); setInitializing(false); markMessagesAsRead([]); @@ -245,7 +249,7 @@ export function CuteChat(props: CuteChatProps) { const snapshotChanges = await prepareSnapshot(snapshot); setMessages((old) => appendSnapshot(old, snapshotChanges)); - setIsLoading(false); + setIsLoadingBool(false); setInitializing(false); } }, @@ -274,7 +278,7 @@ export function CuteChat(props: CuteChatProps) { chatId, docToMessage, markMessagesAsRead, - setIsLoading, + setIsLoadingBool, startDate, prepareSnapshot, ]); @@ -342,10 +346,18 @@ export function CuteChat(props: CuteChatProps) { // Function to fetch more messages const fetchMoreMessages = useCallback(async () => { if (initializing) { - return; + return console.log( + 'Skipping fetching more messages since initializing is still true' + ); } - setIsLoading(true); + if (loading) { + return console.log( + 'Skipping fetching more messages since loading is already true' + ); + } + + setIsLoadingBool(true); try { console.log('Fetching more messages...'); const messagesRef = firestore().collection(`chats/${chatId}/messages`); @@ -358,13 +370,23 @@ export function CuteChat(props: CuteChatProps) { if (!snapshot.empty) { const snapshotChanges = await prepareSnapshot(snapshot); setMessages((old) => appendSnapshot(old, snapshotChanges)); - setIsLoading(false); - } else console.log('Snapshot empty'); + } else { + console.log('Snapshot empty'); + } + + setIsLoadingBool(false); }); } catch (error) { console.error('Error fetching more messages: ', error); } - }, [chatId, lastMessageDoc, setIsLoading, initializing, prepareSnapshot]); + }, [ + chatId, + lastMessageDoc, + setIsLoadingBool, + initializing, + prepareSnapshot, + loading, + ]); // Keep `lastMessageDoc` up to date based on `messages` useEffect(() => { From 31dbddf20e1c80847f398df8217ab19c2c1995b6 Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 10:42:59 +0200 Subject: [PATCH 05/15] refactor: extract docToMessage to standalone function --- src/CuteChat.tsx | 139 +++++++++++++++++++++++------------------------ 1 file changed, 69 insertions(+), 70 deletions(-) diff --git a/src/CuteChat.tsx b/src/CuteChat.tsx index 6a723bd..4ee717d 100644 --- a/src/CuteChat.tsx +++ b/src/CuteChat.tsx @@ -55,73 +55,6 @@ export function CuteChat(props: CuteChatProps) { [setIsLoading] ); - // Utility function to convert a Firestore document to a Gifted Chat message - const docToMessage = useCallback( - async (doc: FirebaseFirestore.QueryDocumentSnapshot): Promise => { - const data = doc.data(); - - if (!data) { - throw new Error('Document data is undefined'); - } - - const [files, sender] = await Promise.all([ - new Promise< - | FirebaseFirestore.QueryDocumentSnapshot[] - | undefined - >((resolve, reject) => { - firestore() - .collection(`chats/${chatId}/messages/${doc.id}/files`) - .onSnapshot((snapshot) => { - if (snapshot.empty) { - resolve(undefined); - } else { - resolve(snapshot.docs); - } - }, reject); - }), - new Promise( - (resolve, reject) => { - firestore() - .doc(data.senderRef._documentPath._parts.join('/')) - .onSnapshot((snapshot) => { - if (!snapshot.exists) { - resolve(undefined); - } else { - resolve(snapshot.data()); - } - }, reject); - } - ), - ]); - - const image = files?.[0]?.data().url; - - // Fetch user data from reference - if (sender) { - return { - _id: doc.id, - createdAt: new Date(data.createdAt), - text: data.content, - user: { _id: data.senderId, ...sender }, - image: image, - readByIds: data.readByIds, - metadata: data.metadata, - }; - } else { - return { - _id: doc.id, - createdAt: new Date(data.createdAt), - text: data.content, - image: image, - system: true, - readByIds: data.readByIds, - metadata: data.metadata, - }; - } - }, - [chatId] - ); - const prepareSnapshot = useCallback( async ( snapshot: FirebaseFirestore.QuerySnapshot @@ -130,11 +63,11 @@ export function CuteChat(props: CuteChatProps) { return Promise.all( snapshot.docChanges().map(async (change) => ({ type: change.type, - message: await docToMessage(change.doc), + message: await docToMessage(change.doc, chatId), })) ); }, - [docToMessage] + [chatId] ); const appendSnapshot = ( @@ -276,7 +209,6 @@ export function CuteChat(props: CuteChatProps) { }; }, [ chatId, - docToMessage, markMessagesAsRead, setIsLoadingBool, startDate, @@ -450,3 +382,70 @@ function isCloseToBottom({ contentSize.height - paddingToBottom ); } + +// Utility function to convert a Firestore document to a Gifted Chat message +export const docToMessage = async ( + doc: FirebaseFirestore.QueryDocumentSnapshot, + chatId: string +): Promise => { + const data = doc.data(); + + if (!data) { + throw new Error('Document data is undefined'); + } + + const [files, sender] = await Promise.all([ + new Promise< + | FirebaseFirestore.QueryDocumentSnapshot[] + | undefined + >((resolve, reject) => { + firestore() + .collection(`chats/${chatId}/messages/${doc.id}/files`) + .onSnapshot((snapshot) => { + if (snapshot.empty) { + resolve(undefined); + } else { + resolve(snapshot.docs); + } + }, reject); + }), + new Promise( + (resolve, reject) => { + firestore() + .doc(data.senderRef._documentPath._parts.join('/')) + .onSnapshot((snapshot) => { + if (!snapshot.exists) { + resolve(undefined); + } else { + resolve(snapshot.data()); + } + }, reject); + } + ), + ]); + + const image = files?.[0]?.data().url; + + // Fetch user data from reference + if (sender) { + return { + _id: doc.id, + createdAt: new Date(data.createdAt), + text: data.content, + user: { _id: data.senderId, ...sender }, + image: image, + readByIds: data.readByIds, + metadata: data.metadata, + }; + } else { + return { + _id: doc.id, + createdAt: new Date(data.createdAt), + text: data.content, + image: image, + system: true, + readByIds: data.readByIds, + metadata: data.metadata, + }; + } +}; From d2939d3a6cf0691cfbeb2756e459363c3614aece Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 10:45:48 +0200 Subject: [PATCH 06/15] refactor: extract prepareSnapShot to standalone function --- src/CuteChat.tsx | 52 ++++++++++++++++++------------------------------ 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/src/CuteChat.tsx b/src/CuteChat.tsx index 4ee717d..0f60bd6 100644 --- a/src/CuteChat.tsx +++ b/src/CuteChat.tsx @@ -55,21 +55,6 @@ export function CuteChat(props: CuteChatProps) { [setIsLoading] ); - const prepareSnapshot = useCallback( - async ( - snapshot: FirebaseFirestore.QuerySnapshot - ): Promise => { - console.log('Preparing snapshot'); - return Promise.all( - snapshot.docChanges().map(async (change) => ({ - type: change.type, - message: await docToMessage(change.doc, chatId), - })) - ); - }, - [chatId] - ); - const appendSnapshot = ( currentMessages: IMessage[], snapshotChanges: SnapshotChange[] @@ -179,7 +164,7 @@ export function CuteChat(props: CuteChatProps) { } if (!snapshot.empty) { - const snapshotChanges = await prepareSnapshot(snapshot); + const snapshotChanges = await prepareSnapshot(snapshot, chatId); setMessages((old) => appendSnapshot(old, snapshotChanges)); setIsLoadingBool(false); @@ -196,7 +181,7 @@ export function CuteChat(props: CuteChatProps) { async (snapshot: FirebaseFirestore.QuerySnapshot) => { if (!snapshot.empty) { console.log('New messages'); - const snapshotChanges = await prepareSnapshot(snapshot); + const snapshotChanges = await prepareSnapshot(snapshot, chatId); setMessages((old) => appendSnapshot(old, snapshotChanges)); } }, @@ -207,13 +192,7 @@ export function CuteChat(props: CuteChatProps) { unsubscribeOldMessages(); unsubscribeNewMessages(); }; - }, [ - chatId, - markMessagesAsRead, - setIsLoadingBool, - startDate, - prepareSnapshot, - ]); + }, [chatId, markMessagesAsRead, setIsLoadingBool, startDate]); // Handle outgoing messages const onSend = async (newMessages: IMessage[] = []) => { @@ -300,7 +279,7 @@ export function CuteChat(props: CuteChatProps) { .onSnapshot(async (snapshot) => { console.log('Messages received'); if (!snapshot.empty) { - const snapshotChanges = await prepareSnapshot(snapshot); + const snapshotChanges = await prepareSnapshot(snapshot, chatId); setMessages((old) => appendSnapshot(old, snapshotChanges)); } else { console.log('Snapshot empty'); @@ -311,14 +290,7 @@ export function CuteChat(props: CuteChatProps) { } catch (error) { console.error('Error fetching more messages: ', error); } - }, [ - chatId, - lastMessageDoc, - setIsLoadingBool, - initializing, - prepareSnapshot, - loading, - ]); + }, [chatId, lastMessageDoc, setIsLoadingBool, initializing, loading]); // Keep `lastMessageDoc` up to date based on `messages` useEffect(() => { @@ -449,3 +421,17 @@ export const docToMessage = async ( }; } }; + +export const prepareSnapshot = async ( + snapshot: FirebaseFirestore.QuerySnapshot, + chatId: string +): Promise => { + console.log('Preparing snapshot'); + + return Promise.all( + snapshot.docChanges().map(async (change) => ({ + type: change.type, + message: await docToMessage(change.doc, chatId), + })) + ); +}; From 5bcd03ab9e4ac4f06d5ff95457241d056dea5e13 Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 10:55:03 +0200 Subject: [PATCH 07/15] refactor: extract appendSnapshot to standalone function --- src/CuteChat.tsx | 87 ++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/src/CuteChat.tsx b/src/CuteChat.tsx index 0f60bd6..b06544b 100644 --- a/src/CuteChat.tsx +++ b/src/CuteChat.tsx @@ -55,50 +55,6 @@ export function CuteChat(props: CuteChatProps) { [setIsLoading] ); - const appendSnapshot = ( - currentMessages: IMessage[], - snapshotChanges: SnapshotChange[] - ): IMessage[] => { - console.log('Appending snapshot...'); - const newMessages = [...currentMessages]; - - for (const change of snapshotChanges) { - switch (change.type) { - case 'removed': - console.log('Message remove id:', change.message._id); - const deleteIndex = newMessages.findIndex( - (m) => change.message._id === m._id - ); - if (deleteIndex === -1) { - console.log('Message does not exist in currentMessage'); - break; - } - newMessages.splice(deleteIndex, 1); - console.log('Message removed'); - break; - case 'added': - case 'modified': - console.log('Message modified or added id:', change.message._id); - const modifiedIndex = newMessages.findIndex( - (m) => change.message._id === m._id - ); - if (modifiedIndex === -1) { - console.log('Message added'); - newMessages.push(change.message); - } else { - console.log('Message updated'); - newMessages[modifiedIndex] = change.message; - } - break; - } - } - - return newMessages.sort( - (a, b) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ); - }; - const markMessagesAsRead = useCallback( async (newMessages: IMessage[]) => { const unreadMessages = newMessages.filter( @@ -435,3 +391,46 @@ export const prepareSnapshot = async ( })) ); }; + +export const appendSnapshot = ( + currentMessages: IMessage[], + snapshotChanges: SnapshotChange[] +): IMessage[] => { + console.log('Appending snapshot...'); + const newMessages = [...currentMessages]; + + for (const change of snapshotChanges) { + switch (change.type) { + case 'removed': + console.log('Message remove id:', change.message._id); + const deleteIndex = newMessages.findIndex( + (m) => change.message._id === m._id + ); + if (deleteIndex === -1) { + console.log('Message does not exist in currentMessage'); + break; + } + newMessages.splice(deleteIndex, 1); + console.log('Message removed'); + break; + case 'added': + case 'modified': + console.log('Message modified or added id:', change.message._id); + const modifiedIndex = newMessages.findIndex( + (m) => change.message._id === m._id + ); + if (modifiedIndex === -1) { + console.log('Message added'); + newMessages.push(change.message); + } else { + console.log('Message updated'); + newMessages[modifiedIndex] = change.message; + } + break; + } + } + + return newMessages.sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); +}; From c391d5ab8e955314920b16ec30dcd3d8db36a063 Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 11:12:54 +0200 Subject: [PATCH 08/15] refactor: move appendSnapshot to own util function --- example/package.json | 3 +- example/yarn.lock | 90 ++++++++++++++++++++++++++++++++++++- src/CuteChat.tsx | 46 +------------------ src/utils/appendSnapshot.ts | 45 +++++++++++++++++++ 4 files changed, 137 insertions(+), 47 deletions(-) create mode 100644 src/utils/appendSnapshot.ts diff --git a/example/package.json b/example/package.json index 1f18246..a8d02de 100644 --- a/example/package.json +++ b/example/package.json @@ -18,7 +18,8 @@ "react-native": "0.71.10", "react-native-image-picker": "^5.4.2", "react-native-svg": "^13.9.0", - "react-native-svg-transformer": "^1.0.0" + "react-native-svg-transformer": "^1.0.0", + "@qteab/react-native-firebase-chat": "file:../" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/example/yarn.lock b/example/yarn.lock index f235369..400d9d2 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1188,6 +1188,14 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.42.0.tgz#484a1d638de2911e6f5a30c12f49c7e4a3270fb6" integrity sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw== +"@expo/react-native-action-sheet@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@expo/react-native-action-sheet/-/react-native-action-sheet-4.1.1.tgz#a94a11088b8f146ac86b3ae710deef609a922d82" + integrity sha512-4KRaba2vhqDRR7ObBj6nrD5uJw8ePoNHdIOMETTpgGTX7StUbrF4j/sfrP1YUyaPEa1P8FXdwG6pB+2WtrJd1A== + dependencies: + "@types/hoist-non-react-statics" "^3.3.1" + hoist-non-react-statics "^3.3.0" + "@hapi/hoek@^9.0.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -1529,6 +1537,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@qteab/react-native-firebase-chat@file:..": + version "0.5.4" + dependencies: + react-native-gifted-chat "^2.1.0" + "@react-native-community/cli-clean@^10.1.1": version "10.1.1" resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-10.1.1.tgz#4c73ce93a63a24d70c0089d4025daac8184ff504" @@ -1943,6 +1956,14 @@ dependencies: "@types/node" "*" +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.6" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz#6bba74383cdab98e8db4e20ce5b4a6b98caed010" + integrity sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -1975,6 +1996,18 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== +"@types/lodash.isequal@^4.5.8": + version "4.5.8" + resolved "https://registry.yarnpkg.com/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz#b30bb6ff6a5f6c19b3daf389d649ac7f7a250499" + integrity sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.17.16" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.16.tgz#94ae78fab4a38d73086e962d0b65c30d816bfb0a" + integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g== + "@types/node@*": version "20.3.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.1.tgz#e8a83f1aa8b649377bb1fb5d7bac5cb90e784dfe" @@ -2983,6 +3016,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +dayjs@^1.11.13: + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + dayjs@^1.8.15: version "1.11.8" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.8.tgz#4282f139c8c19dd6d0c7bd571e30c2d0ba7698ea" @@ -4066,6 +4104,13 @@ hermes-profile-transformer@^0.0.6: dependencies: source-map "^0.7.3" +hoist-non-react-statics@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -5087,6 +5132,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -6169,7 +6219,7 @@ prompts@^2.0.1, prompts@^2.4.0: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@*, prop-types@^15.8.1: +prop-types@*, prop-types@^15.7.x, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -6219,7 +6269,7 @@ react-devtools-core@^4.26.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react-is@^16.13.1: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -6239,6 +6289,25 @@ react-native-codegen@^0.71.5: jscodeshift "^0.13.1" nullthrows "^1.1.1" +react-native-communications@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-native-communications/-/react-native-communications-2.2.1.tgz#7883b56b20a002eeb790c113f8616ea8692ca795" + integrity sha512-5+C0X9mopI0+qxyQHzOPEi5v5rxNBQjxydPPiKMQSlX1RBIcJ8uTcqUPssQ9Mo8p6c1IKIWJUSqCj4jAmD0qVQ== + +react-native-gifted-chat@^2.1.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/react-native-gifted-chat/-/react-native-gifted-chat-2.8.1.tgz#92380aceb56024103de7c0003e3b43572c5a5641" + integrity sha512-x4Kq0YvmaHqQg/ENAmFzwcjJyH31cGrCWETFzUMmTZgWsXkkiJ1MamTnkLGQp6deVxM6G0QNxrF7IZnBOeMbsw== + dependencies: + "@expo/react-native-action-sheet" "^4.1.1" + "@types/lodash.isequal" "^4.5.8" + dayjs "^1.11.13" + lodash.isequal "^4.5.0" + react-native-communications "^2.2.1" + react-native-iphone-x-helper "^1.3.1" + react-native-lightbox-v2 "^0.9.2" + react-native-parsed-text "^0.0.22" + react-native-gradle-plugin@^0.71.19: version "0.71.19" resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.19.tgz#3379e28341fcd189bc1f4691cefc84c1a4d7d232" @@ -6249,6 +6318,23 @@ react-native-image-picker@^5.4.2: resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-5.4.2.tgz#017d6b379cddf706acd7feb8222c2eb58e57eb8d" integrity sha512-C/k3cYAh8fBImoGEwmiChNwHx9fJGqAIu2E4BUJdI1XlL17tSYjfTDx/bsuF4amZwa7hxZdQnZmpk0EnwIEUaw== +react-native-iphone-x-helper@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" + integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== + +react-native-lightbox-v2@^0.9.2: + version "0.9.2" + resolved "https://registry.yarnpkg.com/react-native-lightbox-v2/-/react-native-lightbox-v2-0.9.2.tgz#58e256ee18bdd2e270f228a911a76831251229e5" + integrity sha512-+8LwINeSWvPP69YAyWhiJQyaw4Gtu8X4EMvT3PEN615NrOeDaz8jOcIw73pzYJst3z4Z0vxvFB73iH16wCPXtw== + +react-native-parsed-text@^0.0.22: + version "0.0.22" + resolved "https://registry.yarnpkg.com/react-native-parsed-text/-/react-native-parsed-text-0.0.22.tgz#a23c756eaa5d6724296814755085127f9072e5f5" + integrity sha512-hfD83RDXZf9Fvth3DowR7j65fMnlqM9PpxZBGWkzVcUTFtqe6/yPcIoIAgrJbKn6YmtzkivmhWE2MCE4JKBXrQ== + dependencies: + prop-types "^15.7.x" + react-native-svg-transformer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/react-native-svg-transformer/-/react-native-svg-transformer-1.0.0.tgz#7a707e5e95d20321b5f3dcfd0c3c8762ebd0221b" diff --git a/src/CuteChat.tsx b/src/CuteChat.tsx index b06544b..1ce5273 100644 --- a/src/CuteChat.tsx +++ b/src/CuteChat.tsx @@ -12,6 +12,7 @@ import React, { import { Alert, NativeScrollEvent } from 'react-native'; import type { IMessage } from 'react-native-gifted-chat'; import { GiftedChat, GiftedChatProps } from 'react-native-gifted-chat'; +import { appendSnapshot } from './utils/appendSnapshot'; interface CustomCuteChatProps { chatId: string; @@ -30,7 +31,7 @@ interface User { type CuteChatProps = Omit & CustomCuteChatProps; -type SnapshotChange = { +export type SnapshotChange = { type: FirebaseFirestore.DocumentChangeType; message: IMessage; }; @@ -391,46 +392,3 @@ export const prepareSnapshot = async ( })) ); }; - -export const appendSnapshot = ( - currentMessages: IMessage[], - snapshotChanges: SnapshotChange[] -): IMessage[] => { - console.log('Appending snapshot...'); - const newMessages = [...currentMessages]; - - for (const change of snapshotChanges) { - switch (change.type) { - case 'removed': - console.log('Message remove id:', change.message._id); - const deleteIndex = newMessages.findIndex( - (m) => change.message._id === m._id - ); - if (deleteIndex === -1) { - console.log('Message does not exist in currentMessage'); - break; - } - newMessages.splice(deleteIndex, 1); - console.log('Message removed'); - break; - case 'added': - case 'modified': - console.log('Message modified or added id:', change.message._id); - const modifiedIndex = newMessages.findIndex( - (m) => change.message._id === m._id - ); - if (modifiedIndex === -1) { - console.log('Message added'); - newMessages.push(change.message); - } else { - console.log('Message updated'); - newMessages[modifiedIndex] = change.message; - } - break; - } - } - - return newMessages.sort( - (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ); -}; diff --git a/src/utils/appendSnapshot.ts b/src/utils/appendSnapshot.ts new file mode 100644 index 0000000..3a86bd9 --- /dev/null +++ b/src/utils/appendSnapshot.ts @@ -0,0 +1,45 @@ +import { IMessage } from 'react-native-gifted-chat'; +import { SnapshotChange } from 'src/CuteChat'; + +export const appendSnapshot = ( + currentMessages: IMessage[], + snapshotChanges: SnapshotChange[] +): IMessage[] => { + console.log('Appending snapshot...'); + const newMessages = [...currentMessages]; + + for (const change of snapshotChanges) { + switch (change.type) { + case 'removed': + console.log('Message remove id:', change.message._id); + const deleteIndex = newMessages.findIndex( + (m) => change.message._id === m._id + ); + if (deleteIndex === -1) { + console.log('Message does not exist in currentMessage'); + break; + } + newMessages.splice(deleteIndex, 1); + console.log('Message removed'); + break; + case 'added': + case 'modified': + console.log('Message modified or added id:', change.message._id); + const modifiedIndex = newMessages.findIndex( + (m) => change.message._id === m._id + ); + if (modifiedIndex === -1) { + console.log('Message added'); + newMessages.push(change.message); + } else { + console.log('Message updated'); + newMessages[modifiedIndex] = change.message; + } + break; + } + } + + return newMessages.sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); +}; From 761fd56a20f02427a7270fca8d98414a8120b5c7 Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 11:17:55 +0200 Subject: [PATCH 09/15] refactor: extract utils functions to own files --- src/CuteChat.tsx | 82 +----------------------------------- src/utils/docToMessage.ts | 71 +++++++++++++++++++++++++++++++ src/utils/prepareSnapshot.ts | 17 ++++++++ 3 files changed, 89 insertions(+), 81 deletions(-) create mode 100644 src/utils/docToMessage.ts create mode 100644 src/utils/prepareSnapshot.ts diff --git a/src/CuteChat.tsx b/src/CuteChat.tsx index 1ce5273..9dafcec 100644 --- a/src/CuteChat.tsx +++ b/src/CuteChat.tsx @@ -13,6 +13,7 @@ import { Alert, NativeScrollEvent } from 'react-native'; import type { IMessage } from 'react-native-gifted-chat'; import { GiftedChat, GiftedChatProps } from 'react-native-gifted-chat'; import { appendSnapshot } from './utils/appendSnapshot'; +import { prepareSnapshot } from './utils/prepareSnapshot'; interface CustomCuteChatProps { chatId: string; @@ -311,84 +312,3 @@ function isCloseToBottom({ contentSize.height - paddingToBottom ); } - -// Utility function to convert a Firestore document to a Gifted Chat message -export const docToMessage = async ( - doc: FirebaseFirestore.QueryDocumentSnapshot, - chatId: string -): Promise => { - const data = doc.data(); - - if (!data) { - throw new Error('Document data is undefined'); - } - - const [files, sender] = await Promise.all([ - new Promise< - | FirebaseFirestore.QueryDocumentSnapshot[] - | undefined - >((resolve, reject) => { - firestore() - .collection(`chats/${chatId}/messages/${doc.id}/files`) - .onSnapshot((snapshot) => { - if (snapshot.empty) { - resolve(undefined); - } else { - resolve(snapshot.docs); - } - }, reject); - }), - new Promise( - (resolve, reject) => { - firestore() - .doc(data.senderRef._documentPath._parts.join('/')) - .onSnapshot((snapshot) => { - if (!snapshot.exists) { - resolve(undefined); - } else { - resolve(snapshot.data()); - } - }, reject); - } - ), - ]); - - const image = files?.[0]?.data().url; - - // Fetch user data from reference - if (sender) { - return { - _id: doc.id, - createdAt: new Date(data.createdAt), - text: data.content, - user: { _id: data.senderId, ...sender }, - image: image, - readByIds: data.readByIds, - metadata: data.metadata, - }; - } else { - return { - _id: doc.id, - createdAt: new Date(data.createdAt), - text: data.content, - image: image, - system: true, - readByIds: data.readByIds, - metadata: data.metadata, - }; - } -}; - -export const prepareSnapshot = async ( - snapshot: FirebaseFirestore.QuerySnapshot, - chatId: string -): Promise => { - console.log('Preparing snapshot'); - - return Promise.all( - snapshot.docChanges().map(async (change) => ({ - type: change.type, - message: await docToMessage(change.doc, chatId), - })) - ); -}; diff --git a/src/utils/docToMessage.ts b/src/utils/docToMessage.ts new file mode 100644 index 0000000..31fcb11 --- /dev/null +++ b/src/utils/docToMessage.ts @@ -0,0 +1,71 @@ +import firestore, { + FirebaseFirestoreTypes as FirebaseFirestore, +} from '@react-native-firebase/firestore'; +import type { IMessage } from 'react-native-gifted-chat'; + +// Utility function to convert a Firestore document to a Gifted Chat message +export const docToMessage = async ( + doc: FirebaseFirestore.QueryDocumentSnapshot, + chatId: string +): Promise => { + const data = doc.data(); + + if (!data) { + throw new Error('Document data is undefined'); + } + + const [files, sender] = await Promise.all([ + new Promise< + | FirebaseFirestore.QueryDocumentSnapshot[] + | undefined + >((resolve, reject) => { + firestore() + .collection(`chats/${chatId}/messages/${doc.id}/files`) + .onSnapshot((snapshot) => { + if (snapshot.empty) { + resolve(undefined); + } else { + resolve(snapshot.docs); + } + }, reject); + }), + new Promise( + (resolve, reject) => { + firestore() + .doc(data.senderRef._documentPath._parts.join('/')) + .onSnapshot((snapshot) => { + if (!snapshot.exists) { + resolve(undefined); + } else { + resolve(snapshot.data()); + } + }, reject); + } + ), + ]); + + const image = files?.[0]?.data().url; + + // Fetch user data from reference + if (sender) { + return { + _id: doc.id, + createdAt: new Date(data.createdAt), + text: data.content, + user: { _id: data.senderId, ...sender }, + image: image, + readByIds: data.readByIds, + metadata: data.metadata, + }; + } else { + return { + _id: doc.id, + createdAt: new Date(data.createdAt), + text: data.content, + image: image, + system: true, + readByIds: data.readByIds, + metadata: data.metadata, + }; + } +}; diff --git a/src/utils/prepareSnapshot.ts b/src/utils/prepareSnapshot.ts new file mode 100644 index 0000000..8b10ca0 --- /dev/null +++ b/src/utils/prepareSnapshot.ts @@ -0,0 +1,17 @@ +import { SnapshotChange } from 'src/CuteChat'; +import { FirebaseFirestoreTypes as FirebaseFirestore } from '@react-native-firebase/firestore'; +import { docToMessage } from './docToMessage'; + +export const prepareSnapshot = async ( + snapshot: FirebaseFirestore.QuerySnapshot, + chatId: string +): Promise => { + console.log('Preparing snapshot'); + + return Promise.all( + snapshot.docChanges().map(async (change) => ({ + type: change.type, + message: await docToMessage(change.doc, chatId), + })) + ); +}; From bbe0dc96527bbcb14cd3178fb99e44681df16421 Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 11:18:55 +0200 Subject: [PATCH 10/15] refactor: move SnapshotChange type to prepareSnapshot.ts --- src/CuteChat.tsx | 5 ----- src/utils/appendSnapshot.ts | 2 +- src/utils/prepareSnapshot.ts | 7 ++++++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CuteChat.tsx b/src/CuteChat.tsx index 9dafcec..1c53eea 100644 --- a/src/CuteChat.tsx +++ b/src/CuteChat.tsx @@ -32,11 +32,6 @@ interface User { type CuteChatProps = Omit & CustomCuteChatProps; -export type SnapshotChange = { - type: FirebaseFirestore.DocumentChangeType; - message: IMessage; -}; - const messageBatch = 20; export function CuteChat(props: CuteChatProps) { diff --git a/src/utils/appendSnapshot.ts b/src/utils/appendSnapshot.ts index 3a86bd9..0056996 100644 --- a/src/utils/appendSnapshot.ts +++ b/src/utils/appendSnapshot.ts @@ -1,5 +1,5 @@ import { IMessage } from 'react-native-gifted-chat'; -import { SnapshotChange } from 'src/CuteChat'; +import { SnapshotChange } from './prepareSnapshot'; export const appendSnapshot = ( currentMessages: IMessage[], diff --git a/src/utils/prepareSnapshot.ts b/src/utils/prepareSnapshot.ts index 8b10ca0..94e406d 100644 --- a/src/utils/prepareSnapshot.ts +++ b/src/utils/prepareSnapshot.ts @@ -1,6 +1,11 @@ -import { SnapshotChange } from 'src/CuteChat'; import { FirebaseFirestoreTypes as FirebaseFirestore } from '@react-native-firebase/firestore'; import { docToMessage } from './docToMessage'; +import { IMessage } from 'react-native-gifted-chat'; + +export type SnapshotChange = { + type: FirebaseFirestore.DocumentChangeType; + message: IMessage; +}; export const prepareSnapshot = async ( snapshot: FirebaseFirestore.QuerySnapshot, From a296390ff14fe350e83b55dfe15d18bbe639ae31 Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 11:26:29 +0200 Subject: [PATCH 11/15] docs: add better docs to util functions --- src/CuteChat.tsx | 8 +++++--- src/utils/appendSnapshot.ts | 7 +++++++ src/utils/docToMessage.ts | 4 +++- src/utils/prepareSnapshot.ts | 3 +++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/CuteChat.tsx b/src/CuteChat.tsx index 1c53eea..cee9d75 100644 --- a/src/CuteChat.tsx +++ b/src/CuteChat.tsx @@ -35,14 +35,16 @@ type CuteChatProps = Omit & const messageBatch = 20; export function CuteChat(props: CuteChatProps) { + const { chatId, user, setIsLoading } = props; + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(false); + const [initializing, setInitializing] = useState(true); const [lastMessageDoc, setLastMessageDoc] = useState(null); - const { chatId, user, setIsLoading } = props; + const memoizedUser = useMemo(() => ({ _id: user.id, ...user }), [user]); - const [initializing, setInitializing] = useState(true); const startDate = useMemo(() => new Date(), []); - const [loading, setLoading] = useState(false); const setIsLoadingBool = useCallback( (isLoading: boolean) => { diff --git a/src/utils/appendSnapshot.ts b/src/utils/appendSnapshot.ts index 0056996..328549c 100644 --- a/src/utils/appendSnapshot.ts +++ b/src/utils/appendSnapshot.ts @@ -1,6 +1,13 @@ import { IMessage } from 'react-native-gifted-chat'; import { SnapshotChange } from './prepareSnapshot'; +/** + * Returns transformed messages based on snapshot events. + * This can include removed, added or modified messages and are updated + * and returned accordingly via this function. + * + * This function **does not** mutate the original `currentMessages` array. + */ export const appendSnapshot = ( currentMessages: IMessage[], snapshotChanges: SnapshotChange[] diff --git a/src/utils/docToMessage.ts b/src/utils/docToMessage.ts index 31fcb11..9ccf01e 100644 --- a/src/utils/docToMessage.ts +++ b/src/utils/docToMessage.ts @@ -3,7 +3,9 @@ import firestore, { } from '@react-native-firebase/firestore'; import type { IMessage } from 'react-native-gifted-chat'; -// Utility function to convert a Firestore document to a Gifted Chat message +/** + * Utility function to convert a Firestore document to a `GiftedChat` message. + */ export const docToMessage = async ( doc: FirebaseFirestore.QueryDocumentSnapshot, chatId: string diff --git a/src/utils/prepareSnapshot.ts b/src/utils/prepareSnapshot.ts index 94e406d..53144d0 100644 --- a/src/utils/prepareSnapshot.ts +++ b/src/utils/prepareSnapshot.ts @@ -7,6 +7,9 @@ export type SnapshotChange = { message: IMessage; }; +/** + * Prepares snapshot changes for internal usage. + */ export const prepareSnapshot = async ( snapshot: FirebaseFirestore.QuerySnapshot, chatId: string From 27155cdf8b1e7d8b144f2410dbec094add99f399 Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 11:37:16 +0200 Subject: [PATCH 12/15] docs: add docs describing messages caching --- src/docs/message-caching.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/docs/message-caching.md diff --git a/src/docs/message-caching.md b/src/docs/message-caching.md new file mode 100644 index 0000000..e0044f6 --- /dev/null +++ b/src/docs/message-caching.md @@ -0,0 +1,9 @@ +### Message caching in CuteChat + +The subscriptions toward messages are setup the following way: + +- 1️⃣ One that fetches and subscribes to the latest 20 messages upon initialization, ignoring incoming messages, therefore staying static and only affected by deletion or modification of specified messages +- 2️⃣ One that fetches and subscribes to messages after a specified `messageDoc` (previous messages) +- 3️⃣ One that fetches and subscribes to messages that were created after the timestamp of the `CuteChat` intialization (only new messages) + +No caching is ever emptied, but only accordingly updated based on the snapshot event changes, meaning the list will persist, and minimize the disruption for the user. From 5aef5ba535d370ecb8525f4cf786f74fc5446ab8 Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 11:40:24 +0200 Subject: [PATCH 13/15] refactor: extract isCloseToBottom to own file --- src/CuteChat.tsx | 14 +------------- src/utils/isCloseToBottom.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 src/utils/isCloseToBottom.ts diff --git a/src/CuteChat.tsx b/src/CuteChat.tsx index cee9d75..398fbcd 100644 --- a/src/CuteChat.tsx +++ b/src/CuteChat.tsx @@ -14,6 +14,7 @@ import type { IMessage } from 'react-native-gifted-chat'; import { GiftedChat, GiftedChatProps } from 'react-native-gifted-chat'; import { appendSnapshot } from './utils/appendSnapshot'; import { prepareSnapshot } from './utils/prepareSnapshot'; +import { isCloseToBottom } from './utils/isCloseToBottom'; interface CustomCuteChatProps { chatId: string; @@ -296,16 +297,3 @@ export function CuteChat(props: CuteChatProps) { /> ); } - -function isCloseToBottom({ - layoutMeasurement, - contentOffset, - contentSize, -}: NativeScrollEvent) { - const paddingToBottom = 500; - - return ( - layoutMeasurement.height + contentOffset.y >= - contentSize.height - paddingToBottom - ); -} diff --git a/src/utils/isCloseToBottom.ts b/src/utils/isCloseToBottom.ts new file mode 100644 index 0000000..a92f52d --- /dev/null +++ b/src/utils/isCloseToBottom.ts @@ -0,0 +1,14 @@ +import { NativeScrollEvent } from 'react-native'; + +export function isCloseToBottom({ + layoutMeasurement, + contentOffset, + contentSize, +}: NativeScrollEvent) { + const paddingToBottom = 500; + + return ( + layoutMeasurement.height + contentOffset.y >= + contentSize.height - paddingToBottom + ); +} From e5d51a3f61df3af7d4076f918277f02717d4d65e Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 13:18:28 +0200 Subject: [PATCH 14/15] fix: use get instead of onSnapshot in docToMessage --- src/utils/docToMessage.ts | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/utils/docToMessage.ts b/src/utils/docToMessage.ts index 9ccf01e..59ac57c 100644 --- a/src/utils/docToMessage.ts +++ b/src/utils/docToMessage.ts @@ -17,36 +17,27 @@ export const docToMessage = async ( } const [files, sender] = await Promise.all([ - new Promise< - | FirebaseFirestore.QueryDocumentSnapshot[] - | undefined - >((resolve, reject) => { - firestore() - .collection(`chats/${chatId}/messages/${doc.id}/files`) - .onSnapshot((snapshot) => { - if (snapshot.empty) { - resolve(undefined); - } else { - resolve(snapshot.docs); - } - }, reject); - }), + new Promise( + (resolve, reject) => { + firestore() + .collection(`chats/${chatId}/messages/${doc.id}/files`) + .get() + .then((data) => resolve(data)) + .catch((e) => reject(e)); + } + ), new Promise( (resolve, reject) => { firestore() .doc(data.senderRef._documentPath._parts.join('/')) - .onSnapshot((snapshot) => { - if (!snapshot.exists) { - resolve(undefined); - } else { - resolve(snapshot.data()); - } - }, reject); + .get() + .then((data) => resolve(data)) + .catch((e) => reject(e)); } ), ]); - const image = files?.[0]?.data().url; + const image = files?.docs[0]?.data().url; // Fetch user data from reference if (sender) { From 810726320859b442dcad5830ed8fc076df98ff12 Mon Sep 17 00:00:00 2001 From: Melvin Date: Wed, 30 Apr 2025 13:25:59 +0200 Subject: [PATCH 15/15] refactor: docToMessage, remove non neccessary Promise instantiation --- src/utils/docToMessage.ts | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/utils/docToMessage.ts b/src/utils/docToMessage.ts index 59ac57c..b8d17bf 100644 --- a/src/utils/docToMessage.ts +++ b/src/utils/docToMessage.ts @@ -17,24 +17,8 @@ export const docToMessage = async ( } const [files, sender] = await Promise.all([ - new Promise( - (resolve, reject) => { - firestore() - .collection(`chats/${chatId}/messages/${doc.id}/files`) - .get() - .then((data) => resolve(data)) - .catch((e) => reject(e)); - } - ), - new Promise( - (resolve, reject) => { - firestore() - .doc(data.senderRef._documentPath._parts.join('/')) - .get() - .then((data) => resolve(data)) - .catch((e) => reject(e)); - } - ), + firestore().collection(`chats/${chatId}/messages/${doc.id}/files`).get(), + firestore().doc(data.senderRef._documentPath._parts.join('/')).get(), ]); const image = files?.docs[0]?.data().url;