stream-chat-react-native-core 10.0.0-rc.4 → 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 (159) 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/hooks/useChannelLifecycle.js +2 -2
  4. package/lib/commonjs/components/Channel/hooks/useChannelLifecycle.js.map +1 -1
  5. package/lib/commonjs/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.js +7 -0
  6. package/lib/commonjs/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.js.map +1 -1
  7. package/lib/commonjs/components/ChannelPreview/ChannelPreviewMessage.js +4 -5
  8. package/lib/commonjs/components/ChannelPreview/ChannelPreviewMessage.js.map +1 -1
  9. package/lib/commonjs/components/Message/Message.js.map +1 -1
  10. package/lib/commonjs/components/Message/MessageItemView/Headers/SentToChannelHeader.js +6 -32
  11. package/lib/commonjs/components/Message/MessageItemView/Headers/SentToChannelHeader.js.map +1 -1
  12. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js +2 -2
  13. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js.map +1 -1
  14. package/lib/commonjs/components/Message/MessageItemView/MessageStatus.js +29 -9
  15. package/lib/commonjs/components/Message/MessageItemView/MessageStatus.js.map +1 -1
  16. package/lib/commonjs/components/Message/hooks/useOpenThreadAtMessage.js +57 -0
  17. package/lib/commonjs/components/Message/hooks/useOpenThreadAtMessage.js.map +1 -0
  18. package/lib/commonjs/components/Message/utils/messageActions.js +1 -1
  19. package/lib/commonjs/components/Message/utils/messageActions.js.map +1 -1
  20. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.js +12 -8
  21. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.js.map +1 -1
  22. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.js +3 -2
  23. package/lib/commonjs/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.js.map +1 -1
  24. package/lib/commonjs/components/MessageInput/hooks/useAudioRecorder.js +2 -2
  25. package/lib/commonjs/components/MessageInput/hooks/useAudioRecorder.js.map +1 -1
  26. package/lib/commonjs/components/MessageList/InlineUnreadIndicator.js +2 -2
  27. package/lib/commonjs/components/MessageList/InlineUnreadIndicator.js.map +1 -1
  28. package/lib/commonjs/components/MessageList/MessageList.js +1 -4
  29. package/lib/commonjs/components/MessageList/MessageList.js.map +1 -1
  30. package/lib/commonjs/components/MessageList/hooks/useMessageListFocus.js +18 -2
  31. package/lib/commonjs/components/MessageList/hooks/useMessageListFocus.js.map +1 -1
  32. package/lib/commonjs/components/Thread/components/ThreadFooterComponent.js +1 -2
  33. package/lib/commonjs/components/Thread/components/ThreadFooterComponent.js.map +1 -1
  34. package/lib/commonjs/contexts/channelPinnedMessageListContext/ChannelPinnedMessageListContext.js +0 -15
  35. package/lib/commonjs/contexts/channelPinnedMessageListContext/ChannelPinnedMessageListContext.js.map +1 -1
  36. package/lib/commonjs/contexts/messageContext/MessageContext.js.map +1 -1
  37. package/lib/commonjs/contexts/messageListItemContext/MessageListItemContext.js.map +1 -1
  38. package/lib/commonjs/index.js +0 -12
  39. package/lib/commonjs/index.js.map +1 -1
  40. package/lib/commonjs/types/types.js.map +1 -1
  41. package/lib/commonjs/version.json +1 -1
  42. package/lib/module/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js +4 -1
  43. package/lib/module/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js.map +1 -1
  44. package/lib/module/components/Channel/hooks/useChannelLifecycle.js +2 -2
  45. package/lib/module/components/Channel/hooks/useChannelLifecycle.js.map +1 -1
  46. package/lib/module/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.js +7 -0
  47. package/lib/module/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.js.map +1 -1
  48. package/lib/module/components/ChannelPreview/ChannelPreviewMessage.js +4 -5
  49. package/lib/module/components/ChannelPreview/ChannelPreviewMessage.js.map +1 -1
  50. package/lib/module/components/Message/Message.js.map +1 -1
  51. package/lib/module/components/Message/MessageItemView/Headers/SentToChannelHeader.js +6 -32
  52. package/lib/module/components/Message/MessageItemView/Headers/SentToChannelHeader.js.map +1 -1
  53. package/lib/module/components/Message/MessageItemView/MessageContent.js +2 -2
  54. package/lib/module/components/Message/MessageItemView/MessageContent.js.map +1 -1
  55. package/lib/module/components/Message/MessageItemView/MessageStatus.js +29 -9
  56. package/lib/module/components/Message/MessageItemView/MessageStatus.js.map +1 -1
  57. package/lib/module/components/Message/hooks/useOpenThreadAtMessage.js +57 -0
  58. package/lib/module/components/Message/hooks/useOpenThreadAtMessage.js.map +1 -0
  59. package/lib/module/components/Message/utils/messageActions.js +1 -1
  60. package/lib/module/components/Message/utils/messageActions.js.map +1 -1
  61. package/lib/module/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.js +12 -8
  62. package/lib/module/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.js.map +1 -1
  63. package/lib/module/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.js +3 -2
  64. package/lib/module/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.js.map +1 -1
  65. package/lib/module/components/MessageInput/hooks/useAudioRecorder.js +2 -2
  66. package/lib/module/components/MessageInput/hooks/useAudioRecorder.js.map +1 -1
  67. package/lib/module/components/MessageList/InlineUnreadIndicator.js +2 -2
  68. package/lib/module/components/MessageList/InlineUnreadIndicator.js.map +1 -1
  69. package/lib/module/components/MessageList/MessageList.js +1 -4
  70. package/lib/module/components/MessageList/MessageList.js.map +1 -1
  71. package/lib/module/components/MessageList/hooks/useMessageListFocus.js +18 -2
  72. package/lib/module/components/MessageList/hooks/useMessageListFocus.js.map +1 -1
  73. package/lib/module/components/Thread/components/ThreadFooterComponent.js +1 -2
  74. package/lib/module/components/Thread/components/ThreadFooterComponent.js.map +1 -1
  75. package/lib/module/contexts/channelPinnedMessageListContext/ChannelPinnedMessageListContext.js +0 -15
  76. package/lib/module/contexts/channelPinnedMessageListContext/ChannelPinnedMessageListContext.js.map +1 -1
  77. package/lib/module/contexts/messageContext/MessageContext.js.map +1 -1
  78. package/lib/module/contexts/messageListItemContext/MessageListItemContext.js.map +1 -1
  79. package/lib/module/index.js +0 -12
  80. package/lib/module/index.js.map +1 -1
  81. package/lib/module/types/types.js.map +1 -1
  82. package/lib/module/version.json +1 -1
  83. package/lib/typescript/ai-components/services/media-picker-service/index.d.ts +1 -1
  84. package/lib/typescript/components/Channel/hooks/useChannelLifecycle.d.ts.map +1 -1
  85. package/lib/typescript/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.d.ts.map +1 -1
  86. package/lib/typescript/components/ChannelPreview/ChannelPreviewMessage.d.ts.map +1 -1
  87. package/lib/typescript/components/Message/Message.d.ts +2 -1
  88. package/lib/typescript/components/Message/Message.d.ts.map +1 -1
  89. package/lib/typescript/components/Message/MessageItemView/Headers/SentToChannelHeader.d.ts.map +1 -1
  90. package/lib/typescript/components/Message/MessageItemView/MessageContent.d.ts.map +1 -1
  91. package/lib/typescript/components/Message/MessageItemView/MessageStatus.d.ts +6 -0
  92. package/lib/typescript/components/Message/MessageItemView/MessageStatus.d.ts.map +1 -1
  93. package/lib/typescript/components/Message/hooks/useOpenThreadAtMessage.d.ts +14 -0
  94. package/lib/typescript/components/Message/hooks/useOpenThreadAtMessage.d.ts.map +1 -0
  95. package/lib/typescript/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.d.ts.map +1 -1
  96. package/lib/typescript/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.d.ts.map +1 -1
  97. package/lib/typescript/components/MessageList/MessageList.d.ts.map +1 -1
  98. package/lib/typescript/components/MessageList/hooks/useMessageListFocus.d.ts +5 -2
  99. package/lib/typescript/components/MessageList/hooks/useMessageListFocus.d.ts.map +1 -1
  100. package/lib/typescript/components/Thread/components/ThreadFooterComponent.d.ts.map +1 -1
  101. package/lib/typescript/contexts/channelPinnedMessageListContext/ChannelPinnedMessageListContext.d.ts.map +1 -1
  102. package/lib/typescript/contexts/messageContext/MessageContext.d.ts +2 -1
  103. package/lib/typescript/contexts/messageContext/MessageContext.d.ts.map +1 -1
  104. package/lib/typescript/contexts/messageListItemContext/MessageListItemContext.d.ts +10 -6
  105. package/lib/typescript/contexts/messageListItemContext/MessageListItemContext.d.ts.map +1 -1
  106. package/lib/typescript/i18n/keys.d.ts +2 -2
  107. package/lib/typescript/i18n/keys.d.ts.map +1 -1
  108. package/lib/typescript/index.d.ts +0 -1
  109. package/lib/typescript/index.d.ts.map +1 -1
  110. package/lib/typescript/types/types.d.ts +0 -4
  111. package/lib/typescript/types/types.d.ts.map +1 -1
  112. package/package.json +2 -2
  113. package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx +1 -1
  114. package/src/components/AttachmentPicker/components/AttachmentMediaPicker/__tests__/AttachmentPickerItem.test.tsx +29 -1
  115. package/src/components/Channel/__tests__/Channel.test.tsx +40 -7
  116. package/src/components/Channel/hooks/useChannelLifecycle.ts +8 -4
  117. package/src/components/Channel/hooks/useCreateOwnCapabilitiesContext.ts +1 -1
  118. package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx +8 -0
  119. package/src/components/ChannelPreview/ChannelPreviewMessage.tsx +2 -7
  120. package/src/components/ChannelPreview/__tests__/ChannelPreviewView.test.tsx +41 -0
  121. package/src/components/Message/Message.tsx +5 -2
  122. package/src/components/Message/MessageItemView/Headers/SentToChannelHeader.tsx +4 -32
  123. package/src/components/Message/MessageItemView/MessageContent.tsx +3 -2
  124. package/src/components/Message/MessageItemView/MessageStatus.tsx +43 -4
  125. package/src/components/Message/MessageItemView/__tests__/MessageContent.test.tsx +68 -1
  126. package/src/components/Message/MessageItemView/__tests__/MessageStatus.test.tsx +57 -0
  127. package/src/components/Message/hooks/useOpenThreadAtMessage.ts +50 -0
  128. package/src/components/Message/utils/__tests__/messageActions.test.ts +57 -0
  129. package/src/components/Message/utils/messageActions.ts +1 -1
  130. package/src/components/MessageInput/__tests__/AttachmentUploadPreviewList.test.tsx +34 -6
  131. package/src/components/MessageInput/__tests__/AudioAttachmentUploadPreview.test.tsx +24 -3
  132. package/src/components/MessageInput/__tests__/SendMessageDisallowedIndicator.test.tsx +3 -6
  133. package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.tsx +19 -8
  134. package/src/components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview.tsx +2 -5
  135. package/src/components/MessageInput/hooks/useAudioRecorder.tsx +2 -2
  136. package/src/components/MessageList/InlineUnreadIndicator.tsx +2 -2
  137. package/src/components/MessageList/MessageList.tsx +1 -5
  138. package/src/components/MessageList/__tests__/InlineUnreadIndicator.test.tsx +2 -2
  139. package/src/components/MessageList/__tests__/MessageList.test.tsx +163 -0
  140. package/src/components/MessageList/hooks/useMessageListFocus.ts +22 -2
  141. package/src/components/Thread/__tests__/Thread.test.tsx +46 -1
  142. package/src/components/Thread/components/ThreadFooterComponent.tsx +0 -1
  143. package/src/contexts/channelPinnedMessageListContext/ChannelPinnedMessageListContext.tsx +5 -14
  144. package/src/contexts/messageContext/MessageContext.tsx +2 -1
  145. package/src/contexts/messageListItemContext/MessageListItemContext.tsx +15 -6
  146. package/src/i18n/__tests__/catalog.fixture.json +2 -2
  147. package/src/i18n/keys.ts +2 -2
  148. package/src/index.ts +0 -1
  149. package/src/types/stream-chat-common-custom-data.d.ts +0 -3
  150. package/src/types/types.ts +0 -5
  151. package/src/version.json +1 -1
  152. package/lib/commonjs/utils/setupCommandUIMiddlewares.js +0 -37
  153. package/lib/commonjs/utils/setupCommandUIMiddlewares.js.map +0 -1
  154. package/lib/module/utils/setupCommandUIMiddlewares.js +0 -37
  155. package/lib/module/utils/setupCommandUIMiddlewares.js.map +0 -1
  156. package/lib/typescript/utils/setupCommandUIMiddlewares.d.ts +0 -3
  157. package/lib/typescript/utils/setupCommandUIMiddlewares.d.ts.map +0 -1
  158. package/src/utils/__tests__/setupCommandUIMiddlewares.test.ts +0 -49
  159. package/src/utils/setupCommandUIMiddlewares.ts +0 -34
