| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
| |
half-megabyte head start. Adam: flipping through clips should just play them, large ones too, and load a bit of what's next — not the whole of it. A clip over the size budget (or one whose size the host won't say: most, behind CORS) used to wait in the pager under a tap-to-play cover, and even a small one waited on its size probe (up to 5 s of HEAD) before playing. Now, on an ordinary connection, the pager plays the clip on screen at once, streaming as it goes, and fetches the opening 512 KB of the next two into the browser's HTTP cache with a range request (a host that ignores the range has the rest cancelled); the player, given the file when it's swiped to, takes its opening from the cache and streams on from there (the test's server sees its first request start at byte 524288; playing ~300 ms after the swipe). The next clips get no player of their own beforehand: one given its file, even with preload=metadata, took the whole 34 MB on a fast link. With Data Saver on or a slow link (2g/3g), the pager is careful as before: a clip over budget waits for a tap and nothing loads ahead. tools/pager-eager-test.js (35 MB clips: covered in the feed, played in the pager, 512 KB each ahead and nothing more, the cache reused; Data Saver: nothing ahead, the cover); feed-media-views-test moves its tap-to-play checks onto a slow link and checks the 50 MB clip plays on an ordinary one (its index-last probe check fails on HEAD before this too).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
the clips ahead are looked for sooner. Adam swiped to the last clip (one more loaded, slowly), closed the pager and the feed was somewhere else. Closing used glideToPost: for a row never drawn — a post the pager went on to, maybe paged in while it was open — it guesses the place from the rows' average height, draws there, and glides once to wherever the row then is; rows of very different heights (a long post, a clip finding its shape) put the guess far off, and the glide was never corrected as they settled (in the test: 325px past it; a row still undrawn after the guess sent the feed to the top). Now landOnPost jumps (the pager was covering the feed), draws, and sets the row under the top again each frame until it has sat still a moment, 2.5 s have gone, the reader touches the page, or the page is sent elsewhere. And the pager keeps more ahead before paging the feed on: six slides for clips (fewer and further between, each watched a while), four for pictures (was three). feed-pager-ahead-test: some of the words-only posts are long now, and closing on the second-last picture checks the feed sits on its post after the pictures above have loaded (fails before: row top -325).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
tapped a feed clip away from its play button and nothing happened (his screenshot: the clip paused, the player's bar up — 0:09 / 0:18). Once a clip has played its own controls are up, and Chrome on Android keeps a tap on such a clip for them (it shows or hides the bar): the page gets the pointer events but never the click, and the click was all videoTap listened for — so the player's full screen before, and the pager since b01ec80, were out of reach by touch. Now a touch that lifts where it went down, within half a second, is taken at pointerup; the control bar and a paused clip's play button stay the player's, a drag stays a scroll. Where the click does come after, it is spent: on the clip (no thread opened, no second pager) and on the pager it opened (which would otherwise pause the clip and raise the details). tools/video-touch-tap-test.js plays the phone out by swallowing the click on a clip with controls: the touch opens the pager; the play button, the bar and a drag open nothing; with the click let through, one pager, no thread, the clip playing with nothing raised.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
post (read several times that day) at the head of his feed under "47 reposted": a repost takes its place in the feed by when it was boosted, and the only check against showing a post twice was the feed's own window — the 80 posts in memory, the 30 cached — so once the copy he'd read was out of it (a reload, a long scroll), the next boost brought the post back as new. Now a post is remembered as read on this device (per wallet, two weeks, 5000 at most) once its row has held half itself, or half the screen for a tall one, in view for a second, or it was shown in the media pager — remembered by the row it was read in (the post, or the repost it came under). A later repost of it under any other row stays out of the feed (mergeFeed); the row that was read keeps its place, so scrolling back still finds it, and your own reposts show. tools/feed-reboost-test.js (read after a second in view, not at a glance; a live boost of the read post stays out while one of an unread post lands; after a cold reload another boost of it stays out and the post is where it was) — with the check off it fails as Adam saw, the post at the top under both boosts. feed-stability's two failures are on HEAD before this too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
clips keep coming. Adam swiped to the last picture of a pager opened from the feed and it simply ended: that pager walks the feed's own posts, which page in ten at a time as the feed is scrolled, and swiping isn't scrolling — its "near the end" ask went to the media grid's crawl, which this pager doesn't read. Now, with three slides or fewer past the one on screen (on arrival, and on the pager's 1.5 s tick if a page came back empty or the feed was busy), the feed pages on — the posts it holds, then older ones from the relays (loadOlderFeed) — and the pager grows, several pages in a row when those posts carry nothing to show, until it is more than three ahead or the feed is at its end. tools/feed-pager-ahead-test.js: 26 pictures across 41 posts with 15 words-only posts in the middle and the older ones only on a stand-in relay (the feed's cache keeps 30); stepping through reaches every picture, in the feed's order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
|
| |
clips a swipe away and a seek bar to drag. Adam full-screened a video in the feed and swiped up for the next one, and nothing moved: a tap on a feed clip handed it to the player's own full screen (video.requestFullscreen), where every touch is the player's and there is no next. Now, as pictures do since 6eda018, the tap opens the media pager on that clip over the feed's clips in the feed's order (every shape, not only the tall ones the Videos view flips through), carrying on from where it was. What the player's full screen gave is kept: the pager puts the page itself full screen (the tap's own gesture), and a wide clip locks the screen to landscape — for that clip; past it the screen follows the phone. Each clip in the pager (the media grid's too) gets its own seek bar, a thin line along the bottom that thickens with the details up; dragging it scrubs with the time shown, and the drag is never the pager's swipe or tap. A tap pauses the clip and shows the post's details, the × and the bar; another plays on. Leaving full screen another way (Back on Android, Escape) leaves the pager in the same step; × or Escape after moving on lands the feed on the post last watched. Threads, profiles and chats keep the player's full screen. tools/feed-video-pager-test.js (open, resume, landscape lock and release, seek drag, tap pause/play, swipe and arrows, Escape onto the post, exit full screen); video-tap-test now checks the feed opens the pager and runs its native full-screen checks in a thread. tools/puppeteer.js takes CHROME=/path for a machine without /usr/bin/google-chrome. feed-media-views-test's range-less probe check fails on HEAD before this too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
|
| |
|
|
|
|
| |
goes to the feeds it belongs to. Adam posted from the laptop with the #photography feed open and found the post in #photography (untagged) but not in Following, on either device, until a later refresh. Two causes. The feed's opening ask (prefetchFeed, and seedOtherFeeds) took only the first 400 of its authors, and your own key was added after the follows — with 898 follows it sat in the third chunk, so the opening ask never named you; the later refresh (feedPass, every chunk) did. Your key now leads the author list, and the opening ask covers every author, a chunk per ask. And publishPost put the provisional post at the top of whatever feed was open: now it goes to the feeds it would reach — the open feed and Following when they carry your posts (Following, a people feed with you in it, Everything) or a topic the post wears — swapped for the signed event or withdrawn in each. The hashtags in the text go out as t tags (NIP-24, up to the hashtag-spam count), so topic feeds here and elsewhere find the post. tools/own-post-feeds-test.js (450 follows: every one asked, your key first; a post from Following; an untagged post from a topic feed stays out of it; a tagged one lands in both).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
| |
swipe away. Tapping a picture in a feed row used to open it alone (the lightbox); it now opens the media grid's pager on that picture, over the feed's pictures in the feed's own order — every picture of a post before the next post's (a repost's inner post, the feed's moderation) — so a swipe up or down, or any arrow key (up/down as before, now left/right too), steps to the next or previous one. A tap on the picture that opened it still closes it, as a full-screen picture always has; once the reader has moved on, the first tap shows the post's details and actions, and the next (or Escape, or the ×) closes the pager with the feed landed on that post (glideToPost, once the pop has brought the feed back). The pager's pictures pinch to zoom (attachZoom, now exported, without its wheel — the wheel is the pager's; at 1× one finger is the pager's swipe). Pictures in threads, profiles and chats still open alone. tools/feed-picture-pager-test.js; image-tap-test and lightbox-scroll-test accept the pager as the full-screen picture.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
| |
memory that puts a reader back on the row they left when Back closes a thread or profile fired on every history pop, and a remembered place goes stale whenever its page is left another way (the Feed button from a thread), so a photo's × jumped the feed to wherever an earlier thread had been opened from, or to its top (Adam's recording, 2026-10-11 22:31, at the 10 s mark; reproduced on his tab: 2500 → 22361). Now the place is restored only when a pop actually leaves a thread or profile — the page being left is read in a capture-phase popstate listener, before the app's own handler re-renders — and the entries above the one it matches are dropped as stale. tools/lightbox-scroll-test.js (stale place from a thread left by the Feed button, a picture opened and closed, Back out of a thread still landing on its row). avatar-profile-test fails on HEAD before this too.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
| |
wallet — the watch-only view of a sealed vault, or a nostr identity signed in over one (Adam's debug Chrome: three watch-only accounts, the vault gone, his extension identity on the first) — has no keys to pay with, and the zap button knew it: it got no handler at all, so the tap pressed the icon and nothing more, under an arrow cursor. Now the tap toasts "Your wallet is locked, so it can't zap. Unlock it to send sats." and opens the Accounts page, where the padlock and Add account are; the open feed is remembered for the way back (by URL, or by id for Following, which has none — returnAfterSignin learns the id form). tools/zap-locked-test.js.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
| |
opening its editor first (a saved timeline already had the drop in the ⋯ menu, having nothing to edit); the menu now offers Delete feed for every feed of yours, Edit above it where there is something to edit. The editor keeps its own Delete feed. feed-custom-test deletes "Coin talk" from the menu.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
| |
server-made copy. The app shrinks a post's photo to the column's size itself (an 1100px copy made once, kept on disk) — but only when the host allows CORS; one that doesn't (primal's blossom, nostrcheck, most personal sites) left the <img> painting the original, and a 9528×6328 camera file froze a wheel scroll for 400 ms on its texture upload. vmedia now has /vmedia/pic.webp?s=col|tile&u=<picture>: the picture at 1100px wide (or a 360px square of its middle for the grid), made with libvips (shrink-on-load, so the original is never decoded whole; turned the way its EXIF says; alpha kept; stripped) as WebP, cached on disk and at the CDN for everyone. The usual guards: https on public addresses only, each redirect's host checked, 60 MB cap, the bytes sniffed (a GIF is refused — its copy would be a still), a few jobs at a time, 600 new copies per address per hour, an 8 GB cache. The client asks the server for the column copy when the host is known to keep its bytes (or has just refused, or there's no worker to make one here), keeps it in the same cache as the copies it makes, and takes the server's tile for the grid the same way; a picture the server has no copy for paints as before. The container gets libvips-tools (Dockerfile). tools/pic-proxy-test.js (canned server answers). Server measured on cs: a 3072×4608 photo in 0.5 s the first time, a millisecond after; feed-stability's two pre-existing asserts still fail on 491904e.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|