From 2032f8385cc74c1a316dfe196bac2632a44ac21d Mon Sep 17 00:00:00 2001 From: Harvey Date: Sat, 18 Apr 2026 02:34:24 +0100 Subject: [PATCH] Update whats new modal logic --- .../src/lib/components/VersionOverlays.svelte | 35 +++++--- .../src/lib/components/WhatsNewModal.svelte | 82 +++++++++++-------- 2 files changed, 72 insertions(+), 45 deletions(-) diff --git a/frontend/src/lib/components/VersionOverlays.svelte b/frontend/src/lib/components/VersionOverlays.svelte index 6e73078..ffb7c29 100644 --- a/frontend/src/lib/components/VersionOverlays.svelte +++ b/frontend/src/lib/components/VersionOverlays.svelte @@ -16,11 +16,28 @@ const currentVersion = $derived(versionQuery.data?.version ?? null); const buildDate = $derived(versionQuery.data?.build_date ?? null); const isDev = $derived(currentVersion === 'dev' || currentVersion === 'hosting-local'); - const currentRelease = $derived( - releaseHistoryQuery.data?.find((r) => r.tag_name === currentVersion) ?? - (isDev ? releaseHistoryQuery.data?.[0] : null) ?? - null - ); + + function getMinorPrefix(tag: string): string | null { + const m = tag.replace(/^v/, '').match(/^(\d+\.\d+)\./); + return m ? m[1] : null; + } + + // Collect all releases sharing the same minor version (e.g. v1.3.0, v1.3.1, …) + const minorReleases = $derived.by(() => { + const releases = releaseHistoryQuery.data; + if (!releases || releases.length === 0) return []; + + const versionToMatch = isDev ? releases[0].tag_name : currentVersion; + if (!versionToMatch) return []; + + const prefix = getMinorPrefix(versionToMatch); + if (!prefix) { + const exact = releases.find((r) => r.tag_name === versionToMatch); + return exact ? [exact] : []; + } + + return releases.filter((r) => getMinorPrefix(r.tag_name) === prefix); + }); $effect(() => { updateAvailable = updateCheckQuery.data?.update_available ?? false; @@ -31,10 +48,4 @@ updateAvailable={updateCheckQuery.data?.update_available ?? false} latestVersion={updateCheckQuery.data?.latest_version ?? null} /> - + diff --git a/frontend/src/lib/components/WhatsNewModal.svelte b/frontend/src/lib/components/WhatsNewModal.svelte index f7fda5a..6a1d343 100644 --- a/frontend/src/lib/components/WhatsNewModal.svelte +++ b/frontend/src/lib/components/WhatsNewModal.svelte @@ -3,43 +3,53 @@ import { isWhatsNewDismissed, dismissWhatsNew } from '$lib/stores/version.svelte'; import { renderMarkdown } from '$lib/utils/markdown'; import { X, Sparkles, ExternalLink } from 'lucide-svelte'; + import type { GitHubRelease } from '$lib/queries/VersionQuery.svelte'; interface Props { currentVersion: string | null; buildDate: string | null; - releaseTag: string | null; - releaseBody: string | null; - releaseName: string | null; + releases: GitHubRelease[]; } - let { currentVersion, buildDate, releaseTag, releaseBody, releaseName }: Props = $props(); + let { currentVersion, buildDate, releases }: Props = $props(); let dialogEl: HTMLDialogElement | undefined = $state(); - let renderedBody = $state(''); + let renderedSections: { tag: string; name: string | null; html: string }[] = $state([]); const isDev = $derived(currentVersion === 'dev' || currentVersion === 'hosting-local'); + const latestRelease = $derived(releases.length > 0 ? releases[0] : null); // In dev: key dismissal to build_date so modal shows once per rebuild, not every refresh - // In prod: key to release tag so modal shows once per new version - const dismissKey = $derived(isDev ? (buildDate ?? 'dev') : (releaseTag ?? currentVersion)); + // In prod: key to latest release tag so modal re-shows when a new patch lands + const dismissKey = $derived( + isDev ? (buildDate ?? 'dev') : (latestRelease?.tag_name ?? currentVersion) + ); + + const hasContent = $derived(releases.some((r) => r.body && r.body.trim().length > 0)); const shouldShow = $derived( - currentVersion !== null && - dismissKey !== null && - releaseBody !== null && - releaseBody.trim().length > 0 && - !isWhatsNewDismissed(dismissKey) + currentVersion !== null && dismissKey !== null && hasContent && !isWhatsNewDismissed(dismissKey) ); $effect(() => { - if (releaseBody && releaseBody.trim()) { - renderMarkdown(releaseBody) - .then((html) => { - renderedBody = html; - }) - .catch(() => { - renderedBody = ''; - }); + const withContent = releases.filter((r) => r.body && r.body.trim()); + if (withContent.length === 0) { + renderedSections = []; + return; } + + Promise.all( + withContent.map(async (r) => ({ + tag: r.tag_name, + name: r.name, + html: await renderMarkdown(r.body!) + })) + ) + .then((sections) => { + renderedSections = sections; + }) + .catch(() => { + renderedSections = []; + }); }); $effect(() => { @@ -102,22 +112,28 @@
- {#if releaseName} -

- {releaseName} -

- {/if} - - {#if renderedBody} + {#if renderedSections.length > 0}
- - {@html renderedBody} + {#each renderedSections as section, i} + {#if i > 0} +
+ {/if} +

+ {section.name ?? section.tag} +

+
+ + {@html section.html} +
+ {/each}
- {:else} + {:else if hasContent}