stream-chat-react 13.8.1 → 13.10.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 (104) hide show
  1. package/dist/components/Channel/Channel.d.ts +2 -2
  2. package/dist/components/Channel/Channel.js +3 -3
  3. package/dist/components/Channel/hooks/useCreateChannelStateContext.js +3 -1
  4. package/dist/components/ChannelPreview/hooks/useMessageDeliveryStatus.d.ts +1 -0
  5. package/dist/components/ChannelPreview/hooks/useMessageDeliveryStatus.js +31 -14
  6. package/dist/components/Chat/hooks/useChat.js +1 -1
  7. package/dist/components/Dialog/ButtonWithSubmenu.d.ts +2 -2
  8. package/dist/components/Dialog/ButtonWithSubmenu.js +6 -6
  9. package/dist/components/Dialog/DialogAnchor.d.ts +5 -11
  10. package/dist/components/Dialog/DialogAnchor.js +22 -26
  11. package/dist/components/Dialog/DialogPortal.d.ts +2 -1
  12. package/dist/components/Dialog/DialogPortal.js +23 -7
  13. package/dist/components/Dialog/hooks/index.d.ts +1 -0
  14. package/dist/components/Dialog/hooks/useDialog.d.ts +4 -0
  15. package/dist/components/Dialog/hooks/useDialog.js +9 -1
  16. package/dist/components/Dialog/hooks/usePopoverPosition.d.ts +68 -0
  17. package/dist/components/Dialog/hooks/usePopoverPosition.js +54 -0
  18. package/dist/components/Form/Dropdown.d.ts +2 -2
  19. package/dist/components/Message/Message.js +1 -1
  20. package/dist/components/Message/MessageRepliesCountButton.js +3 -1
  21. package/dist/components/Message/MessageStatus.d.ts +1 -0
  22. package/dist/components/Message/MessageStatus.js +22 -12
  23. package/dist/components/Message/hooks/useDeleteHandler.js +2 -2
  24. package/dist/components/Message/icons.d.ts +1 -0
  25. package/dist/components/Message/icons.js +6 -2
  26. package/dist/components/Message/renderText/remarkPlugins/index.d.ts +1 -0
  27. package/dist/components/Message/renderText/remarkPlugins/index.js +1 -0
  28. package/dist/components/Message/renderText/remarkPlugins/keepLineBreaksPlugin.d.ts +10 -2
  29. package/dist/components/Message/renderText/remarkPlugins/keepLineBreaksPlugin.js +46 -26
  30. package/dist/components/Message/renderText/remarkPlugins/remarkIgnoreMarkdown.d.ts +8 -0
  31. package/dist/components/Message/renderText/remarkPlugins/remarkIgnoreMarkdown.js +11 -0
  32. package/dist/components/Message/types.d.ts +2 -0
  33. package/dist/components/Message/utils.d.ts +4 -1
  34. package/dist/components/Message/utils.js +9 -0
  35. package/dist/components/MessageActions/MessageActions.js +4 -4
  36. package/dist/components/MessageActions/MessageActionsBox.js +3 -1
  37. package/dist/components/MessageInput/AttachmentSelector.d.ts +1 -1
  38. package/dist/components/MessageInput/AttachmentSelector.js +9 -4
  39. package/dist/components/MessageList/MessageList.js +2 -2
  40. package/dist/components/MessageList/VirtualizedMessageList.d.ts +2 -0
  41. package/dist/components/MessageList/VirtualizedMessageList.js +10 -4
  42. package/dist/components/MessageList/VirtualizedMessageListComponents.js +2 -2
  43. package/dist/components/MessageList/hooks/MessageList/useMessageListElements.d.ts +2 -2
  44. package/dist/components/MessageList/hooks/MessageList/useMessageListElements.js +13 -5
  45. package/dist/components/MessageList/hooks/useLastDeliveredData.d.ts +8 -0
  46. package/dist/components/MessageList/hooks/useLastDeliveredData.js +16 -0
  47. package/dist/components/MessageList/hooks/useLastReadData.d.ts +3 -8
  48. package/dist/components/MessageList/hooks/useLastReadData.js +10 -7
  49. package/dist/components/MessageList/renderMessages.d.ts +5 -4
  50. package/dist/components/MessageList/renderMessages.js +2 -2
  51. package/dist/components/MessageList/utils.d.ts +1 -5
  52. package/dist/components/MessageList/utils.js +0 -30
  53. package/dist/components/Modal/GlobalModal.js +2 -2
  54. package/dist/components/Reactions/ReactionSelectorWithButton.js +4 -4
  55. package/dist/components/Tooltip/Tooltip.d.ts +2 -2
  56. package/dist/components/Tooltip/Tooltip.js +11 -12
  57. package/dist/context/ChannelActionContext.d.ts +2 -2
  58. package/dist/context/DialogManagerContext.d.ts +1 -0
  59. package/dist/context/DialogManagerContext.js +1 -0
  60. package/dist/context/MessageContext.d.ts +5 -3
  61. package/dist/css/v2/index.css +1 -1
  62. package/dist/css/v2/index.layout.css +1 -1
  63. package/dist/experimental/MessageActions/MessageActions.js +5 -5
  64. package/dist/experimental/index.browser.cjs +324 -249
  65. package/dist/experimental/index.browser.cjs.map +4 -4
  66. package/dist/experimental/index.node.cjs +324 -249
  67. package/dist/experimental/index.node.cjs.map +4 -4
  68. package/dist/i18n/Streami18n.d.ts +2 -0
  69. package/dist/i18n/de.json +2 -0
  70. package/dist/i18n/en.json +2 -0
  71. package/dist/i18n/es.json +2 -0
  72. package/dist/i18n/fr.json +2 -0
  73. package/dist/i18n/hi.json +2 -0
  74. package/dist/i18n/it.json +2 -0
  75. package/dist/i18n/ja.json +2 -0
  76. package/dist/i18n/ko.json +2 -0
  77. package/dist/i18n/nl.json +2 -0
  78. package/dist/i18n/pt.json +2 -0
  79. package/dist/i18n/ru.json +2 -0
  80. package/dist/i18n/tr.json +2 -0
  81. package/dist/index.browser.cjs +1587 -1380
  82. package/dist/index.browser.cjs.map +4 -4
  83. package/dist/index.node.cjs +1592 -1381
  84. package/dist/index.node.cjs.map +4 -4
  85. package/dist/plugins/Emojis/EmojiPicker.d.ts +9 -4
  86. package/dist/plugins/Emojis/EmojiPicker.js +10 -5
  87. package/dist/plugins/Emojis/index.browser.cjs +89 -29
  88. package/dist/plugins/Emojis/index.browser.cjs.map +4 -4
  89. package/dist/plugins/Emojis/index.node.cjs +89 -29
  90. package/dist/plugins/Emojis/index.node.cjs.map +4 -4
  91. package/dist/scss/v2/Dialog/Dialog-layout.scss +12 -3
  92. package/dist/scss/v2/Icon/Icon-layout.scss +6 -0
  93. package/dist/scss/v2/Icon/Icon-theme.scss +4 -0
  94. package/dist/scss/v2/Message/Message-layout.scss +34 -3
  95. package/dist/scss/v2/Message/Message-theme.scss +9 -0
  96. package/dist/scss/v2/MessageActionsBox/MessageActionsBox-layout.scss +1 -0
  97. package/dist/scss/v2/MessageInput/MessageInput-layout.scss +8 -0
  98. package/dist/scss/v2/MessageInput/MessageInput-theme.scss +2 -1
  99. package/dist/scss/v2/Poll/Poll-layout.scss +29 -7
  100. package/package.json +7 -8
  101. package/dist/components/MessageActions/hooks/index.d.ts +0 -1
  102. package/dist/components/MessageActions/hooks/index.js +0 -1
  103. package/dist/components/MessageActions/hooks/useMessageActionsBoxPopper.d.ts +0 -18
  104. package/dist/components/MessageActions/hooks/useMessageActionsBoxPopper.js +0 -31
