Skip to content

Commit

Permalink
fix: show unread msg banner above unread msg only (#2596)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinCupela authored Jan 10, 2025
1 parent 8c65bbf commit b9eb846
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 102 deletions.
37 changes: 11 additions & 26 deletions src/components/MessageList/VirtualizedMessageListComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading';
import { isMessageEdited, Message } from '../Message';

import { StreamMessage, useComponentContext } from '../../context';
import { isDateSeparatorMessage } from './utils';
import { getIsFirstUnreadMessage, isDateSeparatorMessage } from './utils';

import type { GroupStyle } from './utils';
import type { VirtuosoContext } from './VirtualizedMessageList';
Expand Down Expand Up @@ -185,29 +185,19 @@ export const messageRenderer = <
shouldGroupByUser &&
(message.user?.id !== maybeNextMessage?.user?.id || isMessageEdited(message));

const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime();
const lastReadTimestamp = lastReadDate?.getTime();
const isFirstMessage = streamMessageIndex === 0;
const isNewestMessage = lastReadMessageId === lastReceivedMessageId;
const isLastReadMessage =
message.id === lastReadMessageId ||
(!unreadMessageCount && createdAtTimestamp === lastReadTimestamp);
const isFirstUnreadMessage =
firstUnreadMessageId === message.id ||
(!!unreadMessageCount &&
createdAtTimestamp &&
lastReadTimestamp &&
createdAtTimestamp > lastReadTimestamp &&
isFirstMessage);

const showUnreadSeparatorAbove = !lastReadMessageId && isFirstUnreadMessage;

const showUnreadSeparatorBelow =
isLastReadMessage && !isNewestMessage && (firstUnreadMessageId || !!unreadMessageCount);
const isFirstUnreadMessage = getIsFirstUnreadMessage({
firstUnreadMessageId,
isFirstMessage: streamMessageIndex === 0,
lastReadDate,
lastReadMessageId,
message,
previousMessage: streamMessageIndex ? messageList[streamMessageIndex - 1] : undefined,
unreadMessageCount,
});

return (
<>
{showUnreadSeparatorAbove && (
{isFirstUnreadMessage && (
<div className='str-chat__unread-messages-separator-wrapper'>
<UnreadMessagesSeparator unreadCount={unreadMessageCount} />
</div>
Expand All @@ -233,11 +223,6 @@ export const messageRenderer = <
sortReactions={sortReactions}
threadList={threadList}
/>
{showUnreadSeparatorBelow && (
<div className='str-chat__unread-messages-separator-wrapper'>
<UnreadMessagesSeparator unreadCount={unreadMessageCount} />
</div>
)}
</>
);
};
4 changes: 2 additions & 2 deletions src/components/MessageList/__tests__/MessageList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ describe('MessageList', () => {
},
});
});
expect(screen.queryByTestId('unread-messages-separator')).not.toBeInTheDocument();
expect(screen.queryByTestId('unread-messages-separator')).toBeInTheDocument();
});

it('should display custom unread messages separator when channel is marked unread', async () => {
Expand Down Expand Up @@ -583,7 +583,7 @@ describe('MessageList', () => {
},
});
});
expect(screen.queryByText(customUnreadMessagesSeparatorText)).not.toBeInTheDocument();
expect(screen.queryByText(customUnreadMessagesSeparatorText)).toBeInTheDocument();
});

