diff options
Diffstat (limited to 'src/app.js')
| -rw-r--r-- | src/app.js | 65 |
1 files changed, 60 insertions, 5 deletions
@@ -790,9 +790,10 @@ function renderInner() { // because nothing else moves. Onboarding keeps its precedence. const lockAsk = ui.pw && ui.pw.purpose === 'lock' && !(ui.onb || onbInProgress()); sendPageRendered = false; + const featScreen = !lockAsk && featureHook('screenView'); const screen = (lockAsk && h('div', { class: 'col', style: 'gap:16px' }, brandHeader(false), pwPromptCard())) - || featureHook('screenView') + || featScreen || (ui.screen === 'wallet' ? walletScreen() : ui.screen === 'accounts' @@ -835,7 +836,9 @@ function renderInner() { // public pages — never over sign-in, onboarding or the lock prompt. const navPage = ui.pubProf || (activeAccount() && ['wallet', 'accounts', 'accountSettings'].includes(ui.screen) && !(ui.onb || onbInProgress())); const bottomNav = navPage && !lockAsk && !_bootDeciding ? featureHook('bottomNav') : null; - morphChildren(root, [screen, footer(), ...(bottomNav ? [bottomNav] : []), ...(ui.lightbox ? [imageViewer()] : [])]); + // the wallet beside the feed and chats on a wide screen (its own page needs no twin) + const side = bottomNav && featScreen && WIDE_MQ && WIDE_MQ.matches ? walletSide() : null; + morphChildren(root, [screen, footer(), ...(bottomNav ? [bottomNav] : []), ...(side ? [side] : []), ...(ui.lightbox ? [imageViewer()] : [])]); promoteLazySrc(root); // sources on the nodes that actually made it into the page try { document.documentElement.classList.toggle('no-scroll', !!(ui.lightbox || ui.mediaPager)); } catch {} if (fpath) { @@ -2575,7 +2578,7 @@ function avatarMenu() { html: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>', }); return h('button', { - class: 'header-avatar', + class: 'header-avatar' + (me ? ' header-railed' : ''), title: me ? t('profYourProfile') : t('walletSettings'), 'aria-label': me ? t('profYourProfile') : t('walletSettings'), onClick: () => { @@ -2588,7 +2591,7 @@ function avatarMenu() { function settingsBtn() { return h('button', { - class: 'header-msgs', title: t('tabSettings'), 'aria-label': t('tabSettings'), + class: 'header-msgs' + ' header-railed', title: t('tabSettings'), 'aria-label': t('tabSettings'), onClick: () => { clearFeatureNav(); ui.screen = 'wallet'; ui.tab = 'settings'; ui.settingsPage = null; render(); @@ -2672,10 +2675,25 @@ function lockBtn() { } // Find anyone on nostr from the header — results open their profile. +// Wide screens trade the search icon for a field in the header: typing in it +// opens (and drives) the search page, which then drops its own copy of it. +function headerSearch() { + if (!featureHook('userSearchAvailable')) return null; + return h('input', { + type: 'text', class: 'header-search', placeholder: t('searchUsers') + '…', 'aria-label': t('searchUsers'), + value: (ui.userSearch && ui.userSearch.q) || '', autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', + onInput: (e) => { clearFeatureNavKeepSearch(); featureHook('userSearchQuery', e.target.value); }, + }); +} +function clearFeatureNavKeepSearch() { + if (ui.userSearch) return; // already on the search page: just the query moves + clearFeatureNav(); ui.screen = 'wallet'; +} + function searchBtn() { if (!featureHook('userSearchAvailable')) return null; return h('button', { - class: 'header-msgs', + class: 'header-msgs' + ' header-railed', title: t('searchUsers'), 'aria-label': t('searchUsers'), onClick: () => { @@ -2705,6 +2723,7 @@ function brandHeader(withLock) { withLock ? h('div', { class: 'row gap6', style: 'align-items:center' }, searchBtn(), + headerSearch(), settingsBtn(), avatarMenu()) : ui.pubProf && !acc ? visitorButtons() : null @@ -4320,6 +4339,42 @@ function setAccountSel(a, dir) { const GEAR_ICON = (n) => `<svg width="${n}" height="${n}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" style="display:block"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.09a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.09a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`; const SWAP_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block"><path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/></svg>'; +// Wide screens: the bottom nav is a rail on the left (CSS only) and, from +// 1220px, the wallet rides in a column on the right of the feed and chats — +// the balance, Receive and Send, and the latest payments, a glance away. +const WIDE_MQ = typeof matchMedia === 'function' ? matchMedia('(min-width: 1220px)') : null; +try { WIDE_MQ && WIDE_MQ.addEventListener('change', () => render()); } catch {} +function walletSide() { + const toWallet = (tab) => { goHome(); ui.tab = tab; render(); }; + const spending = featureHook('spendingSat') || 0; + const saving = wallet.spendable || 0; + const sel = accountSel(); + const txs = sel === 'spending' ? [] : wallet.history || []; + const entries = wallet.loaded ? [ + ...txs.map((tx) => ({ time: tx.confirmed ? (tx.blockTime || 0) * 1000 : Date.now(), render: () => txHistoryItem(tx) })), + ...featureAll('historyEntries', txs, sel), + ].sort((a, b) => b.time - a.time).slice(0, 6) : []; + const rows = entries.map((e) => { + const row = e.render(); + // a row opens its detail on the wallet page: go there first + row.addEventListener('click', () => { goHome(); ui.tab = 'history'; }, true); + return row; + }); + const line = (label, sat) => h('div', { class: 'row between small' }, + h('span', { class: 'muted' }, label), h('span', {}, fmtAmount(sat) + ' ' + unitLabel())); + return h('aside', { class: 'wallet-side col', 'data-key': 'wallet-side' }, + h('div', { class: 'card col', style: 'gap:10px' }, + h('button', { type: 'button', class: 'wallet-side-total', onClick: () => toWallet('history') }, + h('span', { class: 'small muted' }, t('balance')), + h('span', { class: 'wallet-side-amount' }, wallet.loaded ? fmtAmount(spending + saving) + ' ' + unitLabel() : '…')), + spendingActive() ? line(t('spendingLabel'), spending) : null, + spendingActive() ? line(t('savingLabel'), saving) : null, + h('div', { class: 'row gap6' }, + h('button', { type: 'button', class: 'btn-primary grow', onClick: () => toWallet('receive') }, t('tabReceive')), + h('button', { type: 'button', class: 'btn-primary grow', onClick: () => toWallet('send') }, t('tabSend')))), + rows.length ? h('div', { class: 'card history-card' }, h('div', { class: 'list' }, ...rows)) : null); +} + function balanceCard() { // Dim + spin until this network has data (cache, Nostr state, or a scan has // populated balances once). `scanning` alone misses the window between |
