feat: What's-new changelog dialog on first launch after an update (EN/FR)

Embeds changelog.json (per-version EN + FR notes, curated from the release's
commits). GetWhatsNew compares the stored last-seen version with the running
build and returns the notes for every newer version, once, then advances the
marker. A dialog shows them in the UI language on the first launch after update.
Seeded with the 0.20.5 notes.
This commit is contained in:
2026-07-20 22:58:05 +02:00
parent bfbd9fa61a
commit 9033e8518c
7 changed files with 170 additions and 4 deletions
+45 -2
View File
@@ -13,7 +13,7 @@ import {
GetQSO, UpdateQSO, DeleteQSO, DeleteQSOs, DeleteAllQSO,
UpdateQSOsFromCty, UpdateQSOsFromQRZ, UpdateQSOsFromClublog, UploadQSOsManual, SendQSORecordingEmail,
LookupCallsign, GetStationSettings, GetListsSettings,
GetStartupStatus, CheckForUpdate, DownloadAndApplyUpdate, GetLiveStations,
GetStartupStatus, CheckForUpdate, DownloadAndApplyUpdate, GetLiveStations, GetWhatsNew,
WorkedBefore,
SetCompactMode,
GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig, FlexApplyBandAntenna,
@@ -283,7 +283,7 @@ function computePrefix(call: string): string {
}
export default function App() {
const { t } = useI18n();
const { t, lang } = useI18n();
// === Lists from settings (fallback for first paint) ===
const [bands, setBands] = useState<string[]>(DEFAULT_BANDS);
const [modes, setModes] = useState<string[]>(DEFAULT_MODES);
@@ -1147,6 +1147,12 @@ export default function App() {
const [settingsSection, setSettingsSection] = useState<string | undefined>(undefined);
const [showDeleteAll, setShowDeleteAll] = useState(false);
const [showAbout, setShowAbout] = useState(false);
// "What's new": the changelog for the version(s) since the operator last ran,
// shown once on the first launch after an update (EN/FR per the UI language).
const [whatsNew, setWhatsNew] = useState<Array<{ version: string; date: string; en: string[]; fr: string[] }> | null>(null);
useEffect(() => {
GetWhatsNew().then((e: any) => { if (Array.isArray(e) && e.length) setWhatsNew(e); }).catch(() => {});
}, []);
const [showDuplicates, setShowDuplicates] = useState(false);
const [updateInfo, setUpdateInfo] = useState<{ latest: string; url: string; downloadUrl: string } | null>(null);
const [updating, setUpdating] = useState(false);
@@ -4132,6 +4138,43 @@ export default function App() {
</div>
)}
{/* What's new — shown once on the first launch after an update. */}
{whatsNew && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/40 backdrop-blur-sm p-4" onClick={() => setWhatsNew(null)}>
<div className="w-full max-w-lg max-h-[85vh] flex flex-col rounded-xl border border-border bg-card shadow-2xl animate-in fade-in zoom-in-95" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center gap-2 px-5 pt-5 pb-3 border-b border-border shrink-0">
<Zap className="size-5 text-primary" />
<h2 className="text-lg font-bold tracking-tight">{t('whatsnew.title')}</h2>
<span className="flex-1" />
<button onClick={() => setWhatsNew(null)} className="text-muted-foreground hover:text-foreground"><X className="size-4" /></button>
</div>
<div className="overflow-y-auto px-5 py-4 space-y-5">
{whatsNew.map((e) => (
<div key={e.version}>
<div className="flex items-baseline gap-2 mb-1.5">
<span className="font-mono font-bold text-primary">v{e.version}</span>
{e.date && <span className="text-[11px] text-muted-foreground">{e.date}</span>}
</div>
<ul className="space-y-1.5 text-sm">
{(lang === 'fr' ? e.fr : e.en).map((line, i) => (
<li key={i} className="flex gap-2">
<span className="text-primary mt-1.5 size-1.5 rounded-full bg-primary shrink-0" />
<span>{line}</span>
</li>
))}
</ul>
</div>
))}
</div>
<div className="px-5 py-3 border-t border-border shrink-0 text-right">
<button onClick={() => setWhatsNew(null)} className="h-8 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90">
{t('whatsnew.close')}
</button>
</div>
</div>
</div>
)}
{/* Transient toasts (bottom-right). Errors stack on top of the green
success toast; both auto-dismiss. */}
{/* Unlock encrypted passwords (set via Settings Security). Dismissable:
+2 -2
View File
@@ -54,7 +54,7 @@ const en: Dict = {
'btn.logQso': 'Log QSO', 'btn.clear': 'Clear', 'btn.spot': 'Spot', 'btn.saving': '…',
// Language chooser
'lang.choose': 'Choose your language', 'lang.chooseHint': 'You can change this later in Settings → General.',
'lang.english': 'English', 'lang.french': 'Français',
'lang.english': 'English', 'lang.french': 'Français', 'whatsnew.title': "What's new", 'whatsnew.close': 'Got it',
'settings.language': 'Language', 'settings.languageHint': 'Interface language.',
'stats.tab': 'Statistics', 'stats.title': 'Logbook statistics', 'stats.loading': 'Crunching the log…',
'stats.noData': 'No data', 'stats.charts': 'Charts', 'stats.table': 'Table', 'stats.refresh': 'Refresh',
@@ -368,7 +368,7 @@ const fr: Dict = {
'field.startUtc': 'Début UTC', 'field.endUtc': 'Fin UTC', 'field.snt': 'Env', 'field.rcv': 'Reç',
'btn.logQso': 'Enregistrer', 'btn.clear': 'Effacer', 'btn.spot': 'Spot', 'btn.saving': '…',
'lang.choose': 'Choisissez votre langue', 'lang.chooseHint': 'Modifiable plus tard dans Réglages → Général.',
'lang.english': 'English', 'lang.french': 'Français',
'lang.english': 'English', 'lang.french': 'Français', 'whatsnew.title': 'Nouveautés', 'whatsnew.close': 'Compris',
'settings.language': 'Langue', 'settings.languageHint': "Langue de l'interface.",
'stats.tab': 'Statistiques', 'stats.title': 'Statistiques du journal', 'stats.loading': 'Analyse du journal…',
'stats.noData': 'Aucune donnée', 'stats.charts': 'Graphiques', 'stats.table': 'Tableau', 'stats.refresh': 'Rafraîchir',
+2
View File
@@ -427,6 +427,8 @@ export function GetUltrabeamSettings():Promise<main.UltrabeamSettings>;
export function GetUltrabeamStatus():Promise<main.UltrabeamStatusInfo>;
export function GetWhatsNew():Promise<Array<main.ChangelogEntry>>;
export function GetWinkeyerSettings():Promise<main.WinkeyerSettings>;
export function GetWinkeyerStatus():Promise<winkeyer.Status>;
+4
View File
@@ -810,6 +810,10 @@ export function GetUltrabeamStatus() {
return window['go']['main']['App']['GetUltrabeamStatus']();
}
export function GetWhatsNew() {
return window['go']['main']['App']['GetWhatsNew']();
}
export function GetWinkeyerSettings() {
return window['go']['main']['App']['GetWinkeyerSettings']();
}
+18
View File
@@ -1728,6 +1728,24 @@ export namespace main {
this.path = source["path"];
}
}
export class ChangelogEntry {
version: string;
date: string;
en: string[];
fr: string[];
static createFrom(source: any = {}) {
return new ChangelogEntry(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.version = source["version"];
this.date = source["date"];
this.en = source["en"];
this.fr = source["fr"];
}
}
export class ChatMessage {
id: number;
operator: string;