source: Klonkt/src/views/pages/admin-push.ejs@ 97bcf7e

main
Last change on this file since 97bcf7e was 9a00f28, checked in by Robin <roboburr@…>, 7 weeks ago

Fix: test feedback round, cirkels tagline + report details + full i18n

Three findings from Bart's test pass, tied together by the new i18n keys:

  1. The admin tagline said "Solo mode" even with Cirkels (federation) on. Solo-tenancy now distinguishes: apEnabled -> "Cirkels-modus: jouw site, verbonden met de fediverse", else the solo line. New key in nl/en/de.
  1. Reports only showed who reported; now they show the reason and WHICH post. getNotifications resolves the stored objects URIs of a report: our own /ap/notes/<id> become "Over de post: <title>" links under the description; an empty reason renders "Geen reden opgegeven". Other URIs (the actor itself) are skipped, the row already names the account.
  1. Notificaties and Betaalde posts were hardcoded Dutch on an English-configured Klonkt. Everything now goes through t(): the two admin pages (including their client-side status strings, passed via the JSON data block), the admin menu buttons, and the visitor-facing paid pages (gate, passkey, result). Server-side push payloads translate too, using the SITE's content language (fallback KLONKT_DEFAULT_LANG), and the test ping uses the request language. Full nl/en/de dictionaries.

Changed files:
src/services/i18n.js

  • admin.tagline_cirkels, admin.b_paid/b_push/back, notif.report_about/ report_noreason, and the push.*, apaid.*, pgate.*, ppk.*, pres.* blocks in nl/en/de

src/routes/admin.js, src/views/pages/admin.ejs

  • circlesOn (apEnabled) -> cirkels tagline; menu buttons via t()

src/services/ActivityPubService.js

  • report objects resolved to post links; push payloads via i18nT with pushLang(slug) (site language)

src/views/pages/fedi-notifications.ejs

  • report reason fallback + "Over de post" links (+ styles)

src/routes/admin-push.js, src/views/pages/admin-push.ejs

  • pageTitleKey push.t; all copy + JS status strings via t()

src/routes/admin-paid.js, src/views/pages/admin-paid.ejs

  • pageTitleKey apaid.t; all copy via t(), copy-button label via data-attr

src/routes/paid.js

  • pageTitleKey pres.t / ppk.t

src/views/pages/paid-gate.ejs, paid-passkey.ejs, paid-result.ejs

  • visitor copy + JS strings via t()

