Update whats new modal logic

This commit is contained in:
Harvey 2026-04-18 02:34:24 +01:00
parent 3cef59f257
commit 2032f8385c
2 changed files with 72 additions and 45 deletions

View file

@ -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}
/>
<WhatsNewModal
{currentVersion}
{buildDate}
releaseTag={currentRelease?.tag_name ?? null}
releaseBody={currentRelease?.body ?? null}
releaseName={currentRelease?.name ?? null}
/>
<WhatsNewModal {currentVersion} {buildDate} releases={minorReleases} />

View file

@ -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 @@
<div class="divider my-0 opacity-10"></div>
{#if releaseName}
<p class="text-base-content mt-4 mb-4 text-sm font-semibold border-l-2 border-accent/50 pl-3">
{releaseName}
</p>
{/if}
{#if renderedBody}
{#if renderedSections.length > 0}
<div
class="whats-new-content release-notes-prose prose prose-sm max-h-[55vh] max-w-none text-base-content/75 overflow-y-auto rounded-lg border border-base-content/5 bg-base-100/50 p-4 {releaseName
? ''
: 'mt-4'}"
class="whats-new-content release-notes-prose max-h-[55vh] max-w-none overflow-y-auto rounded-lg border border-base-content/5 bg-base-100/50 p-4 mt-4"
>
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized via DOMPurify -->
{@html renderedBody}
{#each renderedSections as section, i}
{#if i > 0}
<div class="divider my-4 opacity-20"></div>
{/if}
<p
class="text-base-content text-sm font-semibold border-l-2 border-accent/50 pl-3 {i > 0
? ''
: 'mt-0'} mb-3"
>
{section.name ?? section.tag}
</p>
<div class="prose prose-sm max-w-none text-base-content/75">
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized via DOMPurify -->
{@html section.html}
</div>
{/each}
</div>
{:else}
{:else if hasContent}
<div class="flex justify-center py-12">
<span class="loading loading-spinner loading-md text-accent/60"></span>
</div>