fix(updater): hold Windows update notice during WinGet moderation window (#1200)

* fix(updater): hold Windows update notice during WinGet moderation window

* docs(changelog): Windows update notice WinGet moderation delay
This commit is contained in:
Psychotoxical
2026-06-27 14:50:37 +02:00
committed by GitHub
parent ed02ba4e1d
commit 7e6a2100e5
4 changed files with 70 additions and 2 deletions
+6
View File
@@ -351,6 +351,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* If a screen hit an unexpected rendering error, the whole window could go blank with no way back. The app now shows a small recoverable error card (Try again / Reload app) instead, and playback keeps going.
### Windows update notice waits out WinGet moderation
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1200](https://github.com/Psychotoxical/psysonic/pull/1200)**
* On Windows, the "update available" notice now waits until a release is a couple of days old, so it no longer points to a version that WinGet has not finished publishing yet. macOS and Linux are unaffected.
## [1.48.1] - 2026-06-15
## Fixed
+6 -2
View File
@@ -4,8 +4,8 @@ import { dirname } from '@tauri-apps/api/path';
import { invoke } from '@tauri-apps/api/core';
import { useTranslation } from 'react-i18next';
import { version as currentVersion } from '../../package.json';
import { IS_LINUX, IS_MACOS } from '../utils/platform';
import { SKIP_KEY, isNewer, pickAsset, type ReleaseData, type DlState } from '../utils/componentHelpers/appUpdaterHelpers';
import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform';
import { SKIP_KEY, isNewer, isWithinModerationWindow, pickAsset, type ReleaseData, type DlState } from '../utils/componentHelpers/appUpdaterHelpers';
/** All update-modal state, the GitHub release probe and the download/relaunch
* handlers. The component owns only the early-return guard and the JSX. */
@@ -36,6 +36,10 @@ export function useAppUpdater() {
if (!isNewer(version, currentVersion)) return;
const skipped = localStorage.getItem(SKIP_KEY);
if (skipped === version) return;
// Windows updates go through WinGet moderation; hold the notice until
// the release clears the moderation window so users aren't pointed at a
// version WinGet hasn't published yet. macOS/Linux use other channels.
if (IS_WINDOWS && isWithinModerationWindow(data.published_at, Date.now())) return;
}
setDismissed(false);
setDlState('idle');
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import { isWithinModerationWindow, WINGET_MODERATION_DELAY_MS } from './appUpdaterHelpers';
describe('isWithinModerationWindow', () => {
const published = '2026-06-27T00:00:00Z';
const publishedMs = Date.parse(published);
it('returns true while the release is younger than the window', () => {
const now = publishedMs + 12 * 60 * 60 * 1000; // 12h after release
expect(isWithinModerationWindow(published, now)).toBe(true);
});
it('returns false once the release is older than the window', () => {
const now = publishedMs + WINGET_MODERATION_DELAY_MS + 1;
expect(isWithinModerationWindow(published, now)).toBe(false);
});
it('returns false exactly at the window boundary', () => {
const now = publishedMs + WINGET_MODERATION_DELAY_MS;
expect(isWithinModerationWindow(published, now)).toBe(false);
});
it('respects a custom window override', () => {
const now = publishedMs + 2 * 60 * 60 * 1000; // 2h after release
expect(isWithinModerationWindow(published, now, 1 * 60 * 60 * 1000)).toBe(false);
expect(isWithinModerationWindow(published, now, 3 * 60 * 60 * 1000)).toBe(true);
});
it('fails open on a missing date', () => {
expect(isWithinModerationWindow(undefined, publishedMs)).toBe(false);
});
it('fails open on an unparseable date', () => {
expect(isWithinModerationWindow('not-a-date', publishedMs)).toBe(false);
});
});
@@ -13,6 +13,28 @@ export function isNewer(a: string, b: string): boolean {
return false;
}
// Windows updates ship through WinGet, which moderates each new release for a
// while after the GitHub release goes live (installer scan + manual review,
// longer for freshly signed binaries building SmartScreen reputation). Holding
// the update notice back until the release clears this window avoids pointing
// Windows users at a version WinGet has not published yet. Conservative start
// value — tune down once real winget-pkgs merge times are known.
export const WINGET_MODERATION_DELAY_MS = 48 * 60 * 60 * 1000;
// True while `publishedAt` is younger than `windowMs` relative to `now`.
// Missing or unparseable date → false (fail open: show the notice rather than
// hide it indefinitely). Platform-agnostic so the time logic stays testable.
export function isWithinModerationWindow(
publishedAt: string | undefined,
now: number,
windowMs: number = WINGET_MODERATION_DELAY_MS,
): boolean {
if (!publishedAt) return false;
const published = Date.parse(publishedAt);
if (Number.isNaN(published)) return false;
return now - published < windowMs;
}
export interface GithubAsset {
name: string;
browser_download_url: string;