From df8689df643e7280317a1056d5438f66f78d8a1a Mon Sep 17 00:00:00 2001 From: Carlos Delgado Date: Mon, 15 Jun 2026 22:00:22 -0400 Subject: [PATCH 1/4] #968 Warn Safari users that annotations may be deleted when app is not installed to home screen --- .../components/SafariAnnotationWarning.svelte | 44 +++++++++++++++++++ src/lib/scripts/safariUtils.ts | 25 +++++++++++ src/routes/bookmarks/+page.svelte | 2 + src/routes/highlights/+page.svelte | 2 + src/routes/notes/+page.svelte | 2 + 5 files changed, 75 insertions(+) create mode 100644 src/lib/components/SafariAnnotationWarning.svelte create mode 100644 src/lib/scripts/safariUtils.ts diff --git a/src/lib/components/SafariAnnotationWarning.svelte b/src/lib/components/SafariAnnotationWarning.svelte new file mode 100644 index 000000000..4ad3447b1 --- /dev/null +++ b/src/lib/components/SafariAnnotationWarning.svelte @@ -0,0 +1,44 @@ + + +{#if visible} +
+ +
+

Annotations may be deleted by Safari

+

+ Safari can delete your bookmarks, highlights, and notes after 1–2 weeks of + inactivity. To keep them permanently, add this app to your Home Screen. +

