diff options
| author | Adam Soltys <[email protected]> | 2026-08-02 19:39:00 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-02 19:39:00 -0700 |
| commit | 78ab79e7527bdab08c59e5ee8ee5dd701c0856e0 (patch) | |
| tree | a93a405f8c23e7158d47375127a2be68dd4b28c2 /src/features | |
| parent | 0f47a16fd81091f03ab1f4e4e8143fedf94d1f68 (diff) | |
| download | coinosv3-78ab79e7527bdab08c59e5ee8ee5dd701c0856e0.tar.gz coinosv3-78ab79e7527bdab08c59e5ee8ee5dd701c0856e0.zip | |
Punk default avatars, header profile button, owner channel creation
Everyone gets a face: profiles without a picture fall back to a coinos
punk, picked deterministically by the pubkey's last byte — the same 64
webps and formula the coinos app uses (initials show through if the
image can't load). Your own avatar moves from the chat home to the app
header next to the wallet selector, opening your editable profile from
anywhere.
Community owners can add channels from the room's + panel: one
owner-signed ChannelMetadata edition (CORD-03), and every member's
channel switcher grows when the fold lands.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 63 |
1 files changed, 48 insertions, 15 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index ef70ccd..c70964b 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -23,7 +23,7 @@ import { communityId, parseInviteLink, makeInviteLink, makeInviteBundleEvent, openInviteBundle, } from '../concord.js'; import { makeDM, unwrapDM, wrapDM } from '../dm.js'; -import { makeSearcher, resultRows } from '../recipient-search.js'; +import { makeSearcher, resultRows, fallbackAvatar } from '../recipient-search.js'; import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; import { t } from '../i18n.js'; @@ -107,7 +107,7 @@ export function messagesFeature(ctx) { if (repaintTimer) return; repaintTimer = setTimeout(() => { repaintTimer = null; - if (ui.screen === 'wallet' && ui.chatOpen) render(); + if (ui.screen === 'wallet') render(); }, 80); }; @@ -360,6 +360,25 @@ export function messagesFeature(ctx) { render(); } + // A new channel is one owner-signed ChannelMetadata edition (CORD-03 §2); + // the control-plane fold picks it up and every member's switcher grows. + async function createChannel(room, name) { + name = (name || '').trim().slice(0, 64); + if (!name) return; + const id = await identity(); + if (!id || id.pubkey !== room.jm.owner) { toast(t('msgOwnerOnly')); return; } + const chId = bytesToHex(crypto.getRandomValues(new Uint8Array(32))); + const edition = { + ...makeEdition({ vsk: 2, eid: chId, version: 1, content: JSON.stringify({ name: name.replace(/^#/, ''), private: false }) }, Date.now()), + pubkey: id.pubkey, + }; + const wrap = await wrapRumor(edition, id.signer, room.control, { plaintext: true }); + const ok = await publishOn(room.relays, wrap); + if (!ok) { toast(t('msgSendFailed')); return; } + ui.msgNewChannel = ''; + toast('#' + name.replace(/^#/, '')); + } + // ---- invites ------------------------------------------------------------ function bundleFor(room, creatorPk) { @@ -746,13 +765,15 @@ export function messagesFeature(ctx) { }; const avatar = (pk, cls = 'chat-avatar', clickable = true) => { - const attrs = clickable - ? { class: cls + ' clickable', onClick: (e) => { e.stopPropagation(); openProfile(pk); } } - : { class: cls }; const p = profileOf(pk); - return p && p.picture - ? h('img', { ...attrs, src: p.picture, alt: '' }) - : h('div', { ...attrs, class: attrs.class + ' fallback' }, displayName(pk).slice(0, 2)); + const node = p && p.picture + ? h('img', { class: cls, src: p.picture, alt: '' }) + : fallbackAvatar(h, pk, p && p.name, cls); + if (clickable) { + node.classList.add('clickable'); + node.addEventListener('click', (e) => { e.stopPropagation(); openProfile(pk); }); + } + return node; }; // ---- profiles: view + own kind-0 editor --------------------------------- @@ -925,12 +946,9 @@ export function messagesFeature(ctx) { const kids = []; // Chat takes the whole screen, so home carries the way back to the wallet. - const me = myPubkeys()[0]; - kids.push(h('div', { class: 'row between', style: 'align-items:center' }, - h('div', { class: 'row gap6', style: 'align-items:center' }, - backBtn(() => { ui.chatOpen = false; render(); }), - h('h3', { style: 'margin:0' }, t('tabMessages'))), - me ? avatar(me) : null)); + kids.push(h('div', { class: 'row gap6', style: 'align-items:center' }, + backBtn(() => { ui.chatOpen = false; render(); }), + h('h3', { style: 'margin:0' }, t('tabMessages')))); if (pendingLink) kids.push(linkInviteCard()); for (const [rid, inv] of pendingDirect) kids.push(directInviteCard(rid, inv)); @@ -1162,6 +1180,15 @@ export function messagesFeature(ctx) { class: 'btn-sm', onClick: () => { sendDirectInvite(room, ui.msgInviteTo); ui.msgInviteTo = ''; render(); }, }, t('msgSend'))), + // owners can add channels (an owner-signed control edition) + myPubkeys().includes(room.jm.owner) + ? h('div', { class: 'row gap6' }, + h('input', { + class: 'grow', type: 'text', placeholder: t('msgChannelPlaceholder'), maxlength: '64', + value: ui.msgNewChannel || '', onInput: (e) => { ui.msgNewChannel = e.target.value; }, + }), + h('button', { class: 'btn-sm', onClick: () => createChannel(room, ui.msgNewChannel) }, t('msgCreate'))) + : null, // leaving discards the keys on this identity — two taps, default community exempt builtin ? null : h('div', { class: 'row' }, h('button', { @@ -1218,10 +1245,16 @@ export function messagesFeature(ctx) { // Chat lives behind a header button and takes over the whole screen — // no balance card, no tabs; each view carries its own way back. headerButtons() { - return [h('button', { + const btns = [h('button', { class: 'btn-sm', title: t('tabMessages'), onClick: () => { ui.chatOpen = true; render(); }, }, h('span', { html: CHAT_ICON }))]; + const me = myPubkeys()[0]; + if (me) btns.push(h('button', { + class: 'header-avatar', title: t('profEdit'), + onClick: () => openProfile(me), + }, avatar(me, 'chat-avatar header-ava', false))); + return btns; }, screenView() { if (ui.screen !== 'wallet') return null; |
