Skip to content
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
100 changes: 50 additions & 50 deletions ai-docs/ai-migration-v14-v15.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions ai-docs/v15-breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ export const DayDivider = ({ message }: { message: LocalMessage }) => {
| `markRead` / `MarkReadWrapperOptions` | `channel.markReadViaReporter()` (`channel.markRead()` is now the direct request: it skips the reporter and the `markReadRequest` handler). The unread UI snapshot is separate state on the paginator; there is no `updateChannelUiUnreadState` option. |
| `setChannelUnreadUiState` | `channel.messagePaginator.setUnreadSnapshot(partial)` |
| `openThread` / `closeThread` | `useWorkspaceNavigation().openThread({ channel, message })` / `.closeThread(threadId)`. Inside a thread, `useCloseThread()`. |
| `onMentionsClick` / `onMentionsHover` | the `onMentionsClick` / `onMentionsHover` props on `MessageList` / `Message` |
| `onMentionsClick` / `onMentionsHover` | the `onMentionsClick` / `onMentionsHover` props on `MessageList` / `VirtualizedMessageList` / `Message` |
| `dispatch` | none, removed (there is no React reducer) |

Custom send/update/delete/markRead request overrides are registered on `client.config` (see "Per-component request-handler props removed" in `ai-migration-v14-v15.md`).
Expand Down Expand Up @@ -1427,10 +1427,10 @@ Note: a bare `AttachmentContextProvider` above the list does not reach message a
- Old API:
- `ChannelProps.onMentionsClick?: OnMentionAction`, `ChannelProps.onMentionsHover?: OnMentionAction`
- `useChannelMentionsHandler(onMentionsHover?, onMentionsClick?)`
- Replacement: `onMentionsClick` / `onMentionsHover` on `MessageList` (drilled to each message) or on `Message`; handler type `CustomMentionHandler` `(event, message: LocalMessage)`; read mention metadata from `message` (BC-089). `VirtualizedMessageList` has no such prop in v15 (v14 virtualized lists got the handlers only from `Channel`), so a virtualized list needs a custom message UI to handle mention clicks.
- Replacement: `onMentionsClick` / `onMentionsHover` on `MessageList` or `VirtualizedMessageList` (drilled to each message, the thread's parent message and the separate giphy preview) or on `Message`; handler type `CustomMentionHandler` `(event, message: LocalMessage)`; read mention metadata from `message` (BC-089).
- Evidence:
- `release-v14:src/components/Channel/Channel.tsx:145-147`; `release-v14:src/components/Channel/index.ts:3`; `release-v14:src/components/Message/hooks/useMentionsHandler.ts:51-59` (fell back to the `Channel` handlers)
- `master:src/components/MessageList/MessageList.tsx:108-109,421-422` (`onMentionsClick`/`onMentionsHover` in `PropsDrilledToMessage`); `master:src/components/MessageList/VirtualizedMessageList.tsx:84-90` (not drilled); `master:src/components/Message/types.ts:54-56`
- `master:src/components/MessageList/MessageList.tsx:108-109,421-422` (`onMentionsClick`/`onMentionsHover` in `PropsDrilledToMessage`); `master:src/components/MessageList/VirtualizedMessageList.tsx:87-88,246-247` (same); `master:src/components/Message/types.ts:54-56`
- commit: `99cbe6704` (PR #3237)
- Docs impact:
- `02-ui-components/04-channel/01-channel.md`, `02-ui-components/04-channel/03-channel_action_context.md`, `03-ui-cookbook/04-message/03-mentions_actions.md`
Expand Down Expand Up @@ -2984,7 +2984,7 @@ const { canReply } = useMessagePermissions(message);
- Old API:
- `onMentionsClickMessage: ReactEventHandler` and `onMentionsHoverMessage: ReactEventHandler`, always set; a no-op when no handler was passed
- `useMentionsHandler(message, handlers)` returned `{ onMentionsClick, onMentionsHover }`, both always functions
- Replacement: the context fields are optional and set only when `onMentionsClick` / `onMentionsHover` was passed to `MessageList` or `Message`. `useMentionsHandler` returns each handler only when the matching one is passed to it, and `undefined` otherwise. Pass them as event props (`onClick={onMentionsClickMessage}`), or call them with `?.`. With `onMentionsClick` set, `MessageText` adds `str-chat__message-text-inner--mentions-clickable`, which gives mentions a pointer cursor.
- Replacement: the context fields are optional and set only when `onMentionsClick` / `onMentionsHover` was passed to `MessageList`, `VirtualizedMessageList` or `Message`. `useMentionsHandler` returns each handler only when the matching one is passed to it, and `undefined` otherwise. Pass them as event props (`onClick={onMentionsClickMessage}`), or call them with `?.`. With `onMentionsClick` set, `MessageText` adds `str-chat__message-text-inner--mentions-clickable`, which gives mentions a pointer cursor.
- Evidence:
- `release-v14:src/context/MessageContext.tsx:63-66`; `release-v14:src/components/Message/Message.tsx:263` `onMentionsClickMessage={onMentionsClick}`; `release-v14:src/components/Message/hooks/useMentionsHandler.ts:55-59` (`|| (() => null)`)
- `master:src/context/MessageContext.tsx:44-47`; `master:src/components/Message/hooks/useMentionsHandler.ts:13-21`, `:28-49`; `master:src/components/Message/Message.tsx:191-192`; `master:src/components/Message/MessageText.tsx:82-83`, `:121-122`, `:125` (`onClick` only when mentions are interactive)
Expand Down
8 changes: 6 additions & 2 deletions src/components/EventComponent/EventComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export type EventComponentProps = TimestampFormatterOptions & {
message: LocalMessage & {
event?: Event;
};
/**
* Renders `message.html`, when the message has one, instead of the text. It is inserted
* unescaped, so enable this only for HTML your server trusts or sanitizes.
*/
unsafeHTML?: boolean;
};

Expand All @@ -20,9 +24,9 @@ const UnMemoizedEventComponent = (props: EventComponentProps) => {
return (
<div className='str-chat__message--system' data-testid='message-system'>
<div className='str-chat__message--system__text'>
{unsafeHTML ? (
{unsafeHTML && message.html ? (
<div
dangerouslySetInnerHTML={{ __html: message.html || '' }}
dangerouslySetInnerHTML={{ __html: message.html }}
data-unsafe-inner-html
/>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ describe('EventComponent', () => {
expect(unsafeDiv.innerHTML).toBe('<strong>html event</strong>');
});

it('should render the text when unsafeHTML is true but the message has no HTML', async () => {
await renderComponent({
props: { message: { ...message, text: 'system event' }, unsafeHTML: true },
});
const systemMsg = screen.getByTestId(SYSTEM_MSG_TEST_ID);
expect(systemMsg.querySelector('[data-unsafe-inner-html]')).not.toBeInTheDocument();
expect(systemMsg).toHaveTextContent('system event');
});

it('should render system event text content (no timestamp displayed)', async () => {
await renderComponent({
props: { message: { ...message, text: 'a system message' } },
Expand Down
4 changes: 3 additions & 1 deletion src/components/Message/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ export type MessageProps = {
/** Comparator to sort reactions. Defaults to first reaction time, else reaction type. */
sortReactions?: ReactionsComparator;
/**
* Renders `message.html` instead of the Markdown text. Posting HTML is only allowed server-side.
* Renders `message.html` instead of the Markdown text. Only server-side code can set
* `message.html`. It is inserted unescaped, so enable this only for HTML your server trusts or
* sanitizes: script in it runs in the app with the current user's session.
*/
unsafeHTML?: boolean;
};
Expand Down
24 changes: 11 additions & 13 deletions src/components/MessageList/GiphyPreviewMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import React from 'react';

import { Message } from '../Message/Message';
import type { LocalMessage } from 'stream-chat';
import type { MessageProps } from '../Message/types';

export type GiphyPreviewMessageProps = {
message: LocalMessage;
};
/**
* Props of the giphy preview: the message plus the list-wide props every message in the list
* gets.
*/
export type GiphyPreviewMessageProps = MessageProps;

export const GiphyPreviewMessage = (props: GiphyPreviewMessageProps) => {
const { message } = props;

return (
<div className='giphy-preview-message'>
<Message message={message} />
</div>
);
};
export const GiphyPreviewMessage = (props: GiphyPreviewMessageProps) => (
<div className='giphy-preview-message'>
<Message {...props} />
</div>
);
28 changes: 25 additions & 3 deletions src/components/MessageList/VirtualizedMessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type {
RenderedMessage,
} from './utils';
import { getLastReceived, getMessageGroupStyles, processMessages } from './utils';
import { omitUndefinedProps } from './omitUndefinedProps';
import type { MessageProps } from '../Message';
import { UnreadMessagesNotification as DefaultUnreadMessagesNotification } from './UnreadMessagesNotification';
import {
Expand Down Expand Up @@ -84,10 +85,15 @@ import { useLastOwnMessage } from './hooks/useLastOwnMessage';
type PropsDrilledToMessage =
| 'additionalMessageComposerProps'
| 'formatDate'
| 'onMentionsClick'
| 'onMentionsHover'
| 'onUserClick'
| 'onUserHover'
| 'reactionDetailsSort'
| 'renderText'
| 'showAvatar'
| 'sortReactions';
| 'sortReactions'
| 'unsafeHTML';

type VirtualizedMessageListPropsForContext =
| PropsDrilledToMessage
Expand Down Expand Up @@ -210,6 +216,10 @@ const VirtualizedMessageListWithContext = (
hideDeletedMessages = false,
hideNewMessageSeparator = false,
maxTimeBetweenGroupedMessages,
onMentionsClick,
onMentionsHover,
onUserClick,
onUserHover,
reactionDetailsSort,
renderText,
returnAllReadData = false,
Expand All @@ -221,22 +231,29 @@ const VirtualizedMessageListWithContext = (
sortReactions,
stickToBottomScrollBehavior = 'smooth',
suppressAutoscroll: suppressAutoscrollFromProps = false,
unsafeHTML,
withDateSeparator = false,
} = props;
const thread = useThreadContext();
const isThreadList = !!thread;
// The list-wide props every message gets, the thread's parent message included.
// The list-wide props every message gets, the thread's parent message and the separate giphy
// preview included.
const messageProps: Pick<
VirtualizedMessageListProps,
PropsDrilledToMessage | 'closeReactionSelectorOnClick'
> = {
additionalMessageComposerProps,
closeReactionSelectorOnClick,
formatDate,
onMentionsClick,
onMentionsHover,
onUserClick,
onUserHover,
reactionDetailsSort,
renderText,
showAvatar,
sortReactions,
unsafeHTML,
};
const threadHead = useThreadHead(messageProps);
const [suppressAutoscrollWhileLoadingOlder, setSuppressAutoscrollWhileLoadingOlder] =
Expand Down Expand Up @@ -656,7 +673,12 @@ const VirtualizedMessageListWithContext = (
scrollToBottom={scrollToBottom}
/>
)}
{giphyPreviewMessage && <GiphyPreviewMessage message={giphyPreviewMessage} />}
{giphyPreviewMessage && (
<GiphyPreviewMessage
{...omitUndefinedProps(messageProps)}
message={giphyPreviewMessage}
/>
)}
</MessageTranslationViewProvider>
</VirtualizedMessageListContextProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Message } from '../Message';
import { useComponentContext } from '../../context';
import type { GroupStyle, RenderedMessage } from './utils';
import { getIsFirstUnreadMessage, isDateSeparatorMessage, isIntroMessage } from './utils';
import { omitUndefinedProps } from './omitUndefinedProps';
import type { VirtuosoContext } from './VirtualizedMessageList';
import type { UnknownType } from '../../types/types';
import { useThreadContext } from '../Threads';
Expand Down Expand Up @@ -121,6 +122,10 @@ export const messageRenderer = (
messageGroupStyles,
MessageSystem,
numItemsPrepended,
onMentionsClick,
onMentionsHover,
onUserClick,
onUserHover,
ownMessagesDeliveredToOthers,
ownMessagesReadByOthers,
processedMessages: messageList,
Expand All @@ -131,6 +136,7 @@ export const messageRenderer = (
sortReactions,
unreadMessageCount = 0,
UnreadMessagesSeparator,
unsafeHTML,
virtuosoRef,
} = virtuosoContext;

Expand All @@ -152,7 +158,9 @@ export const messageRenderer = (
}

if (message.type === 'system') {
return MessageSystem ? <MessageSystem message={message} /> : null;
return MessageSystem ? (
<MessageSystem {...omitUndefinedProps({ unsafeHTML })} message={message} />
) : null;
}

const isFirstUnreadMessage = getIsFirstUnreadMessage({
Expand Down Expand Up @@ -184,12 +192,17 @@ export const messageRenderer = (
lastOwnMessage={lastOwnMessage}
lastReceivedId={lastReceivedMessageId}
message={message}
onMentionsClick={onMentionsClick}
onMentionsHover={onMentionsHover}
onUserClick={onUserClick}
onUserHover={onUserHover}
reactionDetailsSort={reactionDetailsSort}
readBy={ownMessagesReadByOthers[message.id] || []}
renderText={renderText}
returnAllReadData={returnAllReadData}
showAvatar={showAvatar}
sortReactions={sortReactions}
unsafeHTML={unsafeHTML}
/>
</>
);
Expand Down
22 changes: 22 additions & 0 deletions src/components/MessageList/__tests__/MessageList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { MESSAGE_LIST_MAIN_PANEL_CLASS } from '../MessageListMainPanel';
import { ThreadProvider } from '../../Threads';
import { useChannel, useMessageContext, WithComponents } from '../../../context';
import { EmptyStateIndicator as EmptyStateIndicatorMock } from '../../EmptyStateIndicator';
import { EventComponent } from '../../EventComponent';
import type { EventComponentProps } from '../../EventComponent';
import { mockedApiResponse } from '../../../mock-builders/api/utils';
import { nanoid } from 'nanoid';
import { StateStore } from '@stream-io/state-store';
Expand Down Expand Up @@ -644,6 +646,26 @@ describe('MessageList', () => {
});
});

it("should keep a MessageSystem override's own unsafeHTML when the list doesn't set it", async () => {
const MessageSystem = (props: EventComponentProps) => (
<EventComponent unsafeHTML {...props} />
);
const system = generateMessage({
html: '<span data-testid="custom-html" />',
text: 'system text',
type: 'system',
});

renderComponent({
channelProps: { channel },
chatClient,
components: { MessageSystem },
msgListProps: { messages: [system] },
});

expect(await screen.findByTestId('custom-html')).toBeInTheDocument();
});

it('should give the system message list item the str-chat__li class', async () => {
const system = generateMessage({
text: 'system message in a list item',
Expand Down
Loading
Loading