stream-chat-react 13.11.0 → 13.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/components/Attachment/Audio.js +38 -15
  2. package/dist/components/Attachment/Card.js +33 -11
  3. package/dist/components/Attachment/Geolocation.js +1 -1
  4. package/dist/components/Attachment/VoiceRecording.js +45 -20
  5. package/dist/components/Attachment/components/PlayButton.js +1 -1
  6. package/dist/components/Attachment/components/PlaybackRateButton.js +1 -1
  7. package/dist/components/Attachment/hooks/useAudioController.d.ts +1 -0
  8. package/dist/components/Attachment/hooks/useAudioController.js +1 -0
  9. package/dist/components/Attachment/index.d.ts +1 -0
  10. package/dist/components/Attachment/index.js +1 -0
  11. package/dist/components/AudioPlayback/AudioPlayer.d.ts +116 -0
  12. package/dist/components/AudioPlayback/AudioPlayer.js +456 -0
  13. package/dist/components/AudioPlayback/AudioPlayerPool.d.ts +49 -0
  14. package/dist/components/AudioPlayback/AudioPlayerPool.js +156 -0
  15. package/dist/components/AudioPlayback/WithAudioPlayback.d.ts +24 -0
  16. package/dist/components/AudioPlayback/WithAudioPlayback.js +57 -0
  17. package/dist/components/AudioPlayback/index.d.ts +3 -0
  18. package/dist/components/AudioPlayback/index.js +3 -0
  19. package/dist/components/AudioPlayback/plugins/AudioPlayerNotificationsPlugin.d.ts +7 -0
  20. package/dist/components/AudioPlayback/plugins/AudioPlayerNotificationsPlugin.js +25 -0
  21. package/dist/components/AudioPlayback/plugins/AudioPlayerPlugin.d.ts +10 -0
  22. package/dist/components/AudioPlayback/plugins/AudioPlayerPlugin.js +1 -0
  23. package/dist/components/AudioPlayback/plugins/index.d.ts +1 -0
  24. package/dist/components/AudioPlayback/plugins/index.js +1 -0
  25. package/dist/components/Channel/Channel.d.ts +2 -0
  26. package/dist/components/Channel/Channel.js +4 -2
  27. package/dist/components/Chat/hooks/useChat.js +1 -1
  28. package/dist/components/MediaRecorder/AudioRecorder/AudioRecordingPreview.d.ts +3 -2
  29. package/dist/components/MediaRecorder/AudioRecorder/AudioRecordingPreview.js +23 -8
  30. package/dist/components/MessageInput/AttachmentPreviewList/FileAttachmentPreview.js +1 -1
  31. package/dist/components/MessageInput/AttachmentPreviewList/GeolocationPreview.js +1 -1
  32. package/dist/components/MessageInput/AttachmentPreviewList/ImageAttachmentPreview.js +1 -1
  33. package/dist/components/MessageInput/AttachmentPreviewList/UnsupportedAttachmentPreview.js +1 -1
  34. package/dist/components/MessageInput/AttachmentPreviewList/VoiceRecordingPreview.d.ts +1 -1
  35. package/dist/components/MessageInput/AttachmentPreviewList/VoiceRecordingPreview.js +20 -7
  36. package/dist/components/MessageList/MessageList.js +8 -5
  37. package/dist/components/MessageList/renderMessages.js +6 -6
  38. package/dist/components/index.d.ts +2 -1
  39. package/dist/components/index.js +2 -1
  40. package/dist/context/ComponentContext.d.ts +4 -0
  41. package/dist/context/MessageListContext.d.ts +3 -0
  42. package/dist/experimental/index.browser.cjs.map +2 -2
  43. package/dist/experimental/index.node.cjs.map +2 -2
  44. package/dist/i18n/TranslationBuilder/notifications/NotificationTranslationTopic.js +2 -0
  45. package/dist/i18n/TranslationBuilder/notifications/browserAudioPlaybackError.d.ts +3 -0
  46. package/dist/i18n/TranslationBuilder/notifications/browserAudioPlaybackError.js +1 -0
  47. package/dist/index.browser.cjs +2833 -2089
  48. package/dist/index.browser.cjs.map +4 -4
  49. package/dist/index.node.cjs +2842 -2089
  50. package/dist/index.node.cjs.map +4 -4
  51. package/package.json +1 -1
