| 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; |
| 27 |
if (header[1] === version) { inSection = true; } |
| 28 |
continue; |
| 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 |
|
| 94 |
|
| 95 |
if (!last) { |
| 96 |
localStorage.setItem(LAST_VERSION_KEY, current); |
| 97 |
return; |
| 98 |
} |
| 99 |
|
| 100 |
if (last === current) return; |
| 101 |
|
| 102 |
|
| 103 |
|
| 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; |
| 115 |
|
| 116 |
show(current, section); |
| 117 |
} |
| 118 |
|
| 119 |
GoingsOn.whatsNew = { maybeShow, extractSection, renderSection }; |
| 120 |
|
| 121 |
})(); |
| 122 |
|