From 5b0d27175cab693e018f61af1119896753d889d6 Mon Sep 17 00:00:00 2001 From: le king fu Date: Thu, 9 Apr 2026 09:34:28 -0400 Subject: [PATCH] fix: replace broken swipe-to-refresh with toolbar refresh button (#61) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The RefreshControl on DraggableFlatList never worked because the library wraps its FlatList in a GestureDetector with Gesture.Pan(), which intercepts vertical swipes before RefreshControl can detect them — particularly with activationDistance=0 in position sort mode. Replace with a toolbar refresh button (RefreshCw icon) on inbox and list detail screens. The button uses an Animated spin during refresh, matching the web UX. Removes all dead RefreshControl code and the useless refreshControl prop. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/(tabs)/index.tsx | 43 ++++++++++++++++++++++++++++++------------- app/list/[id].tsx | 43 ++++++++++++++++++++++++++++++------------- 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index d2c4a12..76b6fe7 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useCallback, useRef } from 'react'; -import { View, Text, Pressable, TextInput, useColorScheme, Alert, RefreshControl } from 'react-native'; +import { View, Text, Pressable, TextInput, useColorScheme, Alert, Animated, Easing } from 'react-native'; import { useRouter } from 'expo-router'; -import { Plus, ArrowUpDown, Filter, Download, Search, X } from 'lucide-react-native'; +import { Plus, ArrowUpDown, Filter, Download, Search, X, RefreshCw } from 'lucide-react-native'; import { useTranslation } from 'react-i18next'; import * as Haptics from 'expo-haptics'; import DraggableFlatList, { RenderItemParams } from 'react-native-draggable-flatlist'; @@ -45,6 +45,7 @@ export default function InboxScreen() { const isDark = (theme === 'system' ? systemScheme : theme) === 'dark'; const isDraggingRef = useRef(false); const [refreshing, setRefreshing] = useState(false); + const spinAnim = useRef(new Animated.Value(0)).current; const { sortBy, sortOrder, filterPriority, filterTag, filterCompleted, filterDueDate, hasActiveFilters } = useTaskStore(); @@ -72,10 +73,29 @@ export default function InboxScreen() { }, [loadTasks]); const handleRefresh = useCallback(async () => { + if (refreshing) return; setRefreshing(true); - await loadTasks(); - setRefreshing(false); - }, [loadTasks]); + spinAnim.setValue(0); + Animated.loop( + Animated.timing(spinAnim, { + toValue: 1, + duration: 800, + easing: Easing.linear, + useNativeDriver: true, + }) + ).start(); + try { + await loadTasks(); + } finally { + setRefreshing(false); + spinAnim.stopAnimation(); + } + }, [loadTasks, refreshing, spinAnim]); + + const spin = spinAnim.interpolate({ + inputRange: [0, 1], + outputRange: ['0deg', '360deg'], + }); const handleToggle = async (id: string) => { await toggleComplete(id); @@ -171,6 +191,11 @@ export default function InboxScreen() { ) : ( + + + + + setShowSearch(true)} className="mr-3 p-1"> @@ -208,14 +233,6 @@ export default function InboxScreen() { onDragBegin={() => { isDraggingRef.current = true; }} onDragEnd={handleDragEnd} activationDistance={canDrag ? 0 : 10000} - refreshControl={ - - } /> )} diff --git a/app/list/[id].tsx b/app/list/[id].tsx index e653816..2d06432 100644 --- a/app/list/[id].tsx +++ b/app/list/[id].tsx @@ -1,8 +1,8 @@ import { useEffect, useState, useCallback, useRef } from 'react'; -import { View, Text, Pressable, TextInput, useColorScheme, Alert, RefreshControl } from 'react-native'; +import { View, Text, Pressable, TextInput, useColorScheme, Alert, Animated, Easing } from 'react-native'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { - ArrowLeft, Plus, ArrowUpDown, Filter, Download, Search, X, + ArrowLeft, Plus, ArrowUpDown, Filter, Download, Search, X, RefreshCw, List, ShoppingCart, Briefcase, Home, Heart, Star, BookOpen, GraduationCap, Dumbbell, Utensils, Plane, Music, Code, Wrench, Gift, Camera, Palette, Dog, Leaf, Zap, @@ -62,6 +62,7 @@ export default function ListDetailScreen() { const isDark = (theme === 'system' ? systemScheme : theme) === 'dark'; const isDraggingRef = useRef(false); const [refreshing, setRefreshing] = useState(false); + const spinAnim = useRef(new Animated.Value(0)).current; const { sortBy, sortOrder, filterPriority, filterTag, filterCompleted, filterDueDate, hasActiveFilters } = useTaskStore(); @@ -97,10 +98,29 @@ export default function ListDetailScreen() { }, [loadData]); const handleRefresh = useCallback(async () => { + if (refreshing) return; setRefreshing(true); - await loadData(); - setRefreshing(false); - }, [loadData]); + spinAnim.setValue(0); + Animated.loop( + Animated.timing(spinAnim, { + toValue: 1, + duration: 800, + easing: Easing.linear, + useNativeDriver: true, + }) + ).start(); + try { + await loadData(); + } finally { + setRefreshing(false); + spinAnim.stopAnimation(); + } + }, [loadData, refreshing, spinAnim]); + + const spin = spinAnim.interpolate({ + inputRange: [0, 1], + outputRange: ['0deg', '360deg'], + }); const handleToggle = async (taskId: string) => { await toggleComplete(taskId); @@ -199,6 +219,11 @@ export default function ListDetailScreen() { + + + + + setShowSearch(true)} className="mr-3 p-1"> @@ -256,14 +281,6 @@ export default function ListDetailScreen() { onDragBegin={() => { isDraggingRef.current = true; }} onDragEnd={handleDragEnd} activationDistance={canDrag ? 0 : 10000} - refreshControl={ - - } /> )} -- 2.45.2