stream-chat-react
Version:
React components to create chat conversations or livestream style chat
61 lines (60 loc) • 3.81 kB
JavaScript
import { useMemo, useRef } from 'react';
import { isLocalMessage } from '../../utils';
const STATUSES_EXCLUDED_FROM_PREPEND = {
failed: true,
sending: true,
};
export function usePrependedMessagesCount(messages, hasDateSeparator) {
const firstRealMessageIndex = hasDateSeparator ? 1 : 0;
const firstMessageOnFirstLoadedPage = useRef(undefined);
const previousFirstMessageOnFirstLoadedPage = useRef(undefined);
const previousNumItemsPrepended = useRef(0);
const numItemsPrepended = useMemo(() => {
if (!messages || !messages.length) {
previousNumItemsPrepended.current = 0;
return 0;
}
const currentFirstMessage = messages?.[firstRealMessageIndex];
const noNewMessages = currentFirstMessage?.id === previousFirstMessageOnFirstLoadedPage.current?.id;
// This is possible only, when sending messages very quickly (basically single char messages submitted like a crazy) in empty channel (first page)
// Optimistic UI update, when sending messages, can lead to a situation, when
// the order of the messages changes for a moment. This can happen, when a user
// sends multiple messages withing few milliseconds. E.g. we send a message A
// then message B. At first we have message array with both messages of status "sending"
// then response for message A is received with a new - later - created_at timestamp
// this leads to rearrangement of 1.B ("sending"), 2.A ("received"). Still firstMessageOnFirstLoadedPage.current
// points to message A, but now this message has index 1 => previousNumItemsPrepended.current === 1
// That in turn leads to incorrect index calculation in VirtualizedMessageList trying to access a message
// at non-existent index. Therefore, we ignore messages of status "sending" / "failed" in order they are
// not considered as prepended messages.
const currentFirstMessageStatus = isLocalMessage(currentFirstMessage)
? currentFirstMessage.status
: undefined;
const firstMsgMovedAfterMessagesInExcludedStatus = !!(currentFirstMessageStatus &&
STATUSES_EXCLUDED_FROM_PREPEND[currentFirstMessageStatus]);
if (noNewMessages || firstMsgMovedAfterMessagesInExcludedStatus) {
return previousNumItemsPrepended.current;
}
if (!firstMessageOnFirstLoadedPage.current) {
firstMessageOnFirstLoadedPage.current = currentFirstMessage;
}
previousFirstMessageOnFirstLoadedPage.current = currentFirstMessage;
// if new messages were prepended, find out how many
// start with this number because there cannot be fewer prepended items than before
for (let prependedMessageCount = previousNumItemsPrepended.current; prependedMessageCount < messages.length; prependedMessageCount += 1) {
const messageIsFirstOnFirstLoadedPage = messages[prependedMessageCount].id === firstMessageOnFirstLoadedPage.current?.id;
if (messageIsFirstOnFirstLoadedPage) {
previousNumItemsPrepended.current = prependedMessageCount - firstRealMessageIndex;
return previousNumItemsPrepended.current;
}
}
// if no match has found, we have jumped - reset the prepended item count.
firstMessageOnFirstLoadedPage.current = currentFirstMessage;
previousNumItemsPrepended.current = 0;
return 0;
// TODO: there's a bug here, the messages prop is the same array instance (something mutates it)
// that's why the second dependency is necessary
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [firstRealMessageIndex, messages, messages?.length]);
return numItemsPrepended;
}