describe('notification', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,55 +435,22 @@ describe('VirtualizedMessageComponents', () => {
);
};

it('should be rendered below the last read message if unread count is non-zero', async () => {
it('should be rendered above the first unread message if unread count is non-zero', async () => {
const { container } = await renderMarkUnread({
virtuosoContext: {
lastReadDate: new Date(messages[0].created_at),
lastReadMessageId: messages[0].id,
lastReceivedMessageId: messages[1].id,
Message,
messageGroupStyles: {},
numItemsPrepended,
numItemsPrepended: 1,
ownMessagesReadByOthers: {},
processedMessages: messages,
unreadMessageCount: 1,
UnreadMessagesSeparator,
virtuosoRef: { current: {} },
},
});
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="message-component"
/>
<div
class="str-chat__unread-messages-separator-wrapper"
>
<div
class="str-chat__unread-messages-separator"
data-testid="unread-messages-separator"
>
Unread messages
</div>
</div>
</div>
`);
});

it('should be rendered above the last first unread message', async () => {
const { container } = await renderMarkUnread({
virtuosoContext: {
lastReadDate: new Date(1),
lastReceivedMessageId: messages[1].id,
Message,
messageGroupStyles: {},
numItemsPrepended,
ownMessagesReadByOthers: {},
processedMessages: messages,
unreadMessageCount: messages.length,
UnreadMessagesSeparator,
virtuosoRef: { current: {} },
},
});
expect(container).toMatchInlineSnapshot(`
<div>
<div
Expand All @@ -506,6 +473,7 @@ describe('VirtualizedMessageComponents', () => {
it('should not be rendered below the last read message if the message is the newest in the channel', async () => {
const { container } = await renderMarkUnread({
virtuosoContext: {
lastReadDate: new Date(messages[1].created_at),
lastReadMessageId: messages[1].id,
lastReceivedMessageId: messages[1].id,
Message,
Expand All @@ -527,15 +495,15 @@ describe('VirtualizedMessageComponents', () => {
`);
});

it('should be rendered if unread count is falsy and first unread message is known', async () => {
it('should be rendered if unread count is falsy and the first unread message is known', async () => {
const { container } = await renderMarkUnread({
virtuosoContext: {
firstUnreadMessageId: messages[1].id,
lastReadMessageId: messages[0].id,
lastReceivedMessageId: messages[1].id,
Message,
messageGroupStyles: {},
numItemsPrepended,
numItemsPrepended: 1,
ownMessagesReadByOthers: {},
processedMessages: messages,
unreadMessageCount: 0,
Expand All @@ -545,9 +513,6 @@ describe('VirtualizedMessageComponents', () => {
});
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="message-component"
/>
<div
class="str-chat__unread-messages-separator-wrapper"
>
Expand All @@ -558,6 +523,9 @@ describe('VirtualizedMessageComponents', () => {
Unread messages
</div>
</div>
<div
class="message-component"
/>
</div>
`);
});
Expand All @@ -569,7 +537,7 @@ describe('VirtualizedMessageComponents', () => {
lastReceivedMessageId: messages[1].id,
Message,
messageGroupStyles: {},
numItemsPrepended,
numItemsPrepended: 1,
ownMessagesReadByOthers: {},
processedMessages: messages,
unreadMessageCount: 0,
Expand All @@ -593,7 +561,7 @@ describe('VirtualizedMessageComponents', () => {
lastReceivedMessageId: messages[1].id,
Message,
messageGroupStyles: {},
numItemsPrepended: 1,
numItemsPrepended: 0,
ownMessagesReadByOthers: {},
processedMessages: messages,
unreadMessageCount: 1,
Expand Down
44 changes: 13 additions & 31 deletions src/components/MessageList/renderMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { Fragment, ReactNode } from 'react';

import type { UserResponse } from 'stream-chat';

import { GroupStyle, isDateSeparatorMessage } from './utils';
import { getIsFirstUnreadMessage, GroupStyle, isDateSeparatorMessage } from './utils';
import { Message } from '../Message';
import { DateSeparator as DefaultDateSeparator } from '../DateSeparator';
import { EventComponent as DefaultMessageSystem } from '../EventComponent';
Expand Down Expand Up @@ -75,6 +75,7 @@ export function defaultRenderMessages<

const renderedMessages = [];
let firstMessage;
let previousMessage = undefined;
for (let index = 0; index < messages.length; index++) {
const message = messages[index];
if (isDateSeparatorMessage(message)) {
Expand Down Expand Up @@ -106,34 +107,19 @@ export function defaultRenderMessages<
const groupStyles: GroupStyle = messageGroupStyles[message.id] || '';
const messageClass = customClasses?.message || `str-chat__li str-chat__li--${groupStyles}`;

const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime();
const lastReadTimestamp = channelUnreadUiState?.last_read.getTime();
const isFirstMessage = firstMessage?.id && firstMessage.id === message.id;
const isNewestMessage = index === messages.length - 1;

const isLastReadMessage =
channelUnreadUiState?.last_read_message_id === message.id ||
(!channelUnreadUiState?.unread_messages && createdAtTimestamp === lastReadTimestamp);

const isFirstUnreadMessage =
channelUnreadUiState?.first_unread_message_id === message.id ||
(!!channelUnreadUiState?.unread_messages &&
!!createdAtTimestamp &&
!!lastReadTimestamp &&
createdAtTimestamp > lastReadTimestamp &&
isFirstMessage);

const showUnreadSeparatorAbove =
!channelUnreadUiState?.last_read_message_id && isFirstUnreadMessage;

const showUnreadSeparatorBelow =
isLastReadMessage &&
!isNewestMessage &&
(channelUnreadUiState?.first_unread_message_id || !!channelUnreadUiState?.unread_messages); // this part has to be here as we do not mark channel read when sending a message
const isFirstUnreadMessage = getIsFirstUnreadMessage({
firstUnreadMessageId: channelUnreadUiState?.first_unread_message_id,
isFirstMessage: !!firstMessage?.id && firstMessage.id === message.id,
lastReadDate: channelUnreadUiState?.last_read,
lastReadMessageId: channelUnreadUiState?.last_read_message_id,
message,
previousMessage,
unreadMessageCount: channelUnreadUiState?.unread_messages,
});

renderedMessages.push(
<Fragment key={message.id || (message.created_at as string)}>
{showUnreadSeparatorAbove && UnreadMessagesSeparator && (
{isFirstUnreadMessage && UnreadMessagesSeparator && (
<li className='str-chat__li str-chat__unread-messages-separator-wrapper'>
<UnreadMessagesSeparator unreadCount={channelUnreadUiState?.unread_messages} />
</li>
Expand All @@ -147,13 +133,9 @@ export function defaultRenderMessages<
{...messageProps}
/>
</li>
{showUnreadSeparatorBelow && UnreadMessagesSeparator && (
<li className='str-chat__li str-chat__unread-messages-separator-wrapper'>
<UnreadMessagesSeparator unreadCount={channelUnreadUiState?.unread_messages} />
</li>
)}
</Fragment>,
);
previousMessage = message;
}
}
return renderedMessages;
Expand Down
32 changes: 32 additions & 0 deletions src/components/MessageList/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,35 @@ export function isDateSeparatorMessage<
>(message: StreamMessage<StreamChatGenerics>): message is DateSeparatorMessage {
return message.customType === CUSTOM_MESSAGE_TYPE.date && !!message.date && isDate(message.date);
}

export const getIsFirstUnreadMessage = ({
firstUnreadMessageId,
isFirstMessage,
lastReadDate,
lastReadMessageId,
message,
previousMessage,
unreadMessageCount = 0,
}: {
isFirstMessage: boolean;
message: StreamMessage;
firstUnreadMessageId?: string;
lastReadDate?: Date;
lastReadMessageId?: string;
previousMessage?: StreamMessage;
unreadMessageCount?: number;
}) => {
const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime();
const lastReadTimestamp = lastReadDate?.getTime();

const messageIsUnread =
!!createdAtTimestamp && !!lastReadTimestamp && createdAtTimestamp > lastReadTimestamp;

const previousMessageIsLastRead =
!!lastReadMessageId && lastReadMessageId === previousMessage?.id;

return (
firstUnreadMessageId === message.id ||
(!!unreadMessageCount && messageIsUnread && (isFirstMessage || previousMessageIsLastRead))
);
};

0 comments on commit b9eb846

Please sign in to comment.