stream-chat-react-native-core 10.0.0-rc.3 → 10.0.0-rc.5

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 (191) hide show
  1. package/lib/commonjs/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js +4 -1
  2. package/lib/commonjs/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js.map +1 -1
  3. package/lib/commonjs/components/Channel/Channel.js +1 -0
  4. package/lib/commonjs/components/Channel/Channel.js.map +1 -1
  5. package/lib/commonjs/components/Channel/ChannelShell.js +8 -1
  6. package/lib/commonjs/components/Channel/ChannelShell.js.map +1 -1
  7. package/lib/commonjs/components/Channel/hooks/useChannelLifecycle.js +2 -2
  8. package/lib/commonjs/components/Channel/hooks/useChannelLifecycle.js.map +1 -1
  9. package/lib/commonjs/components/Channel/hooks/useJumpToLatestOnOpen.js +26 -0
  10. package/lib/commonjs/components/Channel/hooks/useJumpToLatestOnOpen.js.map +1 -0
  11. package/lib/commonjs/components/ChannelList/hooks/usePaginatedChannels.js +1 -1
  12. package/lib/commonjs/components/ChannelList/hooks/usePaginatedChannels.js.map +1 -1
  13. package/lib/commonjs/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.js +7 -0
  14. package/lib/commonjs/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.js.map +1 -1
  15. package/lib/commonjs/components/ChannelPreview/ChannelPreviewMessage.js +4 -5
  16. package/lib/commonjs/components/ChannelPreview/ChannelPreviewMessage.js.map +1 -1
  17. package/lib/commonjs/components/Message/Message.js.map +1 -1
  18. package/lib/commonjs/components/Message/MessageItemView/Headers/SentToChannelHeader.js +6 -32
  19. package/lib/commonjs/components/Message/MessageItemView/Headers/SentToChannelHeader.js.map +1 -1
  20. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js +2 -2
  21. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js.map +1 -1
  22. package/lib/commonjs/components/Message/MessageItemView/MessageStatus.js +29 -9
  23. package/lib/commonjs/components/Message/MessageItemView/MessageStatus.js.map +1 -1
  24. package/lib/commonjs/components/Message/hooks/useOpenThreadAtMessage.js +57 -0
  25. package/lib/commonjs/components/Message/hooks/useOpenThreadAtMessage.js.map +1 -0
  26. package/lib/commonjs/components/Message/utils/messageActions.js +1 -1
  27. package/lib/commonjs/components/Message/utils/messageActions.js.map +1 -1
  28. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.js +12 -8
  29. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.js.map +1 -1
  30. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.js +3 -2
  31. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.js.map +1 -1
  32. package/lib/commonjs/components/MessageInput/hooks/useAudioRecorder.js +2 -2
  33. package/lib/commonjs/components/MessageInput/hooks/useAudioRecorder.js.map +1 -1
  34. package/lib/commonjs/components/MessageList/InlineUnreadIndicator.js +2 -2
  35. package/lib/commonjs/components/MessageList/InlineUnreadIndicator.js.map +1 -1
  36. package/lib/commonjs/components/MessageList/MessageList.js +1 -4
  37. package/lib/commonjs/components/MessageList/MessageList.js.map +1 -1
  38. package/lib/commonjs/components/MessageList/hooks/useMessageListFocus.js +18 -2
  39. package/lib/commonjs/components/MessageList/hooks/useMessageListFocus.js.map +1 -1
  40. package/lib/commonjs/components/Thread/components/ThreadFooterComponent.js +1 -2
  41. package/lib/commonjs/components/Thread/components/ThreadFooterComponent.js.map +1 -1
  42. package/lib/commonjs/components/index.js +12 -0
  43. package/lib/commonjs/components/index.js.map +1 -1
  44. package/lib/commonjs/contexts/channelPinnedMessageListContext/ChannelPinnedMessageListContext.js +0 -15
  45. package/lib/commonjs/contexts/channelPinnedMessageListContext/ChannelPinnedMessageListContext.js.map +1 -1
  46. package/lib/commonjs/contexts/messageContext/MessageContext.js.map +1 -1
  47. package/lib/commonjs/contexts/messageListItemContext/MessageListItemContext.js.map +1 -1
  48. package/lib/commonjs/index.js +0 -12
  49. package/lib/commonjs/index.js.map +1 -1
  50. package/lib/commonjs/types/types.js.map +1 -1
  51. package/lib/commonjs/version.json +1 -1
  52. package/lib/module/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js +4 -1
  53. package/lib/module/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js.map +1 -1
  54. package/lib/module/components/Channel/Channel.js +1 -0
  55. package/lib/module/components/Channel/Channel.js.map +1 -1
  56. package/lib/module/components/Channel/ChannelShell.js +8 -1
  57. package/lib/module/components/Channel/ChannelShell.js.map +1 -1
  58. package/lib/module/components/Channel/hooks/useChannelLifecycle.js +2 -2
  59. package/lib/module/components/Channel/hooks/useChannelLifecycle.js.map +1 -1
  60. package/lib/module/components/Channel/hooks/useJumpToLatestOnOpen.js +26 -0
  61. package/lib/module/components/Channel/hooks/useJumpToLatestOnOpen.js.map +1 -0
  62. package/lib/module/components/ChannelList/hooks/usePaginatedChannels.js +1 -1
  63. package/lib/module/components/ChannelList/hooks/usePaginatedChannels.js.map +1 -1
  64. package/lib/module/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.js +7 -0
  65. package/lib/module/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.js.map +1 -1
  66. package/lib/module/components/ChannelPreview/ChannelPreviewMessage.js +4 -5
  67. package/lib/module/components/ChannelPreview/ChannelPreviewMessage.js.map +1 -1
  68. package/lib/module/components/Message/Message.js.map +1 -1
  69. package/lib/module/components/Message/MessageItemView/Headers/SentToChannelHeader.js +6 -32
  70. package/lib/module/components/Message/MessageItemView/Headers/SentToChannelHeader.js.map +1 -1
  71. package/lib/module/components/Message/MessageItemView/MessageContent.js +2 -2
  72. package/lib/module/components/Message/MessageItemView/MessageContent.js.map +1 -1
  73. package/lib/module/components/Message/MessageItemView/MessageStatus.js +29 -9
  74. package/lib/module/components/Message/MessageItemView/MessageStatus.js.map +1 -1
  75. package/lib/module/components/Message/hooks/useOpenThreadAtMessage.js +57 -0
  76. package/lib/module/components/Message/hooks/useOpenThreadAtMessage.js.map +1 -0
  77. package/lib/module/components/Message/utils/messageActions.js +1 -1
  78. package/lib/module/components/Message/utils/messageActions.js.map +1 -1
  79. package/lib/module/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.js +12 -8
  80. package/lib/module/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.js.map +1 -1
  81. package/lib/module/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.js +3 -2
  82. package/lib/module/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.js.map +1 -1
  83. package/lib/module/components/MessageInput/hooks/useAudioRecorder.js +2 -2
  84. package/lib/module/components/MessageInput/hooks/useAudioRecorder.js.map +1 -1
  85. package/lib/module/components/MessageList/InlineUnreadIndicator.js +2 -2
  86. package/lib/module/components/MessageList/InlineUnreadIndicator.js.map +1 -1
  87. package/lib/module/components/MessageList/MessageList.js +1 -4
  88. package/lib/module/components/MessageList/MessageList.js.map +1 -1
  89. package/lib/module/components/MessageList/hooks/useMessageListFocus.js +18 -2
  90. package/lib/module/components/MessageList/hooks/useMessageListFocus.js.map +1 -1
  91. package/lib/module/components/Thread/components/ThreadFooterComponent.js +1 -2
  92. package/lib/module/components/Thread/components/ThreadFooterComponent.js.map +1 -1
  93. package/lib/module/components/index.js +12 -0
  94. package/lib/module/components/index.js.map +1 -1
  95. package/lib/module/contexts/channelPinnedMessageListContext/ChannelPinnedMessageListContext.js +0 -15
  96. package/lib/module/contexts/channelPinnedMessageListContext/ChannelPinnedMessageListContext.js.map +1 -1
  97. package/lib/module/contexts/messageContext/MessageContext.js.map +1 -1
  98. package/lib/module/contexts/messageListItemContext/MessageListItemContext.js.map +1 -1
  99. package/lib/module/index.js +0 -12
  100. package/lib/module/index.js.map +1 -1
  101. package/lib/module/types/types.js.map +1 -1
  102. package/lib/module/version.json +1 -1
  103. package/lib/typescript/ai-components/services/media-picker-service/index.d.ts +1 -1
  104. package/lib/typescript/components/Channel/Channel.d.ts.map +1 -1
  105. package/lib/typescript/components/Channel/ChannelShell.d.ts +2 -0
  106. package/lib/typescript/components/Channel/ChannelShell.d.ts.map +1 -1
  107. package/lib/typescript/components/Channel/hooks/useChannelLifecycle.d.ts.map +1 -1
  108. package/lib/typescript/components/Channel/hooks/useJumpToLatestOnOpen.d.ts +15 -0
  109. package/lib/typescript/components/Channel/hooks/useJumpToLatestOnOpen.d.ts.map +1 -0
  110. package/lib/typescript/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.d.ts.map +1 -1
  111. package/lib/typescript/components/ChannelPreview/ChannelPreviewMessage.d.ts.map +1 -1
  112. package/lib/typescript/components/Message/Message.d.ts +2 -1
  113. package/lib/typescript/components/Message/Message.d.ts.map +1 -1
  114. package/lib/typescript/components/Message/MessageItemView/Headers/SentToChannelHeader.d.ts.map +1 -1
  115. package/lib/typescript/components/Message/MessageItemView/MessageContent.d.ts.map +1 -1
  116. package/lib/typescript/components/Message/MessageItemView/MessageStatus.d.ts +6 -0
  117. package/lib/typescript/components/Message/MessageItemView/MessageStatus.d.ts.map +1 -1
  118. package/lib/typescript/components/Message/hooks/useOpenThreadAtMessage.d.ts +14 -0
  119. package/lib/typescript/components/Message/hooks/useOpenThreadAtMessage.d.ts.map +1 -0
  120. package/lib/typescript/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.d.ts.map +1 -1
  121. package/lib/typescript/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.d.ts.map +1 -1
  122. package/lib/typescript/components/MessageList/MessageList.d.ts.map +1 -1
  123. package/lib/typescript/components/MessageList/hooks/useMessageListFocus.d.ts +5 -2
  124. package/lib/typescript/components/MessageList/hooks/useMessageListFocus.d.ts.map +1 -1
  125. package/lib/typescript/components/Thread/components/ThreadFooterComponent.d.ts.map +1 -1
  126. package/lib/typescript/components/index.d.ts +1 -0
  127. package/lib/typescript/components/index.d.ts.map +1 -1
  128. package/lib/typescript/contexts/channelPinnedMessageListContext/ChannelPinnedMessageListContext.d.ts.map +1 -1
  129. package/lib/typescript/contexts/messageContext/MessageContext.d.ts +2 -1
  130. package/lib/typescript/contexts/messageContext/MessageContext.d.ts.map +1 -1
  131. package/lib/typescript/contexts/messageListItemContext/MessageListItemContext.d.ts +10 -6
  132. package/lib/typescript/contexts/messageListItemContext/MessageListItemContext.d.ts.map +1 -1
  133. package/lib/typescript/i18n/keys.d.ts +2 -2
  134. package/lib/typescript/i18n/keys.d.ts.map +1 -1
  135. package/lib/typescript/index.d.ts +0 -1
  136. package/lib/typescript/index.d.ts.map +1 -1
  137. package/lib/typescript/types/types.d.ts +0 -4
  138. package/lib/typescript/types/types.d.ts.map +1 -1
  139. package/package.json +2 -2
  140. package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx +1 -1
  141. package/src/components/AttachmentPicker/components/AttachmentMediaPicker/__tests__/AttachmentPickerItem.test.tsx +29 -1
  142. package/src/components/Channel/Channel.tsx +1 -0
  143. package/src/components/Channel/ChannelShell.tsx +12 -0
  144. package/src/components/Channel/__tests__/Channel.test.tsx +142 -7
  145. package/src/components/Channel/hooks/useChannelLifecycle.ts +8 -4
  146. package/src/components/Channel/hooks/useCreateOwnCapabilitiesContext.ts +1 -1
  147. package/src/components/Channel/hooks/useJumpToLatestOnOpen.ts +42 -0
  148. package/src/components/ChannelList/__tests__/ChannelList.test.tsx +28 -0
  149. package/src/components/ChannelList/hooks/usePaginatedChannels.ts +1 -1
  150. package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx +8 -0
  151. package/src/components/ChannelPreview/ChannelPreviewMessage.tsx +2 -7
  152. package/src/components/ChannelPreview/__tests__/ChannelPreviewView.test.tsx +41 -0
  153. package/src/components/Message/Message.tsx +5 -2
  154. package/src/components/Message/MessageItemView/Headers/SentToChannelHeader.tsx +4 -32
  155. package/src/components/Message/MessageItemView/MessageContent.tsx +3 -2
  156. package/src/components/Message/MessageItemView/MessageStatus.tsx +43 -4
  157. package/src/components/Message/MessageItemView/__tests__/MessageContent.test.tsx +68 -1
  158. package/src/components/Message/MessageItemView/__tests__/MessageStatus.test.tsx +57 -0
  159. package/src/components/Message/hooks/useOpenThreadAtMessage.ts +50 -0
  160. package/src/components/Message/utils/__tests__/messageActions.test.ts +57 -0
  161. package/src/components/Message/utils/messageActions.ts +1 -1
  162. package/src/components/MessageInput/__tests__/AttachmentUploadPreviewList.test.tsx +34 -6
  163. package/src/components/MessageInput/__tests__/AudioAttachmentUploadPreview.test.tsx +24 -3
  164. package/src/components/MessageInput/__tests__/SendMessageDisallowedIndicator.test.tsx +3 -6
  165. package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.tsx +19 -8
  166. package/src/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.tsx +2 -5
  167. package/src/components/MessageInput/hooks/useAudioRecorder.tsx +2 -2
  168. package/src/components/MessageList/InlineUnreadIndicator.tsx +2 -2
  169. package/src/components/MessageList/MessageList.tsx +1 -5
  170. package/src/components/MessageList/__tests__/InlineUnreadIndicator.test.tsx +2 -2
  171. package/src/components/MessageList/__tests__/MessageList.test.tsx +252 -6
  172. package/src/components/MessageList/hooks/useMessageListFocus.ts +22 -2
  173. package/src/components/Thread/__tests__/Thread.test.tsx +131 -1
  174. package/src/components/Thread/components/ThreadFooterComponent.tsx +0 -1
  175. package/src/components/index.ts +1 -0
  176. package/src/contexts/channelPinnedMessageListContext/ChannelPinnedMessageListContext.tsx +5 -14
  177. package/src/contexts/messageContext/MessageContext.tsx +2 -1
  178. package/src/contexts/messageListItemContext/MessageListItemContext.tsx +15 -6
  179. package/src/i18n/__tests__/catalog.fixture.json +2 -2
  180. package/src/i18n/keys.ts +2 -2
  181. package/src/index.ts +0 -1
  182. package/src/types/stream-chat-common-custom-data.d.ts +0 -3
  183. package/src/types/types.ts +0 -5
  184. package/src/version.json +1 -1
  185. package/lib/commonjs/utils/setupCommandUIMiddlewares.js +0 -34
  186. package/lib/commonjs/utils/setupCommandUIMiddlewares.js.map +0 -1
  187. package/lib/module/utils/setupCommandUIMiddlewares.js +0 -34
  188. package/lib/module/utils/setupCommandUIMiddlewares.js.map +0 -1
  189. package/lib/typescript/utils/setupCommandUIMiddlewares.d.ts +0 -3
  190. package/lib/typescript/utils/setupCommandUIMiddlewares.d.ts.map +0 -1
  191. package/src/utils/setupCommandUIMiddlewares.ts +0 -31
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { StyleSheet, View } from 'react-native';
3
3
 
