aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/messages.js63
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;