stream-chat-react 13.0.5 → 13.1.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 (103) hide show
  1. package/dist/components/Channel/Channel.d.ts +1 -1
  2. package/dist/components/Channel/Channel.js +7 -0
  3. package/dist/components/ChannelList/hooks/useChannelListShape.js +3 -3
  4. package/dist/components/Chat/hooks/useChat.js +7 -3
  5. package/dist/components/Dialog/ButtonWithSubmenu.d.ts +11 -0
  6. package/dist/components/Dialog/ButtonWithSubmenu.js +88 -0
  7. package/dist/components/Dialog/index.d.ts +1 -0
  8. package/dist/components/Dialog/index.js +1 -0
  9. package/dist/components/Loading/LoadingErrorIndicator.js +1 -1
  10. package/dist/components/Message/Message.js +3 -2
  11. package/dist/components/Message/MessageSimple.js +11 -4
  12. package/dist/components/Message/MessageThreadReplyInChannelButtonIndicator.d.ts +2 -0
  13. package/dist/components/Message/MessageThreadReplyInChannelButtonIndicator.js +63 -0
  14. package/dist/components/Message/ReminderNotification.d.ts +6 -0
  15. package/dist/components/Message/ReminderNotification.js +30 -0
  16. package/dist/components/Message/hooks/index.d.ts +1 -0
  17. package/dist/components/Message/hooks/index.js +1 -0
  18. package/dist/components/Message/hooks/useMessageReminder.d.ts +1 -0
  19. package/dist/components/Message/hooks/useMessageReminder.js +11 -0
  20. package/dist/components/Message/index.d.ts +1 -0
  21. package/dist/components/Message/index.js +1 -0
  22. package/dist/components/Message/utils.d.ts +4 -2
  23. package/dist/components/Message/utils.js +11 -1
  24. package/dist/components/MessageActions/MessageActionsBox.js +12 -6
  25. package/dist/components/MessageActions/RemindMeSubmenu.d.ts +6 -0
  26. package/dist/components/MessageActions/RemindMeSubmenu.js +18 -0
  27. package/dist/components/MessageInput/MessageInputFlat.js +5 -3
  28. package/dist/components/MessageInput/SendToChannelCheckbox.d.ts +2 -0
  29. package/dist/components/MessageInput/SendToChannelCheckbox.js +20 -0
  30. package/dist/components/MessageList/MessageListNotifications.js +8 -3
  31. package/dist/components/MessageList/VirtualizedMessageListComponents.d.ts +1 -1
  32. package/dist/components/MessageList/VirtualizedMessageListComponents.js +4 -0
  33. package/dist/components/Notifications/hooks/index.d.ts +1 -0
  34. package/dist/components/Notifications/hooks/index.js +1 -0
  35. package/dist/components/Notifications/hooks/useNotifications.d.ts +2 -0
  36. package/dist/components/Notifications/hooks/useNotifications.js +10 -0
  37. package/dist/components/Notifications/index.d.ts +1 -0
  38. package/dist/components/Notifications/index.js +1 -0
  39. package/dist/components/TextareaComposer/TextareaComposer.js +4 -0
  40. package/dist/components/Thread/LegacyThreadContext.d.ts +8 -0
  41. package/dist/components/Thread/LegacyThreadContext.js +3 -0
  42. package/dist/components/Thread/Thread.d.ts +0 -4
  43. package/dist/components/Thread/Thread.js +2 -3
  44. package/dist/components/Thread/index.d.ts +1 -0
  45. package/dist/components/Thread/index.js +1 -0
  46. package/dist/components/index.d.ts +1 -0
  47. package/dist/components/index.js +1 -0
  48. package/dist/context/ComponentContext.d.ts +6 -1
  49. package/dist/css/v2/index.css +1 -1
  50. package/dist/css/v2/index.layout.css +1 -1
  51. package/dist/experimental/MessageActions/defaults.d.ts +1 -1
  52. package/dist/experimental/MessageActions/defaults.js +27 -4
  53. package/dist/experimental/index.browser.cjs +382 -169
  54. package/dist/experimental/index.browser.cjs.map +4 -4
  55. package/dist/experimental/index.node.cjs +382 -169
  56. package/dist/experimental/index.node.cjs.map +4 -4
  57. package/dist/i18n/Streami18n.d.ts +32 -3
  58. package/dist/i18n/Streami18n.js +34 -5
  59. package/dist/i18n/TranslationBuilder/TranslationBuilder.d.ts +31 -0
  60. package/dist/i18n/TranslationBuilder/TranslationBuilder.js +68 -0
  61. package/dist/i18n/TranslationBuilder/index.d.ts +2 -0
  62. package/dist/i18n/TranslationBuilder/index.js +2 -0
  63. package/dist/i18n/TranslationBuilder/notifications/NotificationTranslationTopic.d.ts +11 -0
  64. package/dist/i18n/TranslationBuilder/notifications/NotificationTranslationTopic.js +27 -0
  65. package/dist/i18n/TranslationBuilder/notifications/attachmentUpload.d.ts +4 -0
  66. package/dist/i18n/TranslationBuilder/notifications/attachmentUpload.js +32 -0
  67. package/dist/i18n/TranslationBuilder/notifications/index.d.ts +1 -0
  68. package/dist/i18n/TranslationBuilder/notifications/index.js +1 -0
  69. package/dist/i18n/TranslationBuilder/notifications/pollComposition.d.ts +3 -0
  70. package/dist/i18n/TranslationBuilder/notifications/pollComposition.js +9 -0
  71. package/dist/i18n/TranslationBuilder/notifications/types.d.ts +4 -0
  72. package/dist/i18n/TranslationBuilder/notifications/types.js +1 -0
  73. package/dist/i18n/de.json +23 -0
  74. package/dist/i18n/en.json +23 -0
  75. package/dist/i18n/es.json +23 -0
  76. package/dist/i18n/fr.json +23 -0
  77. package/dist/i18n/hi.json +23 -0
  78. package/dist/i18n/index.d.ts +1 -0
  79. package/dist/i18n/index.js +1 -0
  80. package/dist/i18n/it.json +23 -0
  81. package/dist/i18n/ja.json +23 -0
  82. package/dist/i18n/ko.json +23 -0
  83. package/dist/i18n/nl.json +23 -0
  84. package/dist/i18n/pt.json +23 -0
  85. package/dist/i18n/ru.json +23 -0
  86. package/dist/i18n/tr.json +23 -0
  87. package/dist/i18n/types.d.ts +54 -0
  88. package/dist/i18n/utils.d.ts +1 -1
  89. package/dist/i18n/utils.js +8 -2
  90. package/dist/index.browser.cjs +3589 -2162
  91. package/dist/index.browser.cjs.map +4 -4
  92. package/dist/index.node.cjs +3645 -2156
  93. package/dist/index.node.cjs.map +4 -4
  94. package/dist/plugins/Emojis/index.browser.cjs +1 -2
  95. package/dist/plugins/Emojis/index.browser.cjs.map +3 -3
  96. package/dist/plugins/Emojis/index.node.cjs +1 -2
  97. package/dist/plugins/Emojis/index.node.cjs.map +3 -3
  98. package/dist/scss/v2/Message/Message-layout.scss +11 -1
  99. package/dist/scss/v2/Message/Message-theme.scss +31 -1
  100. package/dist/scss/v2/MessageActionsBox/MessageActionsBox-theme.scss +8 -0
  101. package/dist/scss/v2/MessageInput/MessageInput-layout.scss +19 -0
  102. package/dist/scss/v2/MessageInput/MessageInput-theme.scss +11 -0
  103. package/package.json +6 -8
