add release monitor and ui toast
This commit is contained in:
@@ -13,6 +13,7 @@ import { useTheme } from "./lib/theme"
|
||||
import { useCommands } from "./lib/hooks/use-commands"
|
||||
import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle"
|
||||
import { getLogger } from "./lib/logger"
|
||||
import { initReleaseNotifications } from "./stores/releases"
|
||||
import {
|
||||
hasInstances,
|
||||
isSelectingFolder,
|
||||
@@ -67,6 +68,10 @@ const App: Component = () => {
|
||||
void initMarkdown(isDark()).catch((error) => log.error("Failed to initialize markdown", error))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
initReleaseNotifications()
|
||||
})
|
||||
|
||||
const activeInstance = createMemo(() => getActiveInstance())
|
||||
const activeSessionIdForInstance = createMemo(() => {
|
||||
const instance = activeInstance()
|
||||
@@ -352,9 +357,8 @@ const App: Component = () => {
|
||||
<RemoteAccessOverlay open={remoteAccessOpen()} onClose={() => setRemoteAccessOpen(false)} />
|
||||
|
||||
<AlertDialog />
|
||||
|
||||
<Toaster
|
||||
|
||||
<Toaster
|
||||
position="top-right"
|
||||
gutter={16}
|
||||
toastOptions={{
|
||||
@@ -367,4 +371,5 @@ const App: Component = () => {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default App
|
||||
|
||||
@@ -8,6 +8,7 @@ import { openNativeFolderDialog, supportsNativeDialogs } from "../lib/native/nat
|
||||
|
||||
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
|
||||
|
||||
|
||||
interface FolderSelectionViewProps {
|
||||
onSelectFolder: (folder: string, binaryPath?: string) => void
|
||||
isLoading?: boolean
|
||||
@@ -253,6 +254,8 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
||||
<div class="space-y-4 flex-1 min-h-0 overflow-hidden flex flex-col">
|
||||
|
||||
<Show
|
||||
|
||||
|
||||
when={folders().length > 0}
|
||||
fallback={
|
||||
<div class="panel panel-empty-state flex-1">
|
||||
|
||||
@@ -2,11 +2,23 @@ import toast from "solid-toast"
|
||||
|
||||
export type ToastVariant = "info" | "success" | "warning" | "error"
|
||||
|
||||
export type ToastHandle = {
|
||||
id: string
|
||||
dismiss: () => void
|
||||
}
|
||||
|
||||
type ToastPosition = "top-left" | "top-right" | "top-center" | "bottom-left" | "bottom-right" | "bottom-center"
|
||||
|
||||
export type ToastPayload = {
|
||||
title?: string
|
||||
message: string
|
||||
variant: ToastVariant
|
||||
duration?: number
|
||||
position?: ToastPosition
|
||||
action?: {
|
||||
label: string
|
||||
href: string
|
||||
}
|
||||
}
|
||||
|
||||
const variantAccent: Record<
|
||||
@@ -44,11 +56,11 @@ const variantAccent: Record<
|
||||
},
|
||||
}
|
||||
|
||||
export function showToastNotification(payload: ToastPayload) {
|
||||
export function showToastNotification(payload: ToastPayload): ToastHandle {
|
||||
const accent = variantAccent[payload.variant]
|
||||
const duration = payload.duration ?? 10000
|
||||
|
||||
toast.custom(
|
||||
const id = toast.custom(
|
||||
() => (
|
||||
<div class={`pointer-events-auto w-[320px] max-w-[360px] rounded-lg border px-4 py-3 shadow-xl ${accent.container}`}>
|
||||
<div class="flex items-start gap-3">
|
||||
@@ -56,16 +68,32 @@ export function showToastNotification(payload: ToastPayload) {
|
||||
<div class="flex-1 text-sm leading-snug">
|
||||
{payload.title && <p class={`font-semibold ${accent.headline}`}>{payload.title}</p>}
|
||||
<p class={`${accent.body} ${payload.title ? "mt-1" : ""}`}>{payload.message}</p>
|
||||
{payload.action && (
|
||||
<a
|
||||
class="mt-3 inline-flex items-center text-xs font-semibold uppercase tracking-wide text-sky-300 hover:text-sky-200"
|
||||
href={payload.action.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{payload.action.label}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
duration,
|
||||
position: payload.position ?? "top-right",
|
||||
ariaProps: {
|
||||
role: "status",
|
||||
"aria-live": "polite",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
id,
|
||||
dismiss: () => toast.dismiss(id),
|
||||
}
|
||||
}
|
||||
|
||||
70
packages/ui/src/stores/releases.ts
Normal file
70
packages/ui/src/stores/releases.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { createSignal } from "solid-js"
|
||||
import type { LatestReleaseInfo, WorkspaceEventPayload } from "../../../server/src/api-types"
|
||||
import { getServerMeta } from "../lib/server-meta"
|
||||
import { serverEvents } from "../lib/server-events"
|
||||
import { showToastNotification, ToastHandle } from "../lib/notifications"
|
||||
import { getLogger } from "../lib/logger"
|
||||
|
||||
const log = getLogger("actions")
|
||||
|
||||
const [availableRelease, setAvailableRelease] = createSignal<LatestReleaseInfo | null>(null)
|
||||
|
||||
let initialized = false
|
||||
let activeToast: ToastHandle | null = null
|
||||
|
||||
function dismissActiveToast() {
|
||||
if (activeToast) {
|
||||
activeToast.dismiss()
|
||||
activeToast = null
|
||||
}
|
||||
}
|
||||
|
||||
export function initReleaseNotifications() {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
initialized = true
|
||||
|
||||
void refreshFromMeta()
|
||||
|
||||
serverEvents.on("app.releaseAvailable", (event) => {
|
||||
const typedEvent = event as Extract<WorkspaceEventPayload, { type: "app.releaseAvailable" }>
|
||||
applyRelease(typedEvent.release)
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshFromMeta() {
|
||||
try {
|
||||
const meta = await getServerMeta(true)
|
||||
if (meta.latestRelease) {
|
||||
applyRelease(meta.latestRelease)
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn("Unable to load server metadata for release info", error)
|
||||
}
|
||||
}
|
||||
|
||||
function applyRelease(release: LatestReleaseInfo | null | undefined) {
|
||||
if (!release) {
|
||||
setAvailableRelease(null)
|
||||
dismissActiveToast()
|
||||
return
|
||||
}
|
||||
setAvailableRelease(release)
|
||||
dismissActiveToast()
|
||||
activeToast = showToastNotification({
|
||||
title: `CodeNomad ${release.version}`,
|
||||
message: release.channel === "dev" ? "Dev release build available." : "New stable build on GitHub.",
|
||||
variant: "info",
|
||||
duration: Number.POSITIVE_INFINITY,
|
||||
position: "bottom-right",
|
||||
action: {
|
||||
label: "View release",
|
||||
href: release.url,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useAvailableRelease() {
|
||||
return availableRelease
|
||||
}
|
||||
Reference in New Issue
Block a user