Chore/011 feed mode shell#4
Merged
Merged
Conversation
useSyncUrlState previously rebuilt the URL from scratch using only the keys in the state object it was given, silently stripping any param it did not own. Now it starts from the current querystring and only mutates the managed keys (set or delete), leaving unknown params intact. Found while planning issue 011: useMode writing mode=feed would have been clobbered by the next filter change. Fix is generic, not feed-mode specific. Regression test added covering managed keys, skipped values, preserved unknown params, and removal of previously-present managed keys when their value becomes skipped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the minimum end-to-end vertical slice from issue 011: enter
feed mode from the grid, swipe between items full-screen using native
scroll-snap, exit back to the grid with scroll position restored.
useMode hook (frontend/src/hooks/useMode.js)
- Owns the 'grid' | 'feed' state.
- Reads/writes URL param mode=feed and localStorage key nightfeed:mode.
URL takes precedence over localStorage on init.
- Captures window.scrollY when entering feed; restores via
requestAnimationFrame when returning to grid.
- Cold-load history bootstrap: when starting in feed mode directly (URL
or localStorage) and the current history entry was not pushed by us,
synthesises a grid history entry below the current one so browser
back exits feed instead of leaving the app.
FeedMode + FeedItem (frontend/src/components/)
- FeedMode is a scroll-snap container that maps items to FeedItem.
- FeedItem reuses getModalItems() from utils/media.js to discriminate
item kind, then renders VideoPlayer for kind='video', plain img for
kind='image' or gallery items, null for audio/embed (v1 out of scope).
- Both components are well under the 200-line guardrail.
FeedExitGesture (frontend/src/components/)
- Behaviour-only Fragment wrapper, attaches listeners to window so the
leftmost-25px edge-swipe-right gesture is captured regardless of
which child receives the touch.
- Esc, edge-swipe, and browser-back all call history.back(); the
popstate listener detects mode=feed leaving the URL and calls onExit
(which is setMode('grid')). One convergence point for all three exits.
feed.css (frontend/src/styles/)
- scroll-snap-type: y mandatory, touch-action: pan-y, overscroll-
behavior: contain, 100dvh per item.
- Imported from FeedMode.jsx via Vite; styles.css stays untouched.
App.jsx
- Surgical changes: three new imports, one useMode call, a single
early-return branch for feed mode, and one onEnterFeed prop on
TopBar. No new useState added.
TopBar.jsx
- Optional onEnterFeed prop renders the Enter Feed icon button in the
top-right cluster. Button is absent in feed mode because TopBar
itself does not render there.
vite-plugin-mkcert
- Installed into the frontend workspace and wired into vite.config.js
behind the VITE_HTTPS=true env flag. Default dev stays HTTP.
- README's "Mobile development" section already documented this
(added in 010 governance), so no README changes needed.
memory.md
- Decisions log entry for issue 011: exit-path convergence, cold-load
history bootstrap, getModalItems reuse, VideoPlayer untouched, and a
followup flag for cross-sibling autoplay-with-sound.
Self-check
- npm run lint at root: 0 errors, 39 warnings (all pre-existing legacy
from the 010 warn-baseline; no new warnings introduced).
- npm test: backend 6/6 passing, frontend 13/13 passing.
- npm run build: clean.
- File sizes: useMode 91, FeedMode 14, FeedItem 48, FeedExitGesture 71,
feed.css 40. App.jsx grew by 14 lines, TopBar by 9.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…Mode setMode previously called history.pushState (and replaceState on exit) from inside the setState updater function. React 18 Strict Mode double-invokes state updater functions in development to verify purity, and side effects inside an updater are an anti-pattern: the push can fire twice, fire on a discarded render, or be mistimed against the commit. The practical symptom on the chore/011 PR was that clicking Enter Feed left the URL unchanged even though feed mode rendered correctly. Refactor: - setMode now reads current mode from a modeRef for synchronous dedup. - pushState / replaceState run synchronously inside setMode, BEFORE setModeState is called. - modeRef is updated synchronously alongside the React state update. - A defensive useEffect keeps modeRef aligned with mode in case state ever changes through a path other than setMode. Regression test added: asserts window.location.search contains mode=feed inside the act() callback, capturing the URL before React flushes the state update queue. With the previous code (pushState inside the updater), the URL inside the act block had not yet been mutated; with the fix, it has. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…n feed mode
VideoPlayer previously hardcoded the native HTML5 controls attribute
on its video element. Feed mode (issue 011) needs chromeless playback
with no native controls visible.
Change is exactly two lines in VideoPlayer.jsx:
- controls = true added to the destructured props with a true default
- controls attribute on the video element now reads from the prop
Default remains true so every existing call site (lightbox, prebuffer
helper, etc) behaves identically. FeedItem opts out by passing
controls={false}.
This is the structurally correct fix versus hiding controls with
::-webkit-media-controls CSS, which would couple feed-mode styling to
Chromium's shadow DOM internals.
The off-limits rule on VideoPlayer.jsx was suspended narrowly for
this two-line change only. See memory.md decisions log.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Records the decision that the off-limits rule on VideoPlayer.jsx was suspended for issue 011, scoped to the two-line addition of a controls prop with a backwards-compatible default. Pre-existing warnings in that file are not addressed under the suspension and remain for the planned shrink work. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
❌ Deploy Preview for nightfeed failed.
|
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.
No description provided.