aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-09 16:04:46 -0700
committerAdam Soltys <[email protected]>2026-10-09 16:04:46 -0700
commit22653c35ab372f7649711a743fd88b71265ae4e5 (patch)
treee96aecb2d926ded1292c35bc81abc740df54cf2a /src/features
parentb6cca5ba5196d3b89df4708c0b1aff31ec624df0 (diff)
downloadcoinosv3-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/features')
-rw-r--r--src/features/messages.js42
1 files changed, 35 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' }))