stream-chat-react-native-core 9.0.2-beta.2 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/lib/commonjs/components/Attachment/Attachment.js +38 -10
  2. package/lib/commonjs/components/Attachment/Attachment.js.map +1 -1
  3. package/lib/commonjs/components/Attachment/AttachmentFileUploadProgressIndicator.js +97 -0
  4. package/lib/commonjs/components/Attachment/AttachmentFileUploadProgressIndicator.js.map +1 -0
  5. package/lib/commonjs/components/Attachment/AttachmentUploadIndicator.js +97 -0
  6. package/lib/commonjs/components/Attachment/AttachmentUploadIndicator.js.map +1 -0
  7. package/lib/commonjs/components/Attachment/CircularProgressIndicator.js +141 -0
  8. package/lib/commonjs/components/Attachment/CircularProgressIndicator.js.map +1 -0
  9. package/lib/commonjs/components/Attachment/FileAttachment.js +18 -4
  10. package/lib/commonjs/components/Attachment/FileAttachment.js.map +1 -1
  11. package/lib/commonjs/components/Attachment/Gallery.js +7 -1
  12. package/lib/commonjs/components/Attachment/Gallery.js.map +1 -1
  13. package/lib/commonjs/components/Attachment/MediaUploadProgressOverlay.js +59 -0
  14. package/lib/commonjs/components/Attachment/MediaUploadProgressOverlay.js.map +1 -0
  15. package/lib/commonjs/components/Attachment/VideoThumbnail.js +18 -8
  16. package/lib/commonjs/components/Attachment/VideoThumbnail.js.map +1 -1
  17. package/lib/commonjs/components/Attachment/utils/buildGallery/buildThumbnail.js +2 -0
  18. package/lib/commonjs/components/Attachment/utils/buildGallery/buildThumbnail.js.map +1 -1
  19. package/lib/commonjs/components/Channel/Channel.js +94 -92
  20. package/lib/commonjs/components/Channel/Channel.js.map +1 -1
  21. package/lib/commonjs/components/Chat/Chat.js +10 -1
  22. package/lib/commonjs/components/Chat/Chat.js.map +1 -1
  23. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.js +42 -54
  24. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.js.map +1 -1
  25. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.js +10 -6
  26. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.js.map +1 -1
  27. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.js +12 -6
  28. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.js.map +1 -1
  29. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.js +13 -7
  30. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.js.map +1 -1
  31. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview.js +6 -2
  32. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview.js.map +1 -1
  33. package/lib/commonjs/components/index.js +33 -0
  34. package/lib/commonjs/components/index.js.map +1 -1
  35. package/lib/commonjs/contexts/componentsContext/defaultComponents.js +6 -0
  36. package/lib/commonjs/contexts/componentsContext/defaultComponents.js.map +1 -1
  37. package/lib/commonjs/contexts/messageInputContext/hooks/useCreateMessageInputContext.js +3 -1
  38. package/lib/commonjs/contexts/messageInputContext/hooks/useCreateMessageInputContext.js.map +1 -1
  39. package/lib/commonjs/contexts/messageInputContext/hooks/useMessageComposerHasSendableData.js +4 -1
  40. package/lib/commonjs/contexts/messageInputContext/hooks/useMessageComposerHasSendableData.js.map +1 -1
  41. package/lib/commonjs/contexts/themeContext/utils/theme.js +5 -0
  42. package/lib/commonjs/contexts/themeContext/utils/theme.js.map +1 -1
  43. package/lib/commonjs/hooks/index.js +11 -0
  44. package/lib/commonjs/hooks/index.js.map +1 -1
  45. package/lib/commonjs/hooks/usePendingAttachmentUpload.js +106 -0
  46. package/lib/commonjs/hooks/usePendingAttachmentUpload.js.map +1 -0
  47. package/lib/commonjs/index.js +12 -0
  48. package/lib/commonjs/index.js.map +1 -1
  49. package/lib/commonjs/middlewares/attachments.js +2 -0
  50. package/lib/commonjs/middlewares/attachments.js.map +1 -1
  51. package/lib/commonjs/native.js +8 -1
  52. package/lib/commonjs/native.js.map +1 -1
  53. package/lib/commonjs/nativeMultipartUpload.js +237 -0
  54. package/lib/commonjs/nativeMultipartUpload.js.map +1 -0
  55. package/lib/commonjs/types/types.js.map +1 -1
  56. package/lib/commonjs/utils/installNativeMultipartAdapter.js +223 -0
  57. package/lib/commonjs/utils/installNativeMultipartAdapter.js.map +1 -0
  58. package/lib/commonjs/utils/utils.js +2 -2
  59. package/lib/commonjs/utils/utils.js.map +1 -1
  60. package/lib/commonjs/version.json +1 -1
  61. package/lib/module/components/Attachment/Attachment.js +38 -10
  62. package/lib/module/components/Attachment/Attachment.js.map +1 -1
  63. package/lib/module/components/Attachment/AttachmentFileUploadProgressIndicator.js +97 -0
  64. package/lib/module/components/Attachment/AttachmentFileUploadProgressIndicator.js.map +1 -0
  65. package/lib/module/components/Attachment/AttachmentUploadIndicator.js +97 -0
  66. package/lib/module/components/Attachment/AttachmentUploadIndicator.js.map +1 -0
  67. package/lib/module/components/Attachment/CircularProgressIndicator.js +141 -0
  68. package/lib/module/components/Attachment/CircularProgressIndicator.js.map +1 -0
  69. package/lib/module/components/Attachment/FileAttachment.js +18 -4
  70. package/lib/module/components/Attachment/FileAttachment.js.map +1 -1
  71. package/lib/module/components/Attachment/Gallery.js +7 -1
  72. package/lib/module/components/Attachment/Gallery.js.map +1 -1
  73. package/lib/module/components/Attachment/MediaUploadProgressOverlay.js +59 -0
  74. package/lib/module/components/Attachment/MediaUploadProgressOverlay.js.map +1 -0
  75. package/lib/module/components/Attachment/VideoThumbnail.js +18 -8
  76. package/lib/module/components/Attachment/VideoThumbnail.js.map +1 -1
  77. package/lib/module/components/Attachment/utils/buildGallery/buildThumbnail.js +2 -0
  78. package/lib/module/components/Attachment/utils/buildGallery/buildThumbnail.js.map +1 -1
  79. package/lib/module/components/Channel/Channel.js +94 -92
  80. package/lib/module/components/Channel/Channel.js.map +1 -1
  81. package/lib/module/components/Chat/Chat.js +10 -1
  82. package/lib/module/components/Chat/Chat.js.map +1 -1
  83. package/lib/module/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.js +42 -54
  84. package/lib/module/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.js.map +1 -1
  85. package/lib/module/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.js +10 -6
  86. package/lib/module/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.js.map +1 -1
  87. package/lib/module/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.js +12 -6
  88. package/lib/module/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.js.map +1 -1
  89. package/lib/module/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.js +13 -7
  90. package/lib/module/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.js.map +1 -1
  91. package/lib/module/components/MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview.js +6 -2
  92. package/lib/module/components/MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview.js.map +1 -1
  93. package/lib/module/components/index.js +33 -0
  94. package/lib/module/components/index.js.map +1 -1
  95. package/lib/module/contexts/componentsContext/defaultComponents.js +6 -0
  96. package/lib/module/contexts/componentsContext/defaultComponents.js.map +1 -1
  97. package/lib/module/contexts/messageInputContext/hooks/useCreateMessageInputContext.js +3 -1
  98. package/lib/module/contexts/messageInputContext/hooks/useCreateMessageInputContext.js.map +1 -1
  99. package/lib/module/contexts/messageInputContext/hooks/useMessageComposerHasSendableData.js +4 -1
  100. package/lib/module/contexts/messageInputContext/hooks/useMessageComposerHasSendableData.js.map +1 -1
  101. package/lib/module/contexts/themeContext/utils/theme.js +5 -0
  102. package/lib/module/contexts/themeContext/utils/theme.js.map +1 -1
  103. package/lib/module/hooks/index.js +11 -0
  104. package/lib/module/hooks/index.js.map +1 -1
  105. package/lib/module/hooks/usePendingAttachmentUpload.js +106 -0
  106. package/lib/module/hooks/usePendingAttachmentUpload.js.map +1 -0
  107. package/lib/module/index.js +12 -0
  108. package/lib/module/index.js.map +1 -1
  109. package/lib/module/middlewares/attachments.js +2 -0
  110. package/lib/module/middlewares/attachments.js.map +1 -1
  111. package/lib/module/native.js +8 -1
  112. package/lib/module/native.js.map +1 -1
  113. package/lib/module/nativeMultipartUpload.js +237 -0
  114. package/lib/module/nativeMultipartUpload.js.map +1 -0
  115. package/lib/module/types/types.js.map +1 -1
  116. package/lib/module/utils/installNativeMultipartAdapter.js +223 -0
  117. package/lib/module/utils/installNativeMultipartAdapter.js.map +1 -0
  118. package/lib/module/utils/utils.js +2 -2
  119. package/lib/module/utils/utils.js.map +1 -1
  120. package/lib/module/version.json +1 -1
  121. package/lib/typescript/components/Attachment/Attachment.d.ts.map +1 -1
  122. package/lib/typescript/components/Attachment/AttachmentFileUploadProgressIndicator.d.ts +14 -0
  123. package/lib/typescript/components/Attachment/AttachmentFileUploadProgressIndicator.d.ts.map +1 -0
  124. package/lib/typescript/components/Attachment/AttachmentUploadIndicator.d.ts +18 -0
  125. package/lib/typescript/components/Attachment/AttachmentUploadIndicator.d.ts.map +1 -0
  126. package/lib/typescript/components/Attachment/CircularProgressIndicator.d.ts +18 -0
  127. package/lib/typescript/components/Attachment/CircularProgressIndicator.d.ts.map +1 -0
  128. package/lib/typescript/components/Attachment/FileAttachment.d.ts.map +1 -1
  129. package/lib/typescript/components/Attachment/Gallery.d.ts.map +1 -1
  130. package/lib/typescript/components/Attachment/MediaUploadProgressOverlay.d.ts +12 -0
  131. package/lib/typescript/components/Attachment/MediaUploadProgressOverlay.d.ts.map +1 -0
  132. package/lib/typescript/components/Attachment/VideoThumbnail.d.ts +4 -0
  133. package/lib/typescript/components/Attachment/VideoThumbnail.d.ts.map +1 -1
  134. package/lib/typescript/components/Attachment/utils/buildGallery/buildThumbnail.d.ts.map +1 -1
  135. package/lib/typescript/components/Attachment/utils/buildGallery/types.d.ts +2 -0
  136. package/lib/typescript/components/Attachment/utils/buildGallery/types.d.ts.map +1 -1
  137. package/lib/typescript/components/Channel/Channel.d.ts.map +1 -1
  138. package/lib/typescript/components/Chat/Chat.d.ts +10 -0
  139. package/lib/typescript/components/Chat/Chat.d.ts.map +1 -1
  140. package/lib/typescript/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.d.ts +7 -2
  141. package/lib/typescript/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.d.ts.map +1 -1
  142. package/lib/typescript/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.d.ts.map +1 -1
  143. package/lib/typescript/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.d.ts.map +1 -1
  144. package/lib/typescript/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.d.ts.map +1 -1
  145. package/lib/typescript/components/MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview.d.ts.map +1 -1
  146. package/lib/typescript/components/index.d.ts +3 -0
  147. package/lib/typescript/components/index.d.ts.map +1 -1
  148. package/lib/typescript/contexts/componentsContext/defaultComponents.d.ts +5 -2
  149. package/lib/typescript/contexts/componentsContext/defaultComponents.d.ts.map +1 -1
  150. package/lib/typescript/contexts/messageInputContext/hooks/useCreateMessageInputContext.d.ts +1 -1
  151. package/lib/typescript/contexts/messageInputContext/hooks/useCreateMessageInputContext.d.ts.map +1 -1
  152. package/lib/typescript/contexts/messageInputContext/hooks/useMessageComposerHasSendableData.d.ts.map +1 -1
  153. package/lib/typescript/contexts/themeContext/ThemeContext.d.ts +5 -0
  154. package/lib/typescript/contexts/themeContext/ThemeContext.d.ts.map +1 -1
  155. package/lib/typescript/contexts/themeContext/utils/theme.d.ts +5 -0
  156. package/lib/typescript/contexts/themeContext/utils/theme.d.ts.map +1 -1
  157. package/lib/typescript/hooks/index.d.ts +1 -0
  158. package/lib/typescript/hooks/index.d.ts.map +1 -1
  159. package/lib/typescript/hooks/usePendingAttachmentUpload.d.ts +14 -0
  160. package/lib/typescript/hooks/usePendingAttachmentUpload.d.ts.map +1 -0
  161. package/lib/typescript/index.d.ts +1 -0
  162. package/lib/typescript/index.d.ts.map +1 -1
  163. package/lib/typescript/middlewares/attachments.d.ts.map +1 -1
  164. package/lib/typescript/native.d.ts +5 -2
  165. package/lib/typescript/native.d.ts.map +1 -1
  166. package/lib/typescript/nativeMultipartUpload.d.ts +98 -0
  167. package/lib/typescript/nativeMultipartUpload.d.ts.map +1 -0
  168. package/lib/typescript/types/types.d.ts +2 -0
  169. package/lib/typescript/types/types.d.ts.map +1 -1
  170. package/lib/typescript/utils/installNativeMultipartAdapter.d.ts +8 -0
  171. package/lib/typescript/utils/installNativeMultipartAdapter.d.ts.map +1 -0
  172. package/lib/typescript/utils/utils.d.ts +1 -1
  173. package/lib/typescript/utils/utils.d.ts.map +1 -1
  174. package/package.json +2 -2
  175. package/src/__tests__/nativeMultipartUpload.test.ts +267 -0
  176. package/src/components/Attachment/Attachment.tsx +54 -5
  177. package/src/components/Attachment/AttachmentFileUploadProgressIndicator.tsx +108 -0
  178. package/src/components/Attachment/AttachmentUploadIndicator.tsx +112 -0
  179. package/src/components/Attachment/CircularProgressIndicator.tsx +161 -0
  180. package/src/components/Attachment/FileAttachment.tsx +22 -6
  181. package/src/components/Attachment/Gallery.tsx +9 -1
  182. package/src/components/Attachment/MediaUploadProgressOverlay.tsx +77 -0
  183. package/src/components/Attachment/VideoThumbnail.tsx +17 -9
  184. package/src/components/Attachment/__tests__/Attachment.test.tsx +63 -16
  185. package/src/components/Attachment/utils/buildGallery/buildThumbnail.ts +3 -0
  186. package/src/components/Attachment/utils/buildGallery/types.ts +2 -0
  187. package/src/components/Channel/Channel.tsx +65 -61
  188. package/src/components/Chat/Chat.tsx +20 -0
  189. package/src/components/MessageInput/__tests__/AttachmentUploadPreviewList.test.tsx +51 -6
  190. package/src/components/MessageInput/__tests__/AudioAttachmentUploadPreview.test.tsx +31 -1
  191. package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator.tsx +30 -44
  192. package/src/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.tsx +11 -5
  193. package/src/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.tsx +20 -5
  194. package/src/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.tsx +20 -9
  195. package/src/components/MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview.tsx +7 -1
  196. package/src/components/index.ts +3 -0
  197. package/src/contexts/componentsContext/defaultComponents.ts +6 -0
  198. package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts +3 -1
  199. package/src/contexts/messageInputContext/hooks/useMessageComposerHasSendableData.ts +5 -1
  200. package/src/contexts/themeContext/utils/theme.ts +10 -0
  201. package/src/hooks/__tests__/usePendingAttachmentUpload.test.tsx +106 -0
  202. package/src/hooks/index.ts +1 -0
  203. package/src/hooks/usePendingAttachmentUpload.ts +131 -0
  204. package/src/index.ts +1 -0
  205. package/src/middlewares/attachments.ts +2 -0
  206. package/src/native.ts +28 -0
  207. package/src/nativeMultipartUpload.ts +384 -0
  208. package/src/types/types.ts +2 -0
  209. package/src/utils/__tests__/installNativeMultipartAdapter.test.ts +437 -0
  210. package/src/utils/installNativeMultipartAdapter.ts +302 -0
  211. package/src/utils/utils.ts +3 -3
  212. package/src/version.json +1 -1
