stream-chat-react-native-core 9.3.1-beta.7 → 9.3.1-beta.9

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 (220) hide show
  1. package/lib/commonjs/a11y/hooks/useAccessibilityServiceEnabled.js +43 -0
  2. package/lib/commonjs/a11y/hooks/useAccessibilityServiceEnabled.js.map +1 -0
  3. package/lib/commonjs/a11y/index.js +11 -0
  4. package/lib/commonjs/a11y/index.js.map +1 -1
  5. package/lib/commonjs/components/Accessibility/OverlayA11yShield.js +45 -0
  6. package/lib/commonjs/components/Accessibility/OverlayA11yShield.js.map +1 -0
  7. package/lib/commonjs/components/Attachment/Gallery.js +10 -0
  8. package/lib/commonjs/components/Attachment/Gallery.js.map +1 -1
  9. package/lib/commonjs/components/ChannelPreview/ChannelPreviewStatus.js +12 -3
  10. package/lib/commonjs/components/ChannelPreview/ChannelPreviewStatus.js.map +1 -1
  11. package/lib/commonjs/components/ImageGallery/ImageGallery.js +56 -1
  12. package/lib/commonjs/components/ImageGallery/ImageGallery.js.map +1 -1
  13. package/lib/commonjs/components/Message/MessageItemView/Headers/MessageReminderHeader.js +2 -0
  14. package/lib/commonjs/components/Message/MessageItemView/Headers/MessageReminderHeader.js.map +1 -1
  15. package/lib/commonjs/components/Message/MessageItemView/Headers/SentToChannelHeader.js +2 -0
  16. package/lib/commonjs/components/Message/MessageItemView/Headers/SentToChannelHeader.js.map +1 -1
  17. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js +26 -5
  18. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js.map +1 -1
  19. package/lib/commonjs/components/Message/MessageItemView/MessageReplies.js +7 -3
  20. package/lib/commonjs/components/Message/MessageItemView/MessageReplies.js.map +1 -1
  21. package/lib/commonjs/components/Message/MessageItemView/MessageRepliesAvatars.js +2 -0
  22. package/lib/commonjs/components/Message/MessageItemView/MessageRepliesAvatars.js.map +1 -1
  23. package/lib/commonjs/components/Message/MessageItemView/MessageTextContainer.js +16 -6
  24. package/lib/commonjs/components/Message/MessageItemView/MessageTextContainer.js.map +1 -1
  25. package/lib/commonjs/components/Message/MessageItemView/ReactionList/ReactionListItem.js +4 -0
  26. package/lib/commonjs/components/Message/MessageItemView/ReactionList/ReactionListItem.js.map +1 -1
  27. package/lib/commonjs/components/Message/hooks/useCreateMessageContext.js +6 -1
  28. package/lib/commonjs/components/Message/hooks/useCreateMessageContext.js.map +1 -1
  29. package/lib/commonjs/components/MessageList/InlineDateSeparator.js +8 -1
  30. package/lib/commonjs/components/MessageList/InlineDateSeparator.js.map +1 -1
  31. package/lib/commonjs/components/MessageMenu/ReactionButton.js +0 -7
  32. package/lib/commonjs/components/MessageMenu/ReactionButton.js.map +1 -1
  33. package/lib/commonjs/components/Poll/Poll.js +2 -0
  34. package/lib/commonjs/components/Poll/Poll.js.map +1 -1
  35. package/lib/commonjs/components/Poll/components/PollOption.js +6 -1
  36. package/lib/commonjs/components/Poll/components/PollOption.js.map +1 -1
  37. package/lib/commonjs/contexts/accessibilityContext/AccessibilityContext.js +6 -22
  38. package/lib/commonjs/contexts/accessibilityContext/AccessibilityContext.js.map +1 -1
  39. package/lib/commonjs/contexts/messageContext/MessageContext.js.map +1 -1
  40. package/lib/commonjs/contexts/overlayContext/MessageOverlayHostLayer.js +1 -0
  41. package/lib/commonjs/contexts/overlayContext/MessageOverlayHostLayer.js.map +1 -1
  42. package/lib/commonjs/contexts/overlayContext/OverlayProvider.js +4 -1
  43. package/lib/commonjs/contexts/overlayContext/OverlayProvider.js.map +1 -1
  44. package/lib/commonjs/i18n/ar.json +7 -1
  45. package/lib/commonjs/i18n/en.json +7 -1
  46. package/lib/commonjs/i18n/es.json +7 -1
  47. package/lib/commonjs/i18n/fr.json +7 -1
  48. package/lib/commonjs/i18n/he.json +7 -1
  49. package/lib/commonjs/i18n/hi.json +7 -1
  50. package/lib/commonjs/i18n/it.json +7 -1
  51. package/lib/commonjs/i18n/ja.json +7 -1
  52. package/lib/commonjs/i18n/ko.json +7 -1
  53. package/lib/commonjs/i18n/nl.json +7 -1
  54. package/lib/commonjs/i18n/pt-br.json +7 -1
  55. package/lib/commonjs/i18n/ru.json +7 -1
  56. package/lib/commonjs/i18n/tr.json +7 -1
  57. package/lib/commonjs/state-store/image-gallery-state-store.js +13 -0
  58. package/lib/commonjs/state-store/image-gallery-state-store.js.map +1 -1
  59. package/lib/commonjs/utils/i18n/getDateString.js +26 -1
  60. package/lib/commonjs/utils/i18n/getDateString.js.map +1 -1
  61. package/lib/commonjs/version.json +1 -1
  62. package/lib/module/a11y/hooks/useAccessibilityServiceEnabled.js +43 -0
  63. package/lib/module/a11y/hooks/useAccessibilityServiceEnabled.js.map +1 -0
  64. package/lib/module/a11y/index.js +11 -0
  65. package/lib/module/a11y/index.js.map +1 -1
  66. package/lib/module/components/Accessibility/OverlayA11yShield.js +45 -0
  67. package/lib/module/components/Accessibility/OverlayA11yShield.js.map +1 -0
  68. package/lib/module/components/Attachment/Gallery.js +10 -0
  69. package/lib/module/components/Attachment/Gallery.js.map +1 -1
  70. package/lib/module/components/ChannelPreview/ChannelPreviewStatus.js +12 -3
  71. package/lib/module/components/ChannelPreview/ChannelPreviewStatus.js.map +1 -1
  72. package/lib/module/components/ImageGallery/ImageGallery.js +56 -1
  73. package/lib/module/components/ImageGallery/ImageGallery.js.map +1 -1
  74. package/lib/module/components/Message/MessageItemView/Headers/MessageReminderHeader.js +2 -0
  75. package/lib/module/components/Message/MessageItemView/Headers/MessageReminderHeader.js.map +1 -1
  76. package/lib/module/components/Message/MessageItemView/Headers/SentToChannelHeader.js +2 -0
  77. package/lib/module/components/Message/MessageItemView/Headers/SentToChannelHeader.js.map +1 -1
  78. package/lib/module/components/Message/MessageItemView/MessageContent.js +26 -5
  79. package/lib/module/components/Message/MessageItemView/MessageContent.js.map +1 -1
  80. package/lib/module/components/Message/MessageItemView/MessageReplies.js +7 -3
  81. package/lib/module/components/Message/MessageItemView/MessageReplies.js.map +1 -1
  82. package/lib/module/components/Message/MessageItemView/MessageRepliesAvatars.js +2 -0
  83. package/lib/module/components/Message/MessageItemView/MessageRepliesAvatars.js.map +1 -1
  84. package/lib/module/components/Message/MessageItemView/MessageTextContainer.js +16 -6
  85. package/lib/module/components/Message/MessageItemView/MessageTextContainer.js.map +1 -1
  86. package/lib/module/components/Message/MessageItemView/ReactionList/ReactionListItem.js +4 -0
  87. package/lib/module/components/Message/MessageItemView/ReactionList/ReactionListItem.js.map +1 -1
  88. package/lib/module/components/Message/hooks/useCreateMessageContext.js +6 -1
  89. package/lib/module/components/Message/hooks/useCreateMessageContext.js.map +1 -1
  90. package/lib/module/components/MessageList/InlineDateSeparator.js +8 -1
  91. package/lib/module/components/MessageList/InlineDateSeparator.js.map +1 -1
  92. package/lib/module/components/MessageMenu/ReactionButton.js +0 -7
  93. package/lib/module/components/MessageMenu/ReactionButton.js.map +1 -1
  94. package/lib/module/components/Poll/Poll.js +2 -0
  95. package/lib/module/components/Poll/Poll.js.map +1 -1
  96. package/lib/module/components/Poll/components/PollOption.js +6 -1
  97. package/lib/module/components/Poll/components/PollOption.js.map +1 -1
  98. package/lib/module/contexts/accessibilityContext/AccessibilityContext.js +6 -22
  99. package/lib/module/contexts/accessibilityContext/AccessibilityContext.js.map +1 -1
  100. package/lib/module/contexts/messageContext/MessageContext.js.map +1 -1
  101. package/lib/module/contexts/overlayContext/MessageOverlayHostLayer.js +1 -0
  102. package/lib/module/contexts/overlayContext/MessageOverlayHostLayer.js.map +1 -1
  103. package/lib/module/contexts/overlayContext/OverlayProvider.js +4 -1
  104. package/lib/module/contexts/overlayContext/OverlayProvider.js.map +1 -1
  105. package/lib/module/i18n/ar.json +7 -1
  106. package/lib/module/i18n/en.json +7 -1
  107. package/lib/module/i18n/es.json +7 -1
  108. package/lib/module/i18n/fr.json +7 -1
  109. package/lib/module/i18n/he.json +7 -1
  110. package/lib/module/i18n/hi.json +7 -1
  111. package/lib/module/i18n/it.json +7 -1
  112. package/lib/module/i18n/ja.json +7 -1
  113. package/lib/module/i18n/ko.json +7 -1
  114. package/lib/module/i18n/nl.json +7 -1
  115. package/lib/module/i18n/pt-br.json +7 -1
  116. package/lib/module/i18n/ru.json +7 -1
  117. package/lib/module/i18n/tr.json +7 -1
  118. package/lib/module/state-store/image-gallery-state-store.js +13 -0
  119. package/lib/module/state-store/image-gallery-state-store.js.map +1 -1
  120. package/lib/module/utils/i18n/getDateString.js +26 -1
  121. package/lib/module/utils/i18n/getDateString.js.map +1 -1
  122. package/lib/module/version.json +1 -1
  123. package/lib/typescript/a11y/hooks/useAccessibilityServiceEnabled.d.ts +20 -0
  124. package/lib/typescript/a11y/hooks/useAccessibilityServiceEnabled.d.ts.map +1 -0
  125. package/lib/typescript/a11y/index.d.ts +1 -0
  126. package/lib/typescript/a11y/index.d.ts.map +1 -1
  127. package/lib/typescript/components/Accessibility/OverlayA11yShield.d.ts +20 -0
  128. package/lib/typescript/components/Accessibility/OverlayA11yShield.d.ts.map +1 -0
  129. package/lib/typescript/components/Attachment/Gallery.d.ts.map +1 -1
  130. package/lib/typescript/components/ChannelPreview/ChannelPreviewStatus.d.ts.map +1 -1
  131. package/lib/typescript/components/ImageGallery/ImageGallery.d.ts.map +1 -1
  132. package/lib/typescript/components/Message/MessageItemView/Headers/MessageReminderHeader.d.ts.map +1 -1
  133. package/lib/typescript/components/Message/MessageItemView/Headers/SentToChannelHeader.d.ts.map +1 -1
  134. package/lib/typescript/components/Message/MessageItemView/MessageContent.d.ts +1 -1
  135. package/lib/typescript/components/Message/MessageItemView/MessageContent.d.ts.map +1 -1
  136. package/lib/typescript/components/Message/MessageItemView/MessageReplies.d.ts.map +1 -1
  137. package/lib/typescript/components/Message/MessageItemView/MessageRepliesAvatars.d.ts.map +1 -1
  138. package/lib/typescript/components/Message/MessageItemView/MessageTextContainer.d.ts +1 -1
  139. package/lib/typescript/components/Message/MessageItemView/MessageTextContainer.d.ts.map +1 -1
  140. package/lib/typescript/components/Message/MessageItemView/ReactionList/ReactionListItem.d.ts.map +1 -1
  141. package/lib/typescript/components/Message/hooks/useCreateMessageContext.d.ts +1 -1
  142. package/lib/typescript/components/Message/hooks/useCreateMessageContext.d.ts.map +1 -1
  143. package/lib/typescript/components/MessageList/InlineDateSeparator.d.ts.map +1 -1
  144. package/lib/typescript/components/MessageMenu/ReactionButton.d.ts.map +1 -1
  145. package/lib/typescript/components/Poll/components/PollOption.d.ts.map +1 -1
  146. package/lib/typescript/contexts/accessibilityContext/AccessibilityContext.d.ts +32 -2
  147. package/lib/typescript/contexts/accessibilityContext/AccessibilityContext.d.ts.map +1 -1
  148. package/lib/typescript/contexts/messageContext/MessageContext.d.ts +8 -0
  149. package/lib/typescript/contexts/messageContext/MessageContext.d.ts.map +1 -1
  150. package/lib/typescript/contexts/overlayContext/MessageOverlayHostLayer.d.ts.map +1 -1
  151. package/lib/typescript/contexts/overlayContext/OverlayProvider.d.ts.map +1 -1
  152. package/lib/typescript/i18n/ar.json +7 -1
  153. package/lib/typescript/i18n/en.json +7 -1
  154. package/lib/typescript/i18n/es.json +7 -1
  155. package/lib/typescript/i18n/fr.json +7 -1
  156. package/lib/typescript/i18n/he.json +7 -1
  157. package/lib/typescript/i18n/hi.json +7 -1
  158. package/lib/typescript/i18n/it.json +7 -1
  159. package/lib/typescript/i18n/ja.json +7 -1
  160. package/lib/typescript/i18n/ko.json +7 -1
  161. package/lib/typescript/i18n/nl.json +7 -1
  162. package/lib/typescript/i18n/pt-br.json +7 -1
  163. package/lib/typescript/i18n/ru.json +7 -1
  164. package/lib/typescript/i18n/tr.json +7 -1
  165. package/lib/typescript/state-store/image-gallery-state-store.d.ts +5 -1
  166. package/lib/typescript/state-store/image-gallery-state-store.d.ts.map +1 -1
  167. package/lib/typescript/utils/i18n/Streami18n.d.ts +6 -0
  168. package/lib/typescript/utils/i18n/Streami18n.d.ts.map +1 -1
  169. package/lib/typescript/utils/i18n/getDateString.d.ts +28 -0
  170. package/lib/typescript/utils/i18n/getDateString.d.ts.map +1 -1
  171. package/package.json +1 -1
  172. package/src/a11y/hooks/useAccessibilityServiceEnabled.ts +66 -0
  173. package/src/a11y/index.ts +1 -0
  174. package/src/components/Accessibility/OverlayA11yShield.tsx +57 -0
  175. package/src/components/Accessibility/__tests__/OverlayA11yShield.test.tsx +118 -0
  176. package/src/components/Attachment/Gallery.tsx +14 -2
  177. package/src/components/Channel/__tests__/ownCapabilities.test.tsx +25 -2
  178. package/src/components/ChannelPreview/ChannelPreviewStatus.tsx +17 -3
  179. package/src/components/ImageGallery/ImageGallery.tsx +82 -4
  180. package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx +13 -3
  181. package/src/components/ImageGallery/__tests__/ImageGalleryAdjustable.test.tsx +141 -0
  182. package/src/components/Message/MessageItemView/Headers/MessageReminderHeader.tsx +7 -1
  183. package/src/components/Message/MessageItemView/Headers/SentToChannelHeader.tsx +7 -1
  184. package/src/components/Message/MessageItemView/MessageContent.tsx +34 -15
  185. package/src/components/Message/MessageItemView/MessageReplies.tsx +9 -7
  186. package/src/components/Message/MessageItemView/MessageRepliesAvatars.tsx +5 -1
  187. package/src/components/Message/MessageItemView/MessageTextContainer.tsx +27 -4
  188. package/src/components/Message/MessageItemView/ReactionList/ReactionListItem.tsx +2 -0
  189. package/src/components/Message/MessageItemView/__tests__/Message.test.tsx +10 -3
  190. package/src/components/Message/MessageItemView/__tests__/MessageReplies.test.tsx +10 -2
  191. package/src/components/Message/hooks/useCreateMessageContext.ts +12 -1
  192. package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.tsx.snap +3 -0
  193. package/src/components/MessageInput/__tests__/__snapshots__/SendButton.test.tsx.snap +2 -0
  194. package/src/components/MessageList/InlineDateSeparator.tsx +10 -3
  195. package/src/components/MessageMenu/ReactionButton.tsx +0 -7
  196. package/src/components/MessageMenu/__tests__/ReactionButton.test.tsx +1 -14
  197. package/src/components/Poll/Poll.tsx +1 -1
  198. package/src/components/Poll/components/PollOption.tsx +7 -1
  199. package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap +1 -0
  200. package/src/contexts/accessibilityContext/AccessibilityContext.tsx +60 -36
  201. package/src/contexts/messageContext/MessageContext.tsx +8 -0
  202. package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx +5 -1
  203. package/src/contexts/overlayContext/OverlayProvider.tsx +6 -1
  204. package/src/i18n/ar.json +7 -1
  205. package/src/i18n/en.json +7 -1
  206. package/src/i18n/es.json +7 -1
  207. package/src/i18n/fr.json +7 -1
  208. package/src/i18n/he.json +7 -1
  209. package/src/i18n/hi.json +7 -1
  210. package/src/i18n/it.json +7 -1
  211. package/src/i18n/ja.json +7 -1
  212. package/src/i18n/ko.json +7 -1
  213. package/src/i18n/nl.json +7 -1
  214. package/src/i18n/pt-br.json +7 -1
  215. package/src/i18n/ru.json +7 -1
  216. package/src/i18n/tr.json +7 -1
  217. package/src/state-store/__tests__/image-gallery-state-store.test.ts +1 -0
  218. package/src/state-store/image-gallery-state-store.ts +13 -1
  219. package/src/utils/i18n/getDateString.ts +57 -0
  220. package/src/version.json +1 -1
