aboutsummaryrefslogtreecommitdiffstats
path: root/src
Commit message (Collapse)AuthorAgeFilesLines
* DMs and calls go to relay.coinos.io and relay.primal.net (both take gift ↵Adam Soltys3 days2-4/+28
| | | | | | | wraps and pass call messages on live), not nos.lol, which refused or ignored the household IP's publishes. A relay that turns a call message away is left out of the next ones on this device: a day for one that won't take the kind (nip17.com: only kind 1059), ten minutes for a rate limit or ban. publishOn tells its caller which relay refused and why (onrefused). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Calls: the routes found before the offer or answer ride inside it (until ↵Adam Soltys3 days2-6/+22
| | | | | | | there's a direct and a relayed one, 1.5 s at most) and only later ones go one by one — each route was its own signed message, dozens per call; a household's two devices calling each other got its IP banned by damus and rate-limited by relay.coinos.io, and the answer was refused with them: the callee stuck on Connecting…, the caller ringing on. An offer, answer, hang-up or decline no relay took is tried again (1.5, 3, 6 s) before the call gives up. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* A voice call can turn video: Turn on camera in the call's bar puts your ↵Adam Soltys3 days3-4/+37
| | | | | | | camera in (a renegotiation, NIP-AC's kind 25055); the other side sees you at once and gets the same button for theirs. The call's sound carries on throughout. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Streams on coinos's server: a ⚑ Report button (what's wrong, a note) for ↵Adam Soltys3 days2-5/+89
| | | | | | | anyone signed in, and Take down for the admin (asks whether to bar the account too); a taken-down stream says so and plays nothing. Go live asks the server first: a name claimed less than a day ago is told when it can, and a barred account that it can't, Start off. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Streams: one our live server has ended reads ended, whatever its last ↵Adam Soltys3 days3-6/+75
| | | | | | | announcement says (an app closed mid-stream never says so, and it read live for an hour with no way to stop it); its host gets End — on the stream and on their profile — which publishes the ended announcement with the recording. Each announcement is now newer than the last to the second (an ended stamped the same second as the live before it lost to it). A recording that is a web page (zap.stream's Rumble links) gets a Watch the recording link instead of a player that can't play it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Sign media uploads with the user's Nostr login identityAdam Soltys3 days3-4/+19
|
* Going live: when the server can't take a dropped stream back (it restarted) ↵Adam Soltys3 days1-8/+26
| | | | | | | and starts a new one, the app now announces the new one, once it plays, and says the old one is over (no recording: there's nothing in it). It went on announcing the dead address, and viewers saw "The stream isn't playing right now" while the camera sent to an address nobody knew. The streamer's own view follows. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Past streams: under the Live chip, the feed's streams of the last month that ↵Adam Soltys3 days3-13/+66
| | | | | | | left a recording (newest first, when each ended); a profile lists its person's too. A tap plays the recording. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* A stream's chat leaves headroom above its first line: the hat on that face ↵Adam Soltys3 days1-1/+2
| | | | | | | (the crown) was cut off by the scroller. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* A Lightning invoice from Receive says to keep coinos open until it's paid ↵Adam Soltys3 days2-0/+13
| | | | | | | — paid while coinos is closed, the money goes back to the sender — and that it lasts an hour (ark.coinos.io's invoices now expire in 1 h, not 48); when the hour is up the QR gives way to "expired" and a New invoice button. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Settings → Wallet → Transaction history: Export CSV — every payment in ↵Adam Soltys4 days3-3/+65
| | | | | | | Spending and Savings as a line of a CSV file, oldest first: date (UTC), account, type, rail (Lightning, Ark, on-chain), what was paid, its fee, the change to the balance and its worth in your currency at the time, status, the transaction or payment hash, who it went to, the invoice. A failed payment is left out; a renewal is its fee. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Live streams have their own chip, after Videos: a grid of the feed's streams ↵Adam Soltys4 days3-15/+24
| | | | | | | on now, a red dot on the chip while someone in the feed is streaming — no longer a strip above every feed's posts. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Answering a call no longer takes seconds: while it rings, the connection is ↵Adam Soltys4 days2-13/+68
| | | | | | | made with the caller's offer and its routes (TURN's too) gather, and the caller's relays and TURN credentials are looked up — Answer only adds the microphone and camera and replies (3.2 s → 0.2 s on a simulated network). An open conversation warms the same for calling out. A remote signer's offer or answer waits for a direct and a relayed route or 1.5 s, not for all of them (up to 4 s). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* A call from someone you don't follow and haven't written to no longer ↵Adam Soltys4 days6-12/+65
| | | | | | | vanishes: it doesn't ring, but it's a missed call in your conversation with them, saying it didn't ring and why, a tap away from calling back (with the app shut, the worker notes it for the app, without a notification). A muted person's still leaves nothing. A logged call no longer counts as having written to someone. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Call history: each call is a row in its conversation — Outgoing / Incoming ↵Adam Soltys4 days4-4/+114
| | | | | | | video call · 4:12, Missed, Declined, No answer, Cancelled, Busy — a tap calls back. Kept like a message and sent to our own inbox (kind 1405, gift-wrapped to us alone) so our other devices list it; a call logged by two of our devices shows once, by its call-id. A call that rang while the app was shut and went unanswered is a missed call when it next opens. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Calls ring a closed app: the notifier wakes the phone for a call offer, the ↵Adam Soltys4 days5-11/+110
| | | | | | | service worker fetches the offer's wrap, opens it with your key and rings — "Alice is calling", until answered or missed — for someone you follow or talk to; the tap opens coinos ringing with the offer it kept. A call answered from a push still connects: the caller sends its candidates again once answered. Settings → Notifications → Calls. Also: the worker's DM key mirror retries when the wallet's keys aren't there yet (it waited five minutes). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Allow custom Blossom servers for attachments and post mediaAdam Soltys4 days5-4/+62
|
* Voice and video calls in DMs (NIP-AC, as Amethyst, nospeak and Noscall speak ↵Adam Soltys4 days5-7/+572
| | | | | | | it): 📞 and 🎥 in a conversation's header; a call from someone you follow or have written to rings over any page (tone, buzz, a notification when the app is out of sight) with Answer and Decline; mute, camera off, switch camera, minimize to a bar, hang up. WebRTC device to device (DTLS-SRTP), set up by signed messages in ephemeral gift wraps to the DM inboxes; TURN from coinos's live server (Cloudflare) for the connections that can't go direct. A remote signer puts the candidates inside the offer and answer instead of signing one each. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Live rings and going live. A face whose person is streaming wears a red ring ↵Adam Soltys4 days4-19/+336
| | | | | | | (LIVE under the bigger ones) and opens the stream. Go live (the composer's red dot, or your profile) sends your camera or screen to coinos's live server (coinosv3-server live/: the recording over a WebSocket, HLS out), announces it as your kind 30311 once three segments exist (Chrome's own player won't start a live playlist with fewer), refreshes it each minute with how many watch, reconnects to the same stream if the link drops, and ends it with its recording. Needs a coinos name. Also: the service worker leaves /live/ and /vmedia/hls alone (it froze a live playlist at its first copy). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Hide new posts notice when scrolling upwardAdam Soltys4 days1-9/+6
|
* Live streams (NIP-53), to watch: a row of what's on above a feed (Following: ↵Adam Soltys4 days8-21/+495
| | | | | | | your follows' streams; Everything and Popular: all), a stream's own page at /naddr1… (the HLS stream playing — hls.js where the browser has no HLS — its chat and zaps as they come, a chat line sent as kind 1311, ⚡ to the host), a card for a post linking a stream, and a LIVE line on a streamer's profile. A stream quiet for an hour is over; one that drops is retried; zaps on a stream carry its a tag. Also: the single-file build's video stub gains streamVideo (its uploads threw since the HLS uploads). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* A clip in the post a reply answers (the "Replying to" card) goes full ↵Adam Soltys4 days1-4/+16
| | | | | | | screen: a tap on it or its full-screen button used to do nothing, the full-screen handler taking it back out for a media pager that doesn't have it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Short posts in another language get translated: a line like "Weißt Du wie ↵Adam Soltys4 days1-1/+9
| | | | | | | man die kriegt?" had one German common word and passed as maybe-English. More German common words, and letters only some languages write (ß, ñ, ł, ğ…) count as one. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* The media pager no longer goes round from its last post to its first (or ↵Adam Soltys4 days1-30/+9
| | | | | | | first to last): a step, a flick or the wheel at an end stays there. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* No white buttons in dark mode: primary buttons, the chosen profile tab, feed ↵Adam Soltys4 days1-9/+18
| | | | | | | chip, new-posts pill, till tip and hat take a lifted grey there (the accent still fills them in light mode). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Clips too big to play as they are (past 1080p, over the data budget, ↵Adam Soltys4 days1-7/+23
| | | | | | | unmeasurable) stream as HLS from vmedia and play by themselves instead of waiting behind a tap-to-play cover; with Data Saver or a slow link the cover stays. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* HLS for long videos: vmedia serves a long clip as a 360p/720p stream made as ↵Adam Soltys4 days4-18/+209
| | | | | | | it's watched (/vmedia/hls.m3u8), the feed and pager play long clips and posted .m3u8 streams through the browser's own HLS, and a long video posted from the app goes up as an HLS stream made on the device (master playlist on Blossom). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* A mouse drag on the feed selects text again instead of grabbing and flicking ↵Adam Soltys4 days2-48/+3
| | | | | | | the page. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* The media grid is never all caught up: once the feed's relays have nothing ↵Adam Soltys5 days1-13/+18
| | | | | | | older it reads on from the archive relays, and a bottom it does find is looked past again a few minutes on (no longer kept for a day). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* A touch stops a pager flight on the post most on screen: a tap only stops ↵Adam Soltys5 days1-2/+19
| | | | | | | it, a swipe back from there is the post before, and one the way it was flying takes it on. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* The picture pager works like the video pager: pictures join once decoded, ↵Adam Soltys5 days2-26/+93
| | | | | | | | | | | | | | | | | | the screen's way round with the shape toggle, and it flies (and counts) once ten are ready; flights end without the crawl, and the feed beneath doesn't repaint mid-flight. Adam, 2026-10-17: "image pager should work similar. we need the tall only chip there too. i'm noticing some stutter/choppiness when flying images. especially towards the end of the fly as it's snapping to the one it's landing on." Measured on his phone (Pixel, 60 Hz) over synthesized touch flings in the live image pager: frames of 33–183 ms mid-flight, some with no script running (pictures fetched and decoded as the flight came near them: the near observer attached every post within 250% of the screen, a few of them originals — GIFs, hosts the server copies can't reach); the feed under the pager re-rendering (renderThreadStable, 40–55 ms each) during flights; and the flight's ease-out cube spending its last ~400 ms under 3 px a frame, the landing picture stepping a pixel at a time. - A picture post waits out of the carousel until its first picture is decoded (img.decode(), the copy falling back to the original as before). pagerWarmPics keeps four decoding at a time, up to sixteen ready ahead of the reader and three behind; a post whose copy and original both fail stays out. The picture's size, learned from the decode, places it for the shape rule. - The shape rule and its toggle ("Tall only" / "Wide only" / "All shapes", one setting with the videos) are in the picture pager's top bar too. - Flying needs ten posts ready, for pictures as for clips; the counter is hidden until then while posts are still warming (a pager with fewer than ten, all of them ready, counts them). The counter fade on settling applies to every pager (the frame loop's motion flag now runs for pictures too). - A flight eases out on a square instead of a cube: no long crawl at the end, and it sets off nearer the finger's speed. - scheduleRepaint holds off while the pager is moving. - pagerLoadAhead fills to the posts found (waiting ones too), not just the ready ones, so a gated pager doesn't crawl page after page. Tests: new tools/picture-pager-ready-test.js (a picture held back by the server waits out and a step passes over it; it joins above the one on screen without moving it; the wide one is out; no counter while one is warming, 3 / 6 once all are in; the toggle lets the wide one in and out and is as tall as the close button). pager-gone-picture now checks the dead picture is passed over. Picture tests not about shape turn all shapes on; pager-swipe-fly has twelve posts (ten ready before it flies) and waits out the longer flights; pager-loading's fixture gains pagerFound; pager-ready-gate: four clips all ready show their count. All pager tests pass except feed-pager-ahead and feed-video-pager, which fail the same checks as on a clean checkout. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* The video pager shows no position counter until ten clips are ready, the ↵Adam Soltys5 days2-0/+4
| | | | | | | | | | | same point it starts to fly. Adam, 2026-10-17: "let's also not show the position/total counter until we have 10". The count is recomputed whenever a clip joins the carousel, so it appears as the tenth one does. Picture pagers and Data Saver are unchanged. tools/pager-ready-gate-test.js: with four clips ready the counter is hidden. pager-seek-hide and pager-swipe-fly pass. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* The video pager flies only once ten clips are ready to play; until then a ↵Adam Soltys5 days1-2/+9
| | | | | | | | | | | flick or a spin of the wheel is one post at a time. Adam, 2026-10-17: "let's not allow flying/coasting until we have at least 10 videos fully warmed up". Right after a refresh a fling outran the warming, landing on whatever was ready while clips joined around it. Ready is in the carousel: the clip's first frame on its slide and its opening cached. Picture pagers and Data Saver are unchanged. tools/pager-ready-gate-test.js: with four clips ready a hard flick goes one post (it came round to the first with the gate off). pager-swipe-fly and pager-wheel pass; feed-pager-ahead fails the same two picture checks as before. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* The pager's counter runs through the posts as a flight passes them, instead ↵Adam Soltys5 days1-3/+14
| | | | | | | | | | | of jumping on landing. Adam, 2026-10-17: "the counter should animate as we fly, right now the current position only updates when we land". While a flight is under way (a flung finger or a spin of the wheel), each scroll counts the post under the middle of the screen (a binary search over the posts' tops); the landing sets the post landed on, as before. tools/pager-swipe-fly-test.js: a flick from post 2 shows 2, 3, 4 … 9 on the way. pager-wheel, pager-ready-gate, profile-pager and thread-pager pass. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* Pager flicks are judged by when the finger moved, not when the move was ↵Adam Soltys5 days1-8/+15
| | | | | | | | | | | | | handled; the counter counts the reachable posts again. Right after a refresh, with clips warming, touch moves queue behind other work and arrive in bursts. The flick's speed was timed on handling, so a burst after a stall read as a fling (a short flick flew) and a stall before the last move as a crawl (a long flick took one step) (Adam, 2026-10-17: "sometimes a quick long flick only proceeds to the next video and doesn't coast as i would expect it to and sometimes the opposite happens -- a short flick does fly"). Each move is now timed by its event's timeStamp, when the finger made it; moves that share one (synthetic ones can) fall back to the handling time. The counter goes back to counting the posts in the carousel (e529ab1 undone): counting the ones still warming confused, since they can't be scrolled to (Adam: "maybe we should go back to showing the total loaded"). tools/pager-swipe-fly-test.js: a gentle swipe whose moves are handled in one burst goes one post; a fast flick whose last move is handled 300 ms late still flies (it took one step with the old timing). pager-ready-gate is back to "the counter grew". pager-ready-gate, pager-tap-slop, pager-seek-hide, pager-first-frame, thread-pager and profile-pager pass. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* The video pager's shape toggle is the height of the close and sound buttons ↵Adam Soltys5 days1-1/+1
| | | | | | | | | beside it (40px). Adam, 2026-10-17: "Can we give the close button mute button and tall only chip the same height?" The toggle was 32px between two 40px buttons; it's 40px now, with its text a size up to match. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* The video pager never puts up a stand-in still: a clip joins the carousel ↵Adam Soltys5 days1-8/+8
| | | | | | | | | | | only once its own first frame is on its slide. Where the server had no first frame for a clip, its slide showed the post's still (a kept frame or the post's picture) and swapped it for the clip's first frame when one came — often a different picture, and the swap jarred (Adam, 2026-10-17: "i don't want to show posters and then switch to the first frame because sometimes the poster and first frame were not the same and that was jarring. could we just always wait until we have a non-black frame before including the video in the pager?"). Now such a clip gets no picture at all and waits out of the carousel until its first frame is drawn from its cached opening as the clip ahead; one with no frame to be had stays out. The clip tapped to open the pager is unchanged. tools/pager-first-frame-test.js: the clip without a server frame has no stand-in and waits out of the carousel, then gets its drawn frame. pager-ready-gate, pager-no-poster, pager-eager, pager-loading and pager-swipe-fly pass. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* A flick near the end of the pager coasts round to the first post, rather ↵Adam Soltys5 days1-2/+10
| | | | | | | | | | | than stopping on the last. A fling whose coast ran past the end used to land on the last post, and a second flick was needed to wrap. Near the end of a video pager the clips still warming join the carousel one by one, so that second flick usually landed on the newest clip instead, and getting back to 1 took flick after flick (Adam, 2026-10-17: "it's a bit difficult to get back around to 1 … can we do something to make it coast right around to the start again even if a video near the end is loading in?"). Now when most of the coast lies beyond the end (fewer posts left before it than half the flick's worth), the flick wraps straight to the first post; flung up from near the top, to the last. A big flick from further back still flies to the last post. tools/pager-swipe-fly-test.js: from post 7 of 9 a flick lands on 1, a flick up from 1 lands on 9, down again to 1, a gentle swipe from 1 is 2; the long flight from post 2 still lands on 9. pager-ready-gate, pager-tap-slop, pager-wheel and pager-close pass; feed-video-pager fails the same two checks as on a clean checkout. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* The video pager's counter counts every post it has found, not just the clips ↵Adam Soltys5 days1-1/+6
| | | | | | | | | | | already warmed up: a refresh no longer shows "1 / 2" over a grid of a dozen. The posts were all there (the grid showed them); the carousel holds a post back until its clip is ready to play — its first frame on the slide and its opening cached — and only the next couple are warmed at a time, so after a refresh the total started at 1 or 2 and crept up as clips warmed (Adam, 2026-10-17: "On refresh the total video count seems to reset to 1 and slowly more load despite showing posters for several more being available"). Now the total, and the place in it, count the waiting posts too; only posts kept out for their shape are left out. A swipe still passes over a post not yet ready. tools/pager-ready-gate-test.js: the counter reads 3 / 4 both before and after the waiting post joins. pager-swipe-fly, profile-pager, thread-pager and pager-seek-hide pass. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* The "Tall only" / "Wide only" / "All shapes" toggle shows only in the video ↵Adam Soltys5 days2-5/+3
| | | | | | | | | | | pager, not over the Videos grid. Over the grid it sat on its own line below the view chips, looking misplaced (Adam, 2026-10-17: "The tall only chip looks a bit misplaced here. Let's only show it in media mode"). The grid still follows the same setting, so a change in the pager shows up in the grid too. tools/pager-ready-gate-test.js: the grid shows four clips with no toggle over it; the pager's toggle checks are unchanged and pass. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
* The video pager's position counter shows while the pager moves and fades out ↵Adam Soltys5 days1-0/+6
| | | | | | | | | | a moment after it settles. It comes back the moment the pager is in motion again — a finger on the slides, a glide, a flight — keyed off the same `moving` flag that hides the seek bar, and fades out 1.2 s after landing (the fade restarts on every stop). Opening the pager shows it briefly, then it fades. Picture pagers are unchanged. (Adam, 2026-10-17: "the video position counter should fade out once you've stopped scrolling and re-appear when you scroll again".) tools/pager-seek-hide-test.js checks it: faded at rest, back while a finger holds the slides, still there just after the snap back, then gone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* The video pager never lands on a black slide: a post joins the carousel once ↵Adam Soltys5 days3-17/+136
| | | | | | | | | | | | its clip is ready to play; and the carousel shows the clips the screen's way round, with a toggle for all shapes. Ready to play: the clip's first frame on its slide and its opening in the cache (the head start done). Until then the post waits out of the carousel — parked off screen, no height, no snap point — and a swipe or a flight passes over it to the next post that is ready; it joins when ready, and a post joining above the clip on screen moves the scroll by its height in the same frame (snapping off for it, and never while the pager is in motion: then once it has settled), so the reader doesn't feel it. The post tapped to open the pager is in whatever its state. Seen on Adam's phone before this: 17 of 18 landings had their frame, the 18th was black for a second while the server was still making its frame. (Adam, 2026-10-16: "i never want to land on a black screen i'd rather just not have a video in carousel yet if it's not ready to play".) A local http clip, which can have no server frame, waits only for its opening. Data Saver is unchanged (nothing waits: heavy clips sit under their tap-to-play cover). The screen's way round: the Videos view used to show tall clips only, everywhere. Now it and the pager show tall clips on a screen taller than wide and wide clips on one wider than tall — a square either way — unless the reader asks for all shapes: a toggle over the Videos grid and in the pager's top bar ("Tall only" / "Wide only" / "All shapes"), remembered on the device. A post the wrong way round is kept out of the carousel the same way (not the one opened on, and never the clip on screen: a turn of the phone doesn't pull it away; shapes not yet known are let in, and a shape is learned from the server frame as it loads). Turning the phone re-sorts them. (Adam: "only show vertical videos by default on a phone that's in vertical orientation and only horizontal videos if the phone is in landscape but have some kind of control to toggle the ability to see all videos".) The posts prepared ahead (frames for five ahead and three behind, head starts for the next two and the one before) skip the ones kept out for shape. tools/pager-ready-gate-test.js: the grid shows four of five clips (not the wide one) with the toggle, and all five after a tap; in the carousel the next clip, its frame delayed, waits while the one after (ready) has joined; a step lands past it on a clip with its frame; the waiting post joins above the clip on screen without moving it, the counter grows, a step back lands on it; the pager's toggle lets the wide post in and out. pager-tap-slop-test keeps all shapes on (its path has a wide clip before a tall one in a portrait window). Every other pager test passes; feed-pager-ahead and feed-video-pager fail the same checks on a clean checkout. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* The video pager's slides near the reader hold their clip's first frame — ↵Adam Soltys5 days1-4/+53
| | | | | | | | | | | | the server's, full size — so no slide comes up black. Some slides were black until the player had its frame, then the picture appeared all at once (Adam, 2026-10-16: "i'm still seeing mostly black between some videos and then they suddenly appear when it starts playing"). The frame drawn in the browser (ebaec42) needed the next clip's cached opening, a file led by its index and a host that allows the read — and only the next clip had one at all: not the one before (its player let go on leaving), not a flight's landing, not the clip two along. vmedia gains /vmedia/first.jpg?u=<clip>: frame 0 itself, longest side 1280 (the player's size), any host, any layout, kept and cached at the CDN like the stills. The pager now gives every slide within three of the reader, the clips ahead and the slide landed on (not the one the pager opened on mid-clip) their clip's first frame: the server's, fetched and decoded aside and put on the slide when ready, so the flick glides onto the very picture the player shows first; a flight's landing slide fetches its frame as the flight sets off. Where the server has none the next clip's frame is drawn here as before, and a clip beyond keeps the post's still — soft, but not black — replaced by a first frame when one comes; never a soft still under the clip on screen. Data Saver is unchanged (posters at build). pager-first-frame-test (https clips, vmedia answered by interception): the next clip holds the server's frame, asked once; a clip the server has none for keeps the still, then has its frame drawn here once it is the clip ahead; the clip left behind holds its frame for the swipe back and plays again on it. pager-no-poster, pager-eager, pager-seek-hide, pager-swipe-fly, video-tile-poster, video-fallback, pager-controls still pass. vmedia deployed to cs first (first frames of live clips: 47–144 KB, 0.4–1.7 s to make). Co-Authored-By: Claude Fable 5.1 <[email protected]>
* The video pager's seek bar shows only on the clip square on the screen: none ↵Adam Soltys5 days2-4/+20
| | | | | | | | | | while the pager is in motion. The bar sits in the clip's slide, so a glide to the next clip carried it up the screen, a white line sliding out while the next slid in (Adam, 2026-10-16: "i don't want to see the seek bar while i'm transitioning to the next video"). The pager's frame loop now marks the pager `moving` whenever the clip on screen's slide is off its mark by more than 2px — under a finger, in a glide or a flight, snapping back — and under that mark no seek bar shows at all; it fades back in (180 ms) once the clip has landed. Bars on slides other than the one on screen stay hidden too (a slide seen before kept its bar ready). pager-seek-hide-test: at rest one bar, on the clip on screen; through a glide to the next clip every frame between the two has the pager marked moving and no bar to be seen; the landed clip's bar fades in and the one before shows none; a finger holding the slides 100px off their mark hides it, letting go brings it back. pager-controls, pager-no-poster and pager-swipe-fly still pass (feed-video-pager-test's two failures are on clean HEAD too). Co-Authored-By: Claude Fable 5.1 <[email protected]>
* The pager glides onto the next clip's own first frame, and onto pictures ↵Adam Soltys5 days1-3/+72
| | | | | | | | | | | | | already decoded — no black slide between. The clip ahead holds its own first frame: full size, frame 0, drawn from the opening of the file (the bytes the head start put in the cache, read again by range — never more of the file than the head start fetched), so a flick to it glides onto the very picture the player shows first, and the player's frame replaces it with no visible change. A dozen are kept, the oldest let go; the tile's kept frame (480px, at 0.1 s) is made from the same decode when the tile has none yet. Only the next clip, not the ones beyond: their head start is half, and a frame from them would cost bytes. On a connection to be careful with the thumbnail poster stays, as before. Pictures: the slides near the reader are attached from two and a half screens out (was one and a half) and decoded as they are attached, so a flight lands on a picture rather than on a blank that fills in. (Adam, 2026-10-16: "could we actually have the next image or video scroll in while the previous scrolls out? Right now it goes black before the next one scrolls in.") pager-no-poster-test: the next clip holds a blob poster at the clip's full size and shape while the one after holds nothing; after a flick the next plays with its frame under it and the one after now holds its own; Data Saver keeps the thumbnails. pager-eager-test (the byte budget ahead of time) still passes; so do the picture pager and sharpen tests. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
* The view strip scrolls sideways; the page can never be wider than the screen.Adam Soltys5 days1-1/+13
| | | | | | | | | Five chips (Notes, Conversations, Images, Videos, Articles — Conversations joined them today) no longer fit a phone, and the strip was a flex row that neither wrapped nor scrolled: the page was 42px wider than a 390px screen, which on an iPhone is a page you can pinch out of and pan, a blank strip down the right (Adam, 2026-10-16: "I can kinda zoom out and pan around a bit on my phone"). The strip now scrolls sideways like the feed chips above it, and the root element clips sideways overflow outright so no stray wide row can do this again (on the root, not body: a body that clips becomes the scroll container a sticky rail measures against). feed-media-views-test: the strip scrolls sideways and the page is no wider than the window. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
* The wheel coasts wherever the pointer is while a feed or a thread is up.Adam Soltys5 days1-2/+10
| | | | | | | | | The coast only took a notch whose target sat inside the feed or the thread, so a pointer resting on the rail, the header or the wallet column beside the feed on a wide screen got the browser's plain scroll (Adam, 2026-10-16: "when my cursor is hovered over the right side bar or any part of the page that's not the feed, wheeling scrolls without coasting. can we make it coast no matter where my pointer is?"). Now the question is whether a feed or a thread is the page on screen; the pointer's spot only matters when it is over something that takes the wheel itself — a text box, the chips strip, the lightbox, the pager, a box that scrolls on its own — and on a page with no feed on it (the wallet) the wheel stays the browser's. feed-wheel-reverse-test: the fixture's page carries a feed unless told otherwise; a notch with the pointer over the wallet column, or over nothing in particular, coasts; one on a page without a feed does not. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
* The video pager: a clip that plays by itself has no poster, and its caption ↵Adam Soltys5 days2-11/+46
| | | | | | | | | | | | | is live — names open profiles, the words open the post. No poster: every poster there was (the kept frame, a post's imeta picture, the server's) is 480px wide and taken at some moment of the clip, so it showed for a beat, soft and not quite the first frame, and the clip's own first frame replaced it — a flash on every flick (Adam, 2026-10-16: "if we're auto playing let's not show the thumbnail let's just start on the first frame of the video"). The slide is the pager's black until the player, whose opening is in the cache by then, has its first frame. Giving the next clip's player its file ahead of time was tried and dropped: it pulled the whole file down (pager-eager-test). On a connection to be careful with the poster stays: a heavy clip waits for a tap, and the still is what there is. The caption was a plain excerpt, which left "@NostrValley" as dead text. It is the post's words rendered as a post's are now — names, tags, note references and links live. A tap on a name, a tag or a note reference leaves the pager (and the full screen) before going where it goes; a link to the web opens in its own tab and the pager stays; a tap anywhere else on the caption leaves the pager and opens the post, where the whole of a long caption reads ("clicking anywhere on the caption that's not a link should take me out of fullscreen and take me to the post"). The expand-in-place toggle is gone. Tests: tools/pager-no-poster-test.js (no poster on the clip on screen nor the ones ahead, playing; with Data Saver the posters stay), tools/pager-caption-test.js (the name is a link and opens the profile out of the pager; a tap on the words opens the post; a web link opens its own tab and the pager stays). Pre-existing on a clean checkout: feed-video-pager-test "from where it was in the feed", feed-pager-ahead-test "past fifteen posts with none" (fails on 33c7218 too). Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
* A turn back against a flying feed brakes first: that notch eases the coast ↵Adam Soltys5 days1-17/+39
| | | | | | | | | | | to a stop, the next is the gentle start back, the ones after wind up. The gentle half push straight away from a flight still overshot (Adam, 2026-10-16: "the first click of the wheel should just stop the coasting/flying maybe with a quick easing rather than a sudden stop and then the next notch should start going slowly in reverse and the next ones should start speeding up"). Now, when the page is flying (over 1.2 px/ms — a notch from rest peaks near 0.9), the first notch back only brakes: the coast's decay is shortened to 40 ms from that moment (the frames a busy thread missed don't travel first), so a flight at 8 px/ms stops within ~300 px, eased. The notch after it, up to 2.5 s later, is the gentle start — half a notch's travel at half the speed — and the ones after wind up as usual. Against a stroll or a coast that only just died there is nothing worth braking: the turn back is the gentle start itself, as before. feed-wheel-reverse-test: the brake moves nothing by itself and eases out within a short way, never the other way; the next notch creeps back half a notch; the ones after wind up; a notch back a second and a half later is still gentle, three seconds later a full push; a stroll turned back starts gently at once. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
* Negentropy (NIP-77): DM and notification catch-up fetch only what this ↵Adam Soltys5 days3-17/+399
| | | | | | | | | | | | | | | | | device lacks. A relay that speaks it compares the ids it holds for a filter with the ids we hold and answers with the difference — a few hundred bytes, one round — instead of re-serving its newest events every time we ask. Measured on relay.coinos.io: the DM subscription re-served 200 gift wraps (468 KB) on every boot and every resubscribe, the Notifications refresh 120 events (105 KB) a visit; a reconcile holding the newest 100 of 120 found the 20 missing in 1.8 KB and 41 ms. src/negentropy.js: the protocol (version 1, as strfry speaks it), a port of Doug Hoyte's reference client (MIT): NegSet of [created_at, id] items, Negentropy.initiate / reconcile → { next, have, need }, WebCrypto SHA-256. Byte-identical opening message to the reference's; seven pairings of ours and theirs as client and relay agreed on every have/need. src/nostr.js: reconcileOn(url, filter, items) on its own socket (the pool's parser has no ear for NEG-* frames) → { need, have } or null; a relay that doesn't speak it (NOTICE "negentropy disabled", CLOSED, a socket shut on the opening message, silence) is remembered for a day in coinos-neg-off, so the plain query goes straight out there next time; NEG-ERR blocked/closed is not remembered. syncOn(relays, filter, items): reconciliation where it works, the plain query where it doesn't, the ids we lack fetched by id in slices, each event once, verified. messages.js — DMs: relay by relay, one that speaks it gets the live subscription with limit 0 (opened first, so nothing slips between) and a reconcile of its newest 400 against wrapItems (the wraps held, full id and time, 600 kept beside the 16-char prefix log; a wrap the prefix log knows but the list doesn't is added as the relay re-serves it, once); nos.lol, with negentropy disabled, keeps the plain subscription with its backlog, and so does every relay while the one-time heal pass runs. Notifications: the refresh reconciles the relay's newest 120 against notifHeld (every notification event seen, 300 kept — not just the rows, or a folded zap twin would come back every visit), persisted beside the rows; the watch is unchanged. Not applied to feeds: the outbox relays mostly lack it (nos.lol, damus, primal, nostr.wine, nostr.mom, nostrplebs say no; relay.coinos.io, purplepag.es, ditto, oxtr, nostr21, offchain yes) and a first older page has nothing to reconcile against. Tests: tools/negentropy-test.js (the reference's bytes, one round for a small difference, eight for a 4 KB frame, nothing to say when equal); tools/negentropy-sync-test.js (an in-page relay speaking NEG with the library on its side: the wrap subscription opens limit 0 and the three wraps lacked are fetched by id, nos.lol's "disabled" gets the plain subscription and is remembered, the boot refresh fetches the three notifications by id and the page's refresh and a refresh after reload fetch nothing). notif-report-fold, notif-faces, dm-filter, dm-row-resort, dm-nip42 pass. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp