feat(desktop): ask once what closing the window should do (cinny-desktop #5)
CI / Build & Quality Checks (push) Successful in 1m57s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 15s
CI / Trigger Desktop Build (push) Successful in 9s
CI / Playwright smoke (e2e) (push) Canceled after 12m7s
CI / Build & Quality Checks (push) Successful in 1m57s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 15s
CI / Trigger Desktop Build (push) Successful in 9s
CI / Playwright smoke (e2e) (push) Canceled after 12m7s
The first time the window is closed, a dialog asks "Keep Lotus Chat
running?" — Keep running in the tray (default, focused) / Quit when I
close the window — with an opt-in "Start Lotus Chat when I sign in"
checkbox in the same moment (per the approved design: one dialog, no
wizard). The choice is saved natively; Settings → General → "When I
close the window" changes it later (tray / quit / ask me).
The dialog is role="dialog" aria-modal, labelled and described, with
focus on the default button. Web-side flow verified with a stubbed
native side: event → dialog → checkbox + Quit sends autostart enable +
resolve_close_request("quit"); the Settings select saves "tray".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
206a3e933a
commit
359c79a440
@@ -0,0 +1,114 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Dialog,
|
||||||
|
Header,
|
||||||
|
Overlay,
|
||||||
|
OverlayBackdrop,
|
||||||
|
OverlayCenter,
|
||||||
|
Text,
|
||||||
|
config,
|
||||||
|
} from 'folds';
|
||||||
|
import { invokeTauri, isTauri, tauriInvoke, useTauriEvent } from '../hooks/useTauri';
|
||||||
|
import { useModalStyle } from '../hooks/useModalStyle';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cinny-desktop #5: the first time the window is closed, ask whether Lotus Chat
|
||||||
|
* should keep running in the tray (calls, messages, notifications) or quit —
|
||||||
|
* and, in the same moment, whether it should start when you sign in. Asked
|
||||||
|
* once; changeable in Settings → General. The native side sends
|
||||||
|
* `lotus-close-requested` only while the choice is still "ask", and never
|
||||||
|
* during a call (a call always goes to the tray).
|
||||||
|
*/
|
||||||
|
export function CloseBehaviorPrompt() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [launchOnLogin, setLaunchOnLogin] = useState(false);
|
||||||
|
const modalStyle = useModalStyle(420);
|
||||||
|
|
||||||
|
// Tells the native side we're listening (see get_close_behavior).
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isTauri()) return;
|
||||||
|
tauriInvoke()?.('get_close_behavior').catch(() => undefined);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useTauriEvent('lotus-close-requested', () => setOpen(true));
|
||||||
|
|
||||||
|
const choose = useCallback(
|
||||||
|
(value: 'tray' | 'quit') => {
|
||||||
|
if (launchOnLogin) invokeTauri('plugin:autostart|enable');
|
||||||
|
setOpen(false);
|
||||||
|
invokeTauri('resolve_close_request', { value });
|
||||||
|
},
|
||||||
|
[launchOnLogin],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
return (
|
||||||
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
|
<OverlayCenter>
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: '#close-behavior-keep',
|
||||||
|
// Dismissing without choosing keeps the window open; the next close
|
||||||
|
// asks again.
|
||||||
|
onDeactivate: () => setOpen(false),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog
|
||||||
|
variant="Surface"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="close-behavior-title"
|
||||||
|
aria-describedby="close-behavior-body"
|
||||||
|
style={modalStyle}
|
||||||
|
>
|
||||||
|
<Header
|
||||||
|
style={{
|
||||||
|
padding: `0 ${config.space.S400}`,
|
||||||
|
borderBottomWidth: config.borderWidth.B300,
|
||||||
|
}}
|
||||||
|
variant="Surface"
|
||||||
|
size="500"
|
||||||
|
>
|
||||||
|
<Text as="h2" size="H4" id="close-behavior-title">
|
||||||
|
Keep Lotus Chat running?
|
||||||
|
</Text>
|
||||||
|
</Header>
|
||||||
|
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||||
|
<Text priority="400" id="close-behavior-body">
|
||||||
|
Lotus Chat can keep running in the system tray when you close this window, so
|
||||||
|
messages, notifications and calls still reach you. Open it again from the tray icon.
|
||||||
|
</Text>
|
||||||
|
<Box as="label" alignItems="Center" gap="200" style={{ cursor: 'pointer' }}>
|
||||||
|
<Checkbox
|
||||||
|
size="300"
|
||||||
|
variant="Primary"
|
||||||
|
checked={launchOnLogin}
|
||||||
|
onClick={(e: React.MouseEvent<HTMLInputElement>) =>
|
||||||
|
setLaunchOnLogin(e.currentTarget.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Text size="T300">Start Lotus Chat when I sign in to my computer</Text>
|
||||||
|
</Box>
|
||||||
|
<Box direction="Column" gap="200">
|
||||||
|
<Button id="close-behavior-keep" variant="Primary" onClick={() => choose('tray')}>
|
||||||
|
<Text size="B400">Keep running in the tray</Text>
|
||||||
|
</Button>
|
||||||
|
<Button variant="Secondary" fill="Soft" onClick={() => choose('quit')}>
|
||||||
|
<Text size="B400">Quit when I close the window</Text>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Text size="T200" priority="300">
|
||||||
|
You can change this in Settings → General.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
</FocusTrap>
|
||||||
|
</OverlayCenter>
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -164,11 +164,18 @@ function AutostartSetting() {
|
|||||||
// [cinny-desktop #3] null until the native side answers (older builds don't
|
// [cinny-desktop #3] null until the native side answers (older builds don't
|
||||||
// have the command, so the switch just stays hidden there).
|
// have the command, so the switch just stays hidden there).
|
||||||
const [startMinimized, setStartMinimized] = useState<boolean | null>(null);
|
const [startMinimized, setStartMinimized] = useState<boolean | null>(null);
|
||||||
|
// [cinny-desktop #5] "ask" | "tray" | "quit"; null on builds without it.
|
||||||
|
const [closeBehavior, setCloseBehavior] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
tauriInvoke()?.('plugin:autostart|is_enabled')
|
tauriInvoke()?.('plugin:autostart|is_enabled')
|
||||||
.then((value) => setEnabled(value === true))
|
.then((value) => setEnabled(value === true))
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
|
tauriInvoke()?.('get_close_behavior')
|
||||||
|
.then((value) => {
|
||||||
|
if (typeof value === 'string') setCloseBehavior(value);
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
tauriInvoke()?.('get_start_minimized')
|
tauriInvoke()?.('get_start_minimized')
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
if (typeof value === 'boolean') setStartMinimized(value);
|
if (typeof value === 'boolean') setStartMinimized(value);
|
||||||
@@ -181,6 +188,11 @@ function AutostartSetting() {
|
|||||||
setEnabled(value);
|
setEnabled(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloseBehavior = (value: string) => {
|
||||||
|
invokeTauri('set_close_behavior', { value });
|
||||||
|
setCloseBehavior(value);
|
||||||
|
};
|
||||||
|
|
||||||
const handleStartMinimized = (value: boolean) => {
|
const handleStartMinimized = (value: boolean) => {
|
||||||
invokeTauri('set_start_minimized', { value });
|
invokeTauri('set_start_minimized', { value });
|
||||||
setStartMinimized(value);
|
setStartMinimized(value);
|
||||||
@@ -194,6 +206,24 @@ function AutostartSetting() {
|
|||||||
description="Start Lotus Chat automatically when you sign in to your computer."
|
description="Start Lotus Chat automatically when you sign in to your computer."
|
||||||
after={<Switch variant="Primary" value={enabled} onChange={handleChange} />}
|
after={<Switch variant="Primary" value={enabled} onChange={handleChange} />}
|
||||||
/>
|
/>
|
||||||
|
{closeBehavior !== null && (
|
||||||
|
<SettingTile
|
||||||
|
title="When I close the window"
|
||||||
|
description="Keep Lotus Chat running in the system tray (messages, notifications and calls keep arriving), or quit it. During a call, closing always keeps it running."
|
||||||
|
after={
|
||||||
|
<SettingsSelect
|
||||||
|
value={closeBehavior}
|
||||||
|
onChange={handleCloseBehavior}
|
||||||
|
aria-label="When I close the window"
|
||||||
|
options={[
|
||||||
|
{ value: 'tray', label: 'Keep running in the tray' },
|
||||||
|
{ value: 'quit', label: 'Quit Lotus Chat' },
|
||||||
|
{ value: 'ask', label: 'Ask me' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{enabled && startMinimized !== null && (
|
{enabled && startMinimized !== null && (
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Start minimized"
|
title="Start minimized"
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import { getRoomRetentionMs, isExpired } from '../../utils/retention';
|
|||||||
import { useTauriUpdateProgress, useTauriUpdater } from '../../hooks/useTauriUpdater';
|
import { useTauriUpdateProgress, useTauriUpdater } from '../../hooks/useTauriUpdater';
|
||||||
import { isNetworkUpdateError } from '../../utils/updateErrors';
|
import { isNetworkUpdateError } from '../../utils/updateErrors';
|
||||||
import { invokeTauri, isTauri as isTauriApp, useTauriEvent } from '../../hooks/useTauri';
|
import { invokeTauri, isTauri as isTauriApp, useTauriEvent } from '../../hooks/useTauri';
|
||||||
|
import { CloseBehaviorPrompt } from '../../components/CloseBehaviorPrompt';
|
||||||
import { TauriDesktopFeatures } from '../../components/TauriDesktopFeatures';
|
import { TauriDesktopFeatures } from '../../components/TauriDesktopFeatures';
|
||||||
import { KeyboardShortcutsDialog, useKeyboardShortcutsTrigger } from '../../features/shortcuts';
|
import { KeyboardShortcutsDialog, useKeyboardShortcutsTrigger } from '../../features/shortcuts';
|
||||||
import { useRoomsListener } from '../../hooks/useRoomsListener';
|
import { useRoomsListener } from '../../hooks/useRoomsListener';
|
||||||
@@ -1088,6 +1089,7 @@ export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) {
|
|||||||
<RetentionSweeper />
|
<RetentionSweeper />
|
||||||
<TauriUpdateFeature />
|
<TauriUpdateFeature />
|
||||||
<TauriDesktopFeatures />
|
<TauriDesktopFeatures />
|
||||||
|
<CloseBehaviorPrompt />
|
||||||
<LotusDenoiseFeature />
|
<LotusDenoiseFeature />
|
||||||
<DeepLinkNavigator />
|
<DeepLinkNavigator />
|
||||||
<KeyboardShortcutsFeature />
|
<KeyboardShortcutsFeature />
|
||||||
|
|||||||
Reference in New Issue
Block a user