aboutsummaryrefslogtreecommitdiffstats
path: root/tools
Commit message (Collapse)AuthorAgeFilesLines
* Ark: a double-tapped zap no longer spends one coin twiceAdam Soltys11 days1-0/+22
| | | | | | | | | | | | A reconcile pass that began before the first zap took its coin handed it back as spendable (the server still said so) while the zap was in flight; the second zap picked it and the server refused: 'vtxo … already spent'. Coins whose state changed during the pass are left alone. And a payment refused for an already-spent coin now reconciles and pays once more instead of failing with the raw error. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* Renewing asks first, and a renewal links to its round's transactionAdam Soltys12 days2-0/+177
| | | | | | | | | | | | | | | | | | | | | - The coins page's Renew button opens a confirmation instead of renewing at once: how many coins, their amount, the fee, and what is left to spend meanwhile — with a red notice when that is nothing (the button selects every coin by default, which is how a user locked their whole balance for an hour on 2026-10-03). It says the coins can't be spent until the round's transaction confirms and that a renewal can't be cancelled; the wallet renews for free near expiry anyway. The inline notice from 328f2db moves into it. - A renewal keeps its round's transaction id as soon as the round has run (before it confirms): the in-progress card on the coins page links to it, and the finished renewal's history row carries it, so its detail shows the id with View/Copy like any on-chain movement. Renewals finished before this change have no id recorded. tools/ark/renew-confirm-ui-test.js, tools/ark/renew-txid-test.js Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* A Lightning payment that can't be covered says why, not 'Payment failed'Adam Soltys12 days1-0/+52
| | | | | | | | | | | | | | | | | | | | | | | | A user renewed all ten of their coins (55,611 sats) from the coins page, tried to pay a 55,300-sat invoice three minutes later, and was told only 'Payment failed'. Nothing had failed: the renewal's round ran at once, but its coins stay unspendable until the round's transaction confirms on-chain, and that block took 64 minutes. The quote found no spendable coin and gave up with the generic message; nothing reached the server. - The quote now tells apart (lnShortfall): money held by another payment (wait, as before — now by sum, not one coin covering it all), money a renewal holds ('Nothing was sent. N sats are being renewed…'), and a real shortfall ('needs X sats with its fee, Y available'). Coins leaving in an offboard or exit are never waited for. - The manager's 'insufficient ark balance' names a renewal too. - The coins page says before the tap that renewed coins can't be spent until the round confirms, and that renewing every coin leaves nothing to pay with meanwhile. tools/ark/ln-shortfall-test.js Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Video frames from bytes, not from a CORS player; pager clips load plainly againAdam Soltys12 days2-10/+25
| | | | | | | | | | | | | | | | | | | | | | | | | | | Regression from 6488806: pager clips were loaded crossorigin so their frame could be kept, and on a phone the large clip's slide went black. Some hosts (Primal's R2 bucket) send CORS headers only to a request with an Origin and don't Vary on it, so a copy the browser cached from a plain player load answers a later CORS load without them, and it fails. The same thing made the size probe fail for any clip that had played in the feed. - Pager clips load plainly again; the still under a tap-to-play cover is restored on every return. - The probe (and its HEAD) go past the browser cache (no-store). - A clip's frame is now made from bytes: the opening of the file (index plus about 1 MB, no-store) decoded from a blob and kept as a JPEG — for tiles and as the pager's poster. No CORS player anywhere. Files whose index trails, or hosts whose bytes can't be read, keep the plain player still (shown, not kept). - Kept frames are 480 px wide (were 270) and capped at 600. - Probe failures are stored as failedAt, so earlier failures are asked again at once. - Test: an R2-style host, its clip cached by a plain load first, is still measured and its frame kept (failed before this change). Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Pager: a large clip keeps its picture when you come back to itAdam Soltys12 days1-0/+4
| | | | | | | | | | | | Leaving a slide lets go of its file, and coming back skipped the still because the cover was already there — black again. The still is now restored on every return. Pager clips also load CORS-readable (a plain retry for hosts without CORS), so their frame is kept the first time — the still under a cover and any clip that plays — and from then on the poster alone covers it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Pager: a large clip shows its frame under the tap-to-play coverAdam Soltys12 days1-1/+4
| | | | | | | | | | | | | The slide's poster was fixed when the pager was built, and a clip over budget held no source, so a clip whose frame was kept a moment later (or never) was a black screen with a button. Now the kept frame is looked up as a slide comes near; without one, the clip's first frame is loaded under the cover (only the start of the file, when it leads with its index or is served in ranges) and kept for next time. A tap plays it from the start. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Video tiles: a frame even when the size check fails; retry it laterAdam Soltys12 days1-10/+27
| | | | | | | | | | | | | | | | | | | | Efrat's clip (blossom.primal.net → r2a.primal.net) showed a blank tile on a phone: the 64 KB probe failed there (slow connection), the failure was kept for good, and a tile with a failed probe never tried for a frame. - A failed probe is dated and asked again after an hour (it was kept forever; old 'failed: true' entries are retried). The probe waits 10 s. - A tile whose probe failed still tries for a first frame, giving up and letting go of the file after 8 s. - Primal's CDN lets scripts read the bytes but not Content-Range, so the size was unknown: a HEAD's Content-Length (readable by any page) fills it in. - No size in imeta but a bitrate and duration (Primal posts those): the size is estimated from them. 'dim 1080.0x1920.0' is understood. - A tile with nothing to show is dark, like a video, not a pale gap. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Media views keep their own cache and crawl for media directlyAdam Soltys12 days1-0/+145
| | | | | | | | | | | | | | | | | | | | | | | | | | Videos was slow because the grid read from the feed's page of posts: the feed cache holds 30, so after every reload the grid re-crawled the relays, ten posts a step, each post first waiting (up to 8 s) for its avatar, pictures and link cards — none of which the grid shows. - Each feed now keeps a media index (IndexedDB, src/media-cache.js): the posts carrying pictures and those carrying clips, 400 of each, and how far back it has looked. The grid and pager read from it; a reload paints them at once. - The crawl reads older posts straight into the index, 100 at a time, without the readiness wait and without touching the feed's own page; it resumes from where it stopped. The bottom of a feed is looked for again after a day. Curated feeds still page the usual way. - A tile's first frame is kept (a small JPEG in the Cache API, when the host lets its pixels be read) and painted as a picture from then on: in the grid, as the pager's poster and the feed's. - Probe results kept for 2000 clips (was 600). - Forgetting a wallet wipes the index and the frames. tools/feed-media-cache-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Videos opens on a grid of thumbnails; the pager waits for a tapAdam Soltys12 days1-5/+14
| | | | | | | | | | | | | | Choosing Videos no longer jumps into the full-screen pager: it shows the vertical clips as a grid, like Images, and a tile opens the pager on that clip. Every tile gets a first frame when that costs only the start of the file (the index leads, or the server serves ranges), whatever the clip's size; a clip over budget wears its cost as a badge. Only an index-last file from a server without ranges shows its cost instead. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Videos: know what a clip costs before fetching it; vertical clips onlyAdam Soltys12 days1-6/+59
| | | | | | | | | | | | | | | | | | | | | | | | | | - What a clip costs is learned up front: the post's imeta (size, dim, image) when it says, else one 64 KB ranged request — the total from Content-Range, whether ranges are served, whether the MP4's index (moov) comes before its data, and the picture size from the track header. Up to three at a time; remembered across sessions. It also replaces the player-based shape probe the feed ran on every clip. - Clips over 25 MB (8 MB on 2G/3G, 3 MB with data saver), above 1080p, index-last files over 8 MB from servers without ranges, or clips that couldn't be measured wait for a tap: a cover saying what they cost ("50 MB", "4K"). Settings → Nostr → Videos: Autoplay large videos. - The pager loads only the clip on screen in full; the next gets its opening if within budget; everything else holds nothing. Sizes are asked three slides ahead. - Feed clips fetch nothing until they're about to play (preload none), with the post's still as a poster. - Video grid tiles show a first frame only for small clips that start quickly; others show their cost. - Videos lists only clips known to stand taller than wide; one of unknown shape is measured and joins when it qualifies; choosing Videos with none known yet opens the pager on the first that turns up. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Feed media views: a grid of pictures, videos to flip throughAdam Soltys12 days1-0/+167
| | | | | | | | | | | | | | | | | | | | | | | All / Images / Videos sits under the feed chips (remembered). - Images lays the feed's pictures out as a three-column grid. - Videos opens straight into a full-screen pager: one clip per screen, snapping, the one on screen playing with sound (muted with an Unmute button when the browser refuses), the next a flick or ↓ away. Closing it leaves a grid of the clips. - The pager shows only the media until tapped; a tap shows the author (tap for their profile), the post's words as a caption without the media links (tap to expand), and likes and sats in the bottom right, which also like and zap. - It is a place: Back or Escape closes it. Built outside the morph, so a repaint never restarts a clip; off-screen clips let go of their media. - Near the end of the grid or the pager the feed fetches older posts and new slides are appended. - Toasts now sit above full-screen viewers. tools/feed-media-views-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Settings → Nostr: manage your relaysAdam Soltys12 days1-0/+138
| | | | | | | | | | | | | | | | | | | | A Relays card edits the two relay lists other Nostr apps go by: the NIP-65 list (each relay Read, Write or both) and the NIP-17 DM inbox. Each relay shows whether it answers (a dot; 'unreachable' spelled out). Both lists are replaceable events other clients write too, so the card edits only a copy it looked up across the index and archive relays, looks again just before publishing, and backs off (reloading the newer copy) if another app saved since. Only a list that changed is republished, to the relays it names, the ones it used to name, and the indexers. Saving updates our own idea of where we write (outbox lookups and wallet sync relays) and resubscribes the DM inbox at once. tools/relays-settings-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Zaps find lightning addresses our relays have prunedAdam Soltys13 days1-0/+56
| | | | | | | | | | | | | | | Zapping a post looked the author's lightning address up on our four home relays only. Those prune old events, so anyone whose kind 0 hadn't changed in a while (1776, Brad Mills) came back as "no Lightning address or Ark address" — while their profile page, which also asks the index relays and the author's own relays, showed the address right there. The zap lookup now asks both ways at once and takes whichever carries a lightning address. And when someone does have one but Lightning can't be paid right now, the toast says that instead of claiming they have no address. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VKv2uEFRR42Gp6NyqcN3A9
* White Noise attachments (encrypted-media-v2)Adam Soltys13 days2-0/+100
| | | | | | | | | | | | | | | | | | | Pictures and files now travel in White Noise groups and direct chats, both ways. A file is sealed under the group's media secret for the epoch its message goes out in (MLS exporter "encrypted-media" -> HKDF -> ChaCha20-Poly1305, AAD bound to hash, type and name), uploaded to Blossom, and sent as a kind 9 with a v2 imeta tag. A commit landing mid-upload moves the epoch, so the file is sealed again rather than sent under a stale key. Receivers keep each epoch's media secret for as long as a message from it is in the log, so older pictures still open. Groups founded here now carry and require marmot.group.encrypted-media.v2 (blossom-v1 on blossom.ditto.pub and nostr.download). Checked against MDK's wn: our pictures decrypt in wn, wn's in ours, and wn uploads into our groups by our policy. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* DM threads: choose by hand whether a message goes by White Noise or Nostr DMAdam Soltys13 days1-0/+22
| | | | | | | | | | Where a person can be reached both ways (a direct White Noise group with them, or a KeyPackage from another app), the thread header now carries a small pill naming the way the next message will travel. Tapping it pins White Noise or Nostr DM for that thread, or puts it back on Automatic (reply the way they last wrote). Threads with only one road show nothing. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* White Noise can find a coinos account: relay lists go to its directory relaysAdam Soltys13 days1-0/+10
| | | | | | | | | | | The Android app said a coinos user "isn't on White Noise yet" although their KeyPackage was published: MDK will not invite an account whose kind 10002 and 10050 lists it cannot find, and it looks on relay.eu/us.whitenoise.chat and purplepag.es, where coinos never wrote. The client now passes the account's already-signed lists along to those relays (no signer needed, nothing rewritten), weekly and until both exist. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Profile: the revealed public key comes with its QRAdam Soltys13 days1-0/+72
| | | | | | | | Tapping "Public key" now shows the npub as a QR above the text, for a phone held up to the screen (White Noise, Amethyst, another coinos). The bare lowercase npub, which every scanner takes. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* README: where the White Noise code lives; the live UI test waits out a relay ↵Adam Soltys13 days1-1/+2
| | | | | | reconnect Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Group chats notice a relay that dropped their subscriptionAdam Soltys13 days1-1/+24
| | | | | | | | | A group that misses a commit is stuck until it hears it, and a group founded by White Noise can live on a single relay. Its subscriptions now report a close to the chat watchdog, and one made while a relay was being skipped as dead is rebuilt when that relay comes back. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Group chats live on relays White Noise will dialAdam Soltys13 days1-7/+14
| | | | | | | | MDK refuses relay.damus.io and relay.nostr.band outright (its retired hosts), so a group founded here must not name them: relay.primal.net takes damus's place. The UI test can also run against a deployed site. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* White Noise (Marmot) chats: MLS group messaging in native JSAdam Soltys13 days3-0/+1643
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The Chat tab now speaks the protocol the White Noise app speaks, beside Concord communities and NIP-17 DMs, without anyone having to pick one: - A two-person group with no name is a direct message. It lands in the same thread as that person's NIP-17 messages, and a reply goes back the way their last message came. A first message goes by White Noise only to someone whose KeyPackage another app published (a coinos wallet reads NIP-17 on all its devices; an MLS group reaches one). - Groups get rows of their own in the Messages list, a New group button, an invitation card, members, add/remove/rename for admins, leave. - Every wallet publishes a KeyPackage (kind 30443), so it can be invited. src/mls.js is RFC 9420 for ciphersuite 0x0001 on the noble libs (no WebAssembly), with the extensions-draft pieces Marmot needs (app_data_dictionary, app_data_update, app_ephemeral, self_remove). src/marmot.js is the Marmot layer: components, the account identity proof, the 30443/444/445 event shapes and the group engine (publish-before-apply, retained epochs, same-epoch tie-break). State is per device, in IndexedDB (src/marmot-store.js). The whole stack is one lazy chunk (72 KB). Tested against the RFC 9420 vectors (tools/mls-vectors-test.js) and against the Rust reference, MDK's wn CLI, at the protocol level (tools/marmot-interop-test.js) and through the real UI (tools/marmot-chat-test.js). Not yet: attachments in these chats, push for group messages, the app-message witness boost in fork selection. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Switching Spending/Savings opens Receive, with that account's addressAdam Soltys13 days1-1/+12
| | | | | | | | It opened the history tab. Any open payment detail still closes with the switch. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Spending settings: Move to Savings opens its form againAdam Soltys13 days1-0/+9
| | | | | | | | | | showSend flipped the wallet's tab to Send but left the screen where it was; from the Spending settings page (where the co-operative exit lives now) the form opened out of sight and the button seemed to do nothing. Opening Send now always means the wallet screen. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* The feed you were on is remembered per wallet; new ones start on PopularAdam Soltys13 days3-2/+76
| | | | | | | | | | | It was one device-wide key, so a new account opened on whatever the last one was reading (#gardenstr: the starter feed has the same id in every account). Now keyed by the wallet, re-read when a wallet opens (the feature outlives a sign-out), Popular when nothing is remembered, and the old device-wide key is dropped. A feed opened from a link stays put. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Send: a lightning address moves on to the amount by itselfAdam Soltys13 days1-0/+87
| | | | | | | | | | | | | - Pasted with the keyboard (not only the Paste button) it is complete text: it advances at once, as a paste always did through the button. - Typed and then left alone for 0.9s, it is checked quietly (names: our registrar or BIP-353 DNS; zaps: the LNURL-pay endpoint answers) and the form moves on only if something answers. A half-typed domain answers nothing, so it just waits, with no error; finishing it moves on. - tools/send-lnaddr-advance-test.js (live registrar/DNS, [email protected]). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Send: the recipient field grows to show it all; history steps aside while ↵Adam Soltys13 days2-9/+72
| | | | | | | | | | | | | | | | | | sending - The recipient is a one-line textarea that wraps and grows (rows from the text's length over the field's measured width), so a whole address is in view in the field itself — no second copy in small type below. Enter doesn't break it and pasted line breaks are dropped. Empty, it is as tall as its hint (measured in the hint's own face), so that's whole too. - Past the recipient — the amount, a review, a Lightning/Ark confirmation, a raw tx to broadcast — the history list below the send pane is left out. - tools/send-dest-test.js (390px and 1280px). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Feed form: relays and an every-post firehose; its Back works; a stronger ↵Adam Soltys13 days1-0/+83
| | | | | | | | | | | | | | | | | | | new-post tint - The new/edit feed form gets a Relays field (read this feed from these relays only) and "Every post on these relays", which saves a firehose feed (optionally curated) named after its relay and hides the people, packs and topics it doesn't need. It used to take a hand-typed /feed/all?r= link. - The form's Back did nothing visible: opening it made no history entry, so Back went to the page behind the feed and left the form up. The editor (and the gear page's Spending/Savings view) are now part of the navigation state. - New-post tint 9% -> 16% green in light mode; dark mode unchanged. - tools/feed-editor-relays-test.js. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* New-post tint fades the moment any of the post is on screenAdam Soltys13 days1-4/+10
| | | | | | | | | | | It waited for half the row to sit in view for two seconds; scrolling reset the clock and a replaced node lost it, so a tall post read in passing or one the window remounted could stay green for good. Now the first visible pixel starts a 3s fade (the row's background transition), marked seen by id, whether the reader stays or scrolls on. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Pictures paint with the page: column-sized copies, and CORS that worksAdam Soltys13 days1-0/+17
| | | | | | | | | | | | | | | | | | | | | | | | | | | Measured in the live tab (screencast frames): back from a profile, the feed's text was on screen ~200ms before its photo. The <img> was loaded and complete on the first frame — a 3024px, 12-megapixel original that Chrome re-decodes whenever the page is rebuilt. - Post photos wider than the column at 2x are painted from an 1100px WebP copy made once (fetch, createImageBitmap, OffscreenCanvas) and kept in Cache Storage; it decodes in a few ms, decoding=sync, inside the same frame as the text. GIFs and small pictures keep the original; the lightbox opens the original. Profile banners use the same copies, warmed on prefetch. - corsFetch: primal's CDN caches its no-Origin answer without Vary: Origin, so a CORS fetch after the <img> got that header-less copy and failed — which had written blossom.primal.net, m.primal.net and coinos.io off as 'no CORS', so their avatars never got local thumbnails and painted late from the originals. A refusal is now retried once past the cache before a host is blamed; the old list is dropped (nocors-v2). - DM list: the unread dot sits centred at the row's end, like the communities' below, instead of beside the time. - Balance card gear: the icon alone, no circle. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* "Start in point of sale" belongs to one wallet, not the whole deviceAdam Soltys13 days1-2/+11
| | | | | | | | | | It was one device-wide localStorage flag, so whoever signed in next on a till landed in the last owner's point of sale. Now it is keyed by the wallet (still per device); the old flag is dropped since it can't say whose it was. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Balance card gear replaces Accounts; Spending's coins live behind itAdam Soltys13 days4-26/+54
| | | | | | | | | | | | | | | | | | | | - The "Accounts" pill on each balance face is now a gear. It opens that face's settings: Spending leads with its coins (renewal, the co-operative move to Savings, unilateral exit), then Security (lock, change password), the recovery phrase, auto-logout; Savings adds its public key. A wallet with no nostr identity also gets sign-in and log-out there, since it has no profile page to hold them. - The exit cost is no longer said under the balance; the Spending settings state it, always, not only when coin chains run deep. - The lone Manage link under the balance is gone (the gear is the way in); warning notices that are about the coins link to the same page. - The Accounts list, Add wallet and Delete all are no longer reached from the wallet (Delete all stays behind profile > Log out > forget). - Tests follow the gear: padlock-ask, lock-wallet and account-access pass again; cooperative-exit-ui clicks the Spending gear. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Never write a follow list over one we failed to findAdam Soltys13 days1-0/+176
| | | | | | | | | | | | | | | | | | | | | | | A kind 3 is replaceable: publishing one built on a list we didn't find wipes the real one wherever it lands. 2026-10-02, an Amethyst user with 398 follows (kept on her own relays and an archive) signed in to coinos for the first time; the starter-follows seed asked only our relays, saw nothing, and published Adam + coinos over her list. - deepContactList: our relays, the profile indexes, the archives, the person's own NIP-65 write relays, and Primal's signed copy (verified). - Starter follows only for a key with no follow list AND no profile anywhere ('new' meant new to coinos, not new to nostr). - A follow, follow-many or mute made while we have never seen the list does the deep lookup first and builds on what it finds. - Settings > Nostr > Follow list: find older copies on history-keeping relays and restore one as a union with the current list (nobody is removed), for anyone already hit. - tools/follows-overwrite-test.js: fake relay network, only an archive holds the list; fails on the old code with the exact two-person event. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* NWC: lookup_invoice, so BTCPay Server can connectAdam Soltys13 days3-0/+79
| | | | | | | | | | | | | | | | | | | BTCPay's NWC connector requires get_info, make_invoice, lookup_invoice and list_transactions; we never offered lookup_invoice, so it refused the connection string. - ArkManager.lnLookup answers lookup_invoice from wallet state (by hash or bolt11, either direction; pending/settled/expired/failed, preimage once claimed). A retry's several actions on one hash defer to the furthest. - The open app pushes a pending receive along before answering, so a merchant polling sees it settle. - The service worker now answers make_invoice and lookup_invoice too, and claims a paid invoice's HTLCs during the lookup: a BTCPay sale settles with the wallet closed. Its list_transactions reports receives as well. - get_info and the 13194 info event list the new method. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Bigger new-posts pill that swipes awayAdam Soltys14 days1-1/+25
| | | | | | | | | | The pill is thumb-sized now (16px, 12/24 padding) and rides above the Post button on a phone. Drag it sideways or down and let go: it flies off and stays away until more posts arrive than it was showing. A short drag springs back and is not a tap. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Tall videos fill the column with no side bars, up to the screen's heightAdam Soltys14 days1-1/+1
| | | | | Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Shared note links open on the post, with names and facesAdam Soltys14 days1-0/+119
| | | | | | | | | | | | The server renders /nevent1 and /note1 pages with the thread inlined (post, root, replies, kind 0s) and Open Graph tags; the client seeds its thread cache from it and opens the thread on the first paint. Without it, a pending frame holds the screen instead of the front door. Profile lookups ask our relays and the profile indexes at once, streamed, instead of one after the other behind five-second waits. A thread opened straight from a link has no Back button. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* New posts go in above the first post at the top of the feedAdam Soltys14 days3-8/+40
| | | | | | | | | | | Arrivals used to wait behind the pill whenever the feed header was on screen. They now go in above the first post with that post held where it is, so the page grows upward and can be scrolled to before tapping the pill. While the composer or another header field is in use they still wait. The pill clears only when the reader scrolls up to the newest post, not on the hold's own downward correction. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Dismiss photos immediately on tap at any zoom levelAdam Soltys14 days2-22/+86
|
* Warm recipient portraits from Send suggestions before selectionAdam Soltys14 days2-0/+131
|
* Sending on-chain from Spending states the fee and minimum up front.Adam Soltys14 days1-0/+102
| | | | | | | | | | | The form asks the ASP for a co-operative offboard quote (mining share + service fee, minimum = fee + dust) for the address on screen, disables Send for a balance below it and points to Lightning. A too-small amount reads in words instead of the manager's internal error, and the unilateral-exit note only appears when co-operation actually failed. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VP5Ljqk7EYcm5QQAbzDwog
* Add cooperative coin exits and clarify profile, preview and zap controlsAdam Soltys14 days2-1/+160
|
* Clear abandoned payments, reset profile Pay and restore native messaging BackAdam Soltys2026-10-011-0/+206
|
* standalone-file-test takes a path, to check a downloaded copy.Adam Soltys2026-10-011-1/+1
| | | | | Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VP5Ljqk7EYcm5QQAbzDwog
* The standalone wallet opened from a disk shows its avatars, font and languages.Adam Soltys2026-10-011-0/+42
| | | | | | | | | | With no files beside it, relative punks/, locales/, nip46.js and jsqr.js looked on the disk. From file:// they now come from v3.coinos.io (images and scripts need no CORS; /locales/ now sends Access-Control-Allow-Origin on the server), and standalone.html carries Public Sans inline. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VP5Ljqk7EYcm5QQAbzDwog
* Stream ready feed posts without waiting for catch-up batchesAdam Soltys2026-10-011-0/+200
|
* Zaps and reactions get their own notification switches.Adam Soltys2026-10-011-0/+39
| | | | | | | | | | | | The notifier now watches kind-7 reactions p-tagged at the user (opt-in by an explicit flag, so older apps that can't word them stay quiet) and sends zap receipts as 'zap' to apps that report the category, 'payment' to older ones. Coinos zap receipts carry their amount into the push. The service worker names the reactor when it knows them and stays quiet for a muted one. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VP5Ljqk7EYcm5QQAbzDwog
* Each post's reply button shows how many replies it has.Adam Soltys2026-10-011-0/+68
| | | | | | | | | | Replies (kind 1 and NIP-22 comments) are asked in their own batched #e request beside the likes/zaps one and counted under every post of the conversation they name; quotes ('mention') aren't replies, muted authors aren't counted, and a sent reply counts at once. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VP5Ljqk7EYcm5QQAbzDwog
* The Back button under a post returns to the feed where the reader left it, ↵Adam Soltys2026-10-011-0/+71
| | | | | | | | | | not its top. It now walks history like the system Back (whose popstate restores the scroll place); the profile page's Back does the same. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VP5Ljqk7EYcm5QQAbzDwog
* Serve a shared snapshot for the public feedAdam Soltys2026-10-012-0/+125
| | | | Build the default Popular feed once on the server and let browsers verify and paint its signed events before slow media finishes. Keep custom relays, pagination, filtering, and relay fallback on their existing paths.\n\nAdd stale-while-refresh persistence, service-worker bypass, development integration, and unit/browser coverage.
* A video in the feed paints at its own shape, not a 16:9 frame with black ↵Adam Soltys2026-10-011-0/+117
| | | | | | | | | | | | | | | bands beside a portrait clip. The shape comes from the post's imeta dim when it gives one (Amethyst, Primal and others publish it), else from the clip's metadata fetched while the row warms (3 s cap), and is remembered on the device. The box is as wide as the column, never taller than 75vh; an unknown shape keeps 16:9. tools/feed-video-aspect-test.js: 3:4 and 16:9 clips measured ahead, a 9:16 one shaped by imeta alone (its file served too late to probe), a broken one. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WMMwWYJkPQxLgfMsY7hWcU