diff --git a/carry-your-live/src/components/CategoryFilter.tsx b/carry-your-live/src/components/CategoryFilter.tsx index b642828..0263a02 100644 --- a/carry-your-live/src/components/CategoryFilter.tsx +++ b/carry-your-live/src/components/CategoryFilter.tsx @@ -100,7 +100,7 @@ function AnimatedCategoryButton({ category, selected, onPress, theme }: Animated toValue: selected ? 1.05 : 1, duration: 150, easing: Easing.out(Easing.cubic), - useNativeDriver: true, + useNativeDriver: false, }).start(); Animated.timing(borderWidthAnim, { diff --git a/carry-your-live/src/database/sync.ts b/carry-your-live/src/database/sync.ts index 9a86ec9..d4fa0c0 100644 --- a/carry-your-live/src/database/sync.ts +++ b/carry-your-live/src/database/sync.ts @@ -1,3 +1,4 @@ +import { AppState, AppStateStatus } from 'react-native'; import { database, collections } from './index'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { apiFetch, getAuthToken } from '@/services/auth'; @@ -473,3 +474,74 @@ async function upsertFriendship(row: ServerRow, force: boolean): Promise void; + isRunning: () => boolean; +} + +let watcherStop: (() => void) | null = null; +let watcherActive = false; +let appInForeground = true; +let syncInFlight = false; + +export function watchForUpdates(intervalMs: number = DEFAULT_WATCH_INTERVAL_MS): UpdateWatcher { + if (watcherStop) { + return { + stop: () => stopWatchingForUpdates(), + isRunning: () => watcherActive, + }; + } + + appInForeground = AppState.currentState === 'active'; + + const appStateSubscription = AppState.addEventListener('change', (state: AppStateStatus) => { + const next = state === 'active'; + const wentForeground = next && !appInForeground; + appInForeground = next; + if (wentForeground) { + maybeSyncOnce().catch(() => {}); + } + }); + + const timer = setInterval(() => { + if (appInForeground && !syncInFlight) { + maybeSyncOnce().catch(() => {}); + } + }, intervalMs); + + watcherActive = true; + watcherStop = () => { + clearInterval(timer); + appStateSubscription.remove(); + watcherStop = null; + watcherActive = false; + }; + + return { + stop: () => stopWatchingForUpdates(), + isRunning: () => watcherActive, + }; +} + +async function maybeSyncOnce(): Promise { + if (syncInFlight) return; + const token = await getAuthToken(); + if (!token) return; + syncInFlight = true; + try { + await runSync(); + } finally { + syncInFlight = false; + } +} + +export function stopWatchingForUpdates(): void { + watcherStop?.(); +} + +export function isWatchingForUpdates(): boolean { + return watcherActive; +} diff --git a/carry-your-live/src/hooks/useDatabase.tsx b/carry-your-live/src/hooks/useDatabase.tsx index 3d572db..0d4bc8a 100644 --- a/carry-your-live/src/hooks/useDatabase.tsx +++ b/carry-your-live/src/hooks/useDatabase.tsx @@ -1,7 +1,7 @@ import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { map } from 'rxjs/operators'; import { database, collections } from '../database'; -import { runSync } from '../database/sync'; +import { runSync, watchForUpdates, stopWatchingForUpdates } from '../database/sync'; import { getAuthToken } from '../services/auth'; import { DEFAULT_CATEGORIES } from '../constants'; import Category from '../models/Category'; @@ -41,7 +41,13 @@ export function DatabaseProvider({ children }: { children: ReactNode }) { const token = await getAuthToken(); if (token) { - runSync().catch(() => {}); + runSync() + .catch(() => {}) + .finally(() => { + watchForUpdates(); + }); + } else { + stopWatchingForUpdates(); } } catch (error) { console.error('Failed to initialize database:', error); @@ -51,6 +57,9 @@ export function DatabaseProvider({ children }: { children: ReactNode }) { useEffect(() => { initializeDatabase(); + return () => { + stopWatchingForUpdates(); + }; }, []); return (