stream-chat-react 12.13.1 → 12.15.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 (52) hide show
  1. package/README.md +27 -30
  2. package/dist/components/Channel/Channel.d.ts +9 -3
  3. package/dist/components/Channel/Channel.js +6 -4
  4. package/dist/components/Chat/hooks/useChat.js +2 -1
  5. package/dist/components/Message/MessageBlocked.d.ts +2 -0
  6. package/dist/components/Message/MessageBlocked.js +16 -0
  7. package/dist/components/Message/MessageSimple.js +6 -2
  8. package/dist/components/Message/utils.d.ts +1 -0
  9. package/dist/components/Message/utils.js +3 -0
  10. package/dist/components/MessageInput/MessageInput.js +3 -0
  11. package/dist/components/MessageInput/MessageInputFlat.js +28 -48
  12. package/dist/components/MessageInput/WithDragAndDropUpload.d.ts +37 -0
  13. package/dist/components/MessageInput/WithDragAndDropUpload.js +85 -0
  14. package/dist/components/MessageInput/index.d.ts +1 -0
  15. package/dist/components/MessageInput/index.js +1 -0
  16. package/dist/context/ComponentContext.d.ts +2 -0
  17. package/dist/context/MessageInputContext.js +3 -2
  18. package/dist/css/v2/index.css +1 -1
  19. package/dist/css/v2/index.layout.css +1 -1
  20. package/dist/experimental/index.browser.cjs.map +2 -2
  21. package/dist/experimental/index.node.cjs.map +2 -2
  22. package/dist/i18n/Streami18n.d.ts +1 -0
  23. package/dist/i18n/de.json +1 -0
  24. package/dist/i18n/en.json +1 -0
  25. package/dist/i18n/es.json +1 -0
  26. package/dist/i18n/fr.json +1 -0
  27. package/dist/i18n/hi.json +1 -0
  28. package/dist/i18n/it.json +1 -0
  29. package/dist/i18n/ja.json +1 -0
  30. package/dist/i18n/ko.json +1 -0
  31. package/dist/i18n/nl.json +1 -0
  32. package/dist/i18n/pt.json +1 -0
  33. package/dist/i18n/ru.json +1 -0
  34. package/dist/i18n/tr.json +1 -0
  35. package/dist/index.browser.cjs +1173 -1084
  36. package/dist/index.browser.cjs.map +4 -4
  37. package/dist/index.node.cjs +1088 -997
  38. package/dist/index.node.cjs.map +4 -4
  39. package/dist/plugins/Emojis/index.browser.cjs +0 -3
  40. package/dist/plugins/Emojis/index.browser.cjs.map +2 -2
  41. package/dist/plugins/Emojis/index.node.cjs +0 -3
  42. package/dist/plugins/Emojis/index.node.cjs.map +2 -2
  43. package/dist/scss/v2/DropzoneContainer/DropzoneContainer-layout.scss +14 -0
  44. package/dist/scss/v2/DropzoneContainer/DropzoneContainer-theme.scss +17 -0
  45. package/dist/scss/v2/Message/Message-layout.scss +8 -0
  46. package/dist/scss/v2/Message/Message-theme.scss +29 -0
  47. package/dist/scss/v2/MessageInput/MessageInput-layout.scss +0 -13
  48. package/dist/scss/v2/MessageInput/MessageInput-theme.scss +8 -19
  49. package/dist/scss/v2/index.layout.scss +2 -1
  50. package/dist/scss/v2/index.scss +1 -0
  51. package/package.json +5 -8
  52. /package/dist/scss/v2/DragAndDropContainer/{DragAmdDropContainer-layout.scss → DragAndDropContainer-layout.scss} +0 -0
