stream-chat-react 12.0.0-rc.10 → 12.0.0-rc.11

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 (89) hide show
  1. package/dist/components/Avatar/Avatar.js +5 -1
  2. package/dist/components/Channel/Channel.d.ts +3 -4
  3. package/dist/components/Channel/Channel.js +76 -24
  4. package/dist/components/Chat/hooks/useChat.js +10 -6
  5. package/dist/components/ChatView/ChatView.d.ts +18 -0
  6. package/dist/components/ChatView/ChatView.js +100 -0
  7. package/dist/components/ChatView/index.d.ts +1 -0
  8. package/dist/components/ChatView/index.js +1 -0
  9. package/dist/components/Message/Message.js +2 -1
  10. package/dist/components/Message/MessageOptions.js +3 -4
  11. package/dist/components/Message/MessageSimple.js +2 -1
  12. package/dist/components/Message/QuotedMessage.js +2 -1
  13. package/dist/components/Message/hooks/useReactionHandler.js +7 -0
  14. package/dist/components/Message/utils.d.ts +10 -1
  15. package/dist/components/Message/utils.js +16 -7
  16. package/dist/components/MessageActions/MessageActions.js +14 -9
  17. package/dist/components/MessageInput/MessageInputFlat.js +2 -2
  18. package/dist/components/MessageInput/QuotedMessagePreview.js +2 -1
  19. package/dist/components/MessageList/MessageList.js +1 -3
  20. package/dist/components/MessageList/VirtualizedMessageList.d.ts +2 -1
  21. package/dist/components/MessageList/VirtualizedMessageList.js +5 -2
  22. package/dist/components/MessageList/VirtualizedMessageListComponents.d.ts +1 -1
  23. package/dist/components/MessageList/VirtualizedMessageListComponents.js +6 -6
  24. package/dist/components/MessageList/renderMessages.d.ts +2 -2
  25. package/dist/components/MessageList/renderMessages.js +4 -1
  26. package/dist/components/Reactions/ReactionSelector.d.ts +5 -2
  27. package/dist/components/Reactions/ReactionSelector.js +2 -1
  28. package/dist/components/Reactions/ReactionsList.d.ts +4 -1
  29. package/dist/components/Reactions/hooks/useProcessReactions.js +2 -1
  30. package/dist/components/Thread/Thread.js +37 -10
  31. package/dist/components/Threads/ThreadContext.d.ts +9 -0
  32. package/dist/components/Threads/ThreadContext.js +9 -0
  33. package/dist/components/Threads/ThreadList/ThreadList.d.ts +9 -0
  34. package/dist/components/Threads/ThreadList/ThreadList.js +41 -0
  35. package/dist/components/Threads/ThreadList/ThreadListEmptyPlaceholder.d.ts +2 -0
  36. package/dist/components/Threads/ThreadList/ThreadListEmptyPlaceholder.js +5 -0
  37. package/dist/components/Threads/ThreadList/ThreadListItem.d.ts +9 -0
  38. package/dist/components/Threads/ThreadList/ThreadListItem.js +52 -0
  39. package/dist/components/Threads/ThreadList/ThreadListItemUI.d.ts +18 -0
  40. package/dist/components/Threads/ThreadList/ThreadListItemUI.js +76 -0
  41. package/dist/components/Threads/ThreadList/ThreadListLoadingIndicator.d.ts +2 -0
  42. package/dist/components/Threads/ThreadList/ThreadListLoadingIndicator.js +14 -0
  43. package/dist/components/Threads/ThreadList/ThreadListUnseenThreadsBanner.d.ts +2 -0
  44. package/dist/components/Threads/ThreadList/ThreadListUnseenThreadsBanner.js +16 -0
  45. package/dist/components/Threads/ThreadList/index.d.ts +3 -0
  46. package/dist/components/Threads/ThreadList/index.js +3 -0
  47. package/dist/components/Threads/UnreadCountBadge.d.ts +6 -0
  48. package/dist/components/Threads/UnreadCountBadge.js +5 -0
  49. package/dist/components/Threads/hooks/useStateStore.d.ts +3 -0
  50. package/dist/components/Threads/hooks/useStateStore.js +15 -0
  51. package/dist/components/Threads/hooks/useThreadManagerState.d.ts +2 -0
  52. package/dist/components/Threads/hooks/useThreadManagerState.js +6 -0
  53. package/dist/components/Threads/hooks/useThreadState.d.ts +5 -0
  54. package/dist/components/Threads/hooks/useThreadState.js +11 -0
  55. package/dist/components/Threads/icons.d.ts +8 -0
  56. package/dist/components/Threads/icons.js +13 -0
  57. package/dist/components/Threads/index.d.ts +3 -0
  58. package/dist/components/Threads/index.js +3 -0
  59. package/dist/components/index.d.ts +2 -0
  60. package/dist/components/index.js +2 -0
  61. package/dist/context/ComponentContext.d.ts +15 -40
  62. package/dist/context/ComponentContext.js +7 -9
  63. package/dist/context/MessageContext.d.ts +1 -1
  64. package/dist/context/MessageContext.js +3 -2
  65. package/dist/context/WithComponents.d.ts +5 -0
  66. package/dist/context/WithComponents.js +7 -0
  67. package/dist/context/index.d.ts +1 -0
  68. package/dist/context/index.js +1 -0
  69. package/dist/css/v2/index.css +2 -2
  70. package/dist/css/v2/index.layout.css +2 -2
  71. package/dist/index.browser.cjs +6456 -5951
  72. package/dist/index.browser.cjs.map +4 -4
  73. package/dist/index.node.cjs +6390 -5870
  74. package/dist/index.node.cjs.map +4 -4
  75. package/dist/scss/v2/Avatar/Avatar-layout.scss +10 -2
  76. package/dist/scss/v2/Avatar/Avatar-theme.scss +5 -0
  77. package/dist/scss/v2/ChatView/ChatView-layout.scss +43 -0
  78. package/dist/scss/v2/ChatView/ChatView-theme.scss +31 -0
  79. package/dist/scss/v2/LoadingIndicator/LoadingIndicator-layout.scss +16 -0
  80. package/dist/scss/v2/MessageList/MessageList-layout.scss +0 -6
  81. package/dist/scss/v2/MessageList/VirtualizedMessageList-layout.scss +0 -12
  82. package/dist/scss/v2/Thread/Thread-layout.scss +15 -1
  83. package/dist/scss/v2/ThreadList/ThreadList-layout.scss +149 -0
  84. package/dist/scss/v2/ThreadList/ThreadList-theme.scss +73 -0
  85. package/dist/scss/v2/UnreadCountBadge/UnreadCountBadge-layout.scss +49 -0
  86. package/dist/scss/v2/UnreadCountBadge/UnreadCountBadge-theme.scss +10 -0
  87. package/dist/scss/v2/index.layout.scss +3 -0
  88. package/dist/scss/v2/index.scss +3 -0
  89. package/package.json +4 -4
