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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user