From 9d7875ea80f73379de3c923ba29a8a72939598d1 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 17 Sep 2026 23:18:01 -0400 Subject: [PATCH] feat(pwa): app-icon badge with the highlight count via navigator.setAppBadge (#154) Same number as the tab title (leaf-room highlights), cleared at zero. Skipped under Tauri where the native set_badge_count owns the badge, and silently absent where the Badging API is not available. Closes #154 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- LOTUS_FEATURES.md | 4 ++++ src/app/pages/client/ClientNonUIFeatures.tsx | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/LOTUS_FEATURES.md b/LOTUS_FEATURES.md index b1ada29f3..b589d31c3 100644 --- a/LOTUS_FEATURES.md +++ b/LOTUS_FEATURES.md @@ -1179,6 +1179,10 @@ Links you paste, send, edit, or merely _see_ lose ad/analytics identifiers — ` The syncable subset of Lotus settings (theme, composer toolbar order, notification/quiet-hour preferences, call keys, privacy toggles, …) is mirrored to the `io.lotus.settings` account-data event on the user's own homeserver and applied on every other device. Device-bound keys stay local (`DEVICE_LOCAL_KEYS` in `src/app/utils/settingsSync.ts`: page zoom, media auto-load, animation pause, glassmorphism, noise-suppression tier/model, bitrates, volumes, notification permission, developer tools, PTT mode, camera-on-join, drawer state). Conflicts are last-write-wins on an `updatedAt` stamp forced monotonic per device; a per-account `lastSyncedAt` marker in localStorage stops a device from echoing a snapshot it just applied. **Settings → General → Sync** has the toggle (itself device-local), **Push now** (make this device win everywhere) and **Clear synced copy**. Hook: `src/app/hooks/useSettingsSync.ts`, mounted from `ClientNonUIFeatures`. +### PWA App-Icon Badge (Gitea #154) + +When Lotus Chat is installed as a PWA (Android Chrome, desktop Chrome/Edge), the app icon carries a numeric badge via the Badging API (`navigator.setAppBadge`). The number is the same highlight count (mentions/DMs, leaf rooms only) that the tab title shows, so the two can never disagree; it clears when the count reaches zero. Lives in `FaviconUpdater` (`ClientNonUIFeatures.tsx`) next to the title/favicon logic. No-op in a plain browser tab (the API is absent) and under Tauri, where the native `set_badge_count` already owns the badge. + ### Media Gallery `MediaGallery.tsx` — a right-side drawer for browsing room media. diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx index 85c604f16..7835191ed 100644 --- a/src/app/pages/client/ClientNonUIFeatures.tsx +++ b/src/app/pages/client/ClientNonUIFeatures.tsx @@ -59,7 +59,7 @@ import { toastQueueAtom } from '../../state/toast'; import { useReminders } from '../../hooks/useReminders'; import { getRoomRetentionMs, isExpired } from '../../utils/retention'; import { useTauriUpdater } from '../../hooks/useTauriUpdater'; -import { invokeTauri } from '../../hooks/useTauri'; +import { invokeTauri, isTauri as isTauriApp } from '../../hooks/useTauri'; import { TauriDesktopFeatures } from '../../components/TauriDesktopFeatures'; import { KeyboardShortcutsDialog, useKeyboardShortcutsTrigger } from '../../features/shortcuts'; import { useRoomsListener } from '../../hooks/useRoomsListener'; @@ -153,6 +153,23 @@ function FaviconUpdater() { } else { document.title = 'Lotus Chat'; } + + // [Gitea #154] Installed-PWA app-icon badge (Android Chrome, desktop + // Chrome/Edge). Same number as the title so the two never disagree: + // highlights only, mirroring the desktop app's native `set_badge_count` + // (which owns the badge under Tauri, so skip there). Plain tabs and + // browsers without the Badging API simply have no `setAppBadge`. + if (!isTauriApp()) { + const nav = navigator as Navigator & { + setAppBadge?: (count?: number) => Promise; + clearAppBadge?: () => Promise; + }; + if (totalHighlight > 0) { + nav.setAppBadge?.(totalHighlight).catch(() => {}); + } else { + nav.clearAppBadge?.().catch(() => {}); + } + } }, [roomToUnread]); return null;