experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add pull-to-refresh gesture to trigger sync in mobile app

+60 -2
+1 -1
.agent-task.md
··· 134 134 135 135 --- 136 136 137 - ## Your Taskclick-and-hold on any window to drag/move it 137 + ## Your Task: pull-to-refresh triggers sync in mobile app 138 138 139 139 Enter plan mode (use EnterPlanMode tool) and create a detailed implementation plan. 140 140 Wait for my review before executing. Do NOT auto-execute.
+59 -1
backend/tauri-mobile/src/App.tsx
··· 159 159 const [lastSync, setLastSync] = useState<string | null>(null); 160 160 const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null); 161 161 162 + // Pull-to-refresh state 163 + const pullStartY = useRef<number | null>(null); 164 + const PULL_THRESHOLD = 80; 165 + 162 166 // Detect system dark mode via native iOS API 163 167 useEffect(() => { 164 168 const checkDarkMode = async () => { ··· 978 982 const scrollToTop = () => { 979 983 mainRef.current?.scrollTo({ top: 0, behavior: "smooth" }); 980 984 }; 985 + 986 + // Pull-to-refresh touch handlers 987 + const handleTouchStart = (e: React.TouchEvent) => { 988 + // Ignore if editing, add input expanded, or already syncing 989 + const anyEditing = editingUrlId || editingTextId || editingTagsetId || editingImageId; 990 + if (anyEditing || addInputExpanded || isSyncing) return; 991 + 992 + const main = mainRef.current; 993 + if (!main) return; 994 + 995 + // Only track if at scroll top 996 + if (main.scrollTop <= 0) { 997 + pullStartY.current = e.touches[0].clientY; 998 + } 999 + }; 1000 + 1001 + const handleTouchMove = (e: TouchEvent) => { 1002 + if (pullStartY.current === null) return; 1003 + 1004 + const pullDistance = e.touches[0].clientY - pullStartY.current; 1005 + 1006 + // If pulling down past threshold, prevent default scroll 1007 + if (pullDistance > PULL_THRESHOLD) { 1008 + e.preventDefault(); 1009 + } 1010 + }; 1011 + 1012 + const handleTouchEnd = (e: React.TouchEvent) => { 1013 + if (pullStartY.current === null) return; 1014 + 1015 + const pullDistance = e.changedTouches[0].clientY - pullStartY.current; 1016 + pullStartY.current = null; 1017 + 1018 + // Trigger sync if pulled past threshold 1019 + if (pullDistance > PULL_THRESHOLD) { 1020 + syncAll(); 1021 + } 1022 + }; 1023 + 1024 + // Attach touchmove with passive: false to allow preventDefault 1025 + useEffect(() => { 1026 + const main = mainRef.current; 1027 + if (!main) return; 1028 + 1029 + main.addEventListener("touchmove", handleTouchMove, { passive: false }); 1030 + return () => { 1031 + main.removeEventListener("touchmove", handleTouchMove); 1032 + }; 1033 + }, [editingUrlId, editingTextId, editingTagsetId, editingImageId, addInputExpanded, isSyncing]); 981 1034 982 1035 // Reset to show all types (home view) and scroll to top 983 1036 const showAll = () => { ··· 1989 2042 </button> 1990 2043 </header> 1991 2044 1992 - <main className="saved-view" ref={mainRef}> 2045 + <main 2046 + className="saved-view" 2047 + ref={mainRef} 2048 + onTouchStart={handleTouchStart} 2049 + onTouchEnd={handleTouchEnd} 2050 + > 1993 2051 {/* Quick add - expandable in place */} 1994 2052 <div className={`expandable-card ${addInputExpanded ? 'expanded' : ''}`}> 1995 2053 {!addInputExpanded ? (