aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-11-10 11:44:52 -0700
committerAdam Soltys <[email protected]>2026-11-10 11:44:52 -0700
commit0fa2ea40ce21b642552d2d64e31bd80461cfa7ef (patch)
tree67942c9bbc058f222807a770eb13c3ced2169109
parent520aa7a1e39dbe9d8e0fd44b5cc3ce06e79f5abf (diff)
downloadcoinosv3-0fa2ea40ce21b642552d2d64e31bd80461cfa7ef.tar.gz
coinosv3-0fa2ea40ce21b642552d2d64e31bd80461cfa7ef.zip
The public chat page's button opens the chat for someone who has a wallet here, and stops telling them to sign in. "andy's having some trouble with the public chat … I can see the feed etc fine, so I am logged in" (Adam relaying Andy, 2026-11-10): /chat read "sign in to join the conversation" to a signed-in reader, and its "Open coinos" button went to the wallet, leaving him to find the chat. With an account on the device the line now reads "open the chat in Coinos to join the conversation" and the button "Join the chat", which goes to /?chat: the app takes the marker off the address and, once the wallet is open, opens the coinos community's room. A saved wallet counts as an account too (a tab opened from a link has no session yet). And /chat, /pos and /feed no longer paint the static profile shell (a card named "chat") while they load. tools/public-chat-join-test.js
Co-Authored-By: Claude Fable 5.2 <[email protected]>
-rw-r--r--build.js2
-rw-r--r--src/features/messages.js14
-rw-r--r--src/i18n.js3
-rw-r--r--src/locales/it.json3
-rw-r--r--src/public-chat.js9
-rw-r--r--tools/public-chat-join-test.js75
6 files changed, 100 insertions, 6 deletions
diff --git a/build.js b/build.js
index 85d4245..8a1b05a 100644
--- a/build.js
+++ b/build.js
@@ -450,7 +450,7 @@ function bootShell(logo, staging) {
: '<span class="beta-badge">v3 beta</span>';
return `<div class="col" style="gap:16px"><div class="row between"><div class="brand ${staging ? 'staging' : 'beta'}">${badge}<div class="logo-full" aria-label="coinos" role="img">${logo}</div></div></div><div class="card col" id="boot-shell" style="gap:12px;display:none"><div class="row gap6" style="align-items:center"><div class="chat-avatar profile-avatar fallback loading"></div><div class="chat-title" id="boot-shell-name"></div></div></div></div>`;
}
-const BOOT_SHELL_SCRIPT = `<script>try{var m=location.pathname.match(/^\\/([A-Za-z0-9._-]{1,64})\\/?$/);if(m&&!/^(note|nevent|naddr)1/i.test(m[1])){document.getElementById('boot-shell').style.display='';document.getElementById('boot-shell-name').textContent=decodeURIComponent(m[1]);}}catch(e){}</script>`;
+const BOOT_SHELL_SCRIPT = `<script>try{var m=location.pathname.match(/^\\/([A-Za-z0-9._-]{1,64})\\/?$/);if(m&&!/^(chat|pos|feed)$/.test(m[1])&&!/^(note|nevent|naddr)1/i.test(m[1])){document.getElementById('boot-shell').style.display='';document.getElementById('boot-shell-name').textContent=decodeURIComponent(m[1]);}}catch(e){}</script>`;
// COINOS_NO_SW=1: the page registers no service worker. The Android bundled
// build serves this page from its own assets, where a worker would only
diff --git a/src/features/messages.js b/src/features/messages.js
index b491a5c..592c5dc 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -2332,6 +2332,13 @@ export function messagesFeature(ctx) {
// An invite link opened in the browser lands here before the wallet exists.
const urlInvite = typeof location !== 'undefined' ? parseInviteLink(location.href) : null;
if (urlInvite) { try { history.replaceState(null, '', '/'); } catch {} }
+ // /?chat: the public chat page's button on a device that has an account.
+ // It opens the coinos community's room itself once the wallet is open
+ // (init below), where "Open coinos" used to leave a signed-in reader on
+ // the wallet to find the chat (Andy, 2026-11-10).
+ let urlChat = typeof location !== 'undefined' && !urlInvite && location.pathname === '/'
+ && /(?:^|&)chat(?:=|&|$)/.test((location.search || '').slice(1));
+ if (urlChat) { try { history.replaceState(null, '', '/'); } catch {} }
// coinos's own pack share link, /emojis/pack/<naddr> — the Android app
// owns every v3.coinos.io path, so one tapped on a phone lands here, not
// in a browser; added once a wallet is open (init below).
@@ -19130,6 +19137,13 @@ export function messagesFeature(ctx) {
loadLinkInvite(urlInvite);
setTimeout(() => { ui.chatOpen = true; ui.msgView = 'home'; render(); }, 0);
}
+ if (urlChat) {
+ urlChat = false; // once: init runs again for every wallet born in this tab
+ setTimeout(() => {
+ ui.chatOpen = true; ui.msgView = 'room'; ui.msgCommunity = COMMUNITY.community_id;
+ ui.msgChannel = null; ui.msgStick = true; render();
+ }, 0);
+ }
if (urlPack) {
addPack(urlPack).catch(() => {});
setTimeout(() => { ui.chatOpen = true; ui.msgView = 'home'; render(); }, 0);
diff --git a/src/i18n.js b/src/i18n.js
index af0bdfb..06afc11 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -709,7 +709,8 @@ const STR = {
publicChatTitle: '{name} community',
publicChatDesc: 'Live chat: read along, sign in to join the conversation',
publicChatSignIn: 'Sign in to chat',
- publicChatOpenApp: 'Open coinos',
+ publicChatDescMember: 'Live chat: open the chat in Coinos to join the conversation',
+ publicChatOpenApp: 'Join the chat',
publicChatLoading: 'Loading messages…',
msgDelete: 'Delete message',
msgNoIdentity: "Your nostr signer isn't connected. Reconnect it to send as yourself",
diff --git a/src/locales/it.json b/src/locales/it.json
index a9998c1..30b05a8 100644
--- a/src/locales/it.json
+++ b/src/locales/it.json
@@ -486,7 +486,8 @@
"publicChatTitle": "Community {name}",
"publicChatDesc": "Chat dal vivo: leggi, accedi per partecipare alla conversazione",
"publicChatSignIn": "Accedi per chattare",
- "publicChatOpenApp": "Apri coinos",
+ "publicChatDescMember": "Chat dal vivo: apri la chat in Coinos per partecipare alla conversazione",
+ "publicChatOpenApp": "Partecipa alla chat",
"publicChatLoading": "Caricamento messaggi…",
"msgDelete": "Elimina messaggio",
"msgNoIdentity": "Il tuo signer nostr non è connesso. Riconnettilo per inviare a tuo nome",
diff --git a/src/public-chat.js b/src/public-chat.js
index 9cce6f5..dd54342 100644
--- a/src/public-chat.js
+++ b/src/public-chat.js
@@ -213,7 +213,9 @@ export function mountPublicChat() {
const hasAccount = (() => {
try {
- return !!(sessionStorage.getItem('btc-wallet-accounts') || (JSON.parse(localStorage.getItem('btc-wallet-watch') || '[]')).length);
+ // a session in this tab, a saved wallet (a tab opened from a link has no session yet), or a watch-only one
+ return !!(sessionStorage.getItem('btc-wallet-accounts') || localStorage.getItem('btc-wallet-vault')
+ || (JSON.parse(localStorage.getItem('btc-wallet-watch') || '[]')).length);
} catch { return false; }
})();
@@ -306,8 +308,9 @@ export function mountPublicChat() {
h('div', { class: 'row between', style: 'align-items:center;gap:10px' },
h('div', { class: 'col', style: 'gap:2px;min-width:0' },
h('div', { class: 'chat-title' }, t('publicChatTitle', { name: COMMUNITY.name })),
- h('div', { class: 'small muted' }, t('publicChatDesc'))),
- h('button', { class: 'btn-primary btn-sm', style: 'flex-shrink:0', onClick: () => { location.href = '/'; } },
+ h('div', { class: 'small muted' }, t(hasAccount ? 'publicChatDescMember' : 'publicChatDesc'))),
+ // with an account on the device the button opens the room itself (/?chat)
+ h('button', { class: 'btn-primary btn-sm', style: 'flex-shrink:0', onClick: () => { location.href = hasAccount ? '/?chat' : '/'; } },
hasAccount ? t('publicChatOpenApp') : t('publicChatSignIn'))),
tabs),
log));
diff --git a/tools/public-chat-join-test.js b/tools/public-chat-join-test.js
new file mode 100644
index 0000000..3f0eee4
--- /dev/null
+++ b/tools/public-chat-join-test.js
@@ -0,0 +1,75 @@
+// The public chat page (/chat) on a device that already has a wallet: its
+// button opens the coinos community's room itself, not the wallet, and the
+// page doesn't tell a signed-in reader to sign in. And /chat's loading shell
+// is not a profile named "chat". (Andy, 2026-11-10: "I can see the feed etc
+// fine, so I am logged in".) Relays are the stand-ins: nothing is sent.
+//
+// Run: bun tools/public-chat-join-test.js
+import puppeteer from './puppeteer.js';
+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 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: 5247, 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();
+await page.setViewport({ width: 390, height: 844 });
+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 waitFor = async (fn, ms = 15000, ...a) => { for (let i = 0; i < ms / 200; i++) { try { if (await page.evaluate(fn, ...a)) return true; } catch {} await sleep(200); } return false; };
+// did the static profile shell ever show while the page loaded?
+await page.evaluateOnNewDocument(() => {
+ window.__shellShown = false;
+ const look = () => { const s = document.getElementById('boot-shell'); if (s && s.style.display !== 'none') window.__shellShown = true; };
+ new MutationObserver(look).observe(document, { childList: true, subtree: true, attributes: true });
+ document.addEventListener('DOMContentLoaded', look);
+});
+const head = () => page.evaluate(() => {
+ const c = document.querySelector('.chat-head');
+ return c ? { text: c.innerText, button: c.querySelector('button')?.textContent.trim() } : null;
+});
+
+try {
+ console.log('\n[a visitor]');
+ await page.goto('http://localhost:5247/chat', { waitUntil: 'domcontentloaded' });
+ await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await waitFor(() => !!document.querySelector('.chat-head'));
+ let hd = await head();
+ check('the public chat mounts', !!hd);
+ check('a visitor is asked to sign in', hd?.button === 'Sign in to chat' && /sign in to join/i.test(hd.text), hd?.button);
+ check("/chat's loading shell is not a profile", !(await page.evaluate(() => window.__shellShown)));
+ await page.goto('http://localhost:5247/someone', { waitUntil: 'domcontentloaded' });
+ check('a name still gets its profile shell', await page.evaluate(() => window.__shellShown));
+
+ console.log('\n[with a wallet on the device]');
+ await page.goto('http://localhost:5247/', { waitUntil: 'domcontentloaded' });
+ await sleep(400);
+ await click('button', 'Create a new wallet');
+ await sleep(300);
+ await click('button', 'Have an existing seed');
+ await sleep(300);
+ await page.waitForSelector('textarea');
+ await page.type('textarea', generateMnemonic(wordlist));
+ await click('button', 'Open wallet');
+ check('wallet opens', await waitFor(() => /receive/i.test(document.body.innerText)));
+ await page.goto('http://localhost:5247/chat', { waitUntil: 'domcontentloaded' });
+ await waitFor(() => !!document.querySelector('.chat-head'));
+ hd = await head();
+ check('no "sign in" for someone signed in', !!hd && !/sign in/i.test(hd.text), hd?.text.replace(/\n/g, ' | '));
+ check('the button offers the chat', hd?.button === 'Join the chat', hd?.button);
+ await Promise.all([page.waitForNavigation({ waitUntil: 'domcontentloaded' }), click('.chat-head button', 'Join the chat')]);
+ const inRoom = await waitFor(() => !!document.querySelector('.chat-compose textarea, .chat-compose input, .chat-compose [contenteditable]'), 20000);
+ check('it lands in the community room, composer ready', inRoom, await page.evaluate(() => document.body.innerText.slice(0, 120).replace(/\n/g, ' | ')));
+ check('the room is the coinos community', await page.evaluate(() => /coinos/i.test(document.querySelector('.chat-head, .chat-title')?.innerText || '')));
+ check('the marker is gone from the address', await page.evaluate(() => location.pathname === '/' && !location.search));
+} finally {
+ await browser.close();
+ server.stop(true);
+}
+console.log(ok ? '\nPASS' : '\nFAIL');
+process.exit(ok ? 0 : 1);