@@ -1,6 +1,6 @@
1
1
  import type { PropsWithChildren } from 'react';
2
2
  import React from 'react';
3
- import type { ChannelQueryOptions, EventAPIResponse, LocalMessage, Message, MessageResponse, SendMessageOptions, Channel as StreamChannel, StreamChat, UpdateMessageOptions } from 'stream-chat';
3
+ import type { ChannelQueryOptions, DeleteMessageOptions, EventAPIResponse, LocalMessage, Message, MessageResponse, SendMessageOptions, Channel as StreamChannel, StreamChat, UpdateMessageOptions } from 'stream-chat';
4
4
  import type { OnMentionAction } from './hooks/useMentionsHandlers';
5
5
  import type { LoadingErrorIndicatorProps } from '../Loading';
6
6
  import type { ComponentContextValue } from '../../context';
@@ -19,7 +19,7 @@ export type ChannelProps = ChannelPropsForwardedToComponentContext & {
19
19
  */
20
20
  channelQueryOptions?: ChannelQueryOptions;
21
21
  /** Custom action handler to override the default `client.deleteMessage(message.id)` function */
22
- doDeleteMessageRequest?: (message: LocalMessage) => Promise<MessageResponse>;
22
+ doDeleteMessageRequest?: (message: LocalMessage, options?: DeleteMessageOptions) => Promise<MessageResponse>;
23
23
  /** Custom action handler to override the default `channel.markRead` request function (advanced usage only) */
24
24
  doMarkReadRequest?: (channel: StreamChannel, setChannelUnreadUiState?: (state: ChannelUnreadUiState) => void) => Promise<EventAPIResponse> | void;
25
25
  /** Custom action handler to override the default `channel.sendMessage` request function (advanced usage only) */
@@ -500,16 +500,16 @@ const ChannelInner = (props) => {
500
500
  t,
501
501
  channelUnreadUiState,
502
502
  ]);
503
- const deleteMessage = useCallback(async (message) => {
503
+ const deleteMessage = useCallback(async (message, options) => {
504
504
  if (!message?.id) {
505
505
  throw new Error('Cannot delete a message - missing message ID.');
506
506
  }
507
507
  let deletedMessage;
508
508
  if (doDeleteMessageRequest) {
509
- deletedMessage = await doDeleteMessageRequest(message);
509
+ deletedMessage = await doDeleteMessageRequest(message, options);
510
510
  }
511
511
  else {
512
- const result = await client.deleteMessage(message.id);
512
+ const result = await client.deleteMessage(message.id, options);
513
513
  deletedMessage = result.message;
514
514
  }
515
515
  return deletedMessage;
@@ -16,10 +16,12 @@ export const useCreateChannelStateContext = (value) => {
16
16
  channelCapabilitiesArray.forEach((capability) => {
17
17
  channelCapabilities[capability] = true;
18
18
  });
19
+ // FIXME: this is crazy - I could not find out why the messages were not getting updated when only message properties that are not part
20
+ // of this serialization has been changed. A great example of memoization gone wrong.
19
21
  const memoizedMessageData = skipMessageDataMemoization
20
22
  ? messages
21
23
  : messages
22
- .map(({ deleted_at, latest_reactions, pinned, reply_count, status, updated_at, user, }) => `${deleted_at}${latest_reactions ? latest_reactions.map(({ type }) => type).join() : ''}${pinned}${reply_count}${status}${updated_at && (isDayOrMoment(updated_at) || isDate(updated_at))
24
+ .map(({ deleted_at, latest_reactions, pinned, reply_count, status, type, updated_at, user, }) => `${type}${deleted_at}${latest_reactions ? latest_reactions.map(({ type }) => type).join() : ''}${pinned}${reply_count}${status}${updated_at && (isDayOrMoment(updated_at) || isDate(updated_at))
23
25
  ? updated_at.toISOString()
24
26
  : updated_at || ''}${user?.updated_at}`)
25
27
  .join();
@@ -1,5 +1,6 @@
1
1
  import type { Channel, LocalMessage } from 'stream-chat';
2
2
  export declare enum MessageDeliveryStatus {
3
+ SENT = "sent",
3
4
  DELIVERED = "delivered",
4
5
  READ = "read"
5
6
  }
@@ -2,53 +2,70 @@ import { useCallback, useEffect, useState } from 'react';
2
2
  import { useChatContext } from '../../../context';
3
3
  export var MessageDeliveryStatus;
4
4
  (function (MessageDeliveryStatus) {
5
+ MessageDeliveryStatus["SENT"] = "sent";
5
6
  MessageDeliveryStatus["DELIVERED"] = "delivered";
6
7
  MessageDeliveryStatus["READ"] = "read";
7
8
  })(MessageDeliveryStatus || (MessageDeliveryStatus = {}));
8
9
  export const useMessageDeliveryStatus = ({ channel, lastMessage, }) => {
9
10
  const { client } = useChatContext();
10
11
  const [messageDeliveryStatus, setMessageDeliveryStatus] = useState();
11
- const isOwnMessage = useCallback((message) => client.user && message?.user?.id === client.user.id, [client]);
12
+ const isOwnMessage = useCallback((message) => client.user && message && message.user?.id === client.user.id, [client]);
12
13
  useEffect(() => {
14
+ // empty channel
15
+ if (!lastMessage) {
16
+ setMessageDeliveryStatus(undefined);
17
+ }
13
18
  const lastMessageIsOwn = isOwnMessage(lastMessage);
14
19
  if (!lastMessage?.created_at || !lastMessageIsOwn)
15
20
  return;
16
- const lastMessageCreatedAtDate = typeof lastMessage.created_at === 'string'
17
- ? new Date(lastMessage.created_at)
18
- : lastMessage.created_at;
19
- const channelReadByOthersAfterLastMessageUpdate = Object.values(channel.state.read).some(({ last_read: channelLastMarkedReadDate, user }) => {
20
- const ignoreOwnReadStatus = client.user && user.id !== client.user.id;
21
- return ignoreOwnReadStatus && lastMessageCreatedAtDate < channelLastMarkedReadDate;
22
- });
23
- setMessageDeliveryStatus(channelReadByOthersAfterLastMessageUpdate
21
+ const msgRef = {
22
+ msgId: lastMessage.id,
23
+ timestampMs: lastMessage.created_at.getTime(),
24
+ };
25
+ const readersForMessage = channel.messageReceiptsTracker.readersForMessage(msgRef);
26
+ const deliveredForMessage = channel.messageReceiptsTracker.deliveredForMessage(msgRef);
27
+ setMessageDeliveryStatus(readersForMessage.length > 1 ||
28
+ (readersForMessage.length === 1 && readersForMessage[0].id !== client.user?.id)
24
29
  ? MessageDeliveryStatus.READ
25
- : MessageDeliveryStatus.DELIVERED);
26
- }, [channel.state.read, client, isOwnMessage, lastMessage]);
30
+ : deliveredForMessage.length > 1 ||
31
+ (deliveredForMessage.length === 1 &&
32
+ deliveredForMessage[0].id !== client.user?.id)
33
+ ? MessageDeliveryStatus.DELIVERED
34
+ : MessageDeliveryStatus.SENT);
35
+ }, [channel, client, isOwnMessage, lastMessage]);
27
36
  useEffect(() => {
28
37
  const handleMessageNew = (event) => {
29
38
  // the last message is not mine, so do not show the delivery status
30
39
  if (!isOwnMessage(event.message)) {
31
40
  return setMessageDeliveryStatus(undefined);
32
41
  }
33
- return setMessageDeliveryStatus(MessageDeliveryStatus.DELIVERED);
42
+ return setMessageDeliveryStatus(MessageDeliveryStatus.SENT);
34
43
  };
35
44
  channel.on('message.new', handleMessageNew);
36
45
  return () => {
37
46
  channel.off('message.new', handleMessageNew);
38
47
  };
39
- }, [channel, client, isOwnMessage]);
48
+ }, [channel, isOwnMessage]);
40
49
  useEffect(() => {
41
50
  if (!isOwnMessage(lastMessage))
42
51
  return;
52
+ const handleMessageDelivered = (event) => {
53
+ if (event.user?.id !== client.user?.id &&
54
+ lastMessage &&
55
+ lastMessage.id === event.last_delivered_message_id)
56
+ setMessageDeliveryStatus(MessageDeliveryStatus.DELIVERED);
57
+ };
43
58
  const handleMarkRead = (event) => {
44
59
  if (event.user?.id !== client.user?.id)
45
60
  setMessageDeliveryStatus(MessageDeliveryStatus.READ);
46
61
  };
62
+ channel.on('message.delivered', handleMessageDelivered);
47
63
  channel.on('message.read', handleMarkRead);
48
64
  return () => {
65
+ channel.off('message.delivered', handleMessageDelivered);
49
66
  channel.off('message.read', handleMarkRead);
50
67
  };
51
- }, [channel, client, lastMessage, isOwnMessage]);
68
+ }, [channel, client, isOwnMessage, lastMessage]);
52
69
  return {
53
70
  messageDeliveryStatus,
54
71
  };
@@ -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.8.1";
27
+ const version = "13.10.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,9 +1,9 @@
1
1
  import React from 'react';
2
2
  import type { ComponentProps, ComponentType } from 'react';
3
- import type { Placement } from '@popperjs/core';
3
+ import type { PopperLikePlacement } from './hooks';
4
4
  type ButtonWithSubmenu = ComponentProps<'button'> & {
5
5
  children: React.ReactNode;
6
- placement: Placement;
6
+ placement: PopperLikePlacement;
7
7
  Submenu: ComponentType;
8
8
  submenuContainerProps?: ComponentProps<'div'>;
9
9
  };
@@ -1,6 +1,6 @@
1
1
  import clsx from 'clsx';
2
2
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { useDialog, useDialogIsOpen } from './hooks';
3
+ import { useDialogIsOpen, useDialogOnNearestManager } from './hooks';
4
4
  import { useDialogAnchor } from './DialogAnchor';
5
5
  export const ButtonWithSubmenu = ({ children, className, placement, Submenu, submenuContainerProps, ...buttonProps }) => {
6
6
  const buttonRef = useRef(null);
@@ -8,9 +8,9 @@ export const ButtonWithSubmenu = ({ children, className, placement, Submenu, sub
8
8
  const keepSubmenuOpen = useRef(false);
9
9
  const dialogCloseTimeout = useRef(null);
10
10
  const dialogId = useMemo(() => `submenu-${Math.random().toString(36).slice(2)}`, []);
11
- const dialog = useDialog({ id: dialogId });
12
- const dialogIsOpen = useDialogIsOpen(dialogId);
13
- const { attributes, setPopperElement, styles } = useDialogAnchor({
11
+ const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId });
12
+ const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager?.id);
13
+ const { setPopperElement, styles } = useDialogAnchor({
14
14
  open: dialogIsOpen,
15
15
  placement,
16
16
  referenceElement: buttonRef.current,
@@ -66,7 +66,7 @@ export const ButtonWithSubmenu = ({ children, className, placement, Submenu, sub
66
66
  keepSubmenuOpen.current = false;
67
67
  closeDialogLazily();
68
68
  }, ref: buttonRef, role: 'option', ...buttonProps }, children),
69
- dialogIsOpen && (React.createElement("div", { ...attributes.popper, onBlur: (event) => {
69
+ dialogIsOpen && (React.createElement("div", { onBlur: (event) => {
70
70
  const isBlurredDescendant = event.relatedTarget instanceof Node &&
71
71
  dialogContainer?.contains(event.relatedTarget);
72
72
  if (isBlurredDescendant)
@@ -83,6 +83,6 @@ export const ButtonWithSubmenu = ({ children, className, placement, Submenu, sub
83
83
  }, ref: (element) => {
84
84
  setPopperElement(element);
85
85
  setDialogContainer(element);
86
- }, style: styles.popper, tabIndex: -1, ...submenuContainerProps },
86
+ }, style: styles, tabIndex: -1, ...submenuContainerProps },
87
87
  React.createElement(Submenu, null)))));
88
88
  };
@@ -1,26 +1,20 @@
1
- import type { Placement } from '@popperjs/core';
2
1
  import type { ComponentProps, PropsWithChildren } from 'react';
3
2
  import React from 'react';
3
+ import type { PopperLikePlacement } from './hooks';
4
4
  export interface DialogAnchorOptions {
5
5
  open: boolean;
6
- placement: Placement;
6
+ placement: PopperLikePlacement;
7
7
  referenceElement: HTMLElement | null;
8
8
  allowFlip?: boolean;
9
9
  }
10
10
  export declare function useDialogAnchor<T extends HTMLElement>({ allowFlip, open, placement, referenceElement, }: DialogAnchorOptions): {
11
- attributes: {
12
- [key: string]: {
13
- [key: string]: string;
14
- } | undefined;
15
- };
16
11
  setPopperElement: React.Dispatch<React.SetStateAction<T | null>>;
17
- styles: {
18
- [key: string]: React.CSSProperties;
19
- };
12
+ styles: React.CSSProperties;
20
13
  };
21
14
  export type DialogAnchorProps = PropsWithChildren<Partial<DialogAnchorOptions>> & {
22
15
  id: string;
16
+ dialogManagerId?: string;
23
17
  focus?: boolean;
24
18
  trapFocus?: boolean;
25
19
  } & ComponentProps<'div'>;
26
- export declare const DialogAnchor: ({ allowFlip, children, className, focus, id, placement, referenceElement, tabIndex, trapFocus, ...restDivProps }: DialogAnchorProps) => React.JSX.Element | null;
20
+ export declare const DialogAnchor: ({ allowFlip, children, className, dialogManagerId, focus, id, placement, referenceElement, tabIndex, trapFocus, ...restDivProps }: DialogAnchorProps) => React.JSX.Element | null;
@@ -1,29 +1,22 @@
1
1
  import clsx from 'clsx';
2
2
  import React, { useEffect, useState } from 'react';
3
3
  import { FocusScope } from '@react-aria/focus';
4
- import { usePopper } from 'react-popper';
5
4
  import { DialogPortalEntry } from './DialogPortal';
6
5
  import { useDialog, useDialogIsOpen } from './hooks';
6
+ import { usePopoverPosition } from './hooks/usePopoverPosition';
7
7
  export function useDialogAnchor({ allowFlip, open, placement, referenceElement, }) {
8
8
  const [popperElement, setPopperElement] = useState(null);
9
- const { attributes, styles, update } = usePopper(referenceElement, popperElement, {
10
- modifiers: [
11
- {
12
- enabled: !!allowFlip, // Prevent flipping
13
- name: 'flip',
14
- },
15
- {
16
- name: 'eventListeners',
17
- options: {
18
- // It's not safe to update popper position on resize and scroll, since popper's
19
- // reference element might not be visible at the time.
20
- resize: false,
21
- scroll: false,
22
- },
23
- },
24
- ],
9
+ const { refs, strategy, update, x, y } = usePopoverPosition({
10
+ allowFlip,
11
+ freeze: true,
25
12
  placement,
26
13
  });
14
+ useEffect(() => {
15
+ refs.setReference(referenceElement);
16
+ }, [referenceElement, refs]);
17
+ useEffect(() => {
18
+ refs.setFloating(popperElement);
19
+ }, [popperElement, refs]);
27
20
  useEffect(() => {
28
21
  if (open && popperElement) {
29
22
  // Since the popper's reference element might not be (and usually is not) visible
@@ -31,20 +24,23 @@ export function useDialogAnchor({ allowFlip, open, placement, referenceElement,
31
24
  // update is non-null only if popperElement is non-null
32
25
  update?.();
33
26
  }
34
- }, [open, popperElement, update]);
27
+ }, [open, placement, popperElement, update]);
35
28
  if (popperElement && !open) {
36
29
  setPopperElement(null);
37
30
  }
38
31
  return {
39
- attributes,
40
32
  setPopperElement,
41
- styles,
33
+ styles: {
34
+ left: x ?? 0,
35
+ position: strategy,
36
+ top: y ?? 0,
37
+ },
42
38
  };
43
39
  }
44
- export const DialogAnchor = ({ allowFlip = true, children, className, focus = true, id, placement = 'auto', referenceElement = null, tabIndex, trapFocus, ...restDivProps }) => {
45
- const dialog = useDialog({ id });
46
- const open = useDialogIsOpen(id);
47
- const { attributes, setPopperElement, styles } = useDialogAnchor({
40
+ export const DialogAnchor = ({ allowFlip = true, children, className, dialogManagerId, focus = true, id, placement = 'auto', referenceElement = null, tabIndex, trapFocus, ...restDivProps }) => {
41
+ const dialog = useDialog({ dialogManagerId, id });
42
+ const open = useDialogIsOpen(id, dialogManagerId);
43
+ const { setPopperElement, styles } = useDialogAnchor({
48
44
  allowFlip,
49
45
  open,
50
46
  placement,
@@ -67,7 +63,7 @@ export const DialogAnchor = ({ allowFlip = true, children, className, focus = tr
67
63
  if (!open) {
68
64
  return null;
69
65
  }
70
- return (React.createElement(DialogPortalEntry, { dialogId: id },
66
+ return (React.createElement(DialogPortalEntry, { dialogId: id, dialogManagerId: dialogManagerId },
71
67
  React.createElement(FocusScope, { autoFocus: focus, contain: trapFocus, restoreFocus: true },
72
- React.createElement("div", { ...restDivProps, ...attributes.popper, className: clsx('str-chat__dialog-contents', className), "data-testid": 'str-chat__dialog-contents', ref: setPopperElement, style: styles.popper, tabIndex: typeof tabIndex !== 'undefined' ? tabIndex : 0 }, children))));
68
+ React.createElement("div", { ...restDivProps, className: clsx('str-chat__dialog-contents', className), "data-testid": 'str-chat__dialog-contents', ref: setPopperElement, style: styles, tabIndex: typeof tabIndex !== 'undefined' ? tabIndex : 0 }, children))));
73
69
  };
@@ -3,6 +3,7 @@ import React from 'react';
3
3
  export declare const DialogPortalDestination: () => React.JSX.Element | null;
4
4
  type DialogPortalEntryProps = {
5
5
  dialogId: string;
6
+ dialogManagerId?: string;
6
7
  };
7
- export declare const DialogPortalEntry: ({ children, dialogId, }: PropsWithChildren<DialogPortalEntryProps>) => React.JSX.Element;
8
+ export declare const DialogPortalEntry: ({ children, dialogId, dialogManagerId, }: PropsWithChildren<DialogPortalEntryProps>) => React.JSX.Element;
8
9
  export {};
@@ -1,19 +1,35 @@
1
1
  import React, { useCallback } from 'react';
2
2
  import { useDialogIsOpen, useOpenedDialogCount } from './hooks';
3
3
  import { Portal } from '../Portal/Portal';
4
- import { useDialogManager } from '../../context';
4
+ import { useDialogManager, useNearestDialogManagerContext } from '../../context';
5
5
  export const DialogPortalDestination = () => {
6
- const { dialogManager } = useDialogManager();
7
- const openedDialogCount = useOpenedDialogCount();
6
+ const { dialogManager } = useNearestDialogManagerContext() ?? {};
7
+ const openedDialogCount = useOpenedDialogCount({ dialogManagerId: dialogManager?.id });
8
+ // const [destinationRoot, setDestinationRoot] = useState<HTMLDivElement | null>(null);
9
+ // todo: allow to configure and then enable
10
+ // useEffect(() => {
11
+ // if (!destinationRoot) return;
12
+ // const handleClickOutside = (event: MouseEvent) => {
13
+ // if (!destinationRoot?.contains(event.target as Node)) {
14
+ // dialogManager?.closeAll();
15
+ // }
16
+ // };
17
+ // document.addEventListener('click', handleClickOutside, { capture: true });
18
+ // return () => {
19
+ // document.removeEventListener('click', handleClickOutside, { capture: true });
20
+ // };
21
+ // }, [destinationRoot, dialogManager]);
8
22
  if (!openedDialogCount)
9
23
  return null;
10
- return (React.createElement("div", { className: 'str-chat__dialog-overlay', "data-str-chat__portal-id": dialogManager.id, "data-testid": 'str-chat__dialog-overlay', onClick: () => dialogManager.closeAll(), style: {
24
+ return (React.createElement("div", { className: 'str-chat__dialog-overlay', "data-str-chat__portal-id": dialogManager?.id, "data-testid": 'str-chat__dialog-overlay', onClick: () => dialogManager?.closeAll(),
25
+ // ref={setDestinationRoot}
26
+ style: {
11
27
  '--str-chat__dialog-overlay-height': openedDialogCount > 0 ? '100%' : '0',
12
28
  } }));
13
29
  };
14
- export const DialogPortalEntry = ({ children, dialogId, }) => {
15
- const { dialogManager } = useDialogManager({ dialogId });
16
- const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager.id);
30
+ export const DialogPortalEntry = ({ children, dialogId, dialogManagerId, }) => {
31
+ const { dialogManager } = useDialogManager({ dialogId, dialogManagerId });
32
+ const dialogIsOpen = useDialogIsOpen(dialogId, dialogManagerId);
17
33
  const getPortalDestination = useCallback(() => document.querySelector(`div[data-str-chat__portal-id="${dialogManager.id}"]`), [dialogManager.id]);
18
34
  return (React.createElement(Portal, { getPortalDestination: getPortalDestination, isOpen: dialogIsOpen }, children));
19
35
  };
@@ -1 +1,2 @@
1
1
  export * from './useDialog';
2
+ export type { PopperLikePlacement } from './usePopoverPosition';
@@ -3,6 +3,10 @@ export type UseDialogParams = GetOrCreateDialogParams & {
3
3
  dialogManagerId?: string;
4
4
  };
5
5
  export declare const useDialog: ({ dialogManagerId, id }: UseDialogParams) => import("../DialogManager").Dialog;
6
+ export declare const useDialogOnNearestManager: ({ id }: Pick<UseDialogParams, 'id'>) => {
7
+ dialog: import("../DialogManager").Dialog;
8
+ dialogManager: import("../DialogManager").DialogManager | undefined;
9
+ };
6
10
  export declare const modalDialogId: "modal-dialog";
7
11
  export declare const useModalDialog: () => import("../DialogManager").Dialog;
8
12
  export declare const useDialogIsOpen: (id: string, dialogManagerId?: string) => boolean;
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect } from 'react';
2
- import { modalDialogManagerId, useDialogManager } from '../../../context';
2
+ import { modalDialogManagerId, useDialogManager, useNearestDialogManagerContext, } from '../../../context';
3
3
  import { useStateStore } from '../../../store';
4
4
  export const useDialog = ({ dialogManagerId, id }) => {
5
5
  const { dialogManager } = useDialogManager({ dialogManagerId });
@@ -12,6 +12,14 @@ export const useDialog = ({ dialogManagerId, id }) => {
12
12
  }, [dialogManager, id]);
13
13
  return dialogManager.getOrCreate({ id });
14
14
  };
15
+ export const useDialogOnNearestManager = ({ id }) => {
16
+ const { dialogManager } = useNearestDialogManagerContext() ?? {};
17
+ const dialog = useDialog({ dialogManagerId: dialogManager?.id, id });
18
+ return {
19
+ dialog,
20
+ dialogManager,
21
+ };
22
+ };
15
23
  export const modalDialogId = 'modal-dialog';
16
24
  export const useModalDialog = () => useDialog({ dialogManagerId: modalDialogManagerId, id: modalDialogId });
17
25
  export const useDialogIsOpen = (id, dialogManagerId) => {
@@ -0,0 +1,68 @@
1
+ /// <reference types="react" />
2
+ import { autoUpdate, type Placement } from '@floating-ui/react';
3
+ export type PopperLikePlacement = Placement | 'auto' | 'auto-start' | 'auto-end';
4
+ type OffsetOpt = number | {
5
+ mainAxis?: number;
6
+ crossAxis?: number;
7
+ alignmentAxis?: number;
8
+ } | [crossAxis: number, mainAxis: number];
9
+ export type UsePopoverParams = {
10
+ placement?: PopperLikePlacement;
11
+ /** Add flip() when placement is not 'auto*' */
12
+ allowFlip?: boolean;
13
+ /** Keep in viewport; default true to match common popper setups */
14
+ allowShift?: boolean;
15
+ /** The floating UI is fitted to the available space (by constraining its max size) instead of letting it overflow; default false */
16
+ fitAvailableSpace?: boolean;
17
+ /** Offset (number, object, or [crossAxis, mainAxis] tuple) */
18
+ offset?: OffsetOpt;
19
+ /**
20
+ * Freeze behavior like Popper's eventListeners: { scroll:false, resize:false }.
21
+ * If true → no autoUpdate (you can call `update()` manually).
22
+ */
23
+ freeze?: boolean;
24
+ /**
25
+ * Fine-grained control of autoUpdate triggers (only if freeze=false).
26
+ * Defaults match Popper's "disabled" example when all set to false.
27
+ */
28
+ autoUpdateOptions?: Partial<Parameters<typeof autoUpdate>[3]>;
29
+ };
30
+ export declare function usePopoverPosition({ allowFlip, allowShift, autoUpdateOptions, fitAvailableSpace, freeze, offset, placement, }: UsePopoverParams): {
31
+ placement: Placement;
32
+ strategy: import("@floating-ui/utils").Strategy;
33
+ middlewareData: import("@floating-ui/core").MiddlewareData;
34
+ y: number;
35
+ x: number;
36
+ isPositioned: boolean;
37
+ update: () => void;
38
+ floatingStyles: import("react").CSSProperties;
39
+ refs: {
40
+ reference: import("react").MutableRefObject<import("@floating-ui/react-dom").ReferenceType | null>;
41
+ floating: import("react").MutableRefObject<HTMLElement | null>;
42
+ setReference: (node: import("@floating-ui/react-dom").ReferenceType | null) => void;
43
+ setFloating: (node: HTMLElement | null) => void;
44
+ } & import("@floating-ui/react").ExtendedRefs<import("@floating-ui/react").ReferenceType>;
45
+ elements: {
46
+ reference: import("@floating-ui/react-dom").ReferenceType | null;
47
+ floating: HTMLElement | null;
48
+ } & import("@floating-ui/react").ExtendedElements<import("@floating-ui/react").ReferenceType>;
49
+ context: {
50
+ y: number;
51
+ x: number;
52
+ placement: Placement;
53
+ strategy: import("@floating-ui/utils").Strategy;
54
+ middlewareData: import("@floating-ui/core").MiddlewareData;
55
+ isPositioned: boolean;
56
+ update: () => void;
57
+ floatingStyles: import("react").CSSProperties;
58
+ open: boolean;
59
+ onOpenChange: (open: boolean, event?: Event | undefined, reason?: import("@floating-ui/react").OpenChangeReason | undefined) => void;
60
+ events: import("@floating-ui/react").FloatingEvents;
61
+ dataRef: import("react").MutableRefObject<import("@floating-ui/react").ContextData>;
62
+ nodeId: string | undefined;
63
+ floatingId: string | undefined;
64
+ refs: import("@floating-ui/react").ExtendedRefs<import("@floating-ui/react").ReferenceType>;
65
+ elements: import("@floating-ui/react").ExtendedElements<import("@floating-ui/react").ReferenceType>;
66
+ };
67
+ };
68
+ export {};
@@ -0,0 +1,54 @@
1
+ import { autoPlacement, autoUpdate, flip as flipMw, offset as offsetMw, shift as shiftMw, size as sizeMw, useFloating, } from '@floating-ui/react';
2
+ const hasResizeObserver = typeof window !== 'undefined' && 'ResizeObserver' in window;
3
+ function autoMiddlewareFor(p) {
4
+ if (!String(p).startsWith('auto'))
5
+ return null;
6
+ const alignment = p === 'auto-start' ? 'start' : p === 'auto-end' ? 'end' : undefined;
7
+ return autoPlacement({ alignment });
8
+ }
9
+ function toOffsetMw(opt) {
10
+ if (opt == null)
11
+ return null;
12
+ if (Array.isArray(opt)) {
13
+ const [crossAxis, mainAxis] = opt;
14
+ return offsetMw({ crossAxis, mainAxis });
15
+ }
16
+ if (typeof opt === 'number')
17
+ return offsetMw(opt);
18
+ return offsetMw(opt);
19
+ }
20
+ export function usePopoverPosition({ allowFlip = true, allowShift = true, autoUpdateOptions, fitAvailableSpace = false, freeze = false, offset, placement = 'bottom-start', }) {
21
+ const autoMw = autoMiddlewareFor(placement);
22
+ const offsetMiddleware = toOffsetMw(offset);
23
+ const isSidePlacement = placement.startsWith('left') || placement.startsWith('right');
24
+ const middleware = [
25
+ // offset first (mirrors common Popper setups)
26
+ ...(offsetMiddleware ? [offsetMiddleware] : []),
27
+ // choose between autoPlacement (Popper's "auto*") OR flip()
28
+ // only allow flip when not explicitly 'left*' or 'right*'
29
+ ...(autoMw ? [autoMw] : allowFlip && !isSidePlacement ? [flipMw()] : []),
30
+ // viewport collision adjustments
31
+ ...(allowShift ? [shiftMw({ padding: 8 })] : []),
32
+ // optional size constraining
33
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
34
+ ...(fitAvailableSpace ? [sizeMw({ apply: () => { } })] : []),
35
+ ];
36
+ // if placement is 'auto*', seed with any static placement; autoPlacement will pick the final one
37
+ const seedPlacement = String(placement).startsWith('auto')
38
+ ? 'bottom'
39
+ : placement;
40
+ return useFloating({
41
+ middleware,
42
+ placement: seedPlacement,
43
+ strategy: 'fixed',
44
+ whileElementsMounted: freeze
45
+ ? undefined
46
+ : (reference, floating, update) => autoUpdate(reference, floating, update, {
47
+ ancestorResize: true,
48
+ ancestorScroll: true,
49
+ animationFrame: false,
50
+ elementResize: hasResizeObserver,
51
+ ...autoUpdateOptions,
52
+ }),
53
+ });
54
+ }
@@ -1,6 +1,6 @@
1
1
  import type { PropsWithChildren } from 'react';
2
2
  import React from 'react';
3
- import type { Placement } from '@popperjs/core';
3
+ import type { PopperLikePlacement } from '../Dialog';
4
4
  type DropdownContextValue = {
5
5
  close(): void;
6
6
  };
@@ -8,7 +8,7 @@ export declare const useDropdownContext: () => DropdownContextValue;
8
8
  export type DropdownProps = PropsWithChildren<{
9
9
  className?: string;
10
10
  openButtonProps?: React.HTMLAttributes<HTMLButtonElement>;
11
- placement?: Placement;
11
+ placement?: PopperLikePlacement;
12
12
  }>;
13
13
  export declare const Dropdown: (props: DropdownProps) => React.JSX.Element;
14
14
  export {};
@@ -116,5 +116,5 @@ export const Message = (props) => {
116
116
  notify: addNotification,
117
117
  });
118
118
  const highlighted = highlightedMessageId === message.id;
119
- return (React.createElement(MemoizedMessage, { additionalMessageInputProps: props.additionalMessageInputProps, autoscrollToBottom: props.autoscrollToBottom, canPin: canPin, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: props.customMessageActions, disableQuotedMessages: props.disableQuotedMessages, endOfGroup: props.endOfGroup, firstOfGroup: props.firstOfGroup, formatDate: props.formatDate, groupedByUser: props.groupedByUser, groupStyles: props.groupStyles, handleAction: handleAction, handleDelete: handleDelete, handleFetchReactions: handleFetchReactions, handleFlag: handleFlag, handleMarkUnread: handleMarkUnread, handleMute: handleMute, handleOpenThread: handleOpenThread, handlePin: handlePin, handleReaction: handleReaction, handleRetry: handleRetry, highlighted: highlighted, initialMessage: props.initialMessage, lastReceivedId: props.lastReceivedId, message: message, Message: props.Message, messageActions: props.messageActions, messageListRect: props.messageListRect, mutes: mutes, onMentionsClickMessage: onMentionsClick, onMentionsHoverMessage: onMentionsHover, onUserClick: props.onUserClick, onUserHover: props.onUserHover, pinPermissions: props.pinPermissions, reactionDetailsSort: reactionDetailsSort, readBy: props.readBy, renderText: props.renderText, sortReactionDetails: sortReactionDetails, sortReactions: sortReactions, threadList: props.threadList, unsafeHTML: props.unsafeHTML, userRoles: userRoles }));
119
+ return (React.createElement(MemoizedMessage, { additionalMessageInputProps: props.additionalMessageInputProps, autoscrollToBottom: props.autoscrollToBottom, canPin: canPin, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: props.customMessageActions, deliveredTo: props.deliveredTo, disableQuotedMessages: props.disableQuotedMessages, endOfGroup: props.endOfGroup, firstOfGroup: props.firstOfGroup, formatDate: props.formatDate, groupedByUser: props.groupedByUser, groupStyles: props.groupStyles, handleAction: handleAction, handleDelete: handleDelete, handleFetchReactions: handleFetchReactions, handleFlag: handleFlag, handleMarkUnread: handleMarkUnread, handleMute: handleMute, handleOpenThread: handleOpenThread, handlePin: handlePin, handleReaction: handleReaction, handleRetry: handleRetry, highlighted: highlighted, initialMessage: props.initialMessage, lastReceivedId: props.lastReceivedId, message: message, Message: props.Message, messageActions: props.messageActions, messageListRect: props.messageListRect, mutes: mutes, onMentionsClickMessage: onMentionsClick, onMentionsHoverMessage: onMentionsHover, onUserClick: props.onUserClick, onUserHover: props.onUserHover, pinPermissions: props.pinPermissions, reactionDetailsSort: reactionDetailsSort, readBy: props.readBy, renderText: props.renderText, sortReactionDetails: sortReactionDetails, sortReactions: sortReactions, threadList: props.threadList, unsafeHTML: props.unsafeHTML, userRoles: userRoles }));
120
120
  };
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
2
  import { useTranslationContext } from '../../context/TranslationContext';
3
+ import { useChannelStateContext } from '../../context';
3
4
  const UnMemoizedMessageRepliesCountButton = (props) => {
4
5
  const { labelPlural, labelSingle, onClick, reply_count = 0 } = props;
6
+ const { channelCapabilities } = useChannelStateContext();
5
7
  const { t } = useTranslationContext('MessageRepliesCountButton');
6
8
  if (!reply_count)
7
9
  return null;
@@ -13,6 +15,6 @@ const UnMemoizedMessageRepliesCountButton = (props) => {
13
15
  replyCountText = `1 ${labelSingle}`;
14
16
  }
15
17
  return (React.createElement("div", { className: 'str-chat__message-replies-count-button-wrapper' },
16
- React.createElement("button", { className: 'str-chat__message-replies-count-button', "data-testid": 'replies-count-button', onClick: onClick }, replyCountText)));
18
+ React.createElement("button", { className: 'str-chat__message-replies-count-button', "data-testid": 'replies-count-button', disabled: !channelCapabilities['send-reply'], onClick: onClick }, replyCountText)));
17
19
  };
18
20
  export const MessageRepliesCountButton = React.memo(UnMemoizedMessageRepliesCountButton);
@@ -6,6 +6,7 @@ export type MessageStatusProps = {
6
6
  MessageDeliveredStatus?: React.ComponentType;
7
7
  MessageReadStatus?: React.ComponentType;
8
8
  MessageSendingStatus?: React.ComponentType;
9
+ MessageSentStatus?: React.ComponentType;
9
10
  messageType?: string;
10
11
  tooltipUserNameMapper?: TooltipUsernameMapper;
11
12
  };