aboutsummaryrefslogtreecommitdiffstats
path: root/src
Commit message (Collapse)AuthorAgeFilesLines
* Under the "Sign in" heading the ways in just name themselves: Seed phrase, ↵Adam Soltys17 hours3-9/+13
| | | | | | | Passkey, Google, Nostr. "I think we can remove 'Sign in' from all the sign in buttons since it's at the top in the heading" (Adam, 2026-11-06). frontDoorSignin takes { short } and the sign-in step asks for it; the coinos.io move page (under "or") and the sign-in sheet in Messages keep the full "Sign in with …" labels, where nothing above says it. "Migrate a coinos account" is unchanged. Tests wait for the list by its last door Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6Ts5Tktega8VbLg31q
* Passkeys in the WebView builds: the apps name v3.coinos.io too, and a failed ↵Adam Soltys17 hours1-0/+5
| | | | | | | passkey sign-in no longer goes on to make a new one. Coinos Beta's passkey sign-in failed after the account was picked — "CredMan getCredential call failed … RP ID cannot be validated" (Pixel 9, 2026-11-06). An app's association with a site runs both ways: the site's assetlinks.json names the app (it did), and the app's manifest names the site through asset_statements meta-data — which the TWA has and the webview/fcm manifests didn't. They declare it now, and the shared asset_statements string carries get_login_creds beside handle_all_urls. And passkeyLogin rolled any sign-in failure into creating a passkey, so the validation error was followed by a second, failed create dialog: only NotAllowedError (how browsers report "no passkey here", or a dismissed sheet) leads to creating one now; any other error is shown Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6Ts5Tktega8VbLg31q
* Pull down to refresh works in the Android app, the Beta and an installed ↵Adam Soltys17 hours3-0/+110
| | | | | | | PWA. "Pull down to refresh isn't working for me in the apk" (Adam, 2026-11-06): measured on his Pixel, the same pull reloads a Chrome tab but not the app — Chrome turns its pull-to-refresh off whenever a page runs full screen, which the TWA (sticky-immersive since 1.14) always does, and a WebView or a standalone PWA has none at all. src/pull-refresh.js takes over where the page reports display-mode standalone or fullscreen, or runs in our WebView (window.CoinosHost), and switches the browser's off there (overscroll-behavior-y: contain) so the two never both fire; a browser tab keeps the browser's. A pull counts only at the top of the page, vertical (sideways swipes stay the tab navigation's), and not from inside something scrolled down (a chat, a picker) or over the page (dialogs, the media viewer, fields); damped like the browser's, a round arrow follows the finger and spins once the pull is far enough, and release reloads (through the network, as a refresh does). On the phone, the local build served into the app: a pull at the top reloads, one while scrolled down only scrolls, a short one does nothing Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6Ts5Tktega8VbLg31q
* The seed-phrase pages say less: no "write these 12 words down on paper" box, ↵Adam Soltys17 hours2-10/+21
| | | | | | | the passphrase behind "Add passphrase", and the import titled "Enter seed phrase". Adam, 2026-11-06: the warning box goes from the create pane and the backup step (the words and the page's own title carry it); the BIP39 passphrase field, which read as a required password under the seed, waits behind an "Add passphrase" button and, opened, is labelled "Passphrase (optional)" with "Leave it empty unless you want one. Most wallets have none." (What's this? still explains it; a typed passphrase keeps it open; clearing the drafts closes it). The import pane is titled "Enter seed phrase" (was "Restore your wallet"), its field "Seed phrase" (was "Recovery phrase or key") with the placeholder "Your seed words" — an xpub or xprv pasted there still opens as before. tools/onboard-backup-test.js follows the new words Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6Ts5Tktega8VbLg31q
* The sign-in list is seed phrase, passkey, Google, Nostr, coinos.io, evenly ↵Adam Soltys17 hours2-42/+62
| | | | | | | spaced, and setup pages keep only the language picker under them. Adam, 2026-11-06: passkey second, below the seed phrase; "check the spacing on all the buttons, some of them seem a bit different" — the sign-in feature returned its doors in a wrapper of their own (8px apart) inside the page's 20px column, so the gaps ran 20/8/8/20. frontDoorSignin now returns them loose (externalButtons takes passkeyFirst), and the front door's lists sit in one 10px column: measured at 390px, every door 53px tall and 10–11px apart; the welcome's two choices 55px and 10px apart; the sign-in sheet in messages spaces them at its own 12px. And "on all the wizard/setup pages let's hide the footer except for the language selection": the unlock screen and every page the wizard draws (.onb) get a footer with just the language picker, as wide as the language's name; everywhere else keeps the full footer. tools/onboard-backup-test.js checks the front page's footer holds only the picker Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6Ts5Tktega8VbLg31q
* The front door is two choices: "Create a new wallet" or "Sign in to an ↵Adam Soltys17 hours2-18/+33
| | | | | | | existing account". "Login page is getting busy" (Adam, 2026-11-06): it had grown a door per method — Google, passkey, Nostr, twelve words, coinos.io — under the way in, and a footer of credits, pickers and the build stamp under that. Now the welcome step holds the two choices and nothing else (no footer on the front-door steps; the language follows the browser's), and the second opens a sign-in step with the ways in: "Sign in with seed phrase" first (was "Restore from twelve words"), then the feature's doors (Google, passkey, Nostr), then "Migrate a coinos account". Back from the seed-phrase import, the Nostr card and the coinos.io move returns to the sign-in step. Tests follow the extra step: onboard-backup (two choices, no footer; the seed phrase first among the ways in), onboard-migrate-door (Back to the sign-in choices), amber-login, account-access (logout lands on the two choices) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6Ts5Tktega8VbLg31q
* Opening the wallet no longer stretches the same keys in JavaScript two and ↵Adam Soltys18 hours5-13/+124
| | | | | | | three times over. Profiled on Adam's Pixel 9 at 4x CPU (a mid-range phone), a cold launch — a fresh tab, so the wallet comes out of the device vault — spent ~850 ms of its 1.9 s to the balance stretching keys on the main thread: scrypt (N=2^15) to open the vault under its blank password (~520 ms), then PBKDF2-2048 of the same twelve words once per saved wallet to match it to its watch-only entry and twice more for the open one (the BIP84 account and, inside nostr-tools' nip06, the nostr identity). Now src/seed.js keeps each phrase's BIP-39 seed: the boot stretches the phrases it's about to open with WebCrypto's native PBKDF2 while the stores open (warmBootSeeds, alongside the IndexedDB wait it already had), and the wallet, zaps and the nostr key (NIP-06 m/44'/1237'/n'/0/0, derived from that seed — nostrKeyFromSeed) read it; a reader that comes first stretches it the old way and leaves it for the rest. A lock forgets them. The vault under the BLANK password — the default, which scrypt protects from no one, since anyone with the blob can compute scrypt('') — keeps its key beside the blob and new blank seals reuse that salt (fresh nonce each time); sealing under a real password drops it, and real passwords are stretched every time as before. Same cold launch at 4x: balance 2.2–2.9 s → ~1.5 s, scrypt and the JS PBKDF2 gone from the profile. tools/seed-cache-test.js: WebCrypto's seed = @scure/bip39's for plain, passphrased and NFKD input, warmed or cold; the NIP-06 key = nostr-tools'; a malformed phrase still refused; a blank vault opens without scrypt and reseals with the kept salt, a wrong guess fails and drops nothing, a real password keeps nothing Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6Ts5Tktega8VbLg31q
* The wallet paints its balance without deep-copying the whole ark state five ↵Adam Soltys19 hours2-2/+41
| | | | | | to seven times on every launch. "Its so slow to load … I'm using the apk. Compared to Minibits or something its way slower" (a user, 2026-11-05): profiled on Adam's Pixel 9 over DevTools, a launch kept the main thread pegged for ~1.7 s, and 1.4 s of it was loadArkState — every arkStateNow() during the first render (balance, history, notices, the Spending figures) structured-cloned the stored state, ~1 MB on a wallet with deep coins, ~220 ms per copy, before the manager connected and its live state took over. ArkStore.peek() hands back the stored object itself (before the store opens, or on localStorage, the parse is kept until the key is next written); wallet.peekArkState() is loadArkState through it, with the misfiled-state repair still going through loadArkState and each object's owner decoded once; arkStateNow and arkWanted read through it — read-only, as they already were with the live ark.state. Same boot on the phone, live tab with the local build: main-thread busy over the first 4 s 3.7 s → 1.8 s, no pegged stretch. tools/ark-store-test.js: peek returns the same object, sees writes at once, and drops a kept localStorage parse on write Co-Authored-By: Claude Opus 5.5 <[email protected]>
* A Lightning invoice that Spending can't cover, with too little in Savings to ↵Adam Soltys3 days3-14/+48
| | | | | | | move over, is offered the door that works: a Lightning receive into Spending. "can you help me" (a user, 2026-11-03, screenshot): 546 sats in Savings, 0 in Spending, a 175-sat invoice — the ask said "Move Savings into Spending first", and that button led to a board panel saying "Needs at least 1,000 sats in Savings" (the coinos server's board minimum), a dead end dressed as a way through. The ask now knows whether Savings can fund a move at all — the server's minimum, from the live manager or the last copy of its info seen, plus the board transaction's chain fee — and when it can't, says what Savings holds and what a move needs and offers "Receive over Lightning instead", which opens Spending's Receive page; a Lightning receive lands in Spending directly, no confirmation, and the invoice can be paid from there. The same question replaces the hardcoded "more than 1,000 sats in Savings" guess behind the pasted-invoice path (its error now carries the same sentence), the Ark send review's move door and the zap card's "Move sats into Spending" button (arkSavingsCanBoard hook), and the board offer's prefill honours the minimum before the manager is connected (it read 0 then, and prefilled 179). tools/ark-ln-savings-small-test.js: 546 sats → the receive door, no move button; 1,000 sats (the minimum, but not the fee on top) → the same; 5,000 → the move, straight to the board panel prefilled at the minimum Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* The wallet's push notifications are worded so Chrome's spam model lets them ↵Adam Soltys4 days5-23/+101
| | | | | | | through. "Any way to stop classifying v3 notifications as spam? I've seen this a few times now" (Adam, 2026-11-03, phone: "Possible spam — Chrome detected possible spam from v3.coinos.io", Unsubscribe / Show notification): Chrome on Android scores every web notification's title and body with an on-device model before showing it, hides it behind that warning above 90 of 100, and — nine warnings in a week on a phone whose engagement with the site is under 50 — revokes the site's notification permission outright (Chromium kAutoRevokeSuspiciousNotification, enabled by default). The model is a text classifier trained on Gemini-made examples of scam notifications, and our lines were their very shape: "Payment received" 95, "Incoming payment / Someone is trying to send you money…" 99, "Payment request / An app is asking your wallet to pay…" 99, "alice is calling / Voice call. Tap to answer." 99, "New message / You have a new private message." 92, "alice / sent you a message." 95, "New reply to your post / <text>" 98, "alice reacted ❤️ / to your post…" 97. Desktop Chrome runs the same model (without the warning) and reports each score to chrome://histograms, so the lines were measured rather than guessed — ~200 candidates on Chrome 156's model: "sats" reads as money without reading as a payment scam, "payment" doesn't; a sender's name in the body of a message or reaction line tips it over, "New chat" / "<name>" doesn't; any "reply" title is 96–99; a reply's own text is out of our hands (an @name or a link in it scores 97+), so it no longer rides along; "Calling" / "<name>" is 11–29 where every "<name> is calling" is 80–99. src/notify-text.js holds every line, with the measurements; the worker, the background responder and the renewal warning draw from it. The new lines score 8–46 (the shortcode reaction 67). tools/notif-spam-score.js re-measures them in a throwaway Chrome (SystemNotifications off, so nothing reaches the desktop) and fails at 90 — run it before changing any of them. On the phone that already warned, "Show notification" then "Always allow" marks the site trusted and stops both the warning and the revocation count Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* A wallet on another ark server tells the registrar it is listening, so ↵Adam Soltys4 days2-1/+20
| | | | | | | Lightning sent to its name arrives the moment it opens. A name whose wallet lives on Second (or a custom server) can only be paid over Lightning — the registrar's float can't arkoor to a server it isn't on — and only while the wallet is open to answer the registrar's offer request; its hourly retry with a six-second window rarely met a short phone session, and manna's 52 sats failed 449 times from 2026-10-18 to 11-03. Once the offer listener is up (nwc listen(), a beat after subscribing), the names feature POSTs /listening (NIP-98, coinosv3-server 14a6dd5) and the registrar sends whatever is queued for the name at once. Once per ten minutes, and never for a wallet on the coinos server: it is paid while asleep and spends no signature on this. tools/names-listening-test.js: nothing on coinos's server, one authed call on Second, no second within ten minutes, a registrar that is down is asked again next time Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* The feed's post button is on screen the moment the feed is, while the page ↵Adam Soltys5 days3-25/+40
| | | | | | | is still sliding in. "I'm still not seeing the Post button right away when I open the feed. It should be there from the start" (Adam, 2026-11-02): measured on his phone over DevTools, the button was in the page 87 ms after the Feed tap — 4109 px down, below an 809 px screen — and in place at 562 ms. It is position: fixed, and it was rendered inside the page, whose entrance animation puts a transform on it; a transform makes fixed descendants position within the page, so the button rode at the foot of the feed card until the slide ended. The feed now hands the button and the new-posts pill to the shell (a pageFloaters hook, taken once per render), which paints them beside the page, outside anything that animates; the pill's CSS no longer depends on sitting inside the page, and its phone-width rule stays after the base one so it still rides above the button. tools/feed-fab-anim-test.js taps Feed from the wallet and samples the button every frame of the slide: on screen from the first. The direct-mount harnesses (feed-new-posts, feed-streaming, feed-bottom-nav) paint the floaters as the shell does Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* A face or cover photo whose host has lost the file is found on another ↵Adam Soltys5 days1-8/+94
| | | | | | | server that holds the same blob. "No avatar for karnage?" (Adam, 2026-11-02): his kind 0 names a picture and a banner on media.21media.to, and that host answers "The requested blob was not found" for both — while blossom.primal.net, the server he publishes in his kind 10063, still has them, which is why other clients show his face. A picture named by its sha256 is a Blossom blob, served under the same name by any server holding the bytes: when the thumbnail maker finds the host without it (a 404, or a host that won't answer), the blob is looked for on the person's published servers and then the public ones we know, asked together, and the first that answers stands in — the thumbnail is made from it, the profile page's big face and banner paint from it, the feed's warm-up loads it — and the find is remembered on the device so the search runs once. tools/blob-alt-test.js serves a dead host and a published server holding the copies and checks the thumbnail, the profile page and the post rows Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* A zoomed-in picture is drawn again at its real scale once the pinch settles, ↵Adam Soltys5 days1-0/+10
| | | | | | | so no faint grid shows across it. "When I zoom in on my image, I see very faint grid lines. Is that a compression artifact?" (Adam, 2026-11-02): no — Chrome keeps a will-change: transform layer rastered at 1× and scales that bitmap on the GPU, and the bitmap's tiles meet in faint seams, a regular grid over the sky. The hint stays on while a gesture moves the picture (no re-raster per frame) and is dropped once it comes to rest zoomed, so Chrome rasters the picture at the scale it is shown at: sharp, and seamless. At 1× the stylesheet's hint is back for the pager's swipe Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* A thread's heart count says what its who-panel lists. "Why does the count ↵Adam Soltys5 days1-3/+11
| | | | | | | beside the heart say 1 if I have 2 heart reactions?" (Adam, 2026-11-02): a thread remembers its counts between visits so that a short relay answer can't erase them, and the bar showed the memory alone — while the tally in hand, which the who-panel reads, had the second heart, brought by the feed's own count fetch before the thread was opened again (the memory is only brought up to date while the thread is on screen). The bar now shows the larger of the remembered number and the live tally, hearts, boosts and sats alike. tools/thread-like-count-test.js opens a thread by its link with one heart on the relay, comes back through the feed when there are two, and checks the thread says 2 Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* The front page has a door for a wallet made elsewhere: "Restore from twelve ↵Adam Soltys5 days2-1/+16
| | | | | | | words", straight to the phrase field. "Signing in on another device … it was hard to find how until I realised you click to make a new wallet, where then you get the option to use the seed phrase for an existing wallet" (user feedback, 2026-11-02): the import used to hide as a line under the freshly minted words behind "Create a new wallet". It sits below the sign-in buttons with the migrate door, and the step it opens is titled "Restore your wallet" rather than "Set up your wallet". tools/onboard-backup-test.js checks the door, that it opens on the phrase field with no minted words, and that a typed phrase opens the wallet Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* A quoted note, or the post a reply answers, is asked for from the near ↵Adam Soltys5 days1-9/+24
| | | | | | | relays first — the reference's own hints, ours, its author's — and from the wide net of public and archive relays only once those have come back empty, or after 1.2 s if they are slow about it. Asked all at once, as before, every quote and every reply's parent went out to a dozen relays, and in the Conversations view, where each row has a parent, half of all REQs in twenty seconds on the live app — three hundred, to 47 relays — were lookups of notes our own relays answered in the first beat. tools/relay-budget-test.js adds a far relay: the six quotes our relay holds are never asked of it, and the one only it holds is still found there Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* Comments scoped to a web address show with the page they are on. DaddyTube ↵Adam Soltys5 days3-5/+38
| | | | | | | posts NIP-22 comments (kind 1111) on a film's IMDb page: an I tag naming the address, no e or E tag. Adam's own ("Charming and wholesome", 2026-11-02, on relay.primal.net) showed on neither his profile's Replies tab — the profile page never asked for kind 1111 at all — nor in the Conversations view, whose warm-up pass asked for posts and reposts rather than the view's own kinds, and the refresh that asks properly is skipped for half a minute after it. The profile asks for comments now, the warm-up pass asks for what the view shows, and a comment with no post to show above it carries the page it is on where "Replying to" would go — "Comment on imdb.com/title/tt32565993", linked, or the comment's own alt tag when it has one ("Comment on Sheep Detectives (2025) in DaddyTube") — in place of the bare "reply" tag. Also, a report naming a post as 'other' with the author's bare p tag beside it counted as a report of the PERSON (an 'other' e tag didn't count as naming a post), and two such from follows folded everything the author wrote; every post named now counts. tools/web-comment-test.js plays all of it against a fake relay Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* The feed asks the relays for less, and asks it in fewer REQs. Measured from ↵Adam Soltys5 days2-40/+121
| | | | | | | the laptop (2026-11-02, after the resume fix), an open of Following sent some 560 REQs in twenty seconds across 57 relays, over a hundred each to damus and primal, and damus answered the home IP "banned: too many rate-limit violations". Four sources, all cut: a quoted note's author (a face tapped, a reply's target) had their relay list asked for one REQ per author per relay — six quotes on screen sent eighteen — and those asks now wait 80 ms and go out as one REQ through the feed's batched fetch, which also remembers who has no list; the live subscriptions were closed and reopened at the end of every refresh pass (seventeen REQs over eight relays, twice per open) and now stay up while the relays and authors they cover are unchanged, the head they count fresh posts against read at each event; the follows' reports were fetched afresh at every reload (an in-memory clock) and now honour the clock the device saved, asking only for what came since; and every one-shot REQ was held open on our own relay until the slowest relay in the ask had answered — the library closes them all together — so the fast one carried each for up to five seconds. One-shot queries now go out per relay (queryOn, queryStreamOn, queryManyOn, the profile and inbox lookups), each closed on that relay's own EOSE, with no more than ten open on any relay at once: the rest wait their turn, and a turn that comes after the ask's own wait is skipped. tools/relay-budget-test.js runs the feed against a fast and a slow fake relay and checks each of these Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* Back from the background, the app checks its relay sockets before it asks ↵Adam Soltys6 days2-6/+63
| | | | | | | them anything: one tiny REQ to each; a socket that answers is kept, one that doesn't is closed and redialed at once, its subscriptions refired by the library with since set to the last event each saw, and only then do the DMs resubscribe and the feed refresh. "No new notes in Following since 11:35? It's 9 pm now … it loaded more posts, it just took a while" (Adam, 2026-11-01): after hours in the background a phone's tab is thawed with every socket reading OPEN and long dead on the network, so the catch-up sent on resume vanished, and the library found a dead socket out only after its 29 s ping and that ping's 20 s timeout, then waited 10 s to redial — a minute or more before the backlog arrived through the refired subscriptions. tools/resume-wake-test.js freezes a fake relay's sockets, posts while the app "sleeps", and has the post on screen 2.7 s after resume (never within 15 s before). Also: Primal's "too many concurrent REQs" notice no longer cools the relay off for 30 s at every boot — it serves every REQ past its twenty anyway (measured 60 at once: 60 EOSEs, 12 notices), so the cool-off only lost us its answers while the relay lists, profiles and the feed all asked at once; a CLOSED with that reason still counts Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* A clip whose file its host no longer has says so where the player stood, ↵Adam Soltys6 days3-8/+46
| | | | | | | with the file's link, instead of sitting as a dead player under "Tap to play". A post's video "won't play" (Adam, 2026-11-01): the mp4 had been deleted from Primal's blossom, which answers a 404 with no CORS headers, so the probe couldn't read the status and the box waited for a tap that loaded nothing — no client renders it (the reply under the post says as much) and no other blossom host has the hash. The player is the first to know: its original failing now tries the server's copy once (a file the browser can't decode fails the same way, and the copy plays that one), and the copy failing too marks the clip gone for a day — the line and the link in the box, its cover, play button and controls off, a short box; the next paint of the post says it at once with no request, the media warm-up and the in-view hook leave it alone, and the videos grid leaves it out as it did a 404 the probe could read. Outside the feed the box used to be hidden on an error before anything was said; it gets the line too. tools/video-gone-test.js serves the clips from a second origin without CORS, one of them a 404, and checks the line, the untouched live clip and the silent repaint Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* The feed opened by URL paints as the restoring wallet's from its first ↵Adam Soltys7 days2-10/+52
| | | | | | | frame: the Post button, the feed chips and the nav come with the page, not a beat after it. /feed is a public page, so it painted as a visitor's (no button, chips or nav) the moment the app ran, and the wallet's restore — synchronous work: the vault's scrypt on a cold start, the wallet's state, every feature's init, ~0.5 s on a laptop and more on a phone — re-painted it with the button only once it had run ("the post button takes a while to appear", Adam, 2026-10-31). Boot now tells before that work whether a wallet is about to open (a session list in the tab, a vault the last seal or unlock opened without a password — noted, since the vault alone can't say short of the scrypt being avoided — or watch-only accounts) and the feed, the nav and the chips paint for it; taps still go by the wallet actually being open, which it is by the time one lands. The wide-screen wallet column waits for the numbers. A visitor with no wallet on the device gets the visitor's page as before. tools/feed-boot-chrome-test.js boots the built app at /feed behind a session, a noted vault, an unnoted vault (the old order, and the unlock leaves the note) and as a visitor Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
* A profile (kind 0) is never written over one we failed to find. 2026-10-30, ↵Adam Soltys8 days22-1/+325
| | | | | | a long-time nostr user signed in with his key for the first time; our relays had never seen his kind 0 (it lived on ditto, snort and oxtr), the 3 s lookup came back empty, and the payment-address publish went out as {lud16, nip05} alone: name, picture and banner gone from every relay (nicstazzema, shown as npub1yk2lkr7). An empty quick lookup now widens to the same search the follow list got after its own overwrite (indexes, archives, the key's own write relays, Primal's signed copy) before anything is written, once per key per session so a brand-new key's first publish waits once. For anyone already hit, the profile editor offers "Look for older copies": a kind 0 from a history-keeping relay fills only the fields still empty in the editor, and Save publishes it. tools/kind0-overwrite-test.js plays the incident against a fake relay network where only an archive holds the profile Co-Authored-By: Claude Fable 5.1 <[email protected]>
* A wallet that renews its own coins (Settings → Spending: self or exit) ↵Adam Soltys8 days3-18/+42
| | | | | | | pays a Lightning invoice from the coins nearest expiry first, so fewer of them come up for a paid renewal (Stacker News request, 2026-10-29). Lightning sends only: the ASP takes those coins, whereas an Ark-to-Ark send would hand the recipient your short timelocks. The custodial default keeps the smallest coin that covers the payment (expiry costs nothing there), a coin past its timelock still goes last, and a pile of old crumbs that would breach the input cap falls back to largest-first. The closed-app NWC worker follows the mirrored tier the same way Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6VLgQdujavxPsfU7A9
* A toast no longer sticks on screen for good. One raised while the app was in ↵Adam Soltys8 days1-2/+9
| | | | | | | the background (a zap failing after you switched away) was hidden by its timer before it ever showed, because a hidden tab pauses requestAnimationFrame but not setTimeout; on return it showed and nothing took it down (Adam's phone, 2026-10-30). The hide timer now starts when the toast actually shows Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6VLgQdujavxPsfU7A9
* A remote signer (Amethyst) that wasn't heard the moment the app needed it no ↵Adam Soltys9 days2-9/+43
| | | | | | | longer means a new connection: "Reconnect your signer" keeps trying the stored session (at once, every 10 s, and on every return to the tab) and closes itself when the signer answers. Before, the screen made one try, or none if a try had failed in the last 20 s (it nearly always had: that failure is what opened it), so the only way out was a fresh nostrconnect, while the stored session answered in 0.7 s whenever anyone asked (Adam's phone, 2026-10-29). A try made while the tab was hidden (frozen on a phone) no longer counts as a failure, a closed signer ("not open anymore") is treated as dead and replaced, and Cordn follows a replaced signer instead of signing with the closed one Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TUeqTyZB8eXLGymUY
* Hats show in the first paint: the app holds the registrar's whole list of ↵Adam Soltys9 days1-0/+43
| | | | | | | who wears what (/hats/worn, a couple of dozen heads, kept per device and refreshed every two minutes), so a face seen for the first time on a message page gets its hat at once instead of after a 400 ms batch and a round trip Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TUeqTyZB8eXLGymUY
* Signer wording no longer names Amber: the Android login reads "Sign in with ↵Adam Soltys9 days11-19/+19
| | | | | | | a signer app", Messages offers "Connect signer", and the login hint says it works with remote signer apps, since Amethyst and others can sign too Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TUeqTyZB8eXLGymUY
* A closed wallet whose payment fails now says so: the error ↵Adam Soltys9 days2-7/+27
| | | | | | | (INSUFFICIENT_BALANCE or PAYMENT_FAILED, with the reason) is left with the notifier at once and goes out unless someone answers within 5 s. Waiting the grace period out in the worker ran past iOS's ten seconds after a failed send, so the other app heard nothing (PullCap, 2026-10-29). A Lightning send no longer picks a coin at the server's depth cap, so such a coin fails here with a reason instead of at the ASP; a capped coin under 330 sats (a round won't take it) says to receive more instead of "waiting to be renewed", and the worker stops booking renewals the server would refuse as dust Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TUeqTyZB8eXLGymUY
* Scheduled posts: the composer's clock button opens a "Post at" time (an hour ↵Adam Soltys9 days23-37/+309
| | | | | | | ahead, on the quarter hour) and turns Post into Schedule. The note is signed now with that created_at, so a remote signer asks while you're there, and handed to the notifier, which publishes it at that time; nothing reaches a relay before then. What's waiting shows as "Scheduled (n)" under the composer and on your profile, each with Cancel (a kind 5 to the notifier, so it never goes out); one cancelled on another device leaves the list at the next status check Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6TUeqTyZB8eXLGymUY
* A gift link names its sender: the link carries the sender's nostr key, ↵Adam Soltys11 days24-22/+178
| | | | | | signed over the gift's code (#f=…, beside the note), so the claim screen shows "From <name>" and claiming follows them. A signature that doesn't match the code names nobody, so a link can't be passed on as someone else's gift. "Show it's from me" is ticked by default when making a gift, and unticking it is remembered; the gift's record keeps the sender, so View and Copy link again keep it too Co-Authored-By: Claude Opus 5.5 <[email protected]>
* The background worker's key window is 250 keys (was 25: PullCap's hourly ↵Adam Soltys11 days3-12/+63
| | | | | | subscription ran out after 19 payments with the app closed, "key window exhausted"), derived once per account and memoized; 60 receive preimages (was 15). When the worker can't act on a request it still wakes the user, and now leaves a "wait, open the app" error with the notifier that goes out only if nobody answers within 20 s, so the other app hears something instead of silence and a sibling's real payment is never shouted over. A request older than its two-minute window (a push that waited at APNs) is ignored quietly instead of showing "Payment request" Co-Authored-By: Claude Fable 5.1 <[email protected]>
* A timed comment opens with its time as a chip that plays the clip from ↵Adam Soltys12 days2-8/+22
| | | | | | | there; the "[0:03] " prefix other clients read is left out of the words (and out of the marker's line on the bar) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* A clip's moments sit on its bar, just under the line (under the waveform for ↵Adam Soltys12 days2-3/+13
| | | | | | | audio) instead of floating above it, and a reply made at a moment shows as its author's face, not ↩ (the pager's bar and the pop-up too) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* On a phone, Return in the reply box is a new line; Reply is the button. A ↵Adam Soltys12 days1-1/+3
| | | | | | | hardware keyboard's Enter still sends Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* An upload that fails says why: the host and its answer (or the signer's ↵Adam Soltys12 days2-5/+11
| | | | | | | error) in the toast, not just "check your connection", which hid hosts refusing the file and a signer that never answered Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* Cancel on a reply discards what was typed and folds the box: it only cleared ↵Adam Soltys12 days1-1/+13
| | | | | | the "replying" flag, and the box stays up while a draft has text, so with anything typed it did nothing. Attachments the draft carried are dropped with it Co-Authored-By: Claude Fable 5.1 <[email protected]>
* A released placeholder address on your profile page shows as the claimed one ↵Adam Soltys12 days1-3/+20
| | | | | | | | on the Lightning line too, and Save in the profile editor repoints it in the kind 0 (a same-domain name that no longer answers); the background heal steps aside for an interactive signer (Amber) instead of waiting forever in the kind-0 queue Jarryd's page still showed [email protected] over ⚡ [email protected]: the header only substituted the NIP-05 line, and his kind 0 was never republished — the heal of ce96364 needs a background signature, which an Amber sign-in can't give, and Save left a lud16 that wasn't the old address alone. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Messages go through messages.coinos.io first: our own messaging relay ↵Adam Soltys13 days2-4/+17
| | | | | | | (Magnitude Relay, built for gift-wrapped DMs) leads the DM and group-chat relays, with relay.coinos.io and primal still read and written. A wallet key's inbox list that only ever held our old defaults is updated to name it; feeds and zaps never ask it (it takes messages, not posts) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* History rows no longer wrap: the recipient's name chip keeps to one line ↵Adam Soltys13 days2-5/+13
| | | | | | (ellipsized when it must) and the "in progress" tag sits beside the time, not in the title row where it broke the name mid-word in the narrow wallet column Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Payment address and Change username are one card: the name sits in an ↵Adam Soltys13 days22-40/+101
| | | | | | | editable field beside @domain with Copy address and Release; typing a different name swaps those for the "old address stops working" line, Cancel and "Change to <name>" Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* An Ark gift's claim button just says "Claim it now" (no "(instant, no ↵Adam Soltys13 days22-3/+26
| | | | | | | fee)"); an on-chain gift still shows its network fee Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* Gifts carry a note: an optional "Add a note" when making one, quoted above ↵Adam Soltys13 days25-13/+74
| | | | | | | the amount for whoever opens it. It rides the link after the # (#m=…), so no server sees it; the gift's record keeps it, so Copy link again and View include it; with a note the QR is byte mode Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* A post you deleted leaves your profile's list (and any feed it was reposted ↵Adam Soltys13 days1-3/+8
| | | | | | | into) instead of leaving a "This post was deleted by its author" row; threads still keep the deleted post's place Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* A gift opened into a new wallet goes on, once its seed is written down, to ↵Adam Soltys13 days23-18/+80
| | | | | | | the first-run steps: pick a username, then a picture and a cover ("Not now" lands in the account the gift arrived in). Also fixes Verify backup on the gift screen, which crashed (pickConfirm wasn't passed to the gifts feature) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* A payment that arrives as several coins is one Received row: a claimed gift ↵Adam Soltys13 days2-2/+34
| | | | | | | held in two coins (dust padding split the sender's 886 into 595 + 291) showed as two receives. Receives stamp their mailbox message and the history folds a message's coins together; older rows fold on the same delivery instant Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* A feed clip with timed reactions seeks on the moments bar alone: the ↵Adam Soltys13 days3-8/+37
| | | | | | | player's own controls (its seek bar) stay off, the middle play/pause shows whenever it's paused (touch too), and the sound button stays to turn it down again Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* One seek bar under a feed video: the moments timeline shows beneath a clip ↵Adam Soltys13 days2-0/+4
| | | | | | | only when the post has timed reactions to mark (the player's own bar does the seeking) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* A closed wallet's NWC payments fit iOS's ten-second wake: a paid send is ↵Adam Soltys13 days5-34/+143
| | | | | | | | | | | | | | | never marked for revocation by a second wake, lookups report outgoing sends and finish them, and pushes no longer run side by side PullCap's night of 2026-10-24: every wake's diary stopped 8-12 s after its push (iOS stops the worker there, not at 30 s). Two wakes two seconds apart both drove one send; the second's InitiateLightningPayment was refused ("state: ln-spent") and the paid send went to 'revoking', where every later wake spent its whole budget on it, so no request was answered after midnight. - manager: an initiate refused as ln-spent follows the payment ('initiated'); a send in 'revoking' asks the ASP first (paid: settle; in flight before expiry: back to waiting; not found: revoke) - worker: 9 s budget on Apple endpoints (27 s elsewhere) for answers, earlier sends and renewals; every ASP step held to it - lookup_invoice asks every copy of the connection and drives an outgoing send in flight, answering settled with the preimage - a lease in the worker's IndexedDB store serialises pushes (the Web Lock didn't); diary notes a missing Web Locks API - the reply and the mirror save run together; stuck sends are finished before a new payment only when they hold its coins Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4
* Feed clips with a mouse: a big play/pause in the middle on hover that plays ↵Adam Soltys2026-10-2423-4/+107
| | | | | | | or pauses in place, a click on a playing clip pauses it (a paused one still opens the pager), and a tall clip is held to three quarters of a computer screen with its box narrowed to the clip's shape Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4