From 3d45a5d77f775e6cf3b1a3ef3f9c824fd4df2cb1 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Tue, 4 Aug 2026 10:30:37 -0400 Subject: [PATCH 1/3] fix: online state Signed-off-by: Adam Setch --- src/renderer/hooks/useOnlineStatus.test.ts | 26 ++++++++++++++++++++++ src/renderer/hooks/useOnlineStatus.ts | 8 +++++++ 2 files changed, 34 insertions(+) diff --git a/src/renderer/hooks/useOnlineStatus.test.ts b/src/renderer/hooks/useOnlineStatus.test.ts index db8f819d6..ebacd62dd 100644 --- a/src/renderer/hooks/useOnlineStatus.test.ts +++ b/src/renderer/hooks/useOnlineStatus.test.ts @@ -12,6 +12,7 @@ describe('renderer/hooks/useOnlineStatus.ts', () => { act(() => { onlineManager.setOnline(true); }); + Object.defineProperty(navigator, 'onLine', { value: true, configurable: true }); }); it('reflects the online manager status', () => { @@ -49,4 +50,29 @@ describe('renderer/hooks/useOnlineStatus.ts', () => { expect(onlineManager.isOnline()).toBe(true); }); + + describe('startup bootstrap', () => { + it('corrects onlineManager to offline when the device starts offline', () => { + // onlineManager defaults to online: true regardless of the device's + // actual state; the hook should force-correct it on mount rather than + // waiting for a native browser online/offline event. + Object.defineProperty(navigator, 'onLine', { value: false, configurable: true }); + onlineManager.setOnline(true); + + const { result } = renderHook(() => useOnlineStatus()); + + expect(onlineManager.isOnline()).toBe(false); + expect(result.current).toBe(false); + }); + + it('confirms onlineManager as online when the device starts online', () => { + Object.defineProperty(navigator, 'onLine', { value: true, configurable: true }); + onlineManager.setOnline(true); + + const { result } = renderHook(() => useOnlineStatus()); + + expect(onlineManager.isOnline()).toBe(true); + expect(result.current).toBe(true); + }); + }); }); diff --git a/src/renderer/hooks/useOnlineStatus.ts b/src/renderer/hooks/useOnlineStatus.ts index d99717ded..5fc28e0f1 100644 --- a/src/renderer/hooks/useOnlineStatus.ts +++ b/src/renderer/hooks/useOnlineStatus.ts @@ -11,6 +11,14 @@ export function useOnlineStatus(): boolean { const [isOnline, setIsOnline] = useState(true); useEffect(() => { + // Force-correct TanStack Query's internal online state to match the + // browser's actual state on mount. `onlineManager` otherwise initializes + // to `online: true` regardless of reality, only self-correcting once the + // browser fires its first native online/offline event - which means an + // app cold-started while offline would fire (and fail/retry) a query + // before ever discovering it should be paused. + onlineManager.setOnline(navigator.onLine); + const handle = () => { setIsOnline(onlineManager.isOnline()); }; From 223992f8560f6cf78de1496bdc2fb616fd4f724a Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Thu, 6 Aug 2026 19:44:00 +0200 Subject: [PATCH 2/3] refactor: correct online state before any query can run --- src/renderer/hooks/useOnlineStatus.test.ts | 25 ------------------ src/renderer/hooks/useOnlineStatus.ts | 8 ------ src/renderer/utils/api/queryClient.test.ts | 30 ++++++++++++++++++++++ src/renderer/utils/api/queryClient.ts | 18 ++++++++++++- 4 files changed, 47 insertions(+), 34 deletions(-) create mode 100644 src/renderer/utils/api/queryClient.test.ts diff --git a/src/renderer/hooks/useOnlineStatus.test.ts b/src/renderer/hooks/useOnlineStatus.test.ts index ebacd62dd..973264cfb 100644 --- a/src/renderer/hooks/useOnlineStatus.test.ts +++ b/src/renderer/hooks/useOnlineStatus.test.ts @@ -50,29 +50,4 @@ describe('renderer/hooks/useOnlineStatus.ts', () => { expect(onlineManager.isOnline()).toBe(true); }); - - describe('startup bootstrap', () => { - it('corrects onlineManager to offline when the device starts offline', () => { - // onlineManager defaults to online: true regardless of the device's - // actual state; the hook should force-correct it on mount rather than - // waiting for a native browser online/offline event. - Object.defineProperty(navigator, 'onLine', { value: false, configurable: true }); - onlineManager.setOnline(true); - - const { result } = renderHook(() => useOnlineStatus()); - - expect(onlineManager.isOnline()).toBe(false); - expect(result.current).toBe(false); - }); - - it('confirms onlineManager as online when the device starts online', () => { - Object.defineProperty(navigator, 'onLine', { value: true, configurable: true }); - onlineManager.setOnline(true); - - const { result } = renderHook(() => useOnlineStatus()); - - expect(onlineManager.isOnline()).toBe(true); - expect(result.current).toBe(true); - }); - }); }); diff --git a/src/renderer/hooks/useOnlineStatus.ts b/src/renderer/hooks/useOnlineStatus.ts index 5fc28e0f1..d99717ded 100644 --- a/src/renderer/hooks/useOnlineStatus.ts +++ b/src/renderer/hooks/useOnlineStatus.ts @@ -11,14 +11,6 @@ export function useOnlineStatus(): boolean { const [isOnline, setIsOnline] = useState(true); useEffect(() => { - // Force-correct TanStack Query's internal online state to match the - // browser's actual state on mount. `onlineManager` otherwise initializes - // to `online: true` regardless of reality, only self-correcting once the - // browser fires its first native online/offline event - which means an - // app cold-started while offline would fire (and fail/retry) a query - // before ever discovering it should be paused. - onlineManager.setOnline(navigator.onLine); - const handle = () => { setIsOnline(onlineManager.isOnline()); }; diff --git a/src/renderer/utils/api/queryClient.test.ts b/src/renderer/utils/api/queryClient.test.ts new file mode 100644 index 000000000..c050c273e --- /dev/null +++ b/src/renderer/utils/api/queryClient.test.ts @@ -0,0 +1,30 @@ +import { onlineManager } from '@tanstack/react-query'; + +import { syncOnlineManagerWithBrowser } from './queryClient'; + +describe('renderer/utils/api/queryClient.ts', () => { + afterEach(() => { + onlineManager.setOnline(true); + Object.defineProperty(navigator, 'onLine', { value: true, configurable: true }); + }); + + describe('syncOnlineManagerWithBrowser', () => { + it('corrects onlineManager to offline when the device is offline', () => { + onlineManager.setOnline(true); + Object.defineProperty(navigator, 'onLine', { value: false, configurable: true }); + + syncOnlineManagerWithBrowser(); + + expect(onlineManager.isOnline()).toBe(false); + }); + + it('corrects onlineManager to online when the device is online', () => { + onlineManager.setOnline(false); + Object.defineProperty(navigator, 'onLine', { value: true, configurable: true }); + + syncOnlineManagerWithBrowser(); + + expect(onlineManager.isOnline()).toBe(true); + }); + }); +}); diff --git a/src/renderer/utils/api/queryClient.ts b/src/renderer/utils/api/queryClient.ts index 0dd974e9b..b1162d05f 100644 --- a/src/renderer/utils/api/queryClient.ts +++ b/src/renderer/utils/api/queryClient.ts @@ -1,7 +1,23 @@ -import { QueryClient } from '@tanstack/react-query'; +import { onlineManager, QueryClient } from '@tanstack/react-query'; import { Constants } from '../../constants'; +/** + * Set TanStack Query's online state from the browser's actual network state. + * + * `onlineManager` initializes to `online: true` regardless of reality, and + * self-corrects only once the browser fires a native online/offline event. + * Browsers emit those on transitions only, so a device already offline when + * Gitify launches never gets one. + */ +export function syncOnlineManagerWithBrowser(): void { + onlineManager.setOnline(navigator.onLine); +} + +// Runs at import, before the client below exists and therefore before any +// query can fire, so a cold start while offline pauses rather than fetches. +syncOnlineManagerWithBrowser(); + /** * TanStack Query client for all API state. * From b5f72e276f761df7bf6a3f24c4be345f045e1d5b Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Thu, 6 Aug 2026 19:44:09 +0200 Subject: [PATCH 3/3] fix: seed online status from the online manager on first render --- src/renderer/hooks/useOnlineStatus.test.ts | 10 ++++++++++ src/renderer/hooks/useOnlineStatus.ts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/renderer/hooks/useOnlineStatus.test.ts b/src/renderer/hooks/useOnlineStatus.test.ts index 973264cfb..16189caa4 100644 --- a/src/renderer/hooks/useOnlineStatus.test.ts +++ b/src/renderer/hooks/useOnlineStatus.test.ts @@ -33,6 +33,16 @@ describe('renderer/hooks/useOnlineStatus.ts', () => { expect(result.current).toBe(true); }); + it('reports offline on the very first render, before any effect runs', () => { + // `queryClient.ts` syncs onlineManager from the browser at import time, + // so a cold start while offline must not paint one frame as online. + onlineManager.setOnline(false); + + const { result } = renderHook(() => useOnlineStatus()); + + expect(result.current).toBe(false); + }); + it('re-probes online state when the system wakes', () => { act(() => { onlineManager.setOnline(false); diff --git a/src/renderer/hooks/useOnlineStatus.ts b/src/renderer/hooks/useOnlineStatus.ts index d99717ded..2110ee37c 100644 --- a/src/renderer/hooks/useOnlineStatus.ts +++ b/src/renderer/hooks/useOnlineStatus.ts @@ -8,7 +8,7 @@ import { onlineManager } from '@tanstack/react-query'; * pause/resume behaviour. */ export function useOnlineStatus(): boolean { - const [isOnline, setIsOnline] = useState(true); + const [isOnline, setIsOnline] = useState(() => onlineManager.isOnline()); useEffect(() => { const handle = () => {