feat: pull the wall down to lay it again, and clean up the feeds it offers - #75
Merged
Conversation
AGENTS.md described a repo one feature behind: two walls rather than two wall sources, no reader, no feed picker, no pull, and a command list missing wasm and deploy. It also carried claims the tree had moved past: guard-dashes is a whole-tree denylist now, guard-autoplay greps for .play( outside VideoPlayer, and check:ci runs two tsc projects because svelte-kit sync excludes the service worker from the app one. Two gotchas added, both of which cost somebody an afternoon to learn: logic belongs in a .svelte.ts module because nothing here typechecks or tests a component body, and an intentional wire change means regenerating the contract fixture before types.ts will typecheck again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
|
🧹 Preview wall removed. |
The refresh control shipped as a button on a bar that never wraps at 375px, and the gesture a reader actually makes at the top of a wall is to drag it. This is that gesture, wired to the same feed.refresh(): one trigger more, not a second kind of refresh, with the same one-shot flag and the same rate limit behind it. The wall itself moves, because the gesture says the reader has hold of the wall rather than of an indicator over it. main#wall is what translates and not the layout wrapper, which on a phone holds the fixed control bar. It follows the finger 1:1 to a 72px threshold, which is where letting go lays the wall again, and past that the band stiffens toward 120px without reaching it. A gesture is read once, in its first 8px: downward and more down than across is a pull, anything else is handed back to the browser and stays handed back for the rest of the drag. A pull that eases back up unwinds to zero and costs nothing. A release settles the wall onto a shelf 54px open and holds it there until the warm ends, so the indicator has a gap to sit in and the gap closing is what says the refresh finished. That corrects the first cut, which sent the wall straight home: a recording on a real iPhone showed the pill sitting on top of the first card for the whole warm, three seconds and more on a live wall. The shelf is the pill plus 8px of air above and below it, and the air is spent deliberately rather than left over, because the first shelf cleared the bricks by two pixels, which is a gap in the arithmetic and none on the screen. overscroll-behavior-y: contain turns off the browser's own version, and that is half the reason to build this at all. Chrome on Android answers the same gesture with a full page reload, which drops the laid wall, its scroll, the seed behind its arrangement and anything playing, then warms a new wall from a cold service worker. That is the restart the refresh control exists to avoid, offered by the platform on the exact drag this claims. Every decision lives in state/pull.svelte.ts, which names no DOM global and is pinned by 20 vitest cases in node, including the one number the wall and the indicator both read so they cannot disagree about where the top of the wall is. The component is left with the listeners, the indicator, and the one question the module cannot answer: at the top, not busy, nothing open over it, one finger. tests/pull-refresh.test.ts is the other half, on a touch device against the real build, because no other lane in this repo renders a component. Three things that cost time and are worth naming. A touch listener on window is passive by default in every browser that matters, so preventDefault needs addEventListener rather than a svelte event attribute, and it is called only on a claimed move that is still cancelable, since iOS stops cancelling once a scroll is under way. An effect that reads the state it writes makes svelte abort the flush, which leaves the DOM holding the last thing written to it: the symptom was the wall staying pulled down after the finger had gone, with the wall's own state already back at zero. And the demo wall's refresh settles in milliseconds, so the shelf is gone before the next animation frame there: the spec reads it by draining microtasks and nothing else, which is the only mechanism that can tell a shelf that never happened from one already let down. A pull also has to start ON THE WALL, and that rule arrived late: a window listener hears every touch on the page, so a drag on the open switcher panel pulled the wall behind its own scrim and let go into a refresh. The blocked prop cannot catch that one, because the switcher's openness is local component state nothing outside the component can read; where the touch landed can. It is asked of the element the touch STARTED on, since a gesture belongs to whatever it began on and a finger that wanders off the wall mid-drag is still pulling it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The door to the feed picker is now the first control in the panel, its only filled one, and where the panel puts focus. The recents sit directly under it, because they answer the same question one tap sooner, and the handle box follows below a rule with a quieter label and an outline submit. Somebody opening this panel already has a wall and wants a different one, and they have one follow graph and thousands of feeds. Focusing the handle field also opened a keyboard on every phone that opened the panel, over the list they were most likely reaching for. The landing page keeps its weighting the other way round, and that asymmetry is the point rather than an inconsistency: a reader who has just arrived has nothing, and their own handle is the one thing they certainly have. There the picker stays "or pick a feed to lay", under the box. The panel now scrolls past 70vh rather than clipping. It grows UPWARD from a fixed bar on a phone, so the door being first means the door is what a panel too tall for its screen would lose; five recents still fit at 375px without it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sumed Every one of the top fifty popular feeds was asked for a logged-out getFeed. Eleven do not clear the bar of more than five posts, and they fail two ways that the one bar covers: most do not answer at all (502, or 401 for skyfeed's), and a few answer 200 with almost nothing. Teams gives one post, spacecowboy17's For You gives one, Trans+Queer Shitposters gives three, every time they are asked. A card that opens onto three bricks reads as a broken feed whether the feed is gated or merely quiet, and the reader cannot tell which either. Two of the eleven were feeds the picker still listed: jcsalterego's Media, which 502s on every attempt three runs apart, and jaz.sh's Trans+Queer Shitposters. The audit also re-tested the existing denylist, which is the half that keeps a list like this from only ever growing, and it found the more interesting result: mason was hiding two feeds that work. skygaze.io's For You answers 19 posts and moonandbaboon's Only Posts answers 20, four times out of four, and both were being hidden by name-only rules aimed at other publishers' copies. Worse, the spaced "only posts" rule never matched skyfeed's "OnlyPosts" at all, so the one copy that IS auth-gated was the one getting through. So the list now splits on a rule it can defend: a name is denied outright only when the name names the VIEWER (mutuals, mentions, popular with friends, quiet posters), which cannot mean anything logged out whoever publishes it. Everything else describes a KIND of feed a logged-out algorithm can produce, so it is pinned to the publisher that was measured. Sixteen entries, twelve of them pinned. web/scripts/audit-feeds.mjs is the tool, wired as pnpm feeds:audit. It pages the popular list to fifty (one call answers about forty, so the cursor is what makes "the top fifty" true), probes each feed twice because a 502 can be a bad minute, and re-tests every denied name by searching for it. No lane runs it: it talks to the live network, so it is a tool and not a test. The static list is what keeps the picker at one request rather than fifty-one, and the cost is that the quiet entries go stale in a way the gated ones do not, which the comment says out loud. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits on one polish branch: the pull gesture, and an AGENTS.md that describes the repo as it is.
Pull to lay again
The refresh control shipped as a button on a bar that never wraps at 375px, and the gesture a reader actually makes at the top of a wall is to drag it. This is that gesture, wired to the same
feed.refresh(): one trigger more, not a second kind of refresh, with the same one-shot flag and the same rate limit behind it.main#wallis what translates and not the layout wrapper, which on a phone holds the fixed control bar.overscroll-behavior-y: containturns off the browser's own version, and that is half the reason to build this at all. Chrome on Android answers the same gesture with a full page reload, which drops the laid wall, its scroll, the seed behind its arrangement and anything playing, then warms a new wall from a cold service worker. That is the restart the refresh control exists to avoid, offered by the platform on the exact drag this claims.motion-safe:and so a reader who asked for less motion gets the wall back at once.aria-hidden: the wall's one polite region already says "laying bricks" while warming, and the button remains the named, platform-disabled way to ask.Every decision lives in
state/pull.svelte.ts, which names no DOM global and is pinned by 16 vitest cases in node. The component keeps the listeners, the indicator, and the one question the module cannot answer (at the top, not busy, nothing open over it, one finger).tests/pull-refresh.test.tsis the other half, on a touch device against the real build, because no other lane in this repo renders a component.Two things that cost time and are named in the code:
windowis passive by default, sopreventDefaultneedsaddEventListenerrather than a svelte event attribute, and it is called only on a claimed move that is still cancelable (iOS stops cancelling once a scroll is under way).Specs: a Pulling section in
08-wall-and-bricks.md, apull.svelte.tsrow and a second trigger in07-web-client.md, and the change spec at.specs/changes/merged/2026-07-28-pull_the_wall_down.md. This closes the Pull to refresh open question 08 has carried since the refresh landed, and leaves two smaller ones (a trackpad pull, and pulling up at the end of a wall).AGENTS.md
Described a repo one feature behind: two walls rather than two wall sources, no reader, no feed picker, no pull, and a command list missing
wasmanddeploy. Also corrected the guards (whole-tree dash denylist,.play(outside VideoPlayer), the two tsc projects, and added two gotchas that cost real time: logic belongs in a.svelte.tsmodule, and an intentional wire change means regenerating the contract fixture beforetypes.tstypechecks again.Verification
just checkgreen (159 cargo nextest, two tsc projects, 231 vitest, four guards, lint, fmt).just test-e2egreen: 78 browser tests, four of them new.🤖 Generated with Claude Code