| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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 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]>
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
ahead of running out.
Appetite: the screens the reader has scrolled by their own hand over the last ten seconds (a scroll within a moment of an input — a page held still under an insert above is not travel). Five screens in that time is someone flying through: the page grows from ten posts to twenty, thirty, forty at once, the lead above the foot of the feed at which the next page is asked for grows the same way (from four screens to sixteen), and each relay round trip asks for proportionally more (30 → up to 100 per chunk). Standing still ten seconds brings the ordinary page back. (Adam, 2026-10-16: "could we load posts faster if it seems like the user is scrolling a lot? … try always loading more posts and loading them sooner when we're further away from the bottom".)
The relays run ahead of the rows now. The older page used to be asked for only once every post in hand was on screen, and shown only once the whole pass had answered — so the reader who reached the end of what we held waited a relay round trip, up to the five seconds the slowest relay in the pass is given. A pass for older posts (fetchOlderAhead) starts as soon as what we hold beyond the shown rows thins to a page or two (more with the appetite), its rows warming at the door while the reader is still on the posts before them; the showing step (loadOlderFeed) takes from hand and only waits on a pass already underway when the hand is empty. The in-memory window keeps up to fifty posts beyond the shown rows for that (it kept one page).
tools/feed-appetite-test.js: an ordinary pace pages ten at a time with the relays asked while posts are still in hand, so the end of what was held finds more already there; two flights of eleven screens page twenty then thirty at once, from further above the foot than the ordinary lead, with the relays asked for 60 and 90 per trip; ten seconds still and the page is ten again. The .notes-feed card carries data-shown / data-held for it. Pre-existing failures unchanged: feed-live "picture already decoded" and feed-streaming "tap reveals the oldest ready new post" fail on a clean worktree of ca3b61e too; feed-stability's two known ones.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
faces and pictures fetched ahead.
Adam, 2026-10-16: "I still see spinner when there's no replies. I'd rather
have replies pop in if they have to than wait just to see there are none.
… for ones we've preloaded they appear fast but they still take a moment to
render and shift the page when they do, can we not pre-render them as well
so they're just instantly there on screen?" (1) No spinner at all now: the
post is on screen the moment it is tapped, replies land as they arrive, and
"no replies" is said only once the relays have spoken. (2) What shifted was
each row's face, pictures, previews and quote cards landing after the row:
a thread's rows in hand — seeded from the count fetch, fetched ahead on a
moment's reading, adopted when the count answers — are now warmed the way
the feed warms a row before painting it (warmThread → noteReady: the face
thumb, the pictures' sizes, the link cards, the quoted notes), so the tap
paints the thread in its final shape; a reply arriving from a relay waits
up to 1.5 s for its face before it goes in, so it pops in whole rather than
shifting as it fills.
tools/thread-instant-test.js: the post fetched ahead opens with its reply
and, at its first paint, the replier's face already a thumb; a post with
nothing in hand reads at once with no spinner and no "no replies", its first
reply popping in at ~480 ms with the slow one still to come, never a
spinner; the rest as before. thread-cache, thread-pager, thread-back-scroll,
feed-conversations, moderation, reply-count, feed-live and
notif-report-fold pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
relays answer.
Adam, 2026-10-16: "Still seeing a spinner sometimes when I click into a
thread." Three things stood between a tap and the conversation. (1) The feed
already fetches every reply to each post on screen — the count on the reply
button comes from them (watchZaps, kinds 1 and 1111 by #e) — and then kept
only their ids. Now the replies themselves are kept (replyEvents: the 400
targets most recently heard from, 60 each) and a thread is seeded from them
when it is opened (repliesInHand: those naming the root, and those naming
only the reply they answer), so a post that has been on screen opens as the
whole conversation, no relay asked; a thread fetched ahead before the count
answered takes them when opened, and the open thread takes them when the
count fetch lands (adoptRepliesInHand). (2) The loader waited for every
relay's EOSE — the slowest relay, 4.5 s at worst — before the thread
replaced its spinner; now replies go in as they arrive (queryStreamOn), the
page repainted in place, and the root the moment it is found. (3) The
spinner stood under whatever was already there while the load ran; now it
shows only while there is nothing to read, and "no replies" only once the
relays have spoken. And the fetch-ahead fires after 600 ms of reading
instead of 1.2 s (a quick reader beat it to the tap), a dozen a minute.
tools/thread-instant-test.js (the relays answered in the page): a post read
for a moment is fetched ahead, the count answer released only after that
entry exists, and the thread opens with its reply 12 ms after the tap, not
after its own 6 s ask; a post on screen opens whole in 16 ms with the three
replies the count brought, before the relays answer the thread's ask, and
their answer adds nothing twice; a post with nothing in hand shows a spinner,
then its first reply at ~470 ms with the slow one still on its way, the
spinner gone, the slow one landing in place. thread-prefetch-test follows
the new dwell and rate; thread-cache, thread-pager, thread-back-scroll,
feed-conversations, moderation, feed-live, feed-switch-seed, profile-pager,
notif-report-fold and reply-count pass (thread-reply-test's 30 s wait fails
on clean HEAD the same way).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Videos and Articles — not a feed of its own.
Adam, 2026-10-16: "conversations [should] be one of the filter chips between
all and images and all should become notes". So the view row reads Notes ·
Conversations · Images · Videos · Articles, and Conversations shows the
current feed's people in reply — Following, Popular, a list, a topic, a pack,
anyone's timeline — each reply under the post it answers, as 885bc26 drew
them. The view is a feed state of its own under the feed's id (suffix
~replies: its own notes, clock, disk cache, fresh counter and live watch),
with a definition derived from the feed's (replies: true) so the merge,
seeding, borrowing, watch and kinds all go by it unasked; Notes ↔
Conversations is entered like a switch of feed, the feed chip unchanged, and
the chosen view persists across feeds and reloads like the others. A feed's
state is forgotten together with its view's (dropFeedState) when its query
or relays change or it is deleted. The built-in Conversations feed chip and
the editor's "Replies instead of posts" toggle from this morning are gone
with it (def.replies stays as the derived definition's mark).
tools/feed-conversations-test.js follows: the view chips in order, no feed
chip of its own, the view's cache under following~replies, the feed chip
staying on Following while the view is on, Back landing on the view, a new
list feed opening straight into the view with the replies borrowed from the
feeds in hand and Notes giving its posts, Following's view back at once, and
a reload opening on the view from its own cache. feed-media-views-test names
the chips; feed-custom-test is back as it was. feed-media-cache-test is
flaky either way (a stray first page in its request window, or its 8 s
timing, on clean HEAD too); feed-custom-test's "lands in Everything" fails on
clean HEAD; the other feed suites pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
the post it answers.
Amethyst's Conversations beside its New Threads (Adam, 2026-10-16: "a replies
mode or Conversations mode like Amethyst has"). The Following feed showed
their posts and dropped every reply, so what people were saying TO each other
only turned up by opening threads. Now a built-in Conversations chip sits
beside Following (id 'replies'; its own notes, clock, disk cache and relay
choice like the other built-ins): the same people, asked for notes and NIP-22
comments, keeping the replies — a reply as NIP-10 marks it, a comment, never a
quote or a repost (feedKinds/feedKeeps; the merge, seeding, borrowing, the
live watch and the media index all go by them). Each row shows what it
answers above it — "↩ Replying to" and the quote card of the parent (the e
tag marked reply, else root, else the last; a comment's e or E), tapping the
card opens that post, tapping the reply its thread at the reply — and the
header drops its "reply" tag there. The parent is usually in hand already
(the Following feed, a thread, someone's page: noteInHand, which quotedNote
checks before any relay — nostr:nevent quotes gain the same), and a thread
opened from such a row paints its root at once instead of fetching it. In a
conversations feed the parent counts as part of the row, so it arrives whole
with the first screen (noteReady's parent option, per feed); a profile's
Replies tab shows the same context and fills its cards in. Not a shareable
URL (it is nobody's timeline), so no Share in its menu. Any custom feed can
be one: "Replies instead of posts" in the editor (def.replies, synced with
the definition), named in its summary. The disk cache now keeps comments.
tools/feed-conversations-test.js: Following keeps posts only; the chip; the
replies newest first with the parent card from hand (no relay asked) and a
missing parent said so; the menu without Share; the reply's own tap opens
its thread at the reply, with the root painted from hand; the card's tap
opens the parent; Back lands on Conversations; the profile's Replies tab
shows the context, Posts none; a custom conversations feed via the editor
borrows the replies from the feeds in hand, not the posts. feed-custom-test
lists the new chip. Pre-existing failures not touched: feed-custom-test's
"a tagged post lands in it"/"lands in Everything" and feed-streaming-test's
pill position fail on clean HEAD the same way; feed-follow, feed-cache,
own-post-feeds, feed-list, feed-media-views, feed-switch-seed, profile-pager,
thread-pager, feed-repost, feed-live, feed-new-posts, feed-window,
feed-choice-per-wallet, feed-nav-destination, feed-wallet-back, moderation,
feed-mention-tap and feed-reboost pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
a closed wallet keeps its coins renewed. Three asks from a subscription service builder (PullCap, by DM, 2026-10-16) testing a 1-sat-an-hour pull against a locked phone. (1) "For a monthly subscription a monthly cap would be tighter": a connection's budget now resets every day, week (Monday) or month, in UTC (nwc-budget.js: the window key stamps the spent counter; both the app and the worker count by it), chosen on the form, read on the card ("21,000 a month · 0 spent this month"); the worker now also counts what the APP spent this window (the counter rides the mirror) — it only ever counted its own log before, so a daily budget could be exceeded across the two; get_budget answers used/total/renews_at/renewal_period (NIP-47), in get_info's method list and the 13194 event. (2) "Does V3 reply with an NWC error when a connection is revoked, or just stop answering?" — it stopped: the open app dropped the request, the mirror forgot the key, the device even de-registered it from the push notifier, so a revocation looked exactly like a phone that is off. Now a revoked connection is kept for 30 days (revokedAt; the earlier date wins in a merge): still subscribed, still registered for pushes, still in the mirror flagged, and every request on it — from the app or the worker, in the asker's scheme — is answered UNAUTHORIZED "this connection was revoked"; no capability event for it; after the grace it is forgotten. (3) "If a user is only woken once a month by an NWC request, will V3 renew the coins on its own?" — only for one cycle: the app registers a standing order per coin, but a coin the worker SPENDS voids its order and the change had none until the app next opened, and a renewed coin sat unclaimed until then too. Now every NWC wake, after the answer and inside what is left of the push budget (tendRenewals, sw-nwc.js): claim orders whose round has run — and drive a claim left in flight on, since one takes several round trips and confirmations — then register an order for every coin without one (backstopMargin shared with the app; limited to the output keys the mirror carries; the mirror now also carries the output key a standing order or a claim in flight burned, which was below the window). Standing orders merge by unlock hash when the app returns (mergeScheduled, in mergeArkStates), so it adopts the worker's. tools/ark/bg-renewal-test.js on regtest, end to end: the worker registers from the mirror alone, a worker spend kills the order and the next wake covers the change, the round runs with nobody online and the next wake claims the renewed coin — and covers it in the same wake — then the app merges it all (fresh coin, right balance, the order adopted, not busy, one history row). tools/nwc-budget-test.js (windows, renews_at, counting with the worker's unabsorbed spends); nwc-sw-test.js (monthly budget over app+worker spends, last month's not counted, get_budget, UNAUTHORIZED in both schemes, the mirror's keys and flags, tending: claim/drive/register and their limits); nwc-test.js (monthly refusal naming the period, get_budget, a counter from last month reads as nothing, the revoked connection answered UNAUTHORIZED, no 13194, revokedAt in the snapshot, forgotten after 31 days, the earlier date wins); nwc-settings-ui-test.js (the form's period choice, the card's wording, Revoke dates it); ark-renewal-sync, sync-blob, sync-outbox and spending-restore tests pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
paid to you; the media pager says when a picture can't be loaded. (1) LUD-03 (Adam, 2026-10-16: "add lnurlw support"): an lnurl whose terms are a withdrawRequest — found when the pay fetch meets one (fetchPayParams throws with the terms attached, resolve() takes them up) — shows the site, their range (or the fixed amount) and description, the amount field at their max; Withdraw makes an invoice over Ark for the amount (arkLnInvoice), hands it to their callback with k1 (submitWithdraw), and watches it (arkLnWatch): "has your invoice for…", then "Received …" on settlement; their refusal in their words, with Try again; Back before it is paid stops watching. An amount outside their range is told the range, nothing sent; a watch-only device says it can't receive. LUD-17 schemes lnurlw:// and lnurlp:// (and lightning: before them) are the URL over https, http on an onion (parseZapTarget). (2) A pager slide whose picture can't load — the server copy and then the original both failing (isolabellart's relay answering 502 that afternoon: the broken-picture glyph sat in the corner of a black slide) — hides the <img> and says "This picture couldn't be loaded" with the host, caption and controls kept (mp-gone / .mp-missing). tools/lnurl-auth-unit-test.js (lnurlw:// and lnurlp://, an onion, the terms, a pay fetch that finds a withdraw, the callback's k1 and pr, a refusal); tools/lnurl-withdraw-test.js (the zaps feature against a faked Ark and a stand-in site: the terms, max prefilled, out of range refused unsent, the invoice at the callback and watched, Received on settlement, a fixed amount with no field, Back unwatches, a refusal's reason); tools/pager-gone-picture-test.js; lnurl-auth and feed-picture-pager tests pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
instead of trying to pay it. A community member pasted Stacker News's lightning-login lnurl into Send and got "stacker.news: no sig query variable provided" (2026-10-16): every lnurl1 was taken for LNURL-pay, their login endpoint was fetched as if for pay parameters, and its refusal came back as the error. Now an lnurl whose URL carries tag=login and a 32-byte k1 (parseLnurlAuth; lightning: prefix and upper case fine; action register|login|link|auth) is a login: the wallet asks first — the site's name, what it is asking (log in / create an account / link / confirm it's you), that the challenge is signed with a key kept for this site alone and that nothing is paid or shared — then signs k1 (ECDSA, DER) with the LUD-05 linking key (m/138'/0 as hashing key, HMAC-SHA256 of the domain read as four 32-bit child indexes under m/138'; the same wallet is the same user on a site every time and a different one elsewhere; from the mnemonic, or an imported xprv) and calls back with sig and key (lnurlAuthKey, lnurlAuthLogin). The site's OK shows "Logged in to <site>", its reason otherwise, with Try again and Back. It takes no balance (checked before canPay in zaps' matchSendText), so it works on an empty wallet and from an Android lightning: intent. tools/lnurl-auth-unit-test.js (login vs pay vs bad k1; one key per site per wallet; the DER signature verifies under the key sent; the server's reason is the error) and tools/lnurl-auth-test.js (a stand-in site that verifies the signature: the ask, nothing sent before Log in, OK, the same key next time, a register link, a refusal's words); send-dest-test passes.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
one zoom layer, not one per slide. A flight through the pager stuttered on the phone (Adam, 2026-10-16: "a bit of choppiness as they scroll"): every slide within a screen and a half loaded its picture's ORIGINAL as it passed — camera files of several megapixels, decoded on the main thread and uploaded as textures mid-flight — and every slide's <img> carried will-change: transform (for the pinch), a full-size layer each. Now a slide's <img> starts on the copy the feed painted (1100px wide, decoded already: mediaReady's thumb), or the server's copy (pic.webp s=col, cached at the CDN), or failing both the original (onError falls back to it too); decoding async. The slide at rest is sharpened a moment after landing (pagerSharpen: the original fetched and decoded aside, then swapped in — same box, same aspect), so a pinch has the detail, and a slide paged past costs nothing. Only the current slide (mp-cur, set in pager.arrive) has the layer. tools/pager-sharpen-test.js (three 4-megapixel pictures: every slide opens on a blob copy, the one on screen alone has will-change, it is sharpened to the original within a moment while the others keep their copies, ArrowDown moves the layer and sharpens the next); feed-picture-pager, pager-resize, pager-wheel, thread-pager and pager-swipe-fly tests pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
mutes on one tap; a flick flies long, to be caught. Three from Adam, 2026-10-16. (1) "Make the reaction button in media mode give a menu of different emojis": a tap on ♥ opens the feed's own emoji sheet (ui.reactPick → reactPicker) over the pager — the backdrop gets over-pager (z-index 300, above the pager's 200) — the emoji chosen shows on the button (postReactNode, a picture for a :code:), a tap again takes it back, and Escape closes the sheet before it would close the pager. (2) "The mute button, when I click it, immediately unmuted again": under a finger the pager arms a pending tap at touchend and stands it down when the browser's click arrives — but that was judged in a bubbling listener on the pager, and the sound button's own handler stops the bubble, so its click never stood the tap down and the pending tap clicked it again 120 ms later (every button in the pager with stopPropagation; the arrows too). Judged in the capture phase now, before any button's handler; tapDoneAt is set after the pending tap's own click so that click isn't swallowed. (3) "Make it fly longer before it settles… I can slow it down by touching again when I see something I like": a flick coasts 1600 ms' worth (was 800), up to 14 posts, at 260 ms a screen (up to 4 s) — a finger down holds it where it is, and lifting lands it on the nearest post. tools/pager-controls-test.js (one tap mutes and it stays muted; a tap again unmutes; the sheet opens over the pager, on top, with several emoji and a way to more; 🔥 picked shows lit on the button; a tap takes it back; Escape closes the sheet, not the pager); pager-swipe-fly-test adjusted for the longer coast; pager-tap-slop and feed-picture-pager tests pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
flight took a hard fling (3 px/ms, coasting 500 ms) — Adam, 2026-10-16: "too hard to get it to fly". Now from 1.6 px/ms, coasting 800 ms (so a brisk swipe is two posts, a hard flick five or six), and the finger's speed is its average over its last 80 ms rather than its last five samples — at a phone's 120 Hz those were a third of that, and the finger slowing as it lifts hid the flick. tools/pager-swipe-fly-test.js: the quick flick is three big moves on a schedule from the start (a move's handling in a headless browser takes 30–50 ms, so a paced flick came out a stroll), flying three to six posts; a 320px swipe over 130ms is still one post; pager-tap-slop and profile-pager tests pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
snapped on one. A swipe went one post, however hard it was flung (Adam, 2026-10-16: "can we make images coast on the phone too when I swipe?"). Now a flick of FLY_V (3 px/ms) or more flies as many posts as the fling would have coasted — its speed times FLY_TAU (500 ms), in screens, up to six — in a glide, the pictures passing, to land square on the last (pagerFlyTo, the flight factored out of the wheel's pagerFly, paced a little slower per screen for a finger); only the landing is arrived at. A gentler swipe is the one post it was. A fling that would go past the end lands on the last post (or the first), and a swipe from there wraps by one post as before (a fling's whole count no longer picks the wrap target); a finger put down mid-flight catches it where it is. tools/pager-swipe-fly-test.js (a 320px swipe over 130ms is one post; a 420px flick in 72ms is three; the counter agrees; near the end it lands on the last and the next flick wraps to the first; a finger catches a flight) — the quick flick is dispatched inside the page, since the browser's touch simulation takes a round trip per move; pager-tap-slop and profile-pager tests pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
pictures; a tap brings up the caption and controls wherever the pager was opened from. A picture tapped on a thread page opened alone, full screen, and a tap on it closed it — no caption, no controls, nothing to swipe to (Adam, 2026-10-16: "clicking again dismisses it instead of showing me the caption and controls… let's make it consistent no matter where media is opened from"). Now a thread is a pager source like the feed and a profile page (pageRowOf/pagerSource `thread`, keyed by the thread's root): its slides are the root's pictures and then its replies', in the order the page draws them (threadTree/threadOrder, the tree walk factored out of threadScreen), with nothing more to fetch; closing after moving on brings that post on screen. And in any pager opened from a page, the first tap shows the post's details and the next closes it — the tap on the picture that opened it no longer closes it outright, so the feed and a profile behave the same as the thread. A face or a chat's picture still opens alone. tools/thread-pager-test.js (the thread's pictures only, root first then the reply's; counter 1/2; a tap shows the author and the words; right and down; Escape leaves the thread with the post on screen; a reply's picture; tap, then tap closes); feed-picture-pager and profile-pager tests updated for the tap rule; image-tap, thread-back-scroll pass; thread-reply-test times out on clean HEAD too.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
Noise sends its two built-in emoji as bare shortcodes — no NIP-30 tag, no picture — so a :marmot: reaction in the community showed as text (Adam; the phone's group log: kind 7, content ":marmot:", an e tag and nothing else). Their artwork is published in the app's repository (builtin_emoji_marmot.png, builtin_emoji_wn.png); emojiUrl now resolves those two codes to it through our picture proxy (serverPic, a 72px tile cached on vmedia for a week, as any picture in a post), not bundled: the app is AGPL and this one MIT. Learned packs and tags still come first.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| | |
|
| |
|
|
|
|
|
| |
learn their emoji pictures. Max added Adam to the 155-member White Noise community and no device showed it: a Marmot inviter picks ONE of a person's published KeyPackages, the Welcome lands on that device alone, and nothing said which (it was the phone's — and the phone, signing through a bunker, couldn't open a 218 KB Welcome until relay.coinos.io's maxEventSize went from 256 KB to 1 MB: the NIP-46 decrypt request carrying it was over the cap). Settings → Nostr gains "Group invitations" (groupDevicesCard): this device's standing, the account's other invitation keys on the relays (by app and date), and "Invite this device only" — a kind 5 naming each other key (NIP-09 `a` for the addressable 30443s, `e` for an old 443) and a fresh key of this device (marmot-client devices/onlyHere). A device whose key was retired by another sees the deletion, newer than its key, when it next checks (checkRetired, a few times a day, before any republish or the card) and stops putting one back, until "only here" is pressed on it in turn. And a group's reactions and messages now teach the app a :code:'s picture from their NIP-30 tag (noteEmoji on the group paths), the way feed reactions do: a :marmot: reaction in the community showed as text. tools/wn-only-device-test.js (the card lists three other keys newest first; the deletion names the two slots and all three events and not ours; the card then shows no others; a retirement by another device is honoured and taken back).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| | |
|
| | |
|
| |
|
|
|
|
|
| |
pictures. Tapped on someone's profile page, a picture opened alone in the lightbox (the pager ran only over the feed), so there was nothing to swipe to (Adam). Now the page a row is on says what the pager runs over (pageRowOf, pagerSource): the feed, or this person's timeline — their posts' pictures (and clips) in the timeline's order, nobody else's, more fetched as the reader nears the end (loadOlderNotes, the way the feed pages on); the tap rules are the feed's, and closing after moving on lands the profile on that post (landOnPost judges the page by what is on screen, since a refetch replaces the timeline's cache entry). The profile screen syncs the pager as the feed does. tools/profile-pager-test.js (her pictures only and in order, the end past her last, Escape lands on the post, a tap on the opening picture closes).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
first moment. The pager's resize handler re-showed pager.cur, which the slide observer sets a frame after the pager opens — so a resize in that first frame (the page going full screen for the pager, as the player's full-screen button does: a taller window, the address bar gone) had nothing to re-show, and the pager was left to the browser's re-snap. Now the slide meant is an anchor: the start slide the moment the pager opens, then each slide arrived at; a resize re-shows it (or the tail, when the reader is waiting there), and an arrival the observer reports at a size other than the one the pager last laid out at is taken as the resize's doing, not the reader's: the anchor goes back instead. Hardening: Adam's full-screen button on a clip 23 posts down showed another post's still, which a headless run of the same path (a user-gesture requestFullscreen on the feed clip) and tools/pager-resize-test.js (the window grows as the pager opens, shrinks and grows again, changes shape) did not reproduce; the recorder on his tab is for the next time.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
one. A notch was a step, and a breath between steps meant a flick of the wheel moved two or three pictures at most (Adam). Now each notch adds a post to go — the third and later notches of a quick run (within 400 ms) add two, so a flick is a few posts and a spin a dozen — and the pager glides through them with the pictures passing (pagerFly: a longer glide, 100 ms + 110 per screen, eased out), landing snapped on the last. The posts passed are not arrived at: the slide observer stands down while flying (no clip starts, nothing marked seen, no counts asked), and the landing arrives (pager.arrive, the observer's old body). A notch back against a flight catches it on the post it is passing and steps back from there; a touch or a key catches it too (pagerGlide now takes a duration and an onDone that pagerHold fires on interruption, with the snap restored). A notch from rest is the single quick step it was; sideways notches and trackpad runs step as before. tools/pager-wheel-test.js: a spin of five flies eight posts on and lands snapped (gliding mid-way), the spin back, a notch against the flight. feed-video-pager-test's two failures are on HEAD before this.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
pointer. Adam reached for the scroll wheel in the pager on his laptop and nothing useful happened: a notch scrolled the snap container a hundred pixels and the glide's snap put it straight back (a headless probe of the live pager: scrollTop stays 0), so the arrow keys were the only desktop way through. Now a wheel notch is a step — down the next post, up the one before, a sideways notch (a trackpad's, or shift+wheel) along the post's pictures — with a 220 ms breath between steps so a spin of the wheel is a step or two, not five; a trackpad's run of small deltas is one step per run (its first 40 px, the rest until a pause is spent). Ctrl+wheel zooms the picture at the cursor, which is also what a trackpad's pinch arrives as in Chrome (attachZoom's new wheel: 'ctrl' mode, 0.004 per unit); while zoomed the plain wheel pans it, and at 1× it is the pager's again. And where there is a pointer to hover with (hover: hover and pointer: fine), arrows: up and down at the right edge, left and right when the post has a picture that way (has-left/has-right, set as the slide on screen changes), faint until the pointer is over the pager. The keys, the wheel and the arrows share one step (pagerStep). No mouse drag-flick: discrete steps suit a pager. tools/pager-wheel-test.js (CDP mouseWheel is always a 120-unit notch to Chrome, so a trackpad run is dispatched page-side; headless has no hovering pointer, so one is given with --blink-settings); feed-picture-pager and image-zoom tests pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
| |
dropped. A reply Amethyst showed was missing from Adam's phone (2026-10-14, Johnny's reply to d558ba5d): the row was in the stored list, but two of his follows had filed kind 1984 person reports ("spam") on Johnny, which meets the two-follows bar, and the page filtered every one of Johnny's rows out without a trace — the feed and a thread fold such a post with who flagged it and a Show, the Notifications page simply lost it. Now the page keeps the row and folds it (notifFoldRow): the face, name and what they did, "Reported by mar, Rich" (up to three names, else how many), the time, and Show — which reveals it here and, being the same id, in its thread. Mutes and spam still drop the row; the unread count still leaves flagged rows out. Opening the page also syncs your follows' reports (syncReports, throttled), which only the feed and chat home did. tools/notif-report-fold-test.js.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Back goes
The × was a blind history.back(), and the entry under the pager isn't
always the feed: the browser's older entries outlive a background reload of
the app, and boot merges entries. It took the reader to the wallet, then the
chat list. closeLayer still steps Back (so the phone's Back doesn't reopen
it), but if Back lands anywhere but the page under the layer, the reader is
put on that page and that entry rewritten to it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
|
|
| |
controlslist=nofullscreen kept Chrome's full screen (no next clip) off the
feed, but greyed the button out. The button is back; pressing it backs out
of Chrome's full screen and opens the pager on that clip instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
faster, no step back
- The glide was timed from now, but a frame's time can be a little earlier:
its first frame stepped back (72px -> 58) before going on, the choppy
'reset' on letting go. Timed from its first frame now.
- Slides follow the finger after 10px (was 30: a dead zone, then a jump).
A touch that comes and goes within 24px and 350ms is still a tap.
- On letting go: on to the next past a tenth of the width across, an eighth
of the height down, or a flick of 24px at 0.2px/ms (was a fifth, 48px, 0.3).
- Glide at most 130ms (was 170).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
|
|
|
| |
The glide aimed at n x the scroller's height, but a post is 100dvh, a hair
off it on a phone; the error grew post by post until it landed past the
slide and the snap yanked it back. It now aims at the measured position of
the next slide. Faster too: at most 170ms (lands in ~195ms from lift-off).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
|
|
| |
Letting go of a swipe (or an arrow key) runs the pager's own eased-out
glide, at most 220ms and quicker after a fast flick; a finger catching it
mid-glide takes it from there. A swipe now lands in ~240ms (was ~425ms).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
|
|
|
| |
A boost that reached a profile from the feed was saved as a kind 1 note,
so reopened it drew its raw JSON as a reply; video events and polls lost
their kind and tags the same way. The saved copy now keeps kinds (boosts
whole), old copies are dropped, and profiles ask for boosts too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
clip play with bars, and turn the phone himself when he wants it bigger. The pager no longer locks the screen to landscape for a wide clip (in the installed app or full screen in a tab), so it follows the phone throughout; in the installed app the pager no longer falls back to the browser's full screen either (that was only to be allowed to turn the screen). A thread's clip and a YouTube clip still go full screen on a tap, without the turn. feed-video-pager-test and fullscreen-notice-test check that nothing asks the screen to turn.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
has to mean it. Adam tapped a clip to pause it and it was taken for a swipe: the slides scrolled natively, and the browser takes a touch that moves a few pixels for a scroll — no click, so no pause — while a quick one flung the pager on to the next slide (reproduced: an 18px drift paged on). Holding the scroll back after the fact doesn't work (the fling comes after touchend), and stopping the first move stops the browser scrolling for the whole touch, so the pager now judges and moves touches itself: the slides hold still until a touch has gone 30px — short of that it's a tap, done here when the browser sent no click (a button under it is pressed), and a later click isn't counted twice — and past it they follow the finger the way it set off (up/down post to post, across within a post, snap off while dragging), going on to the next on letting go if the finger went a fifth of the screen or flicked (0.3 px/ms over at least 48px), else back. Touches on the seek bar, in an opened caption, or on a zoomed-in picture are left to those. And the arrow keys go to the slide or post next to the one on screen, by name, not "a screen further" — from a scroll still settling that landed short (quote-pager-test failed two runs in three). tools/pager-tap-slop-test.js (drifts of 18px and 14px and a quick 25px flick: taps — paused, played, not paged; a 320px swipe up and a 300px swipe down page; a slow 70px drag springs back; a quick 80px flick pages).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
it. Adam turned his phone while watching a clip and heard it start again from the beginning while the picture played on: a second copy playing unseen. Not reproduced here (a turn in the test browser, with the clip muted or unmuted in the feed first, on this build or the last, leaves one clip playing), but three ways to a second copy are now closed. The feed under the pager stays laid out, and its autoplay watched it: anything there coming into view — a turn re-lays the page out — started, the pager's own clip as often as not, with sound if it had been unmuted there (with the guard off, the test's scroll under the pager starts the feed's copy). Under the pager the feed's autoplay now starts nothing, and opening the pager pauses every player on the page (YouTube too). In the pager only the slide on screen may start (not one a half-way layout passed over, whose start was still on its way), and starting one pauses any other. tools/pager-rotate-test.js (installed app, no full screen: one clip through a turn and back, on the slide it was on; a feed clip brought into view under the pager stays still; muted and unmuted in the feed first).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
pictures. Adam: swipes and arrows up/down should go to the next post's media, left/right to the next within a post. The pager is now posts stacked top to bottom, each a row of its pictures or clips side by side — a quoted post's join the post that quotes it — with dots along the top when a post has more than one, saying how many and which. Up and down (swipe, arrows, j/k, Page) move a post at a time and land on a post where its row was left; left and right (swipe, arrows, h/l) move within the post and stop at its ends. A post's pictures all load as it comes near (those along its row too); a clip's head start goes to the next post's (where its row rests), the next along this one, and the post after next, at half; the look-ahead (the feed paging on, the media crawl) counts posts. Pictures pan sideways as well as up and down at 1×. Tests find the slide on screen by where it sits (the pager's children are posts now); feed-picture-pager walks right within a post, down past one with none, back up to where a post was left, left to its first, and a sideways swipe next to a swipe up; quote-pager walks the quoting post's row (its own picture, then the quote's).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
full-screened the screenshots in a quoted post and couldn't swipe on: a tap on a picture in the feed opens the pager on it only if it is among the pager's slides, and those were each feed post's own pictures — a quoted post's are its own, drawn inside the one quoting it, so the tap fell back to the picture alone (the lightbox), with nothing to swipe to. Now the pager takes, after each post's own pictures (or clips), those of the posts it quotes, as far as they've been fetched (and not hidden or folded as sensitive): swipes walk through them and on to the next post's, the slide's details name the quoted post's author and words, and closing lands on the post that quoted it. tools/quote-pager-test.js (a post with its own picture quoting one with two, served only by a relay: the tap opens the pager on the quoted picture, down through the quote and on, up to the quoting post's own, the quoted author in the details, Escape onto the quoting post) — with the quotes left out it fails as Adam saw, the picture opening alone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
pauses any playing clip. Adam: a fixed 512 KB head start isn't right for every clip; and a tap anywhere on a playing clip but the seek bar should pause it. The head start is now seconds of the clip, not bytes: two seconds' worth on a link well ahead of the clip's bitrate, four when only just ahead, and on a link slower than the clip enough to cover the shortfall over its length — at least 256 KB, at most 4 MB, the second clip ahead half. The bitrate is the file's size over its length when the post says both (imeta size and duration; duration is now kept), else guessed from its picture size, else a phone clip's; the link's speed is measured off the head starts themselves (a moving average), and until there is a measure it's two seconds' worth — Chrome's own downlink guess runs low (in the test browser it would have had every first head start at the cap). A reader who swipes on within seconds (a moving average of how long clips hold them) gets just the opening; a clip of 3 MB or less comes whole. A tap on a playing clip in the media grid's Videos pager now pauses it with its details up, as one opened from the feed does; another plays on (it only toggled the details). And mergeFeed's duplicate check now counts the posts waiting a moment to go in together: a copy from a second relay, or another version of an addressable clip, could slip in during that window (video-events-test caught the second, one run in a few). pager-eager-test: clips whose imeta states size and length; two seconds' worth ahead (half for the second) before the link is measured, still that on a fast link measured, the cap (half) on a 3 Mbps link the server paces (the player still takes the opening from the cache there — DevTools' throttling had defeated the cache, not the app); feed-media-views-test checks the tap to pause and play on in the Videos pager.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
feed could ask for just the posts with videos; for notes it can't (a relay filters by kind, author, time and one-letter tags, never by what a post says, and imeta isn't indexed), but NIP-71 gives clips kinds of their own. Of the 900 people he follows, 58 have posted 665 of them (191 kind 21, 33 kind 22, 170 kind 34235, 271 kind 34236 — divine.video's), 12 in the last fortnight next to 220 notes linking a video. Those kinds are now asked for with the notes (FEED_KINDS: feeds, profiles, topics, live), and drawn as posts: the title, the words, and the clip from its imeta — of the versions on offer, the one nearest 1080 on its short side (not the 4K original), never an HLS playlist, its url tag as a fallback — with the imeta's poster and size. A file whose name doesn't say it's a clip (divine.video's have no extension) plays when an imeta types it video/* or it's a video event's file, in a note as much as a video event. An addressable clip (34235/34236) shows once, not once per version. The clips are in the pager and the media grid's Videos (their captions: title and words, no file link); replies are NIP-22 comments, which threads already fetch. tools/video-events-test.js (the kinds asked for; a kind 21 as a post with its 720p version and poster, not "kind 21"; a 34236 with an extensionless file and its older version once; a note's imeta-typed file; the pager over all three with the caption).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
Community header: "Add person" wrapped onto two lines in a box twice the height of the bell and the door beside it. It is now a person with a plus, the same size and stroke as those (and lit while its panel is open), with "Add person" as its label and tooltip — in a community's header and a private group's. The form's own Add person button keeps its words. chat-layout-test finds the button by its label too; SHOT_PHONE=path saves the phone-width header.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
one tap. Adam: Chrome's "v3.coinos.io – To exit full screen, drag from the top and touch the back button" shows every time, and a YouTube clip needs its own small full-screen icon. The notice is Chrome's, put up whenever a page goes full screen; a page can't turn it off, so the app goes full screen less. In the installed app (display-mode standalone, or opened from an android-app:// referrer) there is no address bar and the pager already fills the window, so it no longer goes full screen: a wide clip asks the screen to turn without it, and only if that's refused (the browser may allow turning only in full screen) goes full screen — the notice then, but the clip on its side; the screen is let go past that clip, or on closing. A browser tab goes full screen as before. A feed clip's own control bar loses its full-screen button (controlslist=nofullscreen): a tap on the clip is the way, into the pager. YouTube: the player's taps never reach the page, so a layer over a playing clip's picture takes them, leaving YouTube's control bar along the bottom to YouTube; one tap on it — or on the still — plays with sound and takes the clip's box full screen, turned on its side (unMute + playVideo through the iframe API for a player autoplaying muted); full screen the layer steps aside, so all of YouTube's controls are there. YouTube can only fill the screen through Chrome's full screen, so its notice still shows there. tools/fullscreen-notice-test.js (installed app: no full screen, the screen turned and let go, the fallback when turning is refused; no full-screen button on feed clips); tools/youtube-tap-test.js (YouTube stood in for: autoplaying muted, a tap on the control bar stays YouTube's, one tap full screen with unMute and playVideo, the layer aside full screen and back after; with Data Saver, the still to full screen with sound); video-unmute-test stubs ui for its slice of videoNode.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
the next one, when it comes, is moved onto. Adam reached the last clip before the next was in, and a swipe only nudged the clip and sprang back. Now the pager ends in a screen of its own: a ring sweeping round, three sparks orbiting the other way, the play mark (a picture mark in a pictures pager) breathing in the middle, and "Fetching the next video…" (or "Fetching more pictures…") with a sheen running through it, all still under prefers-reduced-motion. Landing there stops the clip left behind and asks for more at once (the feed pages on; the media grid's pager crawls); slides that arrive go in above it, and one arriving while the reader waits is moved onto and plays. With nothing older to be had (the feed at its end, or the media index), it says "That's everything for now" and goes still. The scroller no longer anchors (overflow-anchor: none), so a slide put in above the screen doesn't hold the screen in view. Strings in all 20 languages. tools/pager-tail-test.js (relays that take 3 s for anything older: the screen at the end, the clip behind stopped, the next clip moved onto and playing, the end saying that's everything, Escape onto the last clip's post); feed-pager-ahead and feed-media-views count slides without it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|