aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-04 17:48:17 -0700
committerAdam Soltys <[email protected]>2026-09-04 17:48:17 -0700
commit410e6b1cc4c88c675a3aae8c9c0eed61e64af47d (patch)
tree22d576fe42777e38e7a75527a77bbe12e8a6043d /src
parentb9987703de8b3ca89f93dbd9995412c9f47f7de2 (diff)
downloadcoinosv3-410e6b1cc4c88c675a3aae8c9c0eed61e64af47d.tar.gz
coinosv3-410e6b1cc4c88c675a3aae8c9c0eed61e64af47d.zip
Profile notes become a real feed: INFINITE SCROLL (nearing the page bottom pulls the next page of older kind-1s — until = oldest seen, one fetch in flight, a short answer marks the end; the listener registers at feature construction so the public no-wallet /username pages scroll too), VIDEO rendering in noteBody (mp4/webm/mov/m4v as <video controls preload=metadata playsinline> beside the existing inline images), and FULL-BLEED on phones — the notes card drops its walls and runs edge to edge under 520px; the inner 440px scrollbox is gone everywhere, the page itself scrolls. Verified headless at phone width on /adam: feed spans 0px-to-edge both sides, images render inline, and scrolling grew the list 51 → 82 posts
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js59
-rw-r--r--src/style.css10
2 files changed, 63 insertions, 6 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index c8e38eb..20dd4ca 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -1733,11 +1733,10 @@ export function messagesFeature(ctx) {
const seen = new Set();
const fresh = (evs || [])
.filter((e) => !seen.has(e.id) && seen.add(e.id))
- .sort((a, b) => b.created_at - a.created_at)
- .slice(0, 20);
+ .sort((a, b) => b.created_at - a.created_at);
if (fresh.length || !c.notes.length) {
c.notes = fresh;
- persistPage('notes', pk, fresh.map(slimNote));
+ persistPage('notes', pk, fresh.slice(0, 20).map(slimNote)); // cache stays bounded
}
})().catch(() => {}).finally(() => {
c.status = 'ready';
@@ -1746,6 +1745,41 @@ export function messagesFeature(ctx) {
return c;
}
+ // Infinite scroll: pull the next page of OLDER notes (until = oldest seen)
+ // when the profile page nears its bottom. Guarded so scroll spam costs
+ // nothing: one fetch in flight, and a short answer marks the end.
+ // Registered at CONSTRUCTION, not init(): the public no-wallet profile
+ // (a /username deep link over the unlock screen) scrolls too, and init
+ // only runs once a wallet opens.
+ if (typeof window !== 'undefined') {
+ window.addEventListener('scroll', () => {
+ if (!ui.profilePk) return;
+ if (window.innerHeight + window.scrollY < (document.documentElement.scrollHeight || 0) - 600) return;
+ loadOlderNotes(ui.profilePk).catch(() => {});
+ }, { passive: true });
+ }
+ async function loadOlderNotes(pk) {
+ const c = notesCache.get(pk);
+ if (!c || c.status !== 'ready' || c.loadingMore || c.end) return;
+ const oldest = c.notes[c.notes.length - 1];
+ if (!oldest) { c.end = true; return; }
+ c.loadingMore = true;
+ render();
+ try {
+ const evs = await queryOn(await notesRelays(pk),
+ { kinds: [1], authors: [pk], limit: 30, until: oldest.created_at - 1 }, 4500);
+ const seen = new Set(c.notes.map((e) => e.id));
+ const older = (evs || [])
+ .filter((e) => !seen.has(e.id) && seen.add(e.id))
+ .sort((a, b) => b.created_at - a.created_at);
+ if (older.length) c.notes = [...c.notes, ...older];
+ else c.end = true; // the relays have nothing older
+ } catch {} finally {
+ c.loadingMore = false;
+ if (ui.profilePk === pk) render();
+ }
+ }
+
// Note content, safely: text stays text nodes — relay content must never
// reach innerHTML. URLs become links (image URLs inline), npub mentions a
// clickable @name, other nostr: refs a dim stub.
@@ -1758,6 +1792,12 @@ export function messagesFeature(ctx) {
if (/\.(png|jpe?g|gif|webp|avif)(\?[^\s]*)?$/i.test(part)) {
out.push(h('img', { src: part, class: 'note-img', loading: 'lazy',
onError: (e) => { e.target.style.display = 'none'; } }));
+ } else if (/\.(mp4|webm|mov|m4v)(\?[^\s]*)?$/i.test(part)) {
+ // metadata-only preload: the poster frame paints, nothing streams
+ // until the viewer presses play
+ out.push(h('video', { src: part, class: 'note-video', controls: true,
+ preload: 'metadata', playsinline: true,
+ onError: (e) => { e.target.style.display = 'none'; } }));
} else {
out.push(h('a', { href: part, target: '_blank', rel: 'noopener noreferrer' },
part.length > 64 ? part.slice(0, 61) + '…' : part));
@@ -2337,7 +2377,10 @@ export function messagesFeature(ctx) {
render();
hook('matchSendText', npubStr);
} }, t('profPay')))),
- // Their latest public notes, zappable in place.
+ // Their public notes: the PAGE scrolls (no inner scrollbox), older
+ // pages stream in as you near the bottom (the init() scroll listener →
+ // loadOlderNotes), and on phones the feed goes full-bleed — edge to
+ // edge, no card walls (see .notes-feed).
(() => {
const c = notesFor(pk);
// while loading: an invisible copy of the empty-state line holds the
@@ -2348,11 +2391,15 @@ export function messagesFeature(ctx) {
return h('div', { class: 'small faint', style: 'text-align:center' }, t('profNotesNone'));
return h('div', { class: 'col', style: 'gap:8px' },
h('div', { class: 'small muted', style: 'padding:0 2px' }, t('profNotesTitle')),
- h('div', { class: 'card col', style: 'gap:0;padding:2px 14px;max-height:440px;overflow-y:auto' },
+ h('div', { class: 'card col notes-feed', style: 'gap:0' },
...c.notes.flatMap((ev, i) => [
i ? h('div', { style: 'height:1px;background:var(--border,rgba(128,128,128,.18));margin:0 -14px' }) : null,
noteRow(pk, ev, name),
- ])));
+ ])),
+ c.loadingMore
+ ? h('div', { class: 'row gap6', style: 'justify-content:center;padding:4px 0' },
+ h('span', { class: 'spinner sm' }))
+ : null);
})(),
h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.profilePk = null; ui.profOverThread = false; ui.pubProf = null; ui.profEdit = null; ui.profEditFilled = false; ui.profCompose = null; render(); } }, t('back')),
mine ? logoutPop() : null);
diff --git a/src/style.css b/src/style.css
index cf0746e..0f50159 100644
--- a/src/style.css
+++ b/src/style.css
@@ -489,6 +489,16 @@ hr.divider { margin: 16px 0; }
/* Profile note feed: inline images stay contained, links wrap. */
.note-img { display: block; max-width: 100%; max-height: 320px; border-radius: 10px; margin-top: 6px; object-fit: contain; }
+.note-video { display: block; max-width: 100%; max-height: 360px; border-radius: 10px; margin-top: 6px; background: #000; }
+/* the profile notes feed: page-scrolled (no inner scrollbox); on phones it
+ runs edge to edge — the card walls step outside #app's padding */
+.notes-feed { padding: 2px 14px; }
+@media (max-width: 520px) {
+ .notes-feed {
+ margin-left: -16px; margin-right: -16px;
+ border-left: none; border-right: none; border-radius: 0;
+ }
+}
/* Inline move-money panel on the balance card. */