stream-chat-react 13.9.0 → 13.10.1

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 (75) hide show
  1. package/dist/components/Channel/hooks/useCreateChannelStateContext.js +7 -3
  2. package/dist/components/ChannelPreview/hooks/useMessageDeliveryStatus.js +8 -3
  3. package/dist/components/Chat/hooks/useChat.js +1 -1
  4. package/dist/components/Dialog/ButtonWithSubmenu.d.ts +2 -2
  5. package/dist/components/Dialog/ButtonWithSubmenu.js +6 -6
  6. package/dist/components/Dialog/DialogAnchor.d.ts +5 -11
  7. package/dist/components/Dialog/DialogAnchor.js +22 -26
  8. package/dist/components/Dialog/DialogPortal.d.ts +2 -1
  9. package/dist/components/Dialog/DialogPortal.js +23 -7
  10. package/dist/components/Dialog/hooks/index.d.ts +1 -0
  11. package/dist/components/Dialog/hooks/useDialog.d.ts +4 -0
  12. package/dist/components/Dialog/hooks/useDialog.js +9 -1
  13. package/dist/components/Dialog/hooks/usePopoverPosition.d.ts +68 -0
  14. package/dist/components/Dialog/hooks/usePopoverPosition.js +54 -0
  15. package/dist/components/Form/Dropdown.d.ts +2 -2
  16. package/dist/components/Message/Message.js +1 -1
  17. package/dist/components/Message/MessageRepliesCountButton.js +3 -1
  18. package/dist/components/Message/MessageStatus.js +6 -2
  19. package/dist/components/Message/renderText/remarkPlugins/index.d.ts +1 -0
  20. package/dist/components/Message/renderText/remarkPlugins/index.js +1 -0
  21. package/dist/components/Message/renderText/remarkPlugins/keepLineBreaksPlugin.d.ts +10 -2
  22. package/dist/components/Message/renderText/remarkPlugins/keepLineBreaksPlugin.js +46 -26
  23. package/dist/components/Message/renderText/remarkPlugins/remarkIgnoreMarkdown.d.ts +8 -0
  24. package/dist/components/Message/renderText/remarkPlugins/remarkIgnoreMarkdown.js +11 -0
  25. package/dist/components/Message/types.d.ts +4 -0
  26. package/dist/components/MessageActions/MessageActions.js +4 -4
  27. package/dist/components/MessageInput/AttachmentSelector.d.ts +1 -1
  28. package/dist/components/MessageInput/AttachmentSelector.js +9 -4
  29. package/dist/components/MessageList/MessageList.js +6 -0
  30. package/dist/components/MessageList/VirtualizedMessageList.d.ts +3 -1
  31. package/dist/components/MessageList/VirtualizedMessageList.js +6 -0
  32. package/dist/components/MessageList/VirtualizedMessageListComponents.js +2 -2
  33. package/dist/components/MessageList/hooks/MessageList/useMessageListElements.d.ts +1 -0
  34. package/dist/components/MessageList/hooks/MessageList/useMessageListElements.js +7 -2
  35. package/dist/components/MessageList/hooks/useLastDeliveredData.d.ts +1 -0
  36. package/dist/components/MessageList/hooks/useLastDeliveredData.js +24 -11
  37. package/dist/components/MessageList/hooks/useLastOwnMessage.d.ts +5 -0
  38. package/dist/components/MessageList/hooks/useLastOwnMessage.js +4 -0
  39. package/dist/components/MessageList/hooks/useLastReadData.d.ts +1 -0
  40. package/dist/components/MessageList/hooks/useLastReadData.js +20 -10
  41. package/dist/components/MessageList/renderMessages.d.ts +4 -2
  42. package/dist/components/MessageList/renderMessages.js +2 -2
  43. package/dist/components/Modal/GlobalModal.js +2 -2
  44. package/dist/components/Reactions/ReactionSelectorWithButton.js +4 -4
  45. package/dist/components/Tooltip/Tooltip.d.ts +2 -2
  46. package/dist/components/Tooltip/Tooltip.js +11 -12
  47. package/dist/context/DialogManagerContext.d.ts +1 -0
  48. package/dist/context/DialogManagerContext.js +1 -0
  49. package/dist/context/MessageContext.d.ts +4 -0
  50. package/dist/css/v2/index.css +1 -1
  51. package/dist/css/v2/index.layout.css +1 -1
  52. package/dist/experimental/MessageActions/MessageActions.js +5 -5
  53. package/dist/experimental/index.browser.cjs +307 -238
  54. package/dist/experimental/index.browser.cjs.map +4 -4
  55. package/dist/experimental/index.node.cjs +307 -238
  56. package/dist/experimental/index.node.cjs.map +4 -4
  57. package/dist/index.browser.cjs +1521 -1338
  58. package/dist/index.browser.cjs.map +4 -4
  59. package/dist/index.node.cjs +1524 -1338
  60. package/dist/index.node.cjs.map +4 -4
  61. package/dist/plugins/Emojis/EmojiPicker.d.ts +9 -4
  62. package/dist/plugins/Emojis/EmojiPicker.js +10 -5
  63. package/dist/plugins/Emojis/index.browser.cjs +89 -29
  64. package/dist/plugins/Emojis/index.browser.cjs.map +4 -4
  65. package/dist/plugins/Emojis/index.node.cjs +89 -29
  66. package/dist/plugins/Emojis/index.node.cjs.map +4 -4
  67. package/dist/scss/v2/Message/Message-layout.scss +4 -0
  68. package/dist/scss/v2/MessageActionsBox/MessageActionsBox-layout.scss +1 -0
  69. package/dist/utils/findReverse.d.ts +1 -0
  70. package/dist/utils/findReverse.js +9 -0
  71. package/package.json +7 -8
  72. package/dist/components/MessageActions/hooks/index.d.ts +0 -1
  73. package/dist/components/MessageActions/hooks/index.js +0 -1
  74. package/dist/components/MessageActions/hooks/useMessageActionsBoxPopper.d.ts +0 -18
  75. package/dist/components/MessageActions/hooks/useMessageActionsBoxPopper.js +0 -31
