Skip to content

Commit bc840d3

Browse files
committed
fix: tests and final touches
1 parent 073c557 commit bc840d3

2 files changed

Lines changed: 89 additions & 63 deletions

File tree

package/src/components/MessageInput/__tests__/AttachmentUploadPreviewList.test.tsx

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { ComponentProps } from 'react';
22

3-
import { ActivityIndicator, FlatList } from 'react-native';
3+
import { ActivityIndicator, ScrollView } from 'react-native';
44

55
import type { ReactTestInstance } from 'react-test-renderer';
66

@@ -179,14 +179,8 @@ describe('AttachmentUploadPreviewList', () => {
179179

180180
describe('FileAttachmentUploadPreview', () => {
181181
it('anchors the preview list to the end when content shrinks near the end', () => {
182-
const requestAnimationFrameSpy = jest
183-
.spyOn(global, 'requestAnimationFrame')
184-
.mockImplementation((callback) => {
185-
callback(0);
186-
return 0;
187-
});
188-
const scrollToOffsetSpy = jest
189-
.spyOn(FlatList.prototype, 'scrollToOffset')
182+
const scrollToSpy = jest
183+
.spyOn(ScrollView.prototype, 'scrollTo')
190184
.mockImplementation(() => undefined);
191185
try {
192186
const attachments = [
@@ -211,22 +205,21 @@ describe('AttachmentUploadPreviewList', () => {
211205

212206
renderComponent({ channel, client, props });
213207

214-
const list = screen.UNSAFE_getByType(FlatList);
208+
const list = screen.UNSAFE_getByType(ScrollView);
215209

216210
act(() => {
217211
fireEvent(list, 'layout', { nativeEvent: { layout: { width: 100 } } });
218212
list.props.onContentSizeChange(300, 0);
219213
list.props.onScroll({ nativeEvent: { contentOffset: { x: 190 } } });
220-
list.props.onContentSizeChange(295, 0);
214+
list.props.onContentSizeChange(250, 0);
221215
});
222216

223-
expect(scrollToOffsetSpy).toHaveBeenCalledWith({
224-
animated: true,
225-
offset: 195,
217+
expect(scrollToSpy).toHaveBeenCalledWith({
218+
animated: false,
219+
x: 150,
226220
});
227221
} finally {
228-
requestAnimationFrameSpy.mockRestore();
229-
scrollToOffsetSpy.mockRestore();
222+
scrollToSpy.mockRestore();
230223
}
231224
});
232225

package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.tsx

Lines changed: 80 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ const ItemSeparatorComponent = () => {
7373
return <View style={[styles.itemSeparator, itemSeparator]} />;
7474
};
7575

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+
7686
const getIsAudioAttachmentPreview =
7787
(soundPackageAvailable: boolean) =>
7888
(
@@ -102,17 +112,17 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
102112
() => getIsAudioAttachmentPreview(soundPackageAvailable),
103113
[soundPackageAvailable],
104114
);
105-
const dataRef = useRef<LocalAttachment[]>([]);
106-
const previousDataRef = useRef<LocalAttachment[]>([]);
115+
const dataRef = useLazyRef<LocalAttachment[]>(() => []);
116+
const previousDataRef = useLazyRef<LocalAttachment[]>(() => []);
107117
const previousNonAudioAttachmentsLengthRef = useRef(0);
108118
const contentWidthRef = useRef(0);
109119
const itemsContentWidthRef = useRef(0);
110120
const viewportWidthRef = useRef(0);
111121
const scrollOffsetXRef = useRef(0);
112-
const attachmentCellWidthsRef = useRef<Record<string, number>>({});
113-
const preparedRemovalIdsRef = useRef<Set<string>>(new Set());
114-
const spacerReleaseFramesRef = useRef<Set<number>>(new Set());
115-
const spacerReleaseTimeoutsRef = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());
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());
116126
const shouldScrollToEndOnContentSizeChangeRef = useRef(false);
117127
const trailingSpacerWidthRef = useRef(0);
118128
const [trailingSpacerWidth, setTrailingSpacerWidth] = useState(0);
@@ -198,47 +208,52 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
198208

199209
spacerReleaseTimeoutsRef.current.add(timeout);
200210
},
201-
[setTrailingSpacerLayoutWidth],
211+
[setTrailingSpacerLayoutWidth, spacerReleaseFramesRef, spacerReleaseTimeoutsRef],
202212
);
203213

204-
const getRemovalMetrics = useCallback((ids: string[], baseData: LocalAttachment[]) => {
205-
const removedIds = new Set(ids);
206-
const fallbackCellWidth = baseData.length ? itemsContentWidthRef.current / baseData.length : 0;
207-
const offsetBefore = scrollOffsetXRef.current;
208-
const oldMaxOffset = Math.max(0, itemsContentWidthRef.current - viewportWidthRef.current);
209-
const wasNearEnd = oldMaxOffset - offsetBefore <= END_ANCHOR_THRESHOLD;
210-
let contentOffset = 0;
211-
let removedContentWidth = 0;
212-
let anchorCorrectionWidth = 0;
213-
214-
baseData.forEach((attachment) => {
215-
const attachmentId = attachment.localMetadata.id;
216-
const cellWidth = attachmentCellWidthsRef.current[attachmentId] ?? fallbackCellWidth;
217-
218-
if (removedIds.has(attachmentId)) {
219-
removedContentWidth += cellWidth;
220-
if (contentOffset <= offsetBefore) {
221-
anchorCorrectionWidth += cellWidth;
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+
}
222236
}
223-
}
224237

225-
contentOffset += cellWidth;
226-
});
238+
contentOffset += cellWidth;
239+
});
240+
241+
if (!removedContentWidth) {
242+
return {
243+
removedContentWidth: 0,
244+
scrollCorrectionWidth: 0,
245+
};
246+
}
227247

228-
if (!removedContentWidth) {
229248
return {
230-
removedContentWidth: 0,
231-
scrollCorrectionWidth: 0,
249+
removedContentWidth,
250+
scrollCorrectionWidth: wasNearEnd
251+
? removedContentWidth
252+
: Math.min(anchorCorrectionWidth, removedContentWidth),
232253
};
233-
}
234-
235-
return {
236-
removedContentWidth,
237-
scrollCorrectionWidth: wasNearEnd
238-
? removedContentWidth
239-
: Math.min(anchorCorrectionWidth, removedContentWidth),
240-
};
241-
}, []);
254+
},
255+
[attachmentCellWidthsRef],
256+
);
242257

243258
const applyRemovalScrollCorrection = useCallback(
244259
(removedContentWidth: number, scrollCorrectionWidth: number) => {
@@ -272,15 +287,21 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
272287
applyRemovalScrollCorrection(removedContentWidth, scrollCorrectionWidth);
273288
ids.forEach((id) => preparedRemovalIdsRef.current.add(id));
274289
},
275-
[applyRemovalScrollCorrection, getRemovalMetrics, isRTL, prepareTrailingSpacer],
290+
[
291+
applyRemovalScrollCorrection,
292+
getRemovalMetrics,
293+
isRTL,
294+
preparedRemovalIdsRef,
295+
prepareTrailingSpacer,
296+
],
276297
);
277298

278299
const removeAttachments = useCallback(
279300
(ids: string[]) => {
280301
prepareForRemoval(ids, dataRef.current);
281302
attachmentManager.removeAttachments(ids);
282303
},
283-
[attachmentManager, prepareForRemoval],
304+
[attachmentManager, dataRef, prepareForRemoval],
284305
);
285306

286307
useLayoutEffect(() => {
@@ -310,7 +331,16 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
310331

311332
previousDataRef.current = data;
312333
dataRef.current = data;
313-
}, [data, getRemovalMetrics, isRTL, prepareForRemoval, scheduleTrailingSpacerRelease]);
334+
}, [
335+
data,
336+
dataRef,
337+
getRemovalMetrics,
338+
isRTL,
339+
preparedRemovalIdsRef,
340+
prepareForRemoval,
341+
previousDataRef,
342+
scheduleTrailingSpacerRelease,
343+
]);
314344

315345
useEffect(
316346
() => () => {
@@ -319,7 +349,7 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
319349
spacerReleaseTimeoutsRef.current.forEach(clearTimeout);
320350
spacerReleaseTimeoutsRef.current.clear();
321351
},
322-
[],
352+
[spacerReleaseFramesRef, spacerReleaseTimeoutsRef],
323353
);
324354

325355
const renderAttachmentPreview = useCallback(
@@ -406,9 +436,12 @@ const UnMemoizedAttachmentUploadPreviewList = () => {
406436
viewportWidthRef.current = event.nativeEvent.layout.width;
407437
}, []);
408438

409-
const onAttachmentCellLayout = useCallback((id: string, event: LayoutChangeEvent) => {
410-
attachmentCellWidthsRef.current[id] = event.nativeEvent.layout.width;
411-
}, []);
439+
const onAttachmentCellLayout = useCallback(
440+
(id: string, event: LayoutChangeEvent) => {
441+
attachmentCellWidthsRef.current[id] = event.nativeEvent.layout.width;
442+
},
443+
[attachmentCellWidthsRef],
444+
);
412445

413446
const onContentSizeChangeHandler = useCallback(
414447
(width: number) => {

0 commit comments

Comments
 (0)