@@ -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(
@@ -1416,6 +1416,169 @@ describe('MessageList in a thread', () => {
1416
1416
  });
1417
1417
  expect(queryAllByLabelText('Inline unread indicator')).toHaveLength(0);
1418
1418
  });
1419
+
1420
+ // A thread opened without its replies shows only its parent message until the first page lands.
1421
+ // Autoscroll has to be on by then, or the list keeps the parent in view and the replies render out
1422
+ // of view below it.
1423
+ it('autoscrolls to the replies that arrive after the thread opened empty', async () => {
1424
+ const mockedChannel = generateChannelResponse();
1425
+ const chatClient = await getTestClientWithUser({ id: 'testID' } as UserResponse);
1426
+ useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]);
1427
+ const channel = chatClient.channel('messaging', mockedChannel.channel.id);
1428
+ await channel.watch();
1429
+ const parentMessage = channel.state.formatMessage(generateMessage({ text: 'parent' }));
1430
+ const threadInstance = new Thread({ channel, client: chatClient, parentMessage });
1431
+ // The replies arrive from the test below, not from a query.
1432
+ jest.spyOn(threadInstance, 'reload').mockResolvedValue(undefined);
1433
+ jest.spyOn(threadInstance.messagePaginator, 'toTail').mockResolvedValue(undefined);
1434
+ const { getByTestId, getByText } = render(
1435
+ <OverlayProvider>
1436
+ <Chat client={chatClient}>
1437
+ <ThreadComponent thread={threadInstance}>
1438
+ <MessageList />
1439
+ </ThreadComponent>
1440
+ </Chat>
1441
+ </OverlayProvider>,
1442
+ );
1443
+ const autoscrollThreshold = () =>
1444
+ getByTestId('message-flat-list').props.maintainVisibleContentPosition
1445
+ ?.autoscrollToTopThreshold;
1446
+ await waitFor(() => expect(autoscrollThreshold()).toBeDefined());
1447
+
1448
+ const replies = Array.from({ length: 3 }, (_, i) =>
1449
+ channel.state.formatMessage(
1450
+ generateMessage({ parent_id: parentMessage.id, text: `reply-${i}` }),
1451
+ ),
1452
+ ) as LocalMessage[];
1453
+ act(() =>
1454
+ threadInstance.messagePaginator.setItems({
1455
+ valueOrFactory: replies,
1456
+ isFirstPage: true,
1457
+ isLastPage: true,
1458
+ }),
1459
+ );
1460
+
1461
+ await waitFor(() => expect(getByText('reply-2')).toBeTruthy());
1462
+ expect(autoscrollThreshold()).toBeDefined();
1463
+ });
1464
+
1465
+ it.each([
1466
+ ['scrolls to', 'testID', 1],
1467
+ ['does not scroll to', 'other-user', 0],
1468
+ ])(
1469
+ '%s the latest reply when a new reply is the user’s own (%s)',
1470
+ async (_, userId, expectedScrolls) => {
1471
+ const { channel, chatClient, getByText, threadInstance } = await setup();
1472
+ const scrollToOffset = jest.spyOn(FlatList.prototype, 'scrollToOffset');
1473
+
1474
+ const reply = channel.state.formatMessage(
1475
+ generateMessage({
1476
+ cid: channel.cid,
1477
+ parent_id: threadInstance.id,
1478
+ text: 'new reply',
1479
+ timestamp: new Date(),
1480
+ user: generateUser({ id: userId }),
1481
+ }),
1482
+ ) as LocalMessage;
1483
+ await act(async () => {
1484
+ threadInstance.messagePaginator.ingestItem(reply);
1485
+ await new Promise((resolve) => setTimeout(resolve, 0));
1486
+ });
1487
+
1488
+ await waitFor(() => expect(getByText('new reply')).toBeTruthy());
1489
+ expect(chatClient.userId).toBe('testID');
1490
+ expect(scrollToOffset).toHaveBeenCalledTimes(expectedScrolls);
1491
+ if (expectedScrolls) {
1492
+ expect(scrollToOffset).toHaveBeenCalledWith({ animated: true, offset: 0 });
1493
+ }
1494
+ },
1495
+ );
1496
+ });
1497
+
1498
+ // Pressing a quote goes to the quoted message. A quoted reply that lives only in another thread is
1499
+ // not in this list, so its thread is opened at it instead.
1500
+ describe('MessageList quote press', () => {
1501
+ afterEach(() => {
1502
+ cleanup();
1503
+ jest.restoreAllMocks();
1504
+ });
1505
+
1506
+ const setup = async ({ quoteThreadOnlyReply }: { quoteThreadOnlyReply: boolean }) => {
1507
+ const user = generateUser();
1508
+ const base = new Date('2020-01-01T00:00:00.000Z').getTime();
1509
+ const parentMessage = generateMessage({ timestamp: new Date(base), user });
1510
+ const quotedMessage = generateMessage({
1511
+ parent_id: quoteThreadOnlyReply ? parentMessage.id : undefined,
1512
+ text: 'quoted text',
1513
+ timestamp: new Date(base + 1000),
1514
+ user,
1515
+ });
1516
+ const mockedChannel = generateChannelResponse({
1517
+ members: [generateMember({ user })],
1518
+ messages: [
1519
+ parentMessage,
1520
+ generateMessage({
1521
+ quoted_message: quotedMessage,
1522
+ quoted_message_id: quotedMessage.id,
1523
+ timestamp: new Date(base + 2000),
1524
+ user,
1525
+ }),
1526
+ ],
1527
+ });
1528
+ const chatClient = await getTestClientWithUser({ id: 'testID' } as UserResponse);
1529
+ useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]);
1530
+ const channel = chatClient.channel('messaging', mockedChannel.channel.id);
1531
+ await channel.watch();
1532
+ const thread = chatClient.threads.ensure({
1533
+ channel,
1534
+ parentMessage: channel.state.formatMessage(parentMessage),
1535
+ });
1536
+ const jumpToMessage = jest
1537
+ .spyOn(channel.messagePaginator, 'jumpToMessage')
1538
+ .mockResolvedValue(true);
1539
+ const onAlsoSentToChannelHeaderPress = jest.fn();
1540
+
1541
+ const utils = render(
1542
+ <OverlayProvider>
1543
+ <Chat client={chatClient}>
1544
+ <Channel
1545
+ channel={channel}
1546
+ onAlsoSentToChannelHeaderPress={onAlsoSentToChannelHeaderPress}
1547
+ >
1548
+ <MessageList />
1549
+ </Channel>
1550
+ </Chat>
1551
+ </OverlayProvider>,
1552
+ );
1553
+ return { ...utils, jumpToMessage, onAlsoSentToChannelHeaderPress, quotedMessage, thread };
1554
+ };
1555
+
1556
+ it('opens the thread of a quoted reply that lives only in that thread', async () => {
1557
+ const { findByText, jumpToMessage, onAlsoSentToChannelHeaderPress, quotedMessage, thread } =
1558
+ await setup({ quoteThreadOnlyReply: true });
1559
+
1560
+ fireEvent.press(await findByText('quoted text'));
1561
+
1562
+ await waitFor(() =>
1563
+ expect(onAlsoSentToChannelHeaderPress).toHaveBeenCalledWith({
1564
+ targetedMessageId: quotedMessage.id,
1565
+ thread,
1566
+ }),
1567
+ );
1568
+ expect(jumpToMessage).not.toHaveBeenCalled();
1569
+ });
1570
+
1571
+ it('jumps to a quoted message that is in the channel', async () => {
1572
+ const { findByText, jumpToMessage, onAlsoSentToChannelHeaderPress, quotedMessage } =
1573
+ await setup({ quoteThreadOnlyReply: false });
1574
+
1575
+ fireEvent.press(await findByText('quoted text'));
1576
+
1577
+ await waitFor(() =>
1578
+ expect(jumpToMessage).toHaveBeenCalledWith(quotedMessage.id, expect.anything()),
1579
+ );
1580
+ expect(onAlsoSentToChannelHeaderPress).not.toHaveBeenCalled();
1581
+ });
1419
1582
  });
