| index 4738b8d..2bd5770 100644 |
| --- a/web/static/sw.js |
| +++ b/web/static/sw.js |
| @@ -32,6 +32,22 @@ function idbGet(key) { |
| }); |
| } |
| |
| +// Per-viewer names (#577) in notification text. `renames` maps published |
| +// label → the name this member calls them. A classic-worker copy of ui.js |
| +// translateMentions (unit-tested there; this file can't import modules) — |
| +// same boundary rules as the feed's renderer: longest label wins, no trailing |
| +// word char, and a preceding word/@/. char means mid-word or an email. |
| +function translateMentions(text, renames) { |
| + const labels = Object.keys(renames || {}); |
| + if (!text || !labels.length || !text.includes("@")) return text; |
| + const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); |
| + const re = new RegExp("@(" + labels.sort((a, b) => b.length - a.length).map(esc).join("|") + ")(?![\\w])", "g"); |
| + return text.replace(re, (m, label, idx) => { |
| + const prev = text[idx - 1]; |
| + return prev !== undefined && /[\w@.]/.test(prev) ? m : "@" + renames[label]; |
| + }); |
| +} |
| + |
| async function decryptBlob(blob, keyB64) { |
| const key = await crypto.subtle.importKey( |
| "raw", b64urlToBytes(keyB64), "AES-GCM", false, ["decrypt"] |
| @@ -98,6 +114,10 @@ async function handlePush(event) { |
| try { |
| // Per-thread keys (id -> {key, name, kind}) the page syncs to IndexedDB. |
| const threads = (await idbGet("threadKeys")) || {}; |
| + // Names as this member shows them (#577): uid → display name, plus the |
| + // published-label → my-name map for rewriting @mentions in body text. |
| + const names = (await idbGet("memberNames")) || {}; |
| + const nameFor = (uid, fallback) => (uid && names.byUid && names.byUid[uid]) || fallback; |
| const t = data.thread && threads[data.thread]; |
| if (data.blob && t && t.key) { |
| const msg = await decryptBlob(data.blob, t.key); |
| @@ -110,8 +130,8 @@ async function handlePush(event) { |
| // author uid notify no one. |
| const me = await idbGet("myUid"); |
| if (!msg.on || !msg.targetUid || !me || msg.targetUid !== me) return; |
| - title = t.name || msg.name || "New message"; |
| - body = `${msg.name || "Someone"} reacted ${msg.emoji || ""} to your message`; |
| + title = t.name || nameFor(msg.uid, msg.name) || "New message"; |
| + body = `${nameFor(msg.uid, msg.name) || "Someone"} reacted ${msg.emoji || ""} to your message`; |
| } else if (msg.notice && msg.notice.title) { |
| // A mini-app event carrying its own (encrypted) notification — e.g. a |
| // reminder the server just delivered. The app wrote the words. |
| @@ -122,8 +142,8 @@ async function handlePush(event) { |
| // silent rather than showing an empty "New message". |
| return; |
| } else { |
| - const sender = msg.name || ""; |
| - let text = msg.text || ""; |
| + const sender = nameFor(msg.uid, msg.name) || ""; |
| + let text = translateMentions(msg.text || "", names.renames); |
| if (!text && Array.isArray(msg.attachments) && msg.attachments.length) { |
| const a = msg.attachments[0]; |
| text = a.type && a.type.startsWith("image/") ? "📷 Photo" : "📎 " + (a.name || "File"); |