feat(desktop): notify users of client updates

This commit is contained in:
tianma-if committed 2026-08-21 21:27:13 +08:00
1 parent 8645e84655
commit a67e4db4b2
4 files changed
+93

No files matched your search

+2
View File
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import { PwaUpdateNotice } from "@/components/PwaUpdateNotice";
import { PwaInstallProvider } from "@/components/PwaInstallContext";
import { PwaIosPrompt } from "@/components/PwaIosPrompt";
import { DesktopUpdateNotice } from "@/components/DesktopUpdateNotice";
import { Button } from "@/components/ui/button";
import {
api,
@@ -229,6 +230,7 @@ export const App = () => {
<Route path="/ai-prompts" element={<AuthenticatedWorkspace />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
<DesktopUpdateNotice />
<PwaUpdateNotice />
<PwaIosPrompt />
</PwaInstallProvider>
@@ -0,0 +1,83 @@
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Download, LoaderCircle, RotateCcw, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
type DesktopUpdateState = "idle" | "available" | "downloaded";
const UPDATE_STATUS_QUERY_KEY = ["desktop-update-status"] as const;
export const DesktopUpdateNotice = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const bridge = window.edgeeverDesktop;
const [dismissedState, setDismissedState] = useState<DesktopUpdateState | null>(null);
const statusQuery = useQuery({
queryKey: UPDATE_STATUS_QUERY_KEY,
queryFn: () => bridge!.updateStatus(),
enabled: bridge?.isAvailable === true,
refetchInterval: 5_000,
retry: 1,
});
const downloadMutation = useMutation({
mutationFn: () => bridge!.downloadUpdate(),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: UPDATE_STATUS_QUERY_KEY });
},
});
const installMutation = useMutation({
mutationFn: () => bridge!.installUpdate(),
});
const updateState = statusQuery.data?.state ?? "idle";
const visible = bridge?.isAvailable === true && updateState !== "idle" && dismissedState !== updateState;
if (!visible) return null;
const downloaded = updateState === "downloaded";
const downloading = downloadMutation.isPending;
const failed = downloadMutation.isError || installMutation.isError;
return (
<div
className="fixed bottom-5 right-5 z-[90] w-[min(22rem,calc(100vw-2rem))] animate-in slide-in-from-bottom-3 fade-in rounded-lg border border-emerald-200 bg-white/95 p-3 text-slate-900 shadow-[0_18px_48px_rgba(15,23,42,0.18)] backdrop-blur duration-300"
role="status"
aria-live="polite"
>
<div className="flex items-start gap-3">
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-emerald-50 text-emerald-700" aria-hidden="true">
{downloading ? <LoaderCircle className="h-4 w-4 animate-spin" /> : downloaded ? <RotateCcw className="h-4 w-4" /> : <Download className="h-4 w-4" />}
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-semibold leading-5">
{t(downloaded ? "systemInfo.desktopUpdateReadyTitle" : "systemInfo.desktopUpdateAvailableTitle")}
</div>
<div className="mt-0.5 text-xs leading-5 text-slate-500">
{t(downloaded ? "systemInfo.desktopUpdateReadyDescription" : "systemInfo.desktopUpdateAvailableDescription")}
</div>
{failed ? <div className="mt-1 text-xs font-medium text-rose-700" role="alert">{t("systemInfo.desktopUpdateFailed")}</div> : null}
<Button
className="mt-2"
size="sm"
variant="solid"
disabled={downloading || installMutation.isPending}
onClick={() => downloaded ? installMutation.mutate() : downloadMutation.mutate()}
>
{downloading
? t("systemInfo.desktopUpdateDownloading")
: t(downloaded ? "systemInfo.desktopUpdateRestart" : "systemInfo.desktopUpdateDownload")}
</Button>
</div>
<button
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-slate-400 transition hover:bg-slate-100 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/70"
type="button"
aria-label={t("common.close")}
onClick={() => setDismissedState(updateState)}
>
<X className="h-4 w-4" />
</button>
</div>
</div>
);
};
+4
View File
@@ -608,6 +608,10 @@ export const enUS = {
releaseNotesUnavailable: "This build does not include an update summary. You can view the full Release notes.",
clientUpdatesNote: "Download desktop updates from <releases>Releases</releases>, or update mobile apps through the corresponding app store.",
viewReleaseNotes: "Full notes",
desktopUpdateAvailableTitle: "Client update available",
desktopUpdateAvailableDescription: "A new version is ready to download and install.",
desktopUpdateReadyTitle: "Client update ready",
desktopUpdateReadyDescription: "Restart EdgeEver to finish updating.",
desktopUpdateDownloading: "Downloading update…",
desktopUpdateReady: "Update downloaded",
desktopUpdateDownload: "Download update",
+4
View File
@@ -608,6 +608,10 @@ export const zhCN = {
releaseNotesUnavailable: "本次构建未包含更新摘要,可查看完整 Release 说明。",
clientUpdatesNote: "桌面端可前往 <releases>Releases</releases> 下载更新,移动端请通过对应的应用商店更新。",
viewReleaseNotes: "完整说明",
desktopUpdateAvailableTitle: "发现客户端新版本",
desktopUpdateAvailableDescription: "新版本已可下载并安装。",
desktopUpdateReadyTitle: "客户端更新已就绪",
desktopUpdateReadyDescription: "重启 EdgeEver 以完成更新。",
desktopUpdateDownloading: "正在下载更新…",
desktopUpdateReady: "更新已下载完成",
desktopUpdateDownload: "下载更新",