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
@@ -126,6 +126,47 @@ describe('ChannelPreviewView', () => {
126
126
  await waitFor(() => screen.queryByText(expectedDisplayName));
127
127
  });
128
128
 
129
+ describe('the sender of the latest message', () => {
130
+ const otherUser = generateUser({ name: 'Count Dooku' });
131
+
132
+ const renderWithLatestMessage = async (latestMessage: ReturnType<typeof generateMessage>) => {
133
+ await initializeChannel(
134
+ generateChannelResponse({
135
+ channel: { custom: { name: 'group' } },
136
+ members: [generateMember({ user: clientUser }), generateMember({ user: otherUser })],
137
+ messages: [latestMessage],
138
+ }),
139
+ );
140
+ render(
141
+ <OverlayProvider accessibility={{ enabled: true }}>
142
+ <Chat client={chatClient}>
143
+ <ChannelPreviewView
144
+ channel={channel as ChannelType}
145
+ lastMessage={latestMessage}
146
+ onSelect={jest.fn()}
147
+ />
148
+ </Chat>
149
+ </OverlayProvider>,
150
+ );
151
+ };
152
+
153
+ it.each([
154
+ ['an own message', clientUser, 'regular', 'You:', 1],
155
+ ['an own deleted message', clientUser, 'deleted', 'You:', 0],
156
+ ['a deleted message from another member', otherUser, 'deleted', 'Count Dooku:', 0],
157
+ ] as const)('names the sender of %s', async (_, user, type, prefix, statusLabels) => {
158
+ await renderWithLatestMessage(generateMessage({ status: 'received', type, user }));
159
+
160
+ // An own message's status hides its sender text from screen readers, so hidden elements count.
161
+ const sender = () =>
162
+ screen.getByTestId('channel-preview-sender', { includeHiddenElements: true });
163
+ await waitFor(() => expect(sender()).toBeTruthy());
164
+ expect(sender().props.children.join('')).toBe(prefix);
165
+ // The delivery status announces itself with "Sent by you" or "Read, sent by you".
166
+ expect(screen.queryAllByLabelText(/sent by you/i)).toHaveLength(statusLabels);
167
+ });
168
+ });
169
+
129
170
  it('should render latest message, truncated to length given by latestMessageLength', async () => {
130
171
  const message = generateMessage();
131
172
  await initializeChannel(generateChannelResponse());
@@ -47,7 +47,10 @@ import {
47
47
  MessageOverlayRuntimeProvider,
48
48
  MessageProvider,
49
49
  } from '../../contexts/messageContext/MessageContext';
50
- import { useMessageListItemContext } from '../../contexts/messageListItemContext/MessageListItemContext';
50
+ import {
51
+ GoToMessage,
52
+ useMessageListItemContext,
53
+ } from '../../contexts/messageListItemContext/MessageListItemContext';
51
54
  import {
52
55
  MessagesContextValue,
53
56
  useMessagesContext,
@@ -223,7 +226,7 @@ export type MessagePropsWithContext = Pick<
223
226
  messagesContext: MessagesContextValue;
224
227
  /** Whether long pressing a message is enabled. Defaults to true. */
225
228
  enableLongPress?: boolean;
226
- goToMessage?: (messageId: string) => void;
229
+ goToMessage?: GoToMessage;
227
230
  isTargetedMessage?: boolean;
228
231
  /**
229
232
  * Called with the message's thread when its replies button is pressed.
@@ -2,8 +2,6 @@ import React, { useMemo } from 'react';
2
2
 
3
3
  import { Pressable, StyleSheet, Text, View } from 'react-native';
4
4
 
5
- import { useChannelContext } from '../../../../contexts/channelContext/ChannelContext';
6
- import { useChatContext } from '../../../../contexts/chatContext/ChatContext';
7
5
  import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext';
8
6
  import { useMessageContext } from '../../../../contexts/messageContext/MessageContext';
9
7
  import { useMessagesContext } from '../../../../contexts/messagesContext/MessagesContext';
@@ -15,6 +13,7 @@ import {
15
13
  import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext';
16
14
  import { useStableCallback } from '../../../../hooks';
17
15
  import { primitives } from '../../../../theme';
16
+ import { useOpenThreadAtMessage } from '../../hooks/useOpenThreadAtMessage';
18
17
  import { useShouldUseOverlayStyles } from '../../hooks/useShouldUseOverlayStyles';
19
18
 
20
19
  type SentToChannelHeaderPropsWithContext = Pick<ThreadContextValue, 'thread'> & {
@@ -67,10 +66,9 @@ export type SentToChannelHeaderProps = Partial<SentToChannelHeaderPropsWithConte
67
66
 
68
67
  export const SentToChannelHeader = (props: SentToChannelHeaderProps) => {
69
68
  const { onAlsoSentToChannelHeaderPress } = useMessagesContext();
70
- const { client } = useChatContext();
71
- const { channel } = useChannelContext();
72
69
  const { thread } = useThreadContext();
73
70
  const { message } = useMessageContext();
71
+ const openThreadAtMessage = useOpenThreadAtMessage();
74
72
 
75
73
  const handleOnPress = useStableCallback(async () => {
76
74
  if (thread) {
@@ -78,34 +76,8 @@ export const SentToChannelHeader = (props: SentToChannelHeaderProps) => {
78
76
  return;
79
77
  }
80
78
 
81
- const parentId = message.parent_id;
82
- if (!parentId) {
83
- return;
84
- }
85
-
86
- // The parent is only fetched when neither its thread nor the message itself is loaded. Opening
87
- // the thread loads whatever else it is missing.
88
- const registeredThread = client.threads.get(parentId);
89
- if (registeredThread) {
90
- onAlsoSentToChannelHeaderPress?.({ targetedMessageId: message.id, thread: registeredThread });
91
- return;
92
- }
93
-
94
- try {
95
- const parentMessage =
96
- client.messageStore.get(parentId) ??
97
- (await channel.getMessagesById([parentId])).messages?.[0];
98
-
99
- if (!parentMessage) {
100
- return;
101
- }
102
-
103
- onAlsoSentToChannelHeaderPress?.({
104
- targetedMessageId: message.id,
105
- thread: client.threads.ensure({ channel, parentMessage }),
106
- });
107
- } catch (error) {
108
- console.error('Error querying parent message:', error);
79
+ if (message.parent_id) {
80
+ await openThreadAtMessage({ parentId: message.parent_id, targetedMessageId: message.id });
109
81
  }
110
82
  });
111
83
 
@@ -254,14 +254,15 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
254
254
 
255
255
  if (onPress) {
256
256
  onPress({
257
- defaultHandler: () => goToMessage(message.quoted_message!.id),
257
+ defaultHandler: () =>
258
+ goToMessage(message.quoted_message!.id, message.quoted_message!),
258
259
  emitter: 'messageContent',
259
260
  event,
260
261
  });
261
262
  return;
262
263
  }
263
264
 
264
- goToMessage(message.quoted_message.id);
265
+ goToMessage(message.quoted_message.id, message.quoted_message);
265
266
  }}
266
267
  onPressIn={(event) => {
267
268
  if (onPressIn) {
@@ -1,13 +1,17 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import { StyleSheet, View } from 'react-native';
3
3
 
4
+ import type { ChannelConfig } from 'stream-chat';
5
+
4
6
  import { useA11yLabel } from '../../../a11y/hooks/useA11yLabel';
7
+ import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
5
8
  import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext';
6
9
  import {
7
10
  MessageContextValue,
8
11
  useMessageContext,
9
12
  } from '../../../contexts/messageContext/MessageContext';
10
13
  import { useTheme } from '../../../contexts/themeContext/ThemeContext';
14
+ import { useStateStore } from '../../../hooks/useStateStore';
11
15
  import { primitives } from '../../../theme';
12
16
  import { MessageStatusTypes } from '../../../utils/utils';
13
17
  import { HiddenA11yText } from '../../Accessibility/HiddenA11yText';
@@ -15,15 +19,27 @@ import { useIsMessageDeliveredToOthers } from '../hooks/useIsMessageDeliveredToO
15
19
  import { useIsMessageReadByOthers } from '../hooks/useIsMessageReadByOthers';
16
20
  import { useShouldUseOverlayStyles } from '../hooks/useShouldUseOverlayStyles';
17
21
 
22
+ const readEventsSelector = ({ readEvents }: ChannelConfig) => ({
23
+ readEventsEnabled: readEvents.enabled,
24
+ });
25
+
18
26
  export type MessageStatusPropsWithContext = Pick<MessageContextValue, 'message'> & {
19
27
  /** Whether the message reached at least one other member. */
20
28
  delivered: boolean;
21
29
  /** Whether at least one other member read the message. */
22
30
  read: boolean;
31
+ /**
32
+ * Whether the channel has read events. Without them only the sending status shows, as in the
33
+ * channel preview.
34
+ * @default true
35
+ */
36
+ readEventsEnabled?: boolean;
23
37
  };
24
38
 
25
39
  const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => {
26
- const { delivered, message, read } = props;
40
+ const { message, readEventsEnabled = true } = props;
41
+ const delivered = readEventsEnabled && props.delivered;
42
+ const read = readEventsEnabled && props.read;
27
43
 
28
44
  const styles = useStyles();
29
45
  const { icons } = useComponentsContext();
@@ -38,6 +54,7 @@ const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => {
38
54
 
39
55
  const sending = message.status === MessageStatusTypes.SENDING;
40
56
  const sent =
57
+ readEventsEnabled &&
41
58
  message.status === MessageStatusTypes.RECEIVED &&
42
59
  !delivered &&
43
60
  !read &&
@@ -55,7 +72,11 @@ const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => {
55
72
  : '',
56
73
  );
57
74
 
58
- if (message.status === MessageStatusTypes.FAILED || message.type === 'error') {
75
+ if (
76
+ message.status === MessageStatusTypes.FAILED ||
77
+ message.type === 'error' ||
78
+ message.type === 'deleted'
79
+ ) {
59
80
  return null;
60
81
  }
61
82
 
@@ -109,8 +130,23 @@ const areEqual = (
109
130
  prevProps: MessageStatusPropsWithContext,
110
131
  nextProps: MessageStatusPropsWithContext,
111
132
  ) => {
112
- const { delivered: prevDelivered, message: prevMessage, read: prevRead } = prevProps;
113
- const { delivered: nextDelivered, message: nextMessage, read: nextRead } = nextProps;
133
+ const {
134
+ delivered: prevDelivered,
135
+ message: prevMessage,
136
+ read: prevRead,
137
+ readEventsEnabled: prevReadEventsEnabled,
138
+ } = prevProps;
139
+ const {
140
+ delivered: nextDelivered,
141
+ message: nextMessage,
142
+ read: nextRead,
143
+ readEventsEnabled: nextReadEventsEnabled,
144
+ } = nextProps;
145
+
146
+ const readEventsEnabledEqual = prevReadEventsEnabled === nextReadEventsEnabled;
147
+ if (!readEventsEnabledEqual) {
148
+ return false;
149
+ }
114
150
 
115
151
  const deliveredEqual = prevDelivered === nextDelivered;
116
152
  if (!deliveredEqual) {
@@ -141,14 +177,17 @@ export type MessageStatusProps = Partial<MessageStatusPropsWithContext>;
141
177
  export const MessageStatus = (props: MessageStatusProps) => {
142
178
  const { message: contextMessage } = useMessageContext();
143
179
  const message = props.message ?? contextMessage;
180
+ const { channel } = useChannelContext();
144
181
  const readByOthers = useIsMessageReadByOthers({ message });
145
182
  const deliveredToOthers = useIsMessageDeliveredToOthers({ message });
183
+ const { readEventsEnabled } = useStateStore(channel?.configState, readEventsSelector) ?? {};
146
184
 
147
185
  return (
148
186
  <MemoizedMessageStatus
149
187
  delivered={props.delivered ?? deliveredToOthers}
150
188
  message={message}
151
189
  read={props.read ?? readByOthers}
190
+ readEventsEnabled={props.readEventsEnabled ?? readEventsEnabled}
152
191
  />
153
192
  );
154
193
  };
@@ -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