aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
Commit message (Collapse)AuthorAgeFilesLines
* Feed: a wheel scroll through a loading feed no longer stutters on a laptop. ↵Adam Soltys10 days2-9/+67
| | | | | | The phone was fine (its scroll is the compositor's) but the desktop's wheel coast is driven from the main thread, so every long task froze it, and a feed that was still loading ran dozens of them: each ready post went in through its own admitFeed → holdScroll → full render (200–450 ms apiece; a Popular switch did 49 in a few seconds, one of them 1.3 s). Inside those renders the window's list getter — run on every scroll frame too — filtered every note through the mute/spam/report checks, and each check re-derived the login identity (two parses of the stored state plus a bech32 encode): 11 s of a 2-minute profile. Now ready posts are admitted together, one render per 120 ms, and while the reader is moving the page the batch waits for a quiet moment (up to 1.5 s); nostrLoginIdentity keeps its answer for 250 ms (dropped by its own saves, a signer change or another wallet's key), npubOf is memoized, modOn reads the cached messages state, the foreign-language guess is remembered per post, and the window list is reused between renders. Measured on Adam's dev tab (tools/feed-scroll-jank.js: real CDP wheel notches, chip clicks mid-load, rAF frame gaps, long tasks, a CPU profile that names each busy stretch): stalled time during a switch 1.9–2.5 s → 0–83 ms, no frame over 50 ms on the wheel path, main thread busy 33% → 17%; settled scrolling was already clean. feed-streaming's "tap reveals the oldest ready new post" fails on the previous commit too. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Messages: attach and GIF inside the field, as in the post composer. The DM ↵Adam Soltys10 days1-35/+38
| | | | | | | and room composer still had them as buttons in its row — Adam's long DM to yorgos sat squeezed between a clip, GIF and Send. They now sit at the right of the field's last line (the field keeps room on that side, less with GIF alone), leaving the row as field + Send. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Lock with fingerprint or face. The padlock's "Protect this device?" now ↵Adam Soltys10 days1-1/+1
| | | | | | | offers a passkey instead of a password, and Settings → Security can turn it on or off. The saved-wallets vault is sealed under a secret only that passkey's PRF produces (behind the phone's fingerprint/face check, the passkey kept by the platform's secure hardware or password manager), so nothing in this browser's storage opens it. Opening the app on a locked device asks for the passkey by itself; a cancel leaves an Unlock button. The prompts that need the vault opened (save, forget, lock a stray wallet) take the passkey too; there's no Change password for a vault that has none; the recovery words remain the way back. The lock passkey has its own PRF salt and a marked user handle, not discoverable where the platform allows — passkey sign-in refuses it rather than derive a stranger's key from it. English + Italian strings. tools/passkey-lock-test.js (Chrome's virtual authenticator with PRF). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Notifications: a custom-emoji reaction shows its picture. djmeistro's ↵Adam Soltys11 days1-6/+22
| | | | | | | :peepoClap: read as the bare text "reacted :peepoClap:" — the notification row kept the reaction's content but not its NIP-30 emoji tag (and cut shortcodes at 12 characters). Rows now keep the tag's picture and the whole :name:, draw it like the reaction chips do, and a row saved the old way gets its picture when the relay hands the reaction again. tools/notif-custom-emoji-test.js. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Feed: a mention opens the person, not the post. A tap on @name in a feed ↵Adam Soltys11 days1-2/+4
| | | | | | | post opened their profile and then reached the row, which opened the post's thread on top — Adam had to tap the mention again from inside the thread. The mention stops its tap, and a post's row no longer opens its thread for a tap on any link in it (a mention, a URL, a #tag). tools/feed-mention-tap-test.js (on the old code the tap landed on /nevent1…). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Italiano: the locale itself and the language list (the previous commit ↵Adam Soltys11 days1-2/+2
| | | | | | | carried only the test's rename — see 3032399 for the description). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Translation: into the phone's language, and DeepL with your own key. ↵Adam Soltys11 days1-13/+62
| | | | | | | isolabellart saw the Translate button but nothing became Italian: the target was the app's language, coinos has no Italian UI (so it ran in English), and the language guess didn't know Italian. Posts now translate into the reader's choice (Settings → Nostr → Translation, any of 31 languages), else the phone's language when the app has no UI for it, else the app's; the guess knows Italian, Dutch and Polish. And, as Jumble offers: a DeepL API key of your own, kept on the device and sent per request as x-deepl-key — the registrar passes it to DeepL (api-free for ":fx" keys; DeepL refuses browsers) and never stores or logs it; a refused key or used-up quota says so once. names: translateDeepl (prose only, links/tags kept, detected source back, 401/402/422 statuses), CORS allows the header. Deployed to cs names. tools/translate-italian-test.js, tools/deepl-translate.test.js. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Messages: the Amber notice no longer waits for Amber to wake. "Messages sent ↵Adam Soltys11 days2-3/+11
| | | | | | | to you can't be opened here · Connect Amber" showed only once the Amber signer had resumed, and the DM drain is what resumes it — on a phone whose every wrap the old code had already logged as opened, nothing is pending, so nothing might ever ask. nostrLoginIdentity now says how the account came in (`via`), and an Amber account counts as one whether or not its signer is live yet (the notice, and the flag that re-opens written-off wraps once a decrypting signer takes over). tools/amber-dm-upgrade-test.js runs that case with ALL_LOGGED=1. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Composers: attach and GIF sit in the field's corner. The post composer's row ↵Adam Soltys11 days1-10/+16
| | | | | | | had grown to Post · 📎 · GIF · Preview · Cancel; the reply box's likewise. The paperclip and GIF button are now small borderless tools inside the text field, bottom right (the field keeps room under its text so nothing runs beneath them), and the row under it is only what you do with the post: Post, Preview, Cancel (Preview, Cancel, Reply in a thread). Tests find the tools in .compose-box. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* GIFs in replies. The thread's reply box gets the same GIF button as the post ↵Adam Soltys11 days1-1/+11
| | | | | | | composer: a pick lands in the draft (preview opened) and the reply goes out with the GIF's NIP-92 imeta. tools/gif-picker-test.js covers it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Feed: your own posts are in Following. The follows feed read only the people ↵Adam Soltys11 days1-2/+9
| | | | | | | you follow, so your own post was there just after you sent it (the optimistic row) and gone after a reload — Adam didn't see his latest. Like every other client, a follows feed now includes you. Someone who follows nobody still gets the "find people" invitation, with their own posts under it. feed-live-test's catch-up queue now goes only to an ask naming the post's author (the feed also asks for yours); tools/feed-repost-test.js checks your own post shows. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Feed: reposts. Amethyst showed Zapstore's repost of LWB's "Tankful" post in ↵Adam Soltys11 days1-9/+56
| | | | | | | Adam's follows; we showed nothing — a feed asked only for posts (kinds 1, 1068). A feed of people now asks for their reposts too (NIP-18 kind 6; topic and firehose asks stay posts-only), and a repost shows as the note itself, under "🔁 <name> reposted" — its reactions, zaps, ⋯ and thread are the note's. Only reposts that carry the note and whose copy checks out (kind 1/1068, the id the e tag names, a valid signature); a muted author's note stays out. A note shows once: as itself, or under the first repost of it to arrive, counting rows still warming at the door. Media grids skip reposts. tools/feed-repost-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* GIF search, from nostr.build's GIF index (gifs.nostr.build). A GIF button in ↵Adam Soltys11 days1-6/+31
| | | | | | | the post composer and in every chat composer (DMs, groups, communities) opens a picker: "GIFs from nostr.build" (linked, as their terms require), starter chips (gm, lol, yes, wow, …; the first runs on open), a search field (debounced 250 ms, no one-letter searches, one shared request per normalised query, never aborted, 429/403/503 answered in words), and a masonry grid whose tiles play the small H.264 copy only while near the screen, over the lqip blur, falling back to the animated preview (stills under reduced motion). A pick puts the original URL in a post — published with a NIP-92 imeta (m, dim, size, alt) — or, in a chat, sends it at once after whatever was typed. Media URLs not on gifs.nostr.build are dropped. Browser calls carry no headers: production needs v3.coinos.io registered in the nostr.build dashboard (localhost works as their shared dev client). tools/gif-picker-test.js runs against the live API. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Videos: a tap on the picture goes full screen. The corner full-screen button ↵Adam Soltys11 days1-0/+41
| | | | | | | is a reach on a phone, so a tap anywhere on a feed clip — outside the player's own big play button and control bar, judged from the tap's position since those live inside the video element — now goes full screen (a wide clip asks for landscape). Full screen, a tap pauses a playing clip, and a tap on a paused one (again outside its play button and controls) comes back out. A video's taps no longer reach the post either: a tap on the player's controls used to open the thread, which in full screen took the player out from under the reader. tools/video-tap-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Feed: a slow picture keeps loading in place instead of turning into a link. ↵Adam Soltys11 days1-3/+14
| | | | | | | A feed row waits up to 8 s for its pictures, then freezes as it is; a picture still downloading at that moment (Juraj's 3.5 MB PNG from blossom.primal.net, on a phone) was frozen as its bare URL, and only showed when the post was opened. Now a picture that was merely slow is drawn in a fixed 4:3 box (fitted inside, so nothing moves when it lands) and keeps loading; only a picture that refused to load (an error, not a timeout — mediaFailed) is still a link. tools/slow-image-test.js (a picture served after 11 s; on the old code it was a link). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Messages: NIP-42 inbox relays. isolabellart's kind-10050 inbox is ↵Adam Soltys11 days1-5/+31
| | | | | | | auth.nostr1.com and a personal relay, both of which answer a REQ for their gift wraps with CLOSED "auth-required" — we never authenticated, so DMs delivered only there (anything sent from Amethyst and other 10050-only clients) never reached v3. The DM subscription on our own inbox relays now authenticates as the DM identity (subscribeAuthed: per relay, AUTH then ask again, still closable; the AUTH event is signed before nostr-tools' cached auth promise so a failing signer can't wedge the socket), sends to a peer's inbox authenticate when a relay refuses the write with auth-required, and our sent copy also goes to our own inbox relays so other clients reading that list see it. Never Amber by intent (an app trip per connection). Checked live: auth.nostr1.com accepts the handshake. tools/dm-nip42-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Messages: Amber users can read their DMs. Signed in with Amber by intent ↵Adam Soltys11 days2-10/+71
| | | | | | | (NIP-55), nothing sealed to the login npub was ever decrypted — each decrypt would be a trip through the app — so isolabellart's thread with Adam showed only what that phone had sent. Worse, decryptorsComplete() counted the Amber signer as complete, so those wraps were tried on the wallet key alone and logged as opened, hidden for good. Now an interactive signer counts as incomplete (wraps wait, uncounted); Messages and the DM thread say "messages sent to you can't be opened here" with Connect Amber, which opens a nostrconnect:// link for the SAME account over NIP-46 (Amber's bunker mode; a different npub is refused, a callbackUrl reload resumes the handshake). Once a signer that can decrypt takes over, a wallet that ran with Amber re-runs the heal pass over every wrap the relays hold, so messages the old code wrote off come back. tools/amber-dm-upgrade-test.js (plays the remote signer over a shimmed relay). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Names: letters and numbers only, 2–24 — coinos v2's rule. The registrar ↵Adam Soltys11 days1-2/+10
| | | | | | | took a-z, 0-9 and . _ - (1–30), so bitcoin-education and a dozen others got through. A NEW claim must now match /^[a-z0-9]{2,24}$/ (v2's /^[\p{L}\p{N}]{2,24}$/u, held to ASCII since a name is a DNS label); a name already held (19 with . _ -, plus "f" and "g") is grandfathered — its owner can still update it — as are v2 migrations carrying their own name and npub-shaped defaults. /name/<n> answers invalid: true for a free name that breaks the rule; the claim form says "Letters and numbers only" as it's typed, and the suggested name from a nostr profile is stripped to letters and digits. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Posts: inline markdown. **bold** / __bold__, *italic* / _italic_, ↵Adam Soltys11 days1-2/+26
| | | | | | | ***both***, ~~struck~~ and `code` render in a post's plain text — what newsletter bots and Amethyst users type and Amethyst shows; we showed the asterisks. Strict about edges, so snake_case, x__init__y, 2*3*4, "$5 * 2" and a lone star stay as typed, and markers never reach across a link, mention or hashtag. No headings or lists in posts: "#" at a line's start is a hashtag here, and articles render their own headings. tools/post-markdown-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Sensitive posts: Popular leaves them out. The NSFW posts Adam saw (and ↵Adam Soltys11 days1-4/+45
| | | | | | | blocked) in Popular were Misskey artists over the mostr bridge — every one carried a content-warning tag (Misskey's "sensitive" media), and their profiles said so too (":role_nsfw:", "🔞成人向け"); the posts had no words to fail the language test, and nothing read the tag. Popular (and its picture/video grids) now drops a post marked sensitive (NIP-36 content-warning, a NIP-32 content-warning/nsfw label, an NSFW hashtag, :nsfw: or 🔞 in the text) and the posts of accounts whose own name or bio says they post adult work. Everywhere else a marked post is folded behind its warning ("Sensitive: <reason>") with Show, and left out of media grids — Settings → Nostr → "Fold posts marked sensitive" (on by default). On the live snapshot: 6 of 184 posts dropped. tools/sensitive-feed-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Messages: a tap on a conversation's face opens THAT person. The avatar's tap ↵Adam Soltys11 days1-2/+13
| | | | | | | handlers were attached with addEventListener, holding the person it was first drawn for; the morph transplants only h()'s on* properties, so when a new message re-sorted the list and the rows were patched in place, a face kept its old handler — a tap on Bill opened someone else's profile, and once that page had opened, a fresh render hid the evidence. The avatar now sets onclick/onpointerdown claimed in _evs (transplanted like any h() handler), and conversation rows are keyed by person so a re-sort moves them. tools/dm-row-resort-test.js (on the old code, Bill's face opened Alice). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Spending: a Lightning receive is one row. The ASP settles it as an arkoor ↵Adam Soltys12 days1-3/+6
| | | | | | | the wallet claims — recorded as ln-receive, without its coin's id — and the same coin can reach the mailbox too, which (seeing no movement for that coin) recorded a plain receive as well: two "Received 1,005" rows and a "Payment received! 2,010" (The Daniel). The claim now stamps its coins' ids on the ln-receive, the mailbox backfill counts them, and lnFoldedIds leaves a receive of a claimed coin out of history, the celebration, the arkMovements hook and the LN tagging — derived at read time, never deleted, so a synced copy can't bring the twin back. Claims recorded before the stamp match by amount within ten minutes. tools/ln-receive-twin-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* @mentions: "@" and a few letters in a post, a reply or an edit open a list ↵Adam Soltys12 days1-3/+95
| | | | | | | of people under the box — the ones you follow whose name matches first (at once, from what's here), then the header's people search. Picking one (tap, Tab or Enter) puts nostr:npub… in the text, shown everywhere as @name (NIP-27), and the post or reply goes out with their p tag so they're told; Escape closes. coinos-text's setSelectionRange walks into the <div>s Chrome wraps lines in after an Enter — it used to put any position past the first line at the end of the field. tools/mention-ac-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Zap receipts read as zaps. A receipt (9735, or coinos's 9737) reached as a ↵Adam Soltys12 days1-3/+13
| | | | | | | note — someone reacted to a zap, and the reaction was followed — showed as "a kind 9735 event this app doesn't show" under the LNURL server's name. It now shows the zapper's face and name, "⚡ zapped 21 sats" and what they said with it. tools/zap-receipt-row-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Profiles: Posts / Replies tabs. Replies were mixed into a profile's list, ↵Adam Soltys12 days1-7/+35
| | | | | | | tagged "↩ reply" — now Posts (the default) is what they posted and Replies is their side of other people's threads (NIP-10 replies, NIP-22 comments). Profiles load thirty mixed notes a page and older pages only on scroll, so a tab that holds few of its kind (someone who mostly replies, or never does) fetches older pages by itself — five at most per tab, so a person who never replies doesn't have their whole history paged through. tools/profile-replies-test.js; profile-feed-test waits for rows (the tabs come before them; the old "Latest posts" heading only appeared with notes in hand). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Amounts: no plus sign on money coming in either — the word (Received) and ↵Adam Soltys12 days3-8/+8
| | | | | | | the green say it. History rows, transaction details, the "payment received" screens (wallet, Spending, gifts, boards/onboards) and the till's paid bills. The till's tip choices keep theirs: "+$2" there is what a tip adds to the bill. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Amounts: no minus sign on money going out. The word (Sent, Renewal, ↵Adam Soltys12 days1-12/+12
| | | | | | | Exiting…) and the red already say it's a debit — history rows, the transaction details, the sent screens, and on-chain rows (whose negative net printed its own sign). Received keeps its +. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Follow packs: a Draftable (mutable.top) link pasted into the feed editor's ↵Adam Soltys12 days1-2/+15
| | | | | | | pack search finds its pack. Draftable makes ordinary kind 39089 packs — the pack index already finds them by title on damus / nos.lol / primal — but its links name the author by only the first eight hex (/draftable/d/<id>?p=<prefix>), which the link parser refused; the id is now asked for and the author matched by prefix. nostr.oxtr.dev (one of Draftable's relays, and the one holding the most packs) joins the pack index. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Video stills: past a black opening. vmedia took its still at half a second, ↵Adam Soltys12 days1-1/+2
| | | | | | | and a clip that fades in from black (7 of the 68 made in three hours) gave a black tile. Each frame's mean brightness now comes out of ffmpeg (signalstats YAVG on pipe:2 — /dev/stderr can't be opened from a Bun-spawned pipe), and a dark one sends it further in (2, 5, 10, 20, 40 s) for the first bright frame; a clip dark all the way through (a night scene, a boot screen) gets its brightest frame with the levels stretched (normalize). Stills are made again under a new key and URL (v=4), so neither the server's nor the CDN's black copies are served. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Videos grid: a tile without a still of its own shows the server's (vmedia ↵Adam Soltys12 days1-2/+14
| | | | | | | thumb.jpg) at once — about a second the first time anyone asks for that clip, a CDN hit after — instead of waiting on the browser's probe (64 KB) and frame grab (≈1 MB of the clip, decoded) per tile, which on a phone left most of the grid as empty boxes. The frame grab still runs for a clip the server had no picture for. A known-heavy clip wears its size badge over the still. vmedia makes 8 stills at once (was 4). tools/video-tile-poster-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Reactions: a custom-emoji reaction draws as its picture in the feed. ↵Adam Soltys12 days1-2/+7
| | | | | | | emojiImg falls back to :code: text while a row's media isn't ready, and the feed freezes that choice per row (so a post's body never reflows) — a reaction usually lands after its row was frozen, so :kek_lord: stayed text for good. Reaction emoji sit in the action bar at a fixed height, so they skip the freeze (reactImg), on the like button, the chips and the who-reacted panel. post-edit-test checks it (fails on the old code). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Translation: a hashtag in a script the reader can't read gets its ↵Adam Soltys12 days1-5/+11
| | | | | | | translation in brackets after it — "#総合部絵チャ (…)" — the tag itself kept as the link it is. A post that is only such hashtags now counts as foreign (it was judged by its prose alone, of which it has none, so nothing ever offered to translate it), and ⋯ → Translate on a post with no words says so instead of "already in your language". The registrar's language detection reads the glossed tags too; lang-guess's scriptOf takes a minimum (a two-character tag has a script too). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Translation: posts in another language are shown in yours — "Translated ↵Adam Soltys12 days1-5/+143
| | | | | | | from Spanish · Show original" under the words, automatic by default for posts the language guess calls foreign (Settings → Nostr → Translation switches it off and leaves a Translate link instead), and ⋯ → Translate for any post. The browser's on-device Translator does it when the language pair is already installed (desktop Chrome); otherwise the registrar's new POST /translate (names/translate.js): a cached, 4-at-a-time front for a LibreTranslate container on cs (names/compose-libretranslate.yml, v1.9.6, 32 languages; none for sw/mr/ta/te, so those readers get no offer). Links, nostr: mentions, :emoji:, #tags and long codes are never sent to the model — only the prose between them, batched in one call (src/translate-split.js, shared). Translations are remembered (coinos-translations, 300). tools/translate-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Accounts: switching no longer leaks the previous identity's notifications ↵Adam Soltys12 days1-8/+39
| | | | | | | and DMs into the next. A notifications query (up to 5 s on the relays) or a wrap mid-decrypt that was still out at the switch landed in the NEW account's list and was saved there under its key — a throwaway account showed adam's zaps, reactions and conversations for good. A session counter bumped in stop() is checked after every await on both paths; notifications only take events p-tagging this identity, and an incoming DM must come from or name it. Healing: notification caches saved before this (no notifsV 2) are dropped once and refetched; a cached DM thread holding a row from someone who is neither us nor the peer is swept. tools/account-switch-leak-test.js (fails on the old code: the reaction and the thread both leak) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* Posts: edit and delete your own. The ⋯ menu on your post gains Edit (a ↵Adam Soltys12 days1-7/+154
| | | | | | | kind 1010 edit beside the original — the draft NIP-41 Amethyst reads and writes; the feed, thread and quote cards show the newest text with "· edited") and Delete (a NIP-09 kind 5, after a confirmation). Edits and deletions are asked for in the same REQ as each post's tallies and its live subscription, and count only when signed by the post's own author; they're remembered between sessions (postEdits cache) so a deleted post never flashes back from a cached feed. A post signed by the wallet key before a login was linked is still signed by that key. A deleted thread root keeps its place as "deleted by its author". relay.coinos.io now accepts kind 1010. tools/post-edit-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
* DMs: a linked nostr login whose signer hadn't resumed yet (a reload before ↵Adam Soltys12 days1-10/+43
| | | | | | | the bunker reconnected, a session just dropped) counted as "every key is here", so each wrap sealed to the login npub was tried on the wallet key alone, given up after ~40 s and logged as opened — never asked for again (a phone kept 9 of 186 messages). Since b06bbb4 the check only passes with no login or a live signer that decrypts (or Amber); a drain without one waits for the signer instead of re-running the wallet key, and the slow retry is rescheduled in finally so an early return can't end it. "All promises were rejected" (every relay refused the bunker request) and "not open anymore" count as try-later, not "not ours". Heal v2 re-opens every wrap the relays still hold, marked done only once nothing it re-opened is still waiting for the signer Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MgAFLnkF7KrdhuyCLMNgtL
* Profiles: the Following / Followers counts no longer flash a dash before ↵Adam Soltys12 days1-1/+3
| | | | | | | they load — the number's place is held by an invisible stand-in and the real count fades in over it Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MgAFLnkF7KrdhuyCLMNgtL
* Feeds: Everything and Follows + lists are no longer built-in chips; any ↵Adam Soltys12 days1-20/+50
| | | | | | | custom feed can be one. The editor gains "Everyone on my lists and packs" (the people of all your other feeds and packs, read fresh each time the feed opens) and "Posts from anyone on hashtags I follow" (your topic feeds' tags and your kind-10015 interests, OR'd in beside the feed's people, or the whole feed when it has no people). The editor reads a feed as saved, never expanded, so the lists don't get copied into it Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MgAFLnkF7KrdhuyCLMNgtL
* Feed: the "new posts" pill only ever takes you up. It counts the new posts ↵Adam Soltys12 days1-20/+57
| | | | | | | you haven't scrolled up to yet (going up to one, or past it, takes it and everything below it off), and the tap glides to the nearest of them above the post at the top of your screen — reading one newer than the oldest, you were sent back down to the oldest. A new post peeking in at the top edge still counts as above you. The count updates as you scroll up, and the notice clears when none are left (this replaces the at-the-top check) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MgAFLnkF7KrdhuyCLMNgtL
* Notifications: a reply, mention or reaction your follows reported is left ↵Adam Soltys12 days1-2/+6
| | | | | | | off the list and the unread count, as a muted one is — the thread folds that post behind who flagged it, so the tap landed on a fold instead of the reply it promised. Checked at render time, so reports that load later (or a report taken back) apply to what was kept Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MgAFLnkF7KrdhuyCLMNgtL
* Chat: a one-tap ⚡ beside every bubble from someone else, in communities ↵Adam Soltys12 days1-5/+18
| | | | | | and DMs, vertically centred on the bubble — tap zaps the default amount (the strike starts at the bolt), hold sets the amount, without opening the message sheet first. Own messages, visitors and wallets that can't zap get none; Marmot groups have no zap yet and are left alone Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Front door: a "Migrate a coinos account" button under the sign-in doors, ↵Adam Soltys12 days1-1/+7
| | | | | | opening a page that says a wallet and its twelve words come first (Create, or the sign-in doors), and /migrate on a device without a wallet opens on that page instead of an unexplained welcome; while a move is queued the recovery-phrase step offers no Later. Spending step: an account whose custom name the registrar handed back (a wallet a sign-in restored) leaves the wizard instead of being shown a claim form it could only retype its own name into — claiming it again "succeeded" onto the same page; the claim form also waits behind "Getting things ready…" until the registrar lookups settle Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Feed images: the column copy and the grid tile are decoded, resized and ↵Adam Soltys12 days1-37/+12
| | | | | | WebP-encoded in a worker, one picture at a time, instead of on the page thread between scroll frames (a profile of the live feed showed 1.8 s of convertToBlob there, in tasks up to 383 ms). A picture that hangs or crashes the worker is given up and the worker restarts for the next; three in a row and originals paint for the session, as they do wherever workers can't run Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Post composer: Preview and Cancel stand as tall as Post and the attach ↵Adam Soltys12 days1-1/+1
| | | | | | button, in the same type Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Change username: a name this wallet's key claimed before a login identity ↵Adam Soltys12 days1-0/+11
| | | | | | was attached no longer reads as taken — the identity-signed claim is refused, so retry signed by the wallet key that owns the record Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Reuse feed overscan to reduce rendering during scroll coastsAdam Soltys12 days1-6/+15
|
* Footer: "Show your support" beside How it works opens the hat shop from any ↵Adam Soltys12 days1-0/+2
| | | | | | page (shown once a wallet is open, since the shop pays from it) Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Profile public key: solid border instead of dashed, a copy icon beside the ↵Adam Soltys12 days1-2/+2
| | | | | | key, and a slightly larger font (13px) Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Amethyst's feed model: an Everything feed (your follows, everyone on your ↵Adam Soltys12 days1-7/+64
| | | | | | lists and packs, and posts from anyone on a hashtag you follow: your topic feeds' tags and your NIP-51 interests, kind 10015) and a Follows + lists feed beside Following, built from your feeds and offered once there's something to add. Long names in search results end in an ellipsis instead of running off the card Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Desktop layout: from 900px the bottom nav is a left rail (your profile first ↵Adam Soltys12 days1-7/+28
| | | | | | with a bigger face and name, Settings last) beside a 600px page column, and from 1220px the wallet (balance, Receive/Send, latest payments) rides in a right column; search is a full field in the header there. Floaters re-centred on the page column, open chats still fill the window, phones unchanged Co-Authored-By: Claude Opus 5.5 <[email protected]>