stream-chat-react-native-core 9.0.0-beta.10 → 9.0.0-beta.11

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 (180) hide show
  1. package/lib/commonjs/components/Attachment/Audio/AudioAttachment.js +28 -15
  2. package/lib/commonjs/components/Attachment/Audio/AudioAttachment.js.map +1 -1
  3. package/lib/commonjs/components/Attachment/Gallery.js +13 -17
  4. package/lib/commonjs/components/Attachment/Gallery.js.map +1 -1
  5. package/lib/commonjs/components/Attachment/Giphy/Giphy.js +21 -23
  6. package/lib/commonjs/components/Attachment/Giphy/Giphy.js.map +1 -1
  7. package/lib/commonjs/components/AttachmentPicker/components/AttachmentPickerContent.js +8 -1
  8. package/lib/commonjs/components/AttachmentPicker/components/AttachmentPickerContent.js.map +1 -1
  9. package/lib/commonjs/components/ChannelPreview/ChannelSwipableWrapper.js +5 -5
  10. package/lib/commonjs/components/ChannelPreview/ChannelSwipableWrapper.js.map +1 -1
  11. package/lib/commonjs/components/Message/Message.js +9 -14
  12. package/lib/commonjs/components/Message/Message.js.map +1 -1
  13. package/lib/commonjs/components/MessageInput/components/AudioRecorder/AudioRecordingPreview.js +20 -7
  14. package/lib/commonjs/components/MessageInput/components/AudioRecorder/AudioRecordingPreview.js.map +1 -1
  15. package/lib/commonjs/components/MessageMenu/EmojiPickerList.js +1 -1
  16. package/lib/commonjs/components/MessageMenu/EmojiPickerList.js.map +1 -1
  17. package/lib/commonjs/components/MessageMenu/MessageUserReactions.js +8 -9
  18. package/lib/commonjs/components/MessageMenu/MessageUserReactions.js.map +1 -1
  19. package/lib/commonjs/components/MessageMenu/MessageUserReactionsItem.js +3 -3
  20. package/lib/commonjs/components/MessageMenu/MessageUserReactionsItem.js.map +1 -1
  21. package/lib/commonjs/components/MessageMenu/hooks/useFetchReactions.js +80 -25
  22. package/lib/commonjs/components/MessageMenu/hooks/useFetchReactions.js.map +1 -1
  23. package/lib/commonjs/components/Poll/components/PollAnswersList.js +1 -0
  24. package/lib/commonjs/components/Poll/components/PollAnswersList.js.map +1 -1
  25. package/lib/commonjs/components/Poll/components/PollButtons.js +2 -0
  26. package/lib/commonjs/components/Poll/components/PollButtons.js.map +1 -1
  27. package/lib/commonjs/components/Poll/components/PollInputDialog.js +2 -1
  28. package/lib/commonjs/components/Poll/components/PollInputDialog.js.map +1 -1
  29. package/lib/commonjs/components/ProgressControl/StableDurationLabel.js +45 -0
  30. package/lib/commonjs/components/ProgressControl/StableDurationLabel.js.map +1 -0
  31. package/lib/commonjs/components/ProgressControl/WaveProgressBar.js +127 -40
  32. package/lib/commonjs/components/ProgressControl/WaveProgressBar.js.map +1 -1
  33. package/lib/commonjs/components/Reply/Reply.js +3 -0
  34. package/lib/commonjs/components/Reply/Reply.js.map +1 -1
  35. package/lib/commonjs/components/UIComponents/BottomSheetModal.js +4 -4
  36. package/lib/commonjs/components/UIComponents/BottomSheetModal.js.map +1 -1
  37. package/lib/commonjs/i18n/en.json +2 -0
  38. package/lib/commonjs/i18n/es.json +2 -0
  39. package/lib/commonjs/i18n/fr.json +2 -0
  40. package/lib/commonjs/i18n/he.json +2 -0
  41. package/lib/commonjs/i18n/hi.json +2 -0
  42. package/lib/commonjs/i18n/it.json +2 -0
  43. package/lib/commonjs/i18n/ja.json +2 -0
  44. package/lib/commonjs/i18n/ko.json +2 -0
  45. package/lib/commonjs/i18n/nl.json +2 -0
  46. package/lib/commonjs/i18n/pt-br.json +2 -0
  47. package/lib/commonjs/i18n/ru.json +2 -0
  48. package/lib/commonjs/i18n/tr.json +2 -0
  49. package/lib/commonjs/middlewares/attachments.js +5 -4
  50. package/lib/commonjs/middlewares/attachments.js.map +1 -1
  51. package/lib/commonjs/state-store/audio-player.js +29 -17
  52. package/lib/commonjs/state-store/audio-player.js.map +1 -1
  53. package/lib/commonjs/version.json +1 -1
  54. package/lib/module/components/Attachment/Audio/AudioAttachment.js +28 -15
  55. package/lib/module/components/Attachment/Audio/AudioAttachment.js.map +1 -1
  56. package/lib/module/components/Attachment/Gallery.js +13 -17
  57. package/lib/module/components/Attachment/Gallery.js.map +1 -1
  58. package/lib/module/components/Attachment/Giphy/Giphy.js +21 -23
  59. package/lib/module/components/Attachment/Giphy/Giphy.js.map +1 -1
  60. package/lib/module/components/AttachmentPicker/components/AttachmentPickerContent.js +8 -1
  61. package/lib/module/components/AttachmentPicker/components/AttachmentPickerContent.js.map +1 -1
  62. package/lib/module/components/ChannelPreview/ChannelSwipableWrapper.js +5 -5
  63. package/lib/module/components/ChannelPreview/ChannelSwipableWrapper.js.map +1 -1
  64. package/lib/module/components/Message/Message.js +9 -14
  65. package/lib/module/components/Message/Message.js.map +1 -1
  66. package/lib/module/components/MessageInput/components/AudioRecorder/AudioRecordingPreview.js +20 -7
  67. package/lib/module/components/MessageInput/components/AudioRecorder/AudioRecordingPreview.js.map +1 -1
  68. package/lib/module/components/MessageMenu/EmojiPickerList.js +1 -1
  69. package/lib/module/components/MessageMenu/EmojiPickerList.js.map +1 -1
  70. package/lib/module/components/MessageMenu/MessageUserReactions.js +8 -9
  71. package/lib/module/components/MessageMenu/MessageUserReactions.js.map +1 -1
  72. package/lib/module/components/MessageMenu/MessageUserReactionsItem.js +3 -3
  73. package/lib/module/components/MessageMenu/MessageUserReactionsItem.js.map +1 -1
  74. package/lib/module/components/MessageMenu/hooks/useFetchReactions.js +80 -25
  75. package/lib/module/components/MessageMenu/hooks/useFetchReactions.js.map +1 -1
  76. package/lib/module/components/Poll/components/PollAnswersList.js +1 -0
  77. package/lib/module/components/Poll/components/PollAnswersList.js.map +1 -1
  78. package/lib/module/components/Poll/components/PollButtons.js +2 -0
  79. package/lib/module/components/Poll/components/PollButtons.js.map +1 -1
  80. package/lib/module/components/Poll/components/PollInputDialog.js +2 -1
  81. package/lib/module/components/Poll/components/PollInputDialog.js.map +1 -1
  82. package/lib/module/components/ProgressControl/StableDurationLabel.js +45 -0
  83. package/lib/module/components/ProgressControl/StableDurationLabel.js.map +1 -0
  84. package/lib/module/components/ProgressControl/WaveProgressBar.js +127 -40
  85. package/lib/module/components/ProgressControl/WaveProgressBar.js.map +1 -1
  86. package/lib/module/components/Reply/Reply.js +3 -0
  87. package/lib/module/components/Reply/Reply.js.map +1 -1
  88. package/lib/module/components/UIComponents/BottomSheetModal.js +4 -4
  89. package/lib/module/components/UIComponents/BottomSheetModal.js.map +1 -1
  90. package/lib/module/i18n/en.json +2 -0
  91. package/lib/module/i18n/es.json +2 -0
  92. package/lib/module/i18n/fr.json +2 -0
  93. package/lib/module/i18n/he.json +2 -0
  94. package/lib/module/i18n/hi.json +2 -0
  95. package/lib/module/i18n/it.json +2 -0
  96. package/lib/module/i18n/ja.json +2 -0
  97. package/lib/module/i18n/ko.json +2 -0
  98. package/lib/module/i18n/nl.json +2 -0
  99. package/lib/module/i18n/pt-br.json +2 -0
  100. package/lib/module/i18n/ru.json +2 -0
  101. package/lib/module/i18n/tr.json +2 -0
  102. package/lib/module/middlewares/attachments.js +5 -4
  103. package/lib/module/middlewares/attachments.js.map +1 -1
  104. package/lib/module/state-store/audio-player.js +29 -17
  105. package/lib/module/state-store/audio-player.js.map +1 -1
  106. package/lib/module/version.json +1 -1
  107. package/lib/typescript/components/Attachment/Audio/AudioAttachment.d.ts.map +1 -1
  108. package/lib/typescript/components/Attachment/Gallery.d.ts.map +1 -1
  109. package/lib/typescript/components/Attachment/Giphy/Giphy.d.ts.map +1 -1
  110. package/lib/typescript/components/AttachmentPicker/components/AttachmentPickerContent.d.ts +1 -0
  111. package/lib/typescript/components/AttachmentPicker/components/AttachmentPickerContent.d.ts.map +1 -1
  112. package/lib/typescript/components/Message/Message.d.ts.map +1 -1
  113. package/lib/typescript/components/MessageInput/components/AudioRecorder/AudioRecordingPreview.d.ts.map +1 -1
  114. package/lib/typescript/components/MessageMenu/MessageUserReactions.d.ts.map +1 -1
  115. package/lib/typescript/components/MessageMenu/hooks/useFetchReactions.d.ts +15 -0
  116. package/lib/typescript/components/MessageMenu/hooks/useFetchReactions.d.ts.map +1 -1
  117. package/lib/typescript/components/Poll/components/PollAnswersList.d.ts.map +1 -1
  118. package/lib/typescript/components/Poll/components/PollButtons.d.ts.map +1 -1
  119. package/lib/typescript/components/Poll/components/PollInputDialog.d.ts +2 -1
  120. package/lib/typescript/components/Poll/components/PollInputDialog.d.ts.map +1 -1
  121. package/lib/typescript/components/ProgressControl/StableDurationLabel.d.ts +19 -0
  122. package/lib/typescript/components/ProgressControl/StableDurationLabel.d.ts.map +1 -0
  123. package/lib/typescript/components/ProgressControl/WaveProgressBar.d.ts.map +1 -1
  124. package/lib/typescript/components/Reply/Reply.d.ts.map +1 -1
  125. package/lib/typescript/components/UIComponents/BottomSheetModal.d.ts.map +1 -1
  126. package/lib/typescript/i18n/en.json +2 -0
  127. package/lib/typescript/i18n/es.json +2 -0
  128. package/lib/typescript/i18n/fr.json +2 -0
  129. package/lib/typescript/i18n/he.json +2 -0
  130. package/lib/typescript/i18n/hi.json +2 -0
  131. package/lib/typescript/i18n/it.json +2 -0
  132. package/lib/typescript/i18n/ja.json +2 -0
  133. package/lib/typescript/i18n/ko.json +2 -0
  134. package/lib/typescript/i18n/nl.json +2 -0
  135. package/lib/typescript/i18n/pt-br.json +2 -0
  136. package/lib/typescript/i18n/ru.json +2 -0
  137. package/lib/typescript/i18n/tr.json +2 -0
  138. package/lib/typescript/middlewares/attachments.d.ts +2 -1
  139. package/lib/typescript/middlewares/attachments.d.ts.map +1 -1
  140. package/lib/typescript/state-store/audio-player.d.ts +3 -2
  141. package/lib/typescript/state-store/audio-player.d.ts.map +1 -1
  142. package/lib/typescript/utils/i18n/Streami18n.d.ts +2 -0
  143. package/lib/typescript/utils/i18n/Streami18n.d.ts.map +1 -1
  144. package/package.json +1 -1
  145. package/src/components/Attachment/Audio/AudioAttachment.tsx +33 -27
  146. package/src/components/Attachment/Gallery.tsx +1 -8
  147. package/src/components/Attachment/Giphy/Giphy.tsx +25 -39
  148. package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx +1 -0
  149. package/src/components/ChannelPreview/ChannelSwipableWrapper.tsx +3 -3
  150. package/src/components/Message/Message.tsx +1 -3
  151. package/src/components/MessageInput/components/AudioRecorder/AudioRecordingPreview.tsx +27 -19
  152. package/src/components/MessageMenu/EmojiPickerList.tsx +1 -1
  153. package/src/components/MessageMenu/MessageUserReactions.tsx +8 -11
  154. package/src/components/MessageMenu/MessageUserReactionsItem.tsx +3 -3
  155. package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx +4 -3
  156. package/src/components/MessageMenu/hooks/__tests__/useFetchReactions.test.ts +56 -0
  157. package/src/components/MessageMenu/hooks/useFetchReactions.ts +96 -11
  158. package/src/components/Poll/components/PollAnswersList.tsx +1 -0
  159. package/src/components/Poll/components/PollButtons.tsx +2 -0
  160. package/src/components/Poll/components/PollInputDialog.tsx +3 -1
  161. package/src/components/ProgressControl/StableDurationLabel.tsx +63 -0
  162. package/src/components/ProgressControl/WaveProgressBar.tsx +179 -60
  163. package/src/components/Reply/Reply.tsx +4 -0
  164. package/src/components/UIComponents/BottomSheetModal.tsx +3 -3
  165. package/src/i18n/en.json +2 -0
  166. package/src/i18n/es.json +2 -0
  167. package/src/i18n/fr.json +2 -0
  168. package/src/i18n/he.json +2 -0
  169. package/src/i18n/hi.json +2 -0
  170. package/src/i18n/it.json +2 -0
  171. package/src/i18n/ja.json +2 -0
  172. package/src/i18n/ko.json +2 -0
  173. package/src/i18n/nl.json +2 -0
  174. package/src/i18n/pt-br.json +2 -0
  175. package/src/i18n/ru.json +2 -0
  176. package/src/i18n/tr.json +2 -0
  177. package/src/middlewares/attachments.ts +5 -3
  178. package/src/state-store/__tests__/audio-player.test.ts +217 -0
  179. package/src/state-store/audio-player.ts +41 -21
  180. package/src/version.json +1 -1