@@ -5,7 +5,7 @@ import type { OnMentionAction } from './hooks/useMentionsHandlers';
5
5
  import type { LoadingErrorIndicatorProps } from '../Loading';
6
6
  import type { ComponentContextValue } from '../../context';
7
7
  import type { ChannelUnreadUiState, GiphyVersions, ImageAttachmentSizeHandler, VideoAttachmentSizeHandler } from '../../types/types';
8
- type ChannelPropsForwardedToComponentContext = Pick<ComponentContextValue, 'Attachment' | 'AttachmentPreviewList' | 'AttachmentSelector' | 'AttachmentSelectorInitiationButtonContents' | 'AudioRecorder' | 'AutocompleteSuggestionItem' | 'AutocompleteSuggestionList' | 'Avatar' | 'BaseImage' | 'CooldownTimer' | 'CustomMessageActionsList' | 'DateSeparator' | 'EditMessageInput' | 'EmojiPicker' | 'emojiSearchIndex' | 'EmptyStateIndicator' | 'FileUploadIcon' | 'GiphyPreviewMessage' | 'HeaderComponent' | 'Input' | 'LinkPreviewList' | 'LoadingIndicator' | 'Message' | 'MessageActions' | 'MessageBouncePrompt' | 'MessageBlocked' | 'MessageDeleted' | 'MessageListNotifications' | 'MessageListMainPanel' | 'MessageNotification' | 'MessageOptions' | 'MessageRepliesCountButton' | 'MessageStatus' | 'MessageSystem' | 'MessageTimestamp' | 'ModalGallery' | 'PinIndicator' | 'PollActions' | 'PollContent' | 'PollCreationDialog' | 'PollHeader' | 'PollOptionSelector' | 'QuotedMessage' | 'QuotedMessagePreview' | 'QuotedPoll' | 'reactionOptions' | 'ReactionSelector' | 'ReactionsList' | 'ReactionsListModal' | 'SendButton' | 'StartRecordingAudioButton' | 'TextareaComposer' | 'ThreadHead' | 'ThreadHeader' | 'ThreadStart' | 'Timestamp' | 'TypingIndicator' | 'UnreadMessagesNotification' | 'UnreadMessagesSeparator' | 'VirtualMessage' | 'StopAIGenerationButton' | 'StreamedMessageText'>;
8
+ type ChannelPropsForwardedToComponentContext = Pick<ComponentContextValue, 'Attachment' | 'AttachmentPreviewList' | 'AttachmentSelector' | 'AttachmentSelectorInitiationButtonContents' | 'AudioRecorder' | 'AutocompleteSuggestionItem' | 'AutocompleteSuggestionList' | 'Avatar' | 'BaseImage' | 'CooldownTimer' | 'CustomMessageActionsList' | 'DateSeparator' | 'EditMessageInput' | 'EmojiPicker' | 'emojiSearchIndex' | 'EmptyStateIndicator' | 'FileUploadIcon' | 'GiphyPreviewMessage' | 'HeaderComponent' | 'Input' | 'LinkPreviewList' | 'LoadingIndicator' | 'Message' | 'MessageActions' | 'MessageBouncePrompt' | 'MessageBlocked' | 'MessageDeleted' | 'MessageIsThreadReplyInChannelButtonIndicator' | 'MessageListNotifications' | 'MessageListMainPanel' | 'MessageNotification' | 'MessageOptions' | 'MessageRepliesCountButton' | 'MessageStatus' | 'MessageSystem' | 'MessageTimestamp' | 'ModalGallery' | 'PinIndicator' | 'PollActions' | 'PollContent' | 'PollCreationDialog' | 'PollHeader' | 'PollOptionSelector' | 'QuotedMessage' | 'QuotedMessagePreview' | 'QuotedPoll' | 'reactionOptions' | 'ReactionSelector' | 'ReactionsList' | 'ReactionsListModal' | 'ReminderNotification' | 'SendButton' | 'SendToChannelCheckbox' | 'StartRecordingAudioButton' | 'TextareaComposer' | 'ThreadHead' | 'ThreadHeader' | 'ThreadStart' | 'Timestamp' | 'TypingIndicator' | 'UnreadMessagesNotification' | 'UnreadMessagesSeparator' | 'VirtualMessage' | 'StopAIGenerationButton' | 'StreamedMessageText'>;
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;
@@ -69,6 +69,7 @@ const ChannelInner = (props) => {
69
69
  ...initialState,
70
70
  hasMore: channel.state.messagePagination.hasPrev,
71
71
  loading: !channel.initialized,
72
+ messages: channel.state.messages,
72
73
  });
73
74
  const jumpToMessageFromSearch = useSearchFocusedMessage();
74
75
  const isMounted = useIsMounted();
@@ -731,6 +732,7 @@ const ChannelInner = (props) => {
731
732
  MessageBlocked: props.MessageBlocked,
732
733
  MessageBouncePrompt: props.MessageBouncePrompt,
733
734
  MessageDeleted: props.MessageDeleted,
735
+ MessageIsThreadReplyInChannelButtonIndicator: props.MessageIsThreadReplyInChannelButtonIndicator,
734
736
  MessageListNotifications: props.MessageListNotifications,
735
737
  MessageNotification: props.MessageNotification,
736
738
  MessageOptions: props.MessageOptions,
@@ -752,7 +754,9 @@ const ChannelInner = (props) => {
752
754
  ReactionSelector: props.ReactionSelector,
753
755
  ReactionsList: props.ReactionsList,
754
756
  ReactionsListModal: props.ReactionsListModal,
757
+ ReminderNotification: props.ReminderNotification,
755
758
  SendButton: props.SendButton,
759
+ SendToChannelCheckbox: props.SendToChannelCheckbox,
756
760
  StartRecordingAudioButton: props.StartRecordingAudioButton,
757
761
  StopAIGenerationButton: props.StopAIGenerationButton,
758
762
  StreamedMessageText: props.StreamedMessageText,
@@ -793,6 +797,7 @@ const ChannelInner = (props) => {
793
797
  props.MessageBlocked,
794
798
  props.MessageBouncePrompt,
795
799
  props.MessageDeleted,
800
+ props.MessageIsThreadReplyInChannelButtonIndicator,
796
801
  props.MessageListNotifications,
797
802
  props.MessageNotification,
798
803
  props.MessageOptions,
@@ -814,7 +819,9 @@ const ChannelInner = (props) => {
814
819
  props.ReactionSelector,
815
820
  props.ReactionsList,
816
821
  props.ReactionsListModal,
822
+ props.ReminderNotification,
817
823
  props.SendButton,
824
+ props.SendToChannelCheckbox,
818
825
  props.StartRecordingAudioButton,
819
826
  props.StopAIGenerationButton,
820
827
  props.StreamedMessageText,
@@ -162,13 +162,13 @@ export const useChannelListShapeDefaults = () => {
162
162
  if (typeof customHandler === 'function') {
163
163
  return customHandler(setChannels, event);
164
164
  }
165
- if (!event.channel) {
165
+ if (!event.channel_id && !event.channel_type) {
166
166
  return;
167
167
  }
168
168
  const channel = await getChannel({
169
169
  client,
170
- id: event.channel.id,
171
- type: event.channel.type,
170
+ id: event.channel_id,
171
+ type: event.channel_type,
172
172
  });
173
173
  const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
174
174
  if (isChannelArchived(channel) && considerArchivedChannels && !filters.archived) {
@@ -1,8 +1,8 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
- import { defaultDateTimeParser, isLanguageSupported, Streami18n } from '../../../i18n';
2
+ import { defaultDateTimeParser, defaultTranslatorFunction, isLanguageSupported, Streami18n, } from '../../../i18n';
3
3
  export const useChat = ({ client, defaultLanguage = 'en', i18nInstance, initialNavOpen, }) => {
4
4
  const [translators, setTranslators] = useState({
5
- t: (key) => key,
5
+ t: defaultTranslatorFunction,
6
6
  tDateTimeParser: defaultDateTimeParser,
7
7
  userLanguage: 'en',
8
8
  });
@@ -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.0.5";
27
+ const version = "13.1.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'
@@ -33,9 +33,13 @@ export const useChat = ({ client, defaultLanguage = 'en', i18nInstance, initialN
33
33
  }
34
34
  client.threads.registerSubscriptions();
35
35
  client.polls.registerSubscriptions();
36
+ client.reminders.registerSubscriptions();
37
+ client.reminders.initTimers();
36
38
  return () => {
37
39
  client.threads.unregisterSubscriptions();
38
40
  client.polls.unregisterSubscriptions();
41
+ client.reminders.unregisterSubscriptions();
42
+ client.reminders.clearTimers();
39
43
  };
40
44
  }, [client]);
41
45
  useEffect(() => {
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import type { ComponentProps, ComponentType } from 'react';
3
+ import type { Placement } from '@popperjs/core';
4
+ type ButtonWithSubmenu = ComponentProps<'button'> & {
5
+ children: React.ReactNode;
6
+ placement: Placement;
7
+ Submenu: ComponentType;
8
+ submenuContainerProps?: ComponentProps<'div'>;
9
+ };
10
+ export declare const ButtonWithSubmenu: ({ children, className, placement, Submenu, submenuContainerProps, ...buttonProps }: ButtonWithSubmenu) => React.JSX.Element;
11
+ export {};
@@ -0,0 +1,88 @@
1
+ import clsx from 'clsx';
2
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useDialog, useDialogIsOpen } from './hooks';
4
+ import { useDialogAnchor } from './DialogAnchor';
5
+ export const ButtonWithSubmenu = ({ children, className, placement, Submenu, submenuContainerProps, ...buttonProps }) => {
6
+ const buttonRef = useRef(null);
7
+ const [dialogContainer, setDialogContainer] = useState(null);
8
+ const keepSubmenuOpen = useRef(false);
9
+ const dialogCloseTimeout = useRef(null);
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({
14
+ open: dialogIsOpen,
15
+ placement,
16
+ referenceElement: buttonRef.current,
17
+ });
18
+ const closeDialogLazily = useCallback(() => {
19
+ if (dialogCloseTimeout.current)
20
+ clearTimeout(dialogCloseTimeout.current);
21
+ dialogCloseTimeout.current = setTimeout(() => {
22
+ if (keepSubmenuOpen.current)
23
+ return;
24
+ dialog.close();
25
+ }, 100);
26
+ }, [dialog]);
27
+ const handleClose = useCallback((event) => {
28
+ const parentButton = buttonRef.current;
29
+ if (!dialogIsOpen || !parentButton)
30
+ return;
31
+ event.stopPropagation();
32
+ closeDialogLazily();
33
+ parentButton.focus();
34
+ }, [closeDialogLazily, dialogIsOpen, buttonRef]);
35
+ const handleFocusParentButton = () => {
36
+ if (dialogIsOpen)
37
+ return;
38
+ dialog.open();
39
+ keepSubmenuOpen.current = true;
40
+ };
41
+ useEffect(() => {
42
+ const parentButton = buttonRef.current;
43
+ if (!dialogIsOpen || !parentButton)
44
+ return;
45
+ const hideOnEscape = (event) => {
46
+ if (event.key !== 'Escape')
47
+ return;
48
+ handleClose(event);
49
+ keepSubmenuOpen.current = false;
50
+ };
51
+ document.addEventListener('keyup', hideOnEscape, { capture: true });
52
+ return () => {
53
+ document.removeEventListener('keyup', hideOnEscape, { capture: true });
54
+ };
55
+ }, [dialogIsOpen, handleClose]);
56
+ return (React.createElement(React.Fragment, null,
57
+ React.createElement("button", { "aria-selected": 'false', className: clsx(className, 'str_chat__button-with-submenu', {
58
+ 'str_chat__button-with-submenu--submenu-open': dialogIsOpen,
59
+ }), onBlur: () => {
60
+ keepSubmenuOpen.current = false;
61
+ closeDialogLazily();
62
+ }, onClick: (event) => {
63
+ event.stopPropagation();
64
+ dialog.toggle();
65
+ }, onFocus: handleFocusParentButton, onMouseEnter: handleFocusParentButton, onMouseLeave: () => {
66
+ keepSubmenuOpen.current = false;
67
+ closeDialogLazily();
68
+ }, ref: buttonRef, role: 'option', ...buttonProps }, children),
69
+ dialogIsOpen && (React.createElement("div", { ...attributes.popper, onBlur: (event) => {
70
+ const isBlurredDescendant = event.relatedTarget instanceof Node &&
71
+ dialogContainer?.contains(event.relatedTarget);
72
+ if (isBlurredDescendant)
73
+ return;
74
+ keepSubmenuOpen.current = false;
75
+ closeDialogLazily();
76
+ }, onFocus: () => {
77
+ keepSubmenuOpen.current = true;
78
+ }, onMouseEnter: () => {
79
+ keepSubmenuOpen.current = true;
80
+ }, onMouseLeave: () => {
81
+ keepSubmenuOpen.current = false;
82
+ closeDialogLazily();
83
+ }, ref: (element) => {
84
+ setPopperElement(element);
85
+ setDialogContainer(element);
86
+ }, style: styles.popper, tabIndex: -1, ...submenuContainerProps },
87
+ React.createElement(Submenu, null)))));
88
+ };
@@ -1,3 +1,4 @@
1
+ export * from './ButtonWithSubmenu';
1
2
  export * from './DialogAnchor';
2
3
  export * from './DialogManager';
3
4
  export * from './DialogPortal';
@@ -1,3 +1,4 @@
1
+ export * from './ButtonWithSubmenu';
1
2
  export * from './DialogAnchor';
2
3
  export * from './DialogManager';
3
4
  export * from './DialogPortal';
@@ -7,6 +7,6 @@ const UnMemoizedLoadingErrorIndicator = ({ error }) => {
7
7
  const { t } = useTranslationContext('LoadingErrorIndicator');
8
8
  if (!error)
9
9
  return null;
10
- return (React.createElement("div", null, t('Error: {{ errorMessage }}', { errorMessage: error.message })));
10
+ return React.createElement("div", null, t('Error: {{ errorMessage }}', { errorMessage: error.message }));
11
11
  };
12
12
  export const LoadingErrorIndicator = React.memo(UnMemoizedLoadingErrorIndicator, (prevProps, nextProps) => prevProps.error?.message === nextProps.error?.message);
@@ -6,7 +6,7 @@ import { MessageSimple as DefaultMessage } from './MessageSimple';
6
6
  const MessageWithContext = (props) => {
7
7
  const { canPin, groupedByUser, Message: propMessage, message, messageActions = Object.keys(MESSAGE_ACTIONS), onUserClick: propOnUserClick, onUserHover: propOnUserHover, userRoles, } = props;
8
8
  const { client, isMessageAIGenerated } = useChatContext('Message');
9
- const { read } = useChannelStateContext('Message');
9
+ const { channelConfig, read } = useChannelStateContext('Message');
10
10
  const { Message: contextMessage } = useComponentContext('Message');
11
11
  const actionsEnabled = message.type === 'regular' && message.status === 'received';
12
12
  const MessageUIComponent = propMessage ?? contextMessage ?? DefaultMessage;
@@ -33,7 +33,7 @@ const MessageWithContext = (props) => {
33
33
  canQuote,
34
34
  canReact,
35
35
  canReply,
36
- }), [
36
+ }, channelConfig), [
37
37
  messageActions,
38
38
  canDelete,
39
39
  canEdit,
@@ -44,6 +44,7 @@ const MessageWithContext = (props) => {
44
44
  canQuote,
45
45
  canReact,
46
46
  canReply,
47
+ channelConfig,
47
48
  ]);
48
49
  const { canPin: canPinPropToNotPass, // eslint-disable-line @typescript-eslint/no-unused-vars
49
50
  messageActions: messageActionsPropToNotPass, // eslint-disable-line @typescript-eslint/no-unused-vars
@@ -9,6 +9,11 @@ import { MessageRepliesCountButton as DefaultMessageRepliesCountButton } from '.
9
9
  import { MessageStatus as DefaultMessageStatus } from './MessageStatus';
10
10
  import { MessageText } from './MessageText';
11
11
  import { MessageTimestamp as DefaultMessageTimestamp } from './MessageTimestamp';
12
+ import { StreamedMessageText as DefaultStreamedMessageText } from './StreamedMessageText';
13
+ import { isDateSeparatorMessage } from '../MessageList';
14
+ import { MessageThreadReplyInChannelButtonIndicator as DefaultMessageIsThreadReplyInChannelButtonIndicator } from './MessageThreadReplyInChannelButtonIndicator';
15
+ import { ReminderNotification as DefaultReminderNotification } from './ReminderNotification';
16
+ import { useMessageReminder } from './hooks';
12
17
  import { areMessageUIPropsEqual, isMessageBlocked, isMessageBounced, isMessageEdited, messageHasAttachments, messageHasReactions, } from './utils';
13
18
  import { Avatar as DefaultAvatar } from '../Avatar';
14
19
  import { Attachment as DefaultAttachment } from '../Attachment';
@@ -21,18 +26,17 @@ import { useComponentContext } from '../../context/ComponentContext';
21
26
  import { useMessageContext } from '../../context/MessageContext';
22
27
  import { useChatContext, useTranslationContext } from '../../context';
23
28
  import { MessageEditedTimestamp } from './MessageEditedTimestamp';
24
- import { StreamedMessageText as DefaultStreamedMessageText } from './StreamedMessageText';
25
- import { isDateSeparatorMessage } from '../MessageList';
26
29
  const MessageSimpleWithContext = (props) => {
27
30
  const { additionalMessageInputProps, editing, endOfGroup, firstOfGroup, groupedByUser, handleAction, handleOpenThread, handleRetry, highlighted, isMessageAIGenerated, isMyMessage, message, onUserClick, onUserHover, renderText, threadList, } = props;
28
31
  const { client } = useChatContext('MessageSimple');
29
32
  const { t } = useTranslationContext('MessageSimple');
30
33
  const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false);
31
34
  const [isEditedTimestampOpen, setEditedTimestampOpen] = useState(false);
35
+ const reminder = useMessageReminder(message.id);
32
36
  const { Attachment = DefaultAttachment, Avatar = DefaultAvatar, MessageOptions = DefaultMessageOptions,
33
37
  // TODO: remove this "passthrough" in the next
34
38
  // major release and use the new default instead
35
- MessageActions = MessageOptions, MessageBlocked = DefaultMessageBlocked, MessageDeleted = DefaultMessageDeleted, MessageBouncePrompt = DefaultMessageBouncePrompt, MessageRepliesCountButton = DefaultMessageRepliesCountButton, MessageStatus = DefaultMessageStatus, MessageTimestamp = DefaultMessageTimestamp, ReactionsList = DefaultReactionList, StreamedMessageText = DefaultStreamedMessageText, PinIndicator, } = useComponentContext('MessageSimple');
39
+ MessageActions = MessageOptions, MessageBlocked = DefaultMessageBlocked, MessageBouncePrompt = DefaultMessageBouncePrompt, MessageDeleted = DefaultMessageDeleted, MessageIsThreadReplyInChannelButtonIndicator = DefaultMessageIsThreadReplyInChannelButtonIndicator, MessageRepliesCountButton = DefaultMessageRepliesCountButton, MessageStatus = DefaultMessageStatus, MessageTimestamp = DefaultMessageTimestamp, ReactionsList = DefaultReactionList, ReminderNotification = DefaultReminderNotification, StreamedMessageText = DefaultStreamedMessageText, PinIndicator, } = useComponentContext('MessageSimple');
36
40
  const hasAttachment = messageHasAttachments(message);
37
41
  const hasReactions = messageHasReactions(message);
38
42
  const isAIGenerated = useMemo(() => isMessageAIGenerated?.(message), [isMessageAIGenerated, message]);
@@ -47,6 +51,7 @@ const MessageSimpleWithContext = (props) => {
47
51
  }
48
52
  const showMetadata = !groupedByUser || endOfGroup;
49
53
  const showReplyCountButton = !threadList && !!message.reply_count;
54
+ const showIsReplyInChannel = !threadList && message.show_in_channel && message.parent_id;
50
55
  const allowRetry = message.status === 'failed' && message.error?.status !== 403;
51
56
  const isBounced = isMessageBounced(message);
52
57
  const isEdited = isMessageEdited(message) && !isAIGenerated;
@@ -68,7 +73,7 @@ const MessageSimpleWithContext = (props) => {
68
73
  'str-chat__message--pinned pinned-message': message.pinned,
69
74
  'str-chat__message--with-reactions': hasReactions,
70
75
  'str-chat__message-send-can-be-retried': message?.status === 'failed' && message?.error?.status !== 403,
71
- 'str-chat__message-with-thread-link': showReplyCountButton,
76
+ 'str-chat__message-with-thread-link': showReplyCountButton || showIsReplyInChannel,
72
77
  'str-chat__virtual-message__wrapper--end': endOfGroup,
73
78
  'str-chat__virtual-message__wrapper--first': firstOfGroup,
74
79
  'str-chat__virtual-message__wrapper--group': groupedByUser,
@@ -79,6 +84,7 @@ const MessageSimpleWithContext = (props) => {
79
84
  isBounceDialogOpen && (React.createElement(MessageBounceModal, { MessageBouncePrompt: MessageBouncePrompt, onClose: () => setIsBounceDialogOpen(false), open: isBounceDialogOpen })),
80
85
  React.createElement("div", { className: rootClassName, key: message.id },
81
86
  PinIndicator && React.createElement(PinIndicator, null),
87
+ !!reminder && React.createElement(ReminderNotification, { reminder: reminder }),
82
88
  message.user && (React.createElement(Avatar, { image: message.user.image, name: message.user.name || message.user.id, onClick: onUserClick, onMouseOver: onUserHover, user: message.user })),
83
89
  React.createElement("div", { className: clsx('str-chat__message-inner', {
84
90
  'str-chat__simple-message--error-failed': allowRetry || isBounced,
@@ -92,6 +98,7 @@ const MessageSimpleWithContext = (props) => {
92
98
  message.mml && (React.createElement(MML, { actionHandler: handleAction, align: isMyMessage() ? 'right' : 'left', source: message.mml })),
93
99
  React.createElement(MessageErrorIcon, null))),
94
100
  showReplyCountButton && (React.createElement(MessageRepliesCountButton, { onClick: handleOpenThread, reply_count: message.reply_count })),
101
+ showIsReplyInChannel && React.createElement(MessageIsThreadReplyInChannelButtonIndicator, null),
95
102
  showMetadata && (React.createElement("div", { className: 'str-chat__message-metadata' },
96
103
  React.createElement(MessageStatus, null),
97
104
  !isMyMessage() && !!message.user && (React.createElement("span", { className: 'str-chat__message-simple-name' }, message.user.name || message.user.id)),
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const MessageThreadReplyInChannelButtonIndicator: () => React.JSX.Element | null;
@@ -0,0 +1,63 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { formatMessage } from 'stream-chat';
3
+ import { useChannelActionContext, useChannelStateContext, useChatContext, useMessageContext, useTranslationContext, } from '../../context';
4
+ export const MessageThreadReplyInChannelButtonIndicator = () => {
5
+ const { client } = useChatContext();
6
+ const { t } = useTranslationContext();
7
+ const { channel } = useChannelStateContext();
8
+ const { openThread } = useChannelActionContext();
9
+ const { message } = useMessageContext();
10
+ const parentMessageRef = useRef(undefined);
11
+ const querySearchParent = () => channel
12
+ .getClient()
13
+ .search({ cid: channel.cid }, { id: message.parent_id })
14
+ .then(({ results }) => {
15
+ if (!results.length) {
16
+ throw new Error('Thread has not been found');
17
+ }
18
+ parentMessageRef.current = formatMessage(results[0].message);
19
+ })
20
+ .catch((error) => {
21
+ client.notifications.addError({
22
+ message: t('Thread has not been found'),
23
+ options: {
24
+ originalError: error,
25
+ type: 'api:message:search:not-found',
26
+ },
27
+ origin: {
28
+ context: { threadReply: message },
29
+ emitter: 'MessageThreadReplyInChannelButtonIndicator',
30
+ },
31
+ });
32
+ });
33
+ useEffect(() => {
34
+ if (parentMessageRef.current ||
35
+ parentMessageRef.current === null ||
36
+ !message.parent_id)
37
+ return;
38
+ const localMessage = channel.state.findMessage(message.parent_id);
39
+ if (localMessage) {
40
+ parentMessageRef.current = localMessage;
41
+ return;
42
+ }
43
+ }, [channel, message]);
44
+ if (!message.parent_id)
45
+ return null;
46
+ return (React.createElement("div", { className: 'str-chat__message-is-thread-reply-button-wrapper' },
47
+ React.createElement("button", { className: 'str-chat__message-is-thread-reply-button', "data-testid": 'message-is-thread-reply-button', onClick: async () => {
48
+ if (!parentMessageRef.current) {
49
+ // search query is performed here in order to prevent multiple search queries in useEffect
50
+ // due to the message list 3x remounting its items
51
+ await querySearchParent();
52
+ if (parentMessageRef.current) {
53
+ openThread(parentMessageRef.current);
54
+ }
55
+ else {
56
+ // prevent further search queries if the message is not found in the DB
57
+ parentMessageRef.current = null;
58
+ }
59
+ return;
60
+ }
61
+ openThread(parentMessageRef.current);
62
+ }, type: 'button' }, t('Thread reply'))));
63
+ };
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import type { Reminder } from 'stream-chat';
3
+ export type ReminderNotificationProps = {
4
+ reminder?: Reminder;
5
+ };
6
+ export declare const ReminderNotification: ({ reminder }: ReminderNotificationProps) => React.JSX.Element;
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useTranslationContext } from '../../context';
3
+ import { useStateStore } from '../../store';
4
+ const reminderStateSelector = (state) => ({
5
+ timeLeftMs: state.timeLeftMs,
6
+ });
7
+ export const ReminderNotification = ({ reminder }) => {
8
+ const { t } = useTranslationContext();
9
+ const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {};
10
+ const stopRefreshBoundaryMs = reminder?.timer.stopRefreshBoundaryMs;
11
+ const stopRefreshTimeStamp = reminder?.remindAt && stopRefreshBoundaryMs
12
+ ? reminder?.remindAt.getTime() + stopRefreshBoundaryMs
13
+ : undefined;
14
+ const isBehindRefreshBoundary = !!stopRefreshTimeStamp && new Date().getTime() > stopRefreshTimeStamp;
15
+ return (React.createElement("p", { className: 'str-chat__message-reminder' },
16
+ React.createElement("span", null, t('Saved for later')),
17
+ reminder?.remindAt && timeLeftMs !== null && (React.createElement(React.Fragment, null,
18
+ React.createElement("span", null, " | "),
19
+ React.createElement("span", null, isBehindRefreshBoundary
20
+ ? t('Due since {{ dueSince }}', {
21
+ dueSince: t(`timestamp/ReminderNotification`, {
22
+ timestamp: reminder.remindAt,
23
+ }),
24
+ })
25
+ : t(`Due {{ timeLeft }}`, {
26
+ timeLeft: t('duration/Message reminder', {
27
+ milliseconds: timeLeftMs,
28
+ }),
29
+ }))))));
30
+ };
@@ -13,3 +13,4 @@ export * from './useUserHandler';
13
13
  export * from './useUserRole';
14
14
  export * from './useReactionsFetcher';
15
15
  export * from './useMessageTextStreaming';
16
+ export * from './useMessageReminder';
@@ -13,3 +13,4 @@ export * from './useUserHandler';
13
13
  export * from './useUserRole';
14
14
  export * from './useReactionsFetcher';
15
15
  export * from './useMessageTextStreaming';
16
+ export * from './useMessageReminder';
@@ -0,0 +1 @@
1
+ export declare const useMessageReminder: (messageId: string) => import("stream-chat").Reminder | undefined;
@@ -0,0 +1,11 @@
1
+ import { useCallback } from 'react';
2
+ import { useChatContext } from '../../../context';
3
+ import { useStateStore } from '../../../store';
4
+ export const useMessageReminder = (messageId) => {
5
+ const { client } = useChatContext();
6
+ const reminderSelector = useCallback((state) => ({
7
+ reminder: state.reminders.get(messageId),
8
+ }), [messageId]);
9
+ const { reminder } = useStateStore(client.reminders.state, reminderSelector);
10
+ return reminder;
11
+ };
@@ -10,6 +10,7 @@ export * from './MessageStatus';
10
10
  export * from './MessageText';
11
11
  export * from './MessageTimestamp';
12
12
  export * from './QuotedMessage';
13
+ export * from './ReminderNotification';
13
14
  export * from './renderText';
14
15
  export * from './types';
15
16
  export * from './utils';
@@ -10,6 +10,7 @@ export * from './MessageStatus';
10
10
  export * from './MessageText';
11
11
  export * from './MessageTimestamp';
12
12
  export * from './QuotedMessage';
13
+ export * from './ReminderNotification';
13
14
  export * from './renderText';
14
15
  export * from './types';
15
16
  export * from './utils';
@@ -1,5 +1,5 @@
1
1
  import type { TFunction } from 'i18next';
2
- import type { LocalMessage, MessageResponse, Mute, StreamChat, UserResponse } from 'stream-chat';
2
+ import type { ChannelConfigWithInfo, LocalMessage, MessageResponse, Mute, StreamChat, UserResponse } from 'stream-chat';
3
3
  import type { PinPermissions } from './hooks';
4
4
  import type { MessageProps } from './types';
5
5
  import type { ComponentContextValue, CustomMessageActions, MessageContextValue } from '../../context';
@@ -21,7 +21,9 @@ export declare const MESSAGE_ACTIONS: {
21
21
  pin: string;
22
22
  quote: string;
23
23
  react: string;
24
+ remindMe: string;
24
25
  reply: string;
26
+ saveForLater: string;
25
27
  };
26
28
  export type MessageActionsArray<T extends string = string> = Array<keyof typeof MESSAGE_ACTIONS | T>;
27
29
  export declare const defaultPinPermissions: PinPermissions;
@@ -36,7 +38,7 @@ export type Capabilities = {
36
38
  canReact?: boolean;
37
39
  canReply?: boolean;
38
40
  };
39
- export declare const getMessageActions: (actions: MessageActionsArray | boolean, { canDelete, canEdit, canFlag, canMarkUnread, canMute, canPin, canQuote, canReact, canReply, }: Capabilities) => MessageActionsArray<string>;
41
+ export declare const getMessageActions: (actions: MessageActionsArray | boolean, { canDelete, canEdit, canFlag, canMarkUnread, canMute, canPin, canQuote, canReact, canReply, }: Capabilities, channelConfig?: ChannelConfigWithInfo) => MessageActionsArray<string>;
40
42
  export declare const ACTIONS_NOT_WORKING_IN_THREAD: string[];
41
43
  /**
42
44
  * @deprecated use `shouldRenderMessageActions` instead
@@ -35,7 +35,9 @@ export const MESSAGE_ACTIONS = {
35
35
  pin: 'pin',
36
36
  quote: 'quote',
37
37
  react: 'react',
38
+ remindMe: 'remindMe',
38
39
  reply: 'reply',
40
+ saveForLater: 'saveForLater',
39
41
  };
40
42
  // @deprecated in favor of `channelCapabilities` - TODO: remove in next major release
41
43
  export const defaultPinPermissions = {
@@ -95,7 +97,7 @@ export const defaultPinPermissions = {
95
97
  user: false,
96
98
  },
97
99
  };
98
- export const getMessageActions = (actions, { canDelete, canEdit, canFlag, canMarkUnread, canMute, canPin, canQuote, canReact, canReply, }) => {
100
+ export const getMessageActions = (actions, { canDelete, canEdit, canFlag, canMarkUnread, canMute, canPin, canQuote, canReact, canReply, }, channelConfig) => {
99
101
  const messageActionsAfterPermission = [];
100
102
  let messageActions = [];
101
103
  if (actions && typeof actions === 'boolean') {
@@ -132,9 +134,17 @@ export const getMessageActions = (actions, { canDelete, canEdit, canFlag, canMar
132
134
  if (canReact && messageActions.indexOf(MESSAGE_ACTIONS.react) > -1) {
133
135
  messageActionsAfterPermission.push(MESSAGE_ACTIONS.react);
134
136
  }
137
+ if (channelConfig?.['user_message_reminders'] &&
138
+ messageActions.indexOf(MESSAGE_ACTIONS.remindMe)) {
139
+ messageActionsAfterPermission.push(MESSAGE_ACTIONS.remindMe);
140
+ }
135
141
  if (canReply && messageActions.indexOf(MESSAGE_ACTIONS.reply) > -1) {
136
142
  messageActionsAfterPermission.push(MESSAGE_ACTIONS.reply);
137
143
  }
144
+ if (channelConfig?.['user_message_reminders'] &&
145
+ messageActions.indexOf(MESSAGE_ACTIONS.saveForLater)) {
146
+ messageActionsAfterPermission.push(MESSAGE_ACTIONS.saveForLater);
147
+ }
138
148
  return messageActionsAfterPermission;
139
149
  };
140
150
  export const ACTIONS_NOT_WORKING_IN_THREAD = [
@@ -1,17 +1,19 @@
1
1
  import clsx from 'clsx';
2
2
  import React from 'react';
3
- import { MESSAGE_ACTIONS } from '../Message/utils';
4
- import { useComponentContext, useMessageContext, useTranslationContext, } from '../../context';
5
3
  import { CustomMessageActionsList as DefaultCustomMessageActionsList } from './CustomMessageActionsList';
4
+ import { RemindMeActionButton } from './RemindMeSubmenu';
5
+ import { useMessageReminder } from '../Message';
6
6
  import { useMessageComposer } from '../MessageInput';
7
+ import { useChatContext, useComponentContext, useMessageContext, useTranslationContext, } from '../../context';
8
+ import { MESSAGE_ACTIONS } from '../Message/utils';
7
9
  const UnMemoizedMessageActionsBox = (props) => {
8
- const { className, getMessageActions, handleDelete, handleEdit, handleFlag, handleMarkUnread, handleMute, handlePin, isUserMuted,
9
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
10
- mine, open, ...restDivProps } = props;
10
+ const { className, getMessageActions, handleDelete, handleEdit, handleFlag, handleMarkUnread, handleMute, handlePin, isUserMuted, mine, open, ...restDivProps } = props;
11
+ const { client } = useChatContext();
11
12
  const { CustomMessageActionsList = DefaultCustomMessageActionsList } = useComponentContext('MessageActionsBox');
12
13
  const { customMessageActions, message, threadList } = useMessageContext('MessageActionsBox');
13
14
  const { t } = useTranslationContext('MessageActionsBox');
14
15
  const messageComposer = useMessageComposer();
16
+ const reminder = useMessageReminder(message.id);
15
17
  const messageActions = getMessageActions();
16
18
  const handleQuote = () => {
17
19
  messageComposer.setQuotedMessage(message);
@@ -38,7 +40,11 @@ const UnMemoizedMessageActionsBox = (props) => {
38
40
  messageActions.indexOf(MESSAGE_ACTIONS.flag) > -1 && (React.createElement("button", { "aria-selected": 'false', className: buttonClassName, onClick: handleFlag, role: 'option' }, t('Flag'))),
39
41
  messageActions.indexOf(MESSAGE_ACTIONS.mute) > -1 && (React.createElement("button", { "aria-selected": 'false', className: buttonClassName, onClick: handleMute, role: 'option' }, isUserMuted() ? t('Unmute') : t('Mute'))),
40
42
  messageActions.indexOf(MESSAGE_ACTIONS.edit) > -1 && (React.createElement("button", { "aria-selected": 'false', className: buttonClassName, onClick: handleEdit, role: 'option' }, t('Edit Message'))),
41
- messageActions.indexOf(MESSAGE_ACTIONS.delete) > -1 && (React.createElement("button", { "aria-selected": 'false', className: buttonClassName, onClick: handleDelete, role: 'option' }, t('Delete'))))));
43
+ messageActions.indexOf(MESSAGE_ACTIONS.delete) > -1 && (React.createElement("button", { "aria-selected": 'false', className: buttonClassName, onClick: handleDelete, role: 'option' }, t('Delete'))),
44
+ messageActions.indexOf(MESSAGE_ACTIONS.remindMe) > -1 && (React.createElement(RemindMeActionButton, { className: buttonClassName, isMine: mine })),
45
+ messageActions.indexOf(MESSAGE_ACTIONS.saveForLater) > -1 && (React.createElement("button", { "aria-selected": 'false', className: buttonClassName, onClick: () => reminder
46
+ ? client.reminders.deleteReminder(reminder.id)
47
+ : client.reminders.createReminder({ messageId: message.id }), role: 'option' }, reminder ? t('Remove reminder') : t('Save for later'))))));
42
48
  };
43
49
  /**
44
50
  * A popup box that displays the available actions on a message, such as edit, delete, pin, etc.
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import type { ComponentProps } from 'react';
3
+ export declare const RemindMeActionButton: ({ className, isMine, }: {
4
+ isMine: boolean;
5
+ } & ComponentProps<'button'>) => React.JSX.Element;
6
+ export declare const RemindMeSubmenu: () => React.JSX.Element;
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { useChatContext, useMessageContext, useTranslationContext } from '../../context';
3
+ import { ButtonWithSubmenu } from '../Dialog';
4
+ export const RemindMeActionButton = ({ className, isMine, }) => {
5
+ const { t } = useTranslationContext();
6
+ return (React.createElement(ButtonWithSubmenu, { "aria-selected": 'false', className: className, placement: isMine ? 'left-start' : 'right-start', Submenu: RemindMeSubmenu }, t('Remind Me')));
7
+ };
8
+ export const RemindMeSubmenu = () => {
9
+ const { t } = useTranslationContext();
10
+ const { client } = useChatContext();
11
+ const { message } = useMessageContext();
12
+ return (React.createElement("div", { "aria-label": t('aria/Remind Me Options'), className: 'str-chat__message-actions-box__submenu', role: 'listbox' }, client.reminders.scheduledOffsetsMs.map((offsetMs) => (React.createElement("button", { className: 'str-chat__message-actions-list-item-button', key: `reminder-offset-option--${offsetMs}`, onClick: () => {
13
+ client.reminders.upsertReminder({
14
+ messageId: message.id,
15
+ remind_at: new Date(new Date().getTime() + offsetMs).toISOString(),
16
+ });
17
+ } }, t('duration/Remind Me', { milliseconds: offsetMs }))))));
18
+ };