stream-chat-react-native-core 9.1.0 → 9.1.1-beta.2

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.
@@ -1,23 +1,17 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
2
  import {
3
- FlatList,
4
3
  I18nManager,
5
4
  LayoutChangeEvent,
6
5
  NativeScrollEvent,
7
6
  NativeSyntheticEvent,
7
+ ScrollView,
8
+ StyleProp,
8
9
  StyleSheet,
9
10
  View,
11
+ ViewStyle,
10
12
  } from 'react-native';
11
13
 
12
- import Animated, {
13
- cancelAnimation,
14
- ZoomIn,
15
- ZoomOut,
16
- LinearTransition,
17
- useAnimatedStyle,
18
- useSharedValue,
19
- withSpring,
20
- } from 'react-native-reanimated';
14
+ import Animated, { LinearTransition, ZoomIn, ZoomOut } from 'react-native-reanimated';
21
15
 
22
16
  import {
23
17
  isLocalAudioAttachment,
@@ -39,16 +33,30 @@ import { isSoundPackageAvailable } from '../../../../native';
39
33
  import { primitives } from '../../../../theme';
40
34
 
41
35
  const END_ANCHOR_THRESHOLD = 16;
42
- const END_SHRINK_COMPENSATION_DURATION = 200;
36
+ const ATTACHMENT_PREVIEW_ANIMATION_DURATION = 200;
37
+ const TRAILING_SPACER_RELEASE_DELAY = ATTACHMENT_PREVIEW_ANIMATION_DURATION + 80;
43
38
  const MAX_AUDIO_ATTACHMENTS_CONTAINER_WIDTH = 560;
39
+ const attachmentPreviewEntering = ZoomIn.duration(ATTACHMENT_PREVIEW_ANIMATION_DURATION);
40
+ const attachmentPreviewExiting = ZoomOut.duration(ATTACHMENT_PREVIEW_ANIMATION_DURATION);
41
+ const attachmentPreviewLayout = LinearTransition.duration(ATTACHMENT_PREVIEW_ANIMATION_DURATION);
44
42
 
45
43
  export type AttachmentUploadListPreviewPropsWithContext = Record<string, never>;
46
44
 
47
- const AttachmentPreviewCell = ({ children }: { children: React.ReactNode }) => (
45
+ const AttachmentPreviewCell = ({
46
+ children,
47
+ onLayout,
48
+ style,
49
+ }: {
50
+ children: React.ReactNode;
51
+ onLayout?: (event: LayoutChangeEvent) => void;
52
+ style?: StyleProp<ViewStyle>;
53
+ }) => (
48
54
  <Animated.View
49
- entering={ZoomIn.duration(200)}
50
- exiting={ZoomOut.duration(200)}
51
- layout={LinearTransition.duration(200)}
55
+ entering={attachmentPreviewEntering}
56
+ exiting={attachmentPreviewExiting}
57
+ layout={attachmentPreviewLayout}
58
+ onLayout={onLayout}
59
+ style={style}
52
60
  >
53
61
  {children}
54
62
  </Animated.View>
@@ -65,6 +73,16 @@ const ItemSeparatorComponent = () => {
65
73
  return <View style={[styles.itemSeparator, itemSeparator]} />;
66
74
  };
67
75
 
76
+ const useLazyRef = <T,>(getInitialValue: () => T) => {
77
+ const ref = useRef<T | null>(null);
78
+
79
+ if (ref.current === null) {
80
+ ref.current = getInitialValue();
81
+ }
82
+
83
+ return ref as React.RefObject<T>;
84
+ };
85
+
68
86
  const getIsAudioAttachmentPreview =
69
87
  (soundPackageAvailable: boolean) =>
70
88
  (
@@ -88,25 +106,46 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
88
106
  const { attachmentManager } = useMessageComposer();
89
107
  const { attachments } = useAttachmentManagerState();
90
108
  const isRTL = I18nManager.isRTL;
91
- const attachmentListRef = useRef<FlatList<LocalAttachment>>(null);
92
- const soundPackageAvailable = isSoundPackageAvailable();
93
- const isAudioAttachmentPreview = getIsAudioAttachmentPreview(soundPackageAvailable);
109
+ const attachmentListRef = useRef<ScrollView>(null);
110
+ const soundPackageAvailable = useMemo(() => isSoundPackageAvailable(), []);
111
+ const isAudioAttachmentPreview = useMemo(
112
+ () => getIsAudioAttachmentPreview(soundPackageAvailable),
113
+ [soundPackageAvailable],
114
+ );
115
+ const dataRef = useLazyRef<LocalAttachment[]>(() => []);
116
+ const previousDataRef = useLazyRef<LocalAttachment[]>(() => []);
94
117
  const previousNonAudioAttachmentsLengthRef = useRef(0);
95
118
  const contentWidthRef = useRef(0);
96
119
  const itemsContentWidthRef = useRef(0);
97
120
  const viewportWidthRef = useRef(0);
98
121
  const scrollOffsetXRef = useRef(0);
99
- const rtlLeadingSpacerWidthRef = useRef(0);
100
- const endShrinkCompensationX = useSharedValue(0);
101
- const [rtlLeadingSpacerWidth, setRtlLeadingSpacerWidth] = useState(0);
102
- const previewAttachments = attachments.filter(
103
- (attachment) => !(audioRecordingSendOnComplete && isLocalVoiceRecordingAttachment(attachment)),
122
+ const attachmentCellWidthsRef = useLazyRef<Record<string, number>>(() => ({}));
123
+ const preparedRemovalIdsRef = useLazyRef<Set<string>>(() => new Set());
124
+ const spacerReleaseFramesRef = useLazyRef<Set<number>>(() => new Set());
125
+ const spacerReleaseTimeoutsRef = useLazyRef<Set<ReturnType<typeof setTimeout>>>(() => new Set());
126
+ const shouldScrollToEndOnContentSizeChangeRef = useRef(false);
127
+ const trailingSpacerWidthRef = useRef(0);
128
+ const [trailingSpacerWidth, setTrailingSpacerWidth] = useState(0);
129
+ const previewAttachments = useMemo(
130
+ () =>
131
+ attachments.filter(
132
+ (attachment) =>
133
+ !(audioRecordingSendOnComplete && isLocalVoiceRecordingAttachment(attachment)),
134
+ ),
135
+ [attachments, audioRecordingSendOnComplete],
136
+ );
137
+ const audioAttachments = useMemo(
138
+ () => previewAttachments.filter(isAudioAttachmentPreview),
139
+ [isAudioAttachmentPreview, previewAttachments],
140
+ );
141
+ const nonAudioAttachments = useMemo(
142
+ () => previewAttachments.filter((attachment) => !isAudioAttachmentPreview(attachment)),
143
+ [isAudioAttachmentPreview, previewAttachments],
104
144
  );
105
- const audioAttachments = previewAttachments.filter(isAudioAttachmentPreview);
106
- const nonAudioAttachments = previewAttachments.filter(
107
- (attachment) => !isAudioAttachmentPreview(attachment),
145
+ const data = useMemo(
146
+ () => (isRTL ? nonAudioAttachments.toReversed() : nonAudioAttachments),
147
+ [isRTL, nonAudioAttachments],
108
148
  );
109
- const data = isRTL ? nonAudioAttachments.toReversed() : nonAudioAttachments;
110
149
 
111
150
  const {
112
151
  theme: {
@@ -116,90 +155,254 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
116
155
  },
117
156
  } = useTheme();
118
157
 
119
- const updateRtlLeadingSpacerWidth = useCallback(
120
- (itemsWidth: number, viewportWidth: number) => {
121
- if (!isRTL || !viewportWidth) {
122
- if (rtlLeadingSpacerWidthRef.current !== 0) {
123
- rtlLeadingSpacerWidthRef.current = 0;
124
- setRtlLeadingSpacerWidth(0);
158
+ const scrollToOffset = useCallback((offset: number, animated = false) => {
159
+ const nextOffset = Math.max(0, offset);
160
+
161
+ attachmentListRef.current?.scrollTo({
162
+ animated,
163
+ x: nextOffset,
164
+ });
165
+ scrollOffsetXRef.current = nextOffset;
166
+ }, []);
167
+
168
+ const setTrailingSpacerLayoutWidth = useCallback((width: number) => {
169
+ const nextWidth = Math.max(0, width);
170
+ trailingSpacerWidthRef.current = nextWidth;
171
+ setTrailingSpacerWidth(nextWidth);
172
+ }, []);
173
+
174
+ const prepareTrailingSpacer = useCallback(
175
+ (width: number) => {
176
+ if (width <= 0) {
177
+ return;
178
+ }
179
+
180
+ const nextWidth = trailingSpacerWidthRef.current + width;
181
+ setTrailingSpacerLayoutWidth(nextWidth);
182
+ },
183
+ [setTrailingSpacerLayoutWidth],
184
+ );
185
+
186
+ const scheduleTrailingSpacerRelease = useCallback(
187
+ (width: number) => {
188
+ if (width <= 0) {
189
+ return;
190
+ }
191
+
192
+ const timeout = setTimeout(() => {
193
+ spacerReleaseTimeoutsRef.current.delete(timeout);
194
+
195
+ const firstFrame = requestAnimationFrame(() => {
196
+ spacerReleaseFramesRef.current.delete(firstFrame);
197
+
198
+ const secondFrame = requestAnimationFrame(() => {
199
+ spacerReleaseFramesRef.current.delete(secondFrame);
200
+ setTrailingSpacerLayoutWidth(trailingSpacerWidthRef.current - width);
201
+ });
202
+
203
+ spacerReleaseFramesRef.current.add(secondFrame);
204
+ });
205
+
206
+ spacerReleaseFramesRef.current.add(firstFrame);
207
+ }, TRAILING_SPACER_RELEASE_DELAY);
208
+
209
+ spacerReleaseTimeoutsRef.current.add(timeout);
210
+ },
211
+ [setTrailingSpacerLayoutWidth, spacerReleaseFramesRef, spacerReleaseTimeoutsRef],
212
+ );
213
+
214
+ const getRemovalMetrics = useCallback(
215
+ (ids: string[], baseData: LocalAttachment[]) => {
216
+ const removedIds = new Set(ids);
217
+ const fallbackCellWidth = baseData.length
218
+ ? itemsContentWidthRef.current / baseData.length
219
+ : 0;
220
+ const offsetBefore = scrollOffsetXRef.current;
221
+ const oldMaxOffset = Math.max(0, itemsContentWidthRef.current - viewportWidthRef.current);
222
+ const wasNearEnd = oldMaxOffset - offsetBefore <= END_ANCHOR_THRESHOLD;
223
+ let contentOffset = 0;
224
+ let removedContentWidth = 0;
225
+ let anchorCorrectionWidth = 0;
226
+
227
+ baseData.forEach((attachment) => {
228
+ const attachmentId = attachment.localMetadata.id;
229
+ const cellWidth = attachmentCellWidthsRef.current[attachmentId] ?? fallbackCellWidth;
230
+
231
+ if (removedIds.has(attachmentId)) {
232
+ removedContentWidth += cellWidth;
233
+ if (contentOffset <= offsetBefore) {
234
+ anchorCorrectionWidth += cellWidth;
235
+ }
125
236
  }
237
+
238
+ contentOffset += cellWidth;
239
+ });
240
+
241
+ if (!removedContentWidth) {
242
+ return {
243
+ removedContentWidth: 0,
244
+ scrollCorrectionWidth: 0,
245
+ };
246
+ }
247
+
248
+ return {
249
+ removedContentWidth,
250
+ scrollCorrectionWidth: wasNearEnd
251
+ ? removedContentWidth
252
+ : Math.min(anchorCorrectionWidth, removedContentWidth),
253
+ };
254
+ },
255
+ [attachmentCellWidthsRef],
256
+ );
257
+
258
+ const applyRemovalScrollCorrection = useCallback(
259
+ (removedContentWidth: number, scrollCorrectionWidth: number) => {
260
+ if (removedContentWidth <= 0 || isRTL) {
126
261
  return;
127
262
  }
128
263
 
129
- const nextSpacerWidth = Math.max(0, viewportWidth - itemsWidth);
130
- if (rtlLeadingSpacerWidthRef.current === nextSpacerWidth) {
264
+ const offsetBefore = scrollOffsetXRef.current;
265
+ const nextContentWidth = Math.max(0, itemsContentWidthRef.current - removedContentWidth);
266
+ const nextMaxOffset = Math.max(0, nextContentWidth - viewportWidthRef.current);
267
+ const nextOffset = Math.min(nextMaxOffset, Math.max(0, offsetBefore - scrollCorrectionWidth));
268
+
269
+ if (nextOffset !== offsetBefore) {
270
+ scrollToOffset(nextOffset, true);
271
+ }
272
+ },
273
+ [isRTL, scrollToOffset],
274
+ );
275
+
276
+ const prepareForRemoval = useCallback(
277
+ (ids: string[], baseData: LocalAttachment[]) => {
278
+ const { removedContentWidth, scrollCorrectionWidth } = getRemovalMetrics(ids, baseData);
279
+
280
+ if (!removedContentWidth) {
131
281
  return;
132
282
  }
133
283
 
134
- rtlLeadingSpacerWidthRef.current = nextSpacerWidth;
135
- setRtlLeadingSpacerWidth(nextSpacerWidth);
284
+ if (!isRTL) {
285
+ prepareTrailingSpacer(removedContentWidth);
286
+ }
287
+ applyRemovalScrollCorrection(removedContentWidth, scrollCorrectionWidth);
288
+ ids.forEach((id) => preparedRemovalIdsRef.current.add(id));
289
+ },
290
+ [
291
+ applyRemovalScrollCorrection,
292
+ getRemovalMetrics,
293
+ isRTL,
294
+ preparedRemovalIdsRef,
295
+ prepareTrailingSpacer,
296
+ ],
297
+ );
298
+
299
+ const removeAttachments = useCallback(
300
+ (ids: string[]) => {
301
+ prepareForRemoval(ids, dataRef.current);
302
+ attachmentManager.removeAttachments(ids);
136
303
  },
137
- [isRTL],
304
+ [attachmentManager, dataRef, prepareForRemoval],
138
305
  );
139
306
 
140
- const renderItem = useCallback(
141
- ({ item }: { item: LocalAttachment }) => {
142
- if (isLocalImageAttachment(item)) {
307
+ useLayoutEffect(() => {
308
+ const previousData = previousDataRef.current;
309
+ const nextIds = new Set(data.map((attachment) => attachment.localMetadata.id));
310
+ const removedIds = previousData
311
+ .map((attachment) => attachment.localMetadata.id)
312
+ .filter((id) => !nextIds.has(id));
313
+
314
+ if (removedIds.length) {
315
+ const { removedContentWidth } = getRemovalMetrics(removedIds, previousData);
316
+ const unpreparedRemovedIds = removedIds.filter(
317
+ (id) => !preparedRemovalIdsRef.current.has(id),
318
+ );
319
+
320
+ const didPrepareAfterRemovalCommit = unpreparedRemovedIds.length > 0;
321
+
322
+ if (didPrepareAfterRemovalCommit) {
323
+ prepareForRemoval(unpreparedRemovedIds, previousData);
324
+ }
325
+
326
+ removedIds.forEach((id) => preparedRemovalIdsRef.current.delete(id));
327
+ if (!isRTL) {
328
+ scheduleTrailingSpacerRelease(removedContentWidth);
329
+ }
330
+ }
331
+
332
+ previousDataRef.current = data;
333
+ dataRef.current = data;
334
+ }, [
335
+ data,
336
+ dataRef,
337
+ getRemovalMetrics,
338
+ isRTL,
339
+ preparedRemovalIdsRef,
340
+ prepareForRemoval,
341
+ previousDataRef,
342
+ scheduleTrailingSpacerRelease,
343
+ ]);
344
+
345
+ useEffect(
346
+ () => () => {
347
+ spacerReleaseFramesRef.current.forEach(cancelAnimationFrame);
348
+ spacerReleaseFramesRef.current.clear();
349
+ spacerReleaseTimeoutsRef.current.forEach(clearTimeout);
350
+ spacerReleaseTimeoutsRef.current.clear();
351
+ },
352
+ [spacerReleaseFramesRef, spacerReleaseTimeoutsRef],
353
+ );
354
+
355
+ const renderAttachmentPreview = useCallback(
356
+ (attachment: LocalAttachment) => {
357
+ if (isLocalImageAttachment(attachment)) {
143
358
  return (
144
- <AttachmentPreviewCell>
145
- <ImageAttachmentUploadPreview
146
- attachment={item}
147
- handleRetry={attachmentManager.uploadAttachment}
148
- removeAttachments={attachmentManager.removeAttachments}
149
- />
150
- </AttachmentPreviewCell>
359
+ <ImageAttachmentUploadPreview
360
+ attachment={attachment}
361
+ handleRetry={attachmentManager.uploadAttachment}
362
+ removeAttachments={removeAttachments}
363
+ />
151
364
  );
152
- } else if (isLocalVoiceRecordingAttachment(item)) {
365
+ } else if (isLocalVoiceRecordingAttachment(attachment)) {
153
366
  return (
154
- <AttachmentPreviewCell>
367
+ <AudioAttachmentUploadPreview
368
+ attachment={attachment}
369
+ handleRetry={attachmentManager.uploadAttachment}
370
+ removeAttachments={removeAttachments}
371
+ />
372
+ );
373
+ } else if (isLocalAudioAttachment(attachment)) {
374
+ if (soundPackageAvailable) {
375
+ return (
155
376
  <AudioAttachmentUploadPreview
156
- attachment={item}
377
+ attachment={attachment}
157
378
  handleRetry={attachmentManager.uploadAttachment}
158
- removeAttachments={attachmentManager.removeAttachments}
379
+ removeAttachments={removeAttachments}
159
380
  />
160
- </AttachmentPreviewCell>
161
- );
162
- } else if (isLocalAudioAttachment(item)) {
163
- if (isSoundPackageAvailable()) {
164
- return (
165
- <AttachmentPreviewCell>
166
- <AudioAttachmentUploadPreview
167
- attachment={item}
168
- handleRetry={attachmentManager.uploadAttachment}
169
- removeAttachments={attachmentManager.removeAttachments}
170
- />
171
- </AttachmentPreviewCell>
172
381
  );
173
382
  } else {
174
383
  return (
175
- <AttachmentPreviewCell>
176
- <FileAttachmentUploadPreview
177
- attachment={item}
178
- handleRetry={attachmentManager.uploadAttachment}
179
- removeAttachments={attachmentManager.removeAttachments}
180
- />
181
- </AttachmentPreviewCell>
384
+ <FileAttachmentUploadPreview
385
+ attachment={attachment}
386
+ handleRetry={attachmentManager.uploadAttachment}
387
+ removeAttachments={removeAttachments}
388
+ />
182
389
  );
183
390
  }
184
- } else if (isVideoAttachment(item)) {
391
+ } else if (isVideoAttachment(attachment)) {
185
392
  return (
186
- <AttachmentPreviewCell>
187
- <VideoAttachmentUploadPreview
188
- attachment={item}
189
- handleRetry={attachmentManager.uploadAttachment}
190
- removeAttachments={attachmentManager.removeAttachments}
191
- />
192
- </AttachmentPreviewCell>
393
+ <VideoAttachmentUploadPreview
394
+ attachment={attachment}
395
+ handleRetry={attachmentManager.uploadAttachment}
396
+ removeAttachments={removeAttachments}
397
+ />
193
398
  );
194
- } else if (isLocalFileAttachment(item)) {
399
+ } else if (isLocalFileAttachment(attachment)) {
195
400
  return (
196
- <AttachmentPreviewCell>
197
- <FileAttachmentUploadPreview
198
- attachment={item}
199
- handleRetry={attachmentManager.uploadAttachment}
200
- removeAttachments={attachmentManager.removeAttachments}
201
- />
202
- </AttachmentPreviewCell>
401
+ <FileAttachmentUploadPreview
402
+ attachment={attachment}
403
+ handleRetry={attachmentManager.uploadAttachment}
404
+ removeAttachments={removeAttachments}
405
+ />
203
406
  );
204
407
  } else return null;
205
408
  },
@@ -208,8 +411,9 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
208
411
  FileAttachmentUploadPreview,
209
412
  ImageAttachmentUploadPreview,
210
413
  VideoAttachmentUploadPreview,
211
- attachmentManager.removeAttachments,
212
414
  attachmentManager.uploadAttachment,
415
+ removeAttachments,
416
+ soundPackageAvailable,
213
417
  ],
214
418
  );
215
419
 
@@ -217,62 +421,61 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
217
421
  scrollOffsetXRef.current = event.nativeEvent.contentOffset.x;
218
422
  }, []);
219
423
 
220
- const onLayoutHandler = useCallback(
221
- (event: LayoutChangeEvent) => {
222
- const viewportWidth = event.nativeEvent.layout.width;
223
- viewportWidthRef.current = viewportWidth;
224
- updateRtlLeadingSpacerWidth(itemsContentWidthRef.current, viewportWidth);
424
+ const scrollToEndOffset = useCallback(
425
+ (contentWidth: number, animated = true) => {
426
+ if (isRTL) {
427
+ return;
428
+ }
429
+
430
+ scrollToOffset(Math.max(0, contentWidth - viewportWidthRef.current), animated);
431
+ },
432
+ [isRTL, scrollToOffset],
433
+ );
434
+
435
+ const onLayoutHandler = useCallback((event: LayoutChangeEvent) => {
436
+ viewportWidthRef.current = event.nativeEvent.layout.width;
437
+ }, []);
438
+
439
+ const onAttachmentCellLayout = useCallback(
440
+ (id: string, event: LayoutChangeEvent) => {
441
+ attachmentCellWidthsRef.current[id] = event.nativeEvent.layout.width;
225
442
  },
226
- [updateRtlLeadingSpacerWidth],
443
+ [attachmentCellWidthsRef],
227
444
  );
228
445
 
229
446
  const onContentSizeChangeHandler = useCallback(
230
447
  (width: number) => {
231
- const itemsContentWidth = isRTL
232
- ? Math.max(0, width - rtlLeadingSpacerWidthRef.current)
233
- : width;
448
+ const scrollableContentWidth = width;
449
+ const itemsContentWidth = Math.max(
450
+ 0,
451
+ scrollableContentWidth - trailingSpacerWidthRef.current,
452
+ );
234
453
  const previousContentWidth = contentWidthRef.current;
235
454
  contentWidthRef.current = itemsContentWidth;
236
455
  itemsContentWidthRef.current = itemsContentWidth;
237
- updateRtlLeadingSpacerWidth(itemsContentWidth, viewportWidthRef.current);
238
456
 
239
- if (!previousContentWidth || itemsContentWidth >= previousContentWidth) {
457
+ if (
458
+ shouldScrollToEndOnContentSizeChangeRef.current &&
459
+ itemsContentWidth > previousContentWidth
460
+ ) {
461
+ shouldScrollToEndOnContentSizeChangeRef.current = false;
462
+ scrollToEndOffset(scrollableContentWidth);
240
463
  return;
241
464
  }
242
465
 
243
- const oldMaxOffset = Math.max(0, previousContentWidth - viewportWidthRef.current);
244
- const newMaxOffset = Math.max(0, itemsContentWidth - viewportWidthRef.current);
245
- const offsetBefore = scrollOffsetXRef.current;
246
- const wasNearEnd = oldMaxOffset - offsetBefore <= END_ANCHOR_THRESHOLD;
247
- const overshoot = Math.max(0, offsetBefore - newMaxOffset);
248
- const shouldAnchorEnd = wasNearEnd || overshoot > 0;
249
-
250
- if (!shouldAnchorEnd) {
466
+ if (!previousContentWidth || itemsContentWidth >= previousContentWidth) {
251
467
  return;
252
468
  }
253
469
 
254
- if (overshoot > 0) {
255
- attachmentListRef.current?.scrollToOffset({
256
- animated: false,
257
- offset: newMaxOffset,
258
- });
259
- scrollOffsetXRef.current = newMaxOffset;
260
- }
261
-
262
- if (isRTL) {
263
- return;
264
- }
470
+ const actualMaxOffset = Math.max(0, scrollableContentWidth - viewportWidthRef.current);
471
+ const offsetBefore = scrollOffsetXRef.current;
472
+ const overshoot = Math.max(0, offsetBefore - actualMaxOffset);
265
473
 
266
- const compensation = newMaxOffset - oldMaxOffset;
267
- if (compensation !== 0) {
268
- cancelAnimation(endShrinkCompensationX);
269
- endShrinkCompensationX.value = compensation;
270
- endShrinkCompensationX.value = withSpring(0, {
271
- duration: END_SHRINK_COMPENSATION_DURATION,
272
- });
474
+ if (overshoot > END_ANCHOR_THRESHOLD) {
475
+ scrollToOffset(actualMaxOffset);
273
476
  }
274
477
  },
275
- [endShrinkCompensationX, isRTL, updateRtlLeadingSpacerWidth],
478
+ [scrollToEndOffset, scrollToOffset],
276
479
  );
277
480
 
278
481
  useEffect(() => {
@@ -285,20 +488,8 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
285
488
  return;
286
489
  }
287
490
 
288
- cancelAnimation(endShrinkCompensationX);
289
- endShrinkCompensationX.value = 0;
290
- requestAnimationFrame(() => {
291
- if (isRTL) {
292
- return;
293
- }
294
-
295
- attachmentListRef.current?.scrollToEnd({ animated: true });
296
- });
297
- }, [endShrinkCompensationX, isRTL, nonAudioAttachments.length]);
298
-
299
- const animatedListWrapperStyle = useAnimatedStyle(() => ({
300
- transform: [{ translateX: endShrinkCompensationX.value }],
301
- }));
491
+ shouldScrollToEndOnContentSizeChangeRef.current = true;
492
+ }, [nonAudioAttachments.length]);
302
493
 
303
494
  if (!previewAttachments.length) {
304
495
  return null;
@@ -308,9 +499,9 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
308
499
  <>
309
500
  {audioAttachments.length ? (
310
501
  <Animated.View
311
- entering={ZoomIn.duration(200)}
312
- exiting={ZoomOut.duration(200)}
313
- layout={LinearTransition.duration(200)}
502
+ entering={attachmentPreviewEntering}
503
+ exiting={attachmentPreviewExiting}
504
+ layout={attachmentPreviewLayout}
314
505
  style={[styles.audioAttachmentsContainer, audioAttachmentsContainer]}
315
506
  >
316
507
  {audioAttachments.map((attachment) => (
@@ -318,7 +509,7 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
318
509
  <AudioAttachmentUploadPreview
319
510
  attachment={attachment}
320
511
  handleRetry={attachmentManager.uploadAttachment}
321
- removeAttachments={attachmentManager.removeAttachments}
512
+ removeAttachments={removeAttachments}
322
513
  />
323
514
  </AttachmentPreviewCell>
324
515
  ))}
@@ -327,33 +518,46 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
327
518
 
328
519
  {data.length ? (
329
520
  <Animated.View
330
- entering={ZoomIn.duration(200)}
331
- exiting={ZoomOut.duration(200)}
332
- layout={LinearTransition.duration(200)}
521
+ entering={attachmentPreviewEntering}
522
+ exiting={attachmentPreviewExiting}
523
+ layout={attachmentPreviewLayout}
524
+ style={styles.flatListContainer}
333
525
  >
334
- <Animated.View style={animatedListWrapperStyle}>
335
- <FlatList
336
- data={data}
337
- horizontal
338
- ItemSeparatorComponent={ItemSeparatorComponent}
339
- keyExtractor={(item) => item.localMetadata.id}
340
- ListHeaderComponent={
341
- isRTL && rtlLeadingSpacerWidth > 0 ? (
342
- <View style={{ width: rtlLeadingSpacerWidth }} />
343
- ) : null
344
- }
345
- onContentSizeChange={onContentSizeChangeHandler}
346
- onLayout={onLayoutHandler}
347
- onScroll={onScrollHandler}
348
- removeClippedSubviews={false}
349
- ref={attachmentListRef}
350
- renderItem={renderItem}
351
- scrollEventThrottle={16}
352
- showsHorizontalScrollIndicator={false}
353
- style={[styles.flatList, flatList]}
354
- testID={'attachment-upload-preview-list'}
355
- />
356
- </Animated.View>
526
+ <ScrollView
527
+ contentContainerStyle={styles.flatListContentContainer}
528
+ horizontal
529
+ onContentSizeChange={onContentSizeChangeHandler}
530
+ onLayout={onLayoutHandler}
531
+ onScroll={onScrollHandler}
532
+ ref={attachmentListRef}
533
+ scrollEventThrottle={16}
534
+ showsHorizontalScrollIndicator={false}
535
+ style={[styles.flatList, flatList]}
536
+ testID={'attachment-upload-preview-list'}
537
+ >
538
+ {data.map((attachment, index) => {
539
+ const attachmentId = attachment.localMetadata.id;
540
+
541
+ return (
542
+ <AttachmentPreviewCell
543
+ key={attachmentId}
544
+ onLayout={(event) => onAttachmentCellLayout(attachmentId, event)}
545
+ style={styles.attachmentPreviewCell}
546
+ >
547
+ {index > 0 ? <ItemSeparatorComponent /> : null}
548
+ <View collapsable={false} style={styles.attachmentPreviewContent}>
549
+ {renderAttachmentPreview(attachment)}
550
+ </View>
551
+ </AttachmentPreviewCell>
552
+ );
553
+ })}
554
+ {!isRTL ? (
555
+ <View
556
+ pointerEvents={'none'}
557
+ style={[styles.trailingSpacer, { width: trailingSpacerWidth }]}
558
+ />
559
+ ) : null}
560
+ </ScrollView>
357
561
  </Animated.View>
358
562
  ) : null}
359
563
  </>
@@ -373,17 +577,37 @@ const MemoizedAttachmentUploadPreviewListWithContext = React.memo(
373
577
  export const AttachmentUploadPreviewList = () => <MemoizedAttachmentUploadPreviewListWithContext />;
374
578
 
375
579
  const styles = StyleSheet.create({
580
+ attachmentPreviewCell: {
581
+ alignItems: 'flex-start',
582
+ flexDirection: 'row',
583
+ flexShrink: 0,
584
+ },
585
+ attachmentPreviewContent: {
586
+ flexShrink: 0,
587
+ },
376
588
  audioAttachmentsContainer: {
377
589
  maxWidth: MAX_AUDIO_ATTACHMENTS_CONTAINER_WIDTH,
378
590
  width: '100%',
379
591
  },
380
592
  flatList: {
381
- overflow: 'visible',
382
593
  direction: 'ltr',
594
+ flexGrow: 0,
595
+ overflow: 'visible',
596
+ },
597
+ flatListContentContainer: {
598
+ alignItems: 'flex-start',
599
+ },
600
+ flatListContainer: {
601
+ alignSelf: 'flex-start',
602
+ flexShrink: 1,
603
+ maxWidth: '100%',
383
604
  },
384
605
  itemSeparator: {
385
606
  width: primitives.spacingXs,
386
607
  },
608
+ trailingSpacer: {
609
+ flexShrink: 0,
610
+ },
387
611
  });
388
612
 
389
613
  AttachmentUploadPreviewList.displayName =