1- import React , { useCallback } from 'react' ;
2- import { FlatList , StyleSheet , View } from 'react-native' ;
1+ import React , { useCallback , useEffect , useRef } from 'react' ;
2+ import {
3+ FlatList ,
4+ LayoutChangeEvent ,
5+ NativeScrollEvent ,
6+ NativeSyntheticEvent ,
7+ StyleSheet ,
8+ View ,
9+ } from 'react-native' ;
310
4- import Animated , { FadeIn , FadeOut , LinearTransition } from 'react-native-reanimated' ;
11+ import Animated , {
12+ cancelAnimation ,
13+ ZoomIn ,
14+ ZoomOut ,
15+ LinearTransition ,
16+ useAnimatedStyle ,
17+ useSharedValue ,
18+ withSpring ,
19+ } from 'react-native-reanimated' ;
520
621import {
722 isLocalAudioAttachment ,
@@ -22,8 +37,8 @@ import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
2237import { isSoundPackageAvailable } from '../../../../native' ;
2338import { primitives } from '../../../../theme' ;
2439
25- const IMAGE_PREVIEW_SIZE = 72 ;
26- const FILE_PREVIEW_HEIGHT = 224 ;
40+ const END_ANCHOR_THRESHOLD = 16 ;
41+ const END_SHRINK_COMPENSATION_DURATION = 200 ;
2742
2843export type AttachmentUploadListPreviewPropsWithContext = Pick <
2944 MessageInputContextValue ,
@@ -33,6 +48,16 @@ export type AttachmentUploadListPreviewPropsWithContext = Pick<
3348 | 'VideoAttachmentUploadPreview'
3449> ;
3550
51+ const AttachmentPreviewCell = ( { children } : { children : React . ReactNode } ) => (
52+ < Animated . View
53+ entering = { ZoomIn . duration ( 200 ) }
54+ exiting = { ZoomOut . duration ( 200 ) }
55+ layout = { LinearTransition . duration ( 200 ) }
56+ >
57+ { children }
58+ </ Animated . View >
59+ ) ;
60+
3661const ItemSeparatorComponent = ( ) => {
3762 const {
3863 theme : {
@@ -44,22 +69,6 @@ const ItemSeparatorComponent = () => {
4469 return < View style = { [ styles . itemSeparator , itemSeparator ] } /> ;
4570} ;
4671
47- const getItemLayout = ( data : ArrayLike < LocalAttachment > | null | undefined , index : number ) => {
48- const item = data ?. [ index ] ;
49- if ( item && isLocalImageAttachment ( item as LocalAttachment ) ) {
50- return {
51- index,
52- length : IMAGE_PREVIEW_SIZE + 8 ,
53- offset : ( IMAGE_PREVIEW_SIZE + 8 ) * index ,
54- } ;
55- }
56- return {
57- index,
58- length : FILE_PREVIEW_HEIGHT + 8 ,
59- offset : ( FILE_PREVIEW_HEIGHT + 8 ) * index ,
60- } ;
61- } ;
62-
6372/**
6473 * AttachmentUploadPreviewList
6574 * UI Component to preview the files set for upload
@@ -75,6 +84,12 @@ const UnMemoizedAttachmentUploadPreviewList = (
7584 } = props ;
7685 const { attachmentManager } = useMessageComposer ( ) ;
7786 const { attachments } = useAttachmentManagerState ( ) ;
87+ const attachmentListRef = useRef < FlatList < LocalAttachment > > ( null ) ;
88+ const previousAttachmentsLengthRef = useRef ( attachments . length ) ;
89+ const contentWidthRef = useRef ( 0 ) ;
90+ const viewportWidthRef = useRef ( 0 ) ;
91+ const scrollOffsetXRef = useRef ( 0 ) ;
92+ const endShrinkCompensationX = useSharedValue ( 0 ) ;
7893
7994 const {
8095 theme : {
@@ -88,89 +103,65 @@ const UnMemoizedAttachmentUploadPreviewList = (
88103 ( { item } : { item : LocalAttachment } ) => {
89104 if ( isLocalImageAttachment ( item ) ) {
90105 return (
91- < Animated . View
92- entering = { FadeIn . duration ( 200 ) }
93- exiting = { FadeOut . duration ( 200 ) }
94- layout = { LinearTransition . duration ( 200 ) }
95- >
106+ < AttachmentPreviewCell >
96107 < ImageAttachmentUploadPreview
97108 attachment = { item }
98109 handleRetry = { attachmentManager . uploadAttachment }
99110 removeAttachments = { attachmentManager . removeAttachments }
100111 />
101- </ Animated . View >
112+ </ AttachmentPreviewCell >
102113 ) ;
103114 } else if ( isLocalVoiceRecordingAttachment ( item ) ) {
104115 return (
105- < Animated . View
106- entering = { FadeIn . duration ( 200 ) }
107- exiting = { FadeOut . duration ( 200 ) }
108- layout = { LinearTransition . duration ( 200 ) }
109- >
116+ < AttachmentPreviewCell >
110117 < AudioAttachmentUploadPreview
111118 attachment = { item }
112119 handleRetry = { attachmentManager . uploadAttachment }
113120 removeAttachments = { attachmentManager . removeAttachments }
114121 />
115- </ Animated . View >
122+ </ AttachmentPreviewCell >
116123 ) ;
117124 } else if ( isLocalAudioAttachment ( item ) ) {
118125 if ( isSoundPackageAvailable ( ) ) {
119126 return (
120- < Animated . View
121- entering = { FadeIn . duration ( 200 ) }
122- exiting = { FadeOut . duration ( 200 ) }
123- layout = { LinearTransition . duration ( 200 ) }
124- >
127+ < AttachmentPreviewCell >
125128 < AudioAttachmentUploadPreview
126129 attachment = { item }
127130 handleRetry = { attachmentManager . uploadAttachment }
128131 removeAttachments = { attachmentManager . removeAttachments }
129132 />
130- </ Animated . View >
133+ </ AttachmentPreviewCell >
131134 ) ;
132135 } else {
133136 return (
134- < Animated . View
135- entering = { FadeIn . duration ( 200 ) }
136- exiting = { FadeOut . duration ( 200 ) }
137- layout = { LinearTransition . duration ( 200 ) }
138- >
137+ < AttachmentPreviewCell >
139138 < FileAttachmentUploadPreview
140139 attachment = { item }
141140 handleRetry = { attachmentManager . uploadAttachment }
142141 removeAttachments = { attachmentManager . removeAttachments }
143142 />
144- </ Animated . View >
143+ </ AttachmentPreviewCell >
145144 ) ;
146145 }
147146 } else if ( isVideoAttachment ( item ) ) {
148147 return (
149- < Animated . View
150- entering = { FadeIn . duration ( 200 ) }
151- exiting = { FadeOut . duration ( 200 ) }
152- layout = { LinearTransition . duration ( 200 ) }
153- >
148+ < AttachmentPreviewCell >
154149 < VideoAttachmentUploadPreview
155150 attachment = { item }
156151 handleRetry = { attachmentManager . uploadAttachment }
157152 removeAttachments = { attachmentManager . removeAttachments }
158153 />
159- </ Animated . View >
154+ </ AttachmentPreviewCell >
160155 ) ;
161156 } else if ( isLocalFileAttachment ( item ) ) {
162157 return (
163- < Animated . View
164- entering = { FadeIn . duration ( 200 ) }
165- exiting = { FadeOut . duration ( 200 ) }
166- layout = { LinearTransition . duration ( 200 ) }
167- >
158+ < AttachmentPreviewCell >
168159 < FileAttachmentUploadPreview
169160 attachment = { item }
170161 handleRetry = { attachmentManager . uploadAttachment }
171162 removeAttachments = { attachmentManager . removeAttachments }
172163 />
173- </ Animated . View >
164+ </ AttachmentPreviewCell >
174165 ) ;
175166 } else return null ;
176167 } ,
@@ -184,22 +175,98 @@ const UnMemoizedAttachmentUploadPreviewList = (
184175 ] ,
185176 ) ;
186177
178+ const onScrollHandler = useCallback ( ( event : NativeSyntheticEvent < NativeScrollEvent > ) => {
179+ scrollOffsetXRef . current = event . nativeEvent . contentOffset . x ;
180+ } , [ ] ) ;
181+
182+ const onLayoutHandler = useCallback ( ( event : LayoutChangeEvent ) => {
183+ viewportWidthRef . current = event . nativeEvent . layout . width ;
184+ } , [ ] ) ;
185+
186+ const onContentSizeChangeHandler = useCallback (
187+ ( width : number ) => {
188+ const previousContentWidth = contentWidthRef . current ;
189+ contentWidthRef . current = width ;
190+
191+ if ( ! previousContentWidth || width >= previousContentWidth ) {
192+ return ;
193+ }
194+
195+ const oldMaxOffset = Math . max ( 0 , previousContentWidth - viewportWidthRef . current ) ;
196+ const newMaxOffset = Math . max ( 0 , width - viewportWidthRef . current ) ;
197+ const offsetBefore = scrollOffsetXRef . current ;
198+ const wasNearEnd = oldMaxOffset - offsetBefore <= END_ANCHOR_THRESHOLD ;
199+ const overshoot = Math . max ( 0 , offsetBefore - newMaxOffset ) ;
200+ const shouldAnchorEnd = wasNearEnd || overshoot > 0 ;
201+
202+ if ( ! shouldAnchorEnd ) {
203+ return ;
204+ }
205+
206+ if ( overshoot > 0 ) {
207+ attachmentListRef . current ?. scrollToOffset ( {
208+ animated : false ,
209+ offset : newMaxOffset ,
210+ } ) ;
211+ scrollOffsetXRef . current = newMaxOffset ;
212+ }
213+
214+ const compensation = newMaxOffset - oldMaxOffset ;
215+ if ( compensation !== 0 ) {
216+ cancelAnimation ( endShrinkCompensationX ) ;
217+ endShrinkCompensationX . value = compensation ;
218+ endShrinkCompensationX . value = withSpring ( 0 , {
219+ duration : END_SHRINK_COMPENSATION_DURATION ,
220+ } ) ;
221+ }
222+ } ,
223+ [ endShrinkCompensationX ] ,
224+ ) ;
225+
226+ useEffect ( ( ) => {
227+ const previousLength = previousAttachmentsLengthRef . current ;
228+ const nextLength = attachments . length ;
229+ const didAddAttachment = nextLength > previousLength ;
230+ previousAttachmentsLengthRef . current = nextLength ;
231+
232+ if ( ! didAddAttachment ) {
233+ return ;
234+ }
235+
236+ cancelAnimation ( endShrinkCompensationX ) ;
237+ endShrinkCompensationX . value = 0 ;
238+ requestAnimationFrame ( ( ) => {
239+ attachmentListRef . current ?. scrollToEnd ( { animated : true } ) ;
240+ } ) ;
241+ } , [ attachments . length , endShrinkCompensationX ] ) ;
242+
243+ const animatedListWrapperStyle = useAnimatedStyle ( ( ) => ( {
244+ transform : [ { translateX : endShrinkCompensationX . value } ] ,
245+ } ) ) ;
246+
187247 if ( ! attachments . length ) {
188248 return null ;
189249 }
190250
191251 return (
192- < FlatList
193- data = { attachments }
194- getItemLayout = { getItemLayout }
195- horizontal
196- ItemSeparatorComponent = { ItemSeparatorComponent }
197- keyExtractor = { ( item ) => item . localMetadata . id }
198- renderItem = { renderItem }
199- showsHorizontalScrollIndicator = { false }
200- style = { [ styles . flatList , flatList ] }
201- testID = { 'attachment-upload-preview-list' }
202- />
252+ < Animated . View style = { animatedListWrapperStyle } >
253+ < FlatList
254+ data = { attachments }
255+ horizontal
256+ ItemSeparatorComponent = { ItemSeparatorComponent }
257+ keyExtractor = { ( item ) => item . localMetadata . id }
258+ onContentSizeChange = { onContentSizeChangeHandler }
259+ onLayout = { onLayoutHandler }
260+ onScroll = { onScrollHandler }
261+ removeClippedSubviews = { false }
262+ ref = { attachmentListRef }
263+ renderItem = { renderItem }
264+ scrollEventThrottle = { 16 }
265+ showsHorizontalScrollIndicator = { false }
266+ style = { [ styles . flatList , flatList ] }
267+ testID = { 'attachment-upload-preview-list' }
268+ />
269+ </ Animated . View >
203270 ) ;
204271} ;
205272
@@ -240,7 +307,6 @@ const styles = StyleSheet.create({
240307 itemSeparator : {
241308 width : primitives . spacingXs ,
242309 } ,
243- wrapper : { } ,
244310} ) ;
245311
246312AttachmentUploadPreviewList . displayName =
0 commit comments