Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: configure message group size by max time between messages #2439

Merged
merged 2 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions docusaurus/docs/React/components/core-components/message-list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,9 @@ pinned [message object](https://getstream.io/chat/docs/javascript/message_format

Callback function to map each message in the list to a group style (` 'middle' | 'top' | 'bottom' | 'single'`).

| Type |
| -------------------------------------------------------------------------------------------------------------------------- |
| (message: StreamMessage, previousMessage: StreamMessage, nextMessage: StreamMessage, noGroupByUser: boolean) => GroupStyle |
| Type |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| (message: StreamMessage, previousMessage: StreamMessage, nextMessage: StreamMessage, noGroupByUser: boolean, maxTimeBetweenGroupedMessages?: number) => GroupStyle |

### hasMore

Expand Down Expand Up @@ -302,6 +302,14 @@ Function called when more messages are to be loaded, provide your own function t
| -------- | ---------------------------------------------------------------------------------------- |
| function | [ChannelActionContextValue['loadMore']](../contexts/channel-action-context.mdx#loadmore) |

### maxTimeBetweenGroupedMessages

Maximum time in milliseconds that should occur between messages to still consider them grouped together.

| Type |
| ------ |
| number |

### Message

Custom UI component to display an individual message.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ If true, disables the injection of date separator UI components.

Callback function to set group styles for each message.

| Type |
| -------------------------------------------------------------------------------------------------------------------------- |
| (message: StreamMessage, previousMessage: StreamMessage, nextMessage: StreamMessage, noGroupByUser: boolean) => GroupStyle |
| Type |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| (message: StreamMessage, previousMessage: StreamMessage, nextMessage: StreamMessage, noGroupByUser: boolean, maxTimeBetweenGroupedMessages?: number) => GroupStyle |

### hasMore

Expand Down Expand Up @@ -173,6 +173,14 @@ Function called when more messages are to be loaded, provide your own function t
| -------- | ---------------------------------------------------------------------------------------- |
| function | [ChannelActionContextValue['loadMore']](../contexts/channel-action-context.mdx#loadmore) |

### maxTimeBetweenGroupedMessages

Maximum time in milliseconds that should occur between messages to still consider them grouped together.

| Type |
| ------ |
| number |

### Message

Custom UI component to display an individual message.
Expand Down
5 changes: 5 additions & 0 deletions src/components/MessageList/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const MessageListWithContext = <
threshold: loadMoreScrollThreshold = DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD,
...restInternalInfiniteScrollProps
} = {},
maxTimeBetweenGroupedMessages,
messageActions = Object.keys(MESSAGE_ACTIONS),
messages = [],
notifications,
Expand Down Expand Up @@ -138,6 +139,7 @@ const MessageListWithContext = <
headerPosition,
hideDeletedMessages,
hideNewMessageSeparator,
maxTimeBetweenGroupedMessages,
messages,
noGroupByUser,
reviewProcessedMessage,
Expand Down Expand Up @@ -320,6 +322,7 @@ export type MessageListProps<
previousMessage: StreamMessage<StreamChatGenerics>,
nextMessage: StreamMessage<StreamChatGenerics>,
noGroupByUser: boolean,
maxTimeBetweenGroupedMessages?: number,
) => GroupStyle;
/** Whether the list has more items to load */
hasMore?: boolean;
Expand All @@ -343,6 +346,8 @@ export type MessageListProps<
loadMore?: ChannelActionContextValue['loadMore'] | (() => Promise<void>);
/** Function called when newer messages are to be loaded, defaults to function stored in [ChannelActionContext](https://getstream.io/chat/docs/sdk/react/contexts/channel_action_context/) */
loadMoreNewer?: ChannelActionContextValue['loadMoreNewer'] | (() => Promise<void>);
/** Maximum time in milliseconds that should occur between messages to still consider them grouped together */
maxTimeBetweenGroupedMessages?: number;
/** The limit to use when paginating messages */
messageLimit?: number;
/** The messages to render in the list, defaults to messages stored in [ChannelStateContext](https://getstream.io/chat/docs/sdk/react/contexts/channel_state_context/) */
Expand Down
7 changes: 6 additions & 1 deletion src/components/MessageList/VirtualizedMessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ const VirtualizedMessageListWithContext = <
loadingMore,
loadMore,
loadMoreNewer,
maxTimeBetweenGroupedMessages,
Message: MessageUIComponentFromProps,
messageActions,
messageLimit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE,
Expand Down Expand Up @@ -312,13 +313,14 @@ const VirtualizedMessageListWithContext = <
processedMessages[i - 1],
processedMessages[i + 1],
!shouldGroupByUser,
maxTimeBetweenGroupedMessages,
);
if (style) acc[message.id] = style;
return acc;
}, {}),
// processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage
// eslint-disable-next-line react-hooks/exhaustive-deps
[processedMessages.length, shouldGroupByUser, groupStylesFn],
[maxTimeBetweenGroupedMessages, processedMessages.length, shouldGroupByUser, groupStylesFn],
);

