From 804caa51307871d010258486ef4369aa6252549e Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 2 Jul 2026 13:31:09 -0400 Subject: [PATCH] feat(desktop): tray Do-Not-Disturb + Launch-on-login toggle (P6-1 web) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useTauriDnd + manualDndAtom: the native tray "Do Not Disturb" toggle (lotus-dnd-changed event) OR's into the notification quiet-gate in ClientNonUIFeatures (both invite + message notifiers), alongside Focus Assist. - AutostartSetting in Settings → General (desktop-only): reads/sets plugin:autostart via invoke. Mirrors the window-chrome setting. - Docs: LOTUS_FEATURES desktop section (Linux parity + DND + autostart), LOTUS_TODO P6-1 → [~], LOTUS_BUGS verification row. Gates: tsc/eslint/prettier clean, build OK, 661 tests. Native side committed on cinny-desktop:main (CI-compile-pending). Co-Authored-By: Claude Opus 4.8 --- LOTUS_BUGS.md | 55 ++++++++++--------- LOTUS_FEATURES.md | 9 +++ LOTUS_TODO.md | 2 +- src/app/components/TauriDesktopFeatures.tsx | 2 + src/app/features/settings/general/General.tsx | 35 +++++++++++- src/app/hooks/useTauriDnd.ts | 21 +++++++ src/app/pages/client/ClientNonUIFeatures.tsx | 13 ++++- src/app/state/manualDnd.ts | 14 +++++ 8 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 src/app/hooks/useTauriDnd.ts create mode 100644 src/app/state/manualDnd.ts diff --git a/LOTUS_BUGS.md b/LOTUS_BUGS.md index e500e5734..1c03383c7 100644 --- a/LOTUS_BUGS.md +++ b/LOTUS_BUGS.md @@ -15,33 +15,34 @@ step-by-step checks in [`LOTUS_TESTING.md`](./LOTUS_TESTING.md). Implemented and gate-green; confirm each per `LOTUS_TESTING.md`, then delete the row. -| ID | Item | File / area | Test | -| :--- | :-------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------- | -| #2 | Chat-background animation flicker (`contain:paint`) | `lotus/chatBackground.ts` | F1 | -| #4 | Ringtone re-fixes: classic loudness + caller decline notice (A2 ✓ live) | `CallEmbedProvider.tsx`, `ringtones.ts` | A1,A3,A4 | -| #6 | Background vs. seasonal theme mutual exclusion | `state/settings.ts`, `General.tsx` | F2 | -| #7 | Composer toolbar touch targets (≥44px) | `room/RoomInput.tsx` | E1 | -| #8 | Room Settings horizontal overflow (mobile) | `components/page/style.css.ts` | E2 | -| #9 | Modal fullscreen on mobile (`useModalStyle`) | 22+ modal files | E3 | -| #10 | Composer not hidden by keyboard (`100dvh`) | `src/index.css` | E4 | -| #12 | PiP "All muted" badge re-fixed (was firing on any single mute) | `hooks/useCallSpeakers.ts` | G1 | -| N96 | Call-recovery overlay single "Back" button | `call/CallView.tsx` | A7 | -| N95 | AFK-monitor mic released on mute (OS indicator clears) | `hooks/useAfkAutoMute.ts` | L1 | -| N108 | Maskable PWA icons (Android adaptive) | `public/manifest.json` + `res/android/maskable-*` | L2 | -| EC | EC iframe load watchdog + self-heal + recovery UI | `plugins/call/CallEmbed.ts`, `CallView.tsx` | A7 | -| N105 | Notification clicks work after tab close (SW `notificationclick` + `showNotification`) | `sw.ts`, `utils/dom.ts`, `ClientNonUIFeatures.tsx` | get a msg notif, close the tab, click it → app focuses/opens + routes to the room | -| Gal | MediaGallery lazy-decrypt (true virtualization deferred) | `room/MediaGallery.tsx` | H1 | -| a11y | aria-labels: edit-history / reaction / thread / reply | `message/*` (`FallbackContent`, `Reaction`, `Reply`) | I | -| P3-8 | Thread Panel (side drawer, chips, threaded receipts, thread composer) | `features/room/thread/*`, `RoomTimeline/RoomInput` | 6-step checklist in LOTUS_TODO §P3-8 | -| P4-4 | KaTeX math (`$…$`, `$$…$$`, data-mx-maths; lazy chunk) | `utils/mathParse.ts`, `components/math/` | send `$x^2$`, `$$\int f$$`, `$5 and $10` (stays text), math inside code block (stays text) | -| P4-8 | Encrypted-search cache (opt-in toggle, clear button, logout wipe) | `utils/searchCache.ts`, message-search | enable in search panel → search → reload → coverage persists; logout wipes | -| N97a | Session blob migration + cross-tab logout sync | `state/sessions.ts`, `useSessionSync` | login on old build → new build migrates; logout in tab A → tab B drops to auth | -| P4-1 | Slack-style thread notifications (participating default, All/Mentions/Mute, badge math) | `utils/threadNotifications.ts`, `ClientNonUIFeatures`, `roomToUnread` | 6-step checklist in LOTUS_TODO §P4-1 | -| AW-1 | Scheduled-message cancel no longer ghost-sends (error row on failure) | `ScheduledMessagesTray.tsx` | schedule → cancel with network cut → item stays + error; retry works | -| AW-2 | Emoji lazy-load (search/autocomplete/recents fill in; board opens fast) | `plugins/emoji.ts` + consumers | first emoji-board open of a session: grid+search populate; reactions still label | -| AW-3 | SW precache (repeat-visit near-instant; deploys still picked up immediately) | `sw.ts`, `vite.config.js` | load app twice (2nd = cached assets); deploy → reload picks new version | -| AW-4 | Desktop CSP tighten + Escape/panel fixes + thread Jump to Latest | `tauri.conf.json`, Room/ThreadPanel | desktop: boots, avatars/media load, VT323 font renders, location maps embed, calls connect, deep links work | -| P3-4 | Accessibility compliance pass (collapsed-msg SR sender, form/overlay labels, typing announce, focus-return, `?` help, jsx-a11y CI gate) | `message/*`, `RoomViewTyping`, `features/shortcuts/*`, `eslint.config.mjs` | LOTUS_TESTING §P — axe-core + VoiceOver/NVDA on the golden path | +| ID | Item | File / area | Test | +| :--- | :-------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | +| #2 | Chat-background animation flicker (`contain:paint`) | `lotus/chatBackground.ts` | F1 | +| #4 | Ringtone re-fixes: classic loudness + caller decline notice (A2 ✓ live) | `CallEmbedProvider.tsx`, `ringtones.ts` | A1,A3,A4 | +| #6 | Background vs. seasonal theme mutual exclusion | `state/settings.ts`, `General.tsx` | F2 | +| #7 | Composer toolbar touch targets (≥44px) | `room/RoomInput.tsx` | E1 | +| #8 | Room Settings horizontal overflow (mobile) | `components/page/style.css.ts` | E2 | +| #9 | Modal fullscreen on mobile (`useModalStyle`) | 22+ modal files | E3 | +| #10 | Composer not hidden by keyboard (`100dvh`) | `src/index.css` | E4 | +| #12 | PiP "All muted" badge re-fixed (was firing on any single mute) | `hooks/useCallSpeakers.ts` | G1 | +| N96 | Call-recovery overlay single "Back" button | `call/CallView.tsx` | A7 | +| N95 | AFK-monitor mic released on mute (OS indicator clears) | `hooks/useAfkAutoMute.ts` | L1 | +| N108 | Maskable PWA icons (Android adaptive) | `public/manifest.json` + `res/android/maskable-*` | L2 | +| EC | EC iframe load watchdog + self-heal + recovery UI | `plugins/call/CallEmbed.ts`, `CallView.tsx` | A7 | +| N105 | Notification clicks work after tab close (SW `notificationclick` + `showNotification`) | `sw.ts`, `utils/dom.ts`, `ClientNonUIFeatures.tsx` | get a msg notif, close the tab, click it → app focuses/opens + routes to the room | +| Gal | MediaGallery lazy-decrypt (true virtualization deferred) | `room/MediaGallery.tsx` | H1 | +| a11y | aria-labels: edit-history / reaction / thread / reply | `message/*` (`FallbackContent`, `Reaction`, `Reply`) | I | +| P3-8 | Thread Panel (side drawer, chips, threaded receipts, thread composer) | `features/room/thread/*`, `RoomTimeline/RoomInput` | 6-step checklist in LOTUS_TODO §P3-8 | +| P4-4 | KaTeX math (`$…$`, `$$…$$`, data-mx-maths; lazy chunk) | `utils/mathParse.ts`, `components/math/` | send `$x^2$`, `$$\int f$$`, `$5 and $10` (stays text), math inside code block (stays text) | +| P4-8 | Encrypted-search cache (opt-in toggle, clear button, logout wipe) | `utils/searchCache.ts`, message-search | enable in search panel → search → reload → coverage persists; logout wipes | +| N97a | Session blob migration + cross-tab logout sync | `state/sessions.ts`, `useSessionSync` | login on old build → new build migrates; logout in tab A → tab B drops to auth | +| P4-1 | Slack-style thread notifications (participating default, All/Mentions/Mute, badge math) | `utils/threadNotifications.ts`, `ClientNonUIFeatures`, `roomToUnread` | 6-step checklist in LOTUS_TODO §P4-1 | +| AW-1 | Scheduled-message cancel no longer ghost-sends (error row on failure) | `ScheduledMessagesTray.tsx` | schedule → cancel with network cut → item stays + error; retry works | +| AW-2 | Emoji lazy-load (search/autocomplete/recents fill in; board opens fast) | `plugins/emoji.ts` + consumers | first emoji-board open of a session: grid+search populate; reactions still label | +| AW-3 | SW precache (repeat-visit near-instant; deploys still picked up immediately) | `sw.ts`, `vite.config.js` | load app twice (2nd = cached assets); deploy → reload picks new version | +| AW-4 | Desktop CSP tighten + Escape/panel fixes + thread Jump to Latest | `tauri.conf.json`, Room/ThreadPanel | desktop: boots, avatars/media load, VT323 font renders, location maps embed, calls connect, deep links work | +| P3-4 | Accessibility compliance pass (collapsed-msg SR sender, form/overlay labels, typing announce, focus-return, `?` help, jsx-a11y CI gate) | `message/*`, `RoomViewTyping`, `features/shortcuts/*`, `eslint.config.mjs` | LOTUS_TESTING §P — axe-core + VoiceOver/NVDA on the golden path | +| P6-1 | Desktop Linux parity (no-sleep in calls, launcher badge), autostart toggle, tray Do-Not-Disturb | `native/power.rs`, `lib.rs`, `useTauriDnd`, `General.tsx` | Linux desktop: no display sleep during a call; tray DND silences notifications; launch-on-login persists; Unity badge (Ubuntu); DND toggle polarity | **Verified working in live testing (2026-06):** A2, B1–B4, C1, C3, D (mic/camera/deafen/screenshare/fullscreen/more-menu/PiP). Denoise quality in D is still poor — tracked under the denoise project, not a regression. diff --git a/LOTUS_FEATURES.md b/LOTUS_FEATURES.md index a349bd93d..f97f68339 100644 --- a/LOTUS_FEATURES.md +++ b/LOTUS_FEATURES.md @@ -1267,6 +1267,15 @@ Windows toasts with **click-to-open-room** and **inline quick reply** (WinRT `To When Windows Focus Assist / Quiet Hours is active, Lotus suppresses its own notifications + sounds (reuses the quiet-hours gate). `useTauriFocusAssist` + `focusAssistActiveAtom` ↔ `native/focus_assist.rs` (`SHQueryUserNotificationState`). +### Linux parity + cross-platform extras (P6-1) + +Rounds out the native app beyond Windows (macOS out of scope): + +- **No-sleep during calls on Linux** — a D-Bus `org.freedesktop.ScreenSaver` inhibit (zbus) keeps the display awake mid-call, matching the Windows behavior. `native/power.rs`. +- **Launcher unread badge on Linux** — best-effort Unity `LauncherEntry` D-Bus signal (Ubuntu/Dash-to-Dock/KDE), mirroring the Windows taskbar badge. +- **Launch on login** — `tauri-plugin-autostart` + a **Settings → General "Launch on login"** toggle (desktop-only). +- **Tray "Do Not Disturb"** — a tray checkbox that silences Lotus notifications (feeds `manualDndAtom` into the same quiet-gate as Focus Assist). `useTauriDnd`. + ### Custom Window Chrome (P5-47) Opt-in (Settings → General → **Custom Window Chrome**): replaces the OS title bar with a TDS-styled titlebar (min / max / close + drag region), runtime-reversible via `set_decorations`. `features/desktop/TitleBar.tsx` + `useTauriWindowChrome` ↔ `native/chrome.rs`. diff --git a/LOTUS_TODO.md b/LOTUS_TODO.md index fb80c426e..273ef8763 100644 --- a/LOTUS_TODO.md +++ b/LOTUS_TODO.md @@ -513,7 +513,7 @@ Check back after each Synapse upgrade — re-run `/matrix/client/versions` and ` Buildable follow-ups surfaced by the deep-audit wave. Web Push (N107) deliberately deferred. **macOS is out of scope for all of these — Linux is the parity target (Windows already has most native features).** -### [ ] P6-1 · Desktop — cross-platform parity (Linux + Windows; NO macOS) +### [~] P6-1 · Desktop — cross-platform parity (Linux + Windows; NO macOS) — IMPLEMENTED (2026-07); native CI-compile-pending, runtime-verify on Linux From the desktop audit. Round out the native app now that the full Rust stack compiles: diff --git a/src/app/components/TauriDesktopFeatures.tsx b/src/app/components/TauriDesktopFeatures.tsx index 13e1eb6ef..be462fbb9 100644 --- a/src/app/components/TauriDesktopFeatures.tsx +++ b/src/app/components/TauriDesktopFeatures.tsx @@ -5,6 +5,7 @@ import { useTauriSmtc } from '../hooks/useTauriSmtc'; import { useTauriNetwork } from '../hooks/useTauriNetwork'; import { useTauriToastActions } from '../hooks/useTauriToastActions'; import { useTauriFocusAssist } from '../hooks/useTauriFocusAssist'; +import { useTauriDnd } from '../hooks/useTauriDnd'; /** * Mounts the client-scoped native desktop feature hooks (call/room aware). Each @@ -21,5 +22,6 @@ export function TauriDesktopFeatures(): null { useTauriNetwork(); // P5-49 network-change awareness → sync retry useTauriToastActions(); // P5-41/35 rich toast click → open room, quick reply → send useTauriFocusAssist(); // P5-56 Windows Focus Assist → DND suppression atom + useTauriDnd(); // P6-1 tray "Do Not Disturb" → notification suppression atom return null; } diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index c445637f0..528cf6c74 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -102,7 +102,7 @@ import { useMessageLayoutItems } from '../../../hooks/useMessageLayout'; import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing'; import { SequenceCardStyle } from '../styles.css'; import { useTauriUpdater } from '../../../hooks/useTauriUpdater'; -import { isTauri as isTauriEnv } from '../../../hooks/useTauri'; +import { isTauri as isTauriEnv, invokeTauri, tauriInvoke } from '../../../hooks/useTauri'; import { customWindowChromeAtom } from '../../../state/customWindowChrome'; import { useDateFormatItems } from '../../../hooks/useDateFormat'; import { playCallJoinSound } from '../../../utils/callSounds'; @@ -129,6 +129,38 @@ function DesktopChromeSetting() { ); } +/** + * P6-1 — "Launch on login" toggle (desktop only). Renders nothing in the + * browser. Reads the current state from the `autostart` plugin on mount and + * enables/disables it via the plugin commands when flipped. Not backed by an + * atom — the OS registration is the source of truth, mirrored into local state. + */ +function AutostartSetting() { + const [enabled, setEnabled] = useState(false); + + useEffect(() => { + tauriInvoke()?.('plugin:autostart|is_enabled') + .then((value) => setEnabled(value === true)) + .catch(() => undefined); + }, []); + + const handleChange = (value: boolean) => { + invokeTauri(value ? 'plugin:autostart|enable' : 'plugin:autostart|disable'); + setEnabled(value); + }; + + if (!isTauriEnv()) return null; + return ( + + } + /> + + ); +} + type ThemeSelectorProps = { themeNames: Record; themes: Theme[]; @@ -443,6 +475,7 @@ function Appearance() { + ('lotus-dnd-changed', ({ active }) => setDnd(active)); +} diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx index fba8ad33f..0ff25ca47 100644 --- a/src/app/pages/client/ClientNonUIFeatures.tsx +++ b/src/app/pages/client/ClientNonUIFeatures.tsx @@ -10,6 +10,7 @@ import { ThreadEvent, } from 'matrix-js-sdk'; import { focusAssistActiveAtom } from '../../state/focusAssist'; +import { manualDndAtom } from '../../state/manualDnd'; import { roomToUnreadAtom, unreadEqual, unreadInfoToUnread } from '../../state/room/roomToUnread'; import LogoSVG from '../../../../public/res/lotus.png'; import LogoUnreadSVG from '../../../../public/res/lotus-unread.png'; @@ -128,6 +129,7 @@ function InviteNotifications() { const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds'); const [quietHoursEnabled] = useSetting(settingsAtom, 'quietHoursEnabled'); const focusAssistActive = useAtomValue(focusAssistActiveAtom); + const manualDnd = useAtomValue(manualDndAtom); const [quietHoursStart] = useSetting(settingsAtom, 'quietHoursStart'); const [quietHoursEnd] = useSetting(settingsAtom, 'quietHoursEnd'); const [inviteSoundId] = useSetting(settingsAtom, 'inviteSoundId'); @@ -187,7 +189,9 @@ function InviteNotifications() { useEffect(() => { if (invites.length > perviousInviteLen && mx.getSyncState() === 'SYNCING') { const quietActive = - focusAssistActive || (quietHoursEnabled && isInQuietHours(quietHoursStart, quietHoursEnd)); + focusAssistActive || + manualDnd || + (quietHoursEnabled && isInQuietHours(quietHoursStart, quietHoursEnd)); if (!quietActive) { if (showNotifications && notificationPermission('granted')) { notify(invites.length - perviousInviteLen); @@ -210,6 +214,7 @@ function InviteNotifications() { quietHoursStart, quietHoursEnd, focusAssistActive, + manualDnd, inviteSoundId, ]); @@ -236,6 +241,7 @@ function MessageNotifications() { const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds'); const [quietHoursEnabled] = useSetting(settingsAtom, 'quietHoursEnabled'); const focusAssistActive = useAtomValue(focusAssistActiveAtom); + const manualDnd = useAtomValue(manualDndAtom); const [quietHoursStart] = useSetting(settingsAtom, 'quietHoursStart'); const [quietHoursEnd] = useSetting(settingsAtom, 'quietHoursEnd'); const [messageSoundId] = useSetting(settingsAtom, 'messageSoundId'); @@ -374,7 +380,9 @@ function MessageNotifications() { } const quietActive = - focusAssistActive || (quietHoursEnabled && isInQuietHours(quietHoursStart, quietHoursEnd)); + focusAssistActive || + manualDnd || + (quietHoursEnabled && isInQuietHours(quietHoursStart, quietHoursEnd)); if (quietActive) return; if (showNotifications && notificationPermission('granted')) { @@ -409,6 +417,7 @@ function MessageNotifications() { quietHoursStart, quietHoursEnd, focusAssistActive, + manualDnd, messageSoundId, ], ); diff --git a/src/app/state/manualDnd.ts b/src/app/state/manualDnd.ts new file mode 100644 index 000000000..da3a80503 --- /dev/null +++ b/src/app/state/manualDnd.ts @@ -0,0 +1,14 @@ +import { atom } from 'jotai'; + +/** + * P6-1 — Tray "Do Not Disturb" ↔ notification suppression (manual toggle). + * + * Standalone, non-persisted boolean atom reflecting whether the user has flipped + * the native tray "Do Not Disturb" item. It is driven at runtime by + * `useTauriDnd` from the native `lotus-dnd-changed` event and read by the + * notification gate to suppress notifications while DND is on. Because it mirrors + * a transient session toggle — not a persisted user preference — it is a plain + * in-memory atom that defaults to `false` and is intentionally NOT written to + * `localStorage`. + */ +export const manualDndAtom = atom(false);