aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-26 09:35:31 -0700
committerAdam Soltys <[email protected]>2026-08-26 09:35:31 -0700
commit2af7811e243ec94d340869b2b420c382fe9350f8 (patch)
treefd7ba8a1aeb8a6fb190841246206a7072f1f8e35 /src
parent250276d70ab25f60a16c85f74a672d1d4b59f8e1 (diff)
downloadcoinosv3-2af7811e243ec94d340869b2b420c382fe9350f8.tar.gz
coinosv3-2af7811e243ec94d340869b2b420c382fe9350f8.zip
The profile page steps out front: /username resolves at page load — not wallet open — and for a visitor with no account it renders as a truly public page over the front door, threads included, with wallet-only actions held back and Back leading into the app
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js67
1 files changed, 42 insertions, 25 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 5400119..ca55334 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -613,16 +613,39 @@ export function messagesFeature(ctx) {
// app — the server tries them first — and gift/invite links carry their own
// multi-segment paths, which this single-segment shape can't match. The
// path is consumed immediately so a reload lands on the wallet as usual.
- // `let`, not const: consumed on first init. Features re-init without a page
- // load (logout-and-forget → new wallet), and the link belongs to the visit
- // that carried it, not to every wallet born in the same tab.
- let urlProfile = (() => {
+ const urlProfile = (() => {
if (typeof location === 'undefined' || urlInvite) return null;
const m = location.pathname.match(/^\/([A-Za-z0-9._-]{1,64})\/?$/);
return m ? m[1] : null;
})();
if (urlProfile) { try { history.replaceState(null, '', '/'); } catch {} }
+ // Resolve and open right away — not in init(), which only runs once a
+ // wallet opens (and would replay on every wallet born in this tab). For a
+ // visitor with no wallet the profile renders as a truly public page over
+ // the unlock screen (ui.pubProf gates that surface) — nostr profiles are
+ // public data, and Back lands on the app's own front door.
+ if (urlProfile) {
+ (async () => {
+ await new Promise((r) => setTimeout(r, 0)); // never render mid-boot
+ let pk = parseNostrPubkey(urlProfile);
+ if (!pk && /^[a-z0-9._-]{1,30}$/i.test(urlProfile)) {
+ // same registrar + domain rule the names feature uses
+ const domain = getNetwork() === 'mutinynet' ? 'staging.coinos.io' : 'coinos.io';
+ const name = urlProfile.toLowerCase();
+ try {
+ const j = await fetch(`https://names.coinos.io/.well-known/nostr.json?name=${encodeURIComponent(name)}&domain=${domain}`)
+ .then((r) => r.json());
+ pk = (j.names || {})[name] || null;
+ } catch {}
+ }
+ if (pk && /^[0-9a-f]{64}$/.test(pk)) {
+ if (ui.screen !== 'wallet') ui.pubProf = true;
+ openProfile(pk);
+ }
+ })();
+ }
+
// ---- push notifications -------------------------------------------------
// The nwcpush notifier watches relays for what it can see without keys:
// kind-1059 wraps p-tagged at us (DMs, direct invites), zap receipts
@@ -1649,7 +1672,9 @@ export function messagesFeature(ctx) {
logoutBtn())
: mine
? logoutBtn() // the editor renders above once the kind 0 loads
- : h('div', { class: 'row gap6 wrap' },
+ : ui.pubProf
+ ? null // no wallet open: messaging and paying both need one
+ : h('div', { class: 'row gap6 wrap' },
h('button', { class: 'btn-primary grow', onClick: () => {
const peer = pk;
ui.profilePk = null;
@@ -1684,7 +1709,7 @@ export function messagesFeature(ctx) {
noteRow(pk, ev, name),
])));
})(),
- h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.profilePk = null; ui.profEdit = null; ui.profEditFilled = false; render(); } }, t('back')),
+ h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.profilePk = null; ui.pubProf = null; ui.profEdit = null; ui.profEditFilled = false; render(); } }, t('back')),
mine ? logoutPop() : null);
}
@@ -2342,7 +2367,16 @@ export function messagesFeature(ctx) {
unreadMessages() { return unreadCount(); },
notifySettingsCards() { return [notifyCard()]; },
screenView() {
- if (ui.screen !== 'wallet') return null;
+ if (ui.screen !== 'wallet') {
+ // The public (no-wallet) surface: a deep-linked profile, and the
+ // threads reachable from it, are public nostr content — shown
+ // without an account. Back falls through to the app's own screens.
+ if (ui.pubProf) {
+ if (ui.noteThread) return threadScreen();
+ if (ui.profilePk) return profileScreen();
+ }
+ return null;
+ }
if (ui.zapSetup) return zapSetupScreen();
if (ui.noteThread) return threadScreen();
if (ui.profilePk) return profileScreen();
@@ -2396,24 +2430,7 @@ export function messagesFeature(ctx) {
loadLinkInvite(urlInvite);
setTimeout(() => { ui.chatOpen = true; ui.msgView = 'home'; render(); }, 0);
}
- if (urlProfile) {
- const target = urlProfile;
- urlProfile = null;
- (async () => {
- let pk = parseNostrPubkey(target);
- if (!pk && /^[a-z0-9._-]{1,30}$/i.test(target)) {
- // same registrar + domain rule the names feature uses
- const domain = getNetwork() === 'mutinynet' ? 'staging.coinos.io' : 'coinos.io';
- const name = target.toLowerCase();
- try {
- const j = await fetch(`https://names.coinos.io/.well-known/nostr.json?name=${encodeURIComponent(name)}&domain=${domain}`)
- .then((r) => r.json());
- pk = (j.names || {})[name] || null;
- } catch {}
- }
- if (pk && /^[0-9a-f]{64}$/.test(pk)) openProfile(pk);
- })();
- }
+ ui.pubProf = null; // a wallet is open now — its chrome owns the profile
window.addEventListener('resize', onViewportResize);
window.visualViewport?.addEventListener('resize', onViewportResize);
allUnsubs.push(() => {