const {
Expand Down Expand Up @@ -542,6 +544,7 @@ export type VirtualizedMessageListProps<
previousMessage: StreamMessage<StreamChatGenerics>,
nextMessage: StreamMessage<StreamChatGenerics>,
noGroupByUser: boolean,
maxTimeBetweenGroupedMessages?: number,
) => GroupStyle;
/** Whether or not the list has more items to load */
hasMore?: boolean;
Expand All @@ -566,6 +569,8 @@ export type VirtualizedMessageListProps<
loadMore?: ChannelActionContextValue['loadMore'] | (() => Promise<void>);
/** Function called when new messages are to be loaded, defaults to function stored in [ChannelActionContext](https://getstream.io/chat/docs/sdk/react/contexts/channel_action_context/) */
loadMoreNewer?: ChannelActionContextValue['loadMore'] | (() => Promise<void>);
/** Maximum time in milliseconds that should occur between messages to still consider them grouped together */
maxTimeBetweenGroupedMessages?: number;
/** Custom UI component to display a message, defaults to and accepts same props as [MessageSimple](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageSimple.tsx) */
Message?: React.ComponentType<MessageUIComponentProps<StreamChatGenerics>>;
/** The limit to use when paginating messages */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ export const messageRenderer = <
messageList[streamMessageIndex - 1];
const maybeNextMessage: StreamMessage<StreamChatGenerics> | undefined =
messageList[streamMessageIndex + 1];

// FIXME: firstOfGroup & endOfGroup should be derived from groupStyles which apply a more complex logic
const firstOfGroup =
shouldGroupByUser &&
(message.user?.id !== maybePrevMessage?.user?.id ||
Expand Down
205 changes: 203 additions & 2 deletions src/components/MessageList/__tests__/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { generateMessage } from '../../../mock-builders';
import { generateFileAttachment, generateMessage, generateUser } from '../../../mock-builders';

import { makeDateMessageId, processMessages } from '../utils';
import { getGroupStyles, makeDateMessageId, processMessages } from '../utils';
import { CUSTOM_MESSAGE_TYPE } from '../../../constants/messageTypes';

const mockedNanoId = 'V1StGXR8_Z5jdHi6B-myT';
Expand Down Expand Up @@ -421,3 +421,204 @@ describe('processMessages', () => {
});
});
});