4
- import { cleanup, render, screen, waitFor } from '@testing-library/react-native';
4
+ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
5
5
  import type { Channel as ChannelType, StreamChat } from 'stream-chat';
6
6
 
7
7
  import { WithComponents } from '../../../../contexts/componentsContext/ComponentsContext';
@@ -382,6 +382,73 @@ describe('MessageContent', () => {
382
382
  expect(contentContainerStyle.paddingBottom).toBeGreaterThan(0);
383
383
  });
384
384
 
385
+ describe('pressing a quote', () => {
386
+ const renderQuote = ({
387
+ quotedMessage,
388
+ }: {
389
+ quotedMessage: ReturnType<typeof generateMessage>;
390
+ }) => {
391
+ const goToMessage = jest.fn();
392
+ render(
393
+ <Chat client={chatClient}>
394
+ <Channel channel={channel}>
395
+ <Message
396
+ goToMessage={goToMessage}
397
+ groupStyles={['bottom']}
398
+ message={generateMessage({ quoted_message: quotedMessage, user })}
399
+ />
400
+ </Channel>
401
+ </Chat>,
402
+ );
403
+ return { goToMessage };
404
+ };
405
+
406
+ const threadOnlyReply = () => {
407
+ const parentMessage = generateMessage({ cid: channel.cid, user });
408
+ const thread = chatClient.threads.ensure({ channel, parentMessage });
409
+ const reply = generateMessage({ parent_id: parentMessage.id, text: 'quoted message', user });
410
+ return { reply, thread };
411
+ };
412
+
413
+ it('passes the quoted message to goToMessage when the quote is pressed', async () => {
414
+ const quotedMessage = generateMessage({ text: 'quoted message', user });
415
+ const { goToMessage } = renderQuote({ quotedMessage });
416
+
417
+ fireEvent.press(await screen.findByText('quoted message'));
418
+
419
+ await waitFor(() =>
420
+ expect(goToMessage).toHaveBeenCalledWith(
421
+ quotedMessage.id,
422
+ expect.objectContaining({ id: quotedMessage.id }),
423
+ ),
424
+ );
425
+ });
426
+
427
+ it('opens the thread from the header of a reply also sent to the channel', async () => {
428
+ const onAlsoSentToChannelHeaderPress = jest.fn();
429
+ const { reply, thread } = threadOnlyReply();
430
+ render(
431
+ <Chat client={chatClient}>
432
+ <Channel
433
+ channel={channel}
434
+ onAlsoSentToChannelHeaderPress={onAlsoSentToChannelHeaderPress}
435
+ >
436
+ <Message groupStyles={['bottom']} message={{ ...reply, show_in_channel: true }} />
437
+ </Channel>
438
+ </Chat>,
439
+ );
440
+
441
+ fireEvent.press(await screen.findByText('View'));
442
+
443
+ await waitFor(() =>
444
+ expect(onAlsoSentToChannelHeaderPress).toHaveBeenCalledWith({
445
+ targetedMessageId: reply.id,
446
+ thread,
447
+ }),
448
+ );
449
+ });
450
+ });
451
+
385
452
  it('does not render the quoted reply for an ephemeral giphy preview', async () => {
386
453
  const user = generateUser();
387
454
  const message = generateMessage({
@@ -84,6 +84,63 @@ describe('MessageStatus', () => {
84
84
  },
85
85
  );
86
86
 
87
+ it.each([
88
+ ['renders', 'regular', 1],
89
+ ['renders no', 'deleted', 0],
90
+ ])('%s status for a %s message', async (_, type, expectedCount) => {
91
+ const message = generateMessage({ user: user1 });
92
+ const { queryAllByLabelText } = renderMessageStatus({
93
+ delivered: true,
94
+ message: { ...message, status: 'received', type },
95
+ read: true,
96
+ });
97
+
98
+ await act(async () => {
99
+ await new Promise((resolve) => setTimeout(resolve, 0));
100
+ });
101
+
102
+ expect(queryAllByLabelText('Read')).toHaveLength(expectedCount);
103
+ });
104
+
105
+ describe('with read events disabled', () => {
106
+ beforeEach(() => {
107
+ chatClient.config.set({ channel: { readEvents: { enabled: false } } });
108
+ });
109
+
110
+ it.each([
111
+ [false, false, 'Sent'],
112
+ [true, false, 'Delivered'],
113
+ [true, true, 'Read'],
114
+ ])(
115
+ 'renders no status when delivered is %s and read is %s',
116
+ async (delivered, read, accessibilityLabel) => {
117
+ const message = generateMessage({ user: user1 });
118
+ const { queryByLabelText } = renderMessageStatus({
119
+ delivered,
120
+ message: { ...message, status: 'received' },
121
+ read,
122
+ });
123
+
124
+ await act(async () => {
125
+ await new Promise((resolve) => setTimeout(resolve, 0));
126
+ });
127
+
128
+ expect(queryByLabelText(accessibilityLabel)).toBeNull();
129
+ },
130
+ );
131
+
132
+ it('still renders the sending status', async () => {
133
+ const message = generateMessage({ user: user1 });
134
+ const { getByLabelText } = renderMessageStatus({
135
+ message: { ...message, status: 'sending' },
136
+ });
137
+
138
+ await waitFor(() => {
139
+ expect(getByLabelText('Sending')).toBeTruthy();
140
+ });
141
+ });
142
+ });
143
+
87
144
  describe('reactive receipt state', () => {
88
145
  // Two own messages, older first. `olderMessage` is the one under test throughout: the whole
89
146
  // point of sourcing from `lastReadRefByOthers` is that it reports correctly even though no
@@ -0,0 +1,50 @@
1
+ import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
2
+ import { useChatContext } from '../../../contexts/chatContext/ChatContext';
3
+ import { useMessagesContext } from '../../../contexts/messagesContext/MessagesContext';
4
+ import { useStableCallback } from '../../../hooks';
5
+
6
+ /**
7
+ * Returns a function that opens the thread holding a message, at that message, through the
8
+ * `onAlsoSentToChannelHeaderPress` handler. The thread is taken from the client when it is
9
+ * registered there; otherwise its parent message is read from the message store, or fetched, and a
10
+ * thread is built from it. Opening the thread loads whatever else it is missing.
11
+ *
12
+ * @returns The function, which takes the thread's `parentId` and the `targetedMessageId` to open
13
+ * the thread at. It does nothing without an `onAlsoSentToChannelHeaderPress` handler.
14
+ */
15
+ export const useOpenThreadAtMessage = () => {
16
+ const { onAlsoSentToChannelHeaderPress } = useMessagesContext();
17
+ const { client } = useChatContext();
18
+ const { channel } = useChannelContext();
19
+
20
+ return useStableCallback(
21
+ async ({ parentId, targetedMessageId }: { parentId: string; targetedMessageId: string }) => {
22
+ if (!onAlsoSentToChannelHeaderPress) {
23
+ return;
24
+ }
25
+
26
+ const registeredThread = client.threads.get(parentId);
27
+ if (registeredThread) {
28
+ onAlsoSentToChannelHeaderPress({ targetedMessageId, thread: registeredThread });
29
+ return;
30
+ }
31
+
32
+ try {
33
+ const parentMessage =
34
+ client.messageStore.get(parentId) ??
35
+ (await channel.getMessagesById([parentId])).messages?.[0];
36
+
37
+ if (!parentMessage) {
38
+ return;
39
+ }
40
+
41
+ onAlsoSentToChannelHeaderPress({
42
+ targetedMessageId,
43
+ thread: client.threads.ensure({ channel, parentMessage }),
44
+ });
45
+ } catch (error) {
46
+ console.error('Error querying parent message:', error);
47
+ }
48
+ },
49
+ );
50
+ };
@@ -0,0 +1,57 @@
1
+ import type { OwnCapabilitiesContextValue } from '../../../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext';
2
+ import { generateMessage } from '../../../../mock-builders/generator/message';
3
+ import type { MessageActionType } from '../../../MessageMenu/MessageActionListItem';
4
+ import { messageActions, MessageActionsParams } from '../messageActions';
5
+
6
+ const action = (actionType: string) => ({ actionType }) as unknown as MessageActionType;
7
+
8
+ const getActionTypes = (overrides: Partial<MessageActionsParams>) =>
9
+ messageActions({
10
+ banUser: action('banUser'),
11
+ blockUser: action('blockUser'),
12
+ copyMessage: action('copyMessage'),
13
+ deleteMessage: action('deleteMessage'),
14
+ dismissOverlay: jest.fn(),
15
+ editMessage: action('editMessage'),
16
+ error: false,
17
+ flagMessage: action('flagMessage'),
18
+ isMyMessage: false,
19
+ isThreadMessage: false,
20
+ markUnread: action('markUnread'),
21
+ message: generateMessage(),
22
+ muteUser: action('muteUser'),
23
+ ownCapabilities: {
24
+ quoteMessage: true,
25
+ readEvents: true,
26
+ sendReply: true,
27
+ } as OwnCapabilitiesContextValue,
28
+ pinMessage: action('pinMessage'),
29
+ quotedReply: action('quotedReply'),
30
+ retry: action('retry'),
31
+ threadReply: action('threadReply'),
32
+ unpinMessage: action('unpinMessage'),
33
+ updateMessage: jest.fn(),
34
+ ...overrides,
35
+ })?.map((item) => item.actionType);
36
+
37
+ describe('messageActions', () => {
38
+ it('offers quoting a thread message, but not replying in a thread or marking it unread', () => {
39
+ const actionTypes = getActionTypes({ isThreadMessage: true });
40
+
41
+ expect(actionTypes).toContain('quotedReply');
42
+ expect(actionTypes).not.toContain('threadReply');
43
+ expect(actionTypes).not.toContain('markUnread');
44
+ });
45
+
46
+ it('does not offer quoting a message that failed or is an error', () => {
47
+ expect(getActionTypes({ error: true })).not.toContain('quotedReply');
48
+ });
49
+
50
+ it('does not offer quoting without the quote-message capability', () => {
51
+ expect(
52
+ getActionTypes({
53
+ ownCapabilities: { quoteMessage: false } as OwnCapabilitiesContextValue,
54
+ }),
55
+ ).not.toContain('quotedReply');
56
+ });
57
+ });
@@ -64,7 +64,7 @@ export const messageActions = ({
64
64
  actions.push(threadReply);
65
65
  }
66
66
 
67
- if (ownCapabilities.quoteMessage && !isThreadMessage && !error) {
67
+ if (ownCapabilities.quoteMessage && !error) {
68
68
  actions.push(quotedReply);
69
69
  }
70
70
 
@@ -312,8 +312,8 @@ describe('AttachmentUploadPreviewList', () => {
312
312
  });
313
313
 
314
314
  it('should render FileAttachmentUploadPreview with all failed files', async () => {
315
- const uploadAttachmentSpy = jest.fn();
316
- channel.messageComposer.attachmentManager.uploadAttachment = uploadAttachmentSpy;
315
+ const uploadFileSpy = jest.fn();
316
+ channel.messageComposer.attachmentManager.uploadFile = uploadFileSpy;
317
317
  const attachments = [
318
318
  generateFileAttachment({
319
319
  localMetadata: {
@@ -353,10 +353,38 @@ describe('AttachmentUploadPreviewList', () => {
353
353
  await waitFor(() => {
354
354
  expect(queryAllByTestId('file-attachment-upload-preview')).toHaveLength(2);
355
355
  expect(channel.messageComposer.attachmentManager.attachments).toHaveLength(2);
356
- expect(uploadAttachmentSpy).toHaveBeenCalled();
356
+ expect(uploadFileSpy).toHaveBeenCalled();
357
357
  });
358
358
  });
359
359
 
360
+ it('does not retry a failed upload while uploads are disabled', async () => {
361
+ const { attachmentManager } = channel.messageComposer;
362
+ const uploadFileSpy = jest.fn();
363
+ attachmentManager.uploadFile = uploadFileSpy;
364
+ jest.spyOn(attachmentManager, 'isUploadEnabled', 'get').mockReturnValue(false);
365
+
366
+ act(() => {
367
+ attachmentManager.upsertAttachments([
368
+ generateFileAttachment({
369
+ localMetadata: { id: 'file-attachment', uploadState: FileState.FAILED },
370
+ }),
371
+ ]);
372
+ });
373
+
374
+ renderComponent({ channel, client, props: {} });
375
+
376
+ await waitFor(() => {
377
+ expect(screen.queryAllByTestId('retry-upload-progress-indicator')).toHaveLength(1);
378
+ });
379
+
380
+ act(() => {
381
+ fireEvent.press(screen.getAllByTestId('retry-upload-progress-indicator')[0]);
382
+ });
383
+
384
+ expect(uploadFileSpy).not.toHaveBeenCalled();
385
+ expect(attachmentManager.attachments).toHaveLength(1);
386
+ });
387
+
360
388
  it('should render FileAttachmentUploadPreview with all unsupported', async () => {
361
389
  const attachments = [
362
390
  generateFileAttachment({
@@ -472,8 +500,8 @@ describe('AttachmentUploadPreviewList', () => {
472
500
  });
473
501
 
474
502
  it('should render ImageAttachmentUploadPreview with all failed images', async () => {
475
- const uploadAttachmentSpy = jest.fn();
476
- channel.messageComposer.attachmentManager.uploadAttachment = uploadAttachmentSpy;
503
+ const uploadFileSpy = jest.fn();
504
+ channel.messageComposer.attachmentManager.uploadFile = uploadFileSpy;
477
505
  const attachments = [
478
506
  generateImageAttachment({
479
507
  localMetadata: {
@@ -511,7 +539,7 @@ describe('AttachmentUploadPreviewList', () => {
511
539
  await waitFor(() => {
512
540
  expect(queryAllByTestId('image-attachment-upload-preview')).toHaveLength(1);
513
541
  expect(channel.messageComposer.attachmentManager.attachments).toHaveLength(1);
514
- expect(uploadAttachmentSpy).toHaveBeenCalled();
542
+ expect(uploadFileSpy).toHaveBeenCalled();
515
543
  });
516
544
  });
517
545
 
@@ -165,9 +165,30 @@ describe('AudioAttachmentUploadPreview render', () => {
165
165
  });
166
166
  });
167
167
 
168
+ it('should render AudioAttachmentUploadPreview for an attachment without a local file', async () => {
169
+ const attachments = [
170
+ generateAudioAttachment({
171
+ localMetadata: {
172
+ id: 'audio-attachment',
173
+ uploadState: FileState.FINISHED,
174
+ },
175
+ }),
176
+ ];
177
+
178
+ act(() => {
179
+ channel.messageComposer.attachmentManager.upsertAttachments(attachments);
180
+ });
181
+
182
+ renderComponent({ channel, client, props: {} });
183
+
184
+ await waitFor(() => {
185
+ expect(screen.queryAllByTestId('audio-attachment-upload-preview')).toHaveLength(1);
186
+ });
187
+ });
188
+
168
189
  it('should render AudioAttachmentUploadPreview with all failed files', async () => {
169
- const uploadAttachmentSpy = jest.fn();
170
- channel.messageComposer.attachmentManager.uploadAttachment = uploadAttachmentSpy;
190
+ const uploadFileSpy = jest.fn();
191
+ channel.messageComposer.attachmentManager.uploadFile = uploadFileSpy;
171
192
  const attachments = [
172
193
  generateAudioAttachment({
173
194
  localMetadata: {
@@ -203,7 +224,7 @@ describe('AudioAttachmentUploadPreview render', () => {
203
224
  expect(queryAllByTestId('audio-attachment-upload-preview')).toHaveLength(1);
204
225
  expect(getByLabelText('audio-attachment-preview')).toBeDefined();
205
226
  expect(channel.messageComposer.attachmentManager.attachments).toHaveLength(1);
206
- expect(uploadAttachmentSpy).toHaveBeenCalled();
227
+ expect(uploadFileSpy).toHaveBeenCalled();
207
228
  });
208
229
  });
209
230
 
@@ -115,8 +115,7 @@ describe('SendMessageDisallowedIndicator', () => {
115
115
  expect(queryByTestId('send-message-disallowed-indicator')).toBeNull();
116
116
  });
117
117
 
118
- // Own capabilities are now sourced reactively from channel.state; `capabilities.changed` is an
119
- // output-only notification in v10, so drive the change through the store directly.
118
+ // Own capabilities are read from `channel.state`, so the change is driven through the store.
120
119
  act(() => {
121
120
  channel.state.partialNext({
122
121
  ownCapabilities: channel.data!.own_capabilities!.filter(
@@ -226,12 +225,10 @@ describe("SendMessageDisallowedIndicator's edited state", () => {
226
225
  });
227
226
 
228
227
  act(() => {
229
- chatClient.dispatchEvent({
230
- cid: customChannel.cid,
231
- own_capabilities: customChannel.data!.own_capabilities!.filter(
228
+ customChannel.state.partialNext({
229
+ ownCapabilities: customChannel.data!.own_capabilities!.filter(
232
230
  (capability: string) => capability !== 'send-message',
233
231
  ),
234
- type: 'capabilities.changed',
235
232
  });
236
233
  });
237
234
 
@@ -21,6 +21,7 @@ import {
21
21
  isVideoAttachment,
22
22
  LocalAttachment,
23
23
  LocalAudioAttachment,
24
+ LocalUploadAttachment,
24
25
  LocalVoiceRecordingAttachment,
25
26
  } from 'stream-chat';
26
27
 
@@ -292,6 +293,16 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
292
293
  [attachmentManager, dataRef, prepareForRemoval],
293
294
  );
294
295
 
296
+ // `uploadFile` does not check whether uploads are enabled, so a retry does not start one while
297
+ // they are off.
298
+ const handleRetry = useCallback(
299
+ (attachment: LocalUploadAttachment) => {
300
+ if (!attachmentManager.isUploadEnabled) return;
301
+ return attachmentManager.uploadFile(attachment);
302
+ },
303
+ [attachmentManager],
304
+ );
305
+
295
306
  useLayoutEffect(() => {
296
307
  const previousData = previousDataRef.current;
297
308
  const nextIds = new Set(data.map((attachment) => attachment.localMetadata.id));
@@ -346,7 +357,7 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
346
357
  return (
347
358
  <ImageAttachmentUploadPreview
348
359
  attachment={attachment}
349
- handleRetry={attachmentManager.uploadAttachment}
360
+ handleRetry={handleRetry}
350
361
  removeAttachments={removeAttachments}
351
362
  />
352
363
  );
@@ -354,7 +365,7 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
354
365
  return (
355
366
  <AudioAttachmentUploadPreview
356
367
  attachment={attachment}
357
- handleRetry={attachmentManager.uploadAttachment}
368
+ handleRetry={handleRetry}
358
369
  removeAttachments={removeAttachments}
359
370
  />
360
371
  );
@@ -363,7 +374,7 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
363
374
  return (
364
375
  <AudioAttachmentUploadPreview
365
376
  attachment={attachment}
366
- handleRetry={attachmentManager.uploadAttachment}
377
+ handleRetry={handleRetry}
367
378
  removeAttachments={removeAttachments}
368
379
  />
369
380
  );
@@ -371,7 +382,7 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
371
382
  return (
372
383
  <FileAttachmentUploadPreview
373
384
  attachment={attachment}
374
- handleRetry={attachmentManager.uploadAttachment}
385
+ handleRetry={handleRetry}
375
386
  removeAttachments={removeAttachments}
376
387
  />
377
388
  );
@@ -380,7 +391,7 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
380
391
  return (
381
392
  <VideoAttachmentUploadPreview
382
393
  attachment={attachment}
383
- handleRetry={attachmentManager.uploadAttachment}
394
+ handleRetry={handleRetry}
384
395
  removeAttachments={removeAttachments}
385
396
  />
386
397
  );
@@ -388,7 +399,7 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
388
399
  return (
389
400
  <FileAttachmentUploadPreview
390
401
  attachment={attachment}
391
- handleRetry={attachmentManager.uploadAttachment}
402
+ handleRetry={handleRetry}
392
403
  removeAttachments={removeAttachments}
393
404
  />
394
405
  );
@@ -399,7 +410,7 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
399
410
  FileAttachmentUploadPreview,
400
411
  ImageAttachmentUploadPreview,
401
412
  VideoAttachmentUploadPreview,
402
- attachmentManager.uploadAttachment,
413
+ handleRetry,
403
414
  removeAttachments,
404
415
  soundPackageAvailable,
405
416
  ],
@@ -496,7 +507,7 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
496
507
  <AttachmentPreviewCell key={attachment.localMetadata.id}>
497
508
  <AudioAttachmentUploadPreview
498
509
  attachment={attachment}
499
- handleRetry={attachmentManager.uploadAttachment}
510
+ handleRetry={handleRetry}
500
511
  removeAttachments={removeAttachments}
501
512
  />
502
513
  </AttachmentPreviewCell>
@@ -43,11 +43,8 @@ export const AudioAttachmentUploadPreview = ({
43
43
  const messageComposer = useMessageComposer();
44
44
  const isDraft = messageComposer.draftId;
45
45
  const isEditing = messageComposer.editedMessage;
46
- const assetUrl =
47
- (isDraft || isEditing
48
- ? attachment.asset_url
49
- : (attachment.localMetadata.file as FileReference).uri) ??
50
- (attachment.localMetadata.file as FileReference).uri;
46
+ const fileUri = (attachment.localMetadata.file as FileReference | undefined)?.uri;
47
+ const assetUrl = (isDraft || isEditing ? attachment.asset_url : fileUri) ?? fileUri;
51
48
 
52
49
  const finalAttachment = useMemo(
53
50
  () => ({
@@ -97,8 +97,8 @@ export const useAudioRecorder = ({
97
97
  if (sendOnComplete) {
98
98
  attachmentManager.upsertAttachments([audioFile]);
99
99
  sendMessage();
100
- } else {
101
- await attachmentManager.uploadAttachment(audioFile);
100
+ } else if (attachmentManager.isUploadEnabled) {
101
+ await attachmentManager.uploadFile(audioFile);
102
102
  }
103
103
  } catch (error) {
104
104
  console.log('Error uploading voice recording: ', error);
@@ -20,8 +20,8 @@ export const InlineUnreadIndicator = (props: InlineUnreadIndicatorProps) => {
20
20
  {unreadCount
21
21
  ? t('messageList.newMessages.text', {
22
22
  count: unreadCount,
23
- defaultValue_one: '{{count}} new message',
24
- defaultValue_other: '{{count}} new messages',
23
+ defaultValue_one: '{{count}} unread message',
24
+ defaultValue_other: '{{count}} unread messages',
25
25
  })
26
26
  : t('messageList.unreadMessages.label', 'Unread Messages')}
27
27
  </Text>
@@ -432,12 +432,8 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
432
432
  }, [disabled]);
433
433
 
434
434
  useEffect(() => {
435
- if (thread) {
436
- setAutoscrollToRecent(true);
437
- return;
438
- }
439
-
440
435
  if (!processedMessageList.length || !paginator) {
436
+ if (thread) setAutoscrollToRecent(true);
441
437
  return;
442
438
  }
443
439
 
@@ -9,8 +9,8 @@ import { InlineUnreadIndicator } from '../InlineUnreadIndicator';
9
9
 
10
10
  describe('InlineUnreadIndicator', () => {
11
11
  it.each([
12
- [1, '1 new message'],
13
- [3, '3 new messages'],
12
+ [1, '1 unread message'],
13
+ [3, '3 unread messages'],
14
14
  ])('labels %i unread messages as "%s"', async (unreadCount, label) => {
15
15
  const translators = await new Streami18n().init();
16
16
  render(