aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/messages.js
Commit message (Collapse)AuthorAgeFilesLines
...
* Cordn group chats: MLS groups delivered through a coordinator (Amethyst 1.17 ↵Adam Soltys9 days1-22/+86
| | | | | | | | | | | | | | | | | | | | interop) src/contextvm.js speaks ContextVM (kind 25910 inside ephemeral gift wraps) to a coordinator; src/cordn.js is the protocol (KeyPackages with hex-text identities, the 0xC04D metadata extension, exporter-keyed ChaCha20 sealing, unsigned nostr-shaped envelopes with the sender as MLS AAD, cordn1 group refs, a record engine like marmot.js); src/cordn-client.js mirrors marmot-client.js with the same interface, so the Messages group screens drive both — New group gains a White Noise / Cordn toggle. Verified against @cordn/cli (ts-mls) on the public coordinator both ways, from the engine (tools/cordn-interop-test.js) and from the real app (tools/cordn-ui-test.js). mls.js: the OpenMLS required_capabilities rule for GroupContextExtensions now applies only to groups that carry one. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
* A quote inside a quote shows as a one-line preview (author + start of the ↵Adam Soltys9 days1-1/+19
| | | | | | | text) instead of a bare 'view note' link Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
* Feed: leaving via the bottom nav closes the composer (its words stay the ↵Adam Soltys9 days1-1/+8
| | | | | | | draft), so the Post button is back when you return Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
* Images grid tiles are cut from the feed's own decode: scrolling All fills ↵Adam Soltys10 days1-16/+22
| | | | | | | the tile cache (3/30 → 22/30 tiles instant on #art) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
* Article cards: a dead cover falls back to the body's first picture, else no ↵Adam Soltys10 days1-2/+14
| | | | | | | cover (no grey box) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
* Profiles show their articles among the notes as cards (newest copy per ↵Adam Soltys10 days1-7/+26
| | | | | | | address), opened in full; the page cache keeps them as articles Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
* Audio links (mp3/m4a/ogg/opus/wav/flac/aac) render as a player, like AmethystAdam Soltys10 days1-0/+25
| | | | | Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
* Feed Articles view: long-form posts (NIP-23) from the feed's ↵Adam Soltys10 days1-21/+124
| | | | | | | people/topics/relays as cards, own cursor + index; Popular keeps articles two others engaged with Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
* nostr:naddr renders: article cards (cover, title, summary) that open to read ↵Adam Soltys10 days1-2/+100
| | | | | | | with headings, list/pack cards that open as feeds Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
* Custom feeds sync across devices: their own sync slot, newest edit wins, ↵Adam Soltys10 days1-5/+45
| | | | | | | deletions remembered Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
* Communities wear their icons: Vector's encrypted logo from the metadata, the ↵Adam Soltys10 days1-1/+39
| | | | | | | coinos logo for coinos Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
* Show cached communities immediately and simplify discoveryAdam Soltys10 days1-26/+41
|
* Show popular Concord communities on the Messages screenAdam Soltys10 days1-14/+28
|
* Feed pictures: a height-capped one's frame narrows with it (no empty bands)Adam Soltys11 days1-1/+1
| | | | | Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* Feed pictures uncropped: the column's width at their own shape, height-cappedAdam Soltys11 days1-10/+4
| | | | | | | | | The 3:4 crop for tall pictures cut the top and bottom off comics. Now nothing is cropped; a picture is as wide as the column allows up to min(560px, 70vh) tall, and only one taller than that narrows. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* Drop an unused timestampAdam Soltys11 days1-1/+0
| | | | | Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* CORS write-off only on a refusal corsFetch confirmed, not a body cut off ↵Adam Soltys11 days1-6/+6
| | | | | | | mid-download Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* CORS write-off only when the host is reachable: a dropped connection is not ↵Adam Soltys11 days1-2/+17
| | | | | | | a refusal Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* Media grid: tiles from 360px square copies kept on disk, loaded at onceAdam Soltys11 days1-2/+78
| | | | | | | | | | A fast flick down the Images grid was a blank screen: every tile was a lazy <img> of the original (or the 1100px column copy) to fetch and decode. Tile copies are made once (from the column copy when there is one), kept in the Cache API and painted eagerly. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* Profile: tapping the avatar opens the picture full sizeAdam Soltys11 days1-1/+6
| | | | | Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* Popular reads where the reactions are: ditto, nos.lol, primal, oursAdam Soltys11 days1-6/+14
| | | | | | | | | | | Measured: ditto held 94% of distinct reactions (and keeps a month of them); damus capped asks at ~300, pruned in a day and added nothing. Everything (raw firehose) gets its own list without damus (empty answer to a bare kind-1 ask) or ditto (~30% Mostr-bridged posts). A saved list equal to the old or current defaults follows the defaults. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* Feed: a tall picture fills a 3:4 frame as wide as the column, not a skinny stripAdam Soltys11 days1-2/+8
| | | | | Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* Media pager: a button opens the post itself (count of replies on it)Adam Soltys11 days1-0/+6
| | | | | Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* Media views: one empty answer is not the bottom; a crawl reads on until it ↵Adam Soltys11 days1-15/+27
| | | | | | | | | | | | finds a few A single empty relay answer (a timeout, a frozen socket) marked a feed's media finished for a day — 'You're all caught up' after four videos. Now it takes three in a row, old unconfirmed ends are ignored, and a crawl reads up to five pages until it has six new items of the mode. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* Media pager: tapping the author opens their profile (the pager goes with it)Adam Soltys11 days1-5/+10
| | | | | Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* Video frames from bytes, not from a CORS player; pager clips load plainly againAdam Soltys11 days1-54/+115
| | | | | | | | | | | | | | | | | | | | | | | | | | | Regression from 6488806: pager clips were loaded crossorigin so their frame could be kept, and on a phone the large clip's slide went black. Some hosts (Primal's R2 bucket) send CORS headers only to a request with an Origin and don't Vary on it, so a copy the browser cached from a plain player load answers a later CORS load without them, and it fails. The same thing made the size probe fail for any clip that had played in the feed. - Pager clips load plainly again; the still under a tap-to-play cover is restored on every return. - The probe (and its HEAD) go past the browser cache (no-store). - A clip's frame is now made from bytes: the opening of the file (index plus about 1 MB, no-store) decoded from a blob and kept as a JPEG — for tiles and as the pager's poster. No CORS player anywhere. Files whose index trails, or hosts whose bytes can't be read, keep the plain player still (shown, not kept). - Kept frames are 480 px wide (were 270) and capped at 600. - Probe failures are stored as failedAt, so earlier failures are asked again at once. - Test: an R2-style host, its clip cached by a plain load first, is still measured and its frame kept (failed before this change). Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Pager: a large clip keeps its picture when you come back to itAdam Soltys11 days1-5/+20
| | | | | | | | | | | | Leaving a slide lets go of its file, and coming back skipped the still because the cover was already there — black again. The still is now restored on every return. Pager clips also load CORS-readable (a plain retry for hosts without CORS), so their frame is kept the first time — the still under a cover and any clip that plays — and from then on the poster alone covers it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Pager: a large clip shows its frame under the tap-to-play coverAdam Soltys11 days1-1/+19
| | | | | | | | | | | | | The slide's poster was fixed when the pager was built, and a clip over budget held no source, so a clip whose frame was kept a moment later (or never) was a black screen with a button. Now the kept frame is looked up as a slide comes near; without one, the clip's first frame is loaded under the cover (only the start of the file, when it leads with its index or is served in ranges) and kept for next time. A tap plays it from the start. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Video tiles: a frame even when the size check fails; retry it laterAdam Soltys11 days1-6/+31
| | | | | | | | | | | | | | | | | | | | Efrat's clip (blossom.primal.net → r2a.primal.net) showed a blank tile on a phone: the 64 KB probe failed there (slow connection), the failure was kept for good, and a tile with a failed probe never tried for a frame. - A failed probe is dated and asked again after an hour (it was kept forever; old 'failed: true' entries are retried). The probe waits 10 s. - A tile whose probe failed still tries for a first frame, giving up and letting go of the file after 8 s. - Primal's CDN lets scripts read the bytes but not Content-Range, so the size was unknown: a HEAD's Content-Length (readable by any page) fills it in. - No size in imeta but a bitrate and duration (Primal posts those): the size is estimated from them. 'dim 1080.0x1920.0' is understood. - A tile with nothing to show is dark, like a video, not a pale gap. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Media views keep their own cache and crawl for media directlyAdam Soltys11 days1-10/+141
| | | | | | | | | | | | | | | | | | | | | | | | | | Videos was slow because the grid read from the feed's page of posts: the feed cache holds 30, so after every reload the grid re-crawled the relays, ten posts a step, each post first waiting (up to 8 s) for its avatar, pictures and link cards — none of which the grid shows. - Each feed now keeps a media index (IndexedDB, src/media-cache.js): the posts carrying pictures and those carrying clips, 400 of each, and how far back it has looked. The grid and pager read from it; a reload paints them at once. - The crawl reads older posts straight into the index, 100 at a time, without the readiness wait and without touching the feed's own page; it resumes from where it stopped. The bottom of a feed is looked for again after a day. Curated feeds still page the usual way. - A tile's first frame is kept (a small JPEG in the Cache API, when the host lets its pixels be read) and painted as a picture from then on: in the grid, as the pager's poster and the feed's. - Probe results kept for 2000 clips (was 600). - Forgetting a wallet wipes the index and the frames. tools/feed-media-cache-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Videos opens on a grid of thumbnails; the pager waits for a tapAdam Soltys12 days1-19/+17
| | | | | | | | | | | | | | Choosing Videos no longer jumps into the full-screen pager: it shows the vertical clips as a grid, like Images, and a tile opens the pager on that clip. Every tile gets a first frame when that costs only the start of the file (the index leads, or the server serves ranges), whatever the clip's size; a clip over budget wears its cost as a badge. Only an index-last file from a server without ranges shows its cost instead. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Videos: know what a clip costs before fetching it; vertical clips onlyAdam Soltys12 days1-15/+293
| | | | | | | | | | | | | | | | | | | | | | | | | | - What a clip costs is learned up front: the post's imeta (size, dim, image) when it says, else one 64 KB ranged request — the total from Content-Range, whether ranges are served, whether the MP4's index (moov) comes before its data, and the picture size from the track header. Up to three at a time; remembered across sessions. It also replaces the player-based shape probe the feed ran on every clip. - Clips over 25 MB (8 MB on 2G/3G, 3 MB with data saver), above 1080p, index-last files over 8 MB from servers without ranges, or clips that couldn't be measured wait for a tap: a cover saying what they cost ("50 MB", "4K"). Settings → Nostr → Videos: Autoplay large videos. - The pager loads only the clip on screen in full; the next gets its opening if within budget; everything else holds nothing. Sizes are asked three slides ahead. - Feed clips fetch nothing until they're about to play (preload none), with the post's still as a poster. - Video grid tiles show a first frame only for small clips that start quickly; others show their cost. - Videos lists only clips known to stand taller than wide; one of unknown shape is measured and joins when it qualifies; choosing Videos with none known yet opens the pager on the first that turns up. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Feed media views: a grid of pictures, videos to flip throughAdam Soltys12 days1-0/+289
| | | | | | | | | | | | | | | | | | | | | | | All / Images / Videos sits under the feed chips (remembered). - Images lays the feed's pictures out as a three-column grid. - Videos opens straight into a full-screen pager: one clip per screen, snapping, the one on screen playing with sound (muted with an Unmute button when the browser refuses), the next a flick or ↓ away. Closing it leaves a grid of the clips. - The pager shows only the media until tapped; a tap shows the author (tap for their profile), the post's words as a caption without the media links (tap to expand), and likes and sats in the bottom right, which also like and zap. - It is a place: Back or Escape closes it. Built outside the morph, so a repaint never restarts a clip; off-screen clips let go of their media. - Near the end of the grid or the pager the feed fetches older posts and new slides are appended. - Toasts now sit above full-screen viewers. tools/feed-media-views-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Settings → Nostr: manage your relaysAdam Soltys12 days1-2/+186
| | | | | | | | | | | | | | | | | | | | A Relays card edits the two relay lists other Nostr apps go by: the NIP-65 list (each relay Read, Write or both) and the NIP-17 DM inbox. Each relay shows whether it answers (a dot; 'unreachable' spelled out). Both lists are replaceable events other clients write too, so the card edits only a copy it looked up across the index and archive relays, looks again just before publishing, and backs off (reloading the newer copy) if another app saved since. Only a list that changed is republished, to the relays it names, the ones it used to name, and the indexers. Saving updates our own idea of where we write (outbox lookups and wallet sync relays) and resubscribes the DM inbox at once. tools/relays-settings-test.js Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Notification rows open the post, not the actor's profileAdam Soltys12 days1-3/+4
| | | | | | | | Tapping the avatar or name in a notification row went to that person's profile; now the whole row leads to the post it's about. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ULQZS9MQa6iAsXnrJUD9Du
* Zaps find lightning addresses our relays have prunedAdam Soltys12 days1-0/+15
| | | | | | | | | | | | | | | Zapping a post looked the author's lightning address up on our four home relays only. Those prune old events, so anyone whose kind 0 hadn't changed in a while (1776, Brad Mills) came back as "no Lightning address or Ark address" — while their profile page, which also asks the index relays and the author's own relays, showed the address right there. The zap lookup now asks both ways at once and takes whichever carries a lightning address. And when someone does have one but Lightning can't be paid right now, the toast says that instead of claiming they have no address. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VKv2uEFRR42Gp6NyqcN3A9
* White Noise attachments (encrypted-media-v2)Adam Soltys12 days1-15/+75
| | | | | | | | | | | | | | | | | | | Pictures and files now travel in White Noise groups and direct chats, both ways. A file is sealed under the group's media secret for the epoch its message goes out in (MLS exporter "encrypted-media" -> HKDF -> ChaCha20-Poly1305, AAD bound to hash, type and name), uploaded to Blossom, and sent as a kind 9 with a v2 imeta tag. A commit landing mid-upload moves the epoch, so the file is sealed again rather than sent under a stale key. Receivers keep each epoch's media secret for as long as a message from it is in the log, so older pictures still open. Groups founded here now carry and require marmot.group.encrypted-media.v2 (blossom-v1 on blossom.ditto.pub and nostr.download). Checked against MDK's wn: our pictures decrypt in wn, wn's in ours, and wn uploads into our groups by our policy. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* DM threads: choose by hand whether a message goes by White Noise or Nostr DMAdam Soltys12 days1-10/+79
| | | | | | | | | | Where a person can be reached both ways (a direct White Noise group with them, or a KeyPackage from another app), the thread header now carries a small pill naming the way the next message will travel. Tapping it pins White Noise or Nostr DM for that thread, or puts it back on Automatic (reply the way they last wrote). Threads with only one road show nothing. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* White Noise shows a coinos user's real name: the profile goes to its relays tooAdam Soltys12 days1-0/+2
| | | | | | | | | A coinos account appeared in White Noise as "Rapid Flamingo": with no kind 0 on its relays the app makes a name up. The profile now travels with the relay lists (the account's own signed events, unchanged), and is sent again when it changes, checked at boot and while Chat is open. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Profile: the revealed public key comes with its QRAdam Soltys12 days1-0/+5
| | | | | | | | Tapping "Public key" now shows the npub as a QR above the text, for a phone held up to the screen (White Noise, Amethyst, another coinos). The bare lowercase npub, which every scanner takes. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* Group chats notice a relay that dropped their subscriptionAdam Soltys12 days1-1/+2
| | | | | | | | | A group that misses a commit is stuck until it hears it, and a group founded by White Noise can live on a single relay. Its subscriptions now report a close to the chat watchdog, and one made while a relay was being skipped as dead is rebuilt when that relay comes back. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* White Noise (Marmot) chats: MLS group messaging in native JSAdam Soltys12 days1-11/+593
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The Chat tab now speaks the protocol the White Noise app speaks, beside Concord communities and NIP-17 DMs, without anyone having to pick one: - A two-person group with no name is a direct message. It lands in the same thread as that person's NIP-17 messages, and a reply goes back the way their last message came. A first message goes by White Noise only to someone whose KeyPackage another app published (a coinos wallet reads NIP-17 on all its devices; an MLS group reaches one). - Groups get rows of their own in the Messages list, a New group button, an invitation card, members, add/remove/rename for admins, leave. - Every wallet publishes a KeyPackage (kind 30443), so it can be invited. src/mls.js is RFC 9420 for ciphersuite 0x0001 on the noble libs (no WebAssembly), with the extensions-draft pieces Marmot needs (app_data_dictionary, app_data_update, app_ephemeral, self_remove). src/marmot.js is the Marmot layer: components, the account identity proof, the 30443/444/445 event shapes and the group engine (publish-before-apply, retained epochs, same-epoch tie-break). State is per device, in IndexedDB (src/marmot-store.js). The whole stack is one lazy chunk (72 KB). Tested against the RFC 9420 vectors (tools/mls-vectors-test.js) and against the Rust reference, MDK's wn CLI, at the protocol level (tools/marmot-interop-test.js) and through the real UI (tools/marmot-chat-test.js). Not yet: attachments in these chats, push for group messages, the app-message witness boost in fork selection. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* The feed you were on is remembered per wallet; new ones start on PopularAdam Soltys13 days1-5/+15
| | | | | | | | | | | It was one device-wide key, so a new account opened on whatever the last one was reading (#gardenstr: the starter feed has the same id in every account). Now keyed by the wallet, re-read when a wallet opens (the feature outlives a sign-out), Popular when nothing is remembered, and the old device-wide key is dropped. A feed opened from a link stays put. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Feed form: relays and an every-post firehose; its Back works; a stronger ↵Adam Soltys13 days1-9/+31
| | | | | | | | | | | | | | | | | | | new-post tint - The new/edit feed form gets a Relays field (read this feed from these relays only) and "Every post on these relays", which saves a firehose feed (optionally curated) named after its relay and hides the people, packs and topics it doesn't need. It used to take a hand-typed /feed/all?r= link. - The form's Back did nothing visible: opening it made no history entry, so Back went to the page behind the feed and left the form up. The editor (and the gear page's Spending/Savings view) are now part of the navigation state. - New-post tint 9% -> 16% green in light mode; dark mode unchanged. - tools/feed-editor-relays-test.js. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* New-post tint fades the moment any of the post is on screenAdam Soltys13 days1-18/+11
| | | | | | | | | | | It waited for half the row to sit in view for two seconds; scrolling reset the clock and a replaced node lost it, so a tall post read in passing or one the window remounted could stay green for good. Now the first visible pixel starts a 3s fade (the row's background transition), marked seen by id, whether the reader stays or scrolls on. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Pictures paint with the page: column-sized copies, and CORS that worksAdam Soltys13 days1-15/+137
| | | | | | | | | | | | | | | | | | | | | | | | | | | Measured in the live tab (screencast frames): back from a profile, the feed's text was on screen ~200ms before its photo. The <img> was loaded and complete on the first frame — a 3024px, 12-megapixel original that Chrome re-decodes whenever the page is rebuilt. - Post photos wider than the column at 2x are painted from an 1100px WebP copy made once (fetch, createImageBitmap, OffscreenCanvas) and kept in Cache Storage; it decodes in a few ms, decoding=sync, inside the same frame as the text. GIFs and small pictures keep the original; the lightbox opens the original. Profile banners use the same copies, warmed on prefetch. - corsFetch: primal's CDN caches its no-Origin answer without Vary: Origin, so a CORS fetch after the <img> got that header-less copy and failed — which had written blossom.primal.net, m.primal.net and coinos.io off as 'no CORS', so their avatars never got local thumbnails and painted late from the originals. A refusal is now retried once past the cache before a host is blamed; the old list is dropped (nocors-v2). - DM list: the unread dot sits centred at the row's end, like the communities' below, instead of beside the time. - Balance card gear: the icon alone, no circle. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Profile npub: full and monospace again, behind a Public key linkAdam Soltys13 days1-13/+13
| | | | | | | | | The truncated key button took a whole row on every profile. A "Public key" link now ends the Following/Followers line; it reveals the npub in full, monospace, tap to copy, and hides it again. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Notifications screen links to its settingsAdam Soltys13 days1-1/+8
| | | | | | | | A Settings button beside the Notifications title opens Settings > Notifications (which kinds buzz this device); browser Back returns. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Never write a follow list over one we failed to findAdam Soltys13 days1-10/+127
| | | | | | | | | | | | | | | | | | | | | | | A kind 3 is replaceable: publishing one built on a list we didn't find wipes the real one wherever it lands. 2026-10-02, an Amethyst user with 398 follows (kept on her own relays and an archive) signed in to coinos for the first time; the starter-follows seed asked only our relays, saw nothing, and published Adam + coinos over her list. - deepContactList: our relays, the profile indexes, the archives, the person's own NIP-65 write relays, and Primal's signed copy (verified). - Starter follows only for a key with no follow list AND no profile anywhere ('new' meant new to coinos, not new to nostr). - A follow, follow-many or mute made while we have never seen the list does the deep lookup first and builds on what it finds. - Settings > Nostr > Follow list: find older copies on history-keeping relays and restore one as a union with the current list (nobody is removed), for anyone already hit. - tools/follows-overwrite-test.js: fake relay network, only an archive holds the list; fails on the old code with the exact two-person event. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
* Profiles show Following and Followers, each a list you can openAdam Soltys13 days1-5/+116
| | | | | | | | | | | | | | | | | | Under the bio: "116 Following · 572 Followers". Tapping either opens the people below it (50 at a time); tapping a person opens their profile. Follows are the person's kind 3, now read from Primal's cache alongside the relays (a list kept only on their outbox relays used to read as nobody). Followers, which no relay counts, come from Primal: the count from user_profile, the list from user_followers, a first screenful fast and the rest after, without reordering what is on screen. Every event taken from Primal is signature-checked in the verify worker first, and the profiles among them fill names and faces the relays lacked. Also translates the post-zap "no address" message. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo