1- import React , { useEffect , useMemo , useRef , useState } from 'react' ;
1+ import React , { useEffect , useMemo , useRef , useState , useCallback } from 'react' ;
22import {
33 View ,
44 Text ,
@@ -8,15 +8,15 @@ import {
88 FlatList ,
99 useWindowDimensions ,
1010 Image ,
11- Modal ,
1211} from 'react-native' ;
13- import { SafeAreaView } from 'react-native-safe-area-context' ;
1412import {
1513 fitContainer ,
1614 ResumableZoom ,
1715 useImageResolution ,
1816} from 'react-native-zoom-toolkit' ;
1917
18+ const ITEM_SPACING = 20 ;
19+
2020export default function VerticalView ( {
2121 data,
2222 loading,
@@ -30,12 +30,28 @@ export default function VerticalView({
3030 const ref = useRef ( null ) ;
3131 const [ zoomMode , setZoomMode ] = useState ( false ) ;
3232 const [ imageSizeAcuired , setImageSizeAcquired ] = useState ( false ) ;
33+ const [ initialSyncDone , setInitialSyncDone ] = useState ( false ) ;
34+ const currentIndexRef = useRef ( 0 ) ;
3335
34- // Update imagesLinks when comicBook changes
3536 useEffect ( ( ) => {
36- if ( data && data ?. length > 0 ) {
37- setImagesLinks ( data [ 0 ] ) ;
37+ if ( ! data || data . length === 0 ) {
38+ setImagesLinks ( '' ) ;
39+ currentIndexRef . current = 0 ;
40+ return ;
3841 }
42+
43+ const maxIndex = data . length - 1 ;
44+ const safeIndex = Math . min (
45+ Math . max ( activeIndex ?? 0 , 0 ) ,
46+ maxIndex ,
47+ ) ;
48+
49+ setImagesLinks ( data [ safeIndex ] ) ;
50+ currentIndexRef . current = safeIndex ;
51+ } , [ activeIndex , data ] ) ;
52+
53+ useEffect ( ( ) => {
54+ setInitialSyncDone ( false ) ;
3955 } , [ data ] ) ;
4056
4157 // Properly prepare the image source object with headers
@@ -79,33 +95,76 @@ export default function VerticalView({
7995 } , [ resolution , isFetching , imagesLinks , imageSource ] ) ;
8096
8197 useEffect ( ( ) => {
82- if ( activeIndex > 0 && data ?. length > 0 ) {
83- // Ensure size is acquired before attempting to scroll
84- if ( imageSizeAcuired && ref . current ) {
85- ref . current . scrollToIndex ( { index : activeIndex , animated : true } ) ;
86- }
98+ if ( ! data || data . length === 0 ) {
99+ return ;
87100 }
88- // Depend on imageSizeAcuired to ensure scroll happens after layout is known
89- } , [ activeIndex , data , imageSizeAcuired ] ) ;
90101
91- // getItemLayout is used to optimize the FlatList performance
92- const getItemLayout = ( data , index ) => {
93- // Use calculated size if available
94- if ( size ?. height ) {
95- return {
96- length : size . height ,
97- offset : size . height * index ,
98- index,
99- } ;
102+ if ( ! imageSizeAcuired || initialSyncDone || ! ref . current ) {
103+ return ;
100104 }
101- // Fallback if size is not yet calculated: use resolutions prop or screen height
102- const estimatedHeight = resolutions ?. height || height ;
103- return {
104- length : estimatedHeight ,
105- offset : estimatedHeight * index ,
105+
106+ const maxIndex = data . length - 1 ;
107+ const safeIndex = Math . min (
108+ Math . max ( activeIndex ?? 0 , 0 ) ,
109+ maxIndex ,
110+ ) ;
111+
112+ if ( safeIndex === 0 ) {
113+ currentIndexRef . current = 0 ;
114+ setInitialSyncDone ( true ) ;
115+ return ;
116+ }
117+
118+ try {
119+ ref . current . scrollToIndex ( { index : safeIndex , animated : false } ) ;
120+ currentIndexRef . current = safeIndex ;
121+ setInitialSyncDone ( true ) ;
122+ } catch ( error ) {
123+ // Ignore out of range errors while list recalculates
124+ }
125+ } , [ activeIndex , data , imageSizeAcuired , initialSyncDone ] ) ;
126+
127+ // getItemLayout is used to optimize the FlatList performance
128+ const itemLength = useMemo ( ( ) => {
129+ const baseHeight = size ?. height || resolutions ?. height || height ;
130+ return baseHeight + ITEM_SPACING ;
131+ } , [ height , resolutions ?. height , size ?. height ] ) ;
132+
133+ const getItemLayout = useCallback (
134+ ( _ , index ) => ( {
135+ length : itemLength ,
136+ offset : itemLength * index ,
106137 index,
107- } ;
108- } ;
138+ } ) ,
139+ [ itemLength ] ,
140+ ) ;
141+
142+ const updateIndexFromOffset = useCallback (
143+ offset => {
144+ if ( ! Number . isFinite ( offset ) || ! itemLength ) {
145+ return ;
146+ }
147+
148+ const rawIndex = Math . round ( offset / itemLength ) ;
149+ const maxIndex = Math . max ( 0 , ( data ?. length ?? 1 ) - 1 ) ;
150+ const safeIndex = Math . min ( Math . max ( rawIndex , 0 ) , maxIndex ) ;
151+
152+ if ( safeIndex === currentIndexRef . current ) {
153+ return ;
154+ }
155+
156+ currentIndexRef . current = safeIndex ;
157+ setImageLinkIndex ( safeIndex ) ;
158+ } ,
159+ [ data ?. length , itemLength , setImageLinkIndex ] ,
160+ ) ;
161+
162+ const handleScrollEnd = useCallback (
163+ event => {
164+ updateIndexFromOffset ( event . nativeEvent . contentOffset . y ) ;
165+ } ,
166+ [ updateIndexFromOffset ] ,
167+ ) ;
109168
110169 if ( loading || ! imagesLinks ) {
111170 return (
@@ -136,7 +195,7 @@ export default function VerticalView({
136195 renderItem = { ( { item, index} ) => (
137196 < TouchableOpacity
138197 style = { {
139- marginBottom : 20 ,
198+ marginBottom : ITEM_SPACING ,
140199 } }
141200 activeOpacity = { 0.8 }
142201 onPress = { ( ) => {
@@ -164,16 +223,10 @@ export default function VerticalView({
164223 ) }
165224 </ TouchableOpacity >
166225 ) }
167- onScroll = { event => {
168- // Calculate the index based on the scroll position
169- // and the height of the image (only if size is known)
170- if ( ! size ?. height ) return ;
171- const index = Math . floor (
172- event . nativeEvent . contentOffset . y / size . height ,
173- ) ;
174- setImageLinkIndex ( index ) ;
175- } }
226+ onScrollEndDrag = { handleScrollEnd }
227+ onMomentumScrollEnd = { handleScrollEnd }
176228 showsVerticalScrollIndicator = { false }
229+ contentContainerStyle = { { paddingBottom : ITEM_SPACING } }
177230 />
178231 { zoomMode && (
179232 < View
0 commit comments