feat(desktop): tray Do-Not-Disturb + Launch-on-login toggle (P6-1 web)

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 13:31:09 -04:00
parent 625f0c2386
commit 804caa5130
8 changed files with 120 additions and 31 deletions
@@ -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;
}
+34 -1
View File
@@ -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 (
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Launch on login"
description="Start Lotus Chat automatically when you sign in to your computer."
after={<Switch variant="Primary" value={enabled} onChange={handleChange} />}
/>
</SequenceCard>
);
}
type ThemeSelectorProps = {
themeNames: Record<string, string>;
themes: Theme[];
@@ -443,6 +475,7 @@ function Appearance() {
</SequenceCard>
<DesktopChromeSetting />
<AutostartSetting />
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
+21
View File
@@ -0,0 +1,21 @@
import { useSetAtom } from 'jotai';
import { manualDndAtom } from '../state/manualDnd';
import { useTauriEvent } from './useTauri';
/** Detail shape of the `lotus-dnd-changed` event emitted by the native side. */
type DndChangedDetail = {
active: boolean;
};
/**
* P6-1 — Tray "Do Not Disturb" → notification suppression (desktop). Subscribes
* to the native `lotus-dnd-changed` event (emitted when the user toggles the
* tray "Do Not Disturb" item, `{ active }`) and mirrors it into `manualDndAtom`,
* which the notification gate reads to suppress notifications while DND is on.
* Inert in the browser, since `useTauriEvent` only listens under Tauri.
*/
export function useTauriDnd(): void {
const setDnd = useSetAtom(manualDndAtom);
useTauriEvent<DndChangedDetail>('lotus-dnd-changed', ({ active }) => setDnd(active));
}
+11 -2
View File
@@ -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,
],
);
+14
View File
@@ -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);