Search code examples
javascriptreact-nativegoogle-cloud-firestoreuse-effectreact-native-gifted-chat

React-native-gifted-chat with cloud firestore pagination


I'm using Firestore to store messages. In order to optimize the mobile application performances, I would like to set a limit(50) in the firestore query. It works well and implemented the onLoadEarlier React-native-gifted-chat available in the props.

All is working fine.

But, when I send a new message in the chat, after scrolled up to see the earliers messages, only the 50 last messages with the new one, off course, are available.

So, each time I'm adding a message in the Firestore database, the onSnapshot (in the useeffect) is executed and apply the limit query.

Is there a way to avoid this ?

Thanks.

Here my useEffect :

useEffect(() => {
    const messagesListener = firestore()
    .collection('groups')
    .doc(group._id)
    .collection('messages')
    .orderBy('createdAt', 'desc')
    .limit(50)
    .onSnapshot(querySnapshot => {
        const newMessages = querySnapshot.docs.map(doc => {
            const firebaseData = doc.data();

            const data = {
                _id: doc.id,
                text: '',
                createdAt: new Date().getTime(),
                ...firebaseData
            };

            return data;
        });

        setMessages(previousMessages => {
            return GiftedChat.append(previousMessages, newMessages);
        });
    });

    return () => messagesListener();
}, []);

Solution

  • I am using FlatList in react-native to render chats and I had to paginate the chats list. Since Firestore query cursor is not supported in live listener, I created two list, recentChats & oldChats.

    I populate recentChats using live listener query.onSnapshot & oldChats using cursor startAfter. FlatList data is combination of both list and I take care of merging logic.

    const MESSAGE_LIMIT = 15;
    
    const ChatWindow = props => {
      const { sessionId, postMessage, onSendTemplateButtonPress } = props;
    
      // Firestore cursor is not supported in query.onSnapshot so maintaining two chat list
      // oldChats -> chat list via cursor, recentChats -> chat list via live listener
      const [oldChats, setOldChats] = useState([]);
      const [recentChats, setRecentChats] = useState([]);
    
      // if true, show a loader at the top of chat list
      const [moreChatsAvailable, setMoreChatsAvailable] = useState(true);
    
      const [inputMessage, setInputMessage] = useState('');
    
      useEffect(() => {
        const query = getGuestChatMessagesQuery(sessionId)
          .limit(MESSAGE_LIMIT);
        const listener = query.onSnapshot(querySnapshot => {
          let chats = [];
          querySnapshot.forEach(snapshot => {
            chats.push(snapshot.data());
          });
          // merge recentChats & chats
          if (recentChats.length > 0) {
            const newRecentChats = [];
            for (let i = 0; i < chats.length; i++) {
              if (chats[i].sessionId === recentChats[0].sessionId) {
                break;
              }
              newRecentChats.push(chats[i]);
            }
            setRecentChats([...newRecentChats, ...recentChats]);
          } else {
            setRecentChats(chats);
            if (chats.length < MESSAGE_LIMIT) {
              setMoreChatsAvailable(false);
            }
          }
        });
    
        return () => {
          // unsubscribe listener
          listener();
        };
      }, []);
    
      const onMessageInputChange = text => {
        setInputMessage(text);
      };
    
      const onMessageSubmit = () => {
        postMessage(inputMessage);
        setInputMessage('');
      };
    
      const renderFlatListItem = ({ item }) => {
        return (<ChatBubble chat={item} />);
      };
    
      const onChatListEndReached = () => {
        if (!moreChatsAvailable) {
          return;
        }
        let startAfterTime;
        if (oldChats.length > 0) {
          startAfterTime = oldChats[oldChats.length - 1].time;
        } else if (recentChats.length > 0) {
          startAfterTime = recentChats[recentChats.length - 1].time;
        } else {
          setMoreChatsAvailable(false);
          return;
        }
        // query data using cursor
        getGuestChatMessagesQuery(sessionId)
          .startAfter(startAfterTime)
          .limit(MESSAGE_LIMIT)
          .get()
          .then(querySnapshot => {
            let chats = [];
            querySnapshot.forEach(snapshot => {
              chats.push(snapshot.data());
            });
            if (chats.length === 0) {
              setMoreChatsAvailable(false);
            } else {
              setOldChats([...oldChats, ...chats]);
            }
          });
      };
    
      return (
        <View style={[GenericStyles.fill, GenericStyles.p16]}>
          <FlatList
            inverted
            data={[...recentChats, ...oldChats]}
            renderItem={renderFlatListItem}
            keyExtractor={item => item.messageId}
            onEndReached={onChatListEndReached}
            onEndReachedThreshold={0.2}
            ListFooterComponent={moreChatsAvailable ? <ActivityIndicator /> : null}
          />
          {
            Singleton.isStaff ?
              null:
              <ChatInput
                onMessageInputChange={onMessageInputChange}
                onMessageSubmit={onMessageSubmit}
                inputMessage={inputMessage}
                style={GenericStyles.selfEnd}
                onSendTemplateButtonPress={onSendTemplateButtonPress}
              />
          }
        </View>
      );
    };