aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-24 07:35:56 -0700
committerAdam Soltys <[email protected]>2026-09-24 07:35:56 -0700
commit7fae8198ef36396e4c65e66597d8e6e6f74ab1d1 (patch)
treec651eccbde76ec2bea34793993da71e0850ea3a8 /tools
parentf6fd9f998831330e3a8819e294cdd8d15c1a87f6 (diff)
downloadcoinosv3-7fae8198ef36396e4c65e66597d8e6e6f74ab1d1.tar.gz
coinosv3-7fae8198ef36396e4c65e66597d8e6e6f74ab1d1.zip
Keep thread avatars and viewport stable on refresh
Diffstat (limited to 'tools')
-rw-r--r--tools/thread-cache-test.js11
-rw-r--r--tools/thread-reply-test.js13
2 files changed, 23 insertions, 1 deletions
diff --git a/tools/thread-cache-test.js b/tools/thread-cache-test.js
index fd3f5c8..14e5da6 100644
--- a/tools/thread-cache-test.js
+++ b/tools/thread-cache-test.js
@@ -19,9 +19,18 @@ check('a fresh store restores full content and tags by any viewed event', () =>
const long = { ...reply, content: 'x'.repeat(5000), tags: [...reply.tags, ['emoji', 'wave', 'https://example.com/wave.png']] };
store.save({ root, replies: [parent, long] }, long.id);
const restored = createThreadStore(storage).find(long.id);
- assert.deepEqual(restored, { rootId: root.id, root, replies: [long, parent] });
+ assert.deepEqual(restored, { rootId: root.id, root, profiles: {}, replies: [long, parent] });
assert.equal(store.find(parent.id).root.id, root.id);
});
+check('thread authors keep their names and local avatar thumbnails across reloads', () => {
+ const thumb = 'data:image/webp;base64,AAAA';
+ const face = { name: 'Nostrich', picture: 'https://example.com/face.webp', eventAt: 50,
+ thumbFor: 'https://example.com/face.webp', thumb, thumbPx: 144 };
+ store.save({ root, replies: [parent, reply] }, reply.id, { [root.pubkey]: face });
+ store.save({ root, replies: [parent, reply] }, reply.id); // a relay refresh must keep the face
+ assert.deepEqual(createThreadStore(storage).find(reply.id).profiles[root.pubkey],
+ { ...face, t: 0 });
+});
check('a large thread retains the selected reply and its ancestors', () => {
const siblings = Array.from({ length: 200 }, (_, i) => note(1000 + i, [['e', root.id, '', 'root']]));
store.save({ root, replies: [...siblings, parent, reply] }, reply.id);
diff --git a/tools/thread-reply-test.js b/tools/thread-reply-test.js
index c125ef8..39f2894 100644
--- a/tools/thread-reply-test.js
+++ b/tools/thread-reply-test.js
@@ -6,6 +6,7 @@ const app = await Bun.file('src/app.js').text();
const dom = app.slice(app.indexOf('function h(tag,'), app.indexOf('// ---------------------------------------------------------------- morphing'));
const entry = `
import { messagesFeature } from './src/features/messages.js';
+import { createThreadStore } from './src/thread-cache.js';
${dom}
const author = 'a'.repeat(64), mine = 'b'.repeat(64), rootId = 'c'.repeat(64);
const root = { id: rootId, pubkey: author, kind: 1, content: 'Root post', tags: [], created_at: 1 };
@@ -24,6 +25,10 @@ function render() {
feature = messagesFeature({ h, wallet, ui, render, toast() {}, brandHeader: () => null,
hook: k => k === 'nostrLoginIdentity' ? { pubkey: mine, signer } : null });
window.test = { ui, reply, other, root, render, waiting,
+ seedFace: () => createThreadStore().save({ root, replies: [reply] }, reply.id, {
+ [author]: { name: 'Nostrich', picture: 'https://example.com/nostrich.webp', eventAt: 10, t: Date.now(),
+ thumbFor: 'https://example.com/nostrich.webp', thumb: 'data:image/png;base64,iVBORw0KGgo=', thumbPx: 144 },
+ }),
release: () => { for (const q of waiting.splice(0)) q.resolve(q.filter.ids ? [root] : []); },
overlay: () => {
ui.profilePk = author; ui.profOverThread = true; render();
@@ -88,6 +93,14 @@ try {
await page.evaluate(() => { test.ui.noteThread = null; test.ui.profilePk = null; test.render(); document.querySelector('#elsewhere').focus(); });
await new Promise(r => setTimeout(r, 120));
check('deferred focus does not steal focus after leaving the thread', await page.evaluate(() => document.activeElement.id === 'elsewhere'));
+ await page.evaluate(() => test.seedFace());
+ await page.reload();
+ await page.waitForSelector('#app .note-act');
+ await page.click('#app .note-act');
+ check('cached thread author paints with name and local face on first frame', await page.evaluate(() => {
+ const row = document.querySelector('.thread-page [data-zap-post]');
+ return row?.textContent.includes('Nostrich') && row.querySelector('.note-avatar.ava-img')?.style.backgroundImage.includes('data:image/png;base64');
+ }));
assert.deepEqual(errors, []);
console.log('✓ no browser errors');
} finally { await browser.close(); server.stop(true); }