+
+ +
+{/if} diff --git a/src/lib/scripts/safariUtils.ts b/src/lib/scripts/safariUtils.ts new file mode 100644 index 000000000..a8143befa --- /dev/null +++ b/src/lib/scripts/safariUtils.ts @@ -0,0 +1,25 @@ +const DISMISSED_KEY = 'safari_annotation_warning_dismissed'; +const DISMISS_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days + +function isSafariWithoutStandalone(): boolean { + if (typeof window === 'undefined') return false; + const ua = navigator.userAgent; + const isSafari = /Safari/.test(ua) && !/Chrome|CriOS|FxiOS|EdgiOS/.test(ua); + const isStandalone = (navigator as Navigator & { standalone?: boolean }).standalone === true; + return isSafari && !isStandalone; +} + +export function shouldShowSafariWarning(): boolean { + if (typeof window === 'undefined') return false; + // Debug override: add ?debug_safari=true to any URL to force-show the warning + const hashQuery = window.location.hash.split('?')[1] ?? ''; + if (new URLSearchParams(hashQuery).get('debug_safari') === 'true') return true; + if (!isSafariWithoutStandalone()) return false; + const dismissed = localStorage.getItem(DISMISSED_KEY); + if (!dismissed) return true; + return Date.now() - parseInt(dismissed, 10) > DISMISS_DURATION_MS; +} + +export function dismissSafariWarning(): void { + localStorage.setItem(DISMISSED_KEY, Date.now().toString()); +} diff --git a/src/routes/bookmarks/+page.svelte b/src/routes/bookmarks/+page.svelte index 845fd46e9..4ff2e8b60 100644 --- a/src/routes/bookmarks/+page.svelte +++ b/src/routes/bookmarks/+page.svelte @@ -8,6 +8,7 @@ import { SORT_DATE, SORT_REFERENCE, toSorted } from '$lib/data/annotation-sort'; import { removeBookmark, type BookmarkItem } from '$lib/data/bookmarks'; import { bodyFontSize, refs, t } from '$lib/data/stores'; + import SafariAnnotationWarning from '$lib/components/SafariAnnotationWarning.svelte'; import { BookmarkIcon } from '$lib/icons'; import ShareIcon from '$lib/icons/ShareIcon.svelte'; import { formatDate } from '$lib/scripts/dateUtils'; @@ -82,6 +83,7 @@ class="overflow-y-auto p-2.5 max-w-screen-md mx-auto w-full" style:font-size="{$bodyFontSize}px" > + {#if data.bookmarks.length === 0}
{$t['Annotation_Bookmarks_None']}
{$t['Annotation_Bookmarks_None_Info']}
diff --git a/src/routes/highlights/+page.svelte b/src/routes/highlights/+page.svelte index 3fc4bbff3..dc2ad0eea 100644 --- a/src/routes/highlights/+page.svelte +++ b/src/routes/highlights/+page.svelte @@ -6,6 +6,7 @@ import SortMenu from '$lib/components/SortMenu.svelte'; import { shareAnnotation, shareAnnotations } from '$lib/data/annotation-share'; import { SORT_COLOR, SORT_DATE, SORT_REFERENCE, toSorted } from '$lib/data/annotation-sort'; + import SafariAnnotationWarning from '$lib/components/SafariAnnotationWarning.svelte'; import { removeHighlight, type HighlightItem } from '$lib/data/highlights'; import { bodyFontSize, refs, t } from '$lib/data/stores'; import ShareIcon from '$lib/icons/ShareIcon.svelte'; @@ -88,6 +89,7 @@ class="overflow-y-auto p-2.5 max-w-screen-md mx-auto w-full" style:font-size="{$bodyFontSize}px" > + {#if data.highlights.length === 0}
{$t['Annotation_Highlights_None']}
{$t['Annotation_Highlights_None_Info']}
diff --git a/src/routes/notes/+page.svelte b/src/routes/notes/+page.svelte index affd97b84..f6fe71b3a 100644 --- a/src/routes/notes/+page.svelte +++ b/src/routes/notes/+page.svelte @@ -6,6 +6,7 @@ import SortMenu from '$lib/components/SortMenu.svelte'; import { shareAnnotation, shareAnnotations } from '$lib/data/annotation-share'; import { SORT_DATE, SORT_REFERENCE, toSorted } from '$lib/data/annotation-sort'; + import SafariAnnotationWarning from '$lib/components/SafariAnnotationWarning.svelte'; import { removeNote, type NoteItem } from '$lib/data/notes'; import { bodyFontSize, monoIconColor, refs, t } from '$lib/data/stores'; import { NoteIcon } from '$lib/icons'; @@ -81,6 +82,7 @@ class="overflow-y-auto p-2.5 max-w-screen-md mx-auto w-full" style:font-size="{$bodyFontSize}px" > + {#if data.notes.length === 0}
{$t['Annotation_Notes_None']}
{$t['Annotation_Notes_None_Info']}
From f2cb4c16c7a7f3d8ae55b77ffc5b852db0138891 Mon Sep 17 00:00:00 2001 From: Carlos Delgado Date: Mon, 15 Jun 2026 22:21:43 -0400 Subject: [PATCH 2/4] Fix Prettier formatting --- src/lib/components/SafariAnnotationWarning.svelte | 15 ++++++++++++--- src/routes/bookmarks/+page.svelte | 2 +- src/routes/highlights/+page.svelte | 2 +- src/routes/notes/+page.svelte | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/lib/components/SafariAnnotationWarning.svelte b/src/lib/components/SafariAnnotationWarning.svelte index 4ad3447b1..afdc2a7f8 100644 --- a/src/lib/components/SafariAnnotationWarning.svelte +++ b/src/lib/components/SafariAnnotationWarning.svelte @@ -10,7 +10,9 @@ {#if visible} -
+
- - + +
diff --git a/src/routes/bookmarks/+page.svelte b/src/routes/bookmarks/+page.svelte index 4ff2e8b60..818bc8401 100644 --- a/src/routes/bookmarks/+page.svelte +++ b/src/routes/bookmarks/+page.svelte @@ -3,12 +3,12 @@ import { resolve } from '$app/paths'; import IconCard from '$lib/components/IconCard.svelte'; import Navbar from '$lib/components/Navbar.svelte'; + import SafariAnnotationWarning from '$lib/components/SafariAnnotationWarning.svelte'; import SortMenu from '$lib/components/SortMenu.svelte'; import { shareAnnotation, shareAnnotations } from '$lib/data/annotation-share'; import { SORT_DATE, SORT_REFERENCE, toSorted } from '$lib/data/annotation-sort'; import { removeBookmark, type BookmarkItem } from '$lib/data/bookmarks'; import { bodyFontSize, refs, t } from '$lib/data/stores'; - import SafariAnnotationWarning from '$lib/components/SafariAnnotationWarning.svelte'; import { BookmarkIcon } from '$lib/icons'; import ShareIcon from '$lib/icons/ShareIcon.svelte'; import { formatDate } from '$lib/scripts/dateUtils'; diff --git a/src/routes/highlights/+page.svelte b/src/routes/highlights/+page.svelte index dc2ad0eea..6e9305496 100644 --- a/src/routes/highlights/+page.svelte +++ b/src/routes/highlights/+page.svelte @@ -3,10 +3,10 @@ import { resolve } from '$app/paths'; import IconCard from '$lib/components/IconCard.svelte'; import Navbar from '$lib/components/Navbar.svelte'; + import SafariAnnotationWarning from '$lib/components/SafariAnnotationWarning.svelte'; import SortMenu from '$lib/components/SortMenu.svelte'; import { shareAnnotation, shareAnnotations } from '$lib/data/annotation-share'; import { SORT_COLOR, SORT_DATE, SORT_REFERENCE, toSorted } from '$lib/data/annotation-sort'; - import SafariAnnotationWarning from '$lib/components/SafariAnnotationWarning.svelte'; import { removeHighlight, type HighlightItem } from '$lib/data/highlights'; import { bodyFontSize, refs, t } from '$lib/data/stores'; import ShareIcon from '$lib/icons/ShareIcon.svelte'; diff --git a/src/routes/notes/+page.svelte b/src/routes/notes/+page.svelte index f6fe71b3a..51f22bcb6 100644 --- a/src/routes/notes/+page.svelte +++ b/src/routes/notes/+page.svelte @@ -3,10 +3,10 @@ import { resolve } from '$app/paths'; import IconCard from '$lib/components/IconCard.svelte'; import Navbar from '$lib/components/Navbar.svelte'; + import SafariAnnotationWarning from '$lib/components/SafariAnnotationWarning.svelte'; import SortMenu from '$lib/components/SortMenu.svelte'; import { shareAnnotation, shareAnnotations } from '$lib/data/annotation-share'; import { SORT_DATE, SORT_REFERENCE, toSorted } from '$lib/data/annotation-sort'; - import SafariAnnotationWarning from '$lib/components/SafariAnnotationWarning.svelte'; import { removeNote, type NoteItem } from '$lib/data/notes'; import { bodyFontSize, monoIconColor, refs, t } from '$lib/data/stores'; import { NoteIcon } from '$lib/icons'; From f0a888afdb1a9499579dbc2817ff035e24e4679a Mon Sep 17 00:00:00 2001 From: Carlos Delgado Date: Mon, 15 Jun 2026 22:25:35 -0400 Subject: [PATCH 3/4] Address CodeRabbit feedback: clarify debug comment and guard localStorage access --- src/lib/scripts/safariUtils.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/lib/scripts/safariUtils.ts b/src/lib/scripts/safariUtils.ts index a8143befa..4a838c3aa 100644 --- a/src/lib/scripts/safariUtils.ts +++ b/src/lib/scripts/safariUtils.ts @@ -11,15 +11,26 @@ function isSafariWithoutStandalone(): boolean { export function shouldShowSafariWarning(): boolean { if (typeof window === 'undefined') return false; - // Debug override: add ?debug_safari=true to any URL to force-show the warning + // Debug override: because the app uses hash routing, append ?debug_safari=true + // inside the hash, e.g. /#/bookmarks?debug_safari=true const hashQuery = window.location.hash.split('?')[1] ?? ''; if (new URLSearchParams(hashQuery).get('debug_safari') === 'true') return true; if (!isSafariWithoutStandalone()) return false; - const dismissed = localStorage.getItem(DISMISSED_KEY); - if (!dismissed) return true; - return Date.now() - parseInt(dismissed, 10) > DISMISS_DURATION_MS; + try { + const dismissed = localStorage.getItem(DISMISSED_KEY); + if (!dismissed) return true; + const dismissedAt = parseInt(dismissed, 10); + if (isNaN(dismissedAt)) return true; + return Date.now() - dismissedAt > DISMISS_DURATION_MS; + } catch { + return true; + } } export function dismissSafariWarning(): void { - localStorage.setItem(DISMISSED_KEY, Date.now().toString()); + try { + localStorage.setItem(DISMISSED_KEY, Date.now().toString()); + } catch { + // If storage is unavailable, silently ignore — the warning will reappear next visit + } } From 8193404cf4b9907f18a41d9cdede9ac5b33c3c8e Mon Sep 17 00:00:00 2001 From: Carlos Delgado Date: Mon, 15 Jun 2026 22:28:58 -0400 Subject: [PATCH 4/4] Fix ESLint curly brace errors in safariUtils.ts --- src/lib/scripts/safariUtils.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/lib/scripts/safariUtils.ts b/src/lib/scripts/safariUtils.ts index 4a838c3aa..7699a8ddc 100644 --- a/src/lib/scripts/safariUtils.ts +++ b/src/lib/scripts/safariUtils.ts @@ -2,7 +2,9 @@ const DISMISSED_KEY = 'safari_annotation_warning_dismissed'; const DISMISS_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days function isSafariWithoutStandalone(): boolean { - if (typeof window === 'undefined') return false; + if (typeof window === 'undefined') { + return false; + } const ua = navigator.userAgent; const isSafari = /Safari/.test(ua) && !/Chrome|CriOS|FxiOS|EdgiOS/.test(ua); const isStandalone = (navigator as Navigator & { standalone?: boolean }).standalone === true; @@ -10,17 +12,27 @@ function isSafariWithoutStandalone(): boolean { } export function shouldShowSafariWarning(): boolean { - if (typeof window === 'undefined') return false; + if (typeof window === 'undefined') { + return false; + } // Debug override: because the app uses hash routing, append ?debug_safari=true // inside the hash, e.g. /#/bookmarks?debug_safari=true const hashQuery = window.location.hash.split('?')[1] ?? ''; - if (new URLSearchParams(hashQuery).get('debug_safari') === 'true') return true; - if (!isSafariWithoutStandalone()) return false; + if (new URLSearchParams(hashQuery).get('debug_safari') === 'true') { + return true; + } + if (!isSafariWithoutStandalone()) { + return false; + } try { const dismissed = localStorage.getItem(DISMISSED_KEY); - if (!dismissed) return true; + if (!dismissed) { + return true; + } const dismissedAt = parseInt(dismissed, 10); - if (isNaN(dismissedAt)) return true; + if (isNaN(dismissedAt)) { + return true; + } return Date.now() - dismissedAt > DISMISS_DURATION_MS; } catch { return true;