stream-chat-react 12.0.0-rc.2 → 12.0.0-rc.3

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 (91) hide show
  1. package/README.md +10 -0
  2. package/dist/components/Attachment/components/WaveProgressBar.d.ts +3 -1
  3. package/dist/components/Attachment/components/WaveProgressBar.js +44 -9
  4. package/dist/components/Channel/channelState.js +1 -0
  5. package/dist/components/DateSeparator/DateSeparator.js +1 -1
  6. package/dist/components/EventComponent/EventComponent.js +1 -1
  7. package/dist/components/InfiniteScrollPaginator/InfiniteScroll.js +9 -3
  8. package/dist/components/MediaRecorder/classes/MediaRecorderController.d.ts +6 -7
  9. package/dist/components/MediaRecorder/classes/MediaRecorderController.js +0 -5
  10. package/dist/components/MediaRecorder/hooks/index.d.ts +1 -1
  11. package/dist/components/MediaRecorder/hooks/useMediaRecorder.d.ts +1 -2
  12. package/dist/components/MediaRecorder/hooks/useMediaRecorder.js +1 -1
  13. package/dist/components/MediaRecorder/index.d.ts +1 -0
  14. package/dist/components/MediaRecorder/transcode/index.d.ts +6 -5
  15. package/dist/components/MediaRecorder/transcode/index.js +5 -15
  16. package/dist/components/Message/MessageSimple.js +1 -1
  17. package/dist/components/Message/MessageTimestamp.d.ts +0 -1
  18. package/dist/components/Message/MessageTimestamp.js +0 -1
  19. package/dist/components/Message/Timestamp.d.ts +0 -1
  20. package/dist/components/Message/Timestamp.js +2 -3
  21. package/dist/components/Message/renderText/remarkPlugins/keepLineBreaksPlugin.js +1 -1
  22. package/dist/components/Message/utils.js +2 -0
  23. package/dist/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.js +23 -27
  24. package/dist/components/MessageInput/AttachmentPreviewList/FileAttachmentPreview.d.ts +1 -0
  25. package/dist/components/MessageInput/AttachmentPreviewList/FileAttachmentPreview.js +1 -1
  26. package/dist/components/MessageInput/AttachmentPreviewList/ImageAttachmentPreview.js +2 -1
  27. package/dist/components/MessageInput/MessageInput.d.ts +4 -6
  28. package/dist/components/MessageInput/MessageInputFlat.js +4 -7
  29. package/dist/components/MessageInput/hooks/useAttachments.d.ts +1 -5
  30. package/dist/components/MessageInput/hooks/useAttachments.js +65 -52
  31. package/dist/components/MessageInput/hooks/useCreateMessageInputContext.js +2 -19
  32. package/dist/components/MessageInput/hooks/useMessageInputState.d.ts +2 -35
  33. package/dist/components/MessageInput/hooks/useMessageInputState.js +2 -107
  34. package/dist/components/MessageInput/hooks/usePasteHandler.js +1 -3
  35. package/dist/components/MessageInput/hooks/useSubmitHandler.js +19 -71
  36. package/dist/components/MessageInput/hooks/utils.d.ts +1 -2
  37. package/dist/components/MessageInput/icons.d.ts +0 -1
  38. package/dist/components/MessageInput/icons.js +0 -3
  39. package/dist/components/MessageInput/types.d.ts +3 -30
  40. package/dist/components/MessageList/MessageList.d.ts +3 -1
  41. package/dist/components/MessageList/MessageList.js +2 -1
  42. package/dist/components/MessageList/VirtualizedMessageList.d.ts +3 -1
  43. package/dist/components/MessageList/VirtualizedMessageList.js +3 -3
  44. package/dist/components/MessageList/VirtualizedMessageListComponents.js +3 -2
  45. package/dist/components/MessageList/hooks/MessageList/useEnrichedMessages.d.ts +2 -1
  46. package/dist/components/MessageList/hooks/MessageList/useEnrichedMessages.js +3 -3
  47. package/dist/components/MessageList/utils.d.ts +1 -1
  48. package/dist/components/MessageList/utils.js +16 -6
  49. package/dist/components/ReactFileUtilities/types.d.ts +0 -29
  50. package/dist/components/ReactFileUtilities/utils.d.ts +2 -0
  51. package/dist/components/ReactFileUtilities/utils.js +2 -0
  52. package/dist/context/ChannelActionContext.d.ts +2 -2
  53. package/dist/context/MessageInputContext.d.ts +1 -5
  54. package/dist/i18n/Streami18n.d.ts +2 -0
  55. package/dist/i18n/de.json +3 -1
  56. package/dist/i18n/en.json +3 -1
  57. package/dist/i18n/es.json +3 -1
  58. package/dist/i18n/fr.json +3 -1
  59. package/dist/i18n/hi.json +3 -1
  60. package/dist/i18n/it.json +3 -1
  61. package/dist/i18n/ja.json +3 -1
  62. package/dist/i18n/ko.json +3 -1
  63. package/dist/i18n/nl.json +3 -1
  64. package/dist/i18n/pt.json +3 -1
  65. package/dist/i18n/ru.json +3 -1
  66. package/dist/i18n/tr.json +3 -1
  67. package/dist/i18n/utils.d.ts +3 -3
  68. package/dist/index.cjs.js +1987 -12143
  69. package/dist/index.cjs.js.map +4 -4
  70. package/dist/{components → plugins}/Emojis/EmojiPicker.js +1 -1
  71. package/dist/plugins/Emojis/icons.d.ts +2 -0
  72. package/dist/plugins/Emojis/icons.js +4 -0
  73. package/dist/{components → plugins}/Emojis/index.cjs.js +23 -22
  74. package/dist/plugins/Emojis/index.cjs.js.map +7 -0
  75. package/dist/plugins/Emojis/index.d.ts +2 -0
  76. package/dist/plugins/Emojis/index.js +2 -0
  77. package/dist/plugins/encoders/mp3.cjs.js +111 -0
  78. package/dist/plugins/encoders/mp3.cjs.js.map +7 -0
  79. package/dist/{components/MediaRecorder/transcode → plugins/encoders}/mp3.js +3 -3
  80. package/package.json +16 -6
  81. package/dist/components/Emojis/index.cjs.js.map +0 -7
  82. package/dist/components/Emojis/index.d.ts +0 -1
  83. package/dist/components/Emojis/index.js +0 -1
  84. package/dist/components/MessageInput/AttachmentPreviewList/UploadPreviewItem.d.ts +0 -11
  85. package/dist/components/MessageInput/AttachmentPreviewList/UploadPreviewItem.js +0 -51
  86. package/dist/components/MessageInput/hooks/useFileUploads.d.ts +0 -7
  87. package/dist/components/MessageInput/hooks/useFileUploads.js +0 -85
  88. package/dist/components/MessageInput/hooks/useImageUploads.d.ts +0 -8
  89. package/dist/components/MessageInput/hooks/useImageUploads.js +0 -94
  90. /package/dist/{components → plugins}/Emojis/EmojiPicker.d.ts +0 -0
  91. /package/dist/{components/MediaRecorder/transcode → plugins/encoders}/mp3.d.ts +0 -0
