feat(ui): toast when UI updates
This commit is contained in:
@@ -26,4 +26,7 @@ export const appMessages = {
|
||||
"releases.upgradeRequired.message.withVersion": "Update to CodeNomad {version} to use the latest UI.",
|
||||
"releases.upgradeRequired.message.noVersion": "Update CodeNomad to use the latest UI.",
|
||||
"releases.upgradeRequired.action.getUpdate": "Get update",
|
||||
|
||||
"releases.uiUpdated.title": "UI updated",
|
||||
"releases.uiUpdated.message": "UI is now updated to {version}.",
|
||||
} as const
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createEffect, createSignal } from "solid-js"
|
||||
import type { SupportMeta } from "../../../server/src/api-types"
|
||||
import type { ServerMeta, SupportMeta } from "../../../server/src/api-types"
|
||||
import { getServerMeta } from "../lib/server-meta"
|
||||
import { showToastNotification, ToastHandle } from "../lib/notifications"
|
||||
import { getLogger } from "../lib/logger"
|
||||
@@ -10,10 +10,13 @@ const log = getLogger("actions")
|
||||
|
||||
const [supportInfo, setSupportInfo] = createSignal<SupportMeta | null>(null)
|
||||
|
||||
const UI_VERSION_STORAGE_KEY = "codenomad:lastSeenUiVersion"
|
||||
|
||||
let initialized = false
|
||||
let visibilityEffectInitialized = false
|
||||
let activeToast: ToastHandle | null = null
|
||||
let activeToastKey: string | null = null
|
||||
let uiUpdateToasted = false
|
||||
|
||||
function dismissActiveToast() {
|
||||
if (activeToast) {
|
||||
@@ -76,6 +79,7 @@ async function refreshFromMeta() {
|
||||
try {
|
||||
const meta = await getServerMeta(true)
|
||||
setSupportInfo(meta.support ?? null)
|
||||
maybeNotifyUiUpdated(meta)
|
||||
} catch (error) {
|
||||
log.warn("Unable to load server metadata for support info", error)
|
||||
}
|
||||
@@ -84,3 +88,82 @@ async function refreshFromMeta() {
|
||||
export function useSupportInfo() {
|
||||
return supportInfo
|
||||
}
|
||||
|
||||
function maybeNotifyUiUpdated(meta: ServerMeta) {
|
||||
if (uiUpdateToasted) return
|
||||
uiUpdateToasted = true
|
||||
|
||||
const currentVersion = meta.ui?.version?.trim()
|
||||
if (!currentVersion) return
|
||||
|
||||
const previousVersion = safeReadLocalStorage(UI_VERSION_STORAGE_KEY)
|
||||
safeWriteLocalStorage(UI_VERSION_STORAGE_KEY, currentVersion)
|
||||
|
||||
if (!previousVersion) return
|
||||
if (previousVersion === currentVersion) return
|
||||
|
||||
// Only show the "updated" toast when the server is serving a downloaded UI bundle.
|
||||
if (meta.ui?.source !== "downloaded") return
|
||||
if (!isSemverUpgrade(previousVersion, currentVersion)) return
|
||||
|
||||
showToastNotification({
|
||||
title: tGlobal("releases.uiUpdated.title"),
|
||||
message: tGlobal("releases.uiUpdated.message", { version: currentVersion }),
|
||||
variant: "success",
|
||||
duration: 8000,
|
||||
position: "bottom-right",
|
||||
})
|
||||
}
|
||||
|
||||
function safeReadLocalStorage(key: string): string | null {
|
||||
try {
|
||||
if (typeof window === "undefined" || !window.localStorage) return null
|
||||
return window.localStorage.getItem(key)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function safeWriteLocalStorage(key: string, value: string) {
|
||||
try {
|
||||
if (typeof window === "undefined" || !window.localStorage) return
|
||||
window.localStorage.setItem(key, value)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function isSemverUpgrade(previous: string, current: string): boolean {
|
||||
const prevParsed = parseSemverCore(previous)
|
||||
const currParsed = parseSemverCore(current)
|
||||
if (!prevParsed || !currParsed) {
|
||||
// If either version isn't semver-like, default to "changed".
|
||||
return true
|
||||
}
|
||||
return compareSemverCore(currParsed, prevParsed) > 0
|
||||
}
|
||||
|
||||
function compareSemverCore(a: { major: number; minor: number; patch: number }, b: { major: number; minor: number; patch: number }): number {
|
||||
if (a.major !== b.major) return a.major > b.major ? 1 : -1
|
||||
if (a.minor !== b.minor) return a.minor > b.minor ? 1 : -1
|
||||
if (a.patch !== b.patch) return a.patch > b.patch ? 1 : -1
|
||||
return 0
|
||||
}
|
||||
|
||||
function parseSemverCore(value: string): { major: number; minor: number; patch: number } | null {
|
||||
const core = value.trim().replace(/^v/i, "").split("-", 1)[0]
|
||||
if (!core) return null
|
||||
const parts = core.split(".")
|
||||
if (parts.length < 2) return null
|
||||
|
||||
const parsePart = (input: string | undefined) => {
|
||||
const n = Number.parseInt((input ?? "0").replace(/[^0-9]/g, ""), 10)
|
||||
return Number.isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
return {
|
||||
major: parsePart(parts[0]),
|
||||
minor: parsePart(parts[1]),
|
||||
patch: parsePart(parts[2]),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user