Skip to main content

max / goingson

4.2 KB · 122 lines History Blame Raw
1 /**
2 * GoingsOn - "What's New" after-update dialog
3 *
4 * After an OTA update applies and the app relaunches on the new version, this
5 * surfaces the matching CHANGELOG.md section once, so beta testers see what
6 * changed at the moment they hit the update. Shown at most once per version;
7 * the first-ever launch is owned by the welcome flow, not this dialog.
8 */
9 (function() {
10 'use strict';
11
12 const LAST_VERSION_KEY = 'go-last-version';
13
14 /**
15 * Pull the body of a single version's section out of a Keep a Changelog
16 * document. Matches the `## [<version>]` header and collects every line up
17 * to the next `## [` header. Returns '' when the version isn't present.
18 */
19 function extractSection(markdown, version) {
20 const lines = String(markdown).split('\n');
21 const body = [];
22 let inSection = false;
23 for (const line of lines) {
24 const header = line.match(/^##\s+\[([^\]]+)\]/);
25 if (header) {
26 if (inSection) break; // next version reached
27 if (header[1] === version) { inSection = true; }
28 continue; // skip the header line itself
29 }
30 if (inSection) body.push(line);
31 }
32 return body.join('\n').trim();
33 }
34
35 /**
36 * Render a changelog section (Keep a Changelog format) to safe HTML.
37 * Everything is HTML-escaped; only the structural markup is ours.
38 */
39 function renderSection(section) {
40 const esc = GoingsOn.utils.escapeHtml;
41 const out = [];
42 let listOpen = false;
43 const closeList = () => { if (listOpen) { out.push('</ul>'); listOpen = false; } };
44 for (const raw of section.split('\n')) {
45 const line = raw.trim();
46 if (!line) continue;
47 const heading = line.match(/^###\s+(.*)$/);
48 const bullet = line.match(/^[-*]\s+(.*)$/);
49 if (heading) {
50 closeList();
51 out.push(`<h3 class="whats-new-group">${esc(heading[1])}</h3>`);
52 } else if (bullet) {
53 if (!listOpen) { out.push('<ul class="whats-new-list">'); listOpen = true; }
54 out.push(`<li>${esc(bullet[1])}</li>`);
55 } else {
56 closeList();
57 out.push(`<p class="whats-new-text">${esc(line)}</p>`);
58 }
59 }
60 closeList();
61 return out.join('');
62 }
63
64 function show(version, section) {
65 const content = `
66 <div class="whats-new-panel">
67 ${renderSection(section)}
68 </div>
69 <div class="form-actions">
70 <button class="btn btn-primary" onclick="GoingsOn.ui.closeModal()">Got It</button>
71 </div>
72 `;
73 GoingsOn.ui.openModal(`What's New in v${version}`, content);
74 }
75
76 /**
77 * Decide whether to show the dialog and, if so, show it. Safe to call on
78 * every startup: it no-ops when nothing changed, and records the current
79 * version so the dialog shows only once per update.
80 */
81 async function maybeShow() {
82 if (!window.__TAURI__) return;
83
84 let current;
85 try {
86 current = await window.__TAURI__.app.getVersion();
87 } catch (_) {
88 return;
89 }
90
91 const last = localStorage.getItem(LAST_VERSION_KEY);
92
93 // First launch on this profile: let the welcome flow own first
94 // impressions; just remember where we are.
95 if (!last) {
96 localStorage.setItem(LAST_VERSION_KEY, current);
97 return;
98 }
99
100 if (last === current) return;
101
102 // An update landed since the last run. Record the new version up front
103 // so a missing/empty changelog section never re-triggers next launch.
104 localStorage.setItem(LAST_VERSION_KEY, current);
105
106 let markdown;
107 try {
108 markdown = await GoingsOn.api.app.getChangelog();
109 } catch (_) {
110 return;
111 }
112
113 const section = extractSection(markdown, current);
114 if (!section) return; // no notes for this version — don't show an empty dialog
115
116 show(current, section);
117 }
118
119 GoingsOn.whatsNew = { maybeShow, extractSection, renderSection };
120
121 })();
122