Skip to content

fix(library): Filters panel tvOS focus, authoritative favorites, request caching#56

Merged
keiver merged 8 commits into
mainfrom
fix/filters-panel-focus-trap
Jul 15, 2026
Merged

fix(library): Filters panel tvOS focus, authoritative favorites, request caching#56
keiver merged 8 commits into
mainfrom
fix/filters-panel-focus-trap

Conversation

@keiver

@keiver keiver commented Jul 15, 2026

Copy link
Copy Markdown
Owner

What

  • Filters panel (tvOS focus): present it as a root route (`app/filters.tsx`) that covers the tabs, so opening it can no longer leak focus to the native tab bar (which popped the nested stack). Same pattern as player/photo-viewer. `LibraryFiltersProvider` moved to the app root so the library stack and the root Filters route share state.
  • Favorites (removed items reappearing): paint hearts authoritatively from the reliable `Filters=IsFavorite` set, overriding the browse's per-item state. Seed the set once globally. Removed the add-only overlay and the extra cache patches.
  • Request caching: in-memory TTL cache for library/search/facet/resume/detail reads with per-item invalidation on favorite and playback/resume changes; folder revisits seed from cache to skip the spinner.
  • Help tab: add Library Filters and Favorites pills.

Root causes (verified against the real 10.11.1 server)

  • tvOS: a route inside the `(tabs)` group leaves the native `UITabBar` on screen and focusable during the push; RN focus traps/holders cannot stop the native focus engine. Root-level routes that cover the tabs do not have this.
  • Favorites: the non-recursive browse returns stale `UserData.IsFavorite` (it keeps reporting a just-unfavorited item as favorite); only `Filters=IsFavorite` is current. The old overlay was add-only, so it could never clear a stale heart.

Verification

  • Driven on the Apple TV 4K simulator: Filters opens over the tabs, Down/Up stay in the panel, Menu dismisses back to the folder; removed favorites (stale-true in the browse) show no heart on reload while genuine favorites keep theirs.
  • 562 tests pass; lint and tsc clean.

keiver added 5 commits July 14, 2026 19:25
…on toggle

- shared request cache for library/search/facet/resume/detail reads, invalidated per-item on favorite and playback/resume changes

- favorite toggle repaints the toggled card via the (id, state) pub/sub, fixing the add-only heart cache that left removed favorites still highlighted

- Help: add Library Filters and Favorites feature pills
…inner

useFolderContents now reads the folder cache synchronously at mount and seeds items/isLoading/pagination from it, so a fresh revisit paints content on the first frame instead of flashing a spinner before the async first-page effect resolves. Cold folders and filtered views are unchanged.

Also anchor the Filters panel's preferred focus on Clear All (outside the ScrollView) so tvOS reliably lands focus on route-push.
…tvOS)

Opening Filters lost focus to the native tab bar (a fast Down during load selected a tab and popped the nested stack). The panel lived inside the (tabs)/(library) stack, so the native UITabBar stayed on screen and focusable during the push; no RN-layer guard (trapFocusUp/Down, a focus holder, InteractionManager) can stop the native focus engine from grabbing it.

Move filters.tsx to a root route (app/filters.tsx) that covers the tabs — same pattern as player/photo-viewer — and host LibraryFiltersProvider at the app root so the library stack and the root Filters route share filter state. Drop the failed RN focus hacks.

Verified on the Apple TV sim: panel opens full-screen with no tab bar, Down navigates within it without escaping/popping, Menu dismisses back to the folder.
… set

The Jellyfin non-recursive browse returns STALE UserData.IsFavorite — it keeps reporting a just-unfavorited item as favorite; only the recursive Filters=IsFavorite query is current. The old overlay was add-only, so it could add a heart from the reliable set but never clear the browse's stale true — a removed favorite kept its heart across reloads.

