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:
+1
-1
@@ -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.
|
**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`.
|
**Mechanism:** Use the `useTauriUpdater` hook in a global component like `ClientNonUIFeatures.tsx`.
|
||||||
|
|||||||
@@ -41,8 +41,12 @@ function ToastCard({ toast }: ToastCardProps) {
|
|||||||
}, [dismiss, toast.id]);
|
}, [dismiss, toast.id]);
|
||||||
|
|
||||||
const handleCardClick = () => {
|
const handleCardClick = () => {
|
||||||
// window.location.hash setter auto-prepends '#', so values must not include it
|
if (toast.onClick) {
|
||||||
window.location.hash = toast.hashPath ?? `/room/${toast.roomId}`;
|
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);
|
dismiss(toast.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { usePresenceUpdater } from '../../hooks/usePresenceUpdater';
|
|||||||
import { useDeepLinkNavigate } from '../../hooks/useDeepLinkNavigate';
|
import { useDeepLinkNavigate } from '../../hooks/useDeepLinkNavigate';
|
||||||
import { toastQueueAtom } from '../../state/toast';
|
import { toastQueueAtom } from '../../state/toast';
|
||||||
import { useReminders } from '../../hooks/useReminders';
|
import { useReminders } from '../../hooks/useReminders';
|
||||||
|
import { useTauriUpdater } from '../../hooks/useTauriUpdater';
|
||||||
|
|
||||||
function isInQuietHours(start: string, end: string): boolean {
|
function isInQuietHours(start: string, end: string): boolean {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -429,6 +430,46 @@ function ReminderMonitor() {
|
|||||||
return null;
|
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() {
|
function LotusDenoiseFeature() {
|
||||||
const setToast = useSetAtom(toastQueueAtom);
|
const setToast = useSetAtom(toastQueueAtom);
|
||||||
|
|
||||||
@@ -465,6 +506,7 @@ export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) {
|
|||||||
<InviteNotifications />
|
<InviteNotifications />
|
||||||
<MessageNotifications />
|
<MessageNotifications />
|
||||||
<ReminderMonitor />
|
<ReminderMonitor />
|
||||||
|
<TauriUpdateFeature />
|
||||||
<LotusDenoiseFeature />
|
<LotusDenoiseFeature />
|
||||||
<DeepLinkNavigator />
|
<DeepLinkNavigator />
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type ToastNotif = {
|
|||||||
roomName: string;
|
roomName: string;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
hashPath?: string; // overrides window.location.hash navigation when set
|
hashPath?: string; // overrides window.location.hash navigation when set
|
||||||
|
onClick?: () => void; // custom click handler; skips hash navigation when set
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseAtom = atom<ToastNotif[]>([]);
|
const baseAtom = atom<ToastNotif[]>([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user