Skip to content

Commit e08d972

Browse files
authored
feat: show read receipts in VirtualizedMessageList (#2076)
1 parent eba171f commit e08d972

File tree

2 files changed

+55
-8
lines changed

2 files changed

+55
-8
lines changed

docusaurus/docs/React/components/core-components/virtualized-list.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,14 @@ The amount of extra content the list should render in addition to what's necessa
199199
|--------|---------|
200200
| number | 0 |
201201

202+
### returnAllReadData
203+
204+
Keep track of read receipts for each message sent by the user. When disabled, only the last own message delivery / read status is rendered.
205+
206+
| Type | Default |
207+
|---------|---------|
208+
| boolean | false |
209+
202210
### scrollSeekPlaceHolder
203211

204212
Custom data passed to the list that determines when message placeholders should be shown during fast scrolling.

src/components/MessageList/VirtualizedMessageList.tsx

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ import {
1111

1212
import { GiphyPreviewMessage as DefaultGiphyPreviewMessage } from './GiphyPreviewMessage';
1313
import { useGiphyPreview } from './hooks/useGiphyPreview';
14-
import { useNewMessageNotification } from './hooks/useNewMessageNotification';
15-
import { usePrependedMessagesCount } from './hooks/usePrependMessagesCount';
16-
import { useShouldForceScrollToBottom } from './hooks/useShouldForceScrollToBottom';
14+
import {
15+
useLastReadData,
16+
useNewMessageNotification,
17+
usePrependedMessagesCount,
18+
useShouldForceScrollToBottom,
19+
} from './hooks';
1720
import { MessageNotification as DefaultMessageNotification } from './MessageNotification';
1821
import { MessageListNotifications as DefaultMessageListNotifications } from './MessageListNotifications';
1922
import { MessageListMainPanel } from './MessageListMainPanel';
20-
import { getGroupStyles, GroupStyle, processMessages } from './utils';
23+
import { getGroupStyles, getLastReceived, GroupStyle, processMessages } from './utils';
2124

2225
import { CUSTOM_MESSAGE_TYPE } from '../../constants/messageTypes';
2326
import { DateSeparator as DefaultDateSeparator } from '../DateSeparator/DateSeparator';
@@ -39,17 +42,23 @@ import { useChatContext } from '../../context/ChatContext';
3942
import { useComponentContext } from '../../context/ComponentContext';
4043
import { isDate } from '../../context/TranslationContext';
4144

42-
import type { Channel } from 'stream-chat';
45+
import type { Channel, ChannelState as StreamChannelState, UserResponse } from 'stream-chat';
4346

4447
import type { ChatProps } from '../Chat';
4548
import type { DefaultStreamChatGenerics, UnknownType } from '../../types/types';
4649

4750
const PREPEND_OFFSET = 10 ** 7;
4851

49-
type VirtuosoContext = {
52+
type VirtuosoContext<
53+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
54+
> = {
5055
customClasses: ChatProps['customClasses'];
56+
/** Latest received message id in the current channel */
57+
lastReceivedMessageId: string | null | undefined;
5158
messageGroupStyles: Record<string, GroupStyle>;
5259
numItemsPrepended: number;
60+
/** Mapping of message ID of own messages to the array of users, who read the given message */
61+
ownMessagesReadByOthers: Record<string, UserResponse<StreamChatGenerics>[]>;
5362
processedMessages: StreamMessage[];
5463
};
5564

@@ -63,6 +72,7 @@ type VirtualizedMessageListWithContextProps<
6372
loadingMore: boolean;
6473
loadingMoreNewer: boolean;
6574
notifications: ChannelNotifications;
75+
read?: StreamChannelState<StreamChatGenerics>['read'];
6676
};
6777

6878
function captureResizeObserverExceededError(e: ErrorEvent) {
@@ -133,6 +143,8 @@ const VirtualizedMessageListWithContext = <
133143
notifications,
134144
// TODO: refactor to scrollSeekPlaceHolderConfiguration and components.ScrollSeekPlaceholder, like the Virtuoso Component
135145
overscan = 0,
146+
read,
147+
returnAllReadData = false,
136148
scrollSeekPlaceHolder,
137149
scrollToLatestMessageOnFocus = false,
138150
separateGiphyPreview = false,
@@ -201,6 +213,18 @@ const VirtualizedMessageListWithContext = <
201213
client.userID,
202214
]);
203215

216+
// get the mapping of own messages to array of users who read them
217+
const ownMessagesReadByOthers = useLastReadData({
218+
messages: processedMessages,
219+
read,
220+
returnAllReadData,
221+
userID: client.userID,
222+
});
223+
224+
const lastReceivedMessageId = useMemo(() => getLastReceived(processedMessages), [
225+
processedMessages,
226+
]);
227+
204228
const groupStylesFn = groupStyles || getGroupStyles;
205229
const messageGroupStyles = useMemo(
206230
() =>
@@ -315,7 +339,12 @@ const VirtualizedMessageListWithContext = <
315339
};
316340

317341
const messageRenderer = useCallback(
318-
(messageList: StreamMessage<StreamChatGenerics>[], virtuosoIndex: number) => {
342+
(
343+
messageList: StreamMessage<StreamChatGenerics>[],
344+
virtuosoIndex: number,
345+
virtuosoContext: VirtuosoContext,
346+
) => {
347+
const { lastReceivedMessageId, ownMessagesReadByOthers } = virtuosoContext;
319348
const streamMessageIndex = virtuosoIndex + numItemsPrepended - PREPEND_OFFSET;
320349
// use custom renderer supplied by client if present and skip the rest
321350
if (customMessageRenderer) {
@@ -353,9 +382,11 @@ const VirtualizedMessageListWithContext = <
353382
endOfGroup={endOfGroup}
354383
firstOfGroup={firstOfGroup}
355384
groupedByUser={groupedByUser}
385+
lastReceivedId={lastReceivedMessageId}
356386
message={message}
357387
Message={MessageUIComponent}
358388
messageActions={props.messageActions}
389+
readBy={ownMessagesReadByOthers[message.id] || []}
359390
/>
360391
);
361392
},
@@ -463,8 +494,10 @@ const VirtualizedMessageListWithContext = <
463494
context={
464495
{
465496
customClasses,
497+
lastReceivedMessageId,
466498
messageGroupStyles,
467499
numItemsPrepended,
500+
ownMessagesReadByOthers,
468501
processedMessages,
469502
} as VirtuosoContext
470503
}
@@ -476,7 +509,9 @@ const VirtualizedMessageListWithContext = <
476509
processedMessages,
477510
highlightedMessageId,
478511
)}
479-
itemContent={(i) => messageRenderer(processedMessages, i)}
512+
itemContent={(i, _, context) =>
513+
messageRenderer(processedMessages, i, context as VirtuosoContext)
514+
}
480515
itemSize={fractionalItemSize}
481516
key={messageSetKey}
482517
overscan={overscan}
@@ -555,6 +590,8 @@ export type VirtualizedMessageListProps<
555590
messages?: StreamMessage<StreamChatGenerics>[];
556591
/** The amount of extra content the list should render in addition to what's necessary to fill in the viewport */
557592
overscan?: number;
593+
/** Keep track of read receipts for each message sent by the user. When disabled, only the last own message delivery / read status is rendered. */
594+
returnAllReadData?: boolean;
558595
/**
559596
* Performance improvement by showing placeholders if user scrolls fast through list.
560597
* it can be used like this:
@@ -605,6 +642,7 @@ export function VirtualizedMessageList<
605642
loadingMoreNewer,
606643
messages: contextMessages,
607644
notifications,
645+
read,
608646
suppressAutoscroll,
609647
} = useChannelStateContext<StreamChatGenerics>('VirtualizedMessageList');
610648

@@ -623,6 +661,7 @@ export function VirtualizedMessageList<
623661
loadMoreNewer={loadMoreNewer}
624662
messages={messages}
625663
notifications={notifications}
664+
read={read}
626665
suppressAutoscroll={suppressAutoscroll}
627666
{...props}
628667
/>

0 commit comments

Comments
 (0)