fix: replace broken swipe-to-refresh with toolbar refresh button (#61)
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) <noreply@anthropic.com>
This commit is contained in:
parent
1a1eddfd68
commit
5b0d27175c
2 changed files with 60 additions and 26 deletions
|
|
@ -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() {
|
|||
</View>
|
||||
) : (
|
||||
<View className={`flex-row items-center justify-end border-b px-4 py-2 ${isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'}`}>
|
||||
<Pressable onPress={handleRefresh} disabled={refreshing} className="mr-3 p-1">
|
||||
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
||||
<RefreshCw size={20} color={isDark ? '#A0A0A0' : '#6B6B6B'} />
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => setShowSearch(true)} className="mr-3 p-1">
|
||||
<Search size={20} color={isDark ? '#A0A0A0' : '#6B6B6B'} />
|
||||
</Pressable>
|
||||
|
|
@ -208,14 +233,6 @@ export default function InboxScreen() {
|
|||
onDragBegin={() => { isDraggingRef.current = true; }}
|
||||
onDragEnd={handleDragEnd}
|
||||
activationDistance={canDrag ? 0 : 10000}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={colors.bleu.DEFAULT}
|
||||
colors={[colors.bleu.DEFAULT]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</GestureHandlerRootView>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<Pressable onPress={handleRefresh} disabled={refreshing} className="mr-3 p-1">
|
||||
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
||||
<RefreshCw size={20} color={isDark ? '#A0A0A0' : '#6B6B6B'} />
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => setShowSearch(true)} className="mr-3 p-1">
|
||||
<Search size={20} color={isDark ? '#A0A0A0' : '#6B6B6B'} />
|
||||
</Pressable>
|
||||
|
|
@ -256,14 +281,6 @@ export default function ListDetailScreen() {
|
|||
onDragBegin={() => { isDraggingRef.current = true; }}
|
||||
onDragEnd={handleDragEnd}
|
||||
activationDistance={canDrag ? 0 : 10000}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={colors.bleu.DEFAULT}
|
||||
colors={[colors.bleu.DEFAULT]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</GestureHandlerRootView>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue