aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-03 23:31:55 -0700
committerAdam Soltys <[email protected]>2026-08-03 23:31:55 -0700
commit3bc4bf2cb95c1bcf61a10d8b892ee9ccaeffcf2d (patch)
treed4c2d34c46246a6c6ae11ec3678f3296dad90e37
parentbd215cbd2e6f46870cb69729673c3e5218f4ee86 (diff)
downloadcoinosv3-3bc4bf2cb95c1bcf61a10d8b892ee9ccaeffcf2d.tar.gz
coinosv3-3bc4bf2cb95c1bcf61a10d8b892ee9ccaeffcf2d.zip
The unread dot holds still
It looked like the dot was flickering on and off after a refresh. It wasn't: measured over the first twelve seconds it appeared once and never went away, while its entrance animation started seven separate times. render() rebuilds the whole DOM, so the pop replayed on every repaint. A dot that means 'someone is waiting' has nothing to animate about, so the animation is gone rather than gated. tools/unread-dot-test.js counts animation starts against real appear/disappear transitions, so a future entrance effect here fails instead of shipping as a flicker. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
-rw-r--r--src/style.css6
-rw-r--r--tools/unread-dot-test.js77
2 files changed, 81 insertions, 2 deletions
diff --git a/src/style.css b/src/style.css
index 5d98aa1..ea29891 100644
--- a/src/style.css
+++ b/src/style.css
@@ -428,9 +428,11 @@ hr.divider { margin: 16px 0; }
.header-msgs.unread::after {
content: ''; position: absolute; top: 2px; right: 2px; width: 9px; height: 9px;
border-radius: 50%; background: var(--red);
- box-shadow: 0 0 0 2px var(--bg); animation: dot-pop .3s ease-out;
+ /* No entrance animation: render() rebuilds the whole DOM, so a pop here
+ replayed on every repaint and read as the dot flashing. It either says
+ someone is waiting or it doesn't. */
+ box-shadow: 0 0 0 2px var(--bg);
}
-@keyframes dot-pop { from { transform: scale(0); } to { transform: scale(1); } }
.chat-avatar.header-ava { width: 30px; height: 30px; font-size: 11px; }
.npub-box { display: flex; align-items: center; gap: 8px; }
.copy-ico { flex-shrink: 0; background: none; border: none; padding: 2px; color: var(--muted); cursor: pointer; display: flex; }
diff --git a/tools/unread-dot-test.js b/tools/unread-dot-test.js
new file mode 100644
index 0000000..76e3417
--- /dev/null
+++ b/tools/unread-dot-test.js
@@ -0,0 +1,77 @@
+// The unread dot either says someone is waiting or it doesn't — it must not
+// flash on the way in. render() rebuilds the whole DOM, so any entrance
+// animation on the dot replays on every repaint; an earlier build popped it
+// seven times in the first seconds after a refresh, which reads as flashing.
+//
+// Run: bun tools/unread-dot-test.js
+import puppeteer from 'puppeteer-core';
+import { generateMnemonic } from '@scure/bip39';
+import { wordlist } from '@scure/bip39/wordlists/english';
+import { buildHtml } from '../build.js';
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+let ok = true;
+const html = await buildHtml({ minify: true, pwa: false });
+const server = Bun.serve({ port: 5233, 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 = (sel, t) => page.evaluate((s, x) => { const e = [...document.querySelectorAll(s)].find((n) => n.textContent.trim().toLowerCase().includes(x.toLowerCase())); if (e) { e.click(); return true; } return false; }, sel, t);
+const waitText = async (x, ms = 25000) => { for (let i = 0; i < ms / 250; i++) { if ((await page.evaluate(() => document.body.innerText)).toLowerCase().includes(x.toLowerCase())) return true; await sleep(250); } return false; };
+
+try {
+ await page.goto('http://localhost:5233/', { waitUntil: 'domcontentloaded' });
+ await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await sleep(400);
+ await click('button', 'I already have a wallet');
+ await sleep(300);
+ await click('button', 'Import existing');
+ await sleep(300);
+ await page.waitForSelector('textarea');
+ await page.type('textarea', generateMnemonic(wordlist));
+ await click('button', 'Open wallet');
+ await waitText('receive', 15000);
+ await sleep(10000); // let the coinos room load so there IS unread
+
+ // Now the refresh the report is about.
+ await page.evaluate(() => {
+ window.__dot = { anims: 0, appear: 0, disappear: 0, renders: 0 };
+ addEventListener('animationstart', (e) => { if (e.target && e.target.classList && e.target.classList.contains('header-msgs')) window.__dot.anims++; }, true);
+ let had = null;
+ new MutationObserver(() => {
+ window.__dot.renders++;
+ const now = !!document.querySelector('.header-msgs.unread');
+ if (had !== null && now !== had) { if (now) window.__dot.appear++; else window.__dot.disappear++; }
+ had = now;
+ }).observe(document.getElementById('app'), { childList: true, subtree: true });
+ });
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ // re-arm after the reload wipes it
+ await page.evaluate(() => {
+ window.__dot = { anims: 0, appear: 0, disappear: 0, renders: 0 };
+ addEventListener('animationstart', (e) => { if (e.target && e.target.classList && e.target.classList.contains('header-msgs')) window.__dot.anims++; }, true);
+ let had = null;
+ new MutationObserver(() => {
+ window.__dot.renders++;
+ const now = !!document.querySelector('.header-msgs.unread');
+ if (had !== null && now !== had) { if (now) window.__dot.appear++; else window.__dot.disappear++; }
+ had = now;
+ }).observe(document.body, { childList: true, subtree: true });
+ });
+ await sleep(12000);
+ const r = await page.evaluate(() => ({ ...window.__dot, dotNow: !!document.querySelector('.header-msgs.unread') }));
+ console.log('over 12s after a refresh:');
+ const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
+ check('the dot never animates', r.anims === 0, `${r.anims} animation(s) started`);
+ check('it settles once and stays', r.appear <= 1 && r.disappear === 0, `+${r.appear} / -${r.disappear}`);
+ check('and the repaints it sat through were real', r.renders > 1, `${r.renders} DOM mutations`);
+ check('it is showing (there is unread history)', r.dotNow);
+} catch (e) {
+ console.log(' ✗ run completed —', e.message);
+ ok = false;
+} finally {
+ await browser.close();
+ server.stop(true);
+}
+console.log(ok ? '\n✅ the dot holds still' : '\n❌ failures above');
+process.exit(ok ? 0 : 1);