| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Our set is three relays — relay.coinos.io, nos.lol, relay.primal.net — plus
whatever the user's own NIP-65 names. Asking those for everyone's posts only
finds the people who happen to use them; anyone else looks like they stopped
posting.
So the feed now reads NIP-65: relay lists are fetched in batches of a hundred,
kept for a week, and the authors covered greedily — the relay carrying the
most people not yet covered, repeated up to ten sockets. A feed of hundreds
is a handful of connections that between them reach everyone, rather than one
each.
Our relays are still asked about everyone, not just the people with no list.
Measured across eight busy accounts, each source holds posts the other misses:
reading the author's own relays surfaced 7 to 40 notes ours had never seen
(jb55 +40, verbiricha +25, pablo +23), and ours held plenty theirs didn't —
fiatjaf's own three relays answered 42 where ours answered 67. The feed wants
the union, so the outbox adds reach rather than replacing it. On the test set
that took the feed from 83 posts to 118.
Profile pages get it for free: relaysOf now reads the batch's cache first, so
opening someone you follow costs no extra round trip.
The test records every socket the page opens and checks the plan by what it
actually dialled.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| | |
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Pinguimdarelva's avatar still flashed, and the reason was one I'd have kept
guessing at: from the live browser, `new Image()` with crossOrigin set FAILS
for that picture, while `fetch(url, {mode:'cors', cache:'reload'})` for the
very same URL returns 200. The host allows us perfectly well.
The picture sits behind a 302, and the avatar is painted as an ordinary
background-image — so the bytes are already in the HTTP cache under the
redirect's target as a no-CORS entry, and the crossOrigin <img> reuses that
entry and fails the CORS check. The image loads, the CORS copy "errors", and
the face gets written off.
So the thumbnailer fetches the bytes itself and draws from a blob: no
crossOrigin element, no cache collision, and an untainted canvas without
depending on the element's CORS bookkeeping. With no taint left to detect,
failures no longer split into hard and soft — each one just backs off, from
six hours up to a fortnight, so a host that truly won't have us is asked
twice a month instead of every boot.
The test now serves a picture behind a redirect and loads it plainly before
thumbnailing it, which is the shape that broke; it fails on the old code.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Pinguimdarelva's picture is 52KB on a CDN that answers in 700ms one minute
and times out the next — I measured both from this machine. The thumbnail
attempt hit one of the bad minutes, and `thumbFail` was written as if the
host had refused us: never retried, so that avatar went back to fetching the
original on every boot.
Only a tainted canvas is a real refusal (no CORS header, nothing to be done),
and that is remembered for good. Everything else — a load error, or a host
that won't answer inside twenty seconds — is stamped with the time and tried
again in six hours. The timeout also frees the slot: three thumbnails run at
a time, and a hanging image used to hold one of them indefinitely.
And zap tallies are remembered between sessions. They were asked of the
relays on every boot, which takes seconds, so a room full of zapped messages
painted bare and filled in afterwards. The totals (never the receipts) are
kept locally and painted straight away; the moment the relays answer for a
message, their word replaces it — so a tally that changed, or a receipt that
was deleted, still settles correctly. Opening a DM thread doesn't forget what
the chat had learned, and the store is capped at 400.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The avatar delay left on the messages page wasn't the image at all. A person
with no cached profile got an empty circle until a relay answered — and the
one that stayed blank longest (npub1k6f89cg, in the live cache) is never
cached at all: persistProfile deliberately refuses to write a row with no
name and no picture, so every boot re-asked the relays and every boot sat
blank for seconds. When it finally arrived it was a punk, which is why it
looked like slow art rather than a slow lookup.
The punk is derived from the pubkey alone, so it can be drawn in the first
frame. It's already the right answer for everyone with no picture, and for a
coinos user who kept the default it IS their published picture — same
formula, same file. A real photo replaces it when it lands. A name lookup in
flight still gets the quiet circle: a picture is expected there, and punk art
must not flash in front of it.
Also: the punk rewrite now recognises the bare relative path a few older
profiles carry, not just the absolute URL.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The art is 240x240 (20KB each, 1.5MB for the set) because that's what a punk
published as someone's nostr picture should be — but the circles the app
draws are 18 to 64px, so a new wallet with nothing cached was fetching eight
times the pixels it could show, one request per face.
punks-sm carries a 128px copy of each: 4KB instead of 20KB, indistinguishable
at 64px. Small circles and the wizard's picker tiles load those; only the 64px
profile avatar and the value the wizard publishes stay full-size.
The bigger win is that a punk picture is our own art wherever it turns up.
Everyone who keeps the default publishes v3.coinos.io/punks/N.webp as their
nostr picture, so those faces now paint straight off disk at the size being
drawn — no request, no thumbnail to make, and no broken circle when the
legacy coinos.io copies 502 (which they do for a third of the set).
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A profile picture is whatever its owner uploaded, and that is very often the
full-size original. Measured against the faces this wallet had cached: one
avatar is a 4,988,208-byte JPEG, another 3,901,966 — megabytes fetched and
decoded to paint a 30px circle. That is the second of blank circles on the
messages page after a refresh, and it repeats every time the upstream cache
header expires.
So the first time we see a picture we downscale it once (96px, webp) and keep
the thumbnail beside the profile in localStorage, which is read synchronously
at boot. Small avatars then paint from it alone and never ask the network for
the original again; the 64px profile avatar layers them, so the thumbnail
shows instantly while the full-size original loads over the top.
Reading the pixels needs CORS — most avatar hosts allow it (6 of the 9 here),
and the ones that don't are remembered as failures rather than retried every
boot. A refresh of the profile carries the thumbnail across when the picture
hasn't changed. Thumbnails live on a 180k budget, oldest face first, and Data
Saver opts out of making them at all.
tools/avatar-thumb-test.js watches a wallet meet a large cross-origin picture
and checks the next boot fetches no picture at all.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The optimistic amount is supposed to stand down when the real receipt is
counted, and it did — but only by deleting itself. An ark zap publishes its
own receipt, which comes back over the live subscription BEFORE the send
call resolves, so the flow's success report arrived second and re-created
the amount on top of the receipt already in the total. 21 paid, 42 shown.
The pending entry now has a state (flying / paid / void) instead of a
deletion: our receipt voids it, and a settle arriving afterwards can't
resurrect it. A receipt older than the tap — the backlog the first paint
fetches for a message zapped before — doesn't void anything, so zapping the
same message twice still counts both.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A zap took a few seconds to pay and its public receipt trailed that by
seconds more, so the tally chip appeared long after the tap — the tap looked
ignored, and the only way to tell it had worked was the toast.
Now the chip appears on the tap carrying your amount, breathing while the
payment is in flight; it goes solid the moment the payment lands, and steps
aside when the real receipt arrives (which is also how it avoids counting
your zap twice). The ark and Lightning one-tap flows report their outcome
back — a failure, or a fallback into the form, takes the chip straight off
again rather than leaving it pulsing. An unreported zap gives up after 45s.
A zap sent from the form reports too, so the tally shows it right away
instead of waiting on a relay. Reduced-motion gets a dimmed chip, no pulse.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Chat and DM message sheets grow a ⚡ Zap item (not on your own messages),
riding the same one-tap flow as a post zap — Ark first, Lightning fallback,
the amount remembered from the first time. A chat message is a rumor with an
id like any event, so the zap e-tags that id; the public receipt reveals
nothing but the id.
Every post row, chat bubble and DM shows how much it's been zapped as a
little amber bolt + sats chip (1.2k / 21k / 1.5M), fed by the public receipts
that e-tag it: NIP-57 kind 9735 (amount from the bolt11, the zap request's
amount as fallback) and coinos' own Ark receipt, kind 9737 (network-checked).
Summed per id, deduped by receipt; underlined when one of ours is in it.
Ids are asked once each (batched per paint, 150 per REQ), and ONE live
subscription follows the ~200 ids most recently on screen so a zap landing
while you watch — yours included, rechecked at 4/12/30s — shows up without
a reload. The tally chip is itself a zap button on others' messages.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01URmfo8VimCNkw6Y7JndYrv
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The Accounts screen is about a seed's Spending/Savings faces — its rows say
nothing about WHO each pair belongs to, so a "Sign into another account"
button there was the wrong door. It's gone from there. On your own profile,
"Sign into another account" now opens a switcher: every identity already
signed in on this device as an avatar + name row (one per seed, wearing the
nostr key its header avatar shows — the login key when there is one, else
the seed's own; remembered on the account and in the durable directory by
xpub, so vault-restored accounts have a face too), tap to switch with nothing
logged out, plus "Sign into a new account" for the additive sign-in doors.
With no other identity on the device the button goes straight to those doors.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01URmfo8VimCNkw6Y7JndYrv
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The additive sign-in door lived only at the foot of the Accounts screen; the
profile page — where people actually go looking for a way out of the current
identity — offered just Log out. Now it carries the same button, via a shared
signInAnother action on the feature ctx (clears the profile/chat state so the
unlock screen's Back lands on the wallet, and the new identity doesn't open
onto the old identity's profile).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01URmfo8VimCNkw6Y7JndYrv
|
| |
|
|
|
|
|
|
|
|
|
| |
The new-post composer held its text in a transient ui field, so the reload
that reconnects a signer (or any reload) lost the post. It now rides the
same persisted draft store under a 'post' key: the composer reopens with
the text after a reload, posting clears it, Cancel closes the composer and
keeps the text for next time.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
field), not a toast.
A pasted-nsec login is never stored, so a reload leaves the link without
a signer. The post, reply and profile-edit paths checked for an identity
by THROWING the "signer isn't connected" message, which the composer then
caught and toasted — bypassing noIdToast, whose whole job is to open the
reconnect screen where the nsec (or extension / bunker) can be re-entered.
Those paths now go through requireIdentity(): it opens the screen (or
toasts, when there's no link to reconnect) and throws a silent error the
callers don't repeat.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
relay-only sync records.
A remote-signer (bunker) wallet re-opened the relay's last ~400 gift wraps
on EVERY boot — the seen-set lived in memory only — and each wrap costs two
NIP-46 round trips with a schnorr sign+verify apiece on the client. On the
Pixel that was ~4s of a 15s pegged boot. Opened (and given-up) wrap ids are
now kept in the messages feature state, bounded to 1500 id prefixes, and
loaded before the inbox subscription.
The outbox also replaces a record whose route changed even when its digest
matches: the 874KB relay-only ark record that could never publish would
otherwise have stayed in front of its Blossom successor forever.
Verified live: the wallet's 633KB ark domain uploaded to blossom.coinos.io
and its pointer was acknowledged by relay.coinos.io on the next boot.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
|
| |
|
|
|
|
|
|
|
| |
A wallet has two nostr keys — the seed-derived key and, when signed in, the login identity (extension/bunker/passkey/Google). Sending already used one identity, but RECEIVING/advertising used both: startDMs subscribed kind-1059 gift wraps on '#p' for both keys and fetched both keys' inbox relays, and ensureDmRelayList always published a kind-10050 inbox for the seed key even under a login. Result: a login wallet advertised the seed key as a SECOND DM inbox, so the welcomer greeted both keys (duplicate welcome) and a person's DMs split across two identities — one showed up as if from "another user".
Collapse to a single DM identity: new dmSubKeys() returns just the login pubkey when signed in, else the seed key, and startDMs subscribes/fetches only that; ensureDmRelayList no longer advertises the seed key when a login identity is active. isMe() and dmDecryptors() stay tolerant of both keys so already-received messages still thread and align. Not a privacy fix — nothing ever leaked to a third party (both keys were the user's own); this removes the confusing second self-identity going forward. (Existing cached DMs to the seed key on a given device don't retro-clean; a chat re-sync/forget clears them.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
Reactions were a sibling of the bubble (a separate row under it), so they hung outside the message. Moved the reaction pills inside .chat-bubble, tucked under the text, across all three renderers: community channel messages, DMs, and the read-only public /chat page. The pill background and the "your reaction" highlight border now derive from the bubble's own text colour (color-mix on currentColor), so they read correctly on both the plain bubble and the accent "me" bubble. Tapping a pill stops propagation so it toggles the reaction without also opening the message action sheet.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
notifications went silent. registerPush reused whatever getSubscription() returned and only ever subscribed fresh when there was none; a subscription made against an OLD VAPID key can never receive a push (the notifier's send fails with a permanent 403, which was never pruned, and the client kept re-registering the same dead endpoint). Now the client fetches the current VAPID key first and, when an existing subscription's applicationServerKey doesn't match, unsubscribes and re-subscribes against the current key — so a device self-heals on next open. And the notifier prunes 403 endpoints alongside 404/410. Deployed nwcpush on cs
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
stops opening the staging PWA. The same seed derives the same nostr pubkey on mainnet and staging, so both PWAs registered push under one pubkey and the notifier — matching only by pubkey — fanned every mainnet payment out to the staging subscription too; tapping it opened staging.coinos.io. Now the client stamps its network on the push registration (pushWatch net: getNetwork()), the registrar declares its own network on the /notify call (CFG.forwarder.network), and nwcpush delivers a payment only to registrations whose net matches — with a rollout-safe fallback: a device that hasn't re-registered yet (net=null) still gets the push, and a caller that omits net still reaches everyone, so nobody goes dark mid-deploy. Deployed names + nwcpush on cs
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
community rooms and DM threads (now sharing a chat-page class) drop their card walls and run edge to edge under 520px — and the type steps up a notch: bubbles 14→15px, names 13→14px, timestamps 11→12px. Plus: a fixed-price LNURL (minSendable == maxSendable — bolt-card top-ups, paywalls, static charges) no longer prompts for an amount — resolve() fills it and locks the field via the same fixedSat machinery fixed-amount BOLT 12 offers already use (disabled input, adjusted hints, confirm bypass)
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
client tag — ['client', 'coinos'], name-only since no kind-31990 handler coordinate is published yet — so other clients show "via coinos". Deliberately absent from everything encrypted (DMs, community wraps): private traffic needs no billboard
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
chunk, so every boot painted bare heads until it arrived — but the core bundle can't carry the hat art. Instead, wrapAvatar now leaves a STAMP per head in localStorage (art + percent geometry, size-independent, capped at 40, cleared when a head goes bare so removals can't ghost), and the core avatar factory replays stamps until hatsReady answers — from then on a null from wrapAvatar genuinely means bare. Verified headless: a stamped hat is on the header avatar 15ms after DOM-ready, and once the chunk lands with a bare-head answer the stamp is cleared with no double-wrap
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
sits INLINE, directly under the note it answers, instead of at the bottom of the thread where nobody scrolled to find it — slotted after the focused note (the root by default), appended at the end only until the focused note loads, with a refocus handshake so the cursor survives the box relocating when the thread finishes loading. (2) Posting is OPTIMISTIC: a provisional dimmed copy tops the feed before any signing or relay round-trip (a remote signer alone can take seconds), swapped for the signed event on success and withdrawn on failure — the composer closes immediately and reopens with the text intact if publishing fails; pending rows can't be opened/replied/zapped until real. (3) The full header nav (search, chat, settings, lock, avatar) now shows on the profile, thread, and search screens whenever a wallet is open — losing the search button on a profile made finding the next person a trek back home; the public no-wallet surface keeps the plain brand. All verified headless: box renders mid-card under the focused note with focus landing after load, a post appears 250ms after tapping Post (pre-publish) and survives settle, and the search button works from a profile
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
the post's thread focused on that note and puts the cursor straight in the reply box (inside an already-open thread it just re-targets the reply to that message). Verified headless on /adam: 51 rows all carry the button, tapping one lands with the thread reply input focused
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
pulls the next page of older kind-1s — until = oldest seen, one fetch in flight, a short answer marks the end; the listener registers at feature construction so the public no-wallet /username pages scroll too), VIDEO rendering in noteBody (mp4/webm/mov/m4v as <video controls preload=metadata playsinline> beside the existing inline images), and FULL-BLEED on phones — the notes card drops its walls and runs edge to edge under 520px; the inner 440px scrollbox is gone everywhere, the page itself scrolls. Verified headless at phone width on /adam: feed spans 0px-to-edge both sides, images render inline, and scrolling grew the list 51 → 82 posts
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
Delete — a gift-wrapped DM can't be retracted from the peer's relays). DM reactions ride the kind-7-in-a-gift-wrap shape 0xchat uses (makeDMReaction in dm.js), sent to the peer and echoed to our own inbox so other devices fold them in; inbound kind-7 rumors land in dmReacts keyed by the reacted message's id — no thread lookup, so a reaction arriving before its message still sticks. Replies e-tag the quoted kind-14 (makeDMRumor grew extraTags) and render the same accent-bar quote as community chat; the composer gets the same replying-to bar. Chips are joinable and your own reaction is highlighted/replaceable, all optimistic with rollback. Round-trip verified: reply and reaction rumors wrap/unwrap with tags intact
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
profile page immediately, not after Save: while the editor is open the DRAFT is the truth for the banner, the avatar, and the display name shown up top — clearing a field previews the removal too. The editor's text fields render per keystroke so pasted URLs track live (the morph never rewrites a focused field, so it can't fight the typing); uploads already rendered after filling the field. Verified headless: typing picture/cover URLs into the edit form updates the page-top avatar and banner backgrounds before any save
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
— it was hidden on your own page), bio, and latest posts, exactly like anyone else's, with the editor behind an explicit Edit profile button (Save or the new Cancel returns to the view) and a New post button that unfolds a composer — publishPost signs a plain kind-1 with the login/wallet identity, publishes to the identity's own relays, and prepends it to the notes list so it shows immediately. The header avatar's tooltip says "Your profile" instead of "Edit profile" to match. Compose/edit state clears on open/back so drafts never leak across profiles. Verified headless end-to-end: onboard → avatar → view mode (buttons + npub, no form) → Edit opens form → Cancel → New post opens composer
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
bubble opens a centered sheet: a quick row of the six reaction emojis (your current one highlighted; picking fires immediately) above Reply / Copy / Delete (delete on own messages only) — replacing the hover-only 🙂 picker, which was invisible on the phones that need it most. Links, images and the hover × keep their own clicks, and a desktop text-selection drag never pops the sheet over the copy. REPLIES are new: Reply arms a composer bar showing who/what you're answering (× to cancel, cleared on channel/room switch), the sent kind-9 e-tags the quoted message, and rows carrying an e-tag render the quoted context above their text — author + one-line snippet, silent when the original is deleted or not held. The public /chat page renders the same quotes. Verified against the built stylesheet: 300px sheet, six even emoji buttons, quote ellipsizes, circular cancel button
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
DM bubbles now run through noteBody (URLs → anchors, image URLs inline, npub mentions clickable, all content kept as text nodes — nothing from a relay reaches innerHTML); the public /chat page gets its own linkedBody (same splitter, nostr refs stay dim stubs since there are no profile screens there). Reactions: a 🙂 opener on every bubble (rides the edge like the delete ×, steps left on own rows) unfolds a six-emoji picker; tapping an existing chip joins that vote, your own reaction is highlighted, and picking a different emoji replaces it (kind-7 rumor on the channel stream, one per author, fold keeps the latest — inbound rendering already existed, this adds the sending half). Optimistic like sendMessage: the chip lands instantly and is rolled back to whatever stood before if signing/wrapping fails
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
that shows the live chat history to anyone, logged in or not, with a "Sign in to chat" button ("Open coinos" when the device already has an account). No server-side decoder needed: the community's join material was already public by construction (baked into the client bundle — every coinos user is meant to be a member), so the page derives the stream keys and decrypts wraps client-side exactly like the app's Chat tab, worker-offloaded with the same sliced fallback. Renders messages + edits + deletes + reactions and honors the folded control state's ban list; presence/typing/guestbook deliberately omitted. Profiles come from ONE batched kind-0 query per burst (a REQ per author tripped relay concurrency limits). Plumbing: the join material moved to src/community.js (shared with messages.js so they can never drift), /chat is excluded from the single-segment profile deep-link (which was eating the path and rewriting the URL before the route ran), and app renders stand down in public-chat mode so a feature-init render can't paint onboarding over the page. Verified headless against the live relays: 59 messages decrypt, ban-fold applies, names resolve
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
after verification moved off (chunk-60dfb4nt in profiles). The verify worker grows two ops: 'openWraps' opens community (Concord) wraps given the symmetric stream conversation key — nip44 decrypt + the seal's schnorr verify + hash checks per wrap, no secret crossing the thread boundary since stream keys are group material; 'unwrapDMs' runs the full NIP-17 unwrap (two ECDH ops + seal verify) when the wallet holds the raw identity key. Remote signers can't export their key, so their DM path stays on the main thread, exactly as before. The time-sliced main-thread queues survive as fallbacks — a dead or stalled worker only changes WHERE the crypto runs (workerCrypto resolves null and callers re-enqueue), never whether chat catches up. Bonus: subChannel derives its stream key once per channel instead of once per wrap (groupKey does hkdf + ECDH per call — real curve work on a 200-wrap backfill)
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
|
| |
|
|
|
|
|
| |
every community room up front (500 control + 500 guestbook + channels each — the bulk of the ~560ms of boot secp256k1 behind the carousel stutter). Now boot builds rooms from the decrypted local cache ONLY (subscribe:false), so the unread dot still reads last-known state, and the live backfill's verify/decrypt waits until the user actually opens Chat (homeView/room view subscribe then). ensureRoom split into cache-build + subscribeRoom
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
the exact cold first-drag window showed ~700ms of secp256k1, and it was the COMMUNITY room's gift-wrap decryption — a relay backlog of 500+ wraps decrypted inline in the subscription callbacks, one synchronous EC burst on boot (the DM inbox already yielded; community rooms didn't). Now their decrypt work is time-sliced through the same yield-every-8ms queue, so it spreads over a few frames and the wallet stays responsive. Measured: first cold flick dropped 6 frames before, and this was the dominant cost
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
(NIP-44 ECDH, ~500ms of secp256k1 in a boot profile — the last of the carousel jank) ran as one uninterrupted loop. Each synchronous run is now capped to ~8ms, one frame's budget, then yields so input and paint get a turn; the backfill just spreads over a few extra frames instead of freezing them
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
profile answer (the 5s query racing still-dialing relays) is retried in minutes instead of being trusted for the 24h TTL, faceless rows are never persisted (they were evicting real faces from the 150-cap), and the logged-in identity's own profile is pinned against eviction entirely
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
ASP's service charge (base + lifetime ppm) record separately and the history detail shows them on their own lines — settling the mystery of a ~350-sat chain fee inside a ~650-sat total. The community row's member count anchors to last session's settled figure instead of ticking upward through the guestbook replay, and a profile's Pay button reached from search actually reaches Send — the lingering search screen was winning the router
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
one puts it away), every landing — login, account creation, an account snapped into the carousel — is the account's own timeline, and that timeline is now SCOPED: Savings shows the on-chain ledger and gift reservations, Spending shows the ark side. The carousel meanwhile learns mouse and mind-reading: desktop gets a real grab-and-pull drag (snap lifted mid-drag, restored on the glide), the peeking neighbor switches on a single click, and the strip wears grab cursors. And a thread's avatars and author names finally go somewhere: profiles stack on top of the conversation and back returns to it
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
clickable @names beside their npub siblings, and note/nevent references become 'view note' links that fetch the event by id (relay hints first) and open its thread right in the app — the dim truncated stubs survive only for what genuinely can't resolve
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
renders through noteBody like any note — npub mentions become clickable @names, other nostr: refs truncate to stubs, URLs linkify — with overflow-wrap catching whatever unbreakable string remains
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
cache socket, the registrar's TLS session, and the NIP-50 relay connections each cost a handshake that used to land on the first query — the cold second of dead air. Touching the send recipient field, opening user search, or unfolding New message now pre-opens all three (throttled; the idle reaper still closes the socket a minute after real use)
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
memoized (make() runs eight mask trials over a version-20 grid — 30ms+ per call on a phone, paid on EVERY render of the receive pane), the header's unread count reuses its answer behind a revision counter bumped only when messages or read-marks actually change, and ark's history list rebuilds at most every 1.5s instead of per repaint
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
still dialing) no longer clobbers a remembered avatar with a punk — stale fields hold while the refresh does its rounds — and the ark merge marks coins spent the moment a synced movement or done action names them as consumed inputs, instead of double-counting a sleeping device's view (output merged in, inputs still 'spendable') until the reconcile caught up and the balance visibly ticked
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
right where a coinos.io veteran is looking at the name they want to bring over
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
the legacy avatar and banner and fills the kind 0's empty slots (fill, never overwrite — an existing nostr identity keeps its own look), the wizard's punk picker steps aside for a profile that already has a picture, and v3 finally wears cover photos — nostr's standard kind-0 banner renders full-bleed across the top of every profile card with the avatar overlapping it, settable by URL or upload in the profile editor
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
first-frame optimism keyed on 'no rooms yet', but init() creates the coinos room before the first paint, so the guard never fired and the dot waited for real messages to decrypt — it now keys on no messages having landed, which is what it meant all along
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|
| |
|
|
|
|
|
| |
return is stashed synchronously at boot and claimed whenever the wallet actually opens (the old 1.5s one-shot silently dropped it), the wizard's 'I'm done, continue' state survives the trip to coinos.io as a persisted step, the legacy page shows the claim in progress instead of re-asking, and a finished claim advances the wizard by itself. Fresh-seed polish riding along: the spending-setup ask stops waiting out registrar round trips that only exist for restored wallets, and the header punk paints synchronously instead of after an async identity fetch
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
|