aboutsummaryrefslogtreecommitdiffstats
path: root/src/i18n.js
Commit message (Collapse)AuthorAgeFilesLines
...
* Point of sale: the tip screen shows an invoice error, and both screens say ↵Adam Soltys2026-09-211-0/+1
| | | | | | when the invoice is being made. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Point of sale: a bill, an optional tip prompt, one invoice, and a ledger ↵Adam Soltys2026-09-211-0/+30
| | | | | | | | | | | | | | | | | | that keeps tips apart. A new deferred feature (src/features/pos.js): type the amount in the display unit, optionally a note, Charge. With the tip prompt on, the customer sees the bill and picks a percentage (configurable, default 10/15/20), their own figure, or no tip; one Lightning invoice is made for the total through the Ark feature's receive (new hooks arkLnInvoice/arkLnWatch/arkLnUnwatch/ arkLnCancel wrap createLnInvoice and pollArkLn). Paid, expired and cancelled sales are recorded with the bill and the tip as separate figures, and the fiat rate at the time; the amount screen shows today's bills, tips and total. Doors: Settings → Payments card, and a button under the payment address on Receive (a receiveExtras hook the names pane renders). Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Settings → Nostr: make coinos the browser's handler for web+nostr: and ↵Adam Soltys2026-09-201-0/+6
| | | | | | | | | | bitcoin: links. njump's "Your default web app" is a web+nostr: link: it opens whichever site registered that scheme. A card offers to register this app (the browser asks), and ?u= now accepts the web+nostr: form. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Feeds: follow packs from following.space (NIP-51 kind 39089) as a feed's people.Adam Soltys2026-09-201-0/+5
| | | | | | | | | | | A feed can name packs as well as people: the pack's current members are its authors, read from the pack event itself and cached on disk, refreshed hourly when a feed using it is opened — so a curator's change reaches the feed by itself. The editor takes a pasted following.space link or naddr, or searches packs by title (NIP-50 on relay.nostr.band). parseNostrRef learned naddr. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Messages: every saved feed is a row on the home list, under Following.Adam Soltys2026-09-201-0/+3
| | | | Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Feeds: Following is one of many; topics from search and #tags; the bolt is ↵Adam Soltys2026-09-201-1/+18
| | | | | | | | | | | | | | | | | | | | | | | | amber only for your own zap. Custom feeds live in the synced messages state — a name, everyone you follow and/or hand-picked people, and topics (hashtags, any of them; with people set as well, only their posts on those topics — the way a nostr filter composes authors and #t). Each feed keeps its own notes, clock and disk cache; a chips row under the feed title switches between them and + opens the editor (people are added through the same search the Send form uses). A word in the header search is a topic too: 'Posts on this topic' opens a session feed for it, as does a #tag in any post body — Save feed keeps it. Topic-only feeds read the big public relays as well as ours, since a hashtag has no author whose outbox we could follow. The zap icon used to go amber once anyone had zapped a post; now only when you have, like the heart. tools/feed-custom-test.js drives the editor, the relay filter, a tagged post landing, the #tag link, reload persistence and delete. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Zaps: the sound can be switched off — from the held bolt or Settings → ↵Adam Soltys2026-09-191-0/+1
| | | | | | | | | | Nostr. A prefs flag (zapSound, absent = on) synced with the zap amount. The strike and the tally are unchanged; only the clip stays quiet, and the clip chunk is not even fetched while it's off. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Zaps: tap again while one flies, hold the bolt to change the amount.Adam Soltys2026-09-191-1/+3
| | | | | | | | | | | | | | | | A pending zap no longer refuses the next tap: the optimistic tally keeps one entry per zap and matches each report and receipt to the oldest it can describe, so two default-amount zaps stack, settle and hand over to their receipts one at a time. Instant post zaps run off the send screen's zap slot, so a second tap can't evict the one still resolving, and send action ids are unique within a millisecond. Holding ⚡ (500ms) opens the amount screen in an edit mode — Save alone, or Save and zap this note — from the action bar and the chat chips alike; the press's timer lives on the element so a background repaint mid-press can't turn a lifted finger into a hold. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Zaps: the sound, the sender, and one action bar for every post.Adam Soltys2026-09-181-0/+2
| | | | | | | | | | | | | | | | | | Tap feedback: a bolt strike, the amount popping off the button, the post trembling, and the zap.mp3 clip. The clip is 62KB of base64 and messages.js is boot code, so it is its own chunk, fetched once a zappable post is on screen and played inside the tap once loaded; the standalone build inlines it. Ark zap receipts are signed by the wallet key, which has no profile: they now carry the login identity as the sender (NIP-57's P tag), readers prefer it, and our own older receipts read as us. The action bar is the same six slots on every post: the zap button and the who-reacted chevron are always there (inert until they have something to do), so the first zap no longer reflows the row, and you can zap your own posts and messages. Community rows centre their unread dot off the edge. Also in here: reactions with custom emoji, reply-composer focus under the tapped post, profile fetch retry, and Lightning zap failures that say so. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Feed: what arrives waits behind the pill; the count bumps; no auto-flush.Adam Soltys2026-09-161-0/+2
| | | | | | | | | | | | | | Posts a refresh found slid in a beat after the cached page painted, moving what you had begun reading, and reaching the top of the page let waiting posts in with a re-render under a finger on its way to the pill — the tap landed on the post beneath. Now everything that arrives after the first paint, the boot catch-up included, waits behind the pill wherever you are; the pill is a keyed node whose count bumps on each change; and a tap scrolls to the top before painting so the new posts open under your eyes. A rebuilt follow list still rebuilds the feed in place. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
* Gift detail links the coin behind the gift, and the claim once claimed.Adam Soltys2026-09-151-0/+2
| | | | | | | | | | | An unclaimed on-chain gift has no transaction of its own yet — its funding transaction is presigned and only broadcast when someone claims. The coin it's locked against is on-chain from the start, so the detail page now links that outpoint's transaction on the block explorer, and the claim transaction once it exists. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
* Renewal copy: automatic when free and near expiry; early renewal insures the ↵Adam Soltys2026-09-151-1/+1
| | | | | | | uncooperative exit. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
* Renewal fee copy: how close the coin is to expiry.Adam Soltys2026-09-151-1/+1
| | | | | Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
* Coins page: renewal is automatic, and the copy says so first.Adam Soltys2026-09-151-3/+3
| | | | | | | | | | | | | | The renewal card opened with how to renew and mentioned only at the end that the wallet does it by itself, which read as an invitation to renew by hand. It now leads with the automatic, free renewal before expiry, then says renewing early is only for someone who would rather not count on the coinos server, with the round cadence taken from the server's own info (two minutes now, not the hour the old text claimed). The "past its expiry" footnote under the table is gone. Stale translations of the rewritten strings fall back to English. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
* Spending coins as a headed table instead of chip rows.Adam Soltys2026-09-151-4/+5
| | | | | | | | | | | Every row repeated "expiry", "exit cost", "renewal cost" as pills. The labels now sit once in a header row over right-aligned tabular figures; the table's wrapper scrolls sideways on a screen too narrow for it (the page never does), and it fits a 390px phone without scrolling. A tap anywhere on a row toggles it. A sub-satoshi renewal share reads "< 1 sat". Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
* A tiny coin's renewal chip says "under 1 sat", not "1 sat".Adam Soltys2026-09-151-1/+2
| | | | | | | | | | | | | The server prices a renewal on all the coins together: ppm accumulates sub-satoshi across every input and rounds up once. The per-coin chip rounded each coin's share up on its own, so nine 1-sat coins each showed "renewal cost 1 sat" under a total of 4 sats — as if the page were skipping them. The chip now shows the coin's real share: "under 1 sat" for a sub-satoshi one, rounded otherwise, and its tooltip says how the fee is priced. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
* A pack link in chat adds the pack in place; coinos has its own share link.Adam Soltys2026-09-151-0/+1
| | | | | | | | | | | | | | | | | | Tapping "Emoji pack" on a phone opened Vector: the chip was an outbound link to vectorapp.io, a domain the Vector app owns. The chip is now a button — one tap adds the pack right here, nothing leaves coinos. Packs also get a coinos share link, v3.coinos.io/emojis/pack/<naddr> (copy it from the picker's pack row). The Android app already claims every v3.coinos.io path, so on a phone that link opens coinos, which adds the pack once the wallet is open. Pack links from any host render as the chip; the naddr's kind is what's checked. Pack changes now save at once (learned shortcodes stay coalesced) — a remove followed straight away by a navigation was lost to the debounce. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
* Custom emoji in chat: :shortcodes: render as pictures, packs sync with Vector.Adam Soltys2026-09-151-0/+8
| | | | | | | | | | | | | | | | | | | | | | | A message from Vector says :pika_wave: and carries a NIP-30 ["emoji", code, url] tag for it; coinos showed the bare shortcode. Now the bubble shows the picture (in rooms, DMs, the feed and the public /chat page), a bubble that is only one to three of them shows them big, and a :code: reaction chip is a picture too. Sending: every :code: you type that resolves travels with its tag, so the other side renders it whether or not they hold the pack. Shortcodes come from packs you hold (NIP-51 kind 30030 sets) and from what you've seen in chat, so anyone can answer a :pika_wave: with one. The composer autocompletes on ":pi" (Tab/Enter takes the first), the reaction picker lists custom emoji first, and Vector's pack share link renders with an Add button; a pasted naddr works too. Packs live on your kind 10030 list — the one Vector reads and writes — so a pack added in either app appears in both. tools/emoji-test.js drives the real coinos room headlessly and checks all of the above. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
* An @handle in a bio or post opens the people search for that name.Adam Soltys2026-09-151-0/+1
| | | | | | | | A bare @name carries no key, so nothing can resolve it for certain — but it can be a tap that opens the search with the name filled in. The @ inside an email or nip05 is left alone, as is anything inside a URL. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Join suggests communities from the Vector Hub, searchable, beside the paste box.Adam Soltys2026-09-141-1/+6
| | | | | | | | | | | The Join panel used to be one empty box asking for a link nobody had. It now lists the Vector Hub's public communities under it — icon, name, member count, category, description, sorted by size, marked when you're already in one — fetched on demand and kept for the session. The same box takes a link or filters the list; a tapped suggestion previews exactly like a pasted invite, since every listing is a CORD-05 link. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Invite links open in Vector, communities sync across Concord clients, and ↵Adam Soltys2026-09-141-0/+3
| | | | | | | | | | | | | | | | | | | | | | | | | | | any emoji can be a reaction. Vector's bundle parser has no default for a channel's key and epoch, so a coinos invite — which sent only id and name — failed its whole JSON parse and the link did nothing there. Every channel entry now carries key and epoch (a public channel's key is the community_root at the base epoch, CORD-05 §1), and bundles minted before this are re-signed at the same coordinate once per device, so links already shared start working. Concord clients now keep their Community List as kind 33302 fragments (CORD-02 §8): addressable per index, NIP-44 to self, 32-byte values as unpadded base64url, embedded join material without its community_id, seed omitted when it equals current. coinos read and wrote only the retired single-event 13302 on its own two relays, so a community joined in Vector under the same key never appeared here. The fragmented list is now read from our relays, the stock CORD set and every held community's relays, merged, and written back in the same shape; the home view re-syncs after a minute and a live subscription brings a fresh write straight in. 13302 stays for older coinos devices. The reaction row keeps its quick emojis, recents first, and a + opens a searchable picker of a few hundred more — eyes included. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* A picture can be sent to a channel or a DM, encrypted, and read by Vector.Adam Soltys2026-09-141-0/+4
| | | | | | | | | | | | | | | The composer grows a paper clip. A picked file is encrypted under a fresh AES-GCM key and 16-byte nonce, the ciphertext uploaded to a public Blossom host that takes anonymous blobs (blossom.ditto.pub, then nostr.download as a fallback copy), and the pointer posted the way Vector posts it: a kind-9 with empty content and a NIP-92 imeta tag carrying url, mime, key, nonce, both hashes, size and dimensions. In a DM it is a NIP-17 kind-15 file message with the same fields as flat tags and the url as content, wrapped to the peer and to ourselves. Kind-15 file messages are now accepted on receive too, rendered like any attachment, and previewed as a photo in the thread list. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* A picture sent to a channel shows up as a picture, not an empty bubble.Adam Soltys2026-09-141-0/+1
| | | | | | | | | | | | | Vector attaches files as NIP-92 imeta tags: the blob is encrypted with a one-off AES-GCM key before it reaches Blossom, and the tag carries the url, mime, key, nonce, plaintext sha256 and dimensions. Such a message has no text, so the bubble rendered blank. The blob is now fetched (fallback hosts too), decrypted in the browser, checked against the hash, and shown from an object URL — a grey box its shape while it loads, tap to view full size, other file types as a download by name. The same tags render in DMs. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Logging out leaves one identity, not everyone on the device.Adam Soltys2026-09-141-0/+4
| | | | | | | | | | | | With several identities signed in, the profile's Log out now signs out only the current one: its accounts leave the session, directory, cache and vault, the others stay put, and the next identity takes the screen. The confirmation names who is leaving and says whether they can sign straight back in (a nostr login) or need their recovery phrase (a bare seed). Logging out of every account is a second, explicit button. Alone on the device, Log out is unchanged. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* A knight's helm, for one small knight.Adam Soltys2026-09-141-0/+1
| | | | | | | | | | A gifted hat that covers the whole face — its own square drawing over the avatar rather than perched on it: steel great helm, cross slit, breathing holes, a red plume. Never sold; the registrar's gift endpoint knows it beside the beta hard hat. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
* A reply or a mention on nostr can buzz your phone.Adam Soltys2026-09-141-0/+1
| | | | | | | | | | | | | The push notifier now watches kind 1 for the user's pubkey beside DMs and zap receipts, and pushes who, what, and an excerpt. The worker shows "New reply to your post" or "You were mentioned" with the text, and a tap lands in the Notifications list — as ?open=notifs when a window has to be opened, or as a worker message when one already is. It has its own switch, "Replies and mentions on nostr", in Settings → Notifications, on by default like payments and DMs. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
* A zap on a chat message shows the message, not "Fetching note…" forever.Adam Soltys2026-09-141-0/+1
| | | | | | | | | | | | A receipt for a zap on a chat message or a DM e-tags the rumor's id, which no relay has ever held as an event — so the row fetched it by id and waited forever. The rooms and threads we hold are searched first: the row shows the message's text and a tap opens that room or thread. A post the relays really don't have says so instead of spinning. And a coinos zap's own receipt beside the LNURL server's is one row, not two. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
* Who reacted, and what happened to your posts.Adam Soltys2026-09-141-0/+10
| | | | | | | | | | | | | | | | | A chevron on a post's action bar, shown once anyone has reacted, boosted or zapped it, opens a panel that says who and with what: each zap on its own line with the amount and any message, then each emoji with the faces and names behind it, then the boosts. Every face opens the profile. The zap tally now keeps who sent each receipt, not just the sum. And a Notifications list on the chat home, under the feed: every like, boost, zap, reply and mention that names you, one filter on the relays we read, painted from last time while the relays are asked and live while open. A row opens the post it is about — or the reply itself — and the entry counts what arrived since you last looked. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
* The renewal happens whether or not anyone opens the app.Adam Soltys2026-09-131-0/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Every coin expires, and until now the only thing standing between a balance and its expiry was maybeAutoRefresh: it renews when the wallet is opened inside the last 96 blocks, roughly sixteen hours. Bark's own developers say the same premise fails on mobile — people don't reliably open a wallet at the right hour, which is why every mobile Ark client they know of has moved to delegated refreshes and why they are building server-driven renewal. captaind already has it, and ark.coinos.io has been carrying the column unused: SubmitRoundParticipation takes a scheduled_height, and the server holds that participation out of every round until the height arrives, then runs it with nobody online — the round tree branch is signed by the server, which is the whole point of a delegated refresh. The fee is priced at the height it will run at, so a standing order lands in the free bracket rather than today's. So each spendable coin now gets one registered 36 blocks before its expiry, six hourly rounds of slack. It is deliberately not an action: the coin stays spendable, the wallet never reads as busy, and spending it simply kills the order — the server drops a participation whose input is gone, and _driveScheduled drops ours. The claim comes later, whenever the wallet next opens: the mailbox breadcrumb we already post wakes it, and the leaf cosign and forfeit are the ordinary refresh path from there. Nothing about the input's expiry blocks that claim, which is what makes this worth having. A wallet that goes quiet for a month comes back to a coin already renewed and anchored in a fresh tree, instead of one the ASP has swept and would have to honor out of goodwill. One cycle per visit is all it can buy — the replacement's id doesn't exist yet, so its own backstop waits for the next time the app is opened. tools/ark/scheduled-refresh-test.js covers the five behaviours on regtest: the server holds the order until its height, runs it while the wallet is closed, the wallet claims on return, a spend voids the order, and a claim still works after the input has EXPIRED (the last one mines a coin's whole lifetime, so it also re-refreshes the funding wallet first — an expired vtxo can be renewed but not sent). Riding along: the expired-coin notice that was already sitting in the working tree, and manager-test's tampered-vtxo check, which had been reading base64 vtxo bytes as hex since they moved to IndexedDB. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019fMdLypdg5JaU7bdeUFF8v
* The feed keeps up, and stops moving the page while you read.Adam Soltys2026-09-131-0/+2
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A phone freezes a backgrounded tab: timers stop and the relay sockets are cut, so the feed came back holding whatever it had when you left and only caught up if you pulled it down by hand. Features now hear about the return — app.js calls f.resumed(awayMs) on focus — and the feed answers it by forcing a pass past the thirty-second throttle and rebuilding its subscriptions, which were talking to sockets that no longer exist. A blink between apps (under five seconds) is left alone. That catch-up raises the second problem rather than solving it: dropping eight posts in at the top moves the words under the thumb of whoever was reading. So posts that arrive on their own — live, or as a catch-up — wait when you're scrolled away from the top. A floating pill says how many, and the tap that lets them in also takes you up to them. Scrolling back to the top by yourself lets them in too, since up there they cost nothing. Anything you actually asked for still goes straight in. Also, on profiles that still show an npub: the batch now falls back to the relays whose whole job is indexing kind 0 (purplepag.es and friends), which we never read notes from and so never asked. profile-batch-test's "handful of asks" bound was a fraction of the row count, which that fallback broke; the property it means to assert is that the number does NOT grow with the rows, so it is a constant now. tools/feed-live-test.js drives a real signed event down a real open subscription — an unsigned fake is dropped by the signature gate and would prove nothing — and checks the page does not move, the pill says "1 new post", tapping it scrolls to the top, and coming back from hidden re-asks the relays on its own. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* See the post before you post it.Adam Soltys2026-09-131-0/+4
| | | | | | | | | | | | | | | | | | | | | | | | A draft is written as plain text but doesn't arrive as plain text: a URL ending in .jpg becomes a picture, a youtu.be link becomes a player, a nostr: reference becomes the note it points at, an npub becomes a name. The only way to find out how yours would land was to send it. The eye beside the paperclip — on the composer and on a reply — opens a preview. It runs the draft through noteBody, the very renderer the feed uses, so this isn't a lookalike of the real thing: it IS the real thing, in a dashed frame with a PREVIEW label so it can never be mistaken for the post itself. It follows the draft live while it's open (and only while it's open — a render per keystroke is not free). Attaching a picture opens it by itself. Showing you what you attached is the answer to the question attaching it raises, and it costs no tap. tools/compose-preview-test.js covers both the composer and a reply: the picture renders rather than sitting there as a URL, the YouTube link becomes a player, the preview tracks typing, and it puts itself away. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* The front door points at your own keys.Adam Soltys2026-09-131-1/+10
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Four buttons of equal weight, and only the sign-in ones said what they did. "Get started" said nothing, so the concrete offer on that screen was Google — which is a convenience wrapped around a seed phrase, not an alternative to one. Now the page says what the wallet is before it offers a button, the way in reads "Create a new wallet" with the cost under it (a minute, twelve words), and the three sign-ins sit below a line labelled "or, if you already have an account". And the second half of the same problem: a wallet that arrives by sign-in has a recovery phrase its owner has never seen and nobody can reissue. So the wizard shows it once, right after the first login, while it still reads as setting up rather than as a warning out of nowhere. "I've written it down" is the way on; "Later" is a real answer, and the wallet then keeps one quiet line — not a modal, not dismissible, gone the moment the phrase is actually seen, here or in settings. seedSeen records that per account: creating shows the words, importing means they typed them, revealing in settings counts. Only a derived seed starts false, which is exactly the case worth asking about. tools/onboard-backup-test.js covers both halves, including that "Later" leaves a reminder that survives a reload and leads somewhere. The sixteen tests that clicked the old button by its text are updated, and it has an id now so its copy can change without breaking them again. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* A YouTube link in a post shows the video.Adam Soltys2026-09-121-0/+2
| | | | | | | | | | | | | | | | All three shapes it comes in — youtube.com/watch?v=, youtu.be/, and a Shorts link — become the video's still with a play button on it and a corner link out to YouTube. A ?t= keeps its start time. Anything that merely looks like one (example.com/watch?v=…) stays a link. The player itself arrives on the first tap, not on render. A feed of ten videos would otherwise load ten YouTube players, each one telling Google what you're scrolling past before you've decided to watch anything — and on a phone, ten players is the scroll. One tap and it plays in place, from youtube-nocookie.com. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* The accounts screen says whose each wallet is.Adam Soltys2026-09-121-0/+1
| | | | | | | | | | | | | | | | | | Three signed-in identities produced three rows all called "Savings", one under the other, with nothing to tell them apart — indistinguishable from duplicates, which is exactly what they looked like. The wallets ARE per-identity; the screen just never said so. They're grouped under the person they belong to now: face, name, and a mark on the identity you're currently signed in as. One identity on the device gets no heading, because there's nothing to tell apart. "Whose wallet is this" also becomes one function rather than two: the accounts screen and the identity switcher were each working it out, and they have to agree or they describe different worlds. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* End to end over the real network, and the advice now fits the app it's in.Adam Soltys2026-09-121-0/+2
| | | | | | | | | | | | | | | | | | | | | | | | | | | A push sent to ntfy.sh reached a killed app on a real phone: 17:02:21.259 ntfy.sh → the phone's ntfy → broadcast to io.coinos.webview 17:02:21.308 app process started (it had been killed) 17:02:21.458 wake: job started 17:02:22.926 wake: finishing (page said so) — 1.7s, and "You were paid" One thing that took two goes: `adb shell am force-stop` marks a package STOPPED, and Android delivers no broadcasts to a stopped package unless the sender sets FLAG_INCLUDE_STOPPED_PACKAGES — which `am broadcast` does and a real distributor rightly doesn't. The first attempt looked like a failure and was a test artefact; `am kill`, which is what Android itself does to a backgrounded app, delivers fine. And the WebView build can now say something useful about push. It was inheriting the browser's line — "your browser has no push service, try Firefox" — which is nonsense inside a WebView that IS the app. It asks the host what's actually there and says: install a push app like ntfy (nothing installed), or wait a moment and reopen (a distributor that hasn't answered yet). Payments and messages still arrive whenever coinos is open, which is the part that matters most to someone reading an apology. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* Tell a phone with no push service what's actually wrong.Adam Soltys2026-09-121-0/+3
| | | | | | | | | | | | | | | | | | | | | | | | | "Registration failed - push service error" is Chrome's own wording, and it tells nobody anything. The cause is that Chromium routes web push through Firebase: on GrapheneOS — or any Android without Google Play Services — there is no push service to register with, so the app cannot be woken while it's closed, and no amount of tapping the toggle will change that. Both places that ask for push (the chat's offer and the NWC "answer while closed" toggle) now recognise that specific failure and say: this browser has no push service; on Android that's usually a phone without Play Services; Firefox brings its own and works there; and zaps are still answered whenever coinos is open. The last part matters most — the feature isn't broken, it's narrower than they thought. The failure is remembered so the app stops retrying it on every launch, and an explicit tap on the toggle always clears that and tries again, because a phone that changed browsers deserves another go. tools/push-unavailable-test.js runs a browser whose subscribe() fails exactly as Chrome's does and reads back what the user is told. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* Answer a post: like it, boost it, quote it, or stop seeing its author.Adam Soltys2026-09-121-0/+12
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Under every post now: a heart with its count, a boost with its count, and a quote. Behind an ellipsis beside reply and zap: quote, boost, copy the note's reference, mute the author, or block them. Likes and boosts come back in the SAME relay request the zap tallies already make — one round trip answers all three — and each action paints before any relay does, the way the zap chip learned to. A like is kind 7 and a boost kind 6, published to the author's own write relays as well as ours, so the person being answered actually sees it. A quote opens the composer with a nostr:nevent reference already in the text and tags the post on publish, so other clients render the embed. Taking a like back is the only withdrawal nostr has — a deletion request naming the reaction. Mute is the NIP-51 list (kind 10000): their posts leave the feed at once and the list is published, so it follows you to other clients. Block is the same list plus an unfollow, which is why it says so. And a bug the mute found, which the follow list had too: the optimistic paint was being read back as fact. Both toggles paint the answer immediately and then publish onto the newest list the relays will give them — but when the relays had nothing newer, "the newest list" WAS the optimistic paint, so the code saw the follow already present and published a list without it. Every follow was a no-op that looked like it worked. The base is now the relays' copy only when it's genuinely newer, and otherwise what we held before the paint. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* A picture in a post opens full screen, and the paperclip is a button again.Adam Soltys2026-09-121-0/+1
| | | | | | | | | | | | | | | | | | | | The attach control was a .btn-sm holding a 16px icon, wedged between two full-height buttons — a sliver to aim at on a phone. It's a 48px square the height of the row now, with a 20px clip in it. And tapping an image opens it full screen: the image centred on a dark ground, tap anywhere (or Escape) to close. Posts crop images to 320px so a feed stays a feed, which makes a photograph a thumbnail — this is where you actually look at one. Two details in the viewer. The page underneath stops scrolling while it's open, and the scrollbar gutter the page keeps reserved stands down with it: fixed positioning is sized to the viewport WITHOUT that gutter, so an "inset: 0" backdrop leaves a pale stripe down the side of an otherwise full-screen picture. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* Say the currency once, and stop hedging about old prices.Adam Soltys2026-09-111-1/+0
| | | | | | | | | | | | | | | | | | | | | | | | | The balance read "US$74.67 USD" — the symbol and the unit tag saying the same thing twice. Amounts now format bare, like sats and BTC do: whatever labels the number says which money it is. The currency symbol stays where a number stands alone in prose, which is the "Worth then" line and nowhere else. The '~' marking a payment older than any price we sampled is gone too. It was bookkeeping the beta doesn't need: a payment we have a price for is shown at that price, one we don't is shown at today's, and nobody has to learn a squiggle. The currency picker has moved to sit beside the language picker at the foot of the page — display preferences together, where they're findable. It spent one day under Settings → Network, where it wasn't. And a bug this feature exposed rather than caused: nine notices end in the word "sats" in every language we ship, and were being handed the display unit. "Uncooperative exit costs about US$2.03 sats" — and long before fiat, "about 0.00435397 sats" for anyone in BTC. They're given sats now, which is what they claim to be showing. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* Every payment remembers what it was worth.Adam Soltys2026-09-111-0/+5
| | | | | | | | | | | | | | | | | | | | | | | | | | | | A payment is worth what it was worth WHEN IT HAPPENED, and that number is gone by the time you look at it unless someone wrote it down. So every price the app fetches is kept as a sample — one an hour, a few months back — and a payment's fiat value is read off the sample nearest its own timestamp. The history rows and both detail pages (on-chain and Spending) pass the time the money moved; balances and forms use the live price, as they should. A payment older than any sample we hold is priced at today's number and says so with a '~', because that is a different claim and shouldn't look like the same one. The detail page spells it out: "Worth then $X · $Y now". Fiat is a third position on the unit toggle rather than a second number beside the first: sats → BTC → your money → sats. One figure, and which figure is your choice. The ring skips fiat entirely until a price has been fetched — a third position showing blanks is worse than two that work. Typing an amount with the unit on USD means dollars, priced now. The rates are coinos' own endpoint, a map of every currency it knows, so there's no third party watching what the wallet asks about; the currency picker under Settings → Network lists all of them. Changing it starts the recorded history again rather than relabelling samples taken in another currency. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* Follow people, read a feed of them, and put pictures in a post.Adam Soltys2026-09-111-0/+12
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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
* The zap chip lands on the tap and pulses until the payment settles.Adam Soltys2026-09-101-0/+1
| | | | | | | | | | | | | | | | | | | 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
* Zaps in chat, and a bolt + sats tally on chat messages, DMs and posts.Adam Soltys2026-09-101-0/+2
| | | | | | | | | | | | | | | | | | | | | 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
* Identity switching moves to the profile page, with faces.Adam Soltys2026-09-101-0/+4
| | | | | | | | | | | | | | | | 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
* Sync: oversized domains ride Blossom blobs — plus a coinos-hosted server.Adam Soltys2026-09-091-0/+7
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A wallet with deep Ark coins had a 633KB ark sync domain against the relay's 40KB budget: the publish silently failed forever, the slot stayed stale, and every boot retried. Relays can't carry it (64KB event cap) and neither can NIP-44 (64KB plaintext), so a domain over budget now goes to Blossom (nostr blob storage, BUD-01/02/06/11/12): sealed into an envelope ("CSB1" + xchacha20poly1305 under an HKDF of the wallet's NIP-44 self-key), uploaded to the configured servers, and the kind-30078 event carries only { blob: { sha256, size, servers } }. Readers resolve pointers from their own outbox copy first, else from the pointer's servers plus the configured list, hash-verified. Small domains are unchanged. The outbox uploads before it publishes (upload/delete authorizations are kind-24242 events signed at enqueue time, so delivery after logout still holds no key), keeps a local copy for recovery, and deletes superseded blobs once the replacement is acknowledged. Servers: Settings → Nostr → Sync storage (any Blossom server; a typed list is kept), else the user's BUD-03 list (kind 10063) with the coinos server as mirror, else blossom.coinos.io. blossom/server.js is that default: a Bun Blossom server that stores ONLY these envelopes (magic check → 415 for anything else), serves every blob as an octet-stream attachment with nosniff, requires signed auth for upload/list/delete, and enforces per-blob size, per-key quota (oldest evicted), rate limits, a disk cap and unused-blob expiry — it cannot serve as a file host. tools/sync-blob-test.js spawns it and covers the policy, the outbox flow and a fresh device resolving a pointer. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
* Add "Sign into another account" — switch identities without logging out.Adam Soltys2026-09-081-0/+2
| | | | | | | | | The only way to move between identities (e.g. Adam via a Nostr remote signer and passkeyguy via passkey) was logout → forget all data → sign in again, which wipes every account. But the machinery to hold several accounts at once already exists: enterWallet/addOrGetAccount ADD an account (they don't replace), the unlock screen carries the sign-in doors (Nostr/passkey/Google via unlockExtra) plus a fromWallet Back button, activateAccount cleanly tears down and re-inits per-account nostr-login state, and the Accounts screen already switches between saved accounts. The nostr-login handoff even saves its session AFTER activating the new account, so each identity's signer state lands under its own namespace and resumes silently (persisted passkey sk / bunker session). The missing piece was a non-destructive entry point. Added a "Sign into another account" button on the Accounts screen that opens the unlock screen in additive mode (ui.fromWallet), so signing in adds the identity alongside the current one — then switching is just tapping a row on the Accounts screen (reached via the balance card's Accounts button), no logout, no data loss. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Durable sync outbox: fix the fresh-device phantom "Sent" that showed a zero ↵Adam Soltys2026-09-061-0/+5
| | | | | | | | | | | | | | | | | | balance (authored by codex). Root cause of the "signed in and my balance is 0 with a huge Sent I never made": a device's snapshot carrying its change/new coins never reached the relay. The publish was a setTimeout debounce that got canceled on page-hide / logout / feature stop, so coins created by another session were never synced; a fresh device then loaded only the old (now-spent) coins, and reconcile dressed the spent inputs as a definitive "Sent" row and zeroed the balance. No funds ever moved — verified on the ASP that no such payment existed and the change coin was still spendable. Fix (codex): - src/sync-outbox.js (new): a durable outbox. Snapshots are signed synchronously at save time under the current identity (no cross-account mixing) and persisted already-encrypted to localStorage (no keys/plaintext in storage). Delivery retries with backoff and wakes on online/pageshow/visibilitychange/storage, surviving suspension, logout and relay outages. The last copy doubles as a local recovery source if an account cache is cleared. - sync.js: replace the cancel-on-stop debounced publish with the outbox; fetchAllStates also merges the outbox's own (possibly-unpublished) events so a rejected publish is still recoverable locally; forgetAll clears it. - nostr.js: factor out stateEvent() (synchronous signing) and decodeStateEvents(), shared by fetch and the outbox. - ark.js: adoptArkSnapshot() applies a synced snapshot to a LIVE connected manager without restarting payment ceremonies (a snapshot arriving while connected previously stayed invisible until reload); the pre-handshake hold-key merges instead of overwrites; and the reconcile row is reworded from a red "Sent -N" to a neutral "History incomplete / Payment details not synced" (↻) with an explainer, since a spent input proves neither an amount nor a date. - app.js: clearAll also clears the outbox (forgetAll). Tests (codex): sync-outbox-test, sync-browser-test (real Chrome: relay rejects a snapshot, browser closes, reopens and retries; a second browser recovers the balance + history without reload), sync-split-test, plus build and morph smoke — all green. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* LNURL auto-pay: budget presets + skip the review-screen flash on an armed pay.Adam Soltys2026-09-061-0/+1
| | | | | | | | | (1) The auto-pay opt-in on a fixed-amount merchant pay was a single checkbox that stored "up to the amount you just paid". Replaced with preset budget buttons — 1000 / 5000 / 10000 / 100000 sats — so you set the ceiling deliberately; the chosen amount is what's stored on a successful pay. (2) An NFC tap into an already-armed auto-pay briefly flashed the review/confirm card (the "Getting a quote…" spinner) before advancing to pay. startArkLnPay now decides up front — from the invoice amount and the stored budget — whether it will auto-pay, and the view renders a centered paying screen straight away (matching the paid screen's layout so it morphs cleanly into it), never showing the review. You just see paying → paid. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Fix the on-chain gift front-running vulnerability architecturally: ↵Adam Soltys2026-09-061-1/+2
| | | | | | | | | | | | | | | | | ephemeral-key (v2) gifts. The old on-chain gift presigned a SIGHASH_NONE/SINGLE spend of the sender's own coin — a blank cheque whose claimer output isn't committed, so anyone (from the link, or a mempool watcher once a claim broadcast) could redirect the presigned input to their own address via a higher-fee tx. A sweeper bot had been front-running coinos gift links for ~a month; it drained a real 5000-sat gift ("claimed! → empty wallet"). Root-caused via chain + code forensics: theft tx input0 sighash = SINGLE. New design (v2, lazy ephemeral key): the sender presigns a FULLY COMMITTED (SIGHASH_ALL) funding tx paying the gift to a throwaway key E, and puts E's private key in the link. The claimer broadcasts the funding tx, then a fresh SIGHASH_ALL sweep of E into their own wallet. Both txs commit to every output, so neither is redirectable in the mempool — redirecting needs E's key, which lives only in the link (the intended bearer property: holding the link is the only way to claim). Lazy: the funding tx isn't broadcast until claim, so an unclaimed gift moves no coins and is revoked for free by spending the still-reserved funding coin. Sender pays the funding fee (from change / the swept total); claimer pays the sweep fee (from the gift). - gifts-wallet.js: _buildGiftPsbt now builds+signs the ephemeral funding tx (coin -> E + change, SIGHASH_ALL); new GIFT_MAGIC_V2 codec (parseLazyGift/_encodeLazyGift: [magic][E priv:32][le32 len][funding tx]); giftClaimTxs returns the {funding, sweep} pair; giftOutpoints/previewGift read E's output; isRetiredGift flags the old v1 (compact + legacy PSBT) links. Fee threaded through _selectGiftCoins/createGift/createGiftAll/createGiftFromSplit. - gifts.js doClaim: broadcast funding (tolerating "already in mempool" from a co-holder/retry) then sweep; refuse retired v1 links with a clear message; verifyClaimWon now watches E's output. - i18n: giftChainWarn reworded (safe bearer link funding a fresh on-chain wallet); giftRetired added. Verified: focused crypto test confirms codec round-trip + both inputs signed SIGHASH_ALL (committed). Full regtest end-to-end: funding + sweep both relayed (child accepted spending an unconfirmed parent), mined, claimer address credited the gift. UI morph smoke passes. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Interim safety for on-chain gifts ahead of the crypto redesign. (1) On-chain ↵Adam Soltys2026-09-061-0/+1
| | | | | | | (Savings) gift creation now shows a hard bearer-risk warning: the presigned input is redirectable, a sweeper bot is actively front-running shared links, so share privately and claim immediately (Spending/ark gifts are unaffected). (2) The false "success" is fixed: a broadcast returning OK was never proof WE won — a higher-fee sweep can evict our claim (the "claimed! → empty wallet" the bot produced). verifyClaimWon now polls the funding coin after broadcast and, if it settles spent by a txid that isn't ours, retracts the celebration and shows the already-taken screen. Full architectural fix (ephemeral-key gifts, non-malleable SIGHASH_ALL claim) is next Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz