From bc20035652e986414624e844d08cd44122eaf23a Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 14 Apr 2026 10:25:55 -0700 Subject: [PATCH] fix: improve rich card display logic for current and unlisted versions (#634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show rich changelog entries at the user's current version (they may not have seen the card yet). When the local version isn't in the changelog JSON, use semver comparison instead of assuming the user is very old — this avoids showing stale cards to users on newer patch releases. --- src/main/updater-changelog.test.ts | 95 ++++++++++++++++++++++++------ src/main/updater-changelog.ts | 19 ++++-- 2 files changed, 92 insertions(+), 22 deletions(-) diff --git a/src/main/updater-changelog.test.ts b/src/main/updater-changelog.test.ts index 8b85db88034..f6a348858d0 100644 --- a/src/main/updater-changelog.test.ts +++ b/src/main/updater-changelog.test.ts @@ -37,13 +37,16 @@ describe('fetchChangelog', () => { it('returns exact match when the incoming version has rich content', async () => { const entries = makeEntries([ - { version: '1.1.21', description: 'New feature', mediaUrl: 'https://onorca.dev/media/1.1.21.gif' }, + { + version: '1.1.21', + description: 'New feature', + mediaUrl: 'https://onorca.dev/media/1.1.21.gif' + }, { version: '1.1.20' }, { version: '1.1.19' } ]) fetchMock.mockResolvedValue(jsonResponse(entries)) - const result = await fetchChangelog('1.1.21', '1.1.19') expect(result).not.toBeNull() @@ -56,13 +59,17 @@ describe('fetchChangelog', () => { // Incoming 1.1.21 is not in JSON; 1.1.17 has rich content. // User is on 1.1.15 which is behind 1.1.17. const entries = makeEntries([ - { version: '1.1.17', description: 'Cool feature', mediaUrl: 'https://onorca.dev/media/1.1.17.gif', releaseNotesUrl: 'https://onorca.dev/changelog/1.1.17' }, + { + version: '1.1.17', + description: 'Cool feature', + mediaUrl: 'https://onorca.dev/media/1.1.17.gif', + releaseNotesUrl: 'https://onorca.dev/changelog/1.1.17' + }, { version: '1.1.16' }, { version: '1.1.15' } ]) fetchMock.mockResolvedValue(jsonResponse(entries)) - const result = await fetchChangelog('1.1.21', '1.1.15') expect(result).not.toBeNull() @@ -78,12 +85,15 @@ describe('fetchChangelog', () => { // 1.1.17 has rich content. const entries = makeEntries([ { version: '1.1.21', description: '', mediaUrl: undefined }, - { version: '1.1.17', description: 'Great update', mediaUrl: 'https://onorca.dev/media/1.1.17.gif' }, + { + version: '1.1.17', + description: 'Great update', + mediaUrl: 'https://onorca.dev/media/1.1.17.gif' + }, { version: '1.1.15' } ]) fetchMock.mockResolvedValue(jsonResponse(entries)) - const result = await fetchChangelog('1.1.21', '1.1.15') expect(result).not.toBeNull() @@ -101,7 +111,6 @@ describe('fetchChangelog', () => { ]) fetchMock.mockResolvedValue(jsonResponse(entries)) - const result = await fetchChangelog('1.1.21', '1.1.19') expect(result).toBeNull() @@ -116,7 +125,6 @@ describe('fetchChangelog', () => { ]) fetchMock.mockResolvedValue(jsonResponse(entries)) - const result = await fetchChangelog('1.1.21', '1.1.19') expect(result).toBeNull() @@ -127,24 +135,52 @@ describe('fetchChangelog', () => { const entries = makeEntries([ { version: '1.1.20', description: '' }, { version: '1.1.18' }, - { version: '1.1.17', description: 'Old feature', mediaUrl: 'https://onorca.dev/media/old.gif' } + { + version: '1.1.17', + description: 'Old feature', + mediaUrl: 'https://onorca.dev/media/old.gif' + } ]) fetchMock.mockResolvedValue(jsonResponse(entries)) - const result = await fetchChangelog('1.1.20', '1.1.18') // 1.1.18 is at index 1, 1.1.17 is at index 2 — user is already past it. expect(result).toBeNull() }) - it('shows rich entry when local version is not in JSON (very old user)', async () => { + it('shows rich entry at the same version as localVersion', async () => { + // User is on 1.1.18 which has rich content; incoming 1.1.20 has none. + // The user may not have seen the rich card for 1.1.18 (e.g., they updated + // silently), so showing it is better than showing nothing. const entries = makeEntries([ { version: '1.1.20', description: '' }, - { version: '1.1.17', description: 'Feature demo', mediaUrl: 'https://onorca.dev/media/demo.gif' } + { + version: '1.1.18', + description: 'Current feature', + mediaUrl: 'https://onorca.dev/media/current.gif' + }, + { version: '1.1.17' } ]) fetchMock.mockResolvedValue(jsonResponse(entries)) + const result = await fetchChangelog('1.1.20', '1.1.18') + + expect(result).not.toBeNull() + expect(result!.release.title).toBe('Release 1.1.18') + expect(result!.release.releaseNotesUrl).toBe('https://onorca.dev/changelog') + }) + + it('shows rich entry when local version is not in JSON (very old user)', async () => { + const entries = makeEntries([ + { version: '1.1.20', description: '' }, + { + version: '1.1.17', + description: 'Feature demo', + mediaUrl: 'https://onorca.dev/media/demo.gif' + } + ]) + fetchMock.mockResolvedValue(jsonResponse(entries)) const result = await fetchChangelog('1.1.21', '1.0.0') @@ -155,10 +191,28 @@ describe('fetchChangelog', () => { expect(result!.releasesBehind).toBeNull() }) + it('skips rich entries when local version is newer than all changelog entries', async () => { + // Local version 1.1.25 is not in the JSON and is newer than the latest + // changelog entry (1.1.20). The rich entry at 1.1.17 is stale — the user + // has already passed it. + const entries = makeEntries([ + { version: '1.1.20', description: '' }, + { + version: '1.1.17', + description: 'Old feature', + mediaUrl: 'https://onorca.dev/media/old.gif' + } + ]) + fetchMock.mockResolvedValue(jsonResponse(entries)) + + const result = await fetchChangelog('1.1.26', '1.1.25') + + expect(result).toBeNull() + }) + it('returns null on non-ok HTTP response', async () => { fetchMock.mockResolvedValue({ ok: false }) - const result = await fetchChangelog('1.1.21', '1.1.19') expect(result).toBeNull() @@ -167,7 +221,6 @@ describe('fetchChangelog', () => { it('returns null on non-array JSON', async () => { fetchMock.mockResolvedValue(jsonResponse({ bad: true })) - const result = await fetchChangelog('1.1.21', '1.1.19') expect(result).toBeNull() @@ -175,13 +228,20 @@ describe('fetchChangelog', () => { it('prefers exact match over fallback when both have rich content', async () => { const entries = makeEntries([ - { version: '1.1.21', description: 'Latest feature', mediaUrl: 'https://onorca.dev/media/latest.gif' }, - { version: '1.1.17', description: 'Older feature', mediaUrl: 'https://onorca.dev/media/old.gif' }, + { + version: '1.1.21', + description: 'Latest feature', + mediaUrl: 'https://onorca.dev/media/latest.gif' + }, + { + version: '1.1.17', + description: 'Older feature', + mediaUrl: 'https://onorca.dev/media/old.gif' + }, { version: '1.1.15' } ]) fetchMock.mockResolvedValue(jsonResponse(entries)) - const result = await fetchChangelog('1.1.21', '1.1.15') expect(result!.release.title).toBe('Release 1.1.21') @@ -196,7 +256,6 @@ describe('fetchChangelog', () => { ]) fetchMock.mockResolvedValue(jsonResponse(entries)) - const result = await fetchChangelog('1.1.21', '1.1.15') expect(result).not.toBeNull() diff --git a/src/main/updater-changelog.ts b/src/main/updater-changelog.ts index 4215aeb1e4b..a33e5b28b4d 100644 --- a/src/main/updater-changelog.ts +++ b/src/main/updater-changelog.ts @@ -1,5 +1,6 @@ import { net } from 'electron' import type { ChangelogData } from '../shared/types' +import { compareVersions } from './updater-fallback' type ChangelogEntry = { version: string @@ -93,10 +94,20 @@ export async function fetchChangelog( continue } - // Only show this entry if the user's local version is behind it. - // When localIndex === -1 the local version isn't in the JSON at all, - // which typically means it's very old — safe to show the content. - if (localIndex !== -1 && localIndex <= i) { + // Only show this entry if the user's local version is at or behind it. + // Why: the user hasn't necessarily seen the rich card for their own + // version (e.g., they updated silently or dismissed the card), so + // entries at the same version as localVersion are still worth showing. + // When localIndex === -1 the local version isn't in the JSON at all — + // this could mean it's very old OR very new (e.g., a patch release not + // yet in the changelog). Fall back to semver comparison to avoid + // showing stale content to users who are already ahead. + if (localIndex !== -1) { + if (localIndex < i) { + continue + } + } else if (compareVersions(localVersion, candidate.version) > 0) { + // localVersion is newer than this candidate — user already passed it. continue }