describe('getGroupStyles', () => {
const user = generateUser();
let message;
let previousMessage;
let nextMessage;
let noGroupByUser;
beforeEach(() => {
message = generateMessage({ created_at: new Date(2), user });
previousMessage = generateMessage({ created_at: new Date(1), user });
nextMessage = generateMessage({ created_at: new Date(100), user });
noGroupByUser = false;
});

describe.each([
['bottom', 'next'],
['top', 'previous'],
])('marks a message as %s when %s message', (position) => {
it('does not exist', () => {
if (position === 'bottom') {
nextMessage = undefined;
}
if (position === 'top') {
previousMessage = undefined;
}
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe(position);
});

it('is intro message', () => {
if (position === 'bottom') {
nextMessage = { ...nextMessage, customType: CUSTOM_MESSAGE_TYPE.intro };
}
if (position === 'top') {
previousMessage = { ...previousMessage, customType: CUSTOM_MESSAGE_TYPE.intro };
}
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe(position);
});

it('is date message', () => {
if (position === 'bottom') {
nextMessage = { ...nextMessage, customType: CUSTOM_MESSAGE_TYPE.date };
}
if (position === 'top') {
previousMessage = { ...previousMessage, customType: CUSTOM_MESSAGE_TYPE.date };
}
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe(position);
});

it('is a system message', () => {
if (position === 'bottom') {
nextMessage = { ...nextMessage, type: 'system' };
}
if (position === 'top') {
previousMessage = { ...previousMessage, type: 'system' };
}
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe(position);
});

it('is an error message', () => {
if (position === 'bottom') {
nextMessage = { ...nextMessage, type: 'error' };
}
if (position === 'top') {
previousMessage = { ...previousMessage, type: 'error' };
}
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe(position);
});

it('has attachments', () => {
if (position === 'bottom') {
nextMessage = { ...nextMessage, attachments: [generateFileAttachment()] };
}
if (position === 'top') {
previousMessage = { ...previousMessage, attachments: [generateFileAttachment()] };
}
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe(position);
});

it('is posted by another user', () => {
const user = generateUser({ id: 'XX' });
if (position === 'bottom') {
nextMessage = { ...nextMessage, user };
}
if (position === 'top') {
previousMessage = { ...previousMessage, user };
}
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe(position);
});

it('is deleted', () => {
if (position === 'bottom') {
nextMessage = { ...nextMessage, deleted_at: new Date() };
}
if (position === 'top') {
previousMessage = { ...previousMessage, deleted_at: new Date() };
}
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe(position);
});
});

it('marks a message as bottom when the message is edited', () => {
message = { ...message, message_text_updated_at: new Date() };
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe('bottom');
});

it('marks a message as top when the previous message is edited', () => {
previousMessage = { ...previousMessage, message_text_updated_at: new Date() };
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe('top');
});

it('marks a message a top if it has reactions', () => {
message = { ...message, reaction_groups: { X: 'Y' } };
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe('top');
});

it('marks a message a bottom if next message has reactions', () => {
nextMessage = { ...nextMessage, reaction_groups: { X: 'Y' } };
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe('bottom');
});

it('marks a message as top when next message is created later than maxTimeBetweenGroupedMessages milliseconds', () => {
const maxTimeBetweenGroupedMessages = 10;
expect(
getGroupStyles(
message,
previousMessage,
nextMessage,
noGroupByUser,
maxTimeBetweenGroupedMessages,
),
).toBe('bottom');
});

it('marks a message as bottom when next message is created later than maxTimeBetweenGroupedMessages milliseconds', () => {
const maxTimeBetweenGroupedMessages = 10;
message = { ...message, created_at: new Date(12) };
nextMessage = { ...nextMessage, created_at: new Date(14) };
expect(
getGroupStyles(
message,
previousMessage,
nextMessage,
noGroupByUser,
maxTimeBetweenGroupedMessages,
),
).toBe('top');
});

it('marks a message as single when next and previous message is created later than maxTimeBetweenGroupedMessages milliseconds', () => {
const maxTimeBetweenGroupedMessages = 10;
message = { ...message, created_at: new Date(12) };
expect(
getGroupStyles(
message,
previousMessage,
nextMessage,
noGroupByUser,
maxTimeBetweenGroupedMessages,
),
).toBe('single');
});

it('marks a message as middle when next message is created earlier than maxTimeBetweenGroupedMessages milliseconds', () => {
const maxTimeBetweenGroupedMessages = 1000;
expect(
getGroupStyles(
message,
previousMessage,
nextMessage,
noGroupByUser,
maxTimeBetweenGroupedMessages,
),
).toBe('middle');
});

it('marks message as middle if not being top, neither bottom message', () => {
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe('middle');
});

it('marks message as single if not being top, neither bottom message being deleted', () => {
message = { ...message, deleted_at: new Date() };
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe('single');
});

it('marks message as single if not being top, neither bottom message being error message', () => {
message = { ...message, type: 'error' };
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe('single');
});

it('marks message at the bottom as single being deleted message', () => {
message = { ...message, deleted_at: new Date() };
nextMessage = undefined;
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe('single');
});

it('marks message at the bottom as single being error message', () => {
message = { ...message, type: 'error' };
nextMessage = undefined;
expect(getGroupStyles(message, previousMessage, nextMessage, noGroupByUser)).toBe('single');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ export const useEnrichedMessages = <
previousMessage: StreamMessage<StreamChatGenerics>,
nextMessage: StreamMessage<StreamChatGenerics>,
noGroupByUser: boolean,
maxTimeBetweenGroupedMessages?: number,
) => GroupStyle;
headerPosition?: number;
maxTimeBetweenGroupedMessages?: number;
reviewProcessedMessage?: ProcessMessagesParams<StreamChatGenerics>['reviewProcessedMessage'];
}) => {
const {
Expand All @@ -42,6 +44,7 @@ export const useEnrichedMessages = <
headerPosition,
hideDeletedMessages,
hideNewMessageSeparator,
maxTimeBetweenGroupedMessages,
messages,
noGroupByUser,
reviewProcessedMessage,
Expand Down Expand Up @@ -80,12 +83,13 @@ export const useEnrichedMessages = <
messagesWithDates[i - 1],
messagesWithDates[i + 1],
noGroupByUser,
maxTimeBetweenGroupedMessages,
);
if (style) acc[message.id] = style;
return acc;
}, {}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[messagesWithDates, noGroupByUser],
[maxTimeBetweenGroupedMessages, messagesWithDates, noGroupByUser],
);

return { messageGroupStyles, messages: messagesWithDates };
Expand Down
Loading
Loading