aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-05 00:27:36 -0700
committerAdam Soltys <[email protected]>2026-10-05 00:27:36 -0700
commit898cb33aba31a37be1e1a5b7e2ded3200dd29180 (patch)
tree26045ed4a6f65d994911ed3cf1c48451aa8a94b0 /src/features
parent609e5d77e7634eb2973b73a677c08f9f2b695951 (diff)
downloadcoinosv3-898cb33aba31a37be1e1a5b7e2ded3200dd29180.tar.gz
coinosv3-898cb33aba31a37be1e1a5b7e2ded3200dd29180.zip
Communities wear their icons: Vector's encrypted logo from the metadata, the coinos logo for coinos
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
Diffstat (limited to 'src/features')
-rw-r--r--src/features/messages.js40
1 files changed, 39 insertions, 1 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 8001d39..e0c5b4c 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -51,6 +51,7 @@ import { qrSvg } from '../qr.js';
// with the public read-only chat page so the two can never drift.
import { COMMUNITY, EPOCH } from '../community.js';
import COMMUNITY_DIRECTORY from '../community-directory.json';
+import { assetUrl } from '../asset-url.js';
const DM_RELAYS = ['wss://relay.coinos.io', 'wss://nos.lol'];
const NOTIFIER = 'https://nwcpush.coinos.io';
const APP_BASE = 'https://v3.coinos.io';
@@ -11166,7 +11167,7 @@ export function messagesFeature(ctx) {
onPointerdown: () => warmRoom(jm),
onClick: () => openRoomWarm(jm, () => { ui.msgView = 'room'; ui.msgCommunity = jm.community_id; ui.msgChannel = null; ui.msgStick = true; }),
},
- h('div', { class: 'chat-avatar fallback' }, name.slice(0, 2)),
+ communityAvatar(jm, room, name),
h('div', { class: 'col grow', style: 'min-width:0;gap:1px' },
h('span', { class: 'chat-name' }, name),
h('div', { class: 'muted small' },
@@ -11235,6 +11236,42 @@ export function messagesFeature(ctx) {
.sort((a, b) => eventMs(b.rumor) - eventMs(a.rumor));
return [...new Set(msgs.map((m) => m.author))].slice(0, 150);
}
+ // A community's face. Vector puts the logo in the vsk-0 metadata as an
+ // AES-256-GCM blob ({ url, key, nonce, hash }, 16-byte nonce, tag
+ // appended); a plain URL is taken as is. Decrypted once per session.
+ // The built-in coinos community wears the coinos logo.
+ const roomIcons = new Map();
+ function communityIcon(jm, room) {
+ const ic = room?.folded?.metadata?.icon;
+ if (typeof ic === 'string' && /^(https:|data:image\/)/.test(ic)) return ic;
+ if (ic && typeof ic === 'object' && /^https:/.test(ic.url || '')) {
+ const k = ic.url + '#' + ic.key;
+ if (!roomIcons.has(k)) {
+ roomIcons.set(k, null);
+ (async () => {
+ const key = hexToBytes(ic.key), iv = hexToBytes(ic.nonce);
+ if (key.length !== 32 || iv.length !== 16) return;
+ const res = await fetch(ic.url, { signal: AbortSignal.timeout(20_000) });
+ if (!res.ok || Number(res.headers.get('content-length') || 0) > 10e6) return;
+ const ct = await res.arrayBuffer();
+ if (ct.byteLength > 10e6) return;
+ const ck = await crypto.subtle.importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt']);
+ const plain = new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, ck, ct));
+ if (ic.hash && bytesToHex(new Uint8Array(await crypto.subtle.digest('SHA-256', plain))) !== String(ic.hash).toLowerCase()) return;
+ roomIcons.set(k, URL.createObjectURL(new Blob([plain])));
+ scheduleRepaint();
+ })().catch(() => {});
+ }
+ if (roomIcons.get(k)) return roomIcons.get(k);
+ }
+ return jm.community_id === COMMUNITY.community_id ? assetUrl('icon-192.png') : null;
+ }
+ const communityAvatar = (jm, room, name, cls = '') => {
+ const src = communityIcon(jm, room);
+ return src
+ ? h('img', { class: 'chat-avatar hub-icon' + cls, src, alt: '' })
+ : h('div', { class: 'chat-avatar fallback' + cls }, name.slice(0, 2));
+ };
// Each face is warmed once per half minute; a later call (the history
// still streaming in when the list first warmed) adds whoever is new.
function warmRoom(jm) {
@@ -11352,6 +11389,7 @@ export function messagesFeature(ctx) {
h('div', { class: 'row between chat-head' },
h('div', { class: 'row gap6', style: 'align-items:center;min-width:0' },
backBtn(() => { ui.msgView = 'home'; ui.msgReplyTo = null; ui.msgSheet = null; render(); }),
+ communityAvatar(jm, room, name, ' header-ava'),
h('div', {
class: 'col clickable', style: 'gap:2px;min-width:0',
onClick: () => { ui.msgMembers = !ui.msgMembers; render(); },