@@ -7,13 +7,11 @@ import { FlashList, type FlashList as FlashListType } from '@shopify/flash-list'
77import * as Haptics from 'expo-haptics' ;
88import { useRouter } from 'expo-router' ;
99import React , { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
10- import { ActivityIndicator , Alert , Animated , DeviceEventEmitter , Pressable , RefreshControl , StyleSheet , Text , View } from 'react-native' ;
10+ import { ActivityIndicator , Alert , Animated , DeviceEventEmitter , InteractionManager , Pressable , RefreshControl , StyleSheet , View } from 'react-native' ;
1111import { useSafeAreaInsets } from 'react-native-safe-area-context' ;
1212
1313import { type Folder , type Note , useApiService } from '@/src/services/api' ;
1414import { useTheme } from '@/src/theme' ;
15- import { detectNoteType } from '@/src/utils/noteTypeDetection' ;
16- import { stripHtmlTags } from '@/src/utils/noteUtils' ;
1715
1816// Constants for FAB scroll behavior
1917const FAB_SCROLL_THRESHOLD_START = 100 ; // Start showing FAB when scrolled past this
@@ -58,6 +56,9 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa
5856 const { folderId, viewType, searchQuery } = route ?. params || { } ;
5957
6058 const [ refreshing , setRefreshing ] = useState ( false ) ;
59+ const [ deletingNoteId , setDeletingNoteId ] = useState < string | null > ( null ) ;
60+ const [ archivingNoteId , setArchivingNoteId ] = useState < string | null > ( null ) ;
61+ const [ closeSwipeables , setCloseSwipeables ] = useState ( 0 ) ;
6162
6263 // Performance tracking
6364 const screenFocusTime = useRef < number > ( 0 ) ;
@@ -203,6 +204,11 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa
203204 if ( flatListRef . current ) {
204205 flatListRef . current . scrollToOffset ( { offset : 0 , animated : false } ) ;
205206 }
207+
208+ // Cleanup function - close all swipeables when navigating away
209+ return ( ) => {
210+ setCloseSwipeables ( prev => prev + 1 ) ;
211+ } ;
206212 // eslint-disable-next-line react-hooks/exhaustive-deps
207213 } , [ folderId , viewType , searchQuery ] )
208214 ) ;
@@ -225,7 +231,18 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa
225231 const onRefresh = async ( ) => {
226232 try {
227233 setRefreshing ( true ) ;
234+
235+ // Save current scroll position before refresh
236+ const currentOffset = scrollY . _value || 0 ;
237+
228238 await loadNotes ( true ) ;
239+
240+ // Restore scroll position after a small delay to let FlashList settle
241+ if ( currentOffset > 0 ) {
242+ setTimeout ( ( ) => {
243+ flatListRef . current ?. scrollToOffset ( { offset : currentOffset , animated : false } ) ;
244+ } , 50 ) ;
245+ }
229246 } finally {
230247 setRefreshing ( false ) ;
231248 }
@@ -476,40 +493,71 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa
476493 } , [ api , notes , setNotes , loadNotes ] ) ;
477494
478495 // Handle delete note (move to trash)
479- const handleDeleteNote = useCallback ( async ( noteId : string ) => {
480- try {
481- // Update note as deleted FIRST
482- await api . updateNote ( noteId , { deleted : true } ) ;
496+ const handleDeleteNote = useCallback ( ( noteId : string ) => {
497+ // Fire haptic feedback immediately
498+ Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Medium ) ;
483499
484- // Emit event - the event listener will handle removing from UI
485- DeviceEventEmitter . emit ( 'noteDeleted' , noteId ) ;
486- } catch ( error ) {
487- console . error ( 'Failed to delete note:' , error ) ;
488- await Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Error ) ;
489- Alert . alert ( 'Error' , 'Failed to delete note. Please try again.' ) ;
490- }
500+ // Set deleting state IMMEDIATELY to show spinner
501+ setDeletingNoteId ( noteId ) ;
502+
503+ // Run API call after interactions complete (non-blocking)
504+ InteractionManager . runAfterInteractions ( async ( ) => {
505+ try {
506+ // Update note as deleted
507+ await api . updateNote ( noteId , { deleted : true } ) ;
508+
509+ // Success haptic feedback
510+ Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Success ) ;
511+
512+ // Keep spinner visible briefly before removing note
513+ await new Promise ( resolve => setTimeout ( resolve , 200 ) ) ;
514+
515+ // Emit event - the event listener will handle removing from UI
516+ DeviceEventEmitter . emit ( 'noteDeleted' , noteId ) ;
517+ } catch ( error ) {
518+ console . error ( 'Failed to delete note:' , error ) ;
519+ await Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Error ) ;
520+ Alert . alert ( 'Error' , 'Failed to delete note. Please try again.' ) ;
521+ } finally {
522+ setDeletingNoteId ( null ) ;
523+ }
524+ } ) ;
491525 } , [ api ] ) ;
492526
493527 // Handle archive note
494- const handleArchiveNote = useCallback ( async ( noteId : string ) => {
495- try {
496- // Update note as archived FIRST
497- await api . updateNote ( noteId , { archived : true } ) ;
528+ const handleArchiveNote = useCallback ( ( noteId : string ) => {
529+ // Fire haptic feedback immediately
530+ Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Medium ) ;
498531
499- // Success haptic feedback
500- await Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Success ) ;
532+ // Set archiving state IMMEDIATELY to show spinner
533+ setArchivingNoteId ( noteId ) ;
501534
502- // Remove from current view immediately (archived notes don't show in folder/all views)
503- setNotes ( prevNotes => prevNotes . filter ( n => n . id !== noteId ) ) ;
504- lastOptimisticUpdateRef . current = Date . now ( ) ;
535+ // Run API call after interactions complete (non-blocking)
536+ InteractionManager . runAfterInteractions ( async ( ) => {
537+ try {
538+ // Update note as archived
539+ await api . updateNote ( noteId , { archived : true } ) ;
505540
506- // Emit event for other listeners
507- DeviceEventEmitter . emit ( 'noteUpdated' , { id : noteId , archived : true } ) ;
508- } catch ( error ) {
509- console . error ( 'Failed to archive note:' , error ) ;
510- await Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Error ) ;
511- Alert . alert ( 'Error' , 'Failed to archive note. Please try again.' ) ;
512- }
541+ // Success haptic feedback
542+ Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Success ) ;
543+
544+ // Keep spinner visible briefly before removing note
545+ await new Promise ( resolve => setTimeout ( resolve , 200 ) ) ;
546+
547+ // Remove from current view immediately (archived notes don't show in folder/all views)
548+ setNotes ( prevNotes => prevNotes . filter ( n => n . id !== noteId ) ) ;
549+ lastOptimisticUpdateRef . current = Date . now ( ) ;
550+
551+ // Emit event for other listeners
552+ DeviceEventEmitter . emit ( 'noteUpdated' , { id : noteId , archived : true } ) ;
553+ } catch ( error ) {
554+ console . error ( 'Failed to archive note:' , error ) ;
555+ await Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Error ) ;
556+ Alert . alert ( 'Error' , 'Failed to archive note. Please try again.' ) ;
557+ } finally {
558+ setArchivingNoteId ( null ) ;
559+ }
560+ } ) ;
513561 } , [ api , setNotes ] ) ;
514562
515563 // Render individual note item
@@ -532,9 +580,12 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa
532580 mutedColor = { mutedColor }
533581 borderColor = { borderColor }
534582 backgroundColor = { backgroundColor }
583+ isDeleting = { deletingNoteId === note . id }
584+ isArchiving = { archivingNoteId === note . id }
585+ closeSwipeables = { closeSwipeables }
535586 />
536587 ) ;
537- } , [ filteredNotes . length , folderId , handleNotePress , handleNoteLongPress , handleDeleteNote , handleArchiveNote , folderPathsMap , foldersMap , skeletonOpacity , notesEnhancedDataCache , foregroundColor , mutedForegroundColor , mutedColor , borderColor , backgroundColor ] ) ;
588+ } , [ filteredNotes . length , folderId , handleNotePress , handleNoteLongPress , handleDeleteNote , handleArchiveNote , folderPathsMap , foldersMap , skeletonOpacity , notesEnhancedDataCache , foregroundColor , mutedForegroundColor , mutedColor , borderColor , backgroundColor , deletingNoteId , archivingNoteId , closeSwipeables ] ) ;
538589
539590 // Render list header (subfolders and create note button)
540591 const renderListHeader = useCallback ( ( ) => {
@@ -607,14 +658,15 @@ export default function NotesList({ navigation, route, renderHeader, scrollY: pa
607658 onRefresh = { onRefresh }
608659 tintColor = { theme . isDark ? '#666666' : '#000000' }
609660 colors = { [ theme . isDark ? '#666666' : '#000000' ] }
661+ progressViewOffset = { insets . top }
610662 />
611663 }
612664 drawDistance = { 2000 }
613- getItemType = { ( item ) => {
665+ getItemType = { ( ) => {
614666 // Help FlashList recycle items better
615667 return 'note' ;
616668 } }
617- overrideItemLayout = { ( layout , item ) => {
669+ overrideItemLayout = { ( layout ) => {
618670 // Fixed layout for better scrolling performance
619671 // Padding: 12*2=24, Header: 23+8, Preview: 40+6, Meta: 20, Divider: 1
620672 layout . size = 112 ;
0 commit comments