Merge pull request #767 from rowboatlabs/feat/update-card-scrollable-notes

feat(x): scrollable release-notes box on the update card, notes on Windows too
This commit is contained in:
Harshvardhan Vatsa 2026-07-21 16:34:38 +05:30 committed by GitHub
commit ed97392557
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 42 additions and 4 deletions

View file

@ -1,4 +1,4 @@
import { app, autoUpdater, nativeImage, BrowserWindow } from "electron";
import { app, autoUpdater, net, nativeImage, BrowserWindow } from "electron";
import { capture } from "@x/core/dist/analytics/posthog.js";
import type { ipc } from "@x/shared";
@ -90,6 +90,12 @@ export function initUpdater(): void {
releaseNotes: releaseNotes || undefined,
});
showReadyBadge();
// Squirrel.Windows never carries notes, and Squirrel.Mac's copy is a
// snapshot from download time — stale when the release body is edited
// after publish (update.electronjs.org caching widens that window).
// Refresh from the GitHub API; on failure the snapshot (or the card's
// static fallback line) stands.
void backfillReleaseNotes(releaseName || undefined);
});
autoUpdater.on("error", (err) => {
setStatus({ state: "error", error: err.message, lastCheckedAt: status.lastCheckedAt });
@ -110,6 +116,30 @@ export function initUpdater(): void {
setInterval(checkForUpdates, CHECK_INTERVAL_MS);
}
/**
* Replace the staged update's release notes with the current GitHub release
* body. releaseName is "0.7.7" from Squirrel.Windows and "v0.7.7" from
* Squirrel.Mac normalize to the tag form.
*/
async function backfillReleaseNotes(releaseName: string | undefined): Promise<void> {
if (!releaseName) return;
try {
const tag = `v${releaseName.replace(/^v/, "")}`;
const res = await net.fetch(`https://api.github.com/repos/${REPO}/releases/tags/${tag}`, {
headers: { Accept: "application/vnd.github+json", "User-Agent": "Rowboat" },
});
if (!res.ok) return;
const { body } = (await res.json()) as { body?: string };
const notes = body?.trim();
// Re-check the state: the fetch raced user actions (quitAndInstall).
if (notes && status.state === "ready" && status.newVersion === releaseName) {
setStatus({ ...status, releaseNotes: notes });
}
} catch {
// Offline or rate-limited — the Squirrel snapshot / fallback line stands.
}
}
/**
* Manual "Check for updates". Only meaningful when idle or errored;
* checking/downloading are already in flight and ready is already staged.

View file

@ -55,6 +55,11 @@ export function UpdateCard() {
if (!visible || status?.state !== "ready") return null
const releaseUrl = version ? `${RELEASES_URL}/tag/v${version}` : `${RELEASES_URL}/latest`
// Release bodies usually open with their own "What's new" heading, which
// would duplicate the card's label above the box — drop it.
const releaseNotes = status.releaseNotes
?.replace(/^\s*#{1,6}[ \t]*what[']?s new[ \t]*\r?\n+/i, "")
.trim()
return (
<div
@ -81,10 +86,13 @@ export function UpdateCard() {
</p>
<div className="mt-3">
<h5 className="text-xs font-semibold">What&apos;s new</h5>
{status.releaseNotes ? (
<div className="mt-1.5 max-h-56 overflow-y-auto">
{releaseNotes ? (
// A bordered, fixed-height scroll box (not a bare max-h clip): the
// frame and inset scrollbar signal there is more content below the
// fold on every platform.
<div className="mt-1.5 max-h-56 overflow-y-auto overscroll-contain rounded-md border border-border/60 bg-muted/30 p-2.5">
<Streamdown className="prose prose-sm dark:prose-invert max-w-none text-xs [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
{status.releaseNotes}
{releaseNotes}
</Streamdown>
</div>
) : (