package/README.md CHANGED
@@ -42,46 +42,35 @@ For complete pricing and details visit our [Chat Pricing Page](https://getstream
42
42
 
43
43
  ## Installation
44
44
 
45
- ### Install with NPM
45
+ ### With NPM
46
46
 
47
- `npm install react react-dom stream-chat stream-chat-react`
47
+ `npm install stream-chat stream-chat-react`
48
48
 
49
- ### Install with Yarn
49
+ ### With Yarn
50
50
 
51
- `yarn add react react-dom stream-chat stream-chat-react`
51
+ `yarn add stream-chat stream-chat-react`
52
52
 
53
- ### Install via CDN
54
-
55
- ```
56
- <script src="https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js"></script>
57
- <script src="https://cdn.jsdelivr.net/npm/react-dom@16/umd/react-dom.production.min.js"></script>
58
- <script src="https://cdn.jsdelivr.net/npm/stream-chat"></script>
59
- <script src="https://cdn.jsdelivr.net/npm/stream-chat-react"></script>
60
- ```
61
-
62
- ## Example Apps
53
+ ## Example Applications
63
54
 
64
55
  We have built five demo applications showcasing a variety of chat use cases, including social messaging, team collaboration, customer support, livestream gaming, and virtual event. You can preview these [demos](https://getstream.io/chat/demos/) on our website. Also, the code is [open source](https://github.com/GetStream/website-react-examples/).
65
56
 
66
- ## Docs
57
+ ## Documentation
67
58
 
68
59
  We use a doc generator to build our [component documentation](https://getstream.io/chat/docs/sdk/react/). We provide a brief description of each chat component and define all of the props it accepts.
69
60
 
70
- The React components are created using the [stream-chat-js](https://github.com/getstream/stream-chat-js) library. If you're customizing the components, it's likely you'll need to make additional calls to our Chat API using our JavaScript client, which has [documentation](https://getstream.io/chat/docs/js/) on our website.
71
-
72
- ## TypeScript Support
61
+ The React components are created using the [stream-chat](https://github.com/getstream/stream-chat-js) library. If you're customizing the components, it's likely you'll need to make additional calls to our Chat API using our JavaScript client, which has [documentation](https://getstream.io/chat/docs/javascript/) on our website.
73
62
 
74
- As of version `5.0.0`, the component library has been converted to TypeScript. Please read the [TypeScript guide](https://github.com/GetStream/stream-chat-react/wiki/Typescript-support) for details and implementation assistance.
63
+ ## Component Reusability
75
64
 
76
- ## Component Reusability
77
-
78
- For components that implement significant logic, it's helpful to split the component into two parts: a top-level component which handles functionality and a lower level component which renders the UI. This way you can swap UI without altering the logic that gives the component its functionality. We use this provider/consumer pattern frequently in the library, and the below example shows how to swap out the `Message` UI component with `MessageTeam`, without affecting any logic in the app.
65
+ For components that implement significant logic, it's helpful to split the component into two parts: a top-level component which handles functionality and a lower level component which renders the UI. This way you can swap UI without altering the logic that gives the component its functionality. We use this provider/consumer pattern frequently in the library, and the below example shows how to swap out the `Message` UI component with `CustomMessageUI` (using `WithComponents`), without affecting any logic in the application.
79
66
 
80
67
  ```jsx
81
- <Channel Message={MessageTeam}>
68
+ <Channel>
82
69
  <Window>
83
70
  <ChannelHeader />
84
- <MessageList />
71
+ <WithComponents overrides={{ Message: CustomMessageUI }}>
72
+ <MessageList />
73
+ </WithComponents>
85
74
  <MessageInput />
86
75
  </Window>
87
76
  <Thread />
@@ -90,11 +79,18 @@ For components that implement significant logic, it's helpful to split the compo
90
79
 
91
80
  ### Customizing Styles
92
81
 
93
- The preferred method for overriding the pre-defined styles in the library is to two-step process. First, import our bundled CSS into the file where you instantiate your chat application. Second, locate any Stream styles you want to override using either the browser inspector or by viewing the library code. You can then add selectors to your local CSS file to override our defaults. For example:
82
+ The preferred method for overriding the pre-defined styles in the library is to two-step process. First, import our bundled CSS into your main CSS file (or CSS file loaded with your chat application). Second, locate any Stream styles you want to override using either the browser inspector or by viewing the library code. You can then add selectors to your local CSS file to override our defaults (ideally within the stream-overrides layer). Layers (when ordered correctly, see example) ensure that your overrides take precedence even if your overriding selectors are less specific. For example:
83
+
84
+ ```css title="index.css"
85
+ @layer stream, stream-overrides;
94
86
 
95
- ```js
96
- import 'stream-chat-react/dist/css/v2/index.css';
97
- import './App.css';
87
+ @import 'stream-chat-react/css/v2/index.css' layer(stream);
88
+ /* or */
89
+ @import 'stream-chat-react/dist/css/v2/index.css' layer(stream);
90
+
91
+ @layer stream-overrides {
92
+ /* your overrides */
93
+ }
98
94
  ```
99
95
 
100
96
  ## Internationalization
@@ -106,6 +102,7 @@ Our library supports auto-translation for various user languages. Please read ou
106
102
  We welcome code changes that improve this library or fix a problem. Please make sure to follow all best practices and add tests, if applicable, before submitting a pull request on GitHub. We are pleased to merge your code into the official repository if it meets a need. Make sure to sign our [Contributor License Agreement (CLA)](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) first. See our license file for more details.
107
103
 
108
104
  ## We are hiring!
105
+
109
106
  We recently closed a [$38 million Series B funding round](https://techcrunch.com/2021/03/04/stream-raises-38m-as-its-chat-and-activity-feed-apis-power-communications-for-1b-users/) and are actively growing.
110
107
  Our APIs are used by more than a billion end-users, and by working at Stream, you have the chance to make a huge impact on a team of very strong engineers.
111
108
 
@@ -113,10 +110,10 @@ Check out our current openings and apply via [Stream's website](https://getstrea
113
110
 
114
111
  ## Acknowledgements
115
112
 
116
- ### Lamejs
113
+ ### lamejs
117
114
 
118
115
  This project uses `lamejs` library under the LGPL license to convert the recorded audio to mp3 format.
119
116
  The library source code is dynamically imported and used only if audio recording is enabled.
120
117
 
121
118
  You can obtain the source code for `lamejs` from the [lamejs repository](https://github.com/gideonstele/lamejs) that is a fork of [the original JS library](https://github.com/zhuker/lamejs).
122
- You can find the source code for LAME at https://lame.sourceforge.net and its license at: https://lame.sourceforge.net/license.txt
119
+ You can find the source code for LAME at https://lame.sourceforge.net and its license at: https://lame.sourceforge.net/license.txt
@@ -6,7 +6,7 @@ import type { ChannelQueryOptions, EventAPIResponse, Message, MessageResponse, C
6
6
  import type { MessageInputProps } from '../MessageInput';
7
7
  import type { ChannelUnreadUiState, CustomTrigger, DefaultStreamChatGenerics, GiphyVersions, ImageAttachmentSizeHandler, SendMessageOptions, UpdateMessageOptions, VideoAttachmentSizeHandler } from '../../types/types';
8
8
  import type { URLEnrichmentConfig } from '../MessageInput/hooks/useLinkPreviews';
9
- type ChannelPropsForwardedToComponentContext<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = Pick<ComponentContextValue<StreamChatGenerics>, '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' | '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' | 'ThreadHead' | 'ThreadHeader' | 'ThreadStart' | 'Timestamp' | 'TriggerProvider' | 'TypingIndicator' | 'UnreadMessagesNotification' | 'UnreadMessagesSeparator' | 'VirtualMessage' | 'StopAIGenerationButton' | 'StreamedMessageText'>;
9
+ type ChannelPropsForwardedToComponentContext<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = Pick<ComponentContextValue<StreamChatGenerics>, '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' | 'ThreadHead' | 'ThreadHeader' | 'ThreadStart' | 'Timestamp' | 'TriggerProvider' | 'TypingIndicator' | 'UnreadMessagesNotification' | 'UnreadMessagesSeparator' | 'VirtualMessage' | 'StopAIGenerationButton' | 'StreamedMessageText'>;
10
10
  export type ChannelProps<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, V extends CustomTrigger = CustomTrigger> = ChannelPropsForwardedToComponentContext<StreamChatGenerics> & {
11
11
  /** List of accepted file types */
12
12
  acceptedFiles?: string[];
@@ -29,7 +29,10 @@ export type ChannelProps<StreamChatGenerics extends DefaultStreamChatGenerics =
29
29
  doSendMessageRequest?: (channel: StreamChannel<StreamChatGenerics>, message: Message<StreamChatGenerics>, options?: SendMessageOptions) => ReturnType<StreamChannel<StreamChatGenerics>['sendMessage']> | void;
30
30
  /** Custom action handler to override the default `client.updateMessage` request function (advanced usage only) */
31
31
  doUpdateMessageRequest?: (cid: string, updatedMessage: UpdatedMessage<StreamChatGenerics>, options?: UpdateMessageOptions) => ReturnType<StreamChat<StreamChatGenerics>['updateMessage']>;
32
- /** If true, chat users will be able to drag and drop file uploads to the entire channel window */
32
+ /**
33
+ * @deprecated Use `WithDragAndDropUpload` instead (wrap draggable-to elements with this component).
34
+ * @description If true, chat users will be able to drag and drop file uploads to the entire channel window
35
+ */
33
36
  dragAndDropWindow?: boolean;
34
37
  /** Custom UI component to be shown if no active channel is set, defaults to null and skips rendering the Channel component */
35
38
  EmptyPlaceholder?: React.ReactElement;
@@ -63,7 +66,10 @@ export type ChannelProps<StreamChatGenerics extends DefaultStreamChatGenerics =
63
66
  onMentionsClick?: OnMentionAction<StreamChatGenerics>;
64
67
  /** Custom action handler function to run on hover of an @mention in a message */
65
68
  onMentionsHover?: OnMentionAction<StreamChatGenerics>;
66
- /** If `dragAndDropWindow` prop is true, the props to pass to the MessageInput component (overrides props placed directly on MessageInput) */
69
+ /**
70
+ * @deprecated Use `WithDragAndDropUpload` instead (wrap draggable-to elements with this component).
71
+ * @description If `dragAndDropWindow` prop is `true`, the props to pass to the `MessageInput` component (overrides props placed directly on `MessageInput`)
72
+ */
67
73
  optionalMessageInputProps?: MessageInputProps<StreamChatGenerics, V>;
68
74
  /** You can turn on/off thumbnail generation for video attachments */
69
75
  shouldGenerateVideoThumbnail?: boolean;
@@ -787,6 +787,7 @@ const ChannelInner = (props) => {
787
787
  LoadingIndicator: props.LoadingIndicator,
788
788
  Message: props.Message,
789
789
  MessageActions: props.MessageActions,
790
+ MessageBlocked: props.MessageBlocked,
790
791
  MessageBouncePrompt: props.MessageBouncePrompt,
791
792
  MessageDeleted: props.MessageDeleted,
792
793
  MessageListNotifications: props.MessageListNotifications,
@@ -838,6 +839,7 @@ const ChannelInner = (props) => {
838
839
  props.DateSeparator,
839
840
  props.EditMessageInput,
840
841
  props.EmojiPicker,
842
+ props.emojiSearchIndex,
841
843
  props.EmptyStateIndicator,
842
844
  props.FileUploadIcon,
843
845
  props.GiphyPreviewMessage,
@@ -847,6 +849,7 @@ const ChannelInner = (props) => {
847
849
  props.LoadingIndicator,
848
850
  props.Message,
849
851
  props.MessageActions,
852
+ props.MessageBlocked,
850
853
  props.MessageBouncePrompt,
851
854
  props.MessageDeleted,
852
855
  props.MessageListNotifications,
@@ -866,11 +869,14 @@ const ChannelInner = (props) => {
866
869
  props.QuotedMessage,
867
870
  props.QuotedMessagePreview,
868
871
  props.QuotedPoll,
872
+ props.reactionOptions,
869
873
  props.ReactionSelector,
870
874
  props.ReactionsList,
871
875
  props.ReactionsListModal,
872
876
  props.SendButton,
873
877
  props.StartRecordingAudioButton,
878
+ props.StopAIGenerationButton,
879
+ props.StreamedMessageText,
874
880
  props.ThreadHead,
875
881
  props.ThreadHeader,
876
882
  props.ThreadStart,
@@ -880,10 +886,6 @@ const ChannelInner = (props) => {
880
886
  props.UnreadMessagesNotification,
881
887
  props.UnreadMessagesSeparator,
882
888
  props.VirtualMessage,
883
- props.StopAIGenerationButton,
884
- props.StreamedMessageText,
885
- props.emojiSearchIndex,
886
- props.reactionOptions,
887
889
  ]);
888
890
  const typingContextValue = useCreateTypingContext({
889
891
  typing,
@@ -24,11 +24,12 @@ export const useChat = ({ client, defaultLanguage = 'en', i18nInstance, initialN
24
24
  useEffect(() => {
25
25
  if (!client)
26
26
  return;
27
+ const version = "12.15.0";
27
28
  const userAgent = client.getUserAgent();
28
29
  if (!userAgent.includes('stream-chat-react')) {
29
30
  // result looks like: 'stream-chat-react-2.3.2-stream-chat-javascript-client-browser-2.2.2'
30
31
  // the upper-case text between double underscores is replaced with the actual semantic version of the library
31
- client.setUserAgent(`stream-chat-react-12.13.1-${userAgent}`);
32
+ client.setUserAgent(`stream-chat-react-${version}-${userAgent}`);
32
33
  }
33
34
  client.threads.registerSubscriptions();
34
35
  client.polls.registerSubscriptions();
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const MessageBlocked: () => React.JSX.Element;
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import clsx from 'clsx';
3
+ import { useUserRole } from './hooks/useUserRole';
4
+ import { useTranslationContext } from '../../context/TranslationContext';
5
+ import { useMessageContext } from '../../context';
6
+ export const MessageBlocked = () => {
7
+ const { message } = useMessageContext();
8
+ const { t } = useTranslationContext('MessageBlocked');
9
+ const { isMyMessage } = useUserRole(message);
10
+ const messageClasses = clsx('str-chat__message str-chat__message-simple str-chat__message--blocked', message.type, {
11
+ 'str-chat__message--me str-chat__message-simple--me': isMyMessage,
12
+ 'str-chat__message--other': !isMyMessage,
13
+ });
14
+ return (React.createElement("div", { className: messageClasses, "data-testid": 'message-blocked-component', key: message.id },
15
+ React.createElement("div", { className: 'str-chat__message--blocked-inner' }, t('Message was blocked by moderation policies'))));
16
+ };
@@ -3,12 +3,13 @@ import clsx from 'clsx';
3
3
  import { MessageErrorIcon } from './icons';
4
4
  import { MessageBouncePrompt as DefaultMessageBouncePrompt } from '../MessageBounce';
5
5
  import { MessageDeleted as DefaultMessageDeleted } from './MessageDeleted';
6
+ import { MessageBlocked as DefaultMessageBlocked } from './MessageBlocked';
6
7
  import { MessageOptions as DefaultMessageOptions } from './MessageOptions';
7
8
  import { MessageRepliesCountButton as DefaultMessageRepliesCountButton } from './MessageRepliesCountButton';
8
9
  import { MessageStatus as DefaultMessageStatus } from './MessageStatus';
9
10
  import { MessageText } from './MessageText';
10
11
  import { MessageTimestamp as DefaultMessageTimestamp } from './MessageTimestamp';
11
- import { areMessageUIPropsEqual, isMessageBounced, isMessageEdited, messageHasAttachments, messageHasReactions, } from './utils';
12
+ import { areMessageUIPropsEqual, isMessageBlocked, isMessageBounced, isMessageEdited, messageHasAttachments, messageHasReactions, } from './utils';
12
13
  import { Avatar as DefaultAvatar } from '../Avatar';
13
14
  import { Attachment as DefaultAttachment } from '../Attachment';
14
15
  import { CUSTOM_MESSAGE_TYPE } from '../../constants/messageTypes';
@@ -32,7 +33,7 @@ const MessageSimpleWithContext = (props) => {
32
33
  const { Attachment = DefaultAttachment, Avatar = DefaultAvatar, EditMessageInput = DefaultEditMessageForm, MessageOptions = DefaultMessageOptions,
33
34
  // TODO: remove this "passthrough" in the next
34
35
  // major release and use the new default instead
35
- MessageActions = MessageOptions, MessageDeleted = DefaultMessageDeleted, MessageBouncePrompt = DefaultMessageBouncePrompt, MessageRepliesCountButton = DefaultMessageRepliesCountButton, MessageStatus = DefaultMessageStatus, MessageTimestamp = DefaultMessageTimestamp, ReactionsList = DefaultReactionList, StreamedMessageText = DefaultStreamedMessageText, PinIndicator, } = useComponentContext('MessageSimple');
36
+ MessageActions = MessageOptions, MessageBlocked = DefaultMessageBlocked, MessageDeleted = DefaultMessageDeleted, MessageBouncePrompt = DefaultMessageBouncePrompt, MessageRepliesCountButton = DefaultMessageRepliesCountButton, MessageStatus = DefaultMessageStatus, MessageTimestamp = DefaultMessageTimestamp, ReactionsList = DefaultReactionList, StreamedMessageText = DefaultStreamedMessageText, PinIndicator, } = useComponentContext('MessageSimple');
36
37
  const hasAttachment = messageHasAttachments(message);
37
38
  const hasReactions = messageHasReactions(message);
38
39
  const isAIGenerated = useMemo(() => isMessageAIGenerated?.(message), [isMessageAIGenerated, message]);
@@ -42,6 +43,9 @@ const MessageSimpleWithContext = (props) => {
42
43
  if (message.deleted_at || message.type === 'deleted') {
43
44
  return React.createElement(MessageDeleted, { message: message });
44
45
  }
46
+ if (isMessageBlocked(message)) {
47
+ return React.createElement(MessageBlocked, null);
48
+ }
45
49
  const showMetadata = !groupedByUser || endOfGroup;
46
50
  const showReplyCountButton = !threadList && !!message.reply_count;
47
51
  const allowRetry = message.status === 'failed' && message.errorStatusCode !== 403;
@@ -77,4 +77,5 @@ export declare const mapToUserNameOrId: TooltipUsernameMapper;
77
77
  export declare const getReadByTooltipText: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(users: UserResponse<StreamChatGenerics>[], t: TFunction, client: StreamChat<StreamChatGenerics>, tooltipUserNameMapper: TooltipUsernameMapper) => string;
78
78
  export declare const isOnlyEmojis: (text?: string) => boolean;
79
79
  export declare const isMessageBounced: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(message: Pick<StreamMessage<StreamChatGenerics>, 'type' | 'moderation' | 'moderation_details'>) => boolean;
80
+ export declare const isMessageBlocked: (message: Pick<StreamMessage, 'type' | 'moderation' | 'moderation_details'>) => boolean;
80
81
  export declare const isMessageEdited: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(message: Pick<StreamMessage<StreamChatGenerics>, 'message_text_updated_at'>) => boolean;
@@ -310,4 +310,7 @@ export const isOnlyEmojis = (text) => {
310
310
  export const isMessageBounced = (message) => message.type === 'error' &&
311
311
  (message.moderation_details?.action === 'MESSAGE_RESPONSE_ACTION_BOUNCE' ||
312
312
  message.moderation?.action === 'bounce');
313
+ export const isMessageBlocked = (message) => message.type === 'error' &&
314
+ (message.moderation_details?.action === 'MESSAGE_RESPONSE_ACTION_REMOVE' ||
315
+ message.moderation?.action === 'remove');
313
316
  export const isMessageEdited = (message) => !!message.message_text_updated_at;
@@ -8,6 +8,7 @@ import { useChannelStateContext } from '../../context/ChannelStateContext';
8
8
  import { useComponentContext, } from '../../context/ComponentContext';
9
9
  import { MessageInputContextProvider } from '../../context/MessageInputContext';
10
10
  import { DialogManagerProvider } from '../../context';
11
+ import { useRegisterDropHandlers } from './WithDragAndDropUpload';
11
12
  const MessageInputProvider = (props) => {
12
13
  const cooldownTimerState = useCooldownTimer();
13
14
  const messageInputState = useMessageInputState(props);
@@ -18,6 +19,8 @@ const MessageInputProvider = (props) => {
18
19
  ...props,
19
20
  emojiSearchIndex: props.emojiSearchIndex ?? emojiSearchIndex,
20
21
  });
22
+ // @ts-expect-error generics to be removed
23
+ useRegisterDropHandlers(messageInputContextValue);
21
24
  return (React.createElement(MessageInputContextProvider, { value: messageInputContextValue }, props.children));
22
25
  };
23
26
  const UnMemoizedMessageInput = (props) => {
@@ -1,6 +1,4 @@
1
1
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
- import clsx from 'clsx';
3
- import { useDropzone } from 'react-dropzone';
4
2
  import { AttachmentSelector as DefaultAttachmentSelector, SimpleAttachmentSelector, } from './AttachmentSelector';
5
3
  import { AttachmentPreviewList as DefaultAttachmentPreviewList } from './AttachmentPreviewList';
6
4
  import { CooldownTimer as DefaultCooldownTimer } from './CooldownTimer';
@@ -14,15 +12,14 @@ import { RecordingAttachmentType } from '../MediaRecorder/classes';
14
12
  import { useChatContext } from '../../context/ChatContext';
15
13
  import { useChannelActionContext } from '../../context/ChannelActionContext';
16
14
  import { useChannelStateContext } from '../../context/ChannelStateContext';
17
- import { useTranslationContext } from '../../context/TranslationContext';
18
15
  import { useMessageInputContext } from '../../context/MessageInputContext';
19
16
  import { useComponentContext } from '../../context/ComponentContext';
20
17
  import { AIStates, useAIState } from '../AIStateIndicator';
18
+ import { WithDragAndDropUpload } from './WithDragAndDropUpload';
21
19
  export const MessageInputFlat = () => {
22
- const { t } = useTranslationContext('MessageInputFlat');
23
- const { asyncMessagesMultiSendEnabled, attachments, cooldownRemaining, findAndEnqueueURLsToEnrich, handleSubmit, hideSendButton, isUploadEnabled, linkPreviews, maxFilesLeft, message, numberOfUploads, parent, recordingController, setCooldownRemaining, text, uploadNewFiles, } = useMessageInputContext('MessageInputFlat');
20
+ const { asyncMessagesMultiSendEnabled, attachments, cooldownRemaining, findAndEnqueueURLsToEnrich, handleSubmit, hideSendButton, isUploadEnabled, linkPreviews, message, numberOfUploads, parent, recordingController, setCooldownRemaining, text, } = useMessageInputContext('MessageInputFlat');
24
21
  const { AttachmentPreviewList = DefaultAttachmentPreviewList, AttachmentSelector = message ? SimpleAttachmentSelector : DefaultAttachmentSelector, AudioRecorder = DefaultAudioRecorder, CooldownTimer = DefaultCooldownTimer, EmojiPicker, LinkPreviewList = DefaultLinkPreviewList, QuotedMessagePreview = DefaultQuotedMessagePreview, RecordingPermissionDeniedNotification = DefaultRecordingPermissionDeniedNotification, SendButton = DefaultSendButton, StartRecordingAudioButton = DefaultStartRecordingAudioButton, StopAIGenerationButton: StopAIGenerationButtonOverride, } = useComponentContext('MessageInputFlat');
25
- const { acceptedFiles = [], multipleUploads, quotedMessage, } = useChannelStateContext('MessageInputFlat');
22
+ const { quotedMessage } = useChannelStateContext('MessageInputFlat');
26
23
  const { setQuotedMessage } = useChannelActionContext('MessageInputFlat');
27
24
  const { channel } = useChatContext('MessageInputFlat');
28
25
  const { aiState } = useAIState(channel);
@@ -32,17 +29,6 @@ export const MessageInputFlat = () => {
32
29
  setShowRecordingPermissionDeniedNotification(false);
33
30
  }, []);
34
31
  const failedUploadsCount = useMemo(() => attachments.filter((a) => a.localMetadata?.uploadState === 'failed').length, [attachments]);
35
- const accept = useMemo(() => acceptedFiles.reduce((mediaTypeMap, mediaType) => {
36
- mediaTypeMap[mediaType] ?? (mediaTypeMap[mediaType] = []);
37
- return mediaTypeMap;
38
- }, {}), [acceptedFiles]);
39
- const { getRootProps, isDragActive, isDragReject } = useDropzone({
40
- accept,
41
- disabled: !isUploadEnabled || maxFilesLeft === 0,
42
- multiple: multipleUploads,
43
- noClick: true,
44
- onDrop: uploadNewFiles,
45
- });
46
32
  useEffect(() => {
47
33
  const handleQuotedMessageUpdate = (e) => {
48
34
  if (e.message?.id !== quotedMessage?.id)
@@ -79,35 +65,29 @@ export const MessageInputFlat = () => {
79
65
  : StopAIGenerationButtonOverride;
80
66
  const shouldDisplayStopAIGeneration = [AIStates.Thinking, AIStates.Generating].includes(aiState) &&
81
67
  !!StopAIGenerationButton;
82
- return (React.createElement(React.Fragment, null,
83
- React.createElement("div", { ...getRootProps({ className: 'str-chat__message-input' }) },
84
- recordingEnabled &&
85
- recordingController.permissionState === 'denied' &&
86
- showRecordingPermissionDeniedNotification && (React.createElement(RecordingPermissionDeniedNotification, { onClose: closePermissionDeniedNotification, permissionName: RecordingPermission.MIC })),
87
- findAndEnqueueURLsToEnrich && (React.createElement(LinkPreviewList, { linkPreviews: Array.from(linkPreviews.values()) })),
88
- isDragActive && (React.createElement("div", { className: clsx('str-chat__dropzone-container', {
89
- 'str-chat__dropzone-container--not-accepted': isDragReject,
90
- }) },
91
- !isDragReject && React.createElement("p", null, t('Drag your files here')),
92
- isDragReject && React.createElement("p", null, t('Some of the files will not be accepted')))),
93
- displayQuotedMessage && React.createElement(QuotedMessagePreviewHeader, null),
94
- React.createElement("div", { className: 'str-chat__message-input-inner' },
95
- React.createElement(AttachmentSelector, null),
96
- React.createElement("div", { className: 'str-chat__message-textarea-container' },
97
- displayQuotedMessage && (React.createElement(QuotedMessagePreview, { quotedMessage: quotedMessage })),
98
- isUploadEnabled &&
99
- !!(numberOfUploads + failedUploadsCount || attachments.length > 0) && (React.createElement(AttachmentPreviewList, null)),
100
- React.createElement("div", { className: 'str-chat__message-textarea-with-emoji-picker' },
101
- React.createElement(ChatAutoComplete, null),
102
- EmojiPicker && React.createElement(EmojiPicker, null))),
103
- shouldDisplayStopAIGeneration ? (React.createElement(StopAIGenerationButton, { onClick: stopGenerating })) : (!hideSendButton && (React.createElement(React.Fragment, null, cooldownRemaining ? (React.createElement(CooldownTimer, { cooldownInterval: cooldownRemaining, setCooldownRemaining: setCooldownRemaining })) : (React.createElement(React.Fragment, null,
104
- React.createElement(SendButton, { disabled: !numberOfUploads &&
105
- !text.length &&
106
- attachments.length - failedUploadsCount === 0, sendMessage: handleSubmit }),
107
- recordingEnabled && (React.createElement(StartRecordingAudioButton, { disabled: isRecording ||
108
- (!asyncMessagesMultiSendEnabled &&
109
- attachments.some((a) => a.type === RecordingAttachmentType.VOICE_RECORDING)), onClick: () => {
110
- recordingController.recorder?.start();
111
- setShowRecordingPermissionDeniedNotification(true);
112
- } })))))))))));
68
+ return (React.createElement(WithDragAndDropUpload, { className: 'str-chat__message-input', component: 'div' },
69
+ recordingEnabled &&
70
+ recordingController.permissionState === 'denied' &&
71
+ showRecordingPermissionDeniedNotification && (React.createElement(RecordingPermissionDeniedNotification, { onClose: closePermissionDeniedNotification, permissionName: RecordingPermission.MIC })),
72
+ findAndEnqueueURLsToEnrich && (React.createElement(LinkPreviewList, { linkPreviews: Array.from(linkPreviews.values()) })),
73
+ displayQuotedMessage && React.createElement(QuotedMessagePreviewHeader, null),
74
+ React.createElement("div", { className: 'str-chat__message-input-inner' },
75
+ React.createElement(AttachmentSelector, null),
76
+ React.createElement("div", { className: 'str-chat__message-textarea-container' },
77
+ displayQuotedMessage && React.createElement(QuotedMessagePreview, { quotedMessage: quotedMessage }),
78
+ isUploadEnabled &&
79
+ !!(numberOfUploads + failedUploadsCount || attachments.length > 0) && (React.createElement(AttachmentPreviewList, null)),
80
+ React.createElement("div", { className: 'str-chat__message-textarea-with-emoji-picker' },
81
+ React.createElement(ChatAutoComplete, null),
82
+ EmojiPicker && React.createElement(EmojiPicker, null))),
83
+ shouldDisplayStopAIGeneration ? (React.createElement(StopAIGenerationButton, { onClick: stopGenerating })) : (!hideSendButton && (React.createElement(React.Fragment, null, cooldownRemaining ? (React.createElement(CooldownTimer, { cooldownInterval: cooldownRemaining, setCooldownRemaining: setCooldownRemaining })) : (React.createElement(React.Fragment, null,
84
+ React.createElement(SendButton, { disabled: !numberOfUploads &&
85
+ !text.length &&
86
+ attachments.length - failedUploadsCount === 0, sendMessage: handleSubmit }),
87
+ recordingEnabled && (React.createElement(StartRecordingAudioButton, { disabled: isRecording ||
88
+ (!asyncMessagesMultiSendEnabled &&
89
+ attachments.some((a) => a.type === RecordingAttachmentType.VOICE_RECORDING)), onClick: () => {
90
+ recordingController.recorder?.start();
91
+ setShowRecordingPermissionDeniedNotification(true);
92
+ } }))))))))));
113
93
  };
@@ -0,0 +1,37 @@
1
+ import React, { CSSProperties, ElementType, PropsWithChildren } from 'react';
2
+ import { MessageInputContextValue } from '../../context';
3
+ export declare const useDragAndDropUploadContext: () => {
4
+ subscribeToDrop: ((fn: (files: File[]) => void) => () => void) | null;
5
+ };
6
+ /**
7
+ * @private This hook should be used only once directly in the `MessageInputProvider` to
8
+ * register `uploadNewFiles` functions of the rendered `MessageInputs`. Each `MessageInput`
9
+ * will then be notified when the drop event occurs from within the `WithDragAndDropUpload`
10
+ * component.
11
+ */
12
+ export declare const useRegisterDropHandlers: ({ uploadNewFiles }: MessageInputContextValue) => void;
13
+ /**
14
+ * Wrapper to replace now deprecated `Channel.dragAndDropWindow` option.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * <Channel>
19
+ * <WithDragAndDropUpload component="section" className="message-list-dnd-wrapper">
20
+ * <Window>
21
+ * <MessageList />
22
+ * <MessageInput />
23
+ * </Window>
24
+ * </WithDragAndDropUpload>
25
+ * <Thread />
26
+ * <Channel>
27
+ * ```
28
+ */
29
+ export declare const WithDragAndDropUpload: ({ children, className, component: Component, style, }: PropsWithChildren<{
30
+ /**
31
+ * @description An element to render as a wrapper onto which drag & drop functionality will be applied.
32
+ * @default 'div'
33
+ */
34
+ component?: ElementType;
35
+ className?: string;
36
+ style?: CSSProperties;
37
+ }>) => React.JSX.Element;
@@ -0,0 +1,85 @@
1
+ import React, { useCallback, useContext, useEffect, useMemo, useRef, } from 'react';
2
+ import { useChannelStateContext, useMessageInputContext, useTranslationContext, } from '../../context';
3
+ import { useDropzone } from 'react-dropzone';
4
+ import clsx from 'clsx';
5
+ const DragAndDropUploadContext = React.createContext({
6
+ subscribeToDrop: null,
7
+ });
8
+ export const useDragAndDropUploadContext = () => useContext(DragAndDropUploadContext);
9
+ /**
10
+ * @private This hook should be used only once directly in the `MessageInputProvider` to
11
+ * register `uploadNewFiles` functions of the rendered `MessageInputs`. Each `MessageInput`
12
+ * will then be notified when the drop event occurs from within the `WithDragAndDropUpload`
13
+ * component.
14
+ */
15
+ export const useRegisterDropHandlers = ({ uploadNewFiles }) => {
16
+ const { subscribeToDrop } = useDragAndDropUploadContext();
17
+ useEffect(() => {
18
+ const unsubscribe = subscribeToDrop?.(uploadNewFiles);
19
+ return unsubscribe;
20
+ }, [subscribeToDrop, uploadNewFiles]);
21
+ };
22
+ /**
23
+ * Wrapper to replace now deprecated `Channel.dragAndDropWindow` option.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * <Channel>
28
+ * <WithDragAndDropUpload component="section" className="message-list-dnd-wrapper">
29
+ * <Window>
30
+ * <MessageList />
31
+ * <MessageInput />
32
+ * </Window>
33
+ * </WithDragAndDropUpload>
34
+ * <Thread />
35
+ * <Channel>
36
+ * ```
37
+ */
38
+ export const WithDragAndDropUpload = ({ children, className, component: Component = 'div', style, }) => {
39
+ const dropHandlersRef = useRef(new Set());
40
+ const { acceptedFiles = [], multipleUploads } = useChannelStateContext();
41
+ const { t } = useTranslationContext();
42
+ const messageInputContext = useMessageInputContext();
43
+ const dragAndDropUploadContext = useDragAndDropUploadContext();
44
+ // if message input context is available, there's no need to use the queue
45
+ const isWithinMessageInputContext = typeof messageInputContext.uploadNewFiles === 'function';
46
+ const accept = useMemo(() => acceptedFiles.reduce((mediaTypeMap, mediaType) => {
47
+ mediaTypeMap[mediaType] ?? (mediaTypeMap[mediaType] = []);
48
+ return mediaTypeMap;
49
+ }, {}), [acceptedFiles]);
50
+ const subscribeToDrop = useCallback((fn) => {
51
+ dropHandlersRef.current.add(fn);
52
+ return () => {
53
+ dropHandlersRef.current.delete(fn);
54
+ };
55
+ }, []);
56
+ const handleDrop = useCallback((files) => {
57
+ dropHandlersRef.current.forEach((fn) => fn(files));
58
+ }, []);
59
+ const { getRootProps, isDragActive, isDragReject } = useDropzone({
60
+ accept,
61
+ // apply `disabled` rules if available, otherwise allow anything and
62
+ // let the `uploadNewFiles` handle the limitations internally
63
+ disabled: isWithinMessageInputContext
64
+ ? !messageInputContext.isUploadEnabled || messageInputContext.maxFilesLeft === 0
65
+ : false,
66
+ multiple: multipleUploads,
67
+ noClick: true,
68
+ onDrop: isWithinMessageInputContext ? messageInputContext.uploadNewFiles : handleDrop,
69
+ });
70
+ // nested WithDragAndDropUpload components render wrappers without functionality
71
+ // (MessageInputFlat has a default WithDragAndDropUpload)
72
+ if (dragAndDropUploadContext.subscribeToDrop !== null) {
73
+ return React.createElement(Component, { className: className }, children);
74
+ }
75
+ return (React.createElement(DragAndDropUploadContext.Provider, { value: {
76
+ subscribeToDrop,
77
+ } },
78
+ React.createElement(Component, { ...getRootProps({ className, style }) },
79
+ isDragActive && (React.createElement("div", { className: clsx('str-chat__dropzone-container', {
80
+ 'str-chat__dropzone-container--not-accepted': isDragReject,
81
+ }) },
82
+ !isDragReject && React.createElement("p", null, t('Drag your files here')),
83
+ isDragReject && React.createElement("p", null, t('Some of the files will not be accepted')))),
84
+ children)));
85
+ };
@@ -11,4 +11,5 @@ export * from './MessageInput';
11
11
  export * from './MessageInputFlat';
12
12
  export * from './QuotedMessagePreview';
13
13
  export * from './SendButton';
14
+ export { WithDragAndDropUpload } from './WithDragAndDropUpload';
14
15
  export * from './types';
@@ -10,4 +10,5 @@ export * from './MessageInput';
10
10
  export * from './MessageInputFlat';
11
11
  export * from './QuotedMessagePreview';
12
12
  export * from './SendButton';
13
+ export { WithDragAndDropUpload } from './WithDragAndDropUpload';
13
14
  export * from './types';
@@ -59,6 +59,8 @@ export type ComponentContextValue<StreamChatGenerics extends DefaultStreamChatGe
59
59
  MessageActions?: React.ComponentType;
60
60
  /** Custom UI component to display the contents of a bounced message modal. Usually it allows to retry, edit, or delete the message. Defaults to and accepts the same props as: [MessageBouncePrompt](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageBounce/MessageBouncePrompt.tsx) */
61
61
  MessageBouncePrompt?: React.ComponentType<MessageBouncePromptProps>;
62
+ /** Custom UI component for a moderation-blocked message, defaults to and accepts same props as: [MessageBlocked](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageBlocked.tsx) */
63
+ MessageBlocked?: React.ComponentType;
62
64
  /** Custom UI component for a deleted message, defaults to and accepts same props as: [MessageDeleted](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageDeleted.tsx) */
63
65
  MessageDeleted?: React.ComponentType<MessageDeletedProps<StreamChatGenerics>>;
64
66
  MessageListMainPanel?: React.ComponentType<PropsWithChildrenOnly>;
@@ -1,10 +1,11 @@
1
1
  import React, { createContext, useContext } from 'react';
2
2
  export const MessageInputContext = createContext(undefined);
3
3
  export const MessageInputContextProvider = ({ children, value, }) => (React.createElement(MessageInputContext.Provider, { value: value }, children));
4
- export const useMessageInputContext = (componentName) => {
4
+ export const useMessageInputContext = (
5
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
6
+ componentName) => {
5
7
  const contextValue = useContext(MessageInputContext);
6
8
  if (!contextValue) {
7
- console.warn(`The useMessageInputContext hook was called outside of the MessageInputContext provider. Make sure this hook is called within the MessageInput's UI component. The errored call is located in the ${componentName} component.`);
8
9
  return {};
9
10
  }
10
11
  return contextValue;