1420
1583
 
1421
1584
  describe('MessageList optimistic edit', () => {
@@ -2,7 +2,11 @@ import { useEffect } from 'react';
2
2
 
3
3
  import type { MessagePaginator } from 'stream-chat';
4
4
 
5
+ import type { GoToMessage } from '../../../contexts/messageListItemContext/MessageListItemContext';
6
+ import { useMessagesContext } from '../../../contexts/messagesContext/MessagesContext';
7
+ import { useThreadContext } from '../../../contexts/threadContext/ThreadContext';
5
8
  import { useStableCallback, useStateStore } from '../../../hooks';
9
+ import { useOpenThreadAtMessage } from '../../Message/hooks/useOpenThreadAtMessage';
6
10
 
7
11
  /** How long, in milliseconds, a message the list jumped to stays highlighted. */
8
12
  export const DEFAULT_HIGHLIGHT_DURATION = 3000;
@@ -23,7 +27,9 @@ const messageFocusSelector = (state: {
23
27
  * still running when the user navigates away would scroll and highlight again on return.
24
28
  *
25
29
  * @param paginator The channel's or the open thread's paginator.
26
- * @returns `focusedMessageId`, `focusToken` and `goToMessage`, which jumps to a message by ID.
30
+ * @returns `focusedMessageId`, `focusToken` and `goToMessage`, which jumps to a message by ID. A
31
+ * message passed along that is a reply living only in another thread is not in this list, so its
32
+ * thread is opened at it instead (through `onAlsoSentToChannelHeaderPress`, when set).
27
33
  */
28
34
  export const useMessageListFocus = (paginator?: MessagePaginator) => {
29
35
  const { focusedMessageId, focusToken } =
@@ -31,7 +37,21 @@ export const useMessageListFocus = (paginator?: MessagePaginator) => {
31
37
 
32
38
  useEffect(() => () => paginator?.clearMessageFocusSignal(), [paginator]);
33
39
 
34
- const goToMessage = useStableCallback(async (messageId: string) => {
40
+ const { onAlsoSentToChannelHeaderPress } = useMessagesContext();
41
+ const { thread } = useThreadContext();
42
+ const openThreadAtMessage = useOpenThreadAtMessage();
43
+
44
+ const goToMessage: GoToMessage = useStableCallback(async (messageId, message) => {
45
+ const parentId = message?.parent_id;
46
+ if (
47
+ onAlsoSentToChannelHeaderPress &&
48
+ parentId &&
49
+ parentId !== thread?.id &&
50
+ !message.show_in_channel
51
+ ) {
52
+ await openThreadAtMessage({ parentId, targetedMessageId: messageId });
53
+ return;
54
+ }
35
55
  await paginator?.jumpToMessage(messageId, {
36
56
  focusReason: 'jump-to-message',
37
57
  focusSignalTtlMs: DEFAULT_HIGHLIGHT_DURATION,
@@ -1,6 +1,14 @@
1
1
  import React from 'react';
2
2
 
3
- import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
3
+ import {
4
+ act,
5
+ cleanup,
6
+ fireEvent,
7
+ render,
8
+ screen,
9
+ waitFor,
10
+ within,
11
+ } from '@testing-library/react-native';
4
12
  import type {
5
13
  Channel as ChannelType,
6
14
  MessageResponse,
@@ -212,6 +220,43 @@ describe('Thread', () => {
212
220
  // already has its data doesn't.
213
221
  // An activation that is never released keeps the thread active for the session, so recovery
214
222
  // keeps reloading it and it keeps being read automatically.
223
+ it.each([
224
+ ['shows', true],
225
+ ['does not show', false],
226
+ ])(
227
+ '%s the delivery status of the parent message when it is the current user’s',
228
+ async (_, isOwn) => {
229
+ const parentMessage = generateMessage({
230
+ cid: channel.cid,
231
+ status: 'received',
232
+ text: 'Parent Message Text',
233
+ ...(isOwn ? { user: generateUser({ id: chatClient.userId }) } : {}),
234
+ });
235
+ const threadInstance = new ThreadClass({ channel, client: chatClient, parentMessage });
236
+ // A thread with its replies loaded is not reloaded on open (see "metadata reload on open").
237
+ threadInstance.messagePaginator.setItems({
238
+ valueOrFactory: [
239
+ channel.state.formatMessage(
240
+ generateMessage({
241
+ cid: channel.cid,
242
+ parent_id: parentMessage.id,
243
+ }) as unknown as MessageResponse,
244
+ ),
245
+ ],
246
+ isFirstPage: true,
247
+ isLastPage: true,
248
+ });
249
+
250
+ renderComponent({ chatClient, thread: threadInstance });
251
+
252
+ const header = await screen.findByTestId('thread-footer-component');
253
+ expect(within(header).getByText('Parent Message Text')).toBeTruthy();
254
+ expect(within(header).queryByTestId('message-status-sent')).toBe(
255
+ isOwn ? within(header).getByTestId('message-status-sent') : null,
256
+ );
257
+ },
258
+ );
259
+
215
260
  it('releases its activation of the thread on unmount', async () => {
216
261
  const parentMessage = generateMessage({ cid: 'messaging:test-channel', text: 'Parent' });
217
262
  const threadInstance = chatClient.threads.ensure({ channel, parentMessage });
@@ -67,7 +67,6 @@ const ThreadFooterComponentWithContext = (props: ThreadFooterComponentPropsWithC
67
67
  groupStyles={['single']}
68
68
  message={parentMessage}
69
69
  preventPress={parentMessagePreventPress}
70
- showMessageStatus={false}
71
70
  />
72
71
  <View style={styles.newThread}>
73
72
  <Text style={styles.text}>
@@ -21,20 +21,11 @@ export const ChannelPinnedMessageListProvider = ({
21
21
  }: PropsWithChildren<{ channel: Channel; searchSource?: MessageSearchSource }>) => {
22
22
  const { client } = useChatContext();
23
23
  const [defaultSearchSource] = useState(() => {
24
- const source = new MessageSearchSource(
25
- client,
26
- { pageSize: 25, allowEmptySearchString: true, resetOnNewSearchQuery: false },
27
- {
28
- messageSearch: {
29
- initialFilterConfig: {
30
- $or: {
31
- enabled: true,
32
- generate: ({ searchQuery }) => (searchQuery ? { text: { $q: searchQuery } } : {}),
33
- },
34
- },
35
- },
36
- },
37
- );
24
+ const source = new MessageSearchSource(client, {
25
+ pageSize: 25,
26
+ allowEmptySearchString: true,
27
+ resetOnNewSearchQuery: false,
28
+ });
38
29
  source.messageSearchChannelFilters = { cid: channel.cid, members: undefined };
39
30
  source.messageSearchFilters = { pinned: true, type: undefined };
40
31
  source.activate();
@@ -19,6 +19,7 @@ import type { Rect } from '../../state-store/message-overlay-store';
19
19
  import type { ViewRef } from '../../types/react-native-compat';
20
20
 
21
21
  import type { MessageComposerAPIContextValue } from '../messageComposerContext/MessageComposerAPIContext';
22
+ import type { GoToMessage } from '../messageListItemContext/MessageListItemContext';
22
23
  import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue';
23
24
 
24
25
  export type Alignment = 'right' | 'left';
@@ -113,7 +114,7 @@ export type MessageContextValue = {
113
114
  showMessageStatus: boolean;
114
115
  /** The video attachments of the message. */
115
116
  videos: Attachment[];
116
- goToMessage?: (messageId: string) => void;
117
+ goToMessage?: GoToMessage;
117
118
  /**
118
119
  * Handles a reaction on the message.
119
120
  *
@@ -1,18 +1,27 @@
1
1
  import React, { createContext, PropsWithChildren, useContext } from 'react';
2
2
 
3
+ import type { LocalMessage } from 'stream-chat';
4
+
3
5
  import { MessageListProps } from '../../components/MessageList/MessageList';
4
6
 
5
7
  import { Theme } from '../themeContext/utils/theme';
6
8
  import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue';
7
9
  import { isTestEnvironment } from '../utils/isTestEnvironment';
8
10
 
11
+ /**
12
+ * Goes to a message, used when a quoted message is pressed.
13
+ *
14
+ * @param messageId The ID of the message to go to.
15
+ * @param message The message, when the caller has it (the pressed quote passes its quoted message).
16
+ */
17
+ export type GoToMessage = (
18
+ messageId: string,
19
+ message?: NonNullable<LocalMessage['quoted_message']>,
20
+ ) => void;
21
+
9
22
  export type MessageListItemContextValue = {
10
- /**
11
- * Goes to a message, used when a quoted message is pressed.
12
- *
13
- * @param messageId The ID of the message to go to.
14
- */
15
- goToMessage: (messageId: string) => void;
23
+ /** Goes to a message, used when a quoted message is pressed. */
24
+ goToMessage: GoToMessage;
16
25
  /** The theme of the list items, with `myMessageTheme` merged in. */
17
26
  modifiedTheme: Theme;
18
27
  /** Turns off grouping of consecutive messages by user. */
@@ -295,8 +295,8 @@
295
295
  "messageInput.sendMessage.accessibilityLabel": "Send message",
296
296
  "messageInput.sendMessageDisallowed.text": "You can't send messages in this channel",
297
297
  "messageList.dismissUnread.accessibilityLabel": "Dismiss unread messages",
298
- "messageList.newMessages.text_one": "{{count}} new message",
299
- "messageList.newMessages.text_other": "{{count}} new messages",
298
+ "messageList.newMessages.text_one": "{{count}} unread message",
299
+ "messageList.newMessages.text_other": "{{count}} unread messages",
300
300
  "messageList.scrollToBottom.accessibilityLabel": "Scroll to bottom",
301
301
  "messageList.scrollToBottom.withCount.accessibilityLabel_one": "Scroll to bottom, {{count}} new message",
302
302
  "messageList.scrollToBottom.withCount.accessibilityLabel_other": "Scroll to bottom, {{count}} new messages",
package/src/i18n/keys.ts CHANGED
@@ -306,8 +306,8 @@ export type TranslationCatalog = {
306
306
  'messageInput.sendMessage.accessibilityLabel': 'Send message';
307
307
  'messageInput.sendMessageDisallowed.text': "You can't send messages in this channel";
308
308
  'messageList.dismissUnread.accessibilityLabel': 'Dismiss unread messages';
309
- 'messageList.newMessages.text_one': '{{count}} new message';
310
- 'messageList.newMessages.text_other': '{{count}} new messages';
309
+ 'messageList.newMessages.text_one': '{{count}} unread message';
310
+ 'messageList.newMessages.text_other': '{{count}} unread messages';
311
311
  'messageList.scrollToBottom.accessibilityLabel': 'Scroll to bottom';
312
312
  'messageList.scrollToBottom.withCount.accessibilityLabel_one': 'Scroll to bottom, {{count}} new message';
313
313
  'messageList.scrollToBottom.withCount.accessibilityLabel_other': 'Scroll to bottom, {{count}} new messages';
package/src/index.ts CHANGED
@@ -23,7 +23,6 @@ export * from './types/types';
23
23
  export * from './utils/attachmentUrls';
24
24
  export * from './utils/i18n/Streami18n';
25
25
  export * from './i18n';
26
- export * from './utils/setupCommandUIMiddlewares';
27
26
  export * from './utils/createGenerateVideoThumbnails';
28
27
  export * from './utils/utils';
29
28
  export * from './nativeMultipartUpload';
@@ -3,7 +3,6 @@ import 'stream-chat';
3
3
  import {
4
4
  DefaultAttachmentData,
5
5
  DefaultChannelData,
6
- DefaultCommandData,
7
6
  DefaultEventData,
8
7
  DefaultMemberData,
9
8
  DefaultMessageData,
@@ -21,8 +20,6 @@ declare module 'stream-chat' {
21
20
 
22
21
  interface CustomChannelData extends DefaultChannelData {}
23
22
 
24
- interface CustomCommandData extends DefaultCommandData {}
25
-
26
23
  interface CustomEventData extends DefaultEventData {}
27
24
 
28
25
  interface CustomMemberData extends DefaultMemberData {}
@@ -60,11 +60,6 @@ export interface DefaultChannelData {
60
60
  name?: string;
61
61
  }
62
62
 
63
- export interface DefaultCommandData {
64
- flag: unknown;
65
- imgur: unknown;
66
- }
67
-
68
63
  /* eslint-disable @typescript-eslint/no-empty-object-type */
69
64
 
70
65
  export interface DefaultEventData {}
package/src/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "10.0.0-rc.4"
2
+ "version": "10.0.0-rc.5"
3
3
  }
@@ -1,37 +0,0 @@
1
- Object.defineProperty(exports, "__esModule", {
2
- value: true
3
- });
4
- exports.setupCommandUIMiddlewares = void 0;
5
- var _streamChat = require("stream-chat");
6
- var setupCommandUIMiddlewares = messageComposer => {
7
- messageComposer.compositionMiddlewareExecutor.insert({
8
- middleware: [(0, _streamChat.createCommandInjectionMiddleware)(messageComposer)],
9
- position: {
10
- after: 'stream-io/message-composer-middleware/attachments'
11
- },
12
- unique: true
13
- });
14
- messageComposer.draftCompositionMiddlewareExecutor.insert({
15
- middleware: [(0, _streamChat.createDraftCommandInjectionMiddleware)(messageComposer)],
16
- position: {
17
- after: 'stream-io/message-composer-middleware/draft-attachments'
18
- },
19
- unique: true
20
- });
21
- messageComposer.textComposer.middlewareExecutor.insert({
22
- middleware: [(0, _streamChat.createActiveCommandGuardMiddleware)()],
23
- position: {
24
- before: 'stream-io/text-composer/commands-middleware'
25
- },
26
- unique: true
27
- });
28
- messageComposer.textComposer.middlewareExecutor.insert({
29
- middleware: [(0, _streamChat.createCommandStringExtractionMiddleware)()],
30
- position: {
31
- after: 'stream-io/text-composer/commands-middleware'
32
- },
33
- unique: true
34
- });
35
- };
36
- exports.setupCommandUIMiddlewares = setupCommandUIMiddlewares;
37
- //# sourceMappingURL=setupCommandUIMiddlewares.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["_streamChat","require","setupCommandUIMiddlewares","messageComposer","compositionMiddlewareExecutor","insert","middleware","createCommandInjectionMiddleware","position","after","unique","draftCompositionMiddlewareExecutor","createDraftCommandInjectionMiddleware","textComposer","middlewareExecutor","createActiveCommandGuardMiddleware","before","createCommandStringExtractionMiddleware","exports"],"sourceRoot":"../../../src","sources":["utils/setupCommandUIMiddlewares.ts"],"mappings":";;;;AAAA,IAAAA,WAAA,GAAAC,OAAA;AASO,IAAMC,yBAAyB,GAAIC,eAAgC,IAAK;EAC7EA,eAAe,CAACC,6BAA6B,CAACC,MAAM,CAAC;IACnDC,UAAU,EAAE,CAAC,IAAAC,4CAAgC,EAACJ,eAAe,CAAC,CAAC;IAC/DK,QAAQ,EAAE;MAAEC,KAAK,EAAE;IAAoD,CAAC;IACxEC,MAAM,EAAE;EACV,CAAC,CAAC;EAEFP,eAAe,CAACQ,kCAAkC,CAACN,MAAM,CAAC;IACxDC,UAAU,EAAE,CAAC,IAAAM,iDAAqC,EAACT,eAAe,CAAC,CAAC;IACpEK,QAAQ,EAAE;MAAEC,KAAK,EAAE;IAA0D,CAAC;IAC9EC,MAAM,EAAE;EACV,CAAC,CAAC;EAEFP,eAAe,CAACU,YAAY,CAACC,kBAAkB,CAACT,MAAM,CAAC;IACrDC,UAAU,EAAE,CAAC,IAAAS,8CAAkC,EAAC,CAAC,CAA2B;IAC5EP,QAAQ,EAAE;MAAEQ,MAAM,EAAE;IAA8C,CAAC;IACnEN,MAAM,EAAE;EACV,CAAC,CAAC;EAEFP,eAAe,CAACU,YAAY,CAACC,kBAAkB,CAACT,MAAM,CAAC;IACrDC,UAAU,EAAE,CAAC,IAAAW,mDAAuC,EAAC,CAAC,CAA2B;IACjFT,QAAQ,EAAE;MAAEC,KAAK,EAAE;IAA8C,CAAC;IAClEC,MAAM,EAAE;EACV,CAAC,CAAC;AACJ,CAAC;AAACQ,OAAA,CAAAhB,yBAAA,GAAAA,yBAAA","ignoreList":[]}