feat(tauri): proactive update notifications via toast (P5-40)

TauriUpdateFeature component in ClientNonUIFeatures checks for updates
on mount and every 12h (skips if checked within the window). On update
available, fires a Lotus toast: "Lotus Chat vX.Y.Z is ready to install."
Clicking the toast calls install(). No-op on web (isTauri guard).

Also adds optional onClick to ToastNotif type and wires it in
LotusToastContainer so custom click handlers can skip hash navigation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 15:39:23 -04:00
parent baa12823f7
commit a6bf4eb7e7
4 changed files with 50 additions and 3 deletions
+1 -1
View File
@@ -349,7 +349,7 @@ Themes:
---
### [ ] P5-40 · Desktop — Proactive Update Notifications (Tauri)
### [x] P5-40 · Desktop — Proactive Update Notifications (Tauri) ⚠️ UNTESTED (requires Tauri build)
**What:** Automatically check for app updates on launch and periodically during long sessions. If an update is available, show an in-app toast or badge (e.g., on the Settings icon) to alert the user without requiring a manual check in settings.
**Mechanism:** Use the `useTauriUpdater` hook in a global component like `ClientNonUIFeatures.tsx`.
@@ -41,8 +41,12 @@ function ToastCard({ toast }: ToastCardProps) {
}, [dismiss, toast.id]);
const handleCardClick = () => {
// window.location.hash setter auto-prepends '#', so values must not include it
window.location.hash = toast.hashPath ?? `/room/${toast.roomId}`;
if (toast.onClick) {
toast.onClick();
} else {
// window.location.hash setter auto-prepends '#', so values must not include it
window.location.hash = toast.hashPath ?? `/room/${toast.roomId}`;
}
dismiss(toast.id);
};
@@ -37,6 +37,7 @@ import { usePresenceUpdater } from '../../hooks/usePresenceUpdater';
import { useDeepLinkNavigate } from '../../hooks/useDeepLinkNavigate';
import { toastQueueAtom } from '../../state/toast';
import { useReminders } from '../../hooks/useReminders';
import { useTauriUpdater } from '../../hooks/useTauriUpdater';
function isInQuietHours(start: string, end: string): boolean {
const now = new Date();
@@ -429,6 +430,46 @@ function ReminderMonitor() {
return null;
}
const TAURI_UPDATE_CHECK_INTERVAL = 12 * 60 * 60_000; // 12 hours
const TAURI_UPDATE_LAST_CHECK_KEY = 'lotus.tauriUpdateLastCheck';
function TauriUpdateFeature() {
const { isTauri, status, check, install } = useTauriUpdater();
const setToast = useSetAtom(toastQueueAtom);
const firedRef = useRef<string | null>(null);
useEffect(() => {
if (!isTauri) return;
const runCheck = () => {
const last = Number(localStorage.getItem(TAURI_UPDATE_LAST_CHECK_KEY) ?? '0');
if (Date.now() - last < TAURI_UPDATE_CHECK_INTERVAL) return;
localStorage.setItem(TAURI_UPDATE_LAST_CHECK_KEY, String(Date.now()));
check();
};
runCheck();
const interval = setInterval(runCheck, TAURI_UPDATE_CHECK_INTERVAL);
return () => clearInterval(interval);
}, [isTauri, check]);
useEffect(() => {
if (status.state !== 'available') return;
if (firedRef.current === status.version) return;
firedRef.current = status.version;
setToast({
id: `tauri-update-${status.version}`,
displayName: 'Update Available',
body: `Lotus Chat ${status.version} is ready to install.`,
roomName: 'System',
roomId: '',
onClick: install,
});
}, [status, setToast, install]);
return null;
}
function LotusDenoiseFeature() {
const setToast = useSetAtom(toastQueueAtom);
@@ -465,6 +506,7 @@ export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) {
<InviteNotifications />
<MessageNotifications />
<ReminderMonitor />
<TauriUpdateFeature />
<LotusDenoiseFeature />
<DeepLinkNavigator />
{children}
+1
View File
@@ -8,6 +8,7 @@ export type ToastNotif = {
roomName: string;
roomId: string;
hashPath?: string; // overrides window.location.hash navigation when set
onClick?: () => void; // custom click handler; skips hash navigation when set
};
const baseAtom = atom<ToastNotif[]>([]);