@@ -1,5 +1,6 @@
1
1
  import clsx from 'clsx';
2
2
  import React, { useEffect, useState } from 'react';
3
+ import { Icon } from '../Threads/icons';
3
4
  import { getWholeChar } from '../../utils';
4
5
  /**
5
6
  * A round avatar image with fallback to username's first letter
@@ -15,6 +16,9 @@ export const Avatar = (props) => {
15
16
  const showImage = image && !error;
16
17
  return (React.createElement("div", { className: clsx(`str-chat__avatar str-chat__message-sender-avatar`, className, {
17
18
  ['str-chat__avatar--multiple-letters']: initials.length > 1,
19
+ ['str-chat__avatar--no-letters']: !initials.length,
18
20
  ['str-chat__avatar--one-letter']: initials.length === 1,
19
- }), "data-testid": 'avatar', onClick: onClick, onMouseOver: onMouseOver, title: name }, showImage ? (React.createElement("img", { alt: initials, className: clsx(`str-chat__avatar-image`), "data-testid": 'avatar-img', onError: () => setError(true), src: image })) : (React.createElement("div", { className: 'str-chat__avatar-fallback', "data-testid": 'avatar-fallback' }, initials))));
21
+ }), "data-testid": 'avatar', onClick: onClick, onMouseOver: onMouseOver, role: 'button', title: name }, showImage ? (React.createElement("img", { alt: initials, className: 'str-chat__avatar-image', "data-testid": 'avatar-img', onError: () => setError(true), src: image })) : (React.createElement(React.Fragment, null,
22
+ !!initials.length && (React.createElement("div", { className: clsx('str-chat__avatar-fallback'), "data-testid": 'avatar-fallback' }, initials)),
23
+ !initials.length && React.createElement(Icon.User, null)))));
20
24
  };
@@ -2,11 +2,10 @@ import React, { PropsWithChildren } from 'react';
2
2
  import { ChannelQueryOptions, EventAPIResponse, Message, MessageResponse, Channel as StreamChannel, StreamChat, UpdatedMessage } from 'stream-chat';
3
3
  import { OnMentionAction } from './hooks/useMentionsHandlers';
4
4
  import { LoadingErrorIndicatorProps } from '../Loading';
5
- import { StreamMessage } from '../../context/ChannelStateContext';
6
- import { ComponentContextValue } from '../../context/ComponentContext';
5
+ import { ComponentContextValue, StreamMessage } from '../../context';
7
6
  import type { UnreadMessagesNotificationProps } from '../MessageList';
8
- import type { MessageProps } from '../Message/types';
9
- import type { MessageInputProps } from '../MessageInput/MessageInput';
7
+ import type { MessageProps } from '../Message';
8
+ import type { MessageInputProps } from '../MessageInput';
10
9
  import type { ChannelUnreadUiState, CustomTrigger, DefaultStreamChatGenerics, GiphyVersions, ImageAttachmentSizeHandler, SendMessageOptions, UpdateMessageOptions, VideoAttachmentSizeHandler } from '../../types/types';
11
10
  import type { URLEnrichmentConfig } from '../MessageInput/hooks/useLinkPreviews';
12
11
  import { ReactionOptions } from '../Reactions';
@@ -10,26 +10,17 @@ import { useCreateTypingContext } from './hooks/useCreateTypingContext';
10
10
  import { useEditMessageHandler } from './hooks/useEditMessageHandler';
11
11
  import { useIsMounted } from './hooks/useIsMounted';
12
12
  import { useMentionsHandlers } from './hooks/useMentionsHandlers';
13
- import { Attachment as DefaultAttachment } from '../Attachment/Attachment';
14
13
  import { LoadingErrorIndicator as DefaultLoadingErrorIndicator, } from '../Loading';
15
14
  import { LoadingChannel as DefaultLoadingIndicator } from './LoadingChannel';
16
- import { MessageSimple } from '../Message/MessageSimple';
17
15
  import { DropzoneProvider } from '../MessageInput/DropzoneProvider';
18
- import { ChannelActionProvider, } from '../../context/ChannelActionContext';
19
- import { ChannelStateProvider, } from '../../context/ChannelStateContext';
20
- import { ComponentProvider } from '../../context/ComponentContext';
21
- import { useChatContext } from '../../context/ChatContext';
22
- import { useTranslationContext } from '../../context/TranslationContext';
23
- import { TypingProvider } from '../../context/TypingContext';
16
+ import { ChannelActionProvider, ChannelStateProvider, TypingProvider, useChatContext, useTranslationContext, WithComponents, } from '../../context';
24
17
  import { DEFAULT_HIGHLIGHT_DURATION, DEFAULT_INITIAL_CHANNEL_PAGE_SIZE, DEFAULT_JUMP_TO_PAGE_SIZE, DEFAULT_NEXT_CHANNEL_PAGE_SIZE, DEFAULT_THREAD_PAGE_SIZE, } from '../../constants/limits';
25
- import { hasMoreMessagesProbably, UnreadMessagesSeparator } from '../MessageList';
18
+ import { hasMoreMessagesProbably } from '../MessageList';
26
19
  import { useChannelContainerClasses } from './hooks/useChannelContainerClasses';
27
20
  import { findInMsgSetByDate, findInMsgSetById, makeAddNotifications } from './utils';
28
21
  import { getChannel } from '../../utils';
29
22
  import { getImageAttachmentConfiguration, getVideoAttachmentConfiguration, } from '../Attachment/attachment-sizing';
30
- import { defaultReactionOptions } from '../Reactions';
31
- import { EventComponent } from '../EventComponent';
32
- import { DateSeparator } from '../DateSeparator';
23
+ import { useThreadContext } from '../Threads';
33
24
  const isUserResponseArray = (output) => output[0]?.id != null;
34
25
  const UnMemoizedChannel = (props) => {
35
26
  const { channel: propsChannel, EmptyPlaceholder = null, LoadingErrorIndicator, LoadingIndicator = DefaultLoadingIndicator, } = props;
@@ -60,6 +51,7 @@ const ChannelInner = (props) => {
60
51
  const { client, customClasses, latestMessageDatesByChannels, mutes, theme, } = useChatContext('Channel');
61
52
  const { t } = useTranslationContext('Channel');
62
53
  const { channelClass, chatClass, chatContainerClass, windowsEmojiClass, } = useChannelContainerClasses({ customClasses });
54
+ const thread = useThreadContext();
63
55
  const [channelConfig, setChannelConfig] = useState(channel.getConfig());
64
56
  const [notifications, setNotifications] = useState([]);
65
57
  const [quotedMessage, setQuotedMessage] = useState();
@@ -565,25 +557,37 @@ const ChannelInner = (props) => {
565
557
  errorStatusCode: parsedError.status || undefined,
566
558
  status: 'failed',
567
559
  });
560
+ thread?.upsertReplyLocally({
561
+ // @ts-expect-error
562
+ message: {
563
+ ...message,
564
+ error: parsedError,
565
+ errorStatusCode: parsedError.status || undefined,
566
+ status: 'failed',
567
+ },
568
+ });
568
569
  }
569
570
  }
570
571
  };
571
572
  const sendMessage = async ({ attachments = [], mentioned_users = [], parent, text = '', }, customMessageData, options) => {
572
573
  channel.state.filterErrorMessages();
573
574
  const messagePreview = {
574
- __html: text,
575
575
  attachments,
576
576
  created_at: new Date(),
577
577
  html: text,
578
578
  id: customMessageData?.id ?? `${client.userID}-${nanoid()}`,
579
579
  mentioned_users,
580
+ parent_id: parent?.id,
580
581
  reactions: [],
581
582
  status: 'sending',
582
583
  text,
583
584
  type: 'regular',
584
585
  user: client.user,
585
- ...(parent?.id ? { parent_id: parent.id } : null),
586
586
  };
587
+ thread?.upsertReplyLocally({
588
+ // @ts-expect-error
589
+ message: messagePreview,
590
+ });
587
591
  updateMessage(messagePreview);
588
592
  await doSendMessage(messagePreview, customMessageData, options);
589
593
  };
@@ -722,8 +726,9 @@ const ChannelInner = (props) => {
722
726
  jumpToLatestMessage,
723
727
  setChannelUnreadUiState,
724
728
  ]);
729
+ // @ts-expect-error
725
730
  const componentContextValue = useMemo(() => ({
726
- Attachment: props.Attachment || DefaultAttachment,
731
+ Attachment: props.Attachment,
727
732
  AttachmentPreviewList: props.AttachmentPreviewList,
728
733
  AudioRecorder: props.AudioRecorder,
729
734
  AutocompleteSuggestionItem: props.AutocompleteSuggestionItem,
@@ -732,7 +737,7 @@ const ChannelInner = (props) => {
732
737
  BaseImage: props.BaseImage,
733
738
  CooldownTimer: props.CooldownTimer,
734
739
  CustomMessageActionsList: props.CustomMessageActionsList,
735
- DateSeparator: props.DateSeparator || DateSeparator,
740
+ DateSeparator: props.DateSeparator,
736
741
  EditMessageInput: props.EditMessageInput,
737
742
  EmojiPicker: props.EmojiPicker,
738
743
  emojiSearchIndex: props.emojiSearchIndex,
@@ -743,7 +748,7 @@ const ChannelInner = (props) => {
743
748
  Input: props.Input,
744
749
  LinkPreviewList: props.LinkPreviewList,
745
750
  LoadingIndicator: props.LoadingIndicator,
746
- Message: props.Message || MessageSimple,
751
+ Message: props.Message,
747
752
  MessageBouncePrompt: props.MessageBouncePrompt,
748
753
  MessageDeleted: props.MessageDeleted,
749
754
  MessageListNotifications: props.MessageListNotifications,
@@ -751,13 +756,13 @@ const ChannelInner = (props) => {
751
756
  MessageOptions: props.MessageOptions,
752
757
  MessageRepliesCountButton: props.MessageRepliesCountButton,
753
758
  MessageStatus: props.MessageStatus,
754
- MessageSystem: props.MessageSystem || EventComponent,
759
+ MessageSystem: props.MessageSystem,
755
760
  MessageTimestamp: props.MessageTimestamp,
756
761
  ModalGallery: props.ModalGallery,
757
762
  PinIndicator: props.PinIndicator,
758
763
  QuotedMessage: props.QuotedMessage,
759
764
  QuotedMessagePreview: props.QuotedMessagePreview,
760
- reactionOptions: props.reactionOptions ?? defaultReactionOptions,
765
+ reactionOptions: props.reactionOptions,
761
766
  ReactionSelector: props.ReactionSelector,
762
767
  ReactionsList: props.ReactionsList,
763
768
  SendButton: props.SendButton,
@@ -769,11 +774,58 @@ const ChannelInner = (props) => {
769
774
  TriggerProvider: props.TriggerProvider,
770
775
  TypingIndicator: props.TypingIndicator,
771
776
  UnreadMessagesNotification: props.UnreadMessagesNotification,
772
- UnreadMessagesSeparator: props.UnreadMessagesSeparator || UnreadMessagesSeparator,
777
+ UnreadMessagesSeparator: props.UnreadMessagesSeparator,
773
778
  VirtualMessage: props.VirtualMessage,
774
- }),
775
- // eslint-disable-next-line react-hooks/exhaustive-deps
776
- [props.reactionOptions]);
779
+ }), [
780
+ props.Attachment,
781
+ props.AttachmentPreviewList,
782
+ props.AudioRecorder,
783
+ props.AutocompleteSuggestionItem,
784
+ props.AutocompleteSuggestionList,
785
+ props.Avatar,
786
+ props.BaseImage,
787
+ props.CooldownTimer,
788
+ props.CustomMessageActionsList,
789
+ props.DateSeparator,
790
+ props.EditMessageInput,
791
+ props.EmojiPicker,
792
+ props.EmptyStateIndicator,
793
+ props.FileUploadIcon,
794
+ props.GiphyPreviewMessage,
795
+ props.HeaderComponent,
796
+ props.Input,
797
+ props.LinkPreviewList,
798
+ props.LoadingIndicator,
799
+ props.Message,
800
+ props.MessageBouncePrompt,
801
+ props.MessageDeleted,
802
+ props.MessageListNotifications,
803
+ props.MessageNotification,
804
+ props.MessageOptions,
805
+ props.MessageRepliesCountButton,
806
+ props.MessageStatus,
807
+ props.MessageSystem,
808
+ props.MessageTimestamp,
809
+ props.ModalGallery,
810
+ props.PinIndicator,
811
+ props.QuotedMessage,
812
+ props.QuotedMessagePreview,
813
+ props.ReactionSelector,
814
+ props.ReactionsList,
815
+ props.SendButton,
816
+ props.StartRecordingAudioButton,
817
+ props.ThreadHead,
818
+ props.ThreadHeader,
819
+ props.ThreadStart,
820
+ props.Timestamp,
821
+ props.TriggerProvider,
822
+ props.TypingIndicator,
823
+ props.UnreadMessagesNotification,
824
+ props.UnreadMessagesSeparator,
825
+ props.VirtualMessage,
826
+ props.emojiSearchIndex,
827
+ props.reactionOptions,
828
+ ]);
777
829
  const typingContextValue = useCreateTypingContext({
778
830
  typing,
779
831
  });
@@ -793,7 +845,7 @@ const ChannelInner = (props) => {
793
845
  return (React.createElement("div", { className: clsx(className, windowsEmojiClass) },
794
846
  React.createElement(ChannelStateProvider, { value: channelStateContextValue },
795
847
  React.createElement(ChannelActionProvider, { value: channelActionContextValue },
796
- React.createElement(ComponentProvider, { value: componentContextValue },
848
+ React.createElement(WithComponents, { overrides: componentContextValue },
797
849
  React.createElement(TypingProvider, { value: typingContextValue },
798
850
  React.createElement("div", { className: `${chatContainerClass}` },
799
851
  dragAndDropWindow && (React.createElement(DropzoneProvider, { ...optionalMessageInputProps }, children)),
@@ -23,13 +23,17 @@ export const useChat = ({ client, defaultLanguage = 'en', i18nInstance, initialN
23
23
  return appSettings.current;
24
24
  };
25
25
  useEffect(() => {
26
- if (client) {
27
- const userAgent = client.getUserAgent();
28
- if (!userAgent.includes('stream-chat-react')) {
29
- // result looks like: 'stream-chat-react-2.3.2-stream-chat-javascript-client-browser-2.2.2'
30
- client.setUserAgent(`stream-chat-react-${version}-${userAgent}`);
31
- }
26
+ if (!client)
27
+ return;
28
+ const userAgent = client.getUserAgent();
29
+ if (!userAgent.includes('stream-chat-react')) {
30
+ // result looks like: 'stream-chat-react-2.3.2-stream-chat-javascript-client-browser-2.2.2'
31
+ client.setUserAgent(`stream-chat-react-${version}-${userAgent}`);
32
32
  }
33
+ client.threads.registerSubscriptions();
34
+ return () => {
35
+ client.threads.unregisterSubscriptions();
36
+ };
33
37
  }, [client]);
34
38
  useEffect(() => {
35
39
  setMutes(clientMutes);
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import type { PropsWithChildren } from 'react';
3
+ import type { Thread } from 'stream-chat';
4
+ export declare const ChatView: {
5
+ ({ children }: PropsWithChildren): React.JSX.Element;
6
+ Channels: ({ children }: PropsWithChildren) => React.JSX.Element | null;
7
+ Threads: ({ children }: PropsWithChildren) => React.JSX.Element | null;
8
+ ThreadAdapter: ({ children }: PropsWithChildren) => React.JSX.Element;
9
+ Selector: () => React.JSX.Element;
10
+ };
11
+ export type ThreadsViewContextValue = {
12
+ activeThread: Thread | undefined;
13
+ setActiveThread: (cv: ThreadsViewContextValue['activeThread']) => void;
14
+ };
15
+ export declare const useThreadsViewContext: () => ThreadsViewContextValue;
16
+ export declare const useActiveThread: ({ activeThread }: {
17
+ activeThread?: Thread;
18
+ }) => void;
@@ -0,0 +1,100 @@
1
+ import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
2
+ import { ThreadProvider, useStateStore } from '../Threads';
3
+ import { Icon } from '../Threads/icons';
4
+ import { UnreadCountBadge } from '../Threads/UnreadCountBadge';
5
+ import { useChatContext } from '../../context';
6
+ const availableChatViews = ['channels', 'threads'];
7
+ const ChatViewContext = createContext({
8
+ activeChatView: 'channels',
9
+ setActiveChatView: () => undefined,
10
+ });
11
+ export const ChatView = ({ children }) => {
12
+ const [activeChatView, setActiveChatView] = useState('channels');
13
+ const value = useMemo(() => ({ activeChatView, setActiveChatView }), [activeChatView]);
14
+ return (React.createElement(ChatViewContext.Provider, { value: value },
15
+ React.createElement("div", { className: 'str-chat str-chat__chat-view' }, children)));
16
+ };
17
+ const ChannelsView = ({ children }) => {
18
+ const { activeChatView } = useContext(ChatViewContext);
19
+ if (activeChatView !== 'channels')
20
+ return null;
21
+ return React.createElement("div", { className: 'str-chat__chat-view__channels' }, children);
22
+ };
23
+ const ThreadsViewContext = createContext({
24
+ activeThread: undefined,
25
+ setActiveThread: () => undefined,
26
+ });
27
+ export const useThreadsViewContext = () => useContext(ThreadsViewContext);
28
+ const ThreadsView = ({ children }) => {
29
+ const { activeChatView } = useContext(ChatViewContext);
30
+ const [activeThread, setActiveThread] = useState(undefined);
31
+ const value = useMemo(() => ({ activeThread, setActiveThread }), [activeThread]);
32
+ if (activeChatView !== 'threads')
33
+ return null;
34
+ return (React.createElement(ThreadsViewContext.Provider, { value: value },
35
+ React.createElement("div", { className: 'str-chat__chat-view__threads' }, children)));
36
+ };
37
+ // thread business logic that's impossible to keep within client but encapsulated for ease of use
38
+ export const useActiveThread = ({ activeThread }) => {
39
+ useEffect(() => {
40
+ if (!activeThread)
41
+ return;
42
+ const handleVisibilityChange = () => {
43
+ if (document.visibilityState === 'visible' && document.hasFocus()) {
44
+ activeThread.activate();
45
+ }
46
+ if (document.visibilityState === 'hidden' || !document.hasFocus()) {
47
+ activeThread.deactivate();
48
+ }
49
+ };
50
+ handleVisibilityChange();
51
+ window.addEventListener('focus', handleVisibilityChange);
52
+ window.addEventListener('blur', handleVisibilityChange);
53
+ return () => {
54
+ activeThread.deactivate();
55
+ window.addEventListener('blur', handleVisibilityChange);
56
+ window.removeEventListener('focus', handleVisibilityChange);
57
+ };
58
+ }, [activeThread]);
59
+ };
60
+ // ThreadList under View.Threads context, will access setting function and on item click will set activeThread
61
+ // which can be accessed for the ease of use by ThreadAdapter which forwards it to required ThreadProvider
62
+ // ThreadList can easily live without this context and click handler can be overriden, ThreadAdapter is then no longer needed
63
+ /**
64
+ * // this setup still works
65
+ * const MyCustomComponent = () => {
66
+ * const [activeThread, setActiveThread] = useState();
67
+ *
68
+ * return <>
69
+ * // simplified
70
+ * <ThreadList onItemPointerDown={setActiveThread} />
71
+ * <ThreadProvider thread={activeThread}>
72
+ * <Thread />
73
+ * </ThreadProvider>
74
+ * </>
75
+ * }
76
+ *
77
+ */
78
+ const ThreadAdapter = ({ children }) => {
79
+ const { activeThread } = useThreadsViewContext();
80
+ useActiveThread({ activeThread });
81
+ return React.createElement(ThreadProvider, { thread: activeThread }, children);
82
+ };
83
+ const selector = (nextValue) => [nextValue.unreadThreadCount];
84
+ const ChatViewSelector = () => {
85
+ const { client } = useChatContext();
86
+ const [unreadThreadCount] = useStateStore(client.threads.state, selector);
87
+ const { activeChatView, setActiveChatView } = useContext(ChatViewContext);
88
+ return (React.createElement("div", { className: 'str-chat__chat-view__selector' },
89
+ React.createElement("button", { "aria-selected": activeChatView === 'channels', className: 'str-chat__chat-view__selector-button', onPointerDown: () => setActiveChatView('channels'), role: 'tab' },
90
+ React.createElement(Icon.MessageBubbleEmpty, null),
91
+ React.createElement("div", { className: 'str-chat__chat-view__selector-button-text' }, "Channels")),
92
+ React.createElement("button", { "aria-selected": activeChatView === 'threads', className: 'str-chat__chat-view__selector-button', onPointerDown: () => setActiveChatView('threads'), role: 'tab' },
93
+ React.createElement(UnreadCountBadge, { count: unreadThreadCount, position: 'top-right' },
94
+ React.createElement(Icon.MessageBubble, null)),
95
+ React.createElement("div", { className: 'str-chat__chat-view__selector-button-text' }, "Threads"))));
96
+ };
97
+ ChatView.Channels = ChannelsView;
98
+ ChatView.Threads = ThreadsView;
99
+ ChatView.ThreadAdapter = ThreadAdapter;
100
+ ChatView.Selector = ChatViewSelector;
@@ -0,0 +1 @@
1
+ export * from './ChatView';
@@ -0,0 +1 @@
1
+ export * from './ChatView';
@@ -2,13 +2,14 @@ import React, { useCallback, useMemo, useRef } from 'react';
2
2
  import { useActionHandler, useDeleteHandler, useEditHandler, useFlagHandler, useMarkUnreadHandler, useMentionsHandler, useMuteHandler, useOpenThreadHandler, usePinHandler, useReactionClick, useReactionHandler, useReactionsFetcher, useRetryHandler, useUserHandler, useUserRole, } from './hooks';