@@ -1,8 +1,12 @@
1
1
  import React, { ComponentProps } from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+
4
+ import type { ReactTestInstance } from 'react-test-renderer';
2
5
 
3
6
  import { render, waitFor } from '@testing-library/react-native';
4
7
  import { v4 as uuidv4 } from 'uuid';
5
8
 
9
+ import { AudioPlayerProvider } from '../../../contexts/audioPlayerContext/AudioPlayerContext';
6
10
  import type { MessageContextValue } from '../../../contexts/messageContext/MessageContext';
7
11
  import { MessageProvider } from '../../../contexts/messageContext/MessageContext';
8
12
  import type { MessagesContextValue } from '../../../contexts/messagesContext/MessagesContext';
@@ -21,32 +25,59 @@ import { ImageLoadingIndicator } from '../../Attachment/ImageLoadingIndicator';
21
25
  import { Attachment } from '../Attachment';
22
26
  import { FilePreview as FilePreviewDefault } from '../FilePreview';
23
27
 
24
- jest.mock('../../../native.ts', () => ({
25
- isVideoPlayerAvailable: jest.fn(() => false),
26
- isSoundPackageAvailable: jest.fn(() => false),
28
+ jest.mock('../../../native.ts', () => {
29
+ const { View } = require('react-native');
30
+
31
+ return {
32
+ NativeHandlers: {
33
+ SDK: 'stream-chat-react-native',
34
+ Sound: {
35
+ initializeSound: jest.fn(() => null),
36
+ Player: View,
37
+ },
38
+ },
39
+ isVideoPlayerAvailable: jest.fn(() => false),
40
+ isSoundPackageAvailable: jest.fn(() => false),
41
+ };
42
+ });
43
+
44
+ jest.mock('../../../hooks/usePendingAttachmentUpload', () => ({
45
+ usePendingAttachmentUpload: jest.fn(() => ({
46
+ isUploading: false,
47
+ uploadProgress: undefined,
48
+ })),
27
49
  }));
28
50
 
29
51
  const getAttachmentComponent = (props: ComponentProps<typeof Attachment>) => {
30
52
  const message = generateMessage();
31
53
  return (
32
54
  <ThemeProvider>
33
- <MessagesProvider
34
- value={
35
- {
36
- ImageLoadingFailedIndicator,
37
- ImageLoadingIndicator,
38
- FilePreview: FilePreviewDefault,
39
- } as unknown as MessagesContextValue
40
- }
41
- >
42
- <MessageProvider value={{ message } as unknown as MessageContextValue}>
43
- <Attachment {...props} />
44
- </MessageProvider>
45
- </MessagesProvider>
55
+ <AudioPlayerProvider value={{ allowConcurrentAudioPlayback: false }}>
56
+ <MessagesProvider
57
+ value={
58
+ {
59
+ FilePreview: FilePreviewDefault,
60
+ ImageLoadingFailedIndicator,
61
+ ImageLoadingIndicator,
62
+ message,
63
+ } as unknown as MessagesContextValue
64
+ }
65
+ >
66
+ <MessageProvider value={{ message } as unknown as MessageContextValue}>
67
+ <Attachment {...props} />
68
+ </MessageProvider>
69
+ </MessagesProvider>
70
+ </AudioPlayerProvider>
46
71
  </ThemeProvider>
47
72
  );
48
73
  };
49
74
 
75
+ const getWaveformBarCount = (root: ReactTestInstance) =>
76
+ root.findAllByType(View).filter((node: ReactTestInstance) => {
77
+ const flattenedStyle = StyleSheet.flatten(node.props.style);
78
+ return flattenedStyle?.width === 2 && typeof flattenedStyle?.height === 'number';
79
+ }).length;
80
+
50
81
  describe('Attachment', () => {
51
82
  it('should render File component for "audio" type attachment', async () => {
52
83
  const attachment = generateAudioAttachment();
@@ -75,6 +106,22 @@ describe('Attachment', () => {
75
106
  });
76
107
  });
77
108
 
109
+ it('should render waveform for playable audio attachments without an active upload', async () => {
110
+ const { isSoundPackageAvailable } = require('../../../native');
111
+ isSoundPackageAvailable.mockReturnValue(true);
112
+ const attachment = generateAudioAttachment({
113
+ duration: 10,
114
+ waveform_data: [0.2, 0.6, 0.4],
115
+ });
116
+ const { getByLabelText, root } = render(getAttachmentComponent({ attachment }));
117
+
118
+ await waitFor(() => {
119
+ expect(getByLabelText('audio-attachment-preview')).toBeTruthy();
120
+ expect(getWaveformBarCount(root)).toBeGreaterThan(0);
121
+ });
122
+ isSoundPackageAvailable.mockReturnValue(false);
123
+ });
124
+
78
125
  it('should render UrlPreview component if attachment has title_link or og_scrape_url', async () => {
79
126
  const attachment = generateImageAttachment({
80
127
  og_scrape_url: uuidv4(),
@@ -5,6 +5,7 @@ import type { Attachment } from 'stream-chat';
5
5
  import type { Thumbnail } from './types';
6
6
 
7
7
  import { ChatConfigContextValue } from '../../../../contexts/chatConfigContext/ChatConfigContext';
8
+ import type { DefaultAttachmentData } from '../../../../types/types';
8
9
 
9
10
  import { getResizedImageUrl } from '../../../../utils/getResizedImageUrl';
10
11
  import { getUrlOfImageAttachment } from '../../../../utils/getUrlOfImageAttachment';
@@ -33,9 +34,11 @@ export function buildThumbnail({
33
34
  ? originalImageHeight + originalImageWidth > height + width
34
35
  : true;
35
36
  const imageUrl = getUrlOfImageAttachment(image) as string;
37
+ const localId = (image as Attachment & DefaultAttachmentData).localId;
36
38
 
37
39
  return {
38
40
  flex,
41
+ localId,
39
42
  resizeMode: resizeMode
40
43
  ? resizeMode
41
44
  : ((image.original_height && image.original_width ? 'contain' : 'cover') as ImageResizeMode),
@@ -4,6 +4,8 @@ export type Thumbnail = {
4
4
  resizeMode: ImageResizeMode;
5
5
  url: string;
6
6
  id?: string;
7
+ /** Same as attachment `localId` for correlating with `client.uploadManager` */
8
+ localId?: string;
7
9
  thumb_url?: string;
8
10
  type?: string;
9
11
  flex?: number;
@@ -4,7 +4,6 @@ import { StyleSheet, Text, View } from 'react-native';
4
4
  import debounce from 'lodash/debounce';
5
5
  import throttle from 'lodash/throttle';
6
6
 
7
- import { lookup } from 'mime-types';
8
7
  import {
9
8
  Channel as ChannelClass,
10
9
  ChannelState,
@@ -101,7 +100,7 @@ import {
101
100
  } from '../../state-store/channel-unread-state';
102
101
  import { MessageInputHeightStore } from '../../state-store/message-input-height-store';
103
102
  import { primitives } from '../../theme';
104
- import { FileTypes } from '../../types/types';
103
+ import { DefaultAttachmentData, FileTypes } from '../../types/types';
105
104
  import { addReactionToLocalState } from '../../utils/addReactionToLocalState';
106
105
  import { compressedImageURI } from '../../utils/compressImage';
107
106
  import { patchMessageTextCommand } from '../../utils/patchMessageTextCommand';
@@ -1053,73 +1052,78 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
1053
1052
 
1054
1053
  const uploadPendingAttachments = useStableCallback(async (message: LocalMessage) => {
1055
1054
  const updatedMessage = { ...message };
1056
- if (updatedMessage.attachments?.length) {
1057
- for (let i = 0; i < updatedMessage.attachments?.length; i++) {
1058
- const attachment = updatedMessage.attachments[i];
1059
-
1060
- // If the attachment is already uploaded, skip it.
1061
- if (
1062
- (attachment.image_url && !isLocalUrl(attachment.image_url)) ||
1063
- (attachment.asset_url && !isLocalUrl(attachment.asset_url))
1064
- ) {
1065
- continue;
1066
- }
1055
+ if (!updatedMessage.attachments?.length || !channel?.cid) {
1056
+ return updatedMessage;
1057
+ }
1067
1058
 
1068
- const image = attachment.originalFile;
1069
- const file = attachment.originalFile;
1070
- if (attachment.type === FileTypes.Image && image?.uri) {
1071
- const filename = image.name ?? getFileNameFromPath(image.uri);
1072
- // if any upload is in progress, cancel it
1073
- const controller = uploadAbortControllerRef.current.get(filename);
1074
- if (controller) {
1075
- controller.abort();
1076
- uploadAbortControllerRef.current.delete(filename);
1077
- }
1078
- const compressedUri = await compressedImageURI(image, compressImageQuality);
1079
- const contentType = lookup(filename) || 'multipart/form-data';
1059
+ const uploadOne = async (attachment: NonNullable<LocalMessage['attachments']>[number]) => {
1060
+ if (
1061
+ (attachment.image_url && !isLocalUrl(attachment.image_url)) ||
1062
+ (attachment.asset_url && !isLocalUrl(attachment.asset_url))
1063
+ ) {
1064
+ return;
1065
+ }
1080
1066
 
1081
- const uploadResponse = doFileUploadRequest
1082
- ? await doFileUploadRequest(image)
1083
- : await channel.sendImage(compressedUri, filename, contentType);
1067
+ const originalFile = attachment.originalFile;
1068
+ if (!originalFile?.uri) {
1069
+ return;
1070
+ }
1084
1071
 
1085
- attachment.image_url = uploadResponse.file;
1086
- delete attachment.originalFile;
1072
+ const localId = (attachment as DefaultAttachmentData).localId;
1073
+ if (!localId) {
1074
+ console.warn('uploadPendingAttachments: local attachment missing localId, skipping upload');
1075
+ return;
1076
+ }
1087
1077
 
1088
- client.offlineDb?.executeQuerySafely(
1089
- (db) =>
1090
- db.updateMessage({
1091
- message: { ...updatedMessage, cid: channel.cid },
1092
- }),
1093
- { method: 'updateMessage' },
1094
- );
1095
- }
1078
+ let fileForUpload = originalFile;
1079
+ if (attachment.type === FileTypes.Image && !doFileUploadRequest) {
1080
+ const filename = originalFile.name ?? getFileNameFromPath(originalFile.uri);
1081
+ const compressedUri = await compressedImageURI(originalFile, compressImageQuality);
1082
+ fileForUpload = { ...originalFile, name: filename, uri: compressedUri };
1083
+ }
1096
1084
 
1097
- if (attachment.type !== FileTypes.Image && file?.uri) {
1098
- // if any upload is in progress, cancel it
1099
- const controller = uploadAbortControllerRef.current.get(file.name);
1100
- if (controller) {
1101
- controller.abort();
1102
- uploadAbortControllerRef.current.delete(file.name);
1103
- }
1104
- const response = doFileUploadRequest
1105
- ? await doFileUploadRequest(file)
1106
- : await channel.sendFile(file.uri, file.name, file.type);
1107
- attachment.asset_url = response.file;
1108
- if (response.thumb_url) {
1109
- attachment.thumb_url = response.thumb_url;
1110
- }
1085
+ const response = await (
1086
+ client as typeof client & {
1087
+ uploadManager: {
1088
+ upload(args: {
1089
+ channelCid: string;
1090
+ file: {
1091
+ name?: string;
1092
+ type?: string;
1093
+ uri: string;
1094
+ };
1095
+ id: string;
1096
+ }): Promise<{ file: string; thumb_url?: string }>;
1097
+ };
1098
+ }
1099
+ ).uploadManager.upload({
1100
+ channelCid: channel.cid,
1101
+ file: fileForUpload,
1102
+ id: localId,
1103
+ });
1111
1104
 
1112
- delete attachment.originalFile;
1113
- client.offlineDb?.executeQuerySafely(
1114
- (db) =>
1115
- db.updateMessage({
1116
- message: { ...updatedMessage, cid: channel.cid },
1117
- }),
1118
- { method: 'updateMessage' },
1119
- );
1105
+ if (attachment.type === FileTypes.Image) {
1106
+ attachment.image_url = response.file;
1107
+ } else {
1108
+ attachment.asset_url = response.file;
1109
+ if (response.thumb_url) {
1110
+ attachment.thumb_url = response.thumb_url;
1120
1111
  }
1121
1112
  }
1122
- }
1113
+
1114
+ delete attachment.originalFile;
1115
+ delete (attachment as DefaultAttachmentData).localId;
1116
+
1117
+ client.offlineDb?.executeQuerySafely(
1118
+ (db) =>
1119
+ db.updateMessage({
1120
+ message: { ...updatedMessage, cid: channel.cid },
1121
+ }),
1122
+ { method: 'updateMessage' },
1123
+ );
1124
+ };
1125
+
1126
+ await Promise.all(updatedMessage.attachments.map((att) => uploadOne(att)));
1123
1127
 
1124
1128
  return updatedMessage;
1125
1129
  });
@@ -26,6 +26,7 @@ import { NativeHandlers } from '../../native';
26
26
  import { OfflineDB } from '../../store/OfflineDB';
27
27
 
28
28
  import type { Streami18n } from '../../utils/i18n/Streami18n';
29
+ import { installNativeMultipartAdapter } from '../../utils/installNativeMultipartAdapter';
29
30
  import { version } from '../../version.json';
30
31
 
31
32
  init();
@@ -43,6 +44,16 @@ export type ChatProps = Pick<ChatContextValue, 'client'> &
43
44
  * Enables offline storage and loading for chat data.
44
45
  */
45
46
  enableOfflineSupport?: boolean;
47
+ /**
48
+ * When true, multipart uploads use the SDK's native upload adapter when available.
49
+ * When false, uploads stay on the default axios adapter.
50
+ *
51
+ * This only controls whether the native adapter gets installed by this Chat instance.
52
+ * It does not uninstall an adapter that was already installed on the client.
53
+ *
54
+ * @default true
55
+ */
56
+ useNativeMultipartUpload?: boolean;
46
57
  /**
47
58
  * Instance of Streami18n class should be provided to Chat component to enable internationalization.
48
59
  *
@@ -141,6 +152,7 @@ const ChatWithContext = (props: PropsWithChildren<ChatProps>) => {
141
152
  i18nInstance,
142
153
  isMessageAIGenerated,
143
154
  style,
155
+ useNativeMultipartUpload = false,
144
156
  } = props;
145
157
  const { ChatLoadingIndicator } = useComponentsContext();
146
158
 
@@ -241,6 +253,14 @@ const ChatWithContext = (props: PropsWithChildren<ChatProps>) => {
241
253
  };
242
254
  }, [client]);
243
255
 
256
+ useEffect(() => {
257
+ if (!useNativeMultipartUpload) {
258
+ return;
259
+ }
260
+
261
+ installNativeMultipartAdapter(client);
262
+ }, [client, useNativeMultipartUpload]);
263
+
244
264
  const initialisedDatabase = !!offlineDbInitialized && userID === offlineDbUserId;
245
265
 
246
266
  const appSettings = useAppSettings(client, isOnline, enableOfflineSupport, initialisedDatabase);
@@ -1,5 +1,9 @@
1
1
  import React, { ComponentProps } from 'react';
2
2
 
3
+ import { ActivityIndicator } from 'react-native';
4
+
5
+ import type { ReactTestInstance } from 'react-test-renderer';
6
+
3
7
  import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
4
8
 
5
9
  import type { Attachment, Channel as ChannelType, LocalAttachment, StreamChat } from 'stream-chat';
@@ -35,6 +39,7 @@ jest.mock('../../../native.ts', () => {
35
39
  isDocumentPickerAvailable: jest.fn(() => true),
36
40
  isImageMediaLibraryAvailable: jest.fn(() => true),
37
41
  isImagePickerAvailable: jest.fn(() => true),
42
+ isNativeMultipartUploadAvailable: jest.fn(() => false),
38
43
  isSoundPackageAvailable: jest.fn(() => false),
39
44
  NativeHandlers: {
40
45
  Sound: {
@@ -64,6 +69,28 @@ const renderComponent = ({
64
69
  );
65
70
  };
66
71
 
72
+ type PendingUploadRecord = {
73
+ id: string;
74
+ uploadProgress?: number;
75
+ };
76
+
77
+ const setPendingUploads = (client: StreamChat, uploads: PendingUploadRecord[]) => {
78
+ act(() => {
79
+ client.uploadManager.state.partialNext({
80
+ uploads: Object.fromEntries(
81
+ uploads.map(({ id, uploadProgress }) => [id, { id, uploadProgress }]),
82
+ ),
83
+ });
84
+ });
85
+ };
86
+
87
+ const countActivityIndicators = (nodes: ReactTestInstance[]) =>
88
+ nodes.reduce(
89
+ (count: number, node: ReactTestInstance) =>
90
+ count + node.findAllByType(ActivityIndicator).length,
91
+ 0,
92
+ );
93
+
67
94
  describe('AttachmentUploadPreviewList', () => {
68
95
  let client: StreamChat;
69
96
  let channel: ChannelType;
@@ -78,6 +105,7 @@ describe('AttachmentUploadPreviewList', () => {
78
105
  jest.clearAllMocks();
79
106
  cleanup();
80
107
  act(() => {
108
+ client?.uploadManager?.reset();
81
109
  channel.messageComposer.attachmentManager.initState();
82
110
  });
83
111
  });
@@ -121,7 +149,11 @@ describe('AttachmentUploadPreviewList', () => {
121
149
  it('should render FileAttachmentUploadPreview when the sound package is unavailable', async () => {
122
150
  const attachments = [
123
151
  generateAudioAttachment({
152
+ asset_url: undefined,
124
153
  localMetadata: {
154
+ file: {
155
+ uri: 'file://audio-attachment.mp3',
156
+ },
125
157
  id: 'audio-attachment',
126
158
  uploadState: FileState.UPLOADING,
127
159
  },
@@ -133,14 +165,15 @@ describe('AttachmentUploadPreviewList', () => {
133
165
  act(() => {
134
166
  channel.messageComposer.attachmentManager.upsertAttachments(attachments);
135
167
  });
168
+ setPendingUploads(client, [{ id: 'audio-attachment' }]);
136
169
 
137
170
  renderComponent({ channel, client, props });
138
171
 
139
- const { queryAllByTestId } = screen;
172
+ const { getAllByTestId, queryAllByTestId } = screen;
140
173
 
141
174
  await waitFor(() => {
142
175
  expect(queryAllByTestId('file-attachment-upload-preview')).toHaveLength(1);
143
- expect(queryAllByTestId('upload-progress-indicator')).toHaveLength(1);
176
+ expect(countActivityIndicators(getAllByTestId('file-attachment-upload-preview'))).toBe(1);
144
177
  });
145
178
  });
146
179
 
@@ -148,13 +181,20 @@ describe('AttachmentUploadPreviewList', () => {
148
181
  it('should render FileAttachmentUploadPreview with all uploading files', async () => {
149
182
  const attachments = [
150
183
  generateFileAttachment({
184
+ asset_url: undefined,
151
185
  localMetadata: {
186
+ file: {
187
+ uri: 'file://file-attachment.xls',
188
+ },
152
189
  id: 'file-attachment',
153
190
  uploadState: FileState.UPLOADING,
154
191
  },
155
192
  }),
156
193
  generateVideoAttachment({
157
194
  localMetadata: {
195
+ file: {
196
+ uri: 'file://video-attachment.mp4',
197
+ },
158
198
  id: 'video-attachment',
159
199
  uploadState: FileState.UPLOADING,
160
200
  },
@@ -165,6 +205,7 @@ describe('AttachmentUploadPreviewList', () => {
165
205
  act(() => {
166
206
  channel.messageComposer.attachmentManager.upsertAttachments(attachments);
167
207
  });
208
+ setPendingUploads(client, [{ id: 'file-attachment' }, { id: 'video-attachment' }]);
168
209
 
169
210
  renderComponent({ channel, client, props });
170
211
 
@@ -172,7 +213,7 @@ describe('AttachmentUploadPreviewList', () => {
172
213
 
173
214
  await waitFor(() => {
174
215
  expect(queryAllByTestId('file-attachment-upload-preview')).toHaveLength(2);
175
- expect(queryAllByTestId('upload-progress-indicator')).toHaveLength(2);
216
+ expect(countActivityIndicators(getAllByTestId('file-attachment-upload-preview'))).toBe(2);
176
217
  });
177
218
 
178
219
  act(() => {
@@ -303,6 +344,7 @@ describe('AttachmentUploadPreviewList', () => {
303
344
  generateImageAttachment({
304
345
  localMetadata: {
305
346
  id: 'image-attachment',
347
+ previewUri: 'file://image-attachment.png',
306
348
  uploadState: FileState.UPLOADING,
307
349
  },
308
350
  }),
@@ -312,6 +354,7 @@ describe('AttachmentUploadPreviewList', () => {
312
354
  await act(() => {
313
355
  channel.messageComposer.attachmentManager.upsertAttachments(attachments ?? []);
314
356
  });
357
+ setPendingUploads(client, [{ id: 'image-attachment' }]);
315
358
 
316
359
  renderComponent({ channel, client, props });
317
360
 
@@ -319,7 +362,7 @@ describe('AttachmentUploadPreviewList', () => {
319
362
 
320
363
  await waitFor(() => {
321
364
  expect(queryAllByTestId('image-attachment-upload-preview')).toHaveLength(1);
322
- expect(queryAllByTestId('upload-progress-indicator')).toHaveLength(1);
365
+ expect(countActivityIndicators(getAllByTestId('image-attachment-upload-preview'))).toBe(1);
323
366
  });
324
367
 
325
368
  await act(() => {
@@ -455,6 +498,7 @@ describe('AttachmentUploadPreviewList', () => {
455
498
  generateImageAttachment({
456
499
  localMetadata: {
457
500
  id: 'image-attachment-1',
501
+ previewUri: 'file://image-attachment-1.png',
458
502
  uploadState: FileState.UPLOADING,
459
503
  },
460
504
  }),
@@ -482,10 +526,11 @@ describe('AttachmentUploadPreviewList', () => {
482
526
  await act(() => {
483
527
  channel.messageComposer.attachmentManager.upsertAttachments(attachments ?? []);
484
528
  });
529
+ setPendingUploads(client, [{ id: 'image-attachment-1' }]);
485
530
 
486
531
  renderComponent({ channel, client, props });
487
532
 
488
- const { queryAllByTestId } = screen;
533
+ const { getAllByTestId, queryAllByTestId } = screen;
489
534
 
490
535
  await waitFor(() => {
491
536
  const imageAttachments = queryAllByTestId('image-attachment-upload-preview-image');
@@ -496,7 +541,7 @@ describe('AttachmentUploadPreviewList', () => {
496
541
 
497
542
  await waitFor(() => {
498
543
  expect(queryAllByTestId('image-attachment-upload-preview')).toHaveLength(4);
499
- expect(queryAllByTestId('upload-progress-indicator')).toHaveLength(1);
544
+ expect(countActivityIndicators(getAllByTestId('image-attachment-upload-preview'))).toBe(1);
500
545
  expect(queryAllByTestId('retry-upload-progress-indicator')).toHaveLength(1);
501
546
  expect(queryAllByTestId('inline-not-supported-indicator')).toHaveLength(1);
502
547
  });
@@ -1,5 +1,9 @@
1
1
  import React, { ComponentProps } from 'react';
2
2
 
3
+ import { ActivityIndicator } from 'react-native';
4
+
5
+ import type { ReactTestInstance } from 'react-test-renderer';
6
+
3
7
  import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
4
8
 
5
9
  import type { Attachment, Channel as ChannelType, LocalAttachment, StreamChat } from 'stream-chat';
@@ -24,6 +28,7 @@ jest.mock('../../../native.ts', () => {
24
28
  isDocumentPickerAvailable: jest.fn(() => true),
25
29
  isImageMediaLibraryAvailable: jest.fn(() => true),
26
30
  isImagePickerAvailable: jest.fn(() => true),
31
+ isNativeMultipartUploadAvailable: jest.fn(() => false),
27
32
  isSoundPackageAvailable: jest.fn(() => true),
28
33
  NativeHandlers: {
29
34
  Sound: {
@@ -53,6 +58,28 @@ const renderComponent = ({
53
58
  );
54
59
  };
55
60
 
61
+ type PendingUploadRecord = {
62
+ id: string;
63
+ uploadProgress?: number;
64
+ };
65
+
66
+ const setPendingUploads = (client: StreamChat, uploads: PendingUploadRecord[]) => {
67
+ act(() => {
68
+ client.uploadManager.state.partialNext({
69
+ uploads: Object.fromEntries(
70
+ uploads.map(({ id, uploadProgress }) => [id, { id, uploadProgress }]),
71
+ ),
72
+ });
73
+ });
74
+ };
75
+
76
+ const countActivityIndicators = (nodes: ReactTestInstance[]) =>
77
+ nodes.reduce(
78
+ (count: number, node: ReactTestInstance) =>
79
+ count + node.findAllByType(ActivityIndicator).length,
80
+ 0,
81
+ );
82
+
56
83
  describe('AudioAttachmentUploadPreview render', () => {
57
84
  let client: StreamChat;
58
85
  let channel: ChannelType;
@@ -67,6 +94,7 @@ describe('AudioAttachmentUploadPreview render', () => {
67
94
  jest.clearAllMocks();
68
95
  cleanup();
69
96
  act(() => {
97
+ client?.uploadManager?.reset();
70
98
  channel.messageComposer.attachmentManager.initState();
71
99
  });
72
100
  });
@@ -74,6 +102,7 @@ describe('AudioAttachmentUploadPreview render', () => {
74
102
  it('should render AudioAttachmentUploadPreview with all uploading files', async () => {
75
103
  const attachments = [
76
104
  generateAudioAttachment({
105
+ asset_url: undefined,
77
106
  localMetadata: {
78
107
  file: {
79
108
  uri: 'file://audio-attachment.mp3',
@@ -88,6 +117,7 @@ describe('AudioAttachmentUploadPreview render', () => {
88
117
  act(() => {
89
118
  channel.messageComposer.attachmentManager.upsertAttachments(attachments);
90
119
  });
120
+ setPendingUploads(client, [{ id: 'audio-attachment' }]);
91
121
 
92
122
  renderComponent({ channel, client, props });
93
123
 
@@ -95,7 +125,7 @@ describe('AudioAttachmentUploadPreview render', () => {
95
125
 
96
126
  await waitFor(() => {
97
127
  expect(queryAllByTestId('audio-attachment-upload-preview')).toHaveLength(1);
98
- expect(queryAllByTestId('upload-progress-indicator')).toHaveLength(1);
128
+ expect(countActivityIndicators(getAllByTestId('audio-attachment-upload-preview'))).toBe(1);
99
129
  });
100
130
 
101
131
  act(() => {