aboutsummaryrefslogtreecommitdiffstats
path: root/tools
Commit message (Collapse)AuthorAgeFilesLines
* Reports: a person-level flag needs two follows to fold their posts; 'other' ↵Adam Soltys2026-09-271-3/+9
| | | | | | | | | | | never counts. One follow's mis-tapped "impersonation" on fiatjaf's key was folding his entire timeline. A flag on a specific post from one follow still folds that post. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
* Feed: the foot says when older posts are loading, and when there are none left.Adam Soltys2026-09-271-3/+16
| | | | | Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
* Media sources wait until the node is in the page; thumbnails use the cache.Adam Soltys2026-09-262-3/+8
| | | | | | | | | | | | | | | | | | | | | Every render builds a fresh tree to diff against the live one, and a media element starts downloading the moment its src is set, even off the document: a sixteen-second feed scroll built 204 <video> and 1,416 <img> elements whose loads the morph then threw away — 375 cancelled video range requests, and full re-downloads from hosts that send no cache headers. h() now puts the source on data-lazy-src and it becomes src only once the node is in the page (render promotes it; a MutationObserver covers nodes appended by hand); the morph treats the two as one attribute. The thumbnail maker fetched every original with cache: 'reload' (38 MB in one scroll); the face was just painted from that URL, so the cache serves. holdScroll now recovers an anchor row that a large insert above pushed out of the window, from the height cache. Tests: the YouTube test accepts the in-view players autoplay loads; the stability test watches the notice for the catch-up, since the new rows land inside the window's spacer. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
* Thread: the reply box appears on Reply, full width, with its tools on a row ↵Adam Soltys2026-09-263-6/+11
| | | | | | | | | | | | | | | | | beneath. A thread opened to read came with an empty field under its first post. The box now appears under the note whose Reply was tapped (or while a draft is pending), folds away once the reply is sent or Cancel is tapped, and takes Amethyst's shape: the field across the full width, attach and preview on a row beneath it with Cancel and Reply at the right. Tests: thread-reply's helper follows the new shape and expects the box to fold after sending; compose-preview and feed-list tap Reply before expecting a box (the thread card carries .thread-card as the marker). Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
* Notifications: the faces the page will paint are warmed before it opens.Adam Soltys2026-09-261-0/+100
| | | | | | | | | | | | | | | | The list was already cached locally and refreshed from the chat home, but each row only asked for its author's profile when painted, so the page opened as quiet circles filling in one by one. Now the actors' names are fetched in one batch and each picture decoded into a thumbnail the moment the list is read from disk or grows — which the chat home does before the page is ever opened. Test: notif-faces (stub relay answers the kind-0 batch; the pictures are fetched and cached from the chat home; every row has its face on the first frame). Note for tests: serve /verify-worker.js from dist, or every relay answer waits four seconds for the inline fallback. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Feed: only the rows near the viewport are in the DOM, however far you scroll.Adam Soltys2026-09-263-3/+135
| | | | | | | | | | | | | | | | | | A long scroll left hundreds of rows mounted, and every repaint (a zap count, a profile landing, the thirty-second sync) rebuilt and diffed all of them — the slowdown deep in the feed. Rows beyond two and a half screens either side of the viewport are now replaced by a spacer at their measured height, so the page keeps its length (scrollbar, Back, arrival anchoring behave as before) while a repaint touches a few dozen rows. After svelte-virtual-list: measure after paint, estimate unmeasured rows at the running average, hold the reader's row still when a measurement corrects an estimate. Applies to the profile notes list too. Tests: feed-window-dom (120 posts, 27,800px page, 12–24 rows mounted, anchor held, spacers both sides); feed-live looks at the top after scrolling up; the moderation test's stub relay no longer lets real posts in. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Feed: cached posts paint at once, never a spinner over them; the chat home ↵Adam Soltys2026-09-251-3/+6
| | | | | | | | | | | | | | prefetches the feed. The boot gate hid everything, cached posts included, until the opening page's faces and pictures had decoded — seconds of spinner on a feed we already held. Cached rows now paint immediately as they are and take their prepared presentation once the warm-up lands (the container carries data-booting meanwhile). The relay fetch starts at once instead of after the warm-up; arrivals still wait at the door. Opening the chat home warms and refreshes the current feed, so the Feed tap opens on the latest posts. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Prefetch a smaller feed and bound its local cacheAdam Soltys2026-09-242-5/+50
|
* Preserve restored thread state across rapid refreshesAdam Soltys2026-09-242-7/+27
|
* Paint cached thread counts and header hat on refreshAdam Soltys2026-09-243-2/+46
|
* Keep thread avatars and viewport stable on refreshAdam Soltys2026-09-242-1/+23
|
* Videos play by themselves, muted, while on screen — with an Unmute button; ↵Adam Soltys2026-09-233-5/+115
| | | | | | | | | | | | | | | | | | | YouTube too. An inline video starts as it scrolls into view (muted, the only way a browser lets a page start one), pauses as it leaves, and a corner button turns the sound on; the native controls' own mute keeps the button honest. A YouTube box loads its player muted once in view (enablejsapi, so the same button can unmute it over the iframe API) and pauses out of view; a tap on the poster still means sound. One IntersectionObserver watches every player. Data Saver keeps tap-to-play. Tests adapted to the feed's new admission rule (a row waits for its author's face and pictures): the markdown and post-actions tests seed the poster's profile and serve their pictures locally; a picture that cannot be decoded is now a link, by design. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Feed: threads cached locally, posts never inserted under the reader, rows ↵Adam Soltys2026-09-236-21/+407
| | | | | | | | | | | | | | | | | | | | | | | | | | | | keep their first look. Work by Codex, wrapped up and verified here. Threads: a viewed conversation is persisted (thread-cache.js, up to 12 threads / 160 events), so a refresh or a cached note link shows the root and ancestors at once instead of the selected reply and a spinner; relays are re-asked in the background and an empty answer keeps the cache. Feed window (feed-window.js): arrivals are merged without inserting between visible rows — a gap fill under the reader's eyes waits until that part of the feed is offscreen, prepends anchor the reading position, the notice counts what went in above, and only its tap goes to the top. Older pages are prepared a few screens ahead; a page's pictures and faces are decoded before it is shown. Rows keep the presentation they were admitted with (feedPaint): background profile and quote refreshes no longer rewrite text or swap faces mid-read; media dimensions are known before paint. Thumbnails queue instead of being dropped past the first three. Tests: thread-cache, feed-window, feed-stability; feed-live updated for the new arrival rules (the notice counts up while you read). Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Keep chat headers and composers pinned without page scrollingAdam Soltys2026-09-231-3/+51
|
* Moderation: the whole mute list everywhere, follows' reports fold posts, ↵Adam Soltys2026-09-232-2/+190
| | | | | | | | | | | | | | | | | | | | | | strangers' spam hidden. Mutes were only applied when the feed merged and painted. One predicate (hiddenWhy / hiddenPk) now gates the feed, thread replies, notifications, the who-reacted panel and counts, quote cards, rooms, the DM list and the DM push. The list itself is read in full — people, words, hashtags, whole threads, and the nip44 private half — and edited through one publish path (mute thread and hidden words from the ⋯ menu and a Settings → Nostr card). Reports (NIP-56) from the people you follow are read from their outbox relays every half hour; a post or author they flagged is folded behind who said so, with Show. Report… in the ⋯ menu publishes a kind 1984 and hides the post for you. Amethyst's stranger rules: a post of 60+ characters repeated under another id is a duplicate and five duplicates hide the author for the session; more than ten hashtags hides the post. Both switchable in Settings. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Social: NIP-51 lists as feeds, posts and people in the address bar, the ⋯ ↵Adam Soltys2026-09-231-0/+176
| | | | | | | | | | | | | | | | | | | | | | menu on every screen. Lists: a feed of hand-picked people is published under your key as a kind-30000 follow set (title, p tags, t tags), so Coracle, Nostria and friends show it; your existing follow sets from other clients turn up as feeds (relay copy wins when newer; private nip44 members read and followed, never rewritten). "Keep as a nostr list" in the editor; unticking sends a NIP-09 deletion and the feed stays. "Add to a list" from a profile or a post's ⋯; someone's lists show on their profile and open as feeds (a kind-30000 naddr works as a pack too). Links: a thread puts its nevent in the address bar, a profile its npub — copy, share, reload; Copy link / Share in the ⋯ sheet. Boot-time renders after a reload share one history slot, so Back goes to the screen before. Fix: the ⋯ sheet and the reaction picker were only drawn by the feed and notification views, so the button did nothing on a thread or a profile. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Payment detail: the unit is the tap-to-toggle tag; tests enter through the ↵Adam Soltys2026-09-2211-12/+12
| | | | | | | wizard's import line. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* Starter feeds: #gardenstr only until a Bitcoin follow pack is chosen.Adam Soltys2026-09-211-6/+5
| | | | | Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* New identities start with something to read: Adam and coinos followed, a ↵Adam Soltys2026-09-211-11/+15
| | | | | | | | | | | | | | | Bitcoin feed, and #gardenstr. A wallet the wizard made from fresh words, or a sign-in that found no wallet on its identity, gets one kind-3 update following the two coinos accounts (a sign-in identity only if it follows nobody yet), plus two starter feeds. The Bitcoin feed has a follow-pack slot (STARTER_BITCOIN_PACK) and reads the #bitcoin topic until a pack is chosen; #gardenstr shows what a topic feed is. Once per identity. The feed test now runs through the real first run, reading the words off the screen. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* First run: "Create a new wallet" shows the twelve words at once.Adam Soltys2026-09-2116-16/+16
| | | | | | | | | | | | No create/import tabs and no Generate button inside the wizard: the words appear the moment the button is tapped. Underneath, "Have an existing seed or entropy to import?" swaps to the import pane (which has its own entropy door), and "Create a new wallet instead" swaps back with fresh words. The create/import card elsewhere (adding an account) keeps its tabs. Tests that entered through the old tab go through the new line. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* Point of sale: a paid sale opens its payment detail, which now says bill and ↵Adam Soltys2026-09-211-1/+9
| | | | | | | | | | | | tip. The row finds the ln-receive movement by the invoice it recorded and opens the same detail screen the history page shows; the detail asks features for more (arkMoveDetailExtra) and the till adds Bills, Tips (with the percentage) and the note. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* Point of sale test: the amount's rendered size is checked.Adam Soltys2026-09-211-0/+1
| | | | | Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* Point of sale: in a currency the pad works like a till — digits shift in ↵Adam Soltys2026-09-211-1/+12
| | | | | | | | | | from the right. 5 is 0.05, 277 is 2.77, 5588 is 55.88; backspace shifts out; the "." key becomes "00". Sats are whole numbers and type plainly. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* Point of sale: /pos, a per-device till mode, an on-screen pad — and off ↵Adam Soltys2026-09-211-6/+14
| | | | | | | | | | | | | | | the Receive tab. v3.coinos.io/pos opens the point of sale once a wallet is up (claimed at boot before any feature reads the path), so the till can be bookmarked or put on a home screen. Settings → Payments gains "Start in point of sale on this device" (localStorage, not synced: the till, not every phone the owner carries). The amount and the custom tip get a 3×4 pad that edits the same field a keyboard or a paste would. The button under the payment address on Receive is gone. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
* Point of sale: a bill, an optional tip prompt, one invoice, and a ledger ↵Adam Soltys2026-09-211-0/+96
| | | | | | | | | | | | | | | | | | 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]>
* Feeds: a following.space link is read before any naddr in the pasted text; ↵Adam Soltys2026-09-201-1/+4
| | | | | | the test clears its field between searches. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Feeds: packs are found by a few letters of their title, and by ↵Adam Soltys2026-09-201-3/+11
| | | | | | | | | | | | following.space's real link form. NIP-50 search only knew the packs the search relay carried — 'Niche Accounts' lives on damus and nos.lol, not relay.nostr.band. So the editor now indexes the wide relays' packs once per session and matches titles and descriptions by substring. A following.space link is /d/<id>?p=<pubkey>, not an naddr; both are accepted. 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-4/+33
| | | | | | | | | | | 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]>
* Feeds: Following is one of many; topics from search and #tags; the bolt is ↵Adam Soltys2026-09-201-0/+130
| | | | | | | | | | | | | | | | | | | | | | | | 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-1/+11
| | | | | | | | | | 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-13/+32
| | | | | | | | | | | | | | | | 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]>
* Feed: live posts go straight in; the pill is a notice, not a gate.Adam Soltys2026-09-191-46/+34
| | | | | | | | | | | | | "Nothing new ever arrives" was four posts waiting behind the pill on the phone and two on the laptop. Now a post that arrives on its own goes into the feed at once, wherever you are: at the top it opens itself under your eyes; further down it goes in above the fold with the page held still (holdScroll), and the pill only says how many are up there. Its tap scrolls to the top; reaching the top by yourself clears it, since only the pill repaints and the keyed rows stay put. The small catch-up case gets the same notice. The feed-live test says so. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Feed: coming back settles once — a few new posts above you, a lot means ↵Adam Soltys2026-09-191-29/+98
| | | | | | | | | | | | | | | | | | | | | | | | the top. The catch-up on arriving at the feed asked every relay for its newest posts with no lower bound, and everything not already in the fifty-post cache went behind the pill, chunk by chunk: the count climbed in steps and stopped at the pending list's cap of 200, whether there were 12 new posts or 500. Now the pass is buffered and settled once, after every relay has answered. Posts older than the newest we had are gap fills and merge silently. Up to twenty genuinely new posts go in above what you're looking at with the page held still (holdScroll: the row at the top of the viewport stays put, measured after the paint so a browser that anchored on its own isn't corrected twice), so scrolling up reveals them in order, newest at the top. More than twenty means you've been away: the feed goes to the top, at the newest post, as on a fresh open. The window grows only by posts landing inside it, so a catch-up doesn't paint two hundred rows. The pill is now only for posts that arrive live while you read further down. The feed-live test covers the three arrivals, and its stale checks (auto-flush on scrolling up was removed on purpose; a fresh <img> reports complete a task late) now say what the app does. 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/+199
| | | | | | | | | | | | | | | | | | 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]>
* Accounts from the header, snapshot-first Spending restore, name ownership ↵Adam Soltys2026-09-186-0/+477
| | | | | | | | | | | | | | | | grants. Header buttons always leave the current feature screen; an account with no remembered nostr identity opens Accounts instead of nothing. Logout confirms in place. A scan finishing after logout or an account switch no longer drops the new session into offline mode (stateGeneration guards in sync + ark). Spending restores from the snapshot first. A migration grant names the identity that owns an address; its manager may register it without becoming that identity (names/ownership.js). Browser tests for account access, profile retry, spending restore, thread reply focus, custom-emoji reactions and registry ownership. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Offline, the chat shows what it read: messages cache on arrival.Adam Soltys2026-09-151-0/+70
| | | | | | | | | | | | | | | | The room cache was written only on our own sends, deletes and attachments, so a member who only reads had nothing on an offline boot — "No messages yet" over a room read the day before, and no faces or names with it. Messages now persist as they arrive (debounced past the backfill burst), so the last fifty per channel are there without a network. Our own face never waits its turn for a thumbnail and never gives it up to the budget, so the header avatar paints offline too. tools/offline-cache-test.js boots the PWA build, caches a room, reloads fully offline and checks messages, names and thumbnails are all there. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
* Chat keeps its relay subscriptions alive while the app is on screen.Adam Soltys2026-09-151-0/+96
| | | | | | | | | | | | | | | | | | | | | New messages rang in Vector but sat unseen in v3 until a reload. The resume hook only rebuilds subscriptions after an absence it was told about; a socket that errors while the page stays visible (a wifi hop, a laptop lid, a phone freezing the tab without the away timer noticing, another tab clearing that timer first) is dropped from the nostr-tools pool with every subscription it carried, and nothing dials it again. A watchdog now runs whenever the app is on screen: on focus, on becoming visible, and every ten seconds between. It asks the pool whether each relay the inbox and the rooms listen on still holds a live socket (a dial hung past 20s counts as dead, and a relay that closed a subscription on us counts too) and rebuilds the lot when one is missing, dropping a stuck relay first so the rebuild dials afresh. Rebuilds retry on a growing backoff while a relay keeps refusing. tools/chat-watchdog-test.js errors every socket under an open room and checks the recovery. 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-6/+17
| | | | | | | | | | | | | | | | | | 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
* A reply's quoted line shows its custom emoji as pictures.Adam Soltys2026-09-151-0/+10
| | | | | | | | | | The quote above a reply bubble, the composer's reply bar, and the same on the public /chat page rendered the raw :shortcode:. They now run the one-line snippet through the same NIP-30 tokenizer, cutting on text so a shortcode is never sliced in half. 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/+113
| | | | | | | | | | | | | | | | | | | | | | | 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
* Who reacted, and what happened to your posts.Adam Soltys2026-09-141-1/+1
| | | | | | | | | | | | | | | | | 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
* A new post opens up instead of appearing.Adam Soltys2026-09-141-1/+15
| | | | | | | | | | | | | | | | | | | | | | A post let in at the top of an open feed was there on the next paint — one frame nothing, the next a whole row, the page shoved down by that much. Now it opens over a third of a second: height from nothing to its own, fading in as it goes, and a repaint mid-way resumes where it was. The same happens to the posts the pill lets in. That needed the morph to know which row is which. Children were matched by position, so a new first row was patched onto the old first row's node and every row below onto its neighbour's — the node the new post was built as never reached the page. A list whose every child carries a data-key is now reconciled by key: new rows are inserted as themselves, the rest keep their nodes and move. Feed rows and their hairlines carry the post id. Partially keyed lists keep the positional rule. feed-live-test catches the arriving row mid-animation: shorter than it will be, one animation running, and settled clean afterwards. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
* A post arrives whole.Adam Soltys2026-09-141-2/+37
| | | | | | | | | | | | | | | | | | A feed row painted the moment its event landed and then finished itself over the next second or two: a punk turning into a photograph, a blank gap becoming the picture. Now a post waits at the door until its author's profile has landed and their face and the images in its body are fetched and decoded — bounded by two seconds, so a slow host holds a post briefly rather than the feed forever. This covers the first load, live arrivals (which are counted behind the pill only once whole), paging older posts, and profile pages. The cached page at boot still paints instantly and warms in the background. feed-live-test serves a deliberately slow picture and checks, from the first instant the row exists, that its image is already decoded. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
* A zap answers as fast as the route allows.Adam Soltys2026-09-141-0/+9
| | | | | | | | | | | | | | | The wallet used to add two to four seconds of its own around a Lightning send: a random pause plus a relay look-back before paying, and a two-second poll for the result after. The notifier's claim now decides who pays in one round trip, with the relay look-back kept only for replays and for when the notifier gives no verdict; a phone answering from behind another app waits a second so a desktop tab wins the claim instead of paying at a throttled crawl. After the send, one held CheckLightningPayment call brings the preimage the moment CLN has it — for wallet-connect pays, the background worker, and the app's own Send. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
* One device pays a zap, whichever of them heard it first.Adam Soltys2026-09-142-0/+16
| | | | | | | | | | | | | Every awake device of a wallet hears a wallet-connect pay request within the same second, and the per-tab lock only reaches one browser profile: two profiles on one laptop each locked an HTLC for the same 21-sat zap. The push notifier now hands each pay request to the first device that claims it; a device that loses the claim stands down silently, and a device that cannot reach the notifier answers as before so nobody goes quiet over a hiccup. The worker path claims the same way. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
* A zap answered by two tabs no longer reports as failed.Adam Soltys2026-09-141-1/+27
| | | | | | | | | | | | | | | Every open tab of the same wallet sees every wallet-connect request. Two hidden tabs on one laptop both tried to pay the same zap: the ASP refused the second with "payment already in progress", and that error reached Amethyst half a second before the first tab's preimage. A Web Lock per request now lets one tab (or the worker) answer while the rest stay quiet, and a device that does meet "already in progress" waits for the sibling payment's outcome — relaying its preimage only if the payer never speaks — instead of shouting a failure over sats that went through. The losing side also stops writing a failed row into history for it. 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-132-2/+153
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 reads the relays it was already willing to open.Adam Soltys2026-09-131-0/+23
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The gap was not where I said it was. "283 of 400 authors have no relay list" sounded like the thing to fix — but those authors turned up in the sample BECAUSE our own relays carry them, and outboxPlan already asks our relays about everyone. The legacy kind-3 relay map, the fallback I proposed for them, recovers 2 of 220. Neither is worth writing. The real gap is that covering every author once was the wrong goal. People publish to several relays and each holds a different slice: of 60 sampled authors with a write list, 45 declared MORE than the three we took, and seven of twenty-five had posts living only on their fourth or later relay. Meanwhile the greedy stopped the moment everyone was covered — nineteen sockets of a twenty-four socket budget, with posts sitting unread on the five it never opened. So it takes six relays per author instead of three, and keeps picking until the budget is spent rather than until coverage is met. Measured over the same 250 authors, twice: 1265 notes before, 1549 after, 502 of them notes the old plan never saw. Same socket count we already allowed. Raising the per-author number ALONE makes it worse, which is worth knowing: more relays per author let a few fat ones cover everybody in nine sockets, and the feed read less than before (1089 against 1312). The two changes only work together. feed-follow-test now seeds one author with six write relays and asserts all six are dialled, so neither knob can drift back on its own. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* Markdown in a post reads as what it means.Adam Soltys2026-09-131-0/+105
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A kind 1 is specified as plain text — NIP-10 says markup SHOULD NOT be used — but bridges and cross-posters emit it anyway, and of the three things we could do with it we were doing the worst: the URL inside ![cover](…) rendered as a picture while the "![cover](" sat above it as litter. That is a real post from a real account, not a hypothetical. Images and links, and nothing else. ![alt](url) is a picture, [text](url) is a link that reads as its text. Headings, bold and italics are deliberately NOT honoured: # opens a hashtag and * turns up in ordinary prose, so supporting those would mangle normal posts to tidy up the rare bridged one. One thing markdown gives us that guessing never could: ![…] is the author stating outright that the URL is an image, so a picture on a CDN path with no file extension now renders as a picture instead of a bare link. The URL rendering is one function now rather than a branch inside the splitter, since a markdown link and a bare link deserve the same answer about what they point at. tools/markdown-note-test.js covers both shapes the real post came in (including the newline after the paren), the no-extension case, and — the one that matters most — that brackets, parens, hashtags and stars in ordinary prose are left exactly as written. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* The feed keeps up, and stops moving the page while you read.Adam Soltys2026-09-132-6/+180
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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