feat(notices): bring the release notes back on every update

The release modal takes the 4.3.0 design: the release and its headline cards
on the left, the note and the support links on the right, at the design's own
size. The cards carry small drawings of the features instead of an icon.

The 4.0.0 entry is retired. In its place sits one entry, release-notes, that
every big release rewrites and keeps. It recurs per version with no upper
bound, so every user sees it once on each update, whatever they did with it
before, and a reload of the same version keeps it closed. An integration test
holds that promise through the real entry and the real dismiss route.

All 23 locales carry the new copy and drop the 4.0.0 strings.
This commit is contained in:
Maurice
2026-09-15 23:50:15 +02:00
parent 99783cc8da
commit 05ee3d50e4
34 changed files with 1562 additions and 1206 deletions
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import semver from 'semver';
import { SYSTEM_NOTICES } from '../../../src/systemNotices/registry.js';
import { RETIRED_NOTICE_IDS, SYSTEM_NOTICES } from '../../../src/systemNotices/registry.js';
import { isNoticeVersionActive } from '../../../src/systemNotices/service.js';
/** Collect all actionIds registered via registerNoticeAction() in client source files. */
@@ -65,29 +65,28 @@ describe('registry integrity', () => {
}
});
it('the 4.0.0 release notice covers the whole 4.x line', () => {
const release = SYSTEM_NOTICES.find(n => n.id === 'release-4-0-0');
it('the release notes come back on every upgrade, with no upper bound', () => {
const release = SYSTEM_NOTICES.find(n => n.id === 'release-notes');
expect(release).toBeDefined();
// It must not greet somebody still on 3.x...
// Nothing on 3.x ships this copy, and the thank-you notice still covers it there.
expect(isNoticeVersionActive(release!, '3.4.1')).toBe(false);
expect(isNoticeVersionActive(release!, '4.0.0')).toBe(true);
expect(isNoticeVersionActive(release!, '4.0.7')).toBe(true);
expect(isNoticeVersionActive(release!, '4.0.12')).toBe(true);
// ...and it stays up across the minors, so no 4.x install is left without a
// notice the way 4.1.0 was under the old per-release window.
expect(isNoticeVersionActive(release!, '4.1.0')).toBe(true);
expect(isNoticeVersionActive(release!, '4.2.0')).toBe(true);
expect(isNoticeVersionActive(release!, '4.9.9')).toBe(true);
// The upper bound is exclusive: 5.0.0 gets its own notice.
expect(isNoticeVersionActive(release!, '5.0.0')).toBe(false);
// From there on every version carries it, a patch release included, so no
// install is left with nothing to show because nobody wrote a new window.
for (const version of ['4.0.0', '4.2.1', '4.3.0', '4.3.1', '4.9.9', '5.0.0', '12.4.0']) {
expect(isNoticeVersionActive(release!, version), version).toBe(true);
}
expect(release!.maxVersion).toBeUndefined();
// The window alone is not enough: a one-time dismissal would retire it for good
// the first time somebody closed it.
expect(release!.recurring).toBe('per-version');
});
it('the 4.0.0 release notice comes back on each upgrade inside that line', () => {
const release = SYSTEM_NOTICES.find(n => n.id === 'release-4-0-0')!;
// The version window above is only half of "covers the whole 4.x line". Without
// this, the default one-time dismissal retired the notice for everyone who
// closed it on 4.0.0, so none of them saw it again on 4.1, 4.2 or later.
expect(release.recurring).toBe('per-version');
it('the 4.0.0 release notice is retired and its id stays reserved', () => {
expect(SYSTEM_NOTICES.some(n => n.id === 'release-4-0-0')).toBe(false);
expect(RETIRED_NOTICE_IDS).toContain('release-4-0-0');
for (const id of RETIRED_NOTICE_IDS) {
expect(SYSTEM_NOTICES.some(n => n.id === id), id).toBe(false);
}
});
it('the thank-you notice hands over to the release modal at 4.0.0', () => {
@@ -98,10 +97,10 @@ describe('registry integrity', () => {
expect(isNoticeVersionActive(thankYou!, '3.4.1')).toBe(true);
expect(isNoticeVersionActive(thankYou!, '4.0.0')).toBe(false);
const release = SYSTEM_NOTICES.find(n => n.id === 'release-4-0-0')!;
for (const version of ['3.4.1', '4.0.0', '4.0.7', '4.1.0', '5.0.0']) {
const release = SYSTEM_NOTICES.find(n => n.id === 'release-notes')!;
for (const version of ['3.4.1', '4.0.0', '4.0.7', '4.1.0', '4.3.0', '5.0.0']) {
const active = [thankYou!, release].filter(n => isNoticeVersionActive(n, version));
expect(active.length, `both thank-you notices active at ${version}`).toBeLessThanOrEqual(1);
expect(active.length, `thank-you and release notes at ${version}`).toBe(1);
}
});
});