11import { Ionicons } from '@expo/vector-icons';
22import { BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView } from '@gorhom/bottom-sheet';
33import { GlassView } from 'expo-glass-effect';
4- import React, { forwardRef, memo, useCallback, useMemo } from 'react';
4+ import * as Haptics from 'expo-haptics';
5+ import React, { forwardRef, memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react';
56import { Pressable, StyleSheet, Text, View } from 'react-native';
67
78import { useTheme } from '@/src/theme';
@@ -30,7 +31,7 @@ interface FilterSortSheetProps {
3031 hasActiveFilters: boolean;
3132}
3233
33- // Memoized filter checkbox component for faster toggling
34+ // Memoized filter checkbox component with optimistic local state for instant feedback
3435const FilterCheckbox = memo(function FilterCheckbox({
3536 label,
3637 isActive,
@@ -48,25 +49,42 @@ const FilterCheckbox = memo(function FilterCheckbox({
4849 foregroundColor: string;
4950 borderColor: string;
5051}) {
52+ // Optimistic local state for instant visual feedback
53+ const [localActive, setLocalActive] = useState(isActive);
54+
55+ // Sync with parent state when it catches up
56+ useEffect(() => {
57+ setLocalActive(isActive);
58+ }, [isActive]);
59+
60+ const handlePress = useCallback(() => {
61+ // Instant visual update
62+ setLocalActive(prev => !prev);
63+ // Haptic feedback
64+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
65+ // Trigger actual state update (deferred)
66+ onToggle();
67+ }, [onToggle]);
68+
5169 return (
5270 <Pressable
5371 style={[
5472 styles.filterOptionGrid,
55- { backgroundColor: isActive ? 'rgba(59, 130, 246, 0.1)' : 'transparent', borderColor },
73+ { backgroundColor: localActive ? 'rgba(59, 130, 246, 0.1)' : 'transparent', borderColor },
5674 ]}
57- onPress={onToggle }
75+ onPress={handlePress }
5876 >
5977 <Ionicons
60- name={isActive ? 'checkbox' : 'square-outline'}
78+ name={localActive ? 'checkbox' : 'square-outline'}
6179 size={20}
62- color={isActive ? primaryColor : mutedColor}
80+ color={localActive ? primaryColor : mutedColor}
6381 />
6482 <Text style={[styles.filterOptionTextGrid, { color: foregroundColor }]}>{label}</Text>
6583 </Pressable>
6684 );
6785});
6886
69- // Memoized sort radio button component
87+ // Memoized sort radio button component with optimistic local state
7088const SortRadio = memo(function SortRadio({
7189 label,
7290 isActive,
@@ -82,15 +100,32 @@ const SortRadio = memo(function SortRadio({
82100 mutedColor: string;
83101 foregroundColor: string;
84102}) {
103+ // Optimistic local state for instant visual feedback
104+ const [localActive, setLocalActive] = useState(isActive);
105+
106+ // Sync with parent state when it catches up
107+ useEffect(() => {
108+ setLocalActive(isActive);
109+ }, [isActive]);
110+
111+ const handlePress = useCallback(() => {
112+ // Instant visual update
113+ setLocalActive(true);
114+ // Haptic feedback
115+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
116+ // Trigger actual state update (deferred)
117+ onSelect();
118+ }, [onSelect]);
119+
85120 return (
86121 <Pressable
87- style={[styles.filterOption, { backgroundColor: isActive ? 'rgba(59, 130, 246, 0.1)' : 'transparent' }]}
88- onPress={onSelect }
122+ style={[styles.filterOption, { backgroundColor: localActive ? 'rgba(59, 130, 246, 0.1)' : 'transparent' }]}
123+ onPress={handlePress }
89124 >
90125 <Ionicons
91- name={isActive ? 'radio-button-on' : 'radio-button-off'}
126+ name={localActive ? 'radio-button-on' : 'radio-button-off'}
92127 size={24}
93- color={isActive ? primaryColor : mutedColor}
128+ color={localActive ? primaryColor : mutedColor}
94129 />
95130 <Text style={[styles.filterOptionText, { color: foregroundColor }]}>{label}</Text>
96131 </Pressable>
@@ -116,22 +151,22 @@ export const FilterSortSheet = forwardRef<BottomSheetModal, FilterSortSheetProps
116151 []
117152 );
118153
119- // Memoized toggle handlers
120- const toggleAttachments = useCallback(() => setFilterConfig(prev => ({ ...prev, showAttachmentsOnly: !prev.showAttachmentsOnly })), [setFilterConfig]);
121- const toggleCode = useCallback(() => setFilterConfig(prev => ({ ...prev, showCodeOnly: !prev.showCodeOnly })), [setFilterConfig]);
122- const toggleDiagram = useCallback(() => setFilterConfig(prev => ({ ...prev, showDiagramOnly: !prev.showDiagramOnly })), [setFilterConfig]);
123- const toggleHidden = useCallback(() => setFilterConfig(prev => ({ ...prev, showHiddenOnly: !prev.showHiddenOnly })), [setFilterConfig]);
124- const toggleNote = useCallback(() => setFilterConfig(prev => ({ ...prev, showNoteOnly: !prev.showNoteOnly })), [setFilterConfig]);
125- const togglePublic = useCallback(() => setFilterConfig(prev => ({ ...prev, showPublicOnly: !prev.showPublicOnly })), [setFilterConfig]);
126- const toggleSheet = useCallback(() => setFilterConfig(prev => ({ ...prev, showSheetOnly: !prev.showSheetOnly })), [setFilterConfig]);
127- const toggleStarred = useCallback(() => setFilterConfig(prev => ({ ...prev, showStarredOnly: !prev.showStarredOnly })), [setFilterConfig]);
128- const clearFilters = useCallback(() => setFilterConfig({ showAttachmentsOnly: false, showCodeOnly: false, showDiagramOnly: false, showHiddenOnly: false, showNoteOnly: false, showPublicOnly: false, showSheetOnly: false, showStarredOnly: false }), [setFilterConfig]);
154+ // Memoized toggle handlers - wrapped in startTransition for responsive UI
155+ const toggleAttachments = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showAttachmentsOnly: !prev.showAttachmentsOnly }) )), [setFilterConfig]);
156+ const toggleCode = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showCodeOnly: !prev.showCodeOnly }) )), [setFilterConfig]);
157+ const toggleDiagram = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showDiagramOnly: !prev.showDiagramOnly }) )), [setFilterConfig]);
158+ const toggleHidden = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showHiddenOnly: !prev.showHiddenOnly }) )), [setFilterConfig]);
159+ const toggleNote = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showNoteOnly: !prev.showNoteOnly }) )), [setFilterConfig]);
160+ const togglePublic = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showPublicOnly: !prev.showPublicOnly }) )), [setFilterConfig]);
161+ const toggleSheet = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showSheetOnly: !prev.showSheetOnly }) )), [setFilterConfig]);
162+ const toggleStarred = useCallback(() => startTransition(() => setFilterConfig(prev => ({ ...prev, showStarredOnly: !prev.showStarredOnly }) )), [setFilterConfig]);
163+ const clearFilters = useCallback(() => startTransition(() => setFilterConfig({ showAttachmentsOnly: false, showCodeOnly: false, showDiagramOnly: false, showHiddenOnly: false, showNoteOnly: false, showPublicOnly: false, showSheetOnly: false, showStarredOnly: false }) ), [setFilterConfig]);
129164
130- // Memoized sort handlers
131- const sortByUpdated = useCallback(() => setSortConfig({ option: 'updated', direction: 'desc' }), [setSortConfig]);
132- const sortByCreated = useCallback(() => setSortConfig({ option: 'created', direction: 'desc' }), [setSortConfig]);
133- const sortByTitleAsc = useCallback(() => setSortConfig({ option: 'title', direction: 'asc' }), [setSortConfig]);
134- const sortByTitleDesc = useCallback(() => setSortConfig({ option: 'title', direction: 'desc' }), [setSortConfig]);
165+ // Memoized sort handlers - wrapped in startTransition for responsive UI
166+ const sortByUpdated = useCallback(() => startTransition(() => setSortConfig({ option: 'updated', direction: 'desc' }) ), [setSortConfig]);
167+ const sortByCreated = useCallback(() => startTransition(() => setSortConfig({ option: 'created', direction: 'desc' }) ), [setSortConfig]);
168+ const sortByTitleAsc = useCallback(() => startTransition(() => setSortConfig({ option: 'title', direction: 'asc' }) ), [setSortConfig]);
169+ const sortByTitleDesc = useCallback(() => startTransition(() => setSortConfig({ option: 'title', direction: 'desc' }) ), [setSortConfig]);
135170
136171 const dismissSheet = useCallback(() => (ref as React.RefObject<BottomSheetModal>).current?.dismiss(), [ref]);
137172
0 commit comments