feat: in-app update checker + Spinner import for General settings

- Add useTauriUpdater hook (check_for_update / install_update commands)
- Add AppUpdates section to General settings (Tauri-only, hidden on web)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 20:31:35 -04:00
parent 77a29ed3c6
commit 469b9aa9c6
2 changed files with 90 additions and 0 deletions
@@ -26,6 +26,7 @@ import {
PopOut,
RectCords,
Scroll,
Spinner,
Switch,
Text,
toRem,
@@ -63,6 +64,7 @@ import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
import { useDateFormatItems } from '../../../hooks/useDateFormat';
import { SequenceCardStyle } from '../styles.css';
import { useTauriUpdater } from '../../../hooks/useTauriUpdater';
type ThemeSelectorProps = {
themeNames: Record<string, string>;
@@ -1476,6 +1478,46 @@ function Messages() {
);
}
function AppUpdates() {
const { isTauri, status, check, install } = useTauriUpdater();
if (!isTauri) return null;
const description =
status.state === 'checking'
? 'Checking for updates...'
: status.state === 'up-to-date'
? 'Lotus Chat is up to date.'
: status.state === 'available'
? `Update available: v${status.version}`
: status.state === 'installing'
? 'Installing update, the app will restart shortly...'
: status.state === 'error'
? `Update check failed: ${status.message}`
: 'Check for a new version of Lotus Chat.';
const after =
status.state === 'available' ? (
<Button size="300" radii="300" onClick={install}>
<Text size="B300">Install &amp; Restart</Text>
</Button>
) : status.state === 'checking' || status.state === 'installing' ? (
<Spinner variant="Secondary" size="200" />
) : (
<Button size="300" radii="300" variant="Secondary" onClick={check}>
<Text size="B300">Check</Text>
</Button>
);
return (
<Box direction="Column" gap="100">
<Text size="L400">App Updates</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Check for Updates" description={description} after={after} />
</SequenceCard>
</Box>
);
}
type GeneralProps = {
requestClose: () => void;
};
@@ -1506,6 +1548,7 @@ export function General({ requestClose }: GeneralProps) {
<Messages />
<Privacy />
<Calls />
<AppUpdates />
</Box>
</PageContent>
</Scroll>
+47
View File
@@ -0,0 +1,47 @@
import { useState, useCallback } from 'react';
type TauriInternals = { invoke: (cmd: string, args?: Record<string, unknown>) => Promise<unknown> };
const tauriInvoke = (): TauriInternals['invoke'] | undefined =>
(window as unknown as { __TAURI_INTERNALS__?: TauriInternals }).__TAURI_INTERNALS__?.invoke;
type UpdateStatus =
| { state: 'idle' }
| { state: 'checking' }
| { state: 'up-to-date' }
| { state: 'available'; version: string }
| { state: 'installing' }
| { state: 'error'; message: string };
export function useTauriUpdater() {
const isTauri = !!tauriInvoke();
const [status, setStatus] = useState<UpdateStatus>({ state: 'idle' });
const check = useCallback(async () => {
const invoke = tauriInvoke();
if (!invoke) return;
setStatus({ state: 'checking' });
try {
const result = (await invoke('check_for_update')) as { available: boolean; version?: string };
setStatus(
result.available && result.version
? { state: 'available', version: result.version }
: { state: 'up-to-date' },
);
} catch (e) {
setStatus({ state: 'error', message: String(e) });
}
}, []);
const install = useCallback(async () => {
const invoke = tauriInvoke();
if (!invoke) return;
setStatus({ state: 'installing' });
try {
await invoke('install_update');
} catch (e) {
setStatus({ state: 'error', message: String(e) });
}
}, []);
return { isTauri, status, check, install };
}