aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/app.js')
-rw-r--r--src/app.js65
1 files changed, 60 insertions, 5 deletions
diff --git a/src/app.js b/src/app.js
index 69425be..bf49dd1 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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