@@ -1,11 +1,22 @@
1
1
  import { useCallback } from 'react';
2
2
  import { nanoid } from 'nanoid';
3
- import { useImageUploads } from './useImageUploads';
4
- import { useFileUploads } from './useFileUploads';
5
3
  import { checkUploadPermissions } from './utils';
6
- import { isLocalAttachment, isLocalImageAttachment, isUploadedImage } from '../../Attachment';
4
+ import { isLocalAttachment, isLocalImageAttachment } from '../../Attachment';
5
+ import { createFileFromBlobs, generateFileName, isBlobButNotFile } from '../../ReactFileUtilities';
7
6
  import { useChannelActionContext, useChannelStateContext, useChatContext, useTranslationContext, } from '../../../context';
8
7
  const apiMaxNumberOfFiles = 10;
8
+ // const isAudioFile = (file: FileLike) => file.type.includes('audio/');
9
+ const isImageFile = (file) => file.type.startsWith('image/') && !file.type.endsWith('.photoshop'); // photoshop files begin with 'image/'
10
+ // const isVideoFile = (file: FileLike) => file.type.includes('video/');
11
+ const getAttachmentTypeFromMime = (mimeType) => {
12
+ if (mimeType.startsWith('image/') && !mimeType.endsWith('.photoshop'))
13
+ return 'image';
14
+ if (mimeType.includes('video/'))
15
+ return 'video';
16
+ if (mimeType.includes('audio/'))
17
+ return 'audio';
18
+ return 'file';
19
+ };
9
20
  const ensureIsLocalAttachment = (attachment) => {
10
21
  if (isLocalAttachment(attachment)) {
11
22
  return attachment;
@@ -21,45 +32,15 @@ const ensureIsLocalAttachment = (attachment) => {
21
32
  };
22
33
  export const useAttachments = (props, state, dispatch, textareaRef) => {
23
34
  const { doFileUploadRequest, doImageUploadRequest, errorHandler, noFiles } = props;
24
- const { fileUploads, imageUploads } = state;
25
35
  const { getAppSettings } = useChatContext('useAttachments');
26
36
  const { t } = useTranslationContext('useAttachments');
27
37
  const { addNotification } = useChannelActionContext('useAttachments');
28
38
  const { channel, maxNumberOfFiles, multipleUploads } = useChannelStateContext('useAttachments');
29
- const { removeFile, uploadFile } = useFileUploads(props, state, dispatch);
30
- const { removeImage, uploadImage } = useImageUploads(props, state, dispatch);
31
39
  // Number of files that the user can still add. Should never be more than the amount allowed by the API.
32
40
  // If multipleUploads is false, we only want to allow a single upload.
33
41
  const maxFilesAllowed = !multipleUploads ? 1 : maxNumberOfFiles || apiMaxNumberOfFiles;
34
- // OG attachments should not be counted towards "numberOfImages"
35
- const numberOfImages = Object.values(imageUploads).filter(({ og_scrape_url, state }) => state !== 'failed' && !og_scrape_url).length;
36
- const numberOfFiles = Object.values(fileUploads).filter(({ state }) => state !== 'failed').length;
37
- const numberOfUploads = numberOfImages + numberOfFiles;
42
+ const numberOfUploads = Object.values(state.attachments).filter(({ localMetadata }) => localMetadata.uploadState && localMetadata.uploadState !== 'failed').length;
38
43
  const maxFilesLeft = maxFilesAllowed - numberOfUploads;
39
- const uploadNewFiles = useCallback((files) => {
40
- Array.from(files)
41
- .slice(0, maxFilesLeft)
42
- .forEach((file) => {
43
- const id = nanoid();
44
- if (file.type.startsWith('image/') &&
45
- !file.type.endsWith('.photoshop') // photoshop files begin with 'image/'
46
- ) {
47
- dispatch({
48
- file,
49
- id,
50
- previewUri: URL.createObjectURL?.(file),
51
- state: 'uploading',
52
- type: 'setImageUpload',
53
- });
54
- }
55
- else if (file instanceof File && !noFiles) {
56
- dispatch({ file, id, state: 'uploading', type: 'setFileUpload' });
57
- }
58
- });
59
- textareaRef?.current?.focus();
60
- },
61
- // eslint-disable-next-line react-hooks/exhaustive-deps
62
- [maxFilesLeft, noFiles]);
63
44
  const removeAttachments = useCallback((ids) => {
64
45
  if (!ids.length)
65
46
  return;
@@ -74,12 +55,13 @@ export const useAttachments = (props, state, dispatch, textareaRef) => {
74
55
  });
75
56
  }, [dispatch]);
76
57
  const uploadAttachment = useCallback(async (att) => {
77
- const { localMetadata, ...attachment } = att;
58
+ const { localMetadata, ...providedAttachmentData } = att;
78
59
  if (!localMetadata?.file)
79
60
  return att;
80
- const isImage = isUploadedImage(attachment);
81
- const id = localMetadata?.id ?? nanoid();
82
61
  const { file } = localMetadata;
62
+ const isImage = isImageFile(file);
63
+ if (noFiles && !isImage)
64
+ return att;
83
65
  const canUpload = await checkUploadPermissions({
84
66
  addNotification,
85
67
  file,
@@ -87,18 +69,31 @@ export const useAttachments = (props, state, dispatch, textareaRef) => {
87
69
  t,
88
70
  uploadType: isImage ? 'image' : 'file',
89
71
  });
90
- if (!canUpload) {
91
- const notificationText = t('Missing permissions to upload the attachment');
92
- console.error(new Error(notificationText));
93
- addNotification(notificationText, 'error');
72
+ if (!canUpload)
94
73
  return att;
74
+ localMetadata.id = localMetadata?.id ?? nanoid();
75
+ const finalAttachment = {
76
+ type: getAttachmentTypeFromMime(file.type),
77
+ };
78
+ if (isImage) {
79
+ localMetadata.previewUri = URL.createObjectURL?.(file);
80
+ if (file instanceof File) {
81
+ finalAttachment.fallback = file.name;
82
+ }
95
83
  }
84
+ else {
85
+ finalAttachment.file_size = file.size;
86
+ finalAttachment.mime_type = file.type;
87
+ if (file instanceof File) {
88
+ finalAttachment.title = file.name;
89
+ }
90
+ }
91
+ Object.assign(finalAttachment, providedAttachmentData);
96
92
  upsertAttachments([
97
93
  {
98
- ...attachment,
94
+ ...finalAttachment,
99
95
  localMetadata: {
100
96
  ...localMetadata,
101
- id,
102
97
  uploadState: 'uploading',
103
98
  },
104
99
  },
@@ -124,7 +119,7 @@ export const useAttachments = (props, state, dispatch, textareaRef) => {
124
119
  console.error(finalError);
125
120
  addNotification(finalError.message, 'error');
126
121
  const failedAttachment = {
127
- ...attachment,
122
+ ...finalAttachment,
128
123
  localMetadata: {
129
124
  ...localMetadata,
130
125
  uploadState: 'failed',
@@ -132,19 +127,19 @@ export const useAttachments = (props, state, dispatch, textareaRef) => {
132
127
  };
133
128
  upsertAttachments([failedAttachment]);
134
129
  if (errorHandler) {
135
- errorHandler(finalError, 'upload-attachment', file);
130
+ errorHandler(finalError, 'upload-attachment', { ...file, id: localMetadata.id });
136
131
  }
137
132
  return failedAttachment;
138
133
  }
139
134
  if (!response) {
140
- // Copied this from useImageUpload / useFileUpload. Not sure how failure could be handled on app level.
135
+ // Copied this from useImageUpload / useFileUpload.
141
136
  // If doUploadRequest returns any falsy value, then don't create the upload preview.
142
137
  // This is for the case if someone wants to handle failure on app level.
143
- removeAttachments([id]);
138
+ removeAttachments([localMetadata.id]);
144
139
  return;
145
140
  }
146
141
  const uploadedAttachment = {
147
- ...attachment,
142
+ ...finalAttachment,
148
143
  localMetadata: {
149
144
  ...localMetadata,
150
145
  uploadState: 'finished',
@@ -160,6 +155,9 @@ export const useAttachments = (props, state, dispatch, textareaRef) => {
160
155
  else {
161
156
  uploadedAttachment.asset_url = response.file;
162
157
  }
158
+ if (response.thumb_url) {
159
+ uploadedAttachment.thumb_url = response.thumb_url;
160
+ }
163
161
  upsertAttachments([uploadedAttachment]);
164
162
  return uploadedAttachment;
165
163
  }, [
@@ -169,19 +167,34 @@ export const useAttachments = (props, state, dispatch, textareaRef) => {
169
167
  doImageUploadRequest,
170
168
  errorHandler,
171
169
  getAppSettings,
170
+ noFiles,
172
171
  removeAttachments,
173
172
  t,
174
173
  upsertAttachments,
175
174
  ]);
175
+ const uploadNewFiles = useCallback((files) => {
176
+ const filesToBeUploaded = noFiles ? Array.from(files).filter(isImageFile) : Array.from(files);
177
+ filesToBeUploaded.slice(0, maxFilesLeft).forEach((fileLike) => {
178
+ uploadAttachment({
179
+ localMetadata: {
180
+ file: isBlobButNotFile(fileLike)
181
+ ? createFileFromBlobs({
182
+ blobsArray: [fileLike],
183
+ fileName: generateFileName(fileLike.type),
184
+ mimeType: fileLike.type,
185
+ })
186
+ : fileLike,
187
+ id: nanoid(),
188
+ },
189
+ });
190
+ });
191
+ textareaRef.current?.focus();
192
+ }, [maxFilesLeft, noFiles, textareaRef, uploadAttachment]);
176
193
  return {
177
194
  maxFilesLeft,
178
195
  numberOfUploads,
179
196
  removeAttachments,
180
- removeFile,
181
- removeImage,
182
197
  uploadAttachment,
183
- uploadFile,
184
- uploadImage,
185
198
  uploadNewFiles,
186
199
  upsertAttachments,
187
200
  };
@@ -1,15 +1,7 @@
1
1
  import { useMemo } from 'react';
2
2
  export const useCreateMessageInputContext = (value) => {
3
- const { additionalTextareaProps, asyncMessagesMultiSendEnabled, attachments, audioRecordingEnabled, autocompleteTriggers, cancelURLEnrichment, clearEditingState, closeCommandsList, closeMentionsList, cooldownInterval, cooldownRemaining, disabled, disableMentions, dismissLinkPreview, doFileUploadRequest, doImageUploadRequest, emojiSearchIndex, errorHandler, fileOrder, fileUploads, findAndEnqueueURLsToEnrich, focus, grow, handleChange, handleSubmit, hideSendButton, imageOrder, imageUploads, insertText, isUploadEnabled, linkPreviews, maxFilesLeft, maxRows, mentionAllAppUsers, mentioned_users, mentionQueryParams, message, minRows, noFiles, numberOfUploads, onPaste, onSelectUser, openCommandsList, openMentionsList, overrideSubmitHandler, parent, publishTypingEvent, recordingController, removeAttachments, removeFile, removeImage, setCooldownRemaining, setText, shouldSubmit, showCommandsList, showMentionsList, text, textareaRef, uploadAttachment, uploadFile, uploadImage, uploadNewFiles, upsertAttachments, useMentionsTransliteration, } = value;
3
+ const { additionalTextareaProps, asyncMessagesMultiSendEnabled, attachments, audioRecordingEnabled, autocompleteTriggers, cancelURLEnrichment, clearEditingState, closeCommandsList, closeMentionsList, cooldownInterval, cooldownRemaining, disabled, disableMentions, dismissLinkPreview, doFileUploadRequest, doImageUploadRequest, emojiSearchIndex, errorHandler, findAndEnqueueURLsToEnrich, focus, grow, handleChange, handleSubmit, hideSendButton, insertText, isUploadEnabled, linkPreviews, maxFilesLeft, maxRows, mentionAllAppUsers, mentioned_users, mentionQueryParams, message, minRows, noFiles, numberOfUploads, onPaste, onSelectUser, openCommandsList, openMentionsList, overrideSubmitHandler, parent, publishTypingEvent, recordingController, removeAttachments, setCooldownRemaining, setText, shouldSubmit, showCommandsList, showMentionsList, text, textareaRef, uploadAttachment, uploadNewFiles, upsertAttachments, useMentionsTransliteration, } = value;
4
4
  const editing = message?.editing;
5
- const fileUploadsValue = Object.entries(fileUploads)
6
- // eslint-disable-next-line
7
- .map(([_, value]) => value.state)
8
- .join();
9
- const imageUploadsValue = Object.entries(imageUploads)
10
- // eslint-disable-next-line
11
- .map(([_, value]) => value.state)
12
- .join();
13
5
  const linkPreviewsValue = Array.from(linkPreviews.values()).join();
14
6
  const mentionedUsersLength = mentioned_users.length;
15
7
  const parentId = parent?.id;
@@ -32,16 +24,12 @@ export const useCreateMessageInputContext = (value) => {
32
24
  doImageUploadRequest,
33
25
  emojiSearchIndex,
34
26
  errorHandler,
35
- fileOrder,
36
- fileUploads,
37
27
  findAndEnqueueURLsToEnrich,
38
28
  focus,
39
29
  grow,
40
30
  handleChange,
41
31
  handleSubmit,
42
32
  hideSendButton,
43
- imageOrder,
44
- imageUploads,
45
33
  insertText,
46
34
  isUploadEnabled,
47
35
  linkPreviews,
@@ -63,8 +51,6 @@ export const useCreateMessageInputContext = (value) => {
63
51
  publishTypingEvent,
64
52
  recordingController,
65
53
  removeAttachments,
66
- removeFile,
67
- removeImage,
68
54
  setCooldownRemaining,
69
55
  setText,
70
56
  shouldSubmit,
@@ -73,8 +59,6 @@ export const useCreateMessageInputContext = (value) => {
73
59
  text,
74
60
  textareaRef,
75
61
  uploadAttachment,
76
- uploadFile,
77
- uploadImage,
78
62
  uploadNewFiles,
79
63
  upsertAttachments,
80
64
  useMentionsTransliteration,
@@ -82,6 +66,7 @@ export const useCreateMessageInputContext = (value) => {
82
66
  // eslint-disable-next-line react-hooks/exhaustive-deps
83
67
  [
84
68
  asyncMessagesMultiSendEnabled,
69
+ attachments,
85
70
  audioRecordingEnabled,
86
71
  cancelURLEnrichment,
87
72
  cooldownInterval,
@@ -89,11 +74,9 @@ export const useCreateMessageInputContext = (value) => {
89
74
  dismissLinkPreview,
90
75
  editing,
91
76
  emojiSearchIndex,
92
- fileUploadsValue,
93
77
  findAndEnqueueURLsToEnrich,
94
78
  handleSubmit,
95
79
  hideSendButton,
96
- imageUploadsValue,
97
80
  isUploadEnabled,
98
81
  linkPreviewsValue,
99
82
  mentionedUsersLength,
@@ -1,18 +1,13 @@
1
1
  import React from 'react';
2
2
  import { EnrichURLsController } from './useLinkPreviews';
3
3
  import { RecordingController } from '../../MediaRecorder/hooks/useMediaRecorder';
4
+ import type { LinkPreviewMap, LocalAttachment } from '../types';
4
5
  import { SetLinkPreviewMode } from '../types';
5
- import type { FileUpload, ImageUpload, LinkPreviewMap, LocalAttachment } from '../types';
6
- import type { FileLike } from '../../ReactFileUtilities';
7
6
  import type { Attachment, Message, UserResponse } from 'stream-chat';
8
7
  import type { MessageInputProps } from '../MessageInput';
9
8
  import type { CustomTrigger, DefaultStreamChatGenerics, SendMessageOptions } from '../../../types/types';
10
9
  export type MessageInputState<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = {
11
10
  attachments: LocalAttachment<StreamChatGenerics>[];
12
- fileOrder: string[];
13
- fileUploads: Record<string, FileUpload>;
14
- imageOrder: string[];
15
- imageUploads: Record<string, ImageUpload>;
16
11
  linkPreviews: LinkPreviewMap;
17
12
  mentioned_users: UserResponse<StreamChatGenerics>[];
18
13
  setText: (text: string) => void;
@@ -33,40 +28,16 @@ type SetTextAction = {
33
28
  type ClearAction = {
34
29
  type: 'clear';
35
30
  };
36
- type SetImageUploadAction = {
37
- id: string;
38
- type: 'setImageUpload';
39
- file?: File | FileLike;
40
- previewUri?: string;
41
- state?: string;
42
- url?: string;
43
- };
44
- type SetFileUploadAction = {
45
- id: string;
46
- type: 'setFileUpload';
47
- file?: File;
48
- state?: string;
49
- thumb_url?: string;
50
- url?: string;
51
- };
52
31
  type SetLinkPreviewsAction = {
53
32
  linkPreviews: LinkPreviewMap;
54
33
  mode: SetLinkPreviewMode;
55
34
  type: 'setLinkPreviews';
56
35
  };
57
- type RemoveImageUploadAction = {
58
- id: string;
59
- type: 'removeImageUpload';
60
- };
61
- type RemoveFileUploadAction = {
62
- id: string;
63
- type: 'removeFileUpload';
64
- };
65
36
  type AddMentionedUserAction<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = {
66
37
  type: 'addMentionedUser';
67
38
  user: UserResponse<StreamChatGenerics>;
68
39
  };
69
- export type MessageInputReducerAction<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = SetTextAction | ClearAction | SetImageUploadAction | SetFileUploadAction | SetLinkPreviewsAction | RemoveImageUploadAction | RemoveFileUploadAction | AddMentionedUserAction<StreamChatGenerics> | UpsertAttachmentsAction | RemoveAttachmentsAction;
40
+ export type MessageInputReducerAction<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = SetTextAction | ClearAction | SetLinkPreviewsAction | AddMentionedUserAction<StreamChatGenerics> | UpsertAttachmentsAction | RemoveAttachmentsAction;
70
41
  export type MessageInputHookProps<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = EnrichURLsController & {
71
42
  handleChange: React.ChangeEventHandler<HTMLTextAreaElement>;
72
43
  handleSubmit: (event?: React.BaseSyntheticEvent, customMessageData?: Partial<Message<StreamChatGenerics>>, options?: SendMessageOptions) => void;
@@ -78,12 +49,8 @@ export type MessageInputHookProps<StreamChatGenerics extends DefaultStreamChatGe
78
49
  onSelectUser: (item: UserResponse<StreamChatGenerics>) => void;
79
50
  recordingController: RecordingController<StreamChatGenerics>;
80
51
  removeAttachments: (ids: string[]) => void;
81
- removeFile: (id: string) => void;
82
- removeImage: (id: string) => void;
83
52
  textareaRef: React.MutableRefObject<HTMLTextAreaElement | null | undefined>;
84
53
  uploadAttachment: (attachment: LocalAttachment<StreamChatGenerics>) => Promise<LocalAttachment<StreamChatGenerics> | undefined>;
85
- uploadFile: (id: string) => void;
86
- uploadImage: (id: string) => void;
87
54
  uploadNewFiles: (files: FileList | File[]) => void;
88
55
  upsertAttachments: (attachments: (Attachment<StreamChatGenerics> | LocalAttachment<StreamChatGenerics>)[]) => void;
89
56
  };
@@ -11,10 +11,6 @@ import { LinkPreviewState, SetLinkPreviewMode } from '../types';
11
11
  import { mergeDeep } from '../../../utils/mergeDeep';
12
12
  const makeEmptyMessageInputState = () => ({
13
13
  attachments: [],
14
- fileOrder: [],
15
- fileUploads: {},
16
- imageOrder: [],
17
- imageUploads: {},
18
14
  linkPreviews: new Map(),
19
15
  mentioned_users: [],
20
16
  setText: () => null,
@@ -27,43 +23,6 @@ const initState = (message) => {
27
23
  if (!message) {
28
24
  return makeEmptyMessageInputState();
29
25
  }
30
- // if message prop is defined, get image uploads, file uploads, text, etc.
31
- const imageUploads = message.attachments
32
- ?.filter(({ type }) => type === 'image')
33
- .reduce((acc, { author_name, fallback = '', image_url, og_scrape_url, text, title, title_link }) => {
34
- const id = nanoid();
35
- acc[id] = {
36
- author_name,
37
- file: {
38
- name: fallback,
39
- },
40
- id,
41
- og_scrape_url, // fixme: why scraped content is mixed with uploaded content?
42
- state: 'finished',
43
- text,
44
- title,
45
- title_link,
46
- url: image_url,
47
- };
48
- return acc;
49
- }, {}) ?? {};
50
- const fileUploads = message.attachments
51
- ?.filter(({ type }) => type === 'file')
52
- .reduce((acc, { asset_url, file_size, mime_type, thumb_url, title = '' }) => {
53
- const id = nanoid();
54
- acc[id] = {
55
- file: {
56
- name: title,
57
- size: file_size,
58
- type: mime_type,
59
- },
60
- id,
61
- state: 'finished',
62
- thumb_url,
63
- url: asset_url,
64
- };
65
- return acc;
66
- }, {}) ?? {};
67
26
  const linkPreviews = message.attachments?.reduce((acc, attachment) => {
68
27
  if (!attachment.og_scrape_url)
69
28
  return acc;
@@ -73,10 +32,8 @@ const initState = (message) => {
73
32
  });
74
33
  return acc;
75
34
  }, new Map()) ?? new Map();
76
- const imageOrder = Object.keys(imageUploads);
77
- const fileOrder = Object.keys(fileUploads);
78
35
  const attachments = message.attachments
79
- ?.filter(({ type }) => type !== 'file' && type !== 'image')
36
+ ?.filter(({ og_scrape_url }) => !og_scrape_url)
80
37
  .map((att) => ({
81
38
  ...att,
82
39
  localMetadata: { id: nanoid() },
@@ -84,10 +41,6 @@ const initState = (message) => {
84
41
  const mentioned_users = message.mentioned_users || [];
85
42
  return {
86
43
  attachments,
87
- fileOrder,
88
- fileUploads,
89
- imageOrder,
90
- imageUploads,
91
44
  linkPreviews,
92
45
  mentioned_users,
93
46
  setText: () => null,
@@ -126,38 +79,6 @@ const messageInputReducer = (state, action) => {
126
79
  attachments: state.attachments.filter((att) => !action.ids.includes(att.localMetadata?.id)),
127
80
  };
128
81
  }
129
- case 'setImageUpload': {
130
- const imageAlreadyExists = state.imageUploads[action.id];
131
- if (!imageAlreadyExists && !action.file)
132
- return state;
133
- const imageOrder = imageAlreadyExists ? state.imageOrder : state.imageOrder.concat(action.id);
134
- const newUploadFields = { ...action };
135
- delete newUploadFields.type;
136
- return {
137
- ...state,
138
- imageOrder,
139
- imageUploads: {
140
- ...state.imageUploads,
141
- [action.id]: { ...state.imageUploads[action.id], ...newUploadFields },
142
- },
143
- };
144
- }
145
- case 'setFileUpload': {
146
- const fileAlreadyExists = state.fileUploads[action.id];
147
- if (!fileAlreadyExists && !action.file)
148
- return state;
149
- const fileOrder = fileAlreadyExists ? state.fileOrder : state.fileOrder.concat(action.id);
150
- const newUploadFields = { ...action };
151
- delete newUploadFields.type;
152
- return {
153
- ...state,
154
- fileOrder,
155
- fileUploads: {
156
- ...state.fileUploads,
157
- [action.id]: { ...state.fileUploads[action.id], ...newUploadFields },
158
- },
159
- };
160
- }
161
82
  case 'setLinkPreviews': {
162
83
  const linkPreviews = new Map(state.linkPreviews);
163
84
  if (action.mode === SetLinkPreviewMode.REMOVE) {
@@ -187,28 +108,6 @@ const messageInputReducer = (state, action) => {
187
108
  linkPreviews,
188
109
  };
189
110
  }
190
- case 'removeImageUpload': {
191
- if (!state.imageUploads[action.id])
192
- return state; // cannot remove anything
193
- const newImageUploads = { ...state.imageUploads };
194
- delete newImageUploads[action.id];
195
- return {
196
- ...state,
197
- imageOrder: state.imageOrder.filter((_id) => _id !== action.id),
198
- imageUploads: newImageUploads,
199
- };
200
- }
201
- case 'removeFileUpload': {
202
- if (!state.fileUploads[action.id])
203
- return state; // cannot remove anything
204
- const newFileUploads = { ...state.fileUploads };
205
- delete newFileUploads[action.id];
206
- return {
207
- ...state,
208
- fileOrder: state.fileOrder.filter((_id) => _id !== action.id),
209
- fileUploads: newFileUploads,
210
- };
211
- }
212
111
  case 'addMentionedUser':
213
112
  return {
214
113
  ...state,
@@ -255,7 +154,7 @@ export const useMessageInputState = (props) => {
255
154
  setShowMentionsList(true);
256
155
  };
257
156
  const closeMentionsList = () => setShowMentionsList(false);
258
- const { maxFilesLeft, numberOfUploads, removeAttachments, removeFile, removeImage, uploadAttachment, uploadFile, uploadImage, uploadNewFiles, upsertAttachments, } = useAttachments(props, state, dispatch, textareaRef);
157
+ const { maxFilesLeft, numberOfUploads, removeAttachments, uploadAttachment, uploadNewFiles, upsertAttachments, } = useAttachments(props, state, dispatch, textareaRef);
259
158
  const { handleSubmit } = useSubmitHandler(props, state, dispatch, numberOfUploads, enrichURLsController);
260
159
  const recordingController = useMediaRecorder({
261
160
  asyncMessagesMultiSendEnabled,
@@ -289,15 +188,11 @@ export const useMessageInputState = (props) => {
289
188
  openMentionsList,
290
189
  recordingController,
291
190
  removeAttachments,
292
- removeFile,
293
- removeImage,
294
191
  setText,
295
192
  showCommandsList,
296
193
  showMentionsList,
297
194
  textareaRef,
298
195
  uploadAttachment,
299
- uploadFile,
300
- uploadImage,
301
196
  uploadNewFiles,
302
197
  upsertAttachments,
303
198
  };
@@ -36,8 +36,6 @@ export const usePasteHandler = (uploadNewFiles, insertText, isUploadEnabled, fin
36
36
  findAndEnqueueURLsToEnrich?.flush();
37
37
  }
38
38
  })(clipboardEvent);
39
- },
40
- // eslint-disable-next-line react-hooks/exhaustive-deps
41
- [insertText, uploadNewFiles]);
39
+ }, [findAndEnqueueURLsToEnrich, insertText, isUploadEnabled, uploadNewFiles]);
42
40
  return { onPaste };
43
41
  };
@@ -3,16 +3,9 @@ import { useChannelActionContext } from '../../../context/ChannelActionContext';
3
3
  import { useChannelStateContext } from '../../../context/ChannelStateContext';
4
4
  import { useTranslationContext } from '../../../context/TranslationContext';
5
5
  import { LinkPreviewState } from '../types';
6
- const getAttachmentTypeFromMime = (mime) => {
7
- if (mime.includes('video/'))
8
- return 'video';
9
- if (mime.includes('audio/'))
10
- return 'audio';
11
- return 'file';
12
- };
13
6
  export const useSubmitHandler = (props, state, dispatch, numberOfUploads, enrichURLsController) => {
14
7
  const { clearEditingState, message, overrideSubmitHandler, parent, publishTypingEvent } = props;
15
- const { attachments, fileOrder, fileUploads, imageOrder, imageUploads, linkPreviews, mentioned_users, text, } = state;
8
+ const { attachments, linkPreviews, mentioned_users, text } = state;
16
9
  const { cancelURLEnrichment, findAndEnqueueURLsToEnrich } = enrichURLsController;
17
10
  const { channel } = useChannelStateContext('useSubmitHandler');
18
11
  const { addNotification, editMessage, sendMessage } = useChannelActionContext('useSubmitHandler');
@@ -25,47 +18,6 @@ export const useSubmitHandler = (props, state, dispatch, numberOfUploads, enrich
25
18
  }
26
19
  textReference.current.hasChanged = text !== textReference.current.initialText;
27
20
  }, [text]);
28
- const getAttachmentsFromUploads = () => {
29
- const imageAttachments = imageOrder
30
- .map((id) => imageUploads[id])
31
- .filter((upload) => upload.state !== 'failed')
32
- .filter(({ id, url }, _, self) => self.every((upload) => upload.id === id || upload.url !== url))
33
- .filter((upload) => {
34
- // keep the OG attachments in case the text has not changed as the BE
35
- // won't re-enrich the message when only attachments have changed
36
- if (!textReference.current.hasChanged)
37
- return true;
38
- return !upload.og_scrape_url;
39
- })
40
- .map(({ file: { name }, url, ...rest }) => ({
41
- author_name: rest.author_name,
42
- fallback: name,
43
- image_url: url,
44
- og_scrape_url: rest.og_scrape_url,
45
- text: rest.text,
46
- title: rest.title,
47
- title_link: rest.title_link,
48
- type: 'image',
49
- }));
50
- const fileAttachments = fileOrder
51
- .map((id) => fileUploads[id])
52
- .filter((upload) => upload.state !== 'failed')
53
- .map((upload) => ({
54
- asset_url: upload.url,
55
- file_size: upload.file.size,
56
- mime_type: upload.file.type,
57
- thumb_url: upload.thumb_url,
58
- title: upload.file.name,
59
- type: getAttachmentTypeFromMime(upload.file.type || ''),
60
- }));
61
- const otherAttachments = attachments
62
- .filter((att) => att.localMetadata?.uploadState !== 'failed')
63
- .map((localAttachment) => {
64
- const { localMetadata: _, ...attachment } = localAttachment;
65
- return attachment;
66
- });
67
- return [...otherAttachments, ...imageAttachments, ...fileAttachments];
68
- };
69
21
  const handleSubmit = async (event, customMessageData, options) => {
70
22
  event?.preventDefault();
71
23
  const trimmedMessage = text.trim();
@@ -79,31 +31,35 @@ export const useSubmitHandler = (props, state, dispatch, numberOfUploads, enrich
79
31
  trimmedMessage === '****';
80
32
  if (isEmptyMessage && numberOfUploads === 0 && attachments.length === 0)
81
33
  return;
82
- // the channel component handles the actual sending of the message
83
- const someAttachmentsUploading = Object.values(imageUploads).some((upload) => upload.state === 'uploading') ||
84
- Object.values(fileUploads).some((upload) => upload.state === 'uploading') ||
85
- attachments.some((att) => att.localMetadata?.uploadState === 'uploading');
34
+ const someAttachmentsUploading = attachments.some((att) => att.localMetadata?.uploadState === 'uploading');
86
35
  if (someAttachmentsUploading) {
87
36
  return addNotification(t('Wait until all attachments have uploaded'), 'error');
88
37
  }
89
- let attachmentsFromUploads = getAttachmentsFromUploads();
38
+ const attachmentsFromUploads = attachments
39
+ .filter((att) => att.localMetadata?.uploadState !== 'failed' ||
40
+ (findAndEnqueueURLsToEnrich && !att.og_scrape_url))
41
+ .map((localAttachment) => {
42
+ const { localMetadata: _, ...attachment } = localAttachment;
43
+ return attachment;
44
+ });
45
+ const sendOptions = { ...options };
90
46
  let attachmentsFromLinkPreviews = [];
91
- let someLinkPreviewsLoading;
92
- let someLinkPreviewsDismissed;
93
47
  if (findAndEnqueueURLsToEnrich) {
94
- // filter out all the attachments scraped before the message was edited - only if the scr
95
- attachmentsFromUploads = attachmentsFromUploads.filter((attachment) => !attachment.og_scrape_url);
96
48
  // prevent showing link preview in MessageInput after the message has been sent
97
49
  cancelURLEnrichment();
98
- someLinkPreviewsLoading = Array.from(linkPreviews.values()).some((linkPreview) => [LinkPreviewState.QUEUED, LinkPreviewState.LOADING].includes(linkPreview.state));
99
- someLinkPreviewsDismissed = Array.from(linkPreviews.values()).some((linkPreview) => linkPreview.state === LinkPreviewState.DISMISSED);
100
- if (!someLinkPreviewsLoading) {
101
- attachmentsFromLinkPreviews = Array.from(linkPreviews.values())
50
+ const someLinkPreviewsLoading = Array.from(linkPreviews.values()).some((linkPreview) => [LinkPreviewState.QUEUED, LinkPreviewState.LOADING].includes(linkPreview.state));
51
+ const someLinkPreviewsDismissed = Array.from(linkPreviews.values()).some((linkPreview) => linkPreview.state === LinkPreviewState.DISMISSED);
52
+ attachmentsFromLinkPreviews = someLinkPreviewsLoading
53
+ ? []
54
+ : Array.from(linkPreviews.values())
102
55
  .filter((linkPreview) => linkPreview.state === LinkPreviewState.LOADED &&
103
56
  !attachmentsFromUploads.find((attFromUpload) => attFromUpload.og_scrape_url === linkPreview.og_scrape_url))
104
57
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
105
58
  .map(({ state: linkPreviewState, ...ogAttachment }) => ogAttachment);
106
- }
59
+ // scraped attachments are added only if all enrich queries has completed. Otherwise, the scraping has to be done server-side.
60
+ sendOptions.skip_enrich_url =
61
+ (!someLinkPreviewsLoading && attachmentsFromLinkPreviews.length > 0) ||
62
+ someLinkPreviewsDismissed;
107
63
  }
108
64
  const newAttachments = [...attachmentsFromUploads, ...attachmentsFromLinkPreviews];
109
65
  // Instead of checking if a user is still mentioned every time the text changes,
@@ -115,14 +71,6 @@ export const useSubmitHandler = (props, state, dispatch, numberOfUploads, enrich
115
71
  mentioned_users: actualMentionedUsers,
116
72
  text,
117
73
  };
118
- // scraped attachments are added only if all enrich queries has completed. Otherwise, the scraping has to be done server-side.
119
- const linkPreviewsEnabled = !!findAndEnqueueURLsToEnrich;
120
- const skip_enrich_url = linkPreviewsEnabled &&
121
- ((!someLinkPreviewsLoading && attachmentsFromLinkPreviews.length > 0) ||
122
- someLinkPreviewsDismissed);
123
- const sendOptions = linkPreviewsEnabled || options
124
- ? Object.assign(linkPreviewsEnabled ? { skip_enrich_url } : {}, options ?? {})
125
- : undefined;
126
74
  if (message && message.type !== 'error') {
127
75
  delete message.i18n;
128
76
  try {