diff options
| author | Adam Soltys <[email protected]> | 2026-09-11 22:07:52 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-11 22:07:52 -0700 |
| commit | f2bc0f7d1a5098a87100d2401231ff97cd4ac626 (patch) | |
| tree | 7bd97220e3e58f2837e253fc6b1f204b0bf8e930 /tools | |
| parent | 8beb1c17df8d1c96a43803c0d87a5c5f1ac2494e (diff) | |
| download | coinosv3-f2bc0f7d1a5098a87100d2401231ff97cd4ac626.tar.gz coinosv3-f2bc0f7d1a5098a87100d2401231ff97cd4ac626.zip | |
Follow people, read a feed of them, and put pictures in a post.
FOLLOWING. A profile page gains Follow / Following beside DM and Pay. The
contact list is the kind-3 event, kept locally so the button is right in the
first frame and refreshed from relays behind it. Publishing one is delicate:
a contact list is a shared document — other clients keep relay hints in its
content and petnames in its tags — so a follow re-fetches the newest list
first and adds or removes exactly one p tag, preserving everything else. The
button flips immediately and flips back if the publish fails. The service
worker's friend filter reads the same list, so following someone also means
their DMs stop being treated as a stranger's.
THE FEED. Posts by the people you follow, newest first, at the top of the
messages home. Replies are left out: a reply belongs to its thread, and a
timeline of half-conversations reads like eavesdropping. The last screenful
is kept locally so it opens with posts in it rather than a spinner, new posts
arrive over a live subscription while you're looking at it, and older ones
stream in as you reach the bottom — the same shape as the profile pages. A
wallet that follows nobody is told so and offered the people search.
MEDIA. The composer (now shared by the feed and the profile page) takes a
picture or a video: it uploads when you pick it and appends the URL to the
text, which is how every nostr client reads media, with a NIP-92 imeta tag
published beside it for the ones that would rather be told than guess. Posts
already render images and video inline, so they show up where they land.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/feed-follow-test.js | 70 |
1 files changed, 70 insertions, 0 deletions
diff --git a/tools/feed-follow-test.js b/tools/feed-follow-test.js new file mode 100644 index 0000000..f665341 --- /dev/null +++ b/tools/feed-follow-test.js @@ -0,0 +1,70 @@ +// Follows and the feed: the Follow button reflects the contact list, the feed +// reads the people on it, and a brand-new wallet is told what to do instead +// of being shown an empty box. +// +// Nothing here publishes to a relay — the contact list is seeded locally, the +// way it would arrive from one. +// +// Run: bun tools/feed-follow-test.js +import puppeteer from 'puppeteer-core'; +import { generateMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english'; +import { cacheKeyFor } from '../src/wallet.js'; +import { npubEncode } from 'nostr-tools/nip19'; +import { buildHtml } from '../build.js'; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +let ok = true; +const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; }; +const html = await buildHtml({ minify: true, pwa: false }); +const server = Bun.serve({ port: 5237, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) }); +const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); +const page = await browser.newPage(); +const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t); +const clickItem = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('.item, button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t); +const text = () => page.evaluate(() => document.body.innerText); +const waitText = async (x, ms = 25000) => { for (let i = 0; i < ms / 250; i++) { if ((await text()).toLowerCase().includes(x)) return true; await sleep(250); } return false; }; +const FOLLOWED = 'e'.repeat(63) + '1'; +try { + await page.setViewport({ width: 390, height: 844 }); + await page.goto('http://localhost:5237/', { waitUntil: 'domcontentloaded' }); + await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest')); + await page.reload({ waitUntil: 'domcontentloaded' }); + await sleep(400); + await click('get started'); await sleep(500); + await click('import existing'); await sleep(400); + await page.waitForSelector('textarea'); + const mnemonic = generateMnemonic(wordlist); + await page.type('textarea', mnemonic); + await click('open wallet'); + await waitText('receive', 20000); + const base = cacheKeyFor(mnemonic + '\n'); + + // chat → the feed is offered before the conversations + await page.evaluate(() => { const b = [...document.querySelectorAll('button')].find((e) => /message/i.test(e.getAttribute('aria-label') || '')); if (b) b.click(); }); + await sleep(1200); + check('the feed is on the messages home', /follow people to fill this/i.test(await text()), (await text()).split('\n').slice(0, 6).join(' | ')); + + await clickItem('feed'); + await sleep(1000); + check('a wallet that follows nobody is told so', /don't follow anyone yet/i.test(await text())); + check('...and offered somewhere to start', /find people/i.test(await text())); + + // a contact list, as a relay would hand it over + await page.evaluate(([k, pk]) => { + localStorage.setItem(k + ':follows', JSON.stringify({ tags: [['p', pk]], c: '', at: Math.floor(Date.now() / 1000) })); + }, [base, FOLLOWED]); + await page.reload({ waitUntil: 'domcontentloaded' }); + await waitText('receive', 20000); + await page.evaluate(() => { const b = [...document.querySelectorAll('button')].find((e) => /message/i.test(e.getAttribute('aria-label') || '')); if (b) b.click(); }); + await sleep(1200); + check('the follow count shows on the messages home', /posts from the one person you follow/i.test(await text()), (await text()).match(/Posts from[^\n]*/)?.[0] || 'no line'); + + // and their own profile page knows we follow them + await page.goto('http://localhost:5237/' + npubEncode(FOLLOWED), { waitUntil: 'domcontentloaded' }); + await sleep(6000); + const btns = await page.evaluate(() => [...document.querySelectorAll('button')].map((b) => b.textContent.trim())); + check('their profile offers to unfollow, not to follow', btns.includes('Following') && !btns.includes('Follow'), + btns.filter((b) => /follow/i.test(b)).join(',') || 'no follow button'); +} finally { await browser.close(); server.stop(true); } +console.log(ok ? '\n✅ follows and feed' : '\n❌ failed'); +process.exit(ok ? 0 : 1); |
