| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
| |
that painted before its picture's size was known (a cached row on a feed just opened, a link card, a clip learning its shape) grew after it was measured, and the stale height is what the spacer took when the row left the window: the page shrank by the difference, the browser's scroll anchoring pulled the reader back by as much, and a coasting scroll lost ground (or ran backwards) right as the media appeared — on a feed opened fresh, 18 such frames in four seconds. Rows are now watched by a ResizeObserver (after layout, nothing forced) so the heights stay live; originals decode async so a big one never holds a paint. Tried and dropped: shrinking no-CORS originals through createImageBitmap — it re-decodes on the main thread. tools/feed-scroll-jank.js grows a hands-off coast scenario (--coast: quick-notch bursts, then let go; dips/jumps in the coast's own travel next to what landed: media loads, layout shifts, row batches, page height, scrollTo/scrollBy callers), --trace (a Chrome trace: the browser's work inside each long frame — decode, layout, paint, GC — and the compositor's dropped-frame count), --url for a never-seen topic feed, --only settled|coast, --start px. Measured: the fresh-feed coast went from 18 dips to 8 small ones; a fresh topic feed coasts with one 33 ms frame in five seconds. Still open: a 9528×6328 original from a host without CORS (primal's) once froze a scroll step for 400 ms on its texture upload — only a resizing proxy fixes that one. feed-stability's "a small feed query starts before opening the feed" and "an unavailable image remains a usable link" fail on 491904e too.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
| |
The phone was fine (its scroll is the compositor's) but the desktop's wheel coast is driven from the main thread, so every long task froze it, and a feed that was still loading ran dozens of them: each ready post went in through its own admitFeed → holdScroll → full render (200–450 ms apiece; a Popular switch did 49 in a few seconds, one of them 1.3 s). Inside those renders the window's list getter — run on every scroll frame too — filtered every note through the mute/spam/report checks, and each check re-derived the login identity (two parses of the stored state plus a bech32 encode): 11 s of a 2-minute profile. Now ready posts are admitted together, one render per 120 ms, and while the reader is moving the page the batch waits for a quiet moment (up to 1.5 s); nostrLoginIdentity keeps its answer for 250 ms (dropped by its own saves, a signer change or another wallet's key), npubOf is memoized, modOn reads the cached messages state, the foreign-language guess is remembered per post, and the window list is reused between renders. Measured on Adam's dev tab (tools/feed-scroll-jank.js: real CDP wheel notches, chip clicks mid-load, rAF frame gaps, long tasks, a CPU profile that names each busy stretch): stalled time during a switch 1.9–2.5 s → 0–83 ms, no frame over 50 ms on the wheel path, main thread busy 33% → 17%; settled scrolling was already clean. feed-streaming's "tap reveals the oldest ready new post" fails on the previous commit too.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
| |
|
|
|
|
|
| |
and room composer still had them as buttons in its row — Adam's long DM to yorgos sat squeezed between a clip, GIF and Send. They now sit at the right of the field's last line (the field keeps room on that side, less with GIF alone), leaving the row as field + Send.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
|
| |
|
|
|
|
|
| |
:peepoClap: read as the bare text "reacted :peepoClap:" — the notification row kept the reaction's content but not its NIP-30 emoji tag (and cut shortcodes at 12 characters). Rows now keep the tag's picture and the whole :name:, draw it like the reaction chips do, and a row saved the old way gets its picture when the relay hands the reaction again. tools/notif-custom-emoji-test.js.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
|
| |
|
|
|
|
|
| |
post opened their profile and then reached the row, which opened the post's thread on top — Adam had to tap the mention again from inside the thread. The mention stops its tap, and a post's row no longer opens its thread for a tap on any link in it (a mention, a URL, a #tag). tools/feed-mention-tap-test.js (on the old code the tap landed on /nevent1…).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
|
| |
|
|
|
|
|
| |
carried only the test's rename — see 3032399 for the description).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
|
| |
|
|
|
|
|
| |
isolabellart saw the Translate button but nothing became Italian: the target was the app's language, coinos has no Italian UI (so it ran in English), and the language guess didn't know Italian. Posts now translate into the reader's choice (Settings → Nostr → Translation, any of 31 languages), else the phone's language when the app has no UI for it, else the app's; the guess knows Italian, Dutch and Polish. And, as Jumble offers: a DeepL API key of your own, kept on the device and sent per request as x-deepl-key — the registrar passes it to DeepL (api-free for ":fx" keys; DeepL refuses browsers) and never stores or logs it; a refused key or used-up quota says so once. names: translateDeepl (prose only, links/tags kept, detected source back, 401/402/422 statuses), CORS allows the header. Deployed to cs names. tools/translate-italian-test.js, tools/deepl-translate.test.js.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
|
| |
|
|
|
|
|
| |
to you can't be opened here · Connect Amber" showed only once the Amber signer had resumed, and the DM drain is what resumes it — on a phone whose every wrap the old code had already logged as opened, nothing is pending, so nothing might ever ask. nostrLoginIdentity now says how the account came in (`via`), and an Amber account counts as one whether or not its signer is live yet (the notice, and the flag that re-opens written-off wraps once a decrypting signer takes over). tools/amber-dm-upgrade-test.js runs that case with ALL_LOGGED=1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
|
| |
|
|
|
|
|
| |
had grown to Post · 📎 · GIF · Preview · Cancel; the reply box's likewise. The paperclip and GIF button are now small borderless tools inside the text field, bottom right (the field keeps room under its text so nothing runs beneath them), and the row under it is only what you do with the post: Post, Preview, Cancel (Preview, Cancel, Reply in a thread). Tests find the tools in .compose-box.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
|
| |
|
|
|
|
|
| |
composer: a pick lands in the draft (preview opened) and the reply goes out with the GIF's NIP-92 imeta. tools/gif-picker-test.js covers it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
|
| |
|
|
|
|
|
| |
you follow, so your own post was there just after you sent it (the optimistic row) and gone after a reload — Adam didn't see his latest. Like every other client, a follows feed now includes you. Someone who follows nobody still gets the "find people" invitation, with their own posts under it. feed-live-test's catch-up queue now goes only to an ask naming the post's author (the feed also asks for yours); tools/feed-repost-test.js checks your own post shows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
|
| |
|
|
|
|
|
| |
Adam's follows; we showed nothing — a feed asked only for posts (kinds 1, 1068). A feed of people now asks for their reposts too (NIP-18 kind 6; topic and firehose asks stay posts-only), and a repost shows as the note itself, under "🔁 <name> reposted" — its reactions, zaps, ⋯ and thread are the note's. Only reposts that carry the note and whose copy checks out (kind 1/1068, the id the e tag names, a valid signature); a muted author's note stays out. A note shows once: as itself, or under the first repost of it to arrive, counting rows still warming at the door. Media grids skip reposts. tools/feed-repost-test.js
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
|