@@ -0,0 +1,57 @@
1
+ import React, { useContext, useState } from 'react';
2
+ import { useEffect } from 'react';
3
+ import { AudioPlayerPool } from './AudioPlayerPool';
4
+ import { audioPlayerNotificationsPluginFactory } from './plugins/AudioPlayerNotificationsPlugin';
5
+ import { useChatContext, useTranslationContext } from '../../context';
6
+ import { useStateStore } from '../../store';
7
+ const AudioPlayerContext = React.createContext({
8
+ audioPlayers: null,
9
+ });
10
+ export const WithAudioPlayback = ({ allowConcurrentPlayback, children, }) => {
11
+ const [audioPlayers] = useState(() => new AudioPlayerPool({ allowConcurrentPlayback }));
12
+ useEffect(() => () => {
13
+ audioPlayers.clear();
14
+ }, [audioPlayers]);
15
+ return (React.createElement(AudioPlayerContext.Provider, { value: { audioPlayers } }, children));
16
+ };
17
+ const makeAudioPlayerId = ({ requester, src }) => `${requester ?? 'requester-unknown'}:${src}`;
18
+ export const useAudioPlayer = ({ durationSeconds, fileSize, mimeType, playbackRates, plugins, requester = '', src, title, waveformData, }) => {
19
+ const { client } = useChatContext();
20
+ const { t } = useTranslationContext();
21
+ const { audioPlayers } = useContext(AudioPlayerContext);
22
+ const audioPlayer = src && audioPlayers
23
+ ? audioPlayers.getOrAdd({
24
+ durationSeconds,
25
+ fileSize,
26
+ id: makeAudioPlayerId({ requester, src }),
27
+ mimeType,
28
+ playbackRates,
29
+ plugins,
30
+ src,
31
+ title,
32
+ waveformData,
33
+ })
34
+ : undefined;
35
+ useEffect(() => {
36
+ if (!audioPlayer)
37
+ return;
38
+ /**
39
+ * Avoid having to pass client and translation function to AudioPlayer instances
40
+ * and instead provide plugin that takes care of translated notifications.
41
+ */
42
+ const notificationsPlugin = audioPlayerNotificationsPluginFactory({ client, t });
43
+ audioPlayer.setPlugins((currentPlugins) => [
44
+ ...currentPlugins.filter((plugin) => plugin.id !== notificationsPlugin.id),
45
+ notificationsPlugin,
46
+ ]);
47
+ }, [audioPlayer, client, t]);
48
+ return audioPlayer;
49
+ };
50
+ const activeAudioPlayerSelector = ({ activeAudioPlayer }) => ({
51
+ activeAudioPlayer,
52
+ });
53
+ export const useActiveAudioPlayer = () => {
54
+ const { audioPlayers } = useContext(AudioPlayerContext);
55
+ const { activeAudioPlayer } = useStateStore(audioPlayers?.state, activeAudioPlayerSelector) ?? {};
56
+ return activeAudioPlayer;
57
+ };
@@ -0,0 +1,3 @@
1
+ export * from './AudioPlayer';
2
+ export * from './plugins';
3
+ export { useActiveAudioPlayer, useAudioPlayer, type UseAudioPlayerProps, type WithAudioPlaybackProps, WithAudioPlayback, } from './WithAudioPlayback';
@@ -0,0 +1,3 @@
1
+ export * from './AudioPlayer';
2
+ export * from './plugins';
3
+ export { useActiveAudioPlayer, useAudioPlayer, WithAudioPlayback, } from './WithAudioPlayback';
@@ -0,0 +1,7 @@
1
+ import type { AudioPlayerPlugin } from './AudioPlayerPlugin';
2
+ import type { StreamChat } from 'stream-chat';
3
+ import type { TFunction } from 'i18next';
4
+ export declare const audioPlayerNotificationsPluginFactory: ({ client, t, }: {
5
+ client: StreamChat;
6
+ t: TFunction;
7
+ }) => AudioPlayerPlugin;
@@ -0,0 +1,25 @@
1
+ export const audioPlayerNotificationsPluginFactory = ({ client, t, }) => {
2
+ const errors = {
3
+ 'failed-to-start': new Error(t('Failed to play the recording')),
4
+ 'not-playable': new Error(t('Recording format is not supported and cannot be reproduced')),
5
+ 'seek-not-supported': new Error(t('Cannot seek in the recording')),
6
+ };
7
+ return {
8
+ id: 'AudioPlayerNotificationsPlugin',
9
+ onError: ({ errCode, error: e }) => {
10
+ const error = (errCode && errors[errCode]) ??
11
+ e ??
12
+ new Error(t('Error reproducing the recording'));
13
+ client?.notifications.addError({
14
+ message: error.message,
15
+ options: {
16
+ originalError: error,
17
+ type: 'browser:audio:playback:error',
18
+ },
19
+ origin: {
20
+ emitter: 'AudioPlayer',
21
+ },
22
+ });
23
+ },
24
+ };
25
+ };
@@ -0,0 +1,10 @@
1
+ import type { AudioPlayer, RegisterAudioPlayerErrorParams } from '../AudioPlayer';
2
+ export type AudioPlayerPluginContext = {
3
+ player: AudioPlayer;
4
+ };
5
+ export type AudioPlayerPlugin = {
6
+ id: string;
7
+ onInit?(ctx: AudioPlayerPluginContext): void;
8
+ onError?(ctx: AudioPlayerPluginContext & RegisterAudioPlayerErrorParams): void;
9
+ onRemove?(ctx: AudioPlayerPluginContext): void;
10
+ };
@@ -0,0 +1 @@
1
+ export * from './AudioPlayerPlugin';
@@ -0,0 +1 @@
1
+ export * from './AudioPlayerPlugin';
@@ -9,6 +9,8 @@ type ChannelPropsForwardedToComponentContext = Pick<ComponentContextValue, 'Atta
9
9
  export type ChannelProps = ChannelPropsForwardedToComponentContext & {
10
10
  /** Custom handler function that runs when the active channel has unread messages and the app is running on a separate browser tab */
11
11
  activeUnreadHandler?: (unread: number, documentTitle: string) => void;
12
+ /** Allows multiple audio players to play the audio at the same time. Disabled by default. */
13
+ allowConcurrentAudioPlayback?: boolean;
12
14
  /** The connected and active channel */
13
15
  channel?: StreamChannel;
14
16
  /**
@@ -21,6 +21,7 @@ import { useThreadContext } from '../Threads';
21
21
  import { getChannel } from '../../utils';
22
22
  import { getImageAttachmentConfiguration, getVideoAttachmentConfiguration, } from '../Attachment/attachment-sizing';
23
23
  import { useSearchFocusedMessage } from '../../experimental/Search/hooks';
24
+ import { WithAudioPlayback } from '../AudioPlayback';
24
25
  const ChannelContainer = ({ children, className: additionalClassName, ...props }) => {
25
26
  const { customClasses, theme } = useChatContext('Channel');
26
27
  const { channelClass, chatClass } = useChannelContainerClasses({
@@ -47,7 +48,7 @@ const UnMemoizedChannel = (props) => {
47
48
  return React.createElement(ChannelInner, { ...props, channel: channel, key: channel.cid });
48
49
  };
49
50
  const ChannelInner = (props) => {
50
- const { activeUnreadHandler, channel, channelQueryOptions: propChannelQueryOptions, children, doDeleteMessageRequest, doMarkReadRequest, doSendMessageRequest, doUpdateMessageRequest, initializeOnMount = true, LoadingErrorIndicator = DefaultLoadingErrorIndicator, LoadingIndicator = DefaultLoadingIndicator, markReadOnMount = true, onMentionsClick, onMentionsHover, skipMessageDataMemoization, } = props;
51
+ const { activeUnreadHandler, allowConcurrentAudioPlayback, channel, channelQueryOptions: propChannelQueryOptions, children, doDeleteMessageRequest, doMarkReadRequest, doSendMessageRequest, doUpdateMessageRequest, initializeOnMount = true, LoadingErrorIndicator = DefaultLoadingErrorIndicator, LoadingIndicator = DefaultLoadingIndicator, markReadOnMount = true, onMentionsClick, onMentionsHover, skipMessageDataMemoization, } = props;
51
52
  const channelQueryOptions = useMemo(() => defaultsDeep(propChannelQueryOptions, {
52
53
  messages: { limit: DEFAULT_INITIAL_CHANNEL_PAGE_SIZE },
53
54
  }), [propChannelQueryOptions]);
@@ -867,7 +868,8 @@ const ChannelInner = (props) => {
867
868
  React.createElement(ChannelActionProvider, { value: channelActionContextValue },
868
869
  React.createElement(WithComponents, { overrides: componentContextValue },
869
870
  React.createElement(TypingProvider, { value: typingContextValue },
870
- React.createElement("div", { className: clsx(chatContainerClass) }, children)))))));
871
+ React.createElement(WithAudioPlayback, { allowConcurrentPlayback: allowConcurrentAudioPlayback },
872
+ React.createElement("div", { className: clsx(chatContainerClass) }, children))))))));
871
873
  };
872
874
  /**
873
875
  * A wrapper component that provides channel data and renders children.
@@ -24,7 +24,7 @@ export const useChat = ({ client, defaultLanguage = 'en', i18nInstance, initialN
24
24
  useEffect(() => {
25
25
  if (!client)
26
26
  return;
27
- const version = "13.11.0";
27
+ const version = "13.13.0";
28
28
  const userAgent = client.getUserAgent();
29
29
  if (!userAgent.includes('stream-chat-react')) {
30
30
  // result looks like: 'stream-chat-react-2.3.2-stream-chat-javascript-client-browser-2.2.2'
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
- export type AudioRecordingPlayerProps = React.ComponentProps<'audio'> & {
2
+ export type AudioRecordingPlayerProps = {
3
3
  durationSeconds: number;
4
4
  mimeType?: string;
5
+ src?: string;
5
6
  waveformData?: number[];
6
7
  };
7
- export declare const AudioRecordingPreview: ({ durationSeconds, mimeType, waveformData, ...props }: AudioRecordingPlayerProps) => React.JSX.Element;
8
+ export declare const AudioRecordingPreview: ({ durationSeconds, mimeType, src, waveformData, }: AudioRecordingPlayerProps) => React.JSX.Element | undefined;
@@ -1,19 +1,34 @@
1
- import React from 'react';
1
+ import React, { useEffect } from 'react';
2
2
  import { PauseIcon, PlayIcon } from '../../MessageInput/icons';
3
3
  import { RecordingTimer } from './RecordingTimer';
4
- import { useAudioController } from '../../Attachment/hooks/useAudioController';
5
4
  import { WaveProgressBar } from '../../Attachment';
6
- export const AudioRecordingPreview = ({ durationSeconds, mimeType, waveformData, ...props }) => {
7
- const { audioRef, isPlaying, progress, secondsElapsed, seek, togglePlay } = useAudioController({
5
+ import { useAudioPlayer } from '../../AudioPlayback';
6
+ import { useStateStore } from '../../../store';
7
+ const audioPlayerStateSelector = (state) => ({
8
+ isPlaying: state.isPlaying,
9
+ progress: state.progressPercent,
10
+ secondsElapsed: state.secondsElapsed,
11
+ });
12
+ export const AudioRecordingPreview = ({ durationSeconds, mimeType, src, waveformData, }) => {
13
+ const audioPlayer = useAudioPlayer({
8
14
  durationSeconds,
9
15
  mimeType,
16
+ src,
17
+ waveformData,
10
18
  });
19
+ const { isPlaying, progress, secondsElapsed } = useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {};
11
20
  const displayedDuration = secondsElapsed || durationSeconds;
21
+ useEffect(() => {
22
+ audioPlayer?.cancelScheduledRemoval();
23
+ return () => {
24
+ audioPlayer?.scheduleRemoval();
25
+ };
26
+ }, [audioPlayer]);
27
+ if (!audioPlayer)
28
+ return;
12
29
  return (React.createElement(React.Fragment, null,
13
- React.createElement("audio", { ref: audioRef },
14
- React.createElement("source", { src: props.src, type: mimeType })),
15
- React.createElement("button", { className: 'str-chat__audio_recorder__toggle-playback-button', "data-testid": 'audio-recording-preview-toggle-play-btn', onClick: togglePlay }, isPlaying ? React.createElement(PauseIcon, null) : React.createElement(PlayIcon, null)),
30
+ React.createElement("button", { className: 'str-chat__audio_recorder__toggle-playback-button', "data-testid": 'audio-recording-preview-toggle-play-btn', onClick: audioPlayer.togglePlay }, isPlaying ? React.createElement(PauseIcon, null) : React.createElement(PlayIcon, null)),
16
31
  React.createElement(RecordingTimer, { durationSeconds: displayedDuration }),
17
32
  React.createElement("div", { className: 'str-chat__wave-progress-bar__track-container' },
18
- React.createElement(WaveProgressBar, { progress: progress, seek: seek, waveformData: waveformData || [] }))));
33
+ React.createElement(WaveProgressBar, { progress: progress, seek: audioPlayer.seek, waveformData: waveformData || [] }))));
19
34
  };
@@ -9,7 +9,7 @@ export const FileAttachmentPreview = ({ attachment, handleRetry, removeAttachmen
9
9
  React.createElement("div", { className: 'str-chat__attachment-preview-file-icon' },
10
10
  React.createElement(FileIcon, { filename: attachment.title, mimeType: attachment.mime_type })),
11
11
  React.createElement("button", { "aria-label": t('aria/Remove attachment'), className: 'str-chat__attachment-preview-delete', "data-testid": 'file-preview-item-delete-button', disabled: uploadState === 'uploading', onClick: () => attachment.localMetadata?.id &&
12
- removeAttachments([attachment.localMetadata?.id]) },
12
+ removeAttachments([attachment.localMetadata?.id]), type: 'button' },
13
13
  React.createElement(CloseIcon, null)),
14
14
  ['blocked', 'failed'].includes(uploadState) && !!handleRetry && (React.createElement("button", { className: 'str-chat__attachment-preview-error str-chat__attachment-preview-error-file', "data-testid": 'file-preview-item-retry-button', onClick: () => {
15
15
  handleRetry(attachment);
@@ -8,7 +8,7 @@ export const GeolocationPreview = ({ location, PreviewImage = GeolocationPreview
8
8
  const { t } = useTranslationContext();
9
9
  return (React.createElement("div", { className: 'str-chat__location-preview', "data-testid": 'location-preview' },
10
10
  React.createElement(PreviewImage, { location: location }),
11
- remove && (React.createElement("button", { "aria-label": t('aria/Remove location attachment'), className: 'str-chat__attachment-preview-delete', "data-testid": 'location-preview-item-delete-button', onClick: remove },
11
+ remove && (React.createElement("button", { "aria-label": t('aria/Remove location attachment'), className: 'str-chat__attachment-preview-delete', "data-testid": 'location-preview-item-delete-button', onClick: remove, type: 'button' },
12
12
  React.createElement(CloseIcon, null))),
13
13
  React.createElement("div", { className: 'str-chat__attachment-preview-metadata' }, location.durationMs ? (React.createElement(React.Fragment, null,
14
14
  React.createElement("div", { className: 'str-chat__attachment-preview-title', title: t('Shared live location') }, t('Live location')),
@@ -13,7 +13,7 @@ export const ImageAttachmentPreview = ({ attachment, handleRetry, removeAttachme
13
13
  return (React.createElement("div", { className: clsx('str-chat__attachment-preview-image', {
14
14
  'str-chat__attachment-preview-image--error': previewError,
15
15
  }), "data-testid": 'attachment-preview-image' },
16
- React.createElement("button", { "aria-label": t('aria/Remove attachment'), className: 'str-chat__attachment-preview-delete', "data-testid": 'image-preview-item-delete-button', disabled: uploadState === 'uploading', onClick: () => id && removeAttachments([id]) },
16
+ React.createElement("button", { "aria-label": t('aria/Remove attachment'), className: 'str-chat__attachment-preview-delete', "data-testid": 'image-preview-item-delete-button', disabled: uploadState === 'uploading', onClick: () => id && removeAttachments([id]), type: 'button' },
17
17
  React.createElement(CloseIcon, null)),
18
18
  ['blocked', 'failed'].includes(uploadState) && (React.createElement("button", { className: 'str-chat__attachment-preview-error str-chat__attachment-preview-error-image', "data-testid": 'image-preview-item-retry-button', onClick: () => handleRetry(attachment) },
19
19
  React.createElement(RetryIcon, null))),
@@ -10,7 +10,7 @@ export const UnsupportedAttachmentPreview = ({ attachment, handleRetry, removeAt
10
10
  React.createElement("div", { className: 'str-chat__attachment-preview-file-icon' },
11
11
  React.createElement(FileIcon, { filename: title, mimeType: attachment.mime_type })),
12
12
  React.createElement("button", { "aria-label": t('aria/Remove attachment'), className: 'str-chat__attachment-preview-delete', "data-testid": 'file-preview-item-delete-button', disabled: attachment.localMetadata?.uploadState === 'uploading', onClick: () => attachment.localMetadata?.id &&
13
- removeAttachments([attachment.localMetadata?.id]) },
13
+ removeAttachments([attachment.localMetadata?.id]), type: 'button' },
14
14
  React.createElement(CloseIcon, null)),
15
15
  isLocalUploadAttachment(attachment) &&
16
16
  ['blocked', 'failed'].includes(attachment.localMetadata?.uploadState) &&
@@ -2,4 +2,4 @@ import React from 'react';
2
2
  import type { LocalVoiceRecordingAttachment } from 'stream-chat';
3
3
  import type { UploadAttachmentPreviewProps } from './types';
4
4
  export type VoiceRecordingPreviewProps<CustomLocalMetadata = Record<string, unknown>> = UploadAttachmentPreviewProps<LocalVoiceRecordingAttachment<CustomLocalMetadata>>;
5
- export declare const VoiceRecordingPreview: ({ attachment, handleRetry, removeAttachments, }: VoiceRecordingPreviewProps) => React.JSX.Element;
5
+ export declare const VoiceRecordingPreview: ({ attachment, handleRetry, removeAttachments, }: VoiceRecordingPreviewProps) => React.JSX.Element | null;
@@ -1,20 +1,33 @@
1
- import React from 'react';
1
+ import React, { useEffect } from 'react';
2
2
  import { PlayButton } from '../../Attachment';
3
3
  import { RecordingTimer } from '../../MediaRecorder';
4
4
  import { CloseIcon, LoadingIndicatorIcon, RetryIcon } from '../icons';
5
5
  import { FileIcon } from '../../ReactFileUtilities';
6
- import { useAudioController } from '../../Attachment/hooks/useAudioController';
7
6
  import { useTranslationContext } from '../../../context';
7
+ import { useAudioPlayer } from '../../AudioPlayback';
8
+ import { useStateStore } from '../../../store';
9
+ const audioPlayerStateSelector = (state) => ({
10
+ isPlaying: state.isPlaying,
11
+ secondsElapsed: state.secondsElapsed,
12
+ });
8
13
  export const VoiceRecordingPreview = ({ attachment, handleRetry, removeAttachments, }) => {
9
14
  const { t } = useTranslationContext();
10
- const { audioRef, isPlaying, secondsElapsed, togglePlay } = useAudioController({
15
+ const audioPlayer = useAudioPlayer({
11
16
  mimeType: attachment.mime_type,
17
+ src: attachment.asset_url,
12
18
  });
19
+ const { isPlaying, secondsElapsed } = useStateStore(audioPlayer?.state, audioPlayerStateSelector) ?? {};
20
+ useEffect(() => {
21
+ audioPlayer?.cancelScheduledRemoval();
22
+ return () => {
23
+ audioPlayer?.scheduleRemoval();
24
+ };
25
+ }, [audioPlayer]);
26
+ if (!audioPlayer)
27
+ return null;
13
28
  return (React.createElement("div", { className: 'str-chat__attachment-preview-voice-recording', "data-testid": 'attachment-preview-voice-recording' },
14
- React.createElement("audio", { ref: audioRef },
15
- React.createElement("source", { "data-testid": 'audio-source', src: attachment.asset_url, type: attachment.mime_type })),
16
- React.createElement(PlayButton, { isPlaying: isPlaying, onClick: togglePlay }),
17
- React.createElement("button", { "aria-label": t('aria/Remove attachment'), className: 'str-chat__attachment-preview-delete', "data-testid": 'file-preview-item-delete-button', disabled: attachment.localMetadata?.uploadState === 'uploading', onClick: () => attachment.localMetadata?.id && removeAttachments([attachment.localMetadata.id]) },
29
+ React.createElement(PlayButton, { isPlaying: !!isPlaying, onClick: audioPlayer.togglePlay }),
30
+ React.createElement("button", { "aria-label": t('aria/Remove attachment'), className: 'str-chat__attachment-preview-delete', "data-testid": 'file-preview-item-delete-button', disabled: attachment.localMetadata?.uploadState === 'uploading', onClick: () => attachment.localMetadata?.id && removeAttachments([attachment.localMetadata.id]), type: 'button' },
18
31
  React.createElement(CloseIcon, null)),
19
32
  ['blocked', 'failed'].includes(attachment.localMetadata?.uploadState) &&
20
33
  !!handleRetry && (React.createElement("button", { className: 'str-chat__attachment-preview-error str-chat__attachment-preview-error-file', "data-testid": 'file-preview-item-retry-button', onClick: () => handleRetry(attachment) },
@@ -25,9 +25,8 @@ const MessageListWithContext = (props) => {
25
25
  const { channel, channelUnreadUiState, disableDateSeparator = false, groupStyles, hasMoreNewer = false, headerPosition, hideDeletedMessages = false, hideNewMessageSeparator = false, highlightedMessageId, internalInfiniteScrollProps: { threshold: loadMoreScrollThreshold = DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD, ...restInternalInfiniteScrollProps } = {}, jumpToLatestMessage = () => Promise.resolve(), loadMore: loadMoreCallback, loadMoreNewer: loadMoreNewerCallback, // @deprecated in favor of `channelCapabilities` - TODO: remove in next major release
26
26
  maxTimeBetweenGroupedMessages, messageActions = Object.keys(MESSAGE_ACTIONS), messageLimit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE, messages = [], noGroupByUser = false, notifications, pinPermissions = defaultPinPermissions, reactionDetailsSort, renderMessages = defaultRenderMessages, returnAllReadData = false, reviewProcessedMessage, showUnreadNotificationAlways, sortReactionDetails, sortReactions, suppressAutoscroll, threadList = false, unsafeHTML = false, } = props;
27
27
  const [listElement, setListElement] = React.useState(null);
28
- const [ulElement, setUlElement] = React.useState(null);
29
28
  const { customClasses } = useChatContext('MessageList');
30
- const { EmptyStateIndicator = DefaultEmptyStateIndicator, LoadingIndicator = DefaultLoadingIndicator, MessageListMainPanel = DefaultMessageListMainPanel, MessageListNotifications = DefaultMessageListNotifications, MessageNotification = DefaultMessageNotification, TypingIndicator = DefaultTypingIndicator, UnreadMessagesNotification = DefaultUnreadMessagesNotification, } = useComponentContext('MessageList');
29
+ const { EmptyStateIndicator = DefaultEmptyStateIndicator, LoadingIndicator = DefaultLoadingIndicator, MessageListMainPanel = DefaultMessageListMainPanel, MessageListNotifications = DefaultMessageListNotifications, MessageListWrapper = 'ul', MessageNotification = DefaultMessageNotification, TypingIndicator = DefaultTypingIndicator, UnreadMessagesNotification = DefaultUnreadMessagesNotification, } = useComponentContext('MessageList');
31
30
  const { hasNewMessages, isMessageListScrolledToBottom, onScroll, scrollToBottom, wrapperRect, } = useScrollLocationLogic({
32
31
  hasMoreNewer,
33
32
  listElement,
@@ -125,7 +124,7 @@ const MessageListWithContext = (props) => {
125
124
  }, [scrollToBottom, hasMoreNewer]);
126
125
  React.useLayoutEffect(() => {
127
126
  if (highlightedMessageId) {
128
- const element = ulElement?.querySelector(`[data-message-id='${highlightedMessageId}']`);
127
+ const element = listElement?.querySelector(`[data-message-id='${highlightedMessageId}']`);
129
128
  element?.scrollIntoView({ block: 'center' });
130
129
  }
131
130
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -135,12 +134,16 @@ const MessageListWithContext = (props) => {
135
134
  const dialogManagerId = threadList
136
135
  ? `message-list-dialog-manager-thread-${id}`
137
136
  : `message-list-dialog-manager-${id}`;
138
- return (React.createElement(MessageListContextProvider, { value: { listElement, scrollToBottom } },
137
+ return (React.createElement(MessageListContextProvider, { value: {
138
+ listElement,
139
+ processedMessages: enrichedMessages,
140
+ scrollToBottom,
141
+ } },
139
142
  React.createElement(MessageListMainPanel, null,
140
143
  React.createElement(DialogManagerProvider, { id: dialogManagerId },
141
144
  !threadList && showUnreadMessagesNotification && (React.createElement(UnreadMessagesNotification, { unreadCount: channelUnreadUiState?.unread_messages })),
142
145
  React.createElement("div", { className: clsx(messageListClass, customClasses?.threadList), onScroll: onScroll, ref: setListElement, tabIndex: 0 }, showEmptyStateIndicator ? (React.createElement(EmptyStateIndicator, { listType: threadList ? 'thread' : 'message' })) : (React.createElement(InfiniteScroll, { className: 'str-chat__message-list-scroll', "data-testid": 'reverse-infinite-scroll', hasNextPage: props.hasMoreNewer, hasPreviousPage: props.hasMore, head: props.head, isLoading: props.loadingMore, loader: React.createElement("div", { className: 'str-chat__list__loading', key: 'loading-indicator' }, props.loadingMore && React.createElement(LoadingIndicator, { size: 20 })), loadNextPage: loadMoreNewer, loadPreviousPage: loadMore, threshold: loadMoreScrollThreshold, ...restInternalInfiniteScrollProps },
143
- React.createElement("ul", { className: 'str-chat__ul', ref: setUlElement }, elements),
146
+ React.createElement(MessageListWrapper, { className: 'str-chat__ul' }, elements),
144
147
  React.createElement(TypingIndicator, { threadList: threadList }),
145
148
  React.createElement("div", { key: 'bottom' })))))),
146
149
  React.createElement(MessageListNotifications, { hasNewMessages: hasNewMessages, isMessageListScrolledToBottom: isMessageListScrolledToBottom, isNotAtLatestMessageSet: hasMoreNewer, MessageNotification: MessageNotification, notifications: notifications, scrollToBottom: scrollToBottomFromNotification, threadList: threadList, unreadCount: threadList ? undefined : channelUnreadUiState?.unread_messages })));
@@ -5,24 +5,24 @@ import { DateSeparator as DefaultDateSeparator } from '../DateSeparator';
5
5
  import { EventComponent as DefaultMessageSystem } from '../EventComponent';
6
6
  import { UnreadMessagesSeparator as DefaultUnreadMessagesSeparator } from './UnreadMessagesSeparator';
7
7
  export function defaultRenderMessages({ channelUnreadUiState, components, customClasses, lastOwnMessage, lastReceivedMessageId: lastReceivedId, messageGroupStyles, messages, ownMessagesDeliveredToOthers, readData, sharedMessageProps: messageProps, }) {
8
- const { DateSeparator = DefaultDateSeparator, HeaderComponent, MessageSystem = DefaultMessageSystem, UnreadMessagesSeparator = DefaultUnreadMessagesSeparator, } = components;
8
+ const { DateSeparator = DefaultDateSeparator, HeaderComponent, MessageListItem = 'li', MessageSystem = DefaultMessageSystem, UnreadMessagesSeparator = DefaultUnreadMessagesSeparator, } = components;
9
9
  const renderedMessages = [];
10
10
  let firstMessage;
11
11
  let previousMessage = undefined;
12
12
  for (let index = 0; index < messages.length; index++) {
13
13
  const message = messages[index];
14
14
  if (isDateSeparatorMessage(message)) {
15
- renderedMessages.push(React.createElement("li", { key: `${message.date.toISOString()}-i` },
15
+ renderedMessages.push(React.createElement(MessageListItem, { "data-index": index, key: `${message.date.toISOString()}-i` },
16
16
  React.createElement(DateSeparator, { date: message.date, formatDate: messageProps.formatDate, unread: message.unread })));
17
17
  }
18
18
  else if (isIntroMessage(message)) {
19
19
  if (HeaderComponent) {
20
- renderedMessages.push(React.createElement("li", { key: 'intro' },
20
+ renderedMessages.push(React.createElement(MessageListItem, { "data-index": index, key: 'intro' },
21
21
  React.createElement(HeaderComponent, null)));
22
22
  }
23
23
  }
24
24
  else if (message.type === 'system') {
25
- renderedMessages.push(React.createElement("li", { "data-message-id": message.id, key: message.id || message.created_at.toISOString() },
25
+ renderedMessages.push(React.createElement(MessageListItem, { "data-index": index, "data-message-id": message.id, key: message.id || message.created_at.toISOString() },
26
26
  React.createElement(MessageSystem, { message: message })));
27
27
  }
28
28
  else {
@@ -41,9 +41,9 @@ export function defaultRenderMessages({ channelUnreadUiState, components, custom
41
41
  unreadMessageCount: channelUnreadUiState?.unread_messages,
42
42
  });
43
43
  renderedMessages.push(React.createElement(Fragment, { key: message.id || message.created_at.toISOString() },
44
- isFirstUnreadMessage && UnreadMessagesSeparator && (React.createElement("li", { className: 'str-chat__li str-chat__unread-messages-separator-wrapper' },
44
+ isFirstUnreadMessage && UnreadMessagesSeparator && (React.createElement(MessageListItem, { className: 'str-chat__li str-chat__unread-messages-separator-wrapper' },
45
45
  React.createElement(UnreadMessagesSeparator, { unreadCount: channelUnreadUiState?.unread_messages }))),
46
- React.createElement("li", { className: messageClass, "data-message-id": message.id, "data-testid": messageClass },
46
+ React.createElement(MessageListItem, { className: messageClass, "data-index": index, "data-message-id": message.id, "data-testid": messageClass },
47
47
  React.createElement(Message, { deliveredTo: ownMessagesDeliveredToOthers[message.id] || [], groupStyles: [groupStyles], lastOwnMessage: lastOwnMessage, lastReceivedId: lastReceivedId, message: message, readBy: readData[message.id] || [], ...messageProps }))));
48
48
  previousMessage = message;
49
49
  }
@@ -1,4 +1,6 @@
1
+ export * from './AIStateIndicator';
1
2
  export * from './Attachment';
3
+ export * from './AudioPlayback';
2
4
  export * from './Avatar';
3
5
  export * from './Channel';
4
6
  export * from './ChannelHeader';
@@ -33,6 +35,5 @@ export * from './TypingIndicator';
33
35
  export * from './Window';
34
36
  export * from './Threads';
35
37
  export * from './ChatView';
36
- export * from './AIStateIndicator';
37
38
  export { UploadButton } from './ReactFileUtilities';
38
39
  export type { UploadButtonProps } from './ReactFileUtilities';
@@ -1,4 +1,6 @@
1
+ export * from './AIStateIndicator';
1
2
  export * from './Attachment';
3
+ export * from './AudioPlayback';
2
4
  export * from './Avatar';
3
5
  export * from './Channel';
4
6
  export * from './ChannelHeader';
@@ -33,5 +35,4 @@ export * from './TypingIndicator';
33
35
  export * from './Window';
34
36
  export * from './Threads';
35
37
  export * from './ChatView';
36
- export * from './AIStateIndicator';
37
38
  export { UploadButton } from './ReactFileUtilities';
@@ -178,6 +178,10 @@ export type ComponentContextValue = {
178
178
  UnreadMessagesSeparator?: React.ComponentType<UnreadMessagesSeparatorProps>;
179
179
  /** Custom UI component to display a message in the `VirtualizedMessageList`, does not have a default implementation */
180
180
  VirtualMessage?: React.ComponentType<FixedHeightMessageProps>;
181
+ /** Custom UI component to wrap MessageList children. Default is the `ul` tag */
182
+ MessageListWrapper?: React.ComponentType<PropsWithChildren>;
183
+ /** Custom UI component to wrap each element of MessageList. Default is the `li` tag */
184
+ MessageListItem?: React.ComponentType<PropsWithChildren>;
181
185
  };
182
186
  export declare const ComponentContext: React.Context<ComponentContextValue>;
183
187
  export declare const ComponentProvider: ({ children, value, }: PropsWithChildren<{
@@ -1,6 +1,9 @@
1
1
  import React from 'react';
2
2
  import type { PropsWithChildren } from 'react';
3
+ import type { RenderedMessage } from '../components';
3
4
  export type MessageListContextValue = {
5
+ /** Enriched message list, including date separators and intro message (if enabled) */
6
+ processedMessages: RenderedMessage[];
4
7
  /** The scroll container within which the messages and typing indicator are rendered */
5
8
  listElement: HTMLDivElement | null;
6
9
  /** Function that scrolls the `listElement` to the bottom. */