@@ -59,6 +59,8 @@ export type MessageProps = {
59
59
  highlighted?: boolean;
60
60
  /** Whether the threaded message is the first in the thread list */
61
61
  initialMessage?: boolean;
62
+ /** Latest own message in currently displayed message set. */
63
+ lastOwnMessage?: LocalMessage;
62
64
  /** Latest message id on current channel */
63
65
  lastReceivedId?: string | null;
64
66
  /** UI component to display a Message in MessageList, overrides value in [ComponentContext](https://getstream.io/chat/docs/sdk/react/contexts/component_context/#message) */
@@ -89,6 +91,8 @@ export type MessageProps = {
89
91
  renderText?: (text?: string, mentioned_users?: UserResponse[], options?: RenderTextOptions) => ReactNode;
90
92
  /** Custom retry send message handler to override default in [ChannelActionContext](https://getstream.io/chat/docs/sdk/react/contexts/channel_action_context/) */
91
93
  retrySendMessage?: ChannelActionContextValue['retrySendMessage'];
94
+ /** Keep track of read receipts for each message sent by the user. When disabled, only the last own message delivery / read status is rendered. */
95
+ returnAllReadData?: boolean;
92
96
  /** Comparator function to sort the list of reacted users
93
97
  * @deprecated use `reactionDetailsSort` instead
94
98
  */
@@ -1,7 +1,7 @@
1
1
  import clsx from 'clsx';
2
2
  import React, { useCallback, useRef } from 'react';
3
3
  import { MessageActionsBox } from './MessageActionsBox';
4
- import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
4
+ import { DialogAnchor, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog';
5
5
  import { ActionsIcon as DefaultActionsIcon } from '../Message/icons';
6
6
  import { isUserMuted, shouldRenderMessageActions } from '../Message/utils';
7
7
  import { useChatContext } from '../../context/ChatContext';
@@ -24,8 +24,8 @@ export const MessageActions = (props) => {
24
24
  const isMuted = useCallback(() => isUserMuted(message, mutes), [message, mutes]);
25
25
  const dialogIdNamespace = threadList ? '-thread-' : '';
26
26
  const dialogId = `message-actions${dialogIdNamespace}--${message.id}`;
27
- const dialog = useDialog({ id: dialogId });
28
- const dialogIsOpen = useDialogIsOpen(dialogId);
27
+ const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId });
28
+ const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager?.id);
29
29
  const messageActions = getMessageActions();
30
30
  const renderMessageActions = shouldRenderMessageActions({
31
31
  customMessageActions,
@@ -37,7 +37,7 @@ export const MessageActions = (props) => {
37
37
  if (!renderMessageActions)
38
38
  return null;
39
39
  return (React.createElement(MessageActionsWrapper, { customWrapperClass: customWrapperClass, inline: inline, toggleOpen: dialog?.toggle },
40
- React.createElement(DialogAnchor, { id: dialogId, placement: isMine ? 'top-end' : 'top-start', referenceElement: actionsBoxButtonRef.current, tabIndex: -1, trapFocus: true },
40
+ React.createElement(DialogAnchor, { dialogManagerId: dialogManager?.id, id: dialogId, placement: isMine ? 'top-end' : 'top-start', referenceElement: actionsBoxButtonRef.current, tabIndex: -1, trapFocus: true },
41
41
  React.createElement(MessageActionsBox, { getMessageActions: getMessageActions, handleDelete: handleDelete, handleEdit: setEditingState, handleFlag: handleFlag, handleMarkUnread: handleMarkUnread, handleMute: handleMute, handlePin: handlePin, isUserMuted: isMuted, mine: isMine, open: dialogIsOpen })),
42
42
  React.createElement("button", { "aria-expanded": dialogIsOpen, "aria-haspopup": 'true', "aria-label": t('aria/Open Message Actions Menu'), className: 'str-chat__message-actions-box-button', "data-testid": 'message-actions-toggle-button', ref: actionsBoxButtonRef },
43
43
  React.createElement(ActionsIcon, { className: 'str-chat__message-action-icon' }))));
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- export declare const SimpleAttachmentSelector: () => React.JSX.Element;
2
+ export declare const SimpleAttachmentSelector: () => React.JSX.Element | null;
3
3
  export type AttachmentSelectorModalContentProps = {
4
4
  close: () => void;
5
5
  };
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { UploadIcon as DefaultUploadIcon } from './icons';
3
3
  import { useAttachmentManagerState } from './hooks/useAttachmentManagerState';
4
4
  import { CHANNEL_CONTAINER_ID } from '../Channel/constants';
5
- import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
5
+ import { DialogAnchor, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog';
6
6
  import { DialogMenuButton } from '../Dialog/DialogMenu';
7
7
  import { Modal as DefaultModal } from '../Modal';
8
8
  import { ShareLocationDialog as DefaultLocationDialog } from '../Location';
@@ -16,6 +16,7 @@ import clsx from 'clsx';
16
16
  import { useMessageComposer } from './hooks';
17
17
  export const SimpleAttachmentSelector = () => {
18
18
  const { AttachmentSelectorInitiationButtonContents, FileUploadIcon = DefaultUploadIcon, } = useComponentContext();
19
+ const { channelCapabilities } = useChannelStateContext();
19
20
  const inputRef = useRef(null);
20
21
  const [labelElement, setLabelElement] = useState(null);
21
22
  const id = useStableId();
@@ -33,6 +34,8 @@ export const SimpleAttachmentSelector = () => {
33
34
  labelElement.removeEventListener('keyup', handleKeyUp);
34
35
  };
35
36
  }, [labelElement]);
37
+ if (!channelCapabilities['upload-file'])
38
+ return null;
36
39
  return (React.createElement("div", { className: 'str-chat__file-input-container', "data-testid": 'file-upload-button' },
37
40
  React.createElement(UploadFileInput, { id: id, ref: inputRef }),
38
41
  React.createElement("label", { className: 'str-chat__file-input-label', htmlFor: id, ref: setLabelElement, tabIndex: 0 }, AttachmentSelectorInitiationButtonContents ? (React.createElement(AttachmentSelectorInitiationButtonContents, null)) : (React.createElement(FileUploadIcon, null)))));
@@ -119,8 +122,10 @@ export const AttachmentSelector = ({ attachmentSelectorActionSet = defaultAttach
119
122
  const messageComposer = useMessageComposer();
120
123
  const actions = useAttachmentSelectorActionsFiltered(attachmentSelectorActionSet);
121
124
  const menuDialogId = `attachment-actions-menu${messageComposer.threadId ? '-thread' : ''}`;
122
- const menuDialog = useDialog({ id: menuDialogId });
123
- const menuDialogIsOpen = useDialogIsOpen(menuDialogId);
125
+ const { dialog: menuDialog, dialogManager } = useDialogOnNearestManager({
126
+ id: menuDialogId,
127
+ });
128
+ const menuDialogIsOpen = useDialogIsOpen(menuDialogId, dialogManager?.id);
124
129
  const [modalContentAction, setModalContentActionAction] = useState();
125
130
  const openModal = useCallback((actionType) => {
126
131
  const action = actions.find((a) => a.type === actionType);
@@ -143,7 +148,7 @@ export const AttachmentSelector = ({ attachmentSelectorActionSet = defaultAttach
143
148
  channelCapabilities['upload-file'] && React.createElement(UploadFileInput, { ref: setFileInput }),
144
149
  React.createElement("button", { "aria-expanded": menuDialogIsOpen, "aria-haspopup": 'true', "aria-label": t('aria/Open Attachment Selector'), className: 'str-chat__attachment-selector__menu-button', "data-testid": 'invoke-attachment-selector-button', onClick: () => menuDialog?.toggle(), ref: menuButtonRef },
145
150
  React.createElement(AttachmentSelectorMenuInitButtonIcon, null)),
146
- React.createElement(DialogAnchor, { id: menuDialogId, placement: 'top-start', referenceElement: menuButtonRef.current, tabIndex: -1, trapFocus: true },
151
+ React.createElement(DialogAnchor, { dialogManagerId: dialogManager?.id, id: menuDialogId, placement: 'top-start', referenceElement: menuButtonRef.current, tabIndex: -1, trapFocus: true },
147
152
  React.createElement("div", { className: 'str-chat__attachment-selector-actions-menu str-chat__dialog-menu', "data-testid": 'attachment-selector-actions-menu' }, actions.map(({ ActionButton, type }) => (React.createElement(ActionButton, { closeMenu: menuDialog.close, key: `attachment-selector-item-${type}`, openModalForAction: openModal }))))),
148
153
  React.createElement(Portal, { getPortalDestination: getModalPortalDestination ?? getDefaultPortalDestination, isOpen: modalIsOpen },
149
154
  React.createElement(Modal, { className: clsx({
@@ -20,6 +20,7 @@ import { MessageListMainPanel as DefaultMessageListMainPanel } from './MessageLi
20
20
  import { defaultRenderMessages } from './renderMessages';
21
21
  import { useStableId } from '../UtilityComponents/useStableId';
22
22
  import { DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD, DEFAULT_NEXT_CHANNEL_PAGE_SIZE, } from '../../constants/limits';
23
+ import { useLastOwnMessage } from './hooks/useLastOwnMessage';
23
24
  const MessageListWithContext = (props) => {
24
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
25
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;
@@ -57,6 +58,10 @@ const MessageListWithContext = (props) => {
57
58
  noGroupByUser,
58
59
  reviewProcessedMessage,
59
60
  });
61
+ const lastOwnMessage = useLastOwnMessage({
62
+ messages,
63
+ ownUserId: channel.getClient().user?.id,
64
+ });
60
65
  const elements = useMessageListElements({
61
66
  channelUnreadUiState,
62
67
  enrichedMessages,
@@ -91,6 +96,7 @@ const MessageListWithContext = (props) => {
91
96
  sortReactions,
92
97
  unsafeHTML,
93
98
  },
99
+ lastOwnMessage,
94
100
  messageGroupStyles,
95
101
  messages,
96
102
  renderMessages,
@@ -10,7 +10,7 @@ import type { ComponentContextValue } from '../../context/ComponentContext';
10
10
  import type { LocalMessage, UserResponse } from 'stream-chat';
11
11
  import type { UnknownType } from '../../types/types';
12
12
  type PropsDrilledToMessage = 'additionalMessageInputProps' | 'customMessageActions' | 'formatDate' | 'messageActions' | 'openThread' | 'reactionDetailsSort' | 'sortReactions' | 'sortReactionDetails';
13
- type VirtualizedMessageListPropsForContext = PropsDrilledToMessage | 'closeReactionSelectorOnClick' | 'customMessageRenderer' | 'head' | 'loadingMore' | 'Message' | 'shouldGroupByUser' | 'threadList';
13
+ type VirtualizedMessageListPropsForContext = PropsDrilledToMessage | 'closeReactionSelectorOnClick' | 'customMessageRenderer' | 'head' | 'loadingMore' | 'Message' | 'returnAllReadData' | 'shouldGroupByUser' | 'threadList';
14
14
  /**
15
15
  * Context object provided to some Virtuoso props that are functions (components rendered by Virtuoso and other functions)
16
16
  */
@@ -29,6 +29,8 @@ export type VirtuosoContext = Required<Pick<ComponentContextValue, 'DateSeparato
29
29
  processedMessages: RenderedMessage[];
30
30
  /** Instance of VirtuosoHandle object providing the API to navigate in the virtualized list by various scroll actions. */
31
31
  virtuosoRef: RefObject<VirtuosoHandle | null>;
32
+ /** Latest own message in currently displayed message set. */
33
+ lastOwnMessage?: LocalMessage;
32
34
  /** Message id which was marked as unread. ALl the messages following this message are considered unrea. */
33
35
  firstUnreadMessageId?: string;
34
36
  lastReadDate?: Date;
@@ -23,6 +23,7 @@ import { VirtualizedMessageListContextProvider } from '../../context/Virtualized
23
23
  import { DEFAULT_NEXT_CHANNEL_PAGE_SIZE } from '../../constants/limits';
24
24
  import { useStableId } from '../UtilityComponents/useStableId';
25
25
  import { useLastDeliveredData } from './hooks/useLastDeliveredData';
26
+ import { useLastOwnMessage } from './hooks/useLastOwnMessage';
26
27
  function captureResizeObserverExceededError(e) {
27
28
  if (e.message === 'ResizeObserver loop completed with undelivered notifications.' ||
28
29
  e.message === 'ResizeObserver loop limit exceeded') {
@@ -101,14 +102,17 @@ const VirtualizedMessageListWithContext = (props) => {
101
102
  messages?.length,
102
103
  client.userID,
103
104
  ]);
105
+ const lastOwnMessage = useLastOwnMessage({ messages, ownUserId: client.user?.id });
104
106
  // get the mapping of own messages to array of users who read them
105
107
  const ownMessagesReadByOthers = useLastReadData({
106
108
  channel,
109
+ lastOwnMessage,
107
110
  messages: messages || [],
108
111
  returnAllReadData,
109
112
  });
110
113
  const ownMessagesDeliveredToOthers = useLastDeliveredData({
111
114
  channel,
115
+ lastOwnMessage,
112
116
  messages: messages || [],
113
117
  returnAllReadData,
114
118
  });
@@ -226,6 +230,7 @@ const VirtualizedMessageListWithContext = (props) => {
226
230
  firstUnreadMessageId: channelUnreadUiState?.first_unread_message_id,
227
231
  formatDate,
228
232
  head,
233
+ lastOwnMessage,
229
234
  lastReadDate: channelUnreadUiState?.last_read,
230
235
  lastReadMessageId: channelUnreadUiState?.last_read_message_id,
231
236
  lastReceivedMessageId,
@@ -240,6 +245,7 @@ const VirtualizedMessageListWithContext = (props) => {
240
245
  ownMessagesReadByOthers,
241
246
  processedMessages,
242
247
  reactionDetailsSort,
248
+ returnAllReadData,
243
249
  shouldGroupByUser,
244
250
  sortReactionDetails,
245
251
  sortReactions,
@@ -51,7 +51,7 @@ export const EmptyPlaceholder = ({ context }) => {
51
51
  return (React.createElement(React.Fragment, null, EmptyStateIndicator && (React.createElement(EmptyStateIndicator, { listType: context?.threadList ? 'thread' : 'message' }))));
52
52
  };
53
53
  export const messageRenderer = (virtuosoIndex, _data, virtuosoContext) => {
54
- const { additionalMessageInputProps, closeReactionSelectorOnClick, customMessageActions, customMessageRenderer, DateSeparator, firstUnreadMessageId, formatDate, lastReadDate, lastReadMessageId, lastReceivedMessageId, Message: MessageUIComponent, messageActions, messageGroupStyles, MessageSystem, numItemsPrepended, openThread, ownMessagesDeliveredToOthers, ownMessagesReadByOthers, processedMessages: messageList, reactionDetailsSort, shouldGroupByUser, sortReactionDetails, sortReactions, threadList, unreadMessageCount = 0, UnreadMessagesSeparator, virtuosoRef, } = virtuosoContext;
54
+ const { additionalMessageInputProps, closeReactionSelectorOnClick, customMessageActions, customMessageRenderer, DateSeparator, firstUnreadMessageId, formatDate, lastOwnMessage, lastReadDate, lastReadMessageId, lastReceivedMessageId, Message: MessageUIComponent, messageActions, messageGroupStyles, MessageSystem, numItemsPrepended, openThread, ownMessagesDeliveredToOthers, ownMessagesReadByOthers, processedMessages: messageList, reactionDetailsSort, returnAllReadData, shouldGroupByUser, sortReactionDetails, sortReactions, threadList, unreadMessageCount = 0, UnreadMessagesSeparator, virtuosoRef, } = virtuosoContext;
55
55
  const streamMessageIndex = calculateItemIndex(virtuosoIndex, numItemsPrepended);
56
56
  if (customMessageRenderer) {
57
57
  return customMessageRenderer(messageList, streamMessageIndex);
@@ -88,5 +88,5 @@ export const messageRenderer = (virtuosoIndex, _data, virtuosoContext) => {
88
88
  return (React.createElement(React.Fragment, null,
89
89
  isFirstUnreadMessage && (React.createElement("div", { className: 'str-chat__unread-messages-separator-wrapper' },
90
90
  React.createElement(UnreadMessagesSeparator, { unreadCount: unreadMessageCount }))),
91
- React.createElement(Message, { additionalMessageInputProps: additionalMessageInputProps, autoscrollToBottom: virtuosoRef.current?.autoscrollToBottom, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: customMessageActions, deliveredTo: ownMessagesDeliveredToOthers[message.id] || [], endOfGroup: endOfGroup, firstOfGroup: firstOfGroup, formatDate: formatDate, groupedByUser: groupedByUser, groupStyles: [messageGroupStyles[message.id] ?? ''], lastReceivedId: lastReceivedMessageId, message: message, Message: MessageUIComponent, messageActions: messageActions, openThread: openThread, reactionDetailsSort: reactionDetailsSort, readBy: ownMessagesReadByOthers[message.id] || [], sortReactionDetails: sortReactionDetails, sortReactions: sortReactions, threadList: threadList })));
91
+ React.createElement(Message, { additionalMessageInputProps: additionalMessageInputProps, autoscrollToBottom: virtuosoRef.current?.autoscrollToBottom, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: customMessageActions, deliveredTo: ownMessagesDeliveredToOthers[message.id] || [], endOfGroup: endOfGroup, firstOfGroup: firstOfGroup, formatDate: formatDate, groupedByUser: groupedByUser, groupStyles: [messageGroupStyles[message.id] ?? ''], lastOwnMessage: lastOwnMessage, lastReceivedId: lastReceivedMessageId, message: message, Message: MessageUIComponent, messageActions: messageActions, openThread: openThread, reactionDetailsSort: reactionDetailsSort, readBy: ownMessagesReadByOthers[message.id] || [], returnAllReadData: returnAllReadData, sortReactionDetails: sortReactionDetails, sortReactions: sortReactions, threadList: threadList })));
92
92
  };
@@ -12,6 +12,7 @@ type UseMessageListElementsProps = {
12
12
  returnAllReadData: boolean;
13
13
  threadList: boolean;
14
14
  channelUnreadUiState?: ChannelUnreadUiState;
15
+ lastOwnMessage?: LocalMessage;
15
16
  };
16
17
  export declare const useMessageListElements: (props: UseMessageListElementsProps) => React.ReactNode[];
17
18
  export {};
@@ -6,18 +6,20 @@ import { useComponentContext } from '../../../../context/ComponentContext';
6
6
  import { useChannelStateContext } from '../../../../context';
7
7
  import { useLastDeliveredData } from '../useLastDeliveredData';
8
8
  export const useMessageListElements = (props) => {
9
- const { channelUnreadUiState, enrichedMessages, internalMessageProps, messageGroupStyles, messages, renderMessages, returnAllReadData, threadList, } = props;
9
+ const { channelUnreadUiState, enrichedMessages, internalMessageProps, lastOwnMessage, messageGroupStyles, messages, renderMessages, returnAllReadData, threadList, } = props;
10
10
  const { customClasses } = useChatContext('useMessageListElements');
11
11
  const { channel } = useChannelStateContext();
12
12
  const components = useComponentContext('useMessageListElements');
13
13
  // get the readData, but only for messages submitted by the user themselves
14
14
  const readData = useLastReadData({
15
15
  channel,
16
+ lastOwnMessage,
16
17
  messages,
17
18
  returnAllReadData,
18
19
  });
19
20
  const ownMessagesDeliveredToOthers = useLastDeliveredData({
20
21
  channel,
22
+ lastOwnMessage,
21
23
  messages,
22
24
  returnAllReadData,
23
25
  });
@@ -26,22 +28,25 @@ export const useMessageListElements = (props) => {
26
28
  channelUnreadUiState,
27
29
  components,
28
30
  customClasses,
31
+ lastOwnMessage,
29
32
  lastReceivedMessageId,
30
33
  messageGroupStyles,
31
34
  messages: enrichedMessages,
32
35
  ownMessagesDeliveredToOthers,
33
36
  readData,
34
- sharedMessageProps: { ...internalMessageProps, threadList },
37
+ sharedMessageProps: { ...internalMessageProps, returnAllReadData, threadList },
35
38
  }),
36
39
  // eslint-disable-next-line react-hooks/exhaustive-deps
37
40
  [
38
41
  enrichedMessages,
39
42
  internalMessageProps,
43
+ lastOwnMessage,
40
44
  lastReceivedMessageId,
41
45
  messageGroupStyles,
42
46
  channelUnreadUiState,
43
47
  readData,
44
48
  renderMessages,
49
+ returnAllReadData,
45
50
  threadList,
46
51
  ]);
47
52
  return elements;
@@ -3,6 +3,7 @@ type UseLastDeliveredDataParams = {
3
3
  channel: Channel;
4
4
  messages: LocalMessage[];
5
5
  returnAllReadData: boolean;
6
+ lastOwnMessage?: LocalMessage;
6
7
  };
7
8
  export declare const useLastDeliveredData: (props: UseLastDeliveredDataParams) => Record<string, UserResponse[]>;
8
9
  export {};
@@ -1,13 +1,26 @@
1
- import { useMemo } from 'react';
1
+ import { useCallback, useEffect, useState } from 'react';
2
2
  export const useLastDeliveredData = (props) => {
3
- const { channel, messages, returnAllReadData } = props;
4
- return useMemo(() => returnAllReadData
5
- ? messages.reduce((acc, msg) => {
6
- acc[msg.id] = channel.messageReceiptsTracker.deliveredForMessage({
7
- msgId: msg.id,
8
- timestampMs: msg.created_at.getTime(),
9
- });
10
- return acc;
11
- }, {})
12
- : channel.messageReceiptsTracker.groupUsersByLastDeliveredMessage(), [channel, messages, returnAllReadData]);
3
+ const { channel, lastOwnMessage, messages, returnAllReadData } = props;
4
+ const calculate = useCallback(() => {
5
+ if (returnAllReadData) {
6
+ return messages.reduce((acc, msg) => {
7
+ acc[msg.id] = channel.messageReceiptsTracker.deliveredForMessage({
8
+ msgId: msg.id,
9
+ timestampMs: msg.created_at.getTime(),
10
+ });
11
+ return acc;
12
+ }, {});
13
+ }
14
+ if (!lastOwnMessage)
15
+ return {};
16
+ return {
17
+ [lastOwnMessage.id]: channel.messageReceiptsTracker.deliveredForMessage({
18
+ msgId: lastOwnMessage.id,
19
+ timestampMs: lastOwnMessage.created_at.getTime(),
20
+ }),
21
+ };
22
+ }, [channel, lastOwnMessage, messages, returnAllReadData]);
23
+ const [deliveredTo, setDeliveredTo] = useState(calculate);
24
+ useEffect(() => channel.on('message.delivered', () => setDeliveredTo(calculate)).unsubscribe, [channel, calculate]);
25
+ return deliveredTo;
13
26
  };
@@ -0,0 +1,5 @@
1
+ import type { LocalMessage } from 'stream-chat';
2
+ export declare const useLastOwnMessage: ({ messages, ownUserId, }: {
3
+ messages?: LocalMessage[];
4
+ ownUserId?: string;
5
+ }) => LocalMessage | undefined;
@@ -0,0 +1,4 @@
1
+ import { useMemo } from 'react';
2
+ import { findReverse } from '../../../utils/findReverse';
3
+ // fixme: we should be able to retrieve last own message quickly from the LLC. Should be done when refactoring the LLC Channel state to reactive.
4
+ export const useLastOwnMessage = ({ messages, ownUserId, }) => useMemo(() => messages && findReverse(messages, (msg) => (msg.user && msg.user.id) === ownUserId), [messages, ownUserId]);
@@ -3,6 +3,7 @@ type UseLastReadDataParams = {
3
3
  channel: Channel;
4
4
  messages: LocalMessage[];
5
5
  returnAllReadData: boolean;
6
+ lastOwnMessage?: LocalMessage;
6
7
  };
7
8
  export declare const useLastReadData: (props: UseLastReadDataParams) => Record<string, UserResponse[]>;
8
9
  export {};
@@ -1,13 +1,23 @@
1
1
  import { useMemo } from 'react';
2
2
  export const useLastReadData = (props) => {
3
- const { channel, messages, returnAllReadData } = props;
4
- return useMemo(() => returnAllReadData
5
- ? messages.reduce((acc, msg) => {
6
- acc[msg.id] = channel.messageReceiptsTracker.readersForMessage({
7
- msgId: msg.id,
8
- timestampMs: msg.created_at.getTime(),
9
- });
10
- return acc;
11
- }, {})
12
- : channel.messageReceiptsTracker.groupUsersByLastReadMessage(), [channel, messages, returnAllReadData]);
3
+ const { channel, lastOwnMessage, messages, returnAllReadData } = props;
4
+ return useMemo(() => {
5
+ if (returnAllReadData) {
6
+ return messages.reduce((acc, msg) => {
7
+ acc[msg.id] = channel.messageReceiptsTracker.readersForMessage({
8
+ msgId: msg.id,
9
+ timestampMs: msg.created_at.getTime(),
10
+ });
11
+ return acc;
12
+ }, {});
13
+ }
14
+ if (!lastOwnMessage)
15
+ return {};
16
+ return {
17
+ [lastOwnMessage.id]: channel.messageReceiptsTracker.readersForMessage({
18
+ msgId: lastOwnMessage.id,
19
+ timestampMs: lastOwnMessage.created_at.getTime(),
20
+ }),
21
+ };
22
+ }, [channel, lastOwnMessage, messages, returnAllReadData]);
13
23
  };
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
2
2
  import React from 'react';
3
3
  import type { GroupStyle, RenderedMessage } from './utils';
4
4
  import type { MessageProps } from '../Message';
5
- import type { UserResponse } from 'stream-chat';
5
+ import type { LocalMessage, UserResponse } from 'stream-chat';
6
6
  import type { ComponentContextValue, CustomClasses } from '../../context';
7
7
  import type { ChannelUnreadUiState } from '../../types';
8
8
  export interface RenderMessagesOptions {
@@ -19,6 +19,8 @@ export interface RenderMessagesOptions {
19
19
  * Props forwarded to the Message component.
20
20
  */
21
21
  sharedMessageProps: SharedMessageProps;
22
+ /** Latest own message in currently displayed message set. */
23
+ lastOwnMessage?: LocalMessage;
22
24
  /**
23
25
  * Current user's channel read state used to render components reflecting unread state.
24
26
  * It does not reflect the back-end state if a channel is marked read on mount.
@@ -30,5 +32,5 @@ export interface RenderMessagesOptions {
30
32
  export type SharedMessageProps = Omit<MessageProps, MessagePropsToOmit>;
31
33
  export type MessageRenderer = (options: RenderMessagesOptions) => Array<ReactNode>;
32
34
  type MessagePropsToOmit = 'channel' | 'deliveredTo' | 'groupStyles' | 'initialMessage' | 'lastReceivedId' | 'message' | 'readBy';
33
- export declare function defaultRenderMessages({ channelUnreadUiState, components, customClasses, lastReceivedMessageId: lastReceivedId, messageGroupStyles, messages, ownMessagesDeliveredToOthers, readData, sharedMessageProps: messageProps, }: RenderMessagesOptions): React.JSX.Element[];
35
+ export declare function defaultRenderMessages({ channelUnreadUiState, components, customClasses, lastOwnMessage, lastReceivedMessageId: lastReceivedId, messageGroupStyles, messages, ownMessagesDeliveredToOthers, readData, sharedMessageProps: messageProps, }: RenderMessagesOptions): React.JSX.Element[];
34
36
  export {};
@@ -4,7 +4,7 @@ import { Message } from '../Message';
4
4
  import { DateSeparator as DefaultDateSeparator } from '../DateSeparator';
5
5
  import { EventComponent as DefaultMessageSystem } from '../EventComponent';
6
6
  import { UnreadMessagesSeparator as DefaultUnreadMessagesSeparator } from './UnreadMessagesSeparator';
7
- export function defaultRenderMessages({ channelUnreadUiState, components, customClasses, lastReceivedMessageId: lastReceivedId, messageGroupStyles, messages, ownMessagesDeliveredToOthers, readData, sharedMessageProps: messageProps, }) {
7
+ export function defaultRenderMessages({ channelUnreadUiState, components, customClasses, lastOwnMessage, lastReceivedMessageId: lastReceivedId, messageGroupStyles, messages, ownMessagesDeliveredToOthers, readData, sharedMessageProps: messageProps, }) {
8
8
  const { DateSeparator = DefaultDateSeparator, HeaderComponent, MessageSystem = DefaultMessageSystem, UnreadMessagesSeparator = DefaultUnreadMessagesSeparator, } = components;
9
9
  const renderedMessages = [];
10
10
  let firstMessage;
@@ -44,7 +44,7 @@ export function defaultRenderMessages({ channelUnreadUiState, components, custom
44
44
  isFirstUnreadMessage && UnreadMessagesSeparator && (React.createElement("li", { className: 'str-chat__li str-chat__unread-messages-separator-wrapper' },
45
45
  React.createElement(UnreadMessagesSeparator, { unreadCount: channelUnreadUiState?.unread_messages }))),
46
46
  React.createElement("li", { className: messageClass, "data-message-id": message.id, "data-testid": messageClass },
47
- React.createElement(Message, { deliveredTo: ownMessagesDeliveredToOthers[message.id] || [], groupStyles: [groupStyles], lastReceivedId: lastReceivedId, message: message, readBy: readData[message.id] || [], ...messageProps }))));
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
  }
50
50
  }
@@ -3,7 +3,7 @@ import { useCallback } from 'react';
3
3
  import React, { useEffect, useRef } from 'react';
4
4
  import { FocusScope } from '@react-aria/focus';
5
5
  import { CloseIconRound } from './icons';
6
- import { useTranslationContext } from '../../context';
6
+ import { modalDialogManagerId, useTranslationContext } from '../../context';
7
7
  import { DialogPortalEntry, modalDialogId, useModalDialog, useModalDialogIsOpen, } from '../Dialog';
8
8
  export const GlobalModal = ({ children, className, onClose, onCloseAttempt, open, }) => {
9
9
  const { t } = useTranslationContext('Modal');
@@ -48,7 +48,7 @@ export const GlobalModal = ({ children, className, onClose, onCloseAttempt, open
48
48
  }, [dialog, open]);
49
49
  if (!open || !isOpen)
50
50
  return null;
51
- return (React.createElement(DialogPortalEntry, { dialogId: modalDialogId },
51
+ return (React.createElement(DialogPortalEntry, { dialogId: modalDialogId, dialogManagerId: modalDialogManagerId },
52
52
  React.createElement("div", { className: clsx('str-chat str-chat__modal str-chat-react__modal str-chat__modal--open', className), onClick: handleClick },
53
53
  React.createElement(FocusScope, { autoFocus: true, contain: true },
54
54
  React.createElement("button", { className: 'str-chat__modal__close-button', ref: closeButtonRef, title: t('Close'), type: 'button' },
@@ -1,6 +1,6 @@
1
1
  import React, { useRef } from 'react';
2
2
  import { ReactionSelector as DefaultReactionSelector } from './ReactionSelector';
3
- import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
3
+ import { DialogAnchor, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog';
4
4
  import { useComponentContext, useMessageContext, useTranslationContext, } from '../../context';
5
5
  /**
6
6
  * Internal convenience component - not to be exported. It just groups the button and the dialog anchor and thus prevents
@@ -13,10 +13,10 @@ export const ReactionSelectorWithButton = ({ ReactionIcon, }) => {
13
13
  const buttonRef = useRef(null);
14
14
  const dialogIdNamespace = threadList ? '-thread-' : '';
15
15
  const dialogId = `reaction-selector${dialogIdNamespace}--${message.id}`;
16
- const dialog = useDialog({ id: dialogId });
17
- const dialogIsOpen = useDialogIsOpen(dialogId);
16
+ const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId });
17
+ const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager?.id);
18
18
  return (React.createElement(React.Fragment, null,
19
- React.createElement(DialogAnchor, { id: dialogId, placement: isMyMessage() ? 'top-end' : 'top-start', referenceElement: buttonRef.current, trapFocus: true },
19
+ React.createElement(DialogAnchor, { dialogManagerId: dialogManager?.id, id: dialogId, placement: isMyMessage() ? 'top-end' : 'top-start', referenceElement: buttonRef.current, trapFocus: true },
20
20
  React.createElement(ReactionSelector, null)),
21
21
  React.createElement("button", { "aria-expanded": dialogIsOpen, "aria-label": t('aria/Open Reaction Selector'), className: 'str-chat__message-reactions-button', "data-testid": 'message-reaction-action', onClick: () => dialog?.toggle(), ref: buttonRef },
22
22
  React.createElement(ReactionIcon, { className: 'str-chat__message-action-icon' }))));
@@ -1,6 +1,6 @@
1
1
  import type { ComponentProps } from 'react';
2
2
  import React from 'react';
3
- import type { PopperProps } from 'react-popper';
3
+ import type { PopperLikePlacement } from '../Dialog';
4
4
  export declare const Tooltip: ({ children, ...rest }: ComponentProps<'div'>) => React.JSX.Element;
5
5
  export type PopperTooltipProps<T extends HTMLElement> = React.PropsWithChildren<{
6
6
  /** Reference element to which the tooltip should attach to */
@@ -8,7 +8,7 @@ export type PopperTooltipProps<T extends HTMLElement> = React.PropsWithChildren<
8
8
  /** Popper's modifier (offset) property - [xAxis offset, yAxis offset], default [0, 10] */
9
9
  offset?: [number, number];
10
10
  /** Popper's placement property defining default position of the tooltip, default 'top' */
11
- placement?: PopperProps<unknown>['placement'];
11
+ placement?: PopperLikePlacement;
12
12
  /** Tells component whether to render its contents */
13
13
  visible?: boolean;
14
14
  }>;
@@ -1,20 +1,19 @@
1
- import React, { useState } from 'react';
2
- import { usePopper } from 'react-popper';
1
+ import React, { useEffect, useState } from 'react';
2
+ import { usePopoverPosition } from '../Dialog/hooks/usePopoverPosition';
3
3
  export const Tooltip = ({ children, ...rest }) => (React.createElement("div", { className: 'str-chat__tooltip', ...rest }, children));
4
4
  export const PopperTooltip = ({ children, offset = [0, 10], placement = 'top', referenceElement, visible = false, }) => {
5
5
  const [popperElement, setPopperElement] = useState(null);
6
- const { attributes, styles } = usePopper(referenceElement, popperElement, {
7
- modifiers: [
8
- {
9
- name: 'offset',
10
- options: {
11
- offset,
12
- },
13
- },
14
- ],
6
+ const { placement: resolvedPlacement, refs, strategy, x, y, } = usePopoverPosition({
7
+ offset,
15
8
  placement,
16
9
  });
10
+ useEffect(() => {
11
+ refs.setReference(referenceElement);
12
+ }, [referenceElement, refs]);
13
+ useEffect(() => {
14
+ refs.setFloating(popperElement);
15
+ }, [popperElement, refs]);
17
16
  if (!visible)
18
17
  return null;
19
- return (React.createElement("div", { className: 'str-chat__tooltip', ref: setPopperElement, style: styles.popper, ...attributes.popper }, children));
18
+ return (React.createElement("div", { className: 'str-chat__tooltip', "data-placement": resolvedPlacement, ref: setPopperElement, style: { left: x ?? 0, position: strategy, top: y ?? 0 } }, children));
20
19
  };
@@ -25,4 +25,5 @@ export declare const useDialogManager: ({ dialogId, dialogManagerId, }?: UseDial
25
25
  export declare const modalDialogManagerId: "modal-dialog-manager";
26
26
  export declare const ModalDialogManagerProvider: ({ children }: PropsWithChildrenOnly) => React.JSX.Element;
27
27
  export declare const useModalDialogManager: () => DialogManager | undefined;
28
+ export declare const useNearestDialogManagerContext: () => DialogManagerProviderContextValue | undefined;
28
29
  export {};
@@ -121,3 +121,4 @@ export const useDialogManager = ({ dialogId, dialogManagerId, } = {}) => {
121
121
  export const modalDialogManagerId = 'modal-dialog-manager';
122
122
  export const ModalDialogManagerProvider = ({ children }) => (React.createElement(DialogManagerProvider, { id: modalDialogManagerId }, children));
123
123
  export const useModalDialogManager = () => useMemo(() => getDialogManager(modalDialogManagerId), []);
124
+ export const useNearestDialogManagerContext = () => useContext(DialogManagerProviderContext);
@@ -92,6 +92,8 @@ export type MessageContextValue = {
92
92
  * A factory function that determines whether a message is AI generated or not.
93
93
  */
94
94
  isMessageAIGenerated?: (message: LocalMessage) => boolean;
95
+ /** Latest own message in currently displayed message set. */
96
+ lastOwnMessage?: LocalMessage;
95
97
  /** Latest message id on current channel */
96
98
  lastReceivedId?: string | null;
97
99
  /** DOMRect object for parent MessageList component */
@@ -106,6 +108,8 @@ export type MessageContextValue = {
106
108
  readBy?: UserResponse[];
107
109
  /** Custom function to render message text content, defaults to the renderText function: [utils](https://github.com/GetStream/stream-chat-react/blob/master/src/utils.tsx) */
108
110
  renderText?: (text?: string, mentioned_users?: UserResponse[], options?: RenderTextOptions) => ReactNode;
111
+ /** Keep track of read receipts for each message sent by the user. When disabled, only the last own message delivery / read status is rendered. */
112
+ returnAllReadData?: boolean;
109
113
  /** Comparator function to sort the list of reacted users
110
114
  * @deprecated use `reactionDetailsSort` instead
111
115
  */