diff --git a/components/conversation-screen.tsx b/components/conversation-screen.tsx index ddf62f97..2cb0afca 100644 --- a/components/conversation-screen.tsx +++ b/components/conversation-screen.tsx @@ -38,14 +38,15 @@ import { import { getRandomString } from '../random/string'; import { api } from '../api/api'; -// TODO: Re-add the ability to load old messages past the first page - const ConversationScreen = ({navigation, route}) => { const [messageFetchTimeout, setMessageFetchTimeout] = useState(false); const [messages, setMessages] = useState(null); const [lastMessageStatus, setLastMessageStatus] = useState< MessageStatus | null >(null); + const hasScrolled = useRef(false); + const hasFetchedAll = useRef(false); + const isFetchingNextPage = useRef(false); const personId: number = route?.params?.personId; const name: string = route?.params?.name; @@ -54,9 +55,21 @@ const ConversationScreen = ({navigation, route}) => { const listRef = useRef(null) + const lastMamId = (() => { + if (!messages) return ''; + if (!messages.length) return ''; + + const mamId = messages[0].mamId; + + if (!mamId) return ''; + + return mamId; + })(); + const scrollToEnd = useCallback(() => { - if (listRef.current) { + if (listRef.current && !hasScrolled.current) { listRef.current.scrollToEnd({animated: true}); + hasScrolled.current = true; } }, [listRef.current]); @@ -81,6 +94,7 @@ const ConversationScreen = ({navigation, route}) => { ); if (messageStatus === 'sent') { + hasScrolled.current = false; setMessages(messages => [...(messages ?? []), message]); // TODO: Ideally, you wouldn't have to mark messages as sent in this way; @@ -103,26 +117,49 @@ const ConversationScreen = ({navigation, route}) => { ); }, [personId, name]); - const _fetchConversation = useCallback(async () => { - const _messages = await fetchConversation(personId); - setMessageFetchTimeout(_messages === 'timeout'); - if (_messages !== 'timeout') { - setMessages(existingMessages => - [...(existingMessages ?? []), ...(_messages ?? [])] - ); + const maybeLoadNextPage = useCallback(async () => { + if (hasFetchedAll.current) { + return; } + if (isFetchingNextPage.current) { + return; + } + + isFetchingNextPage.current = true; + const fetchedMessages = await fetchConversation(personId, lastMamId); + + isFetchingNextPage.current = false; + + setMessageFetchTimeout(fetchedMessages === 'timeout'); + if (fetchedMessages !== 'timeout') { + // Prevents the list from moving up to the newly added speech bubbles and + // triggering another fetch + if (listRef.current) listRef.current.scrollTo({y: 1}); + + setMessages([...(fetchedMessages ?? []), ...(messages ?? [])]); + + hasFetchedAll.current = !(fetchedMessages && fetchedMessages.length); + } + }, [messages, lastMamId]); + + const _onReceiveMessage = useCallback((msg) => { + hasScrolled.current = false; + setMessages(msgs => [...(msgs ?? []), msg]); }, []); - const _onReceiveMessage = useCallback( - (msg) => setMessages(msgs => [...(msgs ?? []), msg]), - [] - ); + const isCloseToTop = ({contentOffset}) => contentOffset.y < 20; + + const onScroll = useCallback(({nativeEvent}) => { + if (isCloseToTop(nativeEvent)) { + maybeLoadNextPage(); + } + }, [maybeLoadNextPage]); useEffect(() => { - _fetchConversation(); + maybeLoadNextPage(); return onReceiveMessage(_onReceiveMessage, personId); - }, [_onReceiveMessage, personId]); + }, []); return ( <> @@ -247,8 +284,11 @@ const ConversationScreen = ({navigation, route}) => { ref={listRef} onLayout={scrollToEnd} onContentSizeChange={scrollToEnd} + onScroll={onScroll} + scrollEventThrottle={0} contentContainerStyle={{ paddingTop: 10, + paddingBottom: 20, maxWidth: 600, width: '100%', alignSelf: 'center', diff --git a/xmpp/xmpp.tsx b/xmpp/xmpp.tsx index 05c09a0e..fb2b4bc9 100644 --- a/xmpp/xmpp.tsx +++ b/xmpp/xmpp.tsx @@ -31,6 +31,7 @@ type Message = { to: string fromCurrentUser: boolean id: string + mamId?: string | undefined timestamp: Date }; @@ -560,7 +561,7 @@ const onReceiveMessage = ( from: from.toString(), to: to.toString(), id: id.toString(), - timestamp: new Date(stamp.toString()), + timestamp: stamp.toString() ? new Date(stamp.toString()) : new Date(), fromCurrentUser: jidToPersonId(from.toString()) == signedInUser?.personId, }; @@ -606,6 +607,7 @@ const moveToChats = async (personId: number) => { const _fetchConversation = async ( withPersonId: number, callback: (messages: Message[] | 'timeout') => void, + beforeId: string = '', ) => { if (!_xmpp) return callback('timeout'); @@ -624,7 +626,7 @@ const _fetchConversation = async ( 50 - + ${beforeId} @@ -650,6 +652,10 @@ const _fetchConversation = async ( const from = xpath.select1(`string(./@from)`, node); const to = xpath.select1(`string(./@to)`, node); const id = xpath.select1(`string(./@id)`, node); + const mamId = xpath.select1( + `string(.//ancestor::*[name()='result']/@id)`, + node + ); const stamp = xpath.select1( `string(./preceding-sibling::*/@stamp | ./following-sibling::*/@stamp)`, node @@ -659,6 +665,7 @@ const _fetchConversation = async ( if (from === null) return; if (to === null) return; if (id === null) return; + if (mamId === null) return; if (stamp === null) return; if (bodyText === null) return; @@ -670,6 +677,7 @@ const _fetchConversation = async ( from: from.toString(), to: to.toString(), id: id.toString(), + mamId: mamId ? mamId.toString() : undefined, timestamp: new Date(stamp.toString()), fromCurrentUser: fromCurrentUser, }); @@ -706,11 +714,12 @@ const _fetchConversation = async ( }; const fetchConversation = async ( - withPersonId: number + withPersonId: number, + beforeId: string = '', ): Promise => { const __fetchConversation = new Promise( (resolve: (messages: Message[] | undefined | 'timeout') => void) => - _fetchConversation(withPersonId, resolve) + _fetchConversation(withPersonId, resolve, beforeId) ); return await withTimeout(5000, __fetchConversation);