summaryrefslogtreecommitdiff
path: root/apps/web/app
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-09 23:13:08 -0800
committerFuwn <[email protected]>2026-02-09 23:13:08 -0800
commit2cb58bde47a2475aa85184e34a1432a997312186 (patch)
treeed5a7e4ef4d96da03028294842a5e0e03360242d /apps/web/app
parentfix: add spacing between collapsible appearance settings sections (diff)
downloadasa.news-2cb58bde47a2475aa85184e34a1432a997312186.tar.xz
asa.news-2cb58bde47a2475aa85184e34a1432a997312186.zip
feat: offline support tier 1 — IndexedDB query persistence and offline banner
Persist React Query cache to IndexedDB via idb-keyval so timeline, entry details, subscriptions, and other read data survive page reloads and brief offline periods. Add network status banner in reader layout.
Diffstat (limited to 'apps/web/app')
-rw-r--r--apps/web/app/providers.tsx32
-rw-r--r--apps/web/app/reader/_components/offline-banner.tsx32
-rw-r--r--apps/web/app/reader/_components/reader-layout-shell.tsx6
3 files changed, 66 insertions, 4 deletions
diff --git a/apps/web/app/providers.tsx b/apps/web/app/providers.tsx
index efe6775..8551748 100644
--- a/apps/web/app/providers.tsx
+++ b/apps/web/app/providers.tsx
@@ -1,15 +1,41 @@
"use client"
import { useState } from "react"
-import { QueryClientProvider } from "@tanstack/react-query"
+import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"
import { Toaster } from "sonner"
import { createQueryClient } from "@/lib/query-client"
+import { createIndexedDatabasePersister } from "@/lib/indexed-database-persister"
+
+const PERSISTABLE_QUERY_KEY_PREFIXES = [
+ "timeline",
+ "saved-entries",
+ "subscriptions",
+ "entry-detail",
+ "user-profile",
+ "unread-counts",
+ "highlights",
+ "custom-feeds",
+ "custom-feed-timeline",
+]
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(createQueryClient)
+ const [persister] = useState(createIndexedDatabasePersister)
return (
- <QueryClientProvider client={queryClient}>
+ <PersistQueryClientProvider
+ client={queryClient}
+ persistOptions={{
+ persister,
+ dehydrateOptions: {
+ shouldDehydrateQuery: (query) => {
+ if (query.state.status !== "success") return false
+ const prefix = query.queryKey[0]
+ return typeof prefix === "string" && PERSISTABLE_QUERY_KEY_PREFIXES.includes(prefix)
+ },
+ },
+ }}
+ >
{children}
<Toaster
position="bottom-right"
@@ -25,6 +51,6 @@ export function Providers({ children }: { children: React.ReactNode }) {
},
}}
/>
- </QueryClientProvider>
+ </PersistQueryClientProvider>
)
}
diff --git a/apps/web/app/reader/_components/offline-banner.tsx b/apps/web/app/reader/_components/offline-banner.tsx
new file mode 100644
index 0000000..81ebf8f
--- /dev/null
+++ b/apps/web/app/reader/_components/offline-banner.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import { useSyncExternalStore } from "react"
+
+function subscribe(callback: () => void) {
+ window.addEventListener("online", callback)
+ window.addEventListener("offline", callback)
+ return () => {
+ window.removeEventListener("online", callback)
+ window.removeEventListener("offline", callback)
+ }
+}
+
+function getSnapshot() {
+ return navigator.onLine
+}
+
+function getServerSnapshot() {
+ return true
+}
+
+export function OfflineBanner() {
+ const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
+
+ if (isOnline) return null
+
+ return (
+ <div className="border-b border-border bg-background-tertiary px-3 py-1.5 text-center text-text-dim">
+ you are offline — showing cached content
+ </div>
+ )
+}
diff --git a/apps/web/app/reader/_components/reader-layout-shell.tsx b/apps/web/app/reader/_components/reader-layout-shell.tsx
index 9d2eb24..fe158b5 100644
--- a/apps/web/app/reader/_components/reader-layout-shell.tsx
+++ b/apps/web/app/reader/_components/reader-layout-shell.tsx
@@ -12,6 +12,7 @@ import { AddFeedDialog } from "./add-feed-dialog"
import { SearchOverlay } from "./search-overlay"
import { KeyboardShortcutsDialog } from "./keyboard-shortcuts-dialog"
import { MfaChallenge } from "./mfa-challenge"
+import { OfflineBanner } from "./offline-banner"
import { useKeyboardNavigation } from "@/lib/hooks/use-keyboard-navigation"
import { createSupabaseBrowserClient } from "@/lib/supabase/client"
import { useSubscriptions } from "@/lib/queries/use-subscriptions"
@@ -284,7 +285,9 @@ export function ReaderLayoutShell({
}
return (
- <div className="flex h-screen">
+ <div className="flex h-screen flex-col">
+ <OfflineBanner />
+ <div className="flex min-h-0 flex-1">
{isMobile ? (
<>
<div
@@ -405,6 +408,7 @@ export function ReaderLayoutShell({
<SearchOverlay onClose={() => setSearchOpen(false)} />
)}
<KeyboardShortcutsDialog />
+ </div>
</div>
)
}