@@ -0,0 +1,141 @@
1
+ import React, { useEffect } from 'react';
2
+
3
+ import type { SharedValue } from 'react-native-reanimated';
4
+
5
+ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
6
+
7
+ import { LocalMessage } from 'stream-chat';
8
+
9
+ import {
10
+ ImageGalleryContext,
11
+ ImageGalleryContextValue,
12
+ } from '../../../contexts/imageGalleryContext/ImageGalleryContext';
13
+ import { OverlayProvider } from '../../../contexts/overlayContext/OverlayProvider';
14
+ import {
15
+ generateImageAttachment,
16
+ generateVideoAttachment,
17
+ } from '../../../mock-builders/generator/attachment';
18
+ import { generateMessage } from '../../../mock-builders/generator/message';
19
+ import { ImageGalleryStateStore } from '../../../state-store/image-gallery-state-store';
20
+ import { ImageGallery } from '../ImageGallery';
21
+
22
+ jest.mock('../../../native.ts', () => {
23
+ const { View } = require('react-native');
24
+ return {
25
+ isFileSystemAvailable: jest.fn(() => true),
26
+ isImageMediaLibraryAvailable: jest.fn(() => true),
27
+ isShareImageAvailable: jest.fn(() => true),
28
+ isVideoPlayerAvailable: jest.fn(() => true),
29
+ NativeHandlers: { Video: View },
30
+ };
31
+ });
32
+
33
+ type HarnessProps = {
34
+ message: LocalMessage;
35
+ accessibilityEnabled?: boolean;
36
+ store: ImageGalleryStateStore;
37
+ };
38
+
39
+ const Harness = ({ accessibilityEnabled = true, message, store }: HarnessProps) => {
40
+ useEffect(() => {
41
+ const unsubscribe = store.registerSubscriptions();
42
+ return () => unsubscribe();
43
+ }, [store]);
44
+
45
+ const { attachments } = message;
46
+ store.openImageGallery({
47
+ messages: [message],
48
+ selectedAttachmentUrl: attachments?.[0]?.asset_url || attachments?.[0]?.image_url || '',
49
+ });
50
+
51
+ return (
52
+ <OverlayProvider
53
+ accessibility={{ enabled: accessibilityEnabled }}
54
+ value={{ overlayOpacity: { value: 1 } as SharedValue<number> }}
55
+ >
56
+ <ImageGalleryContext.Provider
57
+ value={{ imageGalleryStateStore: store } as unknown as ImageGalleryContextValue}
58
+ >
59
+ <ImageGallery />
60
+ </ImageGalleryContext.Provider>
61
+ </OverlayProvider>
62
+ );
63
+ };
64
+
65
+ const findGalleryRoot = () =>
66
+ screen.getByLabelText('Image Gallery', { includeHiddenElements: true });
67
+
68
+ const fireAccessibilityAction = (actionName: 'increment' | 'decrement') => {
69
+ fireEvent(findGalleryRoot(), 'accessibilityAction', { nativeEvent: { actionName } });
70
+ };
71
+
72
+ const renderWithAssets = (assetsCount: number, accessibilityEnabled = true) => {
73
+ const attachments = [
74
+ ...Array.from({ length: Math.max(assetsCount - 1, 0) }, () => generateImageAttachment()),
75
+ ...(assetsCount > 0 ? [generateVideoAttachment({ type: 'video' })] : []),
76
+ ];
77
+ const message = generateMessage({ attachments });
78
+ const store = new ImageGalleryStateStore();
79
+ render(<Harness accessibilityEnabled={accessibilityEnabled} message={message} store={store} />);
80
+ return { store };
81
+ };
82
+
83
+ describe('ImageGallery adjustable cycling', () => {
84
+ it('marks the root as adjustable with the position value when a11y is enabled and there is more than one asset', async () => {
85
+ renderWithAssets(3);
86
+
87
+ await waitFor(() => {
88
+ const root = findGalleryRoot();
89
+ expect(root.props.accessibilityRole).toBe('adjustable');
90
+ expect(root.props.accessibilityValue).toEqual({ text: '1 of 3' });
91
+ expect(root.props.accessibilityActions).toEqual([
92
+ { name: 'increment' },
93
+ { name: 'decrement' },
94
+ ]);
95
+ });
96
+ });
97
+
98
+ it('does not apply the adjustable role when accessibility is disabled', async () => {
99
+ renderWithAssets(3, false);
100
+
101
+ await waitFor(() => {
102
+ const root = findGalleryRoot();
103
+ expect(root.props.accessibilityRole).toBeUndefined();
104
+ expect(root.props.accessibilityActions).toBeUndefined();
105
+ });
106
+ });
107
+
108
+ it('moves to the next asset on increment and clamps at the last asset', async () => {
109
+ const { store } = renderWithAssets(3);
110
+
111
+ await waitFor(() => expect(findGalleryRoot().props.accessibilityRole).toBe('adjustable'));
112
+
113
+ act(() => fireAccessibilityAction('increment'));
114
+ expect(store.state.getLatestValue().currentIndex).toBe(1);
115
+
116
+ act(() => fireAccessibilityAction('increment'));
117
+ expect(store.state.getLatestValue().currentIndex).toBe(2);
118
+
119
+ act(() => fireAccessibilityAction('increment'));
120
+ expect(store.state.getLatestValue().currentIndex).toBe(2);
121
+ });
122
+
123
+ it('moves to the previous asset on decrement and clamps at the first asset', async () => {
124
+ const { store } = renderWithAssets(3);
125
+
126
+ await waitFor(() => expect(findGalleryRoot().props.accessibilityRole).toBe('adjustable'));
127
+
128
+ act(() => fireAccessibilityAction('increment'));
129
+ act(() => fireAccessibilityAction('increment'));
130
+ expect(store.state.getLatestValue().currentIndex).toBe(2);
131
+
132
+ act(() => fireAccessibilityAction('decrement'));
133
+ expect(store.state.getLatestValue().currentIndex).toBe(1);
134
+
135
+ act(() => fireAccessibilityAction('decrement'));
136
+ expect(store.state.getLatestValue().currentIndex).toBe(0);
137
+
138
+ act(() => fireAccessibilityAction('decrement'));
139
+ expect(store.state.getLatestValue().currentIndex).toBe(0);
140
+ });
141
+ });
@@ -33,7 +33,13 @@ const MessageReminderHeaderWithContext = (props: MessageReminderHeaderPropsWithC
33
33
  <Text style={styles.label}>
34
34
  {isReminderTimeLeft ? t('Reminder set') : t('Reminder overdue')}
35
35
  </Text>
36
- <Text style={styles.dot}>·</Text>
36
+ <Text
37
+ accessibilityElementsHidden
38
+ importantForAccessibility='no-hide-descendants'
39
+ style={styles.dot}
40
+ >
41
+ ·
42
+ </Text>
37
43
  <Text style={styles.time}>
38
44
  {t('duration/Message reminder', {
39
45
  milliseconds: timeLeftMs,
@@ -43,7 +43,13 @@ const SentToChannelHeaderWithContext = (props: SentToChannelHeaderPropsWithConte
43
43
  </Text>
44
44
  {showViewText ? (
45
45
  <>
46
- <Text style={styles.dot}>·</Text>
46
+ <Text
47
+ accessibilityElementsHidden
48
+ importantForAccessibility='no-hide-descendants'
49
+ style={styles.dot}
50
+ >
51
+ ·
52
+ </Text>
47
53
  <Pressable onPress={onPress}>
48
54
  <Text style={styles.link}>{t('View')}</Text>
49
55
  </Pressable>
@@ -1,5 +1,5 @@
1
1
  import React, { useMemo } from 'react';
2
- import { ColorValue, Pressable, StyleSheet, View, ViewStyle } from 'react-native';
2
+ import { ColorValue, Platform, Pressable, StyleSheet, View, ViewStyle } from 'react-native';
3
3
 
4
4
  import { MessageTextContainer } from './MessageTextContainer';
5
5
 
@@ -55,6 +55,7 @@ export type MessageContentPropsWithContext = Pick<
55
55
  | 'alignment'
56
56
  | 'goToMessage'
57
57
  | 'groupStyles'
58
+ | 'hasInteractiveAccessibilityContent'
58
59
  | 'isMyMessage'
59
60
  | 'message'
60
61
  | 'messageContentOrder'
@@ -111,6 +112,7 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
111
112
  enableMessageGroupingByUser,
112
113
  groupStyles,
113
114
  goToMessage,
115
+ hasInteractiveAccessibilityContent,
114
116
  isMessageAIGenerated,
115
117
  isMyMessage,
116
118
  isVeryLastMessage,
@@ -129,6 +131,10 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
129
131
  } = props;
130
132
  const { client } = useChatContext();
131
133
  const accessibilityHint = useA11yLabel('a11y/Double tap and hold to activate contextual menu');
134
+ const a11ySenderLabel = useA11yLabel(
135
+ isMyMessage ? 'a11y/Message from you' : 'a11y/Message from {{sender}}',
136
+ isMyMessage ? undefined : { sender: message.user?.name || message.user?.id || '' },
137
+ );
132
138
  const {
133
139
  Attachment,
134
140
  FileAttachmentGroup,
@@ -317,22 +323,18 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
317
323
  )}
318
324
  </>
319
325
  );
320
-
321
- // Drop the Pressable's single-focus-stop behavior when the message contains
322
- // interactive children (poll options, attachment cells, the quoted-reply
323
- // navigator, shared location). Without this, VO/TalkBack subsume those
324
- // children into the row's one announcement and they can't be activated.
325
- const hasInteractiveContent = !!(
326
- message.poll_id ||
327
- message.quoted_message ||
328
- message.attachments?.length ||
329
- message.shared_location
330
- );
326
+ const a11yPressableLabel = useMemo(() => {
327
+ if (!a11ySenderLabel) return undefined;
328
+ return message.text && !hasInteractiveAccessibilityContent
329
+ ? `${a11ySenderLabel}. ${message.text}`
330
+ : a11ySenderLabel;
331
+ }, [a11ySenderLabel, hasInteractiveAccessibilityContent, message.text]);
331
332
 
332
333
  return (
333
334
  <Pressable
335
+ accessibilityLabel={a11yPressableLabel}
334
336
  accessibilityHint={accessibilityHint}
335
- accessible={hasInteractiveContent ? false : undefined}
337
+ accessible={hasInteractiveAccessibilityContent ? false : undefined}
336
338
  disabled={preventPress}
337
339
  onLongPress={(event) => {
338
340
  if (onLongPress) {
@@ -382,6 +384,15 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
382
384
  ]}
383
385
  testID='message-content-wrapper'
384
386
  >
387
+ {a11ySenderLabel && Platform.OS !== 'android' && hasInteractiveAccessibilityContent ? (
388
+ <View
389
+ accessibilityLabel={a11ySenderLabel}
390
+ accessibilityHint={accessibilityHint}
391
+ accessible
392
+ pointerEvents='none'
393
+ style={StyleSheet.absoluteFill}
394
+ />
395
+ ) : null}
385
396
  {MessageContentTopView ? <MessageContentTopView /> : null}
386
397
  {hasContentSideViews ? (
387
398
  <View
@@ -414,6 +425,7 @@ const areEqual = (
414
425
  preventPress: prevPreventPress,
415
426
  goToMessage: prevGoToMessage,
416
427
  groupStyles: prevGroupStyles,
428
+ hasInteractiveAccessibilityContent: prevHasInteractiveAccessibilityContent,
417
429
  isAttachmentEqual,
418
430
  message: prevMessage,
419
431
  messageContentOrder: prevMessageContentOrder,
@@ -427,6 +439,7 @@ const areEqual = (
427
439
  preventPress: nextPreventPress,
428
440
  goToMessage: nextGoToMessage,
429
441
  groupStyles: nextGroupStyles,
442
+ hasInteractiveAccessibilityContent: nextHasInteractiveAccessibilityContent,
430
443
  message: nextMessage,
431
444
  messageContentOrder: nextMessageContentOrder,
432
445
  myMessageTheme: nextMyMessageTheme,
@@ -434,6 +447,10 @@ const areEqual = (
434
447
  t: nextT,
435
448
  } = nextProps;
436
449
 
450
+ if (prevHasInteractiveAccessibilityContent !== nextHasInteractiveAccessibilityContent) {
451
+ return false;
452
+ }
453
+
437
454
  if (prevBackgroundColor !== nextBackgroundColor) {
438
455
  return false;
439
456
  }
@@ -569,8 +586,11 @@ export type MessageContentProps = Partial<MessageContentPropsWithContext>;
569
586
  export const MessageContent = (props: MessageContentProps) => {
570
587
  const {
571
588
  alignment,
589
+ files,
572
590
  goToMessage,
573
591
  groupStyles,
592
+ hasInteractiveAccessibilityContent,
593
+ images,
574
594
  isMessageAIGenerated,
575
595
  isMyMessage,
576
596
  message,
@@ -581,8 +601,6 @@ export const MessageContent = (props: MessageContentProps) => {
581
601
  otherAttachments,
582
602
  preventPress,
583
603
  threadList,
584
- files,
585
- images,
586
604
  videos,
587
605
  } = useMessageContext();
588
606
  const {
@@ -637,6 +655,7 @@ export const MessageContent = (props: MessageContentProps) => {
637
655
  enableMessageGroupingByUser,
638
656
  goToMessage,
639
657
  groupStyles,
658
+ hasInteractiveAccessibilityContent,
640
659
  isAttachmentEqual,
641
660
  isMessageAIGenerated,
642
661
  isMyMessage,
@@ -75,10 +75,18 @@ const MessageRepliesWithContext = (props: MessageRepliesPropsWithContext) => {
75
75
  return null;
76
76
  }
77
77
 
78
+ const replyCountLabel =
79
+ message.reply_count === 1
80
+ ? t('1 Reply')
81
+ : t('{{ replyCount }} Replies', { replyCount: message.reply_count });
82
+
78
83
  return (
79
84
  <View style={[styles.container, container]}>
80
85
  {alignment === 'left' ? connector : null}
81
86
  <Pressable
87
+ accessibilityHint={t('a11y/Double tap to view thread')}
88
+ accessibilityLabel={replyCountLabel}
89
+ accessibilityRole='button'
82
90
  disabled={preventPress}
83
91
  onLongPress={(event) => {
84
92
  if (onLongPress) {
@@ -114,13 +122,7 @@ const MessageRepliesWithContext = (props: MessageRepliesPropsWithContext) => {
114
122
  testID='message-replies'
115
123
  >
116
124
  <MessageRepliesAvatars />
117
- <Text style={[styles.messageRepliesText, messageRepliesText]}>
118
- {message.reply_count === 1
119
- ? t('1 Reply')
120
- : t('{{ replyCount }} Replies', {
121
- replyCount: message.reply_count,
122
- })}
123
- </Text>
125
+ <Text style={[styles.messageRepliesText, messageRepliesText]}>{replyCountLabel}</Text>
124
126
  </Pressable>
125
127
  {alignment === 'right' ? connector : null}
126
128
  </View>
@@ -24,7 +24,11 @@ export const MessageRepliesAvatarsWithContext = (props: MessageRepliesAvatarsPro
24
24
  const avatars = message?.thread_participants || [];
25
25
 
26
26
  return (
27
- <View style={avatarStackContainer}>
27
+ <View
28
+ accessibilityElementsHidden
29
+ importantForAccessibility='no-hide-descendants'
30
+ style={avatarStackContainer}
31
+ >
28
32
  <UserAvatarStack users={avatars} avatarSize='sm' maxVisible={3} overlap={0.4} />
29
33
  </View>
30
34
  );
@@ -31,7 +31,13 @@ export type MessageTextProps = MessageTextContainerProps & {
31
31
 
32
32
  export type MessageTextContainerPropsWithContext = Pick<
33
33
  MessageContextValue,
34
- 'message' | 'onLongPress' | 'onlyEmojis' | 'onPress' | 'preventPress' | 'isMyMessage'
34
+ | 'hasInteractiveAccessibilityContent'
35
+ | 'isMyMessage'
36
+ | 'message'
37
+ | 'onLongPress'
38
+ | 'onlyEmojis'
39
+ | 'onPress'
40
+ | 'preventPress'
35
41
  > &
36
42
  Pick<MessagesContextValue, 'markdownRules' | 'myMessageTheme' | 'messageTextNumberOfLines'> & {
37
43
  markdownStyles?: MarkdownStyle;
@@ -45,6 +51,7 @@ const MessageTextContainerWithContext = (props: MessageTextContainerPropsWithCon
45
51
  const theme = useTheme();
46
52
 
47
53
  const {
54
+ hasInteractiveAccessibilityContent,
48
55
  isMyMessage,
49
56
  markdownRules,
50
57
  markdownStyles: markdownStylesProp = {},
@@ -81,6 +88,8 @@ const MessageTextContainerWithContext = (props: MessageTextContainerPropsWithCon
81
88
 
82
89
  return (
83
90
  <View
91
+ accessible={hasInteractiveAccessibilityContent || undefined}
92
+ accessibilityRole={hasInteractiveAccessibilityContent ? 'text' : undefined}
84
93
  style={[styles.textContainer, textContainer, stylesProp.textContainer]}
85
94
  testID='message-text-container'
86
95
  >
@@ -113,18 +122,24 @@ const areEqual = (
113
122
  nextProps: MessageTextContainerPropsWithContext,
114
123
  ) => {
115
124
  const {
125
+ hasInteractiveAccessibilityContent: prevHasInteractiveAccessibilityContent,
116
126
  markdownStyles: prevMarkdownStyles,
117
127
  message: prevMessage,
118
128
  myMessageTheme: prevMyMessageTheme,
119
129
  onlyEmojis: prevOnlyEmojis,
120
130
  } = prevProps;
121
131
  const {
132
+ hasInteractiveAccessibilityContent: nextHasInteractiveAccessibilityContent,
122
133
  markdownStyles: nextMarkdownStyles,
123
134
  message: nextMessage,
124
135
  myMessageTheme: nextMyMessageTheme,
125
136
  onlyEmojis: nextOnlyEmojis,
126
137
  } = nextProps;
127
138
 
139
+ if (prevHasInteractiveAccessibilityContent !== nextHasInteractiveAccessibilityContent) {
140
+ return false;
141
+ }
142
+
128
143
  const messageStatusEqual = prevMessage.status === nextMessage.status;
129
144
  if (!messageStatusEqual) {
130
145
  return false;
@@ -182,16 +197,24 @@ const MemoizedMessageTextContainer = React.memo(
182
197
  export type MessageTextContainerProps = Partial<MessageTextContainerPropsWithContext>;
183
198
 
184
199
  export const MessageTextContainer = (props: MessageTextContainerProps) => {
185
- const { message, onLongPress, onlyEmojis, onPress, preventPress, isMyMessage } =
186
- useMessageContext();
200
+ const {
201
+ hasInteractiveAccessibilityContent,
202
+ isMyMessage,
203
+ message,
204
+ onLongPress,
205
+ onlyEmojis,
206
+ onPress,
207
+ preventPress,
208
+ } = useMessageContext();
187
209
  const { markdownRules, messageTextNumberOfLines, myMessageTheme } = useMessagesContext();
188
210
 
189
211
  return (
190
212
  <MemoizedMessageTextContainer
191
213
  {...{
214
+ hasInteractiveAccessibilityContent,
215
+ isMyMessage,
192
216
  markdownRules,
193
217
  message,
194
- isMyMessage,
195
218
  messageTextNumberOfLines,
196
219
  myMessageTheme,
197
220
  onLongPress,
@@ -72,6 +72,8 @@ export const ReactionListItem = (props: ReactionListItemProps) => {
72
72
 
73
73
  return (
74
74
  <ReactionListItemWrapper
75
+ accessibilityRole='button'
76
+ accessibilityState={{ selected }}
75
77
  disabled={preventPress}
76
78
  key={reaction.type}
77
79
  testID='reaction-list-item'
@@ -171,10 +171,17 @@ describe('Message', () => {
171
171
 
172
172
  fireEvent(getByTestId('custom-overlay-trigger'), 'longPress');
173
173
 
174
+ // Once the overlay opens, the host layer's `accessibilityViewIsModal`
175
+ // marks the chat-side subtree as hidden to RNTL. The overlay target's
176
+ // children are Portal-teleported but their React parent stays in the
177
+ // chat, so default visibility filtering excludes them. Pass
178
+ // `includeHiddenElements` to look past the modal-sibling heuristic.
174
179
  await waitFor(() => {
175
- expect(getByText('outside:normal')).toBeTruthy();
176
- expect(getByText('inside:overlay')).toBeTruthy();
177
- expect(getByTestId('custom-overlay-target-placeholder')).toBeTruthy();
180
+ expect(getByText('outside:normal', { includeHiddenElements: true })).toBeTruthy();
181
+ expect(getByText('inside:overlay', { includeHiddenElements: true })).toBeTruthy();
182
+ expect(
183
+ getByTestId('custom-overlay-target-placeholder', { includeHiddenElements: true }),
184
+ ).toBeTruthy();
178
185
  });
179
186
  });
180
187
  });
@@ -33,7 +33,11 @@ describe('MessageReplies', () => {
33
33
  );
34
34
 
35
35
  await waitFor(() => {
36
- expect(screen.getByTestId('message-replies')).toBeTruthy();
36
+ const pluralPressable = screen.getByTestId('message-replies');
37
+ expect(pluralPressable).toBeTruthy();
38
+ expect(pluralPressable.props.accessibilityRole).toBe('button');
39
+ expect(pluralPressable.props.accessibilityLabel).toBe('{{ replyCount }} Replies');
40
+ expect(pluralPressable.props.accessibilityHint).toBe('a11y/Double tap to view thread');
37
41
  expect(t).toHaveBeenCalledWith('{{ replyCount }} Replies', {
38
42
  replyCount: message.reply_count,
39
43
  });
@@ -57,7 +61,11 @@ describe('MessageReplies', () => {
57
61
 
58
62
  await waitFor(() => {
59
63
  expect(onPressMock).toHaveBeenCalled();
60
- expect(screen.getByTestId('message-replies')).toBeTruthy();
64
+ const singularPressable = screen.getByTestId('message-replies');
65
+ expect(singularPressable).toBeTruthy();
66
+ expect(singularPressable.props.accessibilityRole).toBe('button');
67
+ expect(singularPressable.props.accessibilityLabel).toBe('1 Reply');
68
+ expect(singularPressable.props.accessibilityHint).toBe('a11y/Double tap to view thread');
61
69
  expect(t).toHaveBeenCalledWith('1 Reply');
62
70
  expect(screen.getByText('1 Reply')).toBeTruthy();
63
71
  });
@@ -1,5 +1,6 @@
1
1
  import { useMemo, useRef } from 'react';
2
2
 
3
+ import { useAccessibilityContext } from '../../../contexts/accessibilityContext/AccessibilityContext';
3
4
  import type { MessageContextValue } from '../../../contexts/messageContext/MessageContext';
4
5
 
5
6
  import { stringifyMessage } from '../../../utils/utils';
@@ -58,7 +59,7 @@ export const useCreateMessageContext = ({
58
59
  threadList,
59
60
  videos,
60
61
  setQuotedMessage,
61
- }: MessageContextValue) => {
62
+ }: Omit<MessageContextValue, 'hasInteractiveAccessibilityContent'>) => {
62
63
  const stableGroupStyles = useStableRefValue(groupStyles);
63
64
  const reactionsValue = reactions.map(({ count, own, type }) => `${own}${type}${count}`).join();
64
65
  const stringifiedMessage = stringifyMessage({ message });
@@ -70,6 +71,14 @@ export const useCreateMessageContext = ({
70
71
  ? stringifyMessage({ includeReactions: false, message: message.quoted_message })
71
72
  : '';
72
73
 
74
+ // Resolved here (not at each consumer) so the boolean lives on MessageContext
75
+ // and downstream components (MessageContent, MessageTextContainer) read it
76
+ // directly. The predicate's identity is stable in the default case; an
77
+ // integrator override is expected to be stable too (documented on the config).
78
+ const { hasInteractiveAccessibilityContent: hasInteractiveAccessibilityContentPredicate } =
79
+ useAccessibilityContext();
80
+ const hasInteractiveAccessibilityContent = hasInteractiveAccessibilityContentPredicate(message);
81
+
73
82
  const messageContext: MessageContextValue = useMemo(
74
83
  () => ({
75
84
  actionsEnabled,
@@ -85,6 +94,7 @@ export const useCreateMessageContext = ({
85
94
  hasAttachmentActions,
86
95
  handleReaction,
87
96
  handleToggleReaction,
97
+ hasInteractiveAccessibilityContent,
88
98
  hasReactions,
89
99
  messageHasOnlySingleAttachment,
90
100
  images,
@@ -123,6 +133,7 @@ export const useCreateMessageContext = ({
123
133
  goToMessage,
124
134
  stableGroupStyles,
125
135
  hasAttachmentActions,
136
+ hasInteractiveAccessibilityContent,
126
137
  hasReactions,
127
138
  messageHasOnlySingleAttachment,
128
139
  lastGroupMessage,
@@ -818,6 +818,7 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
818
818
  </View>
819
819
  </View>
820
820
  <View
821
+ accessibilityViewIsModal={false}
821
822
  collapsable={false}
822
823
  pointerEvents="box-none"
823
824
  style={
@@ -1733,6 +1734,7 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
1733
1734
  </View>
1734
1735
  </View>
1735
1736
  <View
1737
+ accessibilityViewIsModal={false}
1736
1738
  collapsable={false}
1737
1739
  pointerEvents="box-none"
1738
1740
  style={
@@ -2648,6 +2650,7 @@ exports[`AttachButton should render an disabled AttachButton 1`] = `
2648
2650
  </View>
2649
2651
  </View>
2650
2652
  <View
2653
+ accessibilityViewIsModal={false}
2651
2654
  collapsable={false}
2652
2655
  pointerEvents="box-none"
2653
2656
  style={
@@ -816,6 +816,7 @@ exports[`SendButton should render a SendButton 1`] = `
816
816
  </View>
817
817
  </View>
818
818
  <View
819
+ accessibilityViewIsModal={false}
819
820
  collapsable={false}
820
821
  pointerEvents="box-none"
821
822
  style={
@@ -1729,6 +1730,7 @@ exports[`SendButton should render a disabled SendButton 1`] = `
1729
1730
  </View>
1730
1731
  </View>
1731
1732
  <View
1733
+ accessibilityViewIsModal={false}
1732
1734
  collapsable={false}
1733
1735
  pointerEvents="box-none"
1734
1736
  style={
@@ -4,7 +4,7 @@ import { StyleSheet, Text, View } from 'react-native';
4
4
  import { useTheme } from '../../contexts/themeContext/ThemeContext';
5
5
  import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
6
6
  import { primitives } from '../../theme';
7
- import { getDateString } from '../../utils/i18n/getDateString';
7
+ import { getDateString, getDateStringForA11y } from '../../utils/i18n/getDateString';
8
8
 
9
9
  /**
10
10
  * Props for the `InlineDateSeparator` component.
@@ -17,7 +17,7 @@ export type InlineDateSeparatorProps = {
17
17
  };
18
18
 
19
19
  export const InlineDateSeparator = ({ date }: InlineDateSeparatorProps) => {
20
- const { t, tDateTimeParser } = useTranslationContext();
20
+ const { t, tDateTimeParser, userLanguage } = useTranslationContext();
21
21
  const styles = useStyles();
22
22
 
23
23
  const dateString = useMemo(
@@ -31,9 +31,16 @@ export const InlineDateSeparator = ({ date }: InlineDateSeparatorProps) => {
31
31
  [date, t, tDateTimeParser],
32
32
  );
33
33
 
34
+ const a11yDateString = useMemo(
35
+ () => getDateStringForA11y({ date, tDateTimeParser, userLanguage }),
36
+ [date, tDateTimeParser, userLanguage],
37
+ );
38
+
34
39
  return (
35
40
  <View style={styles.container} testID='date-separator'>
36
- <Text style={styles.text}>{dateString}</Text>
41
+ <Text accessibilityLabel={a11yDateString} style={styles.text}>
42
+ {dateString}
43
+ </Text>
37
44
  </View>
38
45
  );
39
46
  };
@@ -49,17 +49,10 @@ export const ReactionButton = (props: ReactionButtonProps) => {
49
49
  () => <Icon size={reactionIconSize ?? 24} />,
50
50
  [Icon, reactionIconSize],
51
51
  );
52
- const selectedLabelState = selected ? 'selected' : 'unselected';
53
- const accessibilityLabelParams = useMemo(
54
- () => ({ selected: selectedLabelState, type }),
55
- [selectedLabelState, type],
56
- );
57
52
 
58
53
  return (
59
54
  <View style={styles.reactionButton}>
60
55
  <Button
61
- accessibilityLabelKey='a11y/reaction-button-{{type}}-{{selected}}'
62
- accessibilityLabelParams={accessibilityLabelParams}
63
56
  variant={'secondary'}
64
57
  type={'outline'}
65
58
  iconOnly={!count}
@@ -2,9 +2,8 @@ import React from 'react';
2
2
 
3
3
  import { Text } from 'react-native';
4
4
 
5
- import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
5
+ import { cleanup, fireEvent, render } from '@testing-library/react-native';
6
6
 
7
- import { OverlayProvider } from '../../../contexts/overlayContext/OverlayProvider';
8
7
  import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
9
8
  import { defaultTheme } from '../../../contexts/themeContext/utils/theme';
10
9
  import { IconProps } from '../../../icons';
@@ -38,18 +37,6 @@ describe('ReactionButton', () => {
38
37
  expect(getByText('24')).toBeTruthy();
39
38
  });
40
39
 
41
- it('uses the released reaction button label when accessibility labels are translated', async () => {
42
- render(
43
- <OverlayProvider accessibility={{ enabled: true }}>
44
- <ReactionButton {...defaultProps} />
45
- </OverlayProvider>,
46
- );
47
-
48
- await waitFor(() => {
49
- expect(screen.getByLabelText('reaction-button-like-unselected')).toBeTruthy();
50
- });
51
- });
52
-
53
40
  it('should call onPress function with the correct reaction type when pressed', () => {
54
41
  const { getByRole } = render(
55
42
  <ThemeProvider theme={defaultTheme}>