diff options
| author | Adam Soltys <[email protected]> | 2026-10-09 16:04:46 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-09 16:04:46 -0700 |
| commit | 22653c35ab372f7649711a743fd88b71265ae4e5 (patch) | |
| tree | e96aecb2d926ded1292c35bc81abc740df54cf2a /src | |
| parent | b6cca5ba5196d3b89df4708c0b1aff31ec624df0 (diff) | |
| download | coinosv3-22653c35ab372f7649711a743fd88b71265ae4e5.tar.gz coinosv3-22653c35ab372f7649711a743fd88b71265ae4e5.zip | |
Profiles: Posts / Replies tabs. Replies were mixed into a profile's list, tagged "↩ reply" — now Posts (the default) is what they posted and Replies is their side of other people's threads (NIP-10 replies, NIP-22 comments). Profiles load thirty mixed notes a page and older pages only on scroll, so a tab that holds few of its kind (someone who mostly replies, or never does) fetches older pages by itself — five at most per tab, so a person who never replies doesn't have their whole history paged through. tools/profile-replies-test.js; profile-feed-test waits for rows (the tabs come before them; the old "Latest posts" heading only appeared with notes in hand).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/messages.js | 42 | ||||
| -rw-r--r-- | src/i18n.js | 3 |
2 files changed, 38 insertions, 7 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 6572e27..abbbf74 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -4551,6 +4551,10 @@ export function messagesFeature(ctx) { // (a relay down, the phone just back from sleep) must not freeze a // profile at two posts for the rest of the session. const NOTES_REFRESH_MS = 60_000; + // A profile's Posts / Replies tab (Posts unless chosen on this profile). + const profTab = (pk) => (ui.profTab && ui.profTab.pk === pk ? ui.profTab.tab : 'posts'); + // what goes under Replies: a note answering another (NIP-10), or a NIP-22 comment + const replyish = (e) => e.kind === 1111 || (e.kind === 1 && isReply(e)); function notesFor(pk) { let c = notesCache.get(pk); if (c) { @@ -10337,18 +10341,42 @@ export function messagesFeature(ctx) { // edge, no card walls (see .notes-feed). (() => { const c = notesFor(pk); + // Posts | Replies: the replies are their side of other people's + // conversations, apart from what they posted themselves + const tab = profTab(pk); + const shown = c.notes.filter((e) => (tab === 'replies') === replyish(e)); + // a tab that holds only a few of the pages fetched so far (someone + // who mostly replies, or mostly posts) asks for older ones by itself: + // too short to scroll means the scroll that loads more never comes + // (five pages at most per tab: someone who never replies must not + // have their whole history paged through looking for one) + const fill = c.autoFill && c.autoFill.tab === tab ? c.autoFill : (c.autoFill = { tab, n: 0 }); + if (c.status === 'ready' && shown.length < 12 && !c.end && !c.loadingMore && fill.n < 5) { + fill.n++; + queueMicrotask(() => loadOlderNotes(pk).catch(() => {})); + } + const tabs = h('div', { class: 'seg prof-tabs', style: 'display:flex;width:100%' }, + ...['posts', 'replies'].map((k) => h('button', { + type: 'button', class: (tab === k ? 'active ' : '') + 'grow', 'data-tab': k, + onClick: () => { if (tab === k) return; ui.profTab = { pk, tab: k }; c.win = null; render(); }, + }, t(k === 'posts' ? 'profTabPosts' : 'profTabReplies')))); // while loading with nothing to show: an invisible copy of the // empty-state line holds the height, so the page doesn't jump when // the answer lands (no spinner). With posts already in hand — from // the feed we came from — those are shown instead of the blank. - if (c.status === 'loading' && !c.notes.length) - return h('div', { class: 'small faint', style: 'text-align:center;visibility:hidden' }, t('profNotesNone')); - if (!c.notes.length) - return h('div', { class: 'small faint', style: 'text-align:center' }, t('profNotesNone')); + const none = t(tab === 'replies' ? 'profRepliesNone' : 'profNotesNone'); + if (!shown.length) { + const waiting = c.status === 'loading' || c.loadingMore || (!c.end && c.status === 'ready' && fill.n < 5); + return h('div', { class: 'col', style: 'gap:8px' }, tabs, + waiting && c.notes.length + ? h('div', { class: 'row gap6', style: 'justify-content:center;padding:8px 0' }, h('span', { class: 'spinner sm' })) + // nothing in hand yet: the empty line holds the height, unseen (no spinner) + : h('div', { class: 'small faint', style: 'text-align:center' + (waiting ? ';visibility:hidden' : '') }, none)); + } return h('div', { class: 'col', style: 'gap:8px' }, - h('div', { class: 'small muted', style: 'padding:0 2px' }, t('profNotesTitle')), - h('div', { class: 'card col notes-feed', style: 'gap:0' }, - ...(c.winList = () => c.notes, windowedRows(c, c.notes, (ev) => keyed(noteRow(pk, ev, name), ev.id)))), + tabs, + h('div', { class: 'card col notes-feed', style: 'gap:0', 'data-key': 'prof-notes:' + tab }, + ...(c.winList = () => c.notes.filter((e) => (profTab(pk) === 'replies') === replyish(e)), windowedRows(c, shown, (ev) => keyed(noteRow(pk, ev, name), ev.id)))), c.loadingMore ? h('div', { class: 'row gap6', style: 'justify-content:center;padding:4px 0' }, h('span', { class: 'spinner sm' })) diff --git a/src/i18n.js b/src/i18n.js index 894012e..9df0127 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -708,6 +708,9 @@ const STR = { hatMainnetOnly: 'Hats are sold on mainnet and Mutinynet only', profNotesTitle: 'Latest posts', profNotesNone: 'No recent posts found.', + profTabPosts: 'Posts', + profTabReplies: 'Replies', + profRepliesNone: 'No recent replies found.', profReplyTag: 'reply', msgSearching: 'Searching…', msgNoMatches: 'No matches', |