@@ -64,10 +64,10 @@ export const MessageUserReactionsItem = ({
64
64
  style={[styles.avatarContainer, avatarContainer]}
65
65
  onPress={onPress}
66
66
  >
67
- <MessageUserReactionsAvatar reaction={reaction} size={'lg'} />
67
+ <MessageUserReactionsAvatar reaction={reaction} size={'md'} />
68
68
  <View style={[styles.avatarNameContainer, avatarNameContainer]}>
69
69
  <Text numberOfLines={1} style={[styles.avatarName, avatarName]}>
70
- {name}
70
+ {isOwnReaction ? t('You') : name}
71
71
  </Text>
72
72
  {isOwnReaction ? (
73
73
  <Text numberOfLines={1} style={[styles.avatarSubtitle, null]}>
@@ -105,7 +105,7 @@ const useStyles = () => {
105
105
  textAlign: 'left',
106
106
  },
107
107
  avatarSubtitle: {
108
- fontSize: primitives.typographyFontSizeSm,
108
+ fontSize: primitives.typographyFontSizeXs,
109
109
  color: semantics.textTertiary,
110
110
  },
111
111
  avatarNameContainer: {
@@ -87,16 +87,16 @@ describe('MessageUserReactions when the supportedReactions are defined', () => {
87
87
 
88
88
  it('renders reaction buttons', () => {
89
89
  const { getByLabelText } = renderComponent();
90
- const likeReactionButton = getByLabelText('reaction-button-like-selected');
90
+ const likeReactionButton = getByLabelText('reaction-button-like-unselected');
91
91
  expect(likeReactionButton).toBeDefined();
92
92
  const loveReactionButton = getByLabelText('reaction-button-love-unselected');
93
93
  expect(loveReactionButton).toBeDefined();
94
94
  });
95
95
 
96
- it('selects the first reaction by default', () => {
96
+ it('starts with no reaction filter selected by default', () => {
97
97
  const { getAllByLabelText } = renderComponent();
98
98
  const reactionButtons = getAllByLabelText(/\breaction-button[^\s]+/);
99
- expect(reactionButtons[0].props.accessibilityLabel).toBe('reaction-button-like-selected');
99
+ expect(reactionButtons[0].props.accessibilityLabel).toBe('reaction-button-like-unselected');
100
100
  expect(reactionButtons[1].props.accessibilityLabel).toBe('reaction-button-love-unselected');
101
101
  });
102
102
 
@@ -114,6 +114,7 @@ describe('MessageUserReactions when the supportedReactions are defined', () => {
114
114
  const { getByText } = renderComponent();
115
115
  const reactionItems = getByText('1 like');
116
116
  expect(reactionItems).toBeDefined();
117
+ expect(getByText('2 love')).toBeDefined();
117
118
  });
118
119
 
119
120
  it('uses provided reactions when passed as a prop', () => {
@@ -0,0 +1,56 @@
1
+ import { ReactionResponse } from 'stream-chat';
2
+
3
+ import {
4
+ reconcileUpdatedReactionInList,
5
+ removeReactionFromList,
6
+ upsertReactionInList,
7
+ } from '../useFetchReactions';
8
+
9
+ const makeReaction = (
10
+ params: Partial<ReactionResponse> & Pick<ReactionResponse, 'type' | 'user_id'>,
11
+ ) =>
12
+ ({
13
+ type: params.type,
14
+ user_id: params.user_id,
15
+ user: params.user ?? { id: params.user_id, name: params.user_id },
16
+ }) as ReactionResponse;
17
+
18
+ describe('useFetchReactions helpers', () => {
19
+ it('upserts reactions in the unfiltered list without duplicating the same user/type pair', () => {
20
+ const existing = makeReaction({ type: 'like', user_id: 'user-1' });
21
+ const updated = makeReaction({ type: 'like', user_id: 'user-1' });
22
+
23
+ const result = upsertReactionInList({
24
+ prevReactions: [existing],
25
+ reaction: updated,
26
+ });
27
+
28
+ expect(result).toHaveLength(1);
29
+ expect(result[0]).toBe(updated);
30
+ });
31
+
32
+ it('removes only the matching user/type pair in the unfiltered list', () => {
33
+ const likeReaction = makeReaction({ type: 'like', user_id: 'user-1' });
34
+ const loveReaction = makeReaction({ type: 'love', user_id: 'user-1' });
35
+
36
+ const result = removeReactionFromList({
37
+ prevReactions: [likeReaction, loveReaction],
38
+ reaction: likeReaction,
39
+ });
40
+
41
+ expect(result).toEqual([loveReaction]);
42
+ });
43
+
44
+ it('removes the previous filtered reaction when an updated event moves that user to another type', () => {
45
+ const existing = makeReaction({ type: 'like', user_id: 'user-1' });
46
+ const updated = makeReaction({ type: 'love', user_id: 'user-1' });
47
+
48
+ const result = reconcileUpdatedReactionInList({
49
+ prevReactions: [existing],
50
+ reaction: updated,
51
+ reactionType: 'like',
52
+ });
53
+
54
+ expect(result).toEqual([]);
55
+ });
56
+ });
@@ -11,6 +11,81 @@ export type UseFetchReactionParams = {
11
11
  sort?: ReactionSort;
12
12
  };
13
13
 
14
+ const isSameReaction = (left: ReactionResponse, right: ReactionResponse) =>
15
+ left.user_id === right.user_id && left.type === right.type;
16
+
17
+ export const upsertReactionInList = ({
18
+ prevReactions,
19
+ reaction,
20
+ reactionType,
21
+ }: {
22
+ prevReactions: ReactionResponse[];
23
+ reaction: ReactionResponse;
24
+ reactionType?: string;
25
+ }) => {
26
+ if (!reactionType) {
27
+ return [
28
+ reaction,
29
+ ...prevReactions.filter((currentReaction) => !isSameReaction(currentReaction, reaction)),
30
+ ];
31
+ }
32
+
33
+ if (reaction.type !== reactionType) {
34
+ return prevReactions;
35
+ }
36
+
37
+ return [
38
+ reaction,
39
+ ...prevReactions.filter((currentReaction) => currentReaction.user_id !== reaction.user_id),
40
+ ];
41
+ };
42
+
43
+ export const reconcileUpdatedReactionInList = ({
44
+ prevReactions,
45
+ reaction,
46
+ reactionType,
47
+ }: {
48
+ prevReactions: ReactionResponse[];
49
+ reaction: ReactionResponse;
50
+ reactionType?: string;
51
+ }) => {
52
+ if (!reactionType) {
53
+ return [
54
+ reaction,
55
+ ...prevReactions.filter((currentReaction) => !isSameReaction(currentReaction, reaction)),
56
+ ];
57
+ }
58
+
59
+ if (reaction.type !== reactionType) {
60
+ return prevReactions.filter((currentReaction) => currentReaction.user_id !== reaction.user_id);
61
+ }
62
+
63
+ return [
64
+ reaction,
65
+ ...prevReactions.filter((currentReaction) => currentReaction.user_id !== reaction.user_id),
66
+ ];
67
+ };
68
+
69
+ export const removeReactionFromList = ({
70
+ prevReactions,
71
+ reaction,
72
+ reactionType,
73
+ }: {
74
+ prevReactions: ReactionResponse[];
75
+ reaction: ReactionResponse;
76
+ reactionType?: string;
77
+ }) => {
78
+ if (!reactionType) {
79
+ return prevReactions.filter((currentReaction) => !isSameReaction(currentReaction, reaction));
80
+ }
81
+
82
+ if (reaction.type !== reactionType) {
83
+ return prevReactions;
84
+ }
85
+
86
+ return prevReactions.filter((currentReaction) => currentReaction.user_id !== reaction.user_id);
87
+ };
88
+
14
89
  export const useFetchReactions = ({
15
90
  limit = 25,
16
91
  message,
@@ -82,8 +157,14 @@ export const useFetchReactions = ({
82
157
  client.on('reaction.new', (event) => {
83
158
  const { reaction } = event;
84
159
 
85
- if (reaction && reaction.type === reactionType) {
86
- setReactions((prevReactions) => [reaction, ...prevReactions]);
160
+ if (reaction) {
161
+ setReactions((prevReactions) =>
162
+ upsertReactionInList({
163
+ prevReactions,
164
+ reaction,
165
+ reactionType,
166
+ }),
167
+ );
87
168
  }
88
169
  }),
89
170
  );
@@ -93,13 +174,13 @@ export const useFetchReactions = ({
93
174
  const { reaction } = event;
94
175
 
95
176
  if (reaction) {
96
- if (reaction.type === reactionType) {
97
- setReactions((prevReactions) => [reaction, ...prevReactions]);
98
- } else {
99
- setReactions((prevReactions) =>
100
- prevReactions.filter((r) => r.user_id !== reaction.user_id),
101
- );
102
- }
177
+ setReactions((prevReactions) =>
178
+ reconcileUpdatedReactionInList({
179
+ prevReactions,
180
+ reaction,
181
+ reactionType,
182
+ }),
183
+ );
103
184
  }
104
185
  }),
105
186
  );
@@ -108,9 +189,13 @@ export const useFetchReactions = ({
108
189
  client.on('reaction.deleted', (event) => {
109
190
  const { reaction } = event;
110
191
 
111
- if (reaction && reaction.type === reactionType) {
192
+ if (reaction) {
112
193
  setReactions((prevReactions) =>
113
- prevReactions.filter((r) => r.user_id !== reaction.user_id),
194
+ removeReactionFromList({
195
+ prevReactions,
196
+ reaction,
197
+ reactionType,
198
+ }),
114
199
  );
115
200
  }
116
201
  }),
@@ -53,6 +53,7 @@ export const AnswerListAddCommentButton = (props: PollButtonProps) => {
53
53
  closeDialog={() => setShowAddCommentDialog(false)}
54
54
  initialValue={ownAnswer?.answer_text ?? ''}
55
55
  onSubmit={addComment}
56
+ placeholder={t('Your comment')}
56
57
  title={t('Add a comment')}
57
58
  visible={showAddCommentDialog}
58
59
  />
@@ -176,6 +176,7 @@ export const SuggestOptionButton = (props: PollButtonProps) => {
176
176
  <PollInputDialog
177
177
  closeDialog={onRequestClose}
178
178
  onSubmit={addOption}
179
+ placeholder={t('Enter a new option')}
179
180
  title={t('Suggest an option')}
180
181
  visible={showAddOptionDialog}
181
182
  />
@@ -214,6 +215,7 @@ export const AddCommentButton = (props: PollButtonProps) => {
214
215
  closeDialog={onRequestClose}
215
216
  initialValue={ownAnswer?.answer_text ?? ''}
216
217
  onSubmit={addComment}
218
+ placeholder={t('Your comment')}
217
219
  title={t('Add a comment')}
218
220
  visible={showAddCommentDialog}
219
221
  />
@@ -19,6 +19,7 @@ import { Button } from '../../ui';
19
19
  export type PollInputDialogProps = {
20
20
  closeDialog: () => void;
21
21
  onSubmit: (text: string) => void;
22
+ placeholder: string;
22
23
  title: string;
23
24
  visible: boolean;
24
25
  initialValue?: string;
@@ -28,6 +29,7 @@ export const PollInputDialog = ({
28
29
  closeDialog,
29
30
  initialValue = '',
30
31
  onSubmit,
32
+ placeholder,
31
33
  title,
32
34
  visible,
33
35
  }: PollInputDialogProps) => {
@@ -62,7 +64,7 @@ export const PollInputDialog = ({
62
64
  <TextInput
63
65
  autoFocus={true}
64
66
  onChangeText={setDialogInput}
65
- placeholder={t('Ask a question')}
67
+ placeholder={placeholder}
66
68
  placeholderTextColor={semantics.inputTextPlaceholder}
67
69
  style={[styles.input, input]}
68
70
  value={dialogInput}
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import {
3
+ StyleSheet,
4
+ Text,
5
+ type StyleProp,
6
+ type TextStyle,
7
+ View,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+
11
+ type StableDurationLabelProps = {
12
+ accessibilityLabel?: string;
13
+ containerStyle?: StyleProp<ViewStyle>;
14
+ reserveLabel: string;
15
+ style?: StyleProp<TextStyle>;
16
+ visibleStyle?: StyleProp<TextStyle>;
17
+ label: string;
18
+ };
19
+
20
+ /**
21
+ * Renders a duration label with a layout-stable width.
22
+ *
23
+ * It reserves space using an invisible max-width label, then absolutely positions
24
+ * the live value on top so playback updates do not cause parent relayout.
25
+ */
26
+ export const StableDurationLabel = React.memo(
27
+ ({
28
+ accessibilityLabel,
29
+ containerStyle,
30
+ reserveLabel,
31
+ style,
32
+ visibleStyle,
33
+ label,
34
+ }: StableDurationLabelProps) => (
35
+ <View style={[styles.container, containerStyle]}>
36
+ <Text accessible={false} style={[style, styles.reserveLabel]}>
37
+ {reserveLabel}
38
+ </Text>
39
+ <Text
40
+ accessibilityLabel={accessibilityLabel}
41
+ style={[style, styles.visibleLabel, visibleStyle]}
42
+ >
43
+ {label}
44
+ </Text>
45
+ </View>
46
+ ),
47
+ );
48
+
49
+ StableDurationLabel.displayName = 'StableDurationLabel';
50
+
51
+ const styles = StyleSheet.create({
52
+ container: {
53
+ position: 'relative',
54
+ },
55
+ reserveLabel: {
56
+ opacity: 0,
57
+ },
58
+ visibleLabel: {
59
+ left: 0,
60
+ position: 'absolute',
61
+ top: 0,
62
+ },
63
+ });
@@ -1,12 +1,8 @@
1
- import React, { useCallback, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { StyleSheet, View } from 'react-native';
3
+ import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
3
4
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
4
- import Animated, {
5
- runOnJS,
6
- useAnimatedReaction,
7
- useAnimatedStyle,
8
- useSharedValue,
9
- } from 'react-native-reanimated';
5
+ import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
10
6
 
11
7
  import { ProgressControlThumb } from './ProgressThumb';
12
8
 
@@ -49,6 +45,43 @@ const WAVEFORM_WIDTH = 2;
49
45
  const WAVEFORM_GAP = 2;
50
46
  const WAVE_MAX_HEIGHT = 20;
51
47
  const WAVE_MIN_HEIGHT = 2;
48
+ const DRAG_HIT_SLOP = {
49
+ bottom: 12,
50
+ left: 12,
51
+ right: 12,
52
+ top: 12,
53
+ };
54
+
55
+ const clampProgress = (progress: number) => {
56
+ 'worklet';
57
+ return Math.max(0, Math.min(progress, 1));
58
+ };
59
+
60
+ type WaveformBarsProps = {
61
+ color: ColorValue;
62
+ heights: number[];
63
+ waveformStyle?: StyleProp<ViewStyle>;
64
+ };
65
+
66
+ const WaveformBars = React.memo(({ color, heights, waveformStyle }: WaveformBarsProps) => (
67
+ <View style={styles.waveformLayer}>
68
+ {heights.map((height, index) => (
69
+ <View
70
+ key={index}
71
+ style={[
72
+ styles.waveform,
73
+ {
74
+ backgroundColor: color,
75
+ height,
76
+ },
77
+ waveformStyle,
78
+ ]}
79
+ />
80
+ ))}
81
+ </View>
82
+ ));
83
+
84
+ WaveformBars.displayName = 'WaveformBars';
52
85
 
53
86
  export const WaveProgressBar = React.memo(
54
87
  (props: WaveProgressBarProps) => {
@@ -62,30 +95,16 @@ export const WaveProgressBar = React.memo(
62
95
  progress,
63
96
  waveformData,
64
97
  } = props;
98
+ const [showInteractiveLayer, setShowInteractiveLayer] = useState(
99
+ () => progress > 0 || isPlaying,
100
+ );
65
101
  const eachWaveformWidth = WAVEFORM_WIDTH + WAVEFORM_GAP;
66
102
  const fullWidth = (amplitudesCount - 1) * eachWaveformWidth;
67
- const state = useSharedValue(progress);
68
- const [currentWaveformProgress, setCurrentWaveformProgress] = useState<number>(0);
69
-
70
- const waveFormNumberFromProgress = useCallback(
71
- (progress: number) => {
72
- 'worklet';
73
- const progressInPrecision = Number(progress.toFixed(2));
74
- const progressInWaveformWidth = Number((progressInPrecision * fullWidth).toFixed(0));
75
- const progressInWaveformNumber = Math.floor(progressInWaveformWidth / 4);
76
- runOnJS(setCurrentWaveformProgress)(progressInWaveformNumber);
77
- },
78
- [fullWidth],
79
- );
80
-
81
- useAnimatedReaction(
82
- () => progress,
83
- (newProgress) => {
84
- state.value = newProgress;
85
- waveFormNumberFromProgress(newProgress);
86
- },
87
- [progress],
88
- );
103
+ const maxThumbTranslateX = Math.max(fullWidth - eachWaveformWidth, 0);
104
+ const maxProgressWidth = fullWidth + WAVEFORM_WIDTH;
105
+ const dragStartProgress = useSharedValue(0);
106
+ const isDragging = useSharedValue(false);
107
+ const visualProgress = useSharedValue(progress);
89
108
 
90
109
  const {
91
110
  theme: {
@@ -94,29 +113,84 @@ export const WaveProgressBar = React.memo(
94
113
  },
95
114
  } = useTheme();
96
115
 
116
+ useEffect(() => {
117
+ if (!isDragging.value) {
118
+ visualProgress.value = progress;
119
+ }
120
+ }, [isDragging, progress, visualProgress]);
121
+
122
+ useEffect(() => {
123
+ setShowInteractiveLayer(progress > 0 || isPlaying);
124
+ }, [isPlaying, progress]);
125
+
126
+ const handleStartDrag = useCallback(
127
+ (nextProgress: number) => {
128
+ setShowInteractiveLayer(true);
129
+ onStartDrag?.(nextProgress);
130
+ },
131
+ [onStartDrag],
132
+ );
133
+
134
+ const handleProgressDrag = useCallback(
135
+ (nextProgress: number) => {
136
+ onProgressDrag?.(nextProgress);
137
+ },
138
+ [onProgressDrag],
139
+ );
140
+
141
+ const handleEndDrag = useCallback(
142
+ (nextProgress: number) => {
143
+ onEndDrag?.(nextProgress);
144
+ },
145
+ [onEndDrag],
146
+ );
147
+
97
148
  const pan = useMemo(
98
149
  () =>
99
150
  Gesture.Pan()
151
+ .hitSlop(DRAG_HIT_SLOP)
100
152
  .maxPointers(1)
101
153
  .onStart(() => {
154
+ const nextProgress = clampProgress(visualProgress.value);
155
+ dragStartProgress.value = nextProgress;
156
+ isDragging.value = true;
102
157
  if (onStartDrag) {
103
- runOnJS(onStartDrag)(state.value);
158
+ runOnJS(handleStartDrag)(nextProgress);
104
159
  }
105
160
  })
106
161
  .onUpdate((event) => {
107
- const newProgress = Math.max(0, Math.min((state.value + event.x) / fullWidth, 1));
108
- state.value = newProgress;
109
- waveFormNumberFromProgress(newProgress);
162
+ if (fullWidth <= 0) {
163
+ return;
164
+ }
165
+ const nextProgress = clampProgress(
166
+ dragStartProgress.value + event.translationX / fullWidth,
167
+ );
168
+ visualProgress.value = nextProgress;
169
+ if (onProgressDrag) {
170
+ runOnJS(handleProgressDrag)(nextProgress);
171
+ }
110
172
  })
111
173
  .onEnd(() => {
174
+ isDragging.value = false;
112
175
  if (onEndDrag) {
113
- runOnJS(onEndDrag)(state.value);
176
+ runOnJS(handleEndDrag)(visualProgress.value);
114
177
  }
115
178
  }),
116
- [fullWidth, onEndDrag, onStartDrag, state, waveFormNumberFromProgress],
179
+ [
180
+ dragStartProgress,
181
+ fullWidth,
182
+ handleEndDrag,
183
+ handleProgressDrag,
184
+ handleStartDrag,
185
+ isDragging,
186
+ onEndDrag,
187
+ onProgressDrag,
188
+ onStartDrag,
189
+ visualProgress,
190
+ ],
117
191
  );
118
192
 
119
- const stringifiedWaveformData = waveformData.toString();
193
+ const stringifiedWaveformData = useMemo(() => waveformData.toString(), [waveformData]);
120
194
 
121
195
  const resampledWaveformData = useMemo(
122
196
  () => resampleWaveformData(waveformData, amplitudesCount),
@@ -124,12 +198,36 @@ export const WaveProgressBar = React.memo(
124
198
  [amplitudesCount, stringifiedWaveformData],
125
199
  );
126
200
 
201
+ const waveformHeights = useMemo(
202
+ () =>
203
+ resampledWaveformData.map((waveform) =>
204
+ waveform * WAVE_MAX_HEIGHT > WAVE_MIN_HEIGHT
205
+ ? waveform * WAVE_MAX_HEIGHT
206
+ : WAVE_MIN_HEIGHT,
207
+ ),
208
+ [resampledWaveformData],
209
+ );
210
+
211
+ const progressOverlayStyles = useAnimatedStyle(
212
+ () => ({
213
+ width: clampProgress(visualProgress.value) * maxProgressWidth,
214
+ }),
215
+ [maxProgressWidth],
216
+ );
217
+
127
218
  const thumbStyles = useAnimatedStyle(
128
219
  () => ({
129
220
  position: 'absolute',
130
- transform: [{ translateX: currentWaveformProgress * eachWaveformWidth }],
221
+ transform: [
222
+ {
223
+ translateX: Math.min(
224
+ clampProgress(visualProgress.value) * fullWidth,
225
+ maxThumbTranslateX,
226
+ ),
227
+ },
228
+ ],
131
229
  }),
132
- [currentWaveformProgress, fullWidth],
230
+ [fullWidth, maxThumbTranslateX],
133
231
  );
134
232
 
135
233
  return (
@@ -140,30 +238,33 @@ export const WaveProgressBar = React.memo(
140
238
  }}
141
239
  style={[styles.container, container]}
142
240
  >
143
- {resampledWaveformData.map((waveform, index) => (
241
+ <WaveformBars
242
+ color={semantics.chatWaveformBar}
243
+ heights={waveformHeights}
244
+ waveformStyle={waveformTheme}
245
+ />
246
+ {showInteractiveLayer ? (
144
247
  <Animated.View
145
- key={index}
146
- style={[
147
- styles.waveform,
148
- {
149
- backgroundColor:
150
- index < currentWaveformProgress
151
- ? semantics.chatWaveformBarPlaying
152
- : semantics.chatWaveformBar,
153
- height:
154
- waveform * WAVE_MAX_HEIGHT > WAVE_MIN_HEIGHT
155
- ? waveform * WAVE_MAX_HEIGHT
156
- : WAVE_MIN_HEIGHT,
157
- },
158
- waveformTheme,
159
- ]}
160
- />
161
- ))}
162
- {(onEndDrag || onProgressDrag) && (
163
- <Animated.View style={[thumbStyles, thumb]}>
164
- <ProgressControlThumb isPlaying={isPlaying} />
248
+ pointerEvents='none'
249
+ style={[styles.progressOverlay, progressOverlayStyles]}
250
+ >
251
+ <WaveformBars
252
+ color={semantics.chatWaveformBarPlaying}
253
+ heights={waveformHeights}
254
+ waveformStyle={waveformTheme}
255
+ />
165
256
  </Animated.View>
166
- )}
257
+ ) : null}
258
+ {(onEndDrag || onProgressDrag) &&
259
+ (showInteractiveLayer ? (
260
+ <Animated.View style={[thumbStyles, thumb]}>
261
+ <ProgressControlThumb isPlaying={isPlaying} />
262
+ </Animated.View>
263
+ ) : (
264
+ <View style={[styles.idleThumb, thumb]}>
265
+ <ProgressControlThumb isPlaying={isPlaying} />
266
+ </View>
267
+ ))}
167
268
  </View>
168
269
  </GestureDetector>
169
270
  );
@@ -172,6 +273,9 @@ export const WaveProgressBar = React.memo(
172
273
  if (prevProps.amplitudesCount !== nextProps.amplitudesCount) {
173
274
  return false;
174
275
  }
276
+ if (prevProps.isPlaying !== nextProps.isPlaying) {
277
+ return false;
278
+ }
175
279
  if (prevProps.progress !== nextProps.progress) {
176
280
  return false;
177
281
  } else {
@@ -182,6 +286,21 @@ export const WaveProgressBar = React.memo(
182
286
 
183
287
  const styles = StyleSheet.create({
184
288
  container: {
289
+ alignItems: 'center',
290
+ flexDirection: 'row',
291
+ position: 'relative',
292
+ },
293
+ idleThumb: {
294
+ left: 0,
295
+ position: 'absolute',
296
+ },
297
+ progressOverlay: {
298
+ left: 0,
299
+ overflow: 'hidden',
300
+ position: 'absolute',
301
+ top: 0,
302
+ },
303
+ waveformLayer: {
185
304
  alignItems: 'center',
186
305
  flexDirection: 'row',
187
306
  gap: WAVEFORM_GAP,
@@ -66,6 +66,10 @@ const RightContent = React.memo(
66
66
  );
67
67
  }
68
68
 
69
+ if (attachment?.type === FileTypes.VoiceRecording) {
70
+ return null;
71
+ }
72
+
69
73
  if (attachment && isFileAttachment(attachment)) {
70
74
  return <FileIcon mimeType={attachment.mime_type} />;
71
75
  }