diff --git a/src/hooks/NetworkManager.jsx b/src/hooks/NetworkManager.jsx index 6afa8d5..65aafa8 100644 --- a/src/hooks/NetworkManager.jsx +++ b/src/hooks/NetworkManager.jsx @@ -57,6 +57,11 @@ class NetworkManager { registerNetworkListener(callback) { this.connectionStatusListeners.push(callback) } + unregisterNetworkListener(callback) { + this.connectionStatusListeners = this.connectionStatusListeners.filter( + cb => cb !== callback, + ) + } registerBackendSyncListener(callback) { // if callback is not in the list already, add it if (!this.queueSyncListeners.includes(callback)) { diff --git a/src/hooks/useSyncOnReconnect.js b/src/hooks/useSyncOnReconnect.js index 8cf7f97..65272ef 100644 --- a/src/hooks/useSyncOnReconnect.js +++ b/src/hooks/useSyncOnReconnect.js @@ -1,3 +1,5 @@ +import { App as capacitorApp } from '@capacitor/app' +import { Capacitor } from '@capacitor/core' import { useQueryClient } from '@tanstack/react-query' import { useEffect, useRef } from 'react' import { commandQueue } from '../utils/CommandQueue' @@ -14,6 +16,18 @@ export function useSyncOnReconnect() { const initialized = useRef(false) useEffect(() => { + let pendingPollInterval + let cacheRefreshInterval + let resumeListener + let networkListener + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + runSync() + } + } + + const handleOnline = () => runSync() + const init = async () => { if (initialized.current) return initialized.current = true @@ -23,24 +37,36 @@ export function useSyncOnReconnect() { } // 1. Device network change (works on native + real network drops) - networkManager.registerNetworkListener(async isOnline => { + networkListener = async isOnline => { if (isOnline) { await runSync() } - }) + } + networkManager.registerNetworkListener(networkListener) // 2. Tab becomes visible (user switches back to the tab after reconnecting backend) - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible') { - runSync() - } - }) + document.addEventListener('visibilitychange', handleVisibilityChange) // 3. Browser online event (fires when device network is restored) - window.addEventListener('online', () => runSync()) + window.addEventListener('online', handleOnline) + + // 3.5 Native app resume (fires when returning to the foreground) + if (Capacitor.isNativePlatform()) { + resumeListener = await capacitorApp.addListener( + 'appStateChange', + ({ isActive }) => { + if (isActive) { + console.log( + 'App resumed, checking connectivity and syncing if online...', + ) + runSync() + } + }, + ) + } // 4. Retry pending commands every 30s (catches backend restart) - setInterval(async () => { + pendingPollInterval = setInterval(async () => { const pending = await commandQueue.getPending() if (pending.length > 0) { runSync() @@ -48,7 +74,7 @@ export function useSyncOnReconnect() { }, PENDING_POLL_MS) // 5. Keep IDB cache fresh every 5 min while online (so offline reads are current) - setInterval(() => { + cacheRefreshInterval = setInterval(() => { runSync() }, CACHE_REFRESH_MS) } @@ -62,5 +88,22 @@ export function useSyncOnReconnect() { } init() + + return () => { + if (pendingPollInterval) { + clearInterval(pendingPollInterval) + } + + if (cacheRefreshInterval) { + clearInterval(cacheRefreshInterval) + } + + if (networkListener) { + networkManager.unregisterNetworkListener(networkListener) + } + document.removeEventListener('visibilitychange', handleVisibilityChange) + window.removeEventListener('online', handleOnline) + resumeListener?.remove() + } }, [queryClient]) } diff --git a/src/queries/ChoreQueries.jsx b/src/queries/ChoreQueries.jsx index 86dc132..d4d2bfe 100644 --- a/src/queries/ChoreQueries.jsx +++ b/src/queries/ChoreQueries.jsx @@ -150,7 +150,7 @@ export const useCreateChore = () => { return { ...oldData, res: [...oldData.res, offlineChore] } }) - return { res: offlineChore } + return offlineChore } return useMutation({ @@ -170,11 +170,11 @@ export const useCreateChore = () => { } // Successfully created the chore on the server, return the created chore // update the local chores cache with the new chore: - queryClient.setQueryData(['chores'], oldData => { + queryClient.setQueryData(['chores', false], oldData => { if (!oldData) return { res: [createdChore.res] } return { res: [...oldData.res, createdChore.res] } }) - return { res: createdChore } + return createdChore.res } catch (error) { if (isNetworkError(error)) { return queueOfflineCreate(newTask) diff --git a/src/queries/UserQueries.jsx b/src/queries/UserQueries.jsx index 20202c7..588f1a2 100644 --- a/src/queries/UserQueries.jsx +++ b/src/queries/UserQueries.jsx @@ -36,7 +36,7 @@ export const useCircleMembers = () => { const result = await GetAllCircleMembers() // Cache for offline use if (result?.res) { - offlineDB.saveKV('circle_members', result.res) + offlineDB.saveKV('circle_members', result.res).catch(() => {}) } return result } catch { diff --git a/src/utils/OfflineDB.js b/src/utils/OfflineDB.js index 29be31d..0bba249 100644 --- a/src/utils/OfflineDB.js +++ b/src/utils/OfflineDB.js @@ -966,7 +966,6 @@ class OfflineDB { async getHistoryByChore(choreId) { if (!isOfflineFeatureEnabled()) return [] await this._ensureInit() - console.log('MO: Fetching history for chore', choreId) return this.backend.getHistoryByChore(choreId) } diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index dd8b9f8..fd6548b 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -367,7 +367,11 @@ const ChoreEdit = () => { SaveFunction(chore) .then(result => { - if (result?._pendingUpdate || result?._pendingCreate) { + if ( + result?._pendingUpdate || + result?._pendingCreate || + result?.res?._pendingCreate + ) { showSuccess({ title: 'Saved Offline', message: 'Your changes will sync when you are back online.', diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx index 7373559..9280e09 100644 --- a/src/views/ChoreEdit/ChoreView.jsx +++ b/src/views/ChoreEdit/ChoreView.jsx @@ -907,8 +907,8 @@ const ChoreView = () => { }} > {(() => { - const content = decodeHtmlEntities(chore.description || '') - const shouldRenderHtml = hasHtmlTags(content) + const raw = chore.description || '' + const shouldRenderHtml = hasHtmlTags(raw) return shouldRenderHtml ? ( { whiteSpace: 'pre-wrap', wordBreak: 'break-word', }} - dangerouslySetInnerHTML={{ __html: content }} + dangerouslySetInnerHTML={{ __html: raw }} /> ) : ( { wordBreak: 'break-word', }} > - {content} + {decodeHtmlEntities(raw)} ) })()} @@ -975,8 +975,8 @@ const ChoreView = () => { }} > {(() => { - const content = decodeHtmlEntities(chore.notes || '') - const shouldRenderHtml = hasHtmlTags(content) + const raw = chore.notes || '' + const shouldRenderHtml = hasHtmlTags(raw) return shouldRenderHtml ? ( { whiteSpace: 'pre-wrap', wordBreak: 'break-word', }} - dangerouslySetInnerHTML={{ __html: content }} + dangerouslySetInnerHTML={{ __html: raw }} /> ) : ( { wordBreak: 'break-word', }} > - {content} + {decodeHtmlEntities(raw)} ) })()} diff --git a/src/views/Projects/ProjectQueries.js b/src/views/Projects/ProjectQueries.js index 83d8386..039d0b4 100644 --- a/src/views/Projects/ProjectQueries.js +++ b/src/views/Projects/ProjectQueries.js @@ -1,5 +1,10 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { GetProjects, CreateProject, UpdateProject, DeleteProject } from '../../utils/Fetcher' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + CreateProject, + DeleteProject, + GetProjects, + UpdateProject, +} from '../../utils/Fetcher' import { offlineDB } from '../../utils/OfflineDB' // Query hook for fetching all projects @@ -33,7 +38,7 @@ export const useCreateProject = () => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async (projectData) => { + mutationFn: async projectData => { try { const response = await CreateProject(projectData) if (response.ok) { @@ -53,7 +58,7 @@ export const useCreateProject = () => { return localProject } }, - onSuccess: (newProject) => { + onSuccess: newProject => { // Update the projects cache queryClient.setQueryData(['projects'], (oldProjects = []) => { const updatedProjects = [...oldProjects, newProject] @@ -63,7 +68,7 @@ export const useCreateProject = () => { // Invalidate and refetch queryClient.invalidateQueries(['projects']) }, - onError: (error) => { + onError: error => { console.error('Create project mutation failed:', error) }, }) @@ -92,18 +97,18 @@ export const useUpdateProject = () => { } } }, - onSuccess: (updatedProject) => { + onSuccess: updatedProject => { // Update the projects cache queryClient.setQueryData(['projects'], (oldProjects = []) => { return oldProjects.map(project => - project.id === updatedProject.id ? updatedProject : project + project.id === updatedProject.id ? updatedProject : project, ) }) // Invalidate and refetch queryClient.invalidateQueries(['projects']) }, - onError: (error) => { + onError: error => { console.error('Update project mutation failed:', error) }, }) @@ -114,7 +119,7 @@ export const useDeleteProject = () => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async (projectId) => { + mutationFn: async projectId => { try { // Prevent deletion of default project if (projectId === 'default') { @@ -141,14 +146,14 @@ export const useDeleteProject = () => { // Invalidate and refetch queryClient.invalidateQueries(['projects']) }, - onError: (error) => { + onError: error => { console.error('Delete project mutation failed:', error) }, }) } // Hook to get a specific project by ID -export const useProject = (projectId) => { +export const useProject = projectId => { return useQuery({ queryKey: ['projects', projectId], queryFn: async () => { @@ -179,4 +184,4 @@ export const useProject = (projectId) => { staleTime: 5 * 60 * 1000, cacheTime: 10 * 60 * 1000, }) -} \ No newline at end of file +}