aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-28 21:41:22 -0700
committerAdam Soltys <[email protected]>2026-09-28 21:41:22 -0700
commitb22b0c98c673df984f347db2be580c2551e113f7 (patch)
tree9c4acce4b593fc293da63a1913bbb2c60c707efd
parent63e7746f98d7910053cd0b9e1a4a2ed3e6612af3 (diff)
downloadcoinosv3-b22b0c98c673df984f347db2be580c2551e113f7.tar.gz
coinosv3-b22b0c98c673df984f347db2be580c2551e113f7.zip
The chat list shows four conversations and four communities, the rest behind Show all.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/features/messages.js21
-rw-r--r--src/i18n.js1
-rw-r--r--tools/feed-bottom-nav-test.js17
3 files changed, 29 insertions, 10 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 64754d0..90410fb 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -8244,11 +8244,10 @@ export function messagesFeature(ctx) {
? h('div', { class: 'list' }, resultRows(h, dmSearch.rows, (r) => openThread(r.pk), (pk, node) => hook('wrapAvatar', pk, node)))
: h('div', { class: 'small muted' }, t('msgNoMatches'))));
}
- // A long DM history must not bury the communities below it: past a
- // handful, the rest waits behind "show all". (No cap for barely-over —
- // a "show 2 more" button costs more than the rows it hides.)
- const DM_PREVIEW = 5;
- const shownDms = (ui.msgAllDms || dmRows.length <= DM_PREVIEW + 2) ? dmRows : dmRows.slice(0, DM_PREVIEW);
+ // A long DM history must not bury the communities below it: past four,
+ // the rest waits behind "show all". Communities get the same cap.
+ const LIST_PREVIEW = 4;
+ const shownDms = ui.msgAllDms ? dmRows : dmRows.slice(0, LIST_PREVIEW);
kids.push(
dmRows.length
? h('div', { class: 'list' }, shownDms.map(({ peer, last, unread }) =>
@@ -8268,7 +8267,7 @@ export function messagesFeature(ctx) {
if (shownDms.length < dmRows.length)
kids.push(h('button', { class: 'linklike small', onClick: () => { ui.msgAllDms = true; render(); } },
t('msgShowAllDms', { n: dmRows.length })));
- else if (ui.msgAllDms && dmRows.length > DM_PREVIEW + 2)
+ else if (ui.msgAllDms && dmRows.length > LIST_PREVIEW)
kids.push(h('button', { class: 'linklike small', onClick: () => { ui.msgAllDms = false; render(); } },
t('msgShowFewerDms')));
@@ -8305,7 +8304,9 @@ export function messagesFeature(ctx) {
value: ui.msgNewName || '', onInput: (e) => { ui.msgNewName = e.target.value; },
}),
h('button', { class: 'btn-sm', onClick: () => createCommunity(ui.msgNewName || '') }, t('msgCreate'))));
- kids.push(h('div', { class: 'list' }, communities().map((jm) => {
+ const allRooms = communities();
+ const shownRooms = ui.msgAllRooms ? allRooms : allRooms.slice(0, LIST_PREVIEW);
+ kids.push(h('div', { class: 'list' }, shownRooms.map((jm) => {
const room = rooms.get(jm.community_id);
const name = room?.folded?.metadata?.name || jm.name;
// Last session's settled count anchors the number: while the guestbook
@@ -8329,6 +8330,12 @@ export function messagesFeature(ctx) {
// the row's own centring puts the dot mid-height, off the edge
unread ? h('i', { class: 'thread-dot' }) : null);
})));
+ if (shownRooms.length < allRooms.length)
+ kids.push(h('button', { class: 'linklike small', onClick: () => { ui.msgAllRooms = true; render(); } },
+ t('msgShowAllCommunities', { n: allRooms.length })));
+ else if (ui.msgAllRooms && allRooms.length > LIST_PREVIEW)
+ kids.push(h('button', { class: 'linklike small', onClick: () => { ui.msgAllRooms = false; render(); } },
+ t('msgShowFewerDms')));
return h('div', { class: 'card col chat-page', style: 'gap:10px' }, ...kids);
}
diff --git a/src/i18n.js b/src/i18n.js
index eff5a73..ce5bf7f 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -425,6 +425,7 @@ const STR = {
msgNoDms: 'No conversations yet.',
msgShowAllDms: 'Show all {n} conversations',
msgShowFewerDms: 'Show fewer',
+ msgShowAllCommunities: 'Show all {n} communities',
msgNoDmsYet: 'No messages yet. Say hi!',
msgDmPlaceholder: 'Message',
msgDmEncrypted: 'Private message · end-to-end encrypted',
diff --git a/tools/feed-bottom-nav-test.js b/tools/feed-bottom-nav-test.js
index f16134c..4354e97 100644
--- a/tools/feed-bottom-nav-test.js
+++ b/tools/feed-bottom-nav-test.js
@@ -15,7 +15,7 @@ const wallet = { xpub: 'test-wallet', loaded: true, nostrRelays: () => [],
let feature;
function render() { if (feature) morphChildren(document.querySelector('#app'), [feature.screenView() || h('div', { class: 'wallet-screen' }, 'Wallet'), feature.bottomNav()]); }
feature = messagesFeature({ h, ui, wallet, render, toast() {}, hook: name => name === 'unreadMessages' ? 1 : null, brandHeader: () => h('div', { style: 'height:60px' }, 'Coinos') });
-window.test = { ui, wallet, render, async feed() {
+window.test = { ui, wallet, render, get feature() { return feature; }, async feed() {
ui.chatOpen = true; ui.msgView = 'feed'; ui.noteThread = null;
await feature.testSeed(Array.from({length:20}, (_, i) => ({ id: i.toString(16).padStart(64,'0'), pubkey:author, kind:1, tags:[], created_at:Math.floor(Date.now()/1000)-i,
content: 'A post in the feed. Scroll to keep Home, messages and notifications within reach. '.repeat(3) })));
@@ -26,7 +26,7 @@ await test.feed();
const bundle = await Bun.build({ entrypoints: ['nav-test-entry'], target: 'browser', plugins: [{ name: 'nav-test', setup(build) {
build.onResolve({filter:/^nav-test-entry$/},()=>({path:'entry',namespace:'nav-test'}));
build.onLoad({filter:/.*/,namespace:'nav-test'},()=>({contents:entry,loader:'js',resolveDir:process.cwd()}));
- build.onLoad({filter:/src\/features\/messages\.js$/},async({path})=>({loader:'js',contents:(await Bun.file(path).text()).replace("id: 'messages',", "id: 'messages', testSeed: async (notes) => { const c = feedNow(); await c.boot; Object.assign(c, { notes, status: 'ready', booting: false, end: true, shown: 20 }); }, testUnseen: () => { feed.unseen = 2; },")}));
+ build.onLoad({filter:/src\/features\/messages\.js$/},async({path})=>({loader:'js',contents:(await Bun.file(path).text()).replace("id: 'messages',", "id: 'messages', testSeed: async (notes) => { const c = feedNow(); await c.boot; Object.assign(c, { notes, status: 'ready', booting: false, end: true, shown: 20 }); }, testUnseen: () => { feed.unseen = 2; }, testChats: (n, m) => { stCache = null; if (m != null) st().communities = Array.from({ length: m }, (_, i) => ({ ...COMMUNITY, community_id: (i + 1).toString(16).padStart(64, 'd'), name: 'Room ' + i })); threads.clear(); for (let i = 0; i < n; i++) { const peer = (i + 1).toString(16).padStart(64, 'c'); threads.set(peer, new Map([['r' + i, { rumor: { id: 'r' + i, kind: 14, pubkey: peer, tags: [], content: 'hi ' + i, created_at: 1700000000 - i }, mine: false }]])); } },")}));
build.onLoad({filter:/src\/nostr\.js$/},async({path})=>({loader:'js',contents:(await Bun.file(path).text())
.replace('export async function queryOn(relays, filter, maxWait = 1500) {','export async function queryOn(relays, filter, maxWait = 1500) { return [];')
.replace(/export function subscribeOn\(([^)]*)\) \{/,'export function subscribeOn($1) { return () => {};')}));
@@ -60,6 +60,17 @@ try {
await page.waitForFunction(()=>test.ui.msgView==='home' && !!document.querySelector('.app-bottom-nav'));
assert((await page.evaluate(()=>document.body.innerText)).includes('Messages'),'chat list renders');
assert(await page.$eval('.app-nav-button:nth-child(2)',e=>e.getAttribute('aria-current')==='page'),'messages marked active');
+ const rows=()=>page.evaluate(()=>{const ls=[...document.querySelectorAll('.chat-page > .list')];return ls.map(l=>l.querySelectorAll('.chat-thread-row').length);});
+ const links=()=>page.$$eval('.chat-page > button.linklike',els=>els.map(e=>e.textContent));
+ await page.evaluate(()=>{const m={};const orig=test.wallet.loadFeatureState;test.wallet.loadFeatureState=(k,d)=>k==='messages'?m:orig(k,d);test.feature.testChats(4,4);test.render();});
+ await page.waitForFunction(()=>document.querySelectorAll('.chat-page .chat-thread-row').length>=8);
+ assert.deepEqual(await rows(),[4,4]); assert.deepEqual(await links(),['Show all 5 communities'],'four DMs show whole; five communities are gated');
+ await page.evaluate(()=>{test.feature.testChats(9);test.render();});
+ assert.deepEqual(await rows(),[4,4]); assert.deepEqual(await links(),['Show all 9 conversations','Show all 5 communities']);
+ await page.$$eval('.chat-page > button.linklike',els=>els.forEach(e=>e.click()));
+ assert.deepEqual(await rows(),[9,5]); assert.deepEqual(await links(),['Show fewer','Show fewer']);
+ await page.$$eval('.chat-page > button.linklike',els=>els.forEach(e=>e.click()));
+ assert.deepEqual(await rows(),[4,4],'show fewer folds both lists back');
await page.evaluate(()=>{test.ui.msgView='dm';test.ui.msgPeer='b'.repeat(64);test.render();});
await page.waitForSelector('.chat-card');
assert(await page.evaluate(()=>document.querySelector('.chat-card').getBoundingClientRect().bottom <= document.querySelector('.app-bottom-nav').getBoundingClientRect().top),'conversation and composer fit above bottom nav');
@@ -84,5 +95,5 @@ try {
await page.tap('.app-nav-button:nth-child(2)');
assert(await page.evaluate(()=>!test.ui.chatOpen && test.ui.msgView===null),'visitor reaches sign-in screen');
assert.deepEqual(errors,[]);
- console.log('✓ Global nav works across feed, chat list, conversations, notifications, threads and wallet; Feed restores the feed; composer remains above nav; visitor entry and narrow layout work');
+ console.log('✓ Global nav works across feed, chat list, conversations, notifications, threads and wallet; Feed restores the feed; composer remains above nav; visitor entry and narrow layout work; DMs and communities cap at four behind Show all');
} finally { await page.close(); if (remote) await browser.disconnect(); else await browser.close(); server.stop(true); }