fix(library): Filters panel tvOS focus, authoritative favorites, request caching#56
Merged
Conversation
… leak to the tab bar
…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.
Contributor
There was a problem hiding this comment.
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
LibraryFiltersProviderto 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.
…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.
… 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.
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.
What
Root causes (verified against the real 10.11.1 server)
Verification