annotateWithFavorites now sets IsFavorite = favoritesSet.has(id) for every item (override both directions, reference-preserving); hearts stay hidden until the set loads so a stale true never leaks. Seed the set once globally (all favorites, fetchFavoriteIds() with no ParentId) instead of per-folder. The favorite subscription just re-annotates. Removed the folderContentsCache favorite-patch and the add-only/optimistic-patch code.

Verified on the tvOS sim: removed favorites (stale-true in the browse) show no heart on reload; genuine favorites keep theirs.
@keiver keiver self-assigned this Jul 15, 2026
@keiver
keiver requested a review from Copilot July 15, 2026 02:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the Library experience in the Expo Router TV app by (1) making the Filters panel a root route to fix tvOS focus leakage to the native tab bar, (2) making Favorites UI state authoritative by deriving hearts from the reliable Filters=IsFavorite set, and (3) adding an in-memory TTL request cache with per-scope invalidation to reduce spinners and redundant reads.

Changes:

  • Introduces an in-memory request cache with TTL + in-flight dedup and wires it into key Jellyfin read paths, plus invalidation on favorites and resume changes.
  • Moves Filters to a root-level route and hoists LibraryFiltersProvider to the app root so tab-covered focus behavior works on tvOS.
  • Makes Favorites rendering authoritative via a globally-seeded favorites id set and live subscription updates in useFolderContents.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
services/requestCache.ts New TTL + in-flight dedup request cache utilities.
services/jellyfinApi.ts Routes multiple GET reads through the cache; adds invalidation hooks on mutations and playback/resume events; updates favorite-change pub/sub payload.
services/tests/requestCache.test.ts Adds unit tests covering TTL behavior, dedup, and invalidation helpers.
services/tests/jellyfinApi.test.ts Adds integration coverage for cached reads and invalidation after favorite toggles.
services/tests/jellyfinApi.filters.test.ts Updates favorite-change listener expectations for new (itemId, favorite) signature.
jest.setup.js Clears the request cache before each test to prevent cross-test cache bleed.
hooks/useFolderContents.ts Adds authoritative favorites annotation, warm-cache synchronous seeding, and favorite-change subscription-driven UI updates.
hooks/tests/useFolderContents.test.tsx Adds tests for authoritative favorite clearing and warm-cache first-render seeding.
constants/app.ts Adds TTL constants for search/resume/facets.
components/settings/QuickConnectSection.tsx Tweaks Quick Connect hint copy.
app/filters.tsx Updates commentary to reflect root-route focus strategy on tvOS.
app/(tabs)/help.tsx Adds “Library Filters” and “Favorites” feature pills.
app/(tabs)/(library)/[folderId].tsx Removes per-screen favorite-change refresh hook (now handled in the data hook).
app/(tabs)/(library)/_layout.tsx Removes LibraryFiltersProvider from the library stack and documents root placement.
app/_layout.tsx Hoists LibraryFiltersProvider and registers the root filters route to cover tabs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread services/requestCache.ts Outdated
Comment thread services/jellyfinApi.ts
Comment thread services/jellyfinApi.ts
Comment thread services/requestCache.ts
keiver added 2 commits July 14, 2026 21:59
…an invalidated read

invalidateRequest/invalidateByPrefix/clearRequestCache now also drop the matching in-flight promises, and cachedRequest only writes its entry if it is still the current in-flight request — so a read invalidated mid-flight (e.g. across an auth clear or a post-mutation refetch) can't re-introduce stale data. Expired entries are evicted on access.

Addresses PR review on requestCache in-flight handling.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 25 changed files in this pull request and generated 1 comment.

Comment thread hooks/useFolderContents.ts
… TotalRecordCount

hasMoreResults was derived only from total !== undefined across the seed init, applyFirstPage, and loadMore, so an absent (optional) TotalRecordCount forced hasMore=false and blocked loadMore even with more pages. Added a hasMorePages() helper: use the total when present, else fall back to a full-page heuristic (lastPage === PAGE_SIZE).

Addresses PR review on pagination when totals aren't provided.
@keiver
keiver merged commit ba9db97 into main Jul 15, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants