From 5075e9f4c585138cddfe7e7962170dbd2e73874a Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 2 Aug 2026 19:57:22 -0700 Subject: Header: wallet + avatar only; the avatar opens an identity menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chat button leaves the header — the avatar's popup menu carries Edit profile, Settings, Messages, and Logout (lock()). The menu is available inside the chat takeover too, which now keeps the full header. Messages provides just the avatar node via the headerAvatar seam; the app owns the menu. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi --- src/app.js | 29 ++++++++++++++++++++++++++--- src/features/messages.js | 20 +++----------------- src/style.css | 3 +++ 3 files changed, 32 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/app.js b/src/app.js index 3a19573..4ae1912 100644 --- a/src/app.js +++ b/src/app.js @@ -1340,6 +1340,30 @@ function lock() { } // ================================================================ WALLET +// The avatar anchors the identity menu: Edit profile, Settings, Messages, +// Logout. The avatar node itself comes from the messages feature (profile +// cache + punk fallback); without that feature there's no menu. +function avatarMenu() { + const me = (featureHook('nostrLoginIdentity') || {}).pubkey || (wallet.nostrPubkey && wallet.nostrPubkey()); + const node = me && featureHook('headerAvatar', me); + if (!node) return null; + const item = (label, fn) => h('button', { + class: 'icon-select-item', + onClick: () => { ui.avatarMenu = false; fn(); render(); }, + }, label); + return h('span', { class: 'avatar-menu-wrap' }, + h('button', { class: 'header-avatar', onClick: () => { ui.avatarMenu = !ui.avatarMenu; render(); } }, node), + ui.avatarMenu + ? h('span', {}, + h('div', { class: 'menu-backdrop', onClick: () => { ui.avatarMenu = false; render(); } }), + h('div', { class: 'icon-select-menu avatar-menu' }, + item(t('profEdit'), () => featureHook('showProfile', me)), + item(t('tabSettings'), () => { ui.chatOpen = false; ui.profilePk = null; ui.screen = 'wallet'; ui.tab = 'settings'; ui.settingsPage = null; }), + item(t('msgDmsTitle'), () => { ui.profilePk = null; ui.chatOpen = true; }), + item(t('logout'), () => lock()))) + : null); +} + function brandHeader(withLock) { const acc = activeAccount(); return h( @@ -1351,11 +1375,10 @@ function brandHeader(withLock) { h('div', { class: 'logo-full', 'aria-label': 'coinos', role: 'img', html: '' }) ), withLock - ? h('div', { class: 'row gap6' }, - ...featureAll('headerButtons'), + ? h('div', { class: 'row gap6', style: 'align-items:center' }, h('button', { class: 'btn-sm', onClick: () => { ui.screen = 'accounts'; render(); } }, acc ? acc.label : t('accounts')), - featureHook('headerAvatar')) + avatarMenu()) : null ); } diff --git a/src/features/messages.js b/src/features/messages.js index d74c338..70142e0 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -42,7 +42,6 @@ const COMMUNITY = { }; const EPOCH = 0; -const CHAT_ICON = ''; const DM_RELAYS = ['wss://relay.coinos.io', 'wss://nos.lol']; const APP_BASE = 'https://v3.coinos.io'; const CACHE_MAX = 50; // messages kept per channel / per DM thread in feature state @@ -1250,27 +1249,14 @@ export function messagesFeature(ctx) { id: 'messages', // 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', { - class: 'btn-sm', title: t('tabMessages'), - onClick: () => { ui.chatOpen = true; render(); }, - }, h('span', { html: CHAT_ICON }))]; - }, - // Rightmost in the header, after the wallet selector. - headerAvatar() { - const me = myPubkeys()[0]; - if (!me) return null; - return h('button', { - class: 'header-avatar', title: t('profEdit'), - onClick: () => openProfile(me), - }, avatar(me, 'chat-avatar header-ava', false)); - }, + // The bare avatar node for the app header's identity menu. + headerAvatar(pk) { return avatar(pk, 'chat-avatar header-ava', false); }, screenView() { if (ui.screen !== 'wallet') return null; if (ui.profilePk) return profileScreen(); if (!ui.chatOpen) return null; return h('div', { class: 'col', style: 'gap:16px' }, - ctx.brandHeader(false), + ctx.brandHeader(true), messagesTab()); }, // Anyone (ark's history, other features) can open a profile or render a diff --git a/src/style.css b/src/style.css index 2bd18f4..fd6553a 100644 --- a/src/style.css +++ b/src/style.css @@ -400,3 +400,6 @@ hr.divider { margin: 16px 0; } .copy-ico:hover { color: var(--ink); } .settings-nav { text-align: left; font-weight: 600; padding: 14px 16px; position: relative; } .settings-nav::after { content: '›'; position: absolute; right: 16px; color: var(--faint); font-weight: 400; } +.avatar-menu-wrap { position: relative; display: inline-flex; } +.menu-backdrop { position: fixed; inset: 0; z-index: 40; } +.avatar-menu { position: absolute; right: 0; top: calc(100% + 8px); z-index: 41; min-width: 170px; } -- cgit v1.2.3