src/routes/push.js

  • test notification via resolveLang(req)

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 8.3 KB
Line 
1<div class="container" style="max-width:640px;margin:1.5rem auto 3rem;padding:0 1rem">
2 <h1 style="margin:0 0 .3rem"><%= t('push.t') %></h1>
3 <p style="color:var(--ink-soft,#888);margin:0 0 1.2rem">
4 <%= t('push.intro') %>
5 </p>
6
7 <% if (!vapidKey) { %>
8 <p style="color:#c0392b"><%= t('push.unavailable') %></p>
9 <% } else { %>
10
11 <div id="np-unsupported" hidden style="color:#c0392b;margin:0 0 1rem">
12 <%= t('push.unsupported') %>
13 </div>
14 <div id="np-ios-hint" hidden style="border:1px solid var(--border,#333);border-radius:10px;padding:.8rem 1rem;margin:0 0 1rem;font-size:.92rem;color:var(--ink-soft,#888)">
15 <%= t('push.ios_hint') %>
16 </div>
17
18 <section style="border:1px solid var(--border,#333);border-radius:12px;padding:1rem 1.1rem;margin:0 0 1.2rem">
19 <p style="margin:0 0 .6rem"><strong><%= t('push.this_device') %></strong> <span id="np-state"><%= t('push.checking') %></span></p>
20 <div style="display:flex;gap:.6rem;flex-wrap:wrap">
21 <button type="button" id="np-on" class="btn btn-primary" hidden><%= t('push.enable') %></button>
22 <button type="button" id="np-off" class="btn" hidden><%= t('push.disable') %></button>
23 <button type="button" id="np-test" class="btn" hidden><%= t('push.test') %></button>
24 </div>
25 <fieldset id="np-alerts" hidden style="border:none;padding:0;margin:1rem 0 0">
26 <legend style="font-weight:600;margin-bottom:.4rem"><%= t('push.what') %></legend>
27 <label style="display:block"><input type="checkbox" data-alert="follow"> <%= t('push.a_follow') %></label>
28 <label style="display:block"><input type="checkbox" data-alert="reply"> <%= t('push.a_reply') %></label>
29 <label style="display:block"><input type="checkbox" data-alert="like"> <%= t('push.a_like') %></label>
30 <label style="display:block"><input type="checkbox" data-alert="boost"> <%= t('push.a_boost') %></label>
31 <label style="display:block"><input type="checkbox" data-alert="dm"> <%= t('push.a_dm') %></label>
32 <p id="np-saved" hidden style="color:var(--accent);margin:.4rem 0 0"><%= t('push.saved') %></p>
33 </fieldset>
34 </section>
35
36 <% if (subscriptions.length) { %>
37 <h2 style="font-size:1.05rem;margin:0 0 .5rem"><%= t('push.devices') %></h2>
38 <ul style="list-style:none;padding:0;margin:0 0 1.2rem">
39 <% subscriptions.forEach(function (s) { %>
40 <li data-endpoint="<%= s.endpoint %>" style="display:flex;justify-content:space-between;gap:.8rem;align-items:center;padding:.45rem 0;border-bottom:1px solid var(--border,#2a2a2a)">
41 <span style="min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><%= s.ua_label || t('push.device') %> <small style="color:var(--ink-soft,#888)"><%= t('push.since') %> <%= String(s.created_at || '').slice(0, 10) %></small></span>
42 <button type="button" class="btn np-remove" data-endpoint="<%= s.endpoint %>"><%= t('push.remove') %></button>
43 </li>
44 <% }); %>
45 </ul>
46 <% } %>
47
48 <% } %>
49
50 <p style="margin-top:1.5rem"><a href="/admin">&larr; <%= t('admin.back') %></a></p>
51</div>
52
53<% if (vapidKey) { %>
54<script id="np-data" type="application/json"><%- JSON.stringify({
55 vapid: vapidKey,
56 alerts: defaultAlerts,
57 saved: subscriptions.reduce(function (m, s) { try { m[s.endpoint] = JSON.parse(s.alert_types || '{}'); } catch (e) { m[s.endpoint] = {}; } return m; }, {}),
58 i18n: { on: t('push.state_on'), off: t('push.state_off'), denied: t('push.state_denied'), unknown: t('push.state_unknown'), unsupported: t('push.state_unsupported'), failed: t('push.enable_failed') },
59}).replace(/</g, '\\u003c') %></script>
60<script>
61(function () {
62 var cfg = JSON.parse(document.getElementById('np-data').textContent);
63 var elState = document.getElementById('np-state');
64 var btnOn = document.getElementById('np-on'), btnOff = document.getElementById('np-off'), btnTest = document.getElementById('np-test');
65 var alertsBox = document.getElementById('np-alerts'), savedMsg = document.getElementById('np-saved');
66 var currentEndpoint = null;
67
68 var isIos = /iPad|iPhone|iPod/.test(navigator.userAgent);
69 var standalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;
70 if (isIos && !standalone) document.getElementById('np-ios-hint').hidden = false;
71
72 if (!('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) {
73 document.getElementById('np-unsupported').hidden = false;
74 elState.textContent = cfg.i18n.unsupported;
75 return;
76 }
77
78 function b64ToU8(s) {
79 var pad = '='.repeat((4 - (s.length % 4)) % 4);
80 var raw = atob((s + pad).replace(/-/g, '+').replace(/_/g, '/'));
81 var out = new Uint8Array(raw.length);
82 for (var i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
83 return out;
84 }
85 function post(url, body) {
86 return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
87 }
88 function deviceLabel() {
89 var ua = navigator.userAgent;
90 var browser = /Firefox\//.test(ua) ? 'Firefox' : /Edg\//.test(ua) ? 'Edge' : /Chrome\//.test(ua) ? 'Chrome' : /Safari\//.test(ua) ? 'Safari' : 'Browser';
91 var os = /Android/.test(ua) ? 'Android' : /iPad|iPhone|iPod/.test(ua) ? 'iOS' : /Mac/.test(ua) ? 'macOS' : /Win/.test(ua) ? 'Windows' : /Linux/.test(ua) ? 'Linux' : '';
92 return (browser + (os ? ' op ' + os : ''));
93 }
94 function readAlertBoxes() {
95 var out = {};
96 alertsBox.querySelectorAll('input[data-alert]').forEach(function (cb) { out[cb.getAttribute('data-alert')] = cb.checked ? 1 : 0; });
97 return out;
98 }
99 function setAlertBoxes(alerts) {
100 alertsBox.querySelectorAll('input[data-alert]').forEach(function (cb) {
101 cb.checked = !!alerts[cb.getAttribute('data-alert')];
102 });
103 }
104
105 function render(sub) {
106 currentEndpoint = sub ? sub.endpoint : null;
107 elState.textContent = sub ? cfg.i18n.on : (Notification.permission === 'denied' ? cfg.i18n.denied : cfg.i18n.off);
108 btnOn.hidden = !!sub || Notification.permission === 'denied';
109 btnOff.hidden = !sub;
110 btnTest.hidden = !sub;
111 alertsBox.hidden = !sub;
112 }
113
114 navigator.serviceWorker.ready.then(function (reg) {
115 return reg.pushManager.getSubscription();
116 }).then(function (sub) {
117 // Show this device's SAVED prefs when we know them, defaults otherwise.
118 setAlertBoxes((sub && cfg.saved[sub.endpoint]) ? Object.assign({}, cfg.alerts, cfg.saved[sub.endpoint]) : cfg.alerts);
119 render(sub);
120 }).catch(function () { elState.textContent = cfg.i18n.unknown; });
121
122 btnOn.addEventListener('click', function () {
123 btnOn.disabled = true;
124 Notification.requestPermission().then(function (perm) {
125 if (perm !== 'granted') { btnOn.disabled = false; render(null); return; }
126 navigator.serviceWorker.ready.then(function (reg) {
127 return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: b64ToU8(cfg.vapid) });
128 }).then(function (sub) {
129 return post('/push/subscribe', { subscription: sub.toJSON(), alerts: readAlertBoxes(), uaLabel: deviceLabel() })
130 .then(function (r) { if (!r.ok) throw new Error('subscribe failed'); render(sub); location.reload(); });
131 }).catch(function () { btnOn.disabled = false; elState.textContent = cfg.i18n.failed; });
132 });
133 });
134
135 btnOff.addEventListener('click', function () {
136 navigator.serviceWorker.ready.then(function (reg) { return reg.pushManager.getSubscription(); }).then(function (sub) {
137 if (!sub) { render(null); return; }
138 var ep = sub.endpoint;
139 sub.unsubscribe().then(function () { return post('/push/unsubscribe', { endpoint: ep }); })
140 .then(function () { location.reload(); });
141 });
142 });
143
144 btnTest.addEventListener('click', function () {
145 btnTest.disabled = true;
146 post('/push/test').then(function () { setTimeout(function () { btnTest.disabled = false; }, 2000); });
147 });
148
149 alertsBox.addEventListener('change', function () {
150 if (!currentEndpoint) return;
151 post('/push/alerts', { endpoint: currentEndpoint, alerts: readAlertBoxes() }).then(function (r) {
152 if (r.ok) { savedMsg.hidden = false; setTimeout(function () { savedMsg.hidden = true; }, 1500); }
153 });
154 });
155
156 document.querySelectorAll('.np-remove').forEach(function (btn) {
157 btn.addEventListener('click', function () {
158 post('/push/unsubscribe', { endpoint: btn.getAttribute('data-endpoint') }).then(function () { location.reload(); });
159 });
160 });
161})();
162</script>
163<% } %>
Note: See TracBrowser for help on using the repository browser.