3
3
  import { areMessagePropsEqual, getMessageActions, MESSAGE_ACTIONS } from './utils';
4
4
  import { MessageProvider, useChannelActionContext, useChannelStateContext, useChatContext, useComponentContext, } from '../../context';
5
+ import { MessageSimple as DefaultMessage } from './MessageSimple';
5
6
  const MessageWithContext = (props) => {
6
7
  const { canPin, groupedByUser, Message: propMessage, message, messageActions = Object.keys(MESSAGE_ACTIONS), onUserClick: propOnUserClick, onUserHover: propOnUserHover, userRoles, } = props;
7
8
  const { client } = useChatContext('Message');
8
9
  const { read } = useChannelStateContext('Message');
9
10
  const { Message: contextMessage } = useComponentContext('Message');
10
11
  const actionsEnabled = message.type === 'regular' && message.status === 'received';
11
- const MessageUIComponent = propMessage || contextMessage;
12
+ const MessageUIComponent = propMessage ?? contextMessage ?? DefaultMessage;
12
13
  const { clearEdit, editing, setEdit } = useEditHandler();
13
14
  const { onUserClick, onUserHover } = useUserHandler(message, {
14
15
  onUserClickHandler: propOnUserClick,
@@ -1,16 +1,15 @@
1
1
  import React from 'react';
2
2
  import { ActionsIcon as DefaultActionsIcon, ReactionIcon as DefaultReactionIcon, ThreadIcon as DefaultThreadIcon, } from './icons';
3
- import { MESSAGE_ACTIONS, showMessageActionsBox } from './utils';
3
+ import { MESSAGE_ACTIONS } from './utils';
4
4
  import { MessageActions } from '../MessageActions';
5
5
  import { useMessageContext } from '../../context/MessageContext';
6
6
  import { useTranslationContext } from '../../context';
7
7
  const UnMemoizedMessageOptions = (props) => {
8
8
  const { ActionsIcon = DefaultActionsIcon, displayReplies = true, handleOpenThread: propHandleOpenThread, messageWrapperRef, ReactionIcon = DefaultReactionIcon, theme = 'simple', ThreadIcon = DefaultThreadIcon, } = props;
9
- const { customMessageActions, getMessageActions, handleOpenThread: contextHandleOpenThread, initialMessage, message, onReactionListClick, showDetailedReactions, threadList, } = useMessageContext('MessageOptions');
9
+ const { getMessageActions, handleOpenThread: contextHandleOpenThread, initialMessage, message, onReactionListClick, showDetailedReactions, threadList, } = useMessageContext('MessageOptions');
10
10
  const { t } = useTranslationContext('MessageOptions');
11
11
  const handleOpenThread = propHandleOpenThread || contextHandleOpenThread;
12
12
  const messageActions = getMessageActions();
13
- const showActionsBox = showMessageActionsBox(messageActions, threadList) || !!customMessageActions;
14
13
  const shouldShowReactions = messageActions.indexOf(MESSAGE_ACTIONS.react) > -1;
15
14
  const shouldShowReplies = messageActions.indexOf(MESSAGE_ACTIONS.reply) > -1 && displayReplies && !threadList;
16
15
  if (!message.type ||
@@ -24,7 +23,7 @@ const UnMemoizedMessageOptions = (props) => {
24
23
  }
25
24
  const rootClassName = `str-chat__message-${theme}__actions str-chat__message-options`;
26
25
  return (React.createElement("div", { className: rootClassName, "data-testid": 'message-options' },
27
- showActionsBox && (React.createElement(MessageActions, { ActionsIcon: ActionsIcon, messageWrapperRef: messageWrapperRef })),
26
+ React.createElement(MessageActions, { ActionsIcon: ActionsIcon, messageWrapperRef: messageWrapperRef }),
28
27
  shouldShowReplies && (React.createElement("button", { "aria-label": t('aria/Open Thread'), className: `str-chat__message-${theme}__actions__action str-chat__message-${theme}__actions__action--thread str-chat__message-reply-in-thread-button`, "data-testid": 'thread-action', onClick: handleOpenThread },
29
28
  React.createElement(ThreadIcon, { className: 'str-chat__message-action-icon' }))),
30
29
  shouldShowReactions && (React.createElement("button", { "aria-expanded": showDetailedReactions, "aria-label": t('aria/Open Reaction Selector'), className: `str-chat__message-${theme}__actions__action str-chat__message-${theme}__actions__action--reactions str-chat__message-reactions-button`, "data-testid": 'message-reaction-action', onClick: onReactionListClick },
@@ -10,6 +10,7 @@ import { MessageText } from './MessageText';
10
10
  import { MessageTimestamp as DefaultMessageTimestamp } from './MessageTimestamp';
11
11
  import { areMessageUIPropsEqual, isMessageBounced, isMessageEdited, messageHasAttachments, messageHasReactions, } from './utils';
12
12
  import { Avatar as DefaultAvatar } from '../Avatar';
13
+ import { Attachment as DefaultAttachment } from '../Attachment';
13
14
  import { CUSTOM_MESSAGE_TYPE } from '../../constants/messageTypes';
14
15
  import { EditMessageForm as DefaultEditMessageForm, MessageInput } from '../MessageInput';
15
16
  import { MML } from '../MML';
@@ -25,7 +26,7 @@ const MessageSimpleWithContext = (props) => {
25
26
  const { t } = useTranslationContext('MessageSimple');
26
27
  const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false);
27
28
  const [isEditedTimestampOpen, setEditedTimestampOpen] = useState(false);
28
- const { Attachment, Avatar = DefaultAvatar, EditMessageInput = DefaultEditMessageForm, MessageDeleted = DefaultMessageDeleted, MessageBouncePrompt = DefaultMessageBouncePrompt, MessageOptions = DefaultMessageOptions, MessageRepliesCountButton = DefaultMessageRepliesCountButton, MessageStatus = DefaultMessageStatus, MessageTimestamp = DefaultMessageTimestamp, ReactionSelector = DefaultReactionSelector, ReactionsList = DefaultReactionList, PinIndicator, } = useComponentContext('MessageSimple');
29
+ const { Attachment = DefaultAttachment, Avatar = DefaultAvatar, EditMessageInput = DefaultEditMessageForm, MessageDeleted = DefaultMessageDeleted, MessageBouncePrompt = DefaultMessageBouncePrompt, MessageOptions = DefaultMessageOptions, MessageRepliesCountButton = DefaultMessageRepliesCountButton, MessageStatus = DefaultMessageStatus, MessageTimestamp = DefaultMessageTimestamp, ReactionSelector = DefaultReactionSelector, ReactionsList = DefaultReactionList, PinIndicator, } = useComponentContext('MessageSimple');
29
30
  const hasAttachment = messageHasAttachments(message);
30
31
  const hasReactions = messageHasReactions(message);
31
32
  if (message.customType === CUSTOM_MESSAGE_TYPE.date) {
@@ -5,8 +5,9 @@ import { useComponentContext } from '../../context/ComponentContext';
5
5
  import { useMessageContext } from '../../context/MessageContext';
6
6
  import { useTranslationContext } from '../../context/TranslationContext';
7
7
  import { useChannelActionContext } from '../../context/ChannelActionContext';
8
+ import { Attachment as DefaultAttachment } from '../Attachment';
8
9
  export const QuotedMessage = () => {
9
- const { Attachment, Avatar: ContextAvatar } = useComponentContext('QuotedMessage');
10
+ const { Attachment = DefaultAttachment, Avatar: ContextAvatar, } = useComponentContext('QuotedMessage');
10
11
  const { isMyMessage, message } = useMessageContext('QuotedMessage');
11
12
  const { t, userLanguage } = useTranslationContext('QuotedMessage');
12
13
  const { jumpToMessage } = useChannelActionContext('QuotedMessage');
@@ -3,9 +3,11 @@ import throttle from 'lodash.throttle';
3
3
  import { useChannelActionContext } from '../../../context/ChannelActionContext';
4
4
  import { useChannelStateContext } from '../../../context/ChannelStateContext';
5
5
  import { useChatContext } from '../../../context/ChatContext';
6
+ import { useThreadContext } from '../../Threads';
6
7
  export const reactionHandlerWarning = `Reaction handler was called, but it is missing one of its required arguments.
7
8
  Make sure the ChannelAction and ChannelState contexts are properly set and the hook is initialized with a valid message.`;
8
9
  export const useReactionHandler = (message) => {
10
+ const thread = useThreadContext();
9
11
  const { updateMessage } = useChannelActionContext('useReactionHandler');
10
12
  const { channel, channelCapabilities } = useChannelStateContext('useReactionHandler');
11
13
  const { client } = useChatContext('useReactionHandler');
@@ -64,14 +66,19 @@ export const useReactionHandler = (message) => {
64
66
  const tempMessage = createMessagePreview(add, newReaction, message);
65
67
  try {
66
68
  updateMessage(tempMessage);
69
+ // @ts-expect-error
70
+ thread?.upsertReplyLocally({ message: tempMessage });
67
71
  const messageResponse = add
68
72
  ? await channel.sendReaction(id, { type })
69
73
  : await channel.deleteReaction(id, type);
74
+ // seems useless as we're expecting WS event to come in and replace this anyway
70
75
  updateMessage(messageResponse.message);
71
76
  }
72
77
  catch (error) {
73
78
  // revert to the original message if the API call fails
74
79
  updateMessage(message);
80
+ // @ts-expect-error
81
+ thread?.upsertReplyLocally({ message });
75
82
  }
76
83
  }, 1000);
77
84
  return async (reactionType, event) => {
@@ -2,7 +2,7 @@ import type { TFunction } from 'i18next';
2
2
  import type { MessageResponse, Mute, StreamChat, UserResponse } from 'stream-chat';
3
3
  import type { PinPermissions } from './hooks';
4
4
  import type { MessageProps } from './types';
5
- import type { MessageContextValue, StreamMessage } from '../../context';
5
+ import type { ComponentContextValue, CustomMessageActions, MessageContextValue, StreamMessage } from '../../context';
6
6
  import type { DefaultStreamChatGenerics } from '../../types/types';
7
7
  /**
8
8
  * Following function validates a function which returns notification message.
@@ -39,7 +39,16 @@ export type Capabilities = {
39
39
  };
40
40
  export declare const getMessageActions: (actions: MessageActionsArray | boolean, { canDelete, canEdit, canFlag, canMarkUnread, canMute, canPin, canQuote, canReact, canReply, }: Capabilities) => MessageActionsArray<string>;
41
41
  export declare const ACTIONS_NOT_WORKING_IN_THREAD: string[];
42
+ /**
43
+ * @deprecated use `shouldRenderMessageActions` instead
44
+ */
42
45
  export declare const showMessageActionsBox: (actions: MessageActionsArray<string>, inThread?: boolean | undefined) => boolean;
46
+ export declare const shouldRenderMessageActions: <SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>({ customMessageActions, CustomMessageActionsList, inThread, messageActions, }: {
47
+ messageActions: MessageActionsArray;
48
+ customMessageActions?: CustomMessageActions<SCG>;
49
+ CustomMessageActionsList?: ComponentContextValue<SCG>['CustomMessageActionsList'];
50
+ inThread?: boolean;
51
+ }) => boolean;
43
52
  export declare const areMessagePropsEqual: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(prevProps: MessageProps<StreamChatGenerics> & {
44
53
  mutes?: Mute<StreamChatGenerics>[];
45
54
  showDetailedReactions?: boolean;
@@ -140,22 +140,31 @@ export const getMessageActions = (actions, { canDelete, canEdit, canFlag, canMar
140
140
  };
141
141
  export const ACTIONS_NOT_WORKING_IN_THREAD = [
142
142
  MESSAGE_ACTIONS.pin,
143
- MESSAGE_ACTIONS.react,
144
143
  MESSAGE_ACTIONS.reply,
145
144
  MESSAGE_ACTIONS.markUnread,
146
145
  ];
147
- export const showMessageActionsBox = (actions, inThread) => {
148
- if (actions.length === 0) {
146
+ /**
147
+ * @deprecated use `shouldRenderMessageActions` instead
148
+ */
149
+ export const showMessageActionsBox = (actions, inThread) => shouldRenderMessageActions({ inThread, messageActions: actions });
150
+ export const shouldRenderMessageActions = ({ customMessageActions, CustomMessageActionsList, inThread, messageActions, }) => {
151
+ if (typeof CustomMessageActionsList !== 'undefined' ||
152
+ typeof customMessageActions !== 'undefined')
153
+ return true;
154
+ if (!messageActions.length)
149
155
  return false;
150
- }
151
156
  if (inThread &&
152
- actions.filter((action) => !ACTIONS_NOT_WORKING_IN_THREAD.includes(action)).length === 0) {
157
+ messageActions.filter((action) => !ACTIONS_NOT_WORKING_IN_THREAD.includes(action)).length === 0) {
153
158
  return false;
154
159
  }
155
- if (actions.length === 1 && (actions.includes('react') || actions.includes('reply'))) {
160
+ if (messageActions.length === 1 &&
161
+ (messageActions.includes(MESSAGE_ACTIONS.react) ||
162
+ messageActions.includes(MESSAGE_ACTIONS.reply))) {
156
163
  return false;
157
164
  }
158
- if (actions.length === 2 && actions.includes('react') && actions.includes('reply')) {
165
+ if (messageActions.length === 2 &&
166
+ messageActions.includes(MESSAGE_ACTIONS.react) &&
167
+ messageActions.includes(MESSAGE_ACTIONS.reply)) {
159
168
  return false;
160
169
  }
161
170
  return true;
@@ -1,15 +1,17 @@
1
1
  import React, { useCallback, useEffect, useRef, useState, } from 'react';
2
+ import clsx from 'clsx';
2
3
  import { MessageActionsBox } from './MessageActionsBox';
3
4
  import { ActionsIcon as DefaultActionsIcon } from '../Message/icons';
4
- import { isUserMuted } from '../Message/utils';
5
+ import { isUserMuted, shouldRenderMessageActions } from '../Message/utils';
5
6
  import { useChatContext } from '../../context/ChatContext';
6
7
  import { useMessageContext } from '../../context/MessageContext';
7
8
  import { useMessageActionsBoxPopper } from './hooks';
8
- import { useTranslationContext } from '../../context';
9
+ import { useComponentContext, useTranslationContext } from '../../context';
9
10
  export const MessageActions = (props) => {
10
11
  const { ActionsIcon = DefaultActionsIcon, customWrapperClass = '', getMessageActions: propGetMessageActions, handleDelete: propHandleDelete, handleFlag: propHandleFlag, handleMarkUnread: propHandleMarkUnread, handleMute: propHandleMute, handlePin: propHandlePin, inline, message: propMessage, messageWrapperRef, mine, } = props;
11
12
  const { mutes } = useChatContext('MessageActions');
12
- const { customMessageActions, getMessageActions: contextGetMessageActions, handleDelete: contextHandleDelete, handleFlag: contextHandleFlag, handleMarkUnread: contextHandleMarkUnread, handleMute: contextHandleMute, handlePin: contextHandlePin, isMyMessage, message: contextMessage, setEditingState, } = useMessageContext('MessageActions');
13
+ const { customMessageActions, getMessageActions: contextGetMessageActions, handleDelete: contextHandleDelete, handleFlag: contextHandleFlag, handleMarkUnread: contextHandleMarkUnread, handleMute: contextHandleMute, handlePin: contextHandlePin, isMyMessage, message: contextMessage, setEditingState, threadList, } = useMessageContext('MessageActions');
14
+ const { CustomMessageActionsList } = useComponentContext('MessageActions');
13
15
  const { t } = useTranslationContext('MessageActions');
14
16
  const getMessageActions = propGetMessageActions || contextGetMessageActions;
15
17
  const handleDelete = propHandleDelete || contextHandleDelete;
@@ -21,13 +23,19 @@ export const MessageActions = (props) => {
21
23
  const isMine = mine ? mine() : isMyMessage();
22
24
  const [actionsBoxOpen, setActionsBoxOpen] = useState(false);
23
25
  const isMuted = useCallback(() => isUserMuted(message, mutes), [message, mutes]);
26
+ const messageActions = getMessageActions();
27
+ const renderMessageActions = shouldRenderMessageActions({
28
+ customMessageActions,
29
+ CustomMessageActionsList,
30
+ inThread: threadList,
31
+ messageActions,
32
+ });
24
33
  const hideOptions = useCallback((event) => {
25
34
  if (event instanceof KeyboardEvent && event.key !== 'Escape') {
26
35
  return;
27
36
  }
28
37
  setActionsBoxOpen(false);
29
38
  }, []);
30
- const messageActions = getMessageActions();
31
39
  const messageDeletedAt = !!message?.deleted_at;
32
40
  useEffect(() => {
33
41
  if (messageWrapperRef?.current) {
@@ -55,7 +63,7 @@ export const MessageActions = (props) => {
55
63
  placement: isMine ? 'top-end' : 'top-start',
56
64
  referenceElement: actionsBoxButtonRef.current,
57
65
  });
58
- if (!messageActions.length && !customMessageActions)
66
+ if (!renderMessageActions)
59
67
  return null;
60
68
  return (React.createElement(MessageActionsWrapper, { customWrapperClass: customWrapperClass, inline: inline, setActionsBoxOpen: setActionsBoxOpen },
61
69
  React.createElement(MessageActionsBox, { ...attributes.popper, getMessageActions: getMessageActions, handleDelete: handleDelete, handleEdit: setEditingState, handleFlag: handleFlag, handleMarkUnread: handleMarkUnread, handleMute: handleMute, handlePin: handlePin, isUserMuted: isMuted, mine: isMine, open: actionsBoxOpen, ref: popperElementRef, style: styles.popper }),
@@ -64,10 +72,7 @@ export const MessageActions = (props) => {
64
72
  };
65
73
  const MessageActionsWrapper = (props) => {
66
74
  const { children, customWrapperClass, inline, setActionsBoxOpen } = props;
67
- const defaultWrapperClass = `
68
- str-chat__message-simple__actions__action
69
- str-chat__message-simple__actions__action--options
70
- str-chat__message-actions-container`;
75
+ const defaultWrapperClass = clsx('str-chat__message-simple__actions__action', 'str-chat__message-simple__actions__action--options', 'str-chat__message-actions-container');
71
76
  const wrapperClass = customWrapperClass || defaultWrapperClass;
72
77
  const onClickOptionsAction = (event) => {
73
78
  event.stopPropagation();