Extract the thumbnail + text content into a shared `RecentlyViewedRowContent` component so both the iOS SwiftUI list and the Android/default `RecentlyViewedRow` render identical row content without duplication.
Rework the iOS recently-viewed list header to use an `RNHostView`-backed React Native view instead of SwiftUI `Section`/`HStack`/`Button`, giving it the app's display font, a history icon, and a native `Pressable` clear button. Switch each list row to the same `RNHostView` pattern so rows render with the app's background color and consistent insets. Replace SF Symbols type icons with Tabler icons (`IconMovie`, `IconDeviceTv`, `IconUser`) to match the rest of the app.
Remove the hardcoded "Title" and "Person" header titles (and the person's name) from transparent headers so the back button floats cleanly over the hero image. Simplify `StatusActionButton` by dropping the unused `--color-status-watchlist` CSS variable lookup and collapsing the icon color logic; also rename the watchlist label to "Watchlisted". Fix `ExpandableText` to only mark the text `selectable` on iOS, since Android's `EditText`-backed selectable text ignores `numberOfLines` and scrolls instead of truncating.
Replace the hardcoded 2-column filmography grid with a responsive layout that uses 2, 3, or 4 columns based on screen width (600px and 900px breakpoints). Switch from per-item fixed widths to horizontal gutter padding so columns are evenly distributed across all screen sizes.
Swap the periodic health-probe loop for a `serverFetch` wrapper that updates reachability state as a side-effect of every real request: any HTTP response marks the server reachable; a network-level error marks it unreachable.
Wire TanStack Query's `onlineManager` and `focusManager` to `expo-network` and `AppState` so queries and mutations are automatically paused when the device goes offline and resumed on reconnect. Set `networkMode: "online"` on both queries and mutations to opt in to this behaviour.
Move `isNetworkError` from `query-client.ts` into `server-reachability.ts` and extend its pattern list. Export `serverFetch` and use it as the custom fetch implementation in both the oRPC `RPCLink` and the Better Auth client so all network traffic flows through the same reachability tracker.
Move the retry logic out of `useServerReachability` and into the banner components directly — on press, resume paused mutations, refetch active queries, and refresh the auth session.
Add a unit test that verifies a transport failure sets the server unreachable and any subsequent HTTP response (even a 503) clears it.
Introduce a new `horizontal-list-spacing` module to standardize spacing and separator components across various screens. Update `DashboardScreen`, `TitleDetailScreen`, and `HorizontalPosterRow` to utilize the new styles and separators, improving layout consistency. Refactor `CastCard` to use `useRouter` for navigation instead of `Link` for better performance and accessibility.
Implement a new Change Password screen with form validation and user feedback. Adjust navigation to include the change password option in the settings. Update the layout to present the change password screen as a modal on Android and a form sheet on iOS. Clean up previous settings layout code by removing redundant options.
Add `accessibilityLabel` (and `accessibilityHint`) to all form inputs across auth and settings screens. Switch the auth stack from a hidden header to a transparent header with back-button support so deep-linked screens navigate correctly. Present the change-password screen as a form sheet on iOS and a modal on Android, and wrap its body in a `ScrollView` to handle the keyboard and small screens. Move inline `contentContainerStyle` objects in the explore and home screens to module-level constants to avoid unnecessary re-renders. Enable `EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH` and `EXPO_UNSTABLE_TREE_SHAKING` in EAS production builds. Also apply miscellaneous layout and styling fixes across `poster-card`, `switch`, `text-field`, `hero-banner`, and several other UI components.
Convert trending, search, people.detail, and dashboard.library to page-based responses (`page` / `totalPages`) so clients can load results incrementally.
Update `@sofa/core` discovery and person services to accept a `page` / `limit` input and slice results accordingly. Add a new DB migration to persist the data needed to back paginated filmography queries.
On the web, introduce a `useInfiniteScroll` hook and wire up `useInfiniteQuery` in the explore, person detail, and filterable title row components with an intersection-observer sentinel. On native, switch the same screens from `useQuery` to `useInfiniteQuery` with `onEndReached` / `ListFooterComponent` loading indicators. Also replace remaining `FlatList` usages with `FlashList` in the home and title detail screens.
The people.detail endpoint now fetches a person's complete filmography
from TMDB's combined_credits API (cast + crew) instead of only returning
locally-enriched titles.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the static `https://sofa.example.com/api/v1` server entry with a variable-based URL (`{protocol}://{host}:{port}/api/v1`) so the spec works against any instance out of the box. Also move the output path from `docs/openapi.json` to `docs/public/openapi.json` so the file is served as a static asset by the docs site.
Switch `mock.module("../src/image-cache", ...)` stubs in thumbhash, person-detail, colors, and credits tests to `spyOn(globalThis, "fetch")` instead. This avoids the cross-file persistence problem with `mock.module` and tests the real code paths more faithfully.
Update `image-cache.ts` to call `globalThis.fetch` so the spy intercepts it, add `mock.restore()` to the global `afterEach` in `preload.ts`, and set `IMAGE_CACHE_ENABLED=false` in affected tests to avoid disk writes during test runs.
Replace the standalone `ChangePasswordSection` card with a `ChangePasswordDialog` triggered from a button next to "Sign out" in the account section. This removes the separate card, merges the dialog logic into `account-section.tsx`, and cleans up the settings page layout accordingly.
Replace the single MDX file per API tag (e.g. `admin.mdx`, `titles.mdx`) with individual files for each procedure (e.g. `admin/admin.backups.create.mdx`). Each file renders a single `<APIPage>` with one operation, includes an inline description, and carries its own `_openapi` frontmatter.
Update `generate-api-docs.ts` to emit this per-procedure structure and adjust `source.ts` and `next.config.mjs` to resolve the new paths correctly.
Add `purgeMetadataCache` and `purgeImageCache` procedures that let admins free disk space on demand. Metadata purge removes un-enriched stub titles not in any user's library plus orphaned person records; image purge deletes all cached TMDB files from disk.
Expose both as POST endpoints under `/admin/cache/` via the oRPC contract, implement the core logic in a new `@sofa/core/cache` module, and surface them in a new "Danger Zone" section on the Settings page with individual confirmation dialogs and a combined "Purge all" action.
Extract spec generation into `openapi-spec.ts` with a `generateOpenApiSpec` function that registers common `$ref` schemas (Title, Person, Episode, Season, etc.) to deduplicate inline definitions across the spec. Add a `/api/v1/spec.json` interceptor that returns the fully normalized document, and point the Scalar docs UI at it instead of the plugin's raw generator output.
Also strip oRPC's impossible `{ not: {} }` void placeholders from request/response bodies via `normalizeOpenApiSpec`, and fix the handler registration so both `/api/v1` and `/api/v1/*` routes are matched.
Seed the Better Auth session atom from SecureStore cache before React
renders so the app shows cached data instantly instead of hanging on the
splash screen for 30-60s when the server is down. Add a server
reachability monitor that probes /api/health and displays an amber
"Can't reach server" banner (distinct from the offline banner). When the
server recovers, the session is re-validated automatically — if expired,
a toast explains the sign-out.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Generate an OpenAPI 3.1 spec from the oRPC contract at build time
using @orpc/openapi, then use fumadocs-openapi to render interactive
API reference pages grouped by tag (Titles, Episodes, Seasons, etc.).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Swap the generic `fumadocs-ui/og/takumi` default for a hand-crafted OG image using the Sofa cinema theme — dark background, amber accent, DM Serif Display title, DM Sans body, and a radial gradient — matching the app's visual identity.
- Load DM Sans (400/500) and DM Serif Display (400) at runtime from Google Fonts via old Safari UA to get TrueType URLs
- Render header with Sofa logo SVG + "Sofa Docs" label, centered title/description block, and an amber accent bar
- Scale title font size based on length (68px → 56px for longer strings)
- Define theme constants (BG, FG, PRIMARY, MUTED) from the app's oklch palette
- Add `docs/` — Next.js 15 app powered by Fumadocs with a landing page, docs layout, search, OG image generation, and LLM text routes
- Cover getting started, configuration, integrations (Plex, Emby, Jellyfin, Radarr, Sonarr), mobile app setup, and telemetry in MDX
- Exclude `docs/` from root Biome config since it has its own `biome.json`
Isolate the query cache and recently-viewed history by instance ID +
user ID so that switching servers or accounts never bleeds data
between them.
- Split `mmkv.ts` into `globalStorage` (app-wide settings, analytics)
and a scoped store keyed by `${instanceId}_${userId}`; add
`setStorageScope`, `clearStorageScope`, `onStorageScopeChange`
- Add `QueryProvider` component — mounts `PersistQueryClientProvider`
keyed by `${instanceId}_${userId}` when scoped storage is ready,
falls back to plain `QueryClientProvider` otherwise; remounts on
scope change to restore from the correct MMKV partition
- `registerServer` in `server-url.ts` persists the instance ID
returned from the health check; `ensureInstanceId` fetches it from
the server for upgrade paths and env-based URLs
- Auth client `storagePrefix` now includes the instance ID so
SecureStore tokens are namespaced per server
- Export `rebuildAuthClient` for use when instance ID resolves after
initial mount
- Remove explicit `clearRecentlyViewed` calls from sign-out — scoped
storage naturally separates history per account
- Update `recently-viewed.ts` and `posthog.ts` to use the appropriate
storage tier (`scopedStorage` / `globalStorage`)
Add an opt-in telemetry system that sends anonymised instance
statistics to the public API once per day, which proxies them to
PostHog when `POSTHOG_API_KEY` is configured.
- Add `packages/core/src/telemetry.ts` — `performTelemetryReport`
and `isTelemetryEnabled`; user and title counts are bucketed
before sending to avoid exposing exact figures
- Add `getInstanceId()` to `settings.ts` — generates and persists a
stable UUIDv7 for the instance
- Schedule a daily `telemetryReport` cron job (00:30)
- Add `POST /v1/telemetry` to `apps/public-api`; forwards payload
to PostHog or returns 204 silently if key is absent
- Add `admin.telemetry` and `admin.toggleTelemetry` oRPC procedures
for inspecting and toggling the setting
- Expose `instanceId` on `system.publicInfo`
Add a change password feature using Better Auth's built-in
changePassword endpoint. On web, a dialog opens from a new card in the
Account section. On mobile, a dedicated screen is pushed from a new
SettingsRow. Both use listAccounts to detect credential-based accounts
and only show the option when the user has a password and password login
is enabled (respecting OIDC-only configurations).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add `apps/public-api` (`@sofa/public-api`) — minimal Hono app
deployable to Vercel; `GET /v1/version` fetches the latest GitHub
release and returns `{ version, releaseUrl }` with a 15-minute
CDN cache (`s-maxage=900, stale-while-revalidate=3600`)
- Update `packages/core/src/update-check.ts` to call
`PUBLIC_API_URL/v1/version` instead of the GitHub API directly;
`PUBLIC_API_URL` defaults to `https://public-api.sofa.watch`
Swap the hand-rolled `ToastView`/`ToastProvider`/queue system for the
`burnt` library, which delegates to native iOS/Android toast APIs.
- Add `burnt@0.13.0` dependency
- Delete `toast.tsx` and `toast-provider.tsx`; remove `<ToastProvider />`
from `_layout.tsx`
- Rewrite `lib/toast.ts` to wrap `burnt` — maps success/error/info/warning
to burnt presets and haptic types; duration converted from ms to seconds
Track titles and people visited in the native app and surface them
in the search tab when the query is empty, replacing the static
empty-state icon.
- Add `lib/recently-viewed.ts` — MMKV-backed store (max 20 items)
with `addRecentlyViewed`, `removeItem`, `clearAll`, and a
`useRecentlyViewed` hook
- Track visits via `useEffect` in `title/[id].tsx` and
`person/[id].tsx` once data resolves
- `RecentlyViewedList` — platform-split component:
- iOS (`.ios.tsx`): native SwiftUI List via `@expo/ui` with
system SF Symbols, swipe-to-delete, and a "Clear" button
- Android/fallback: FlatList with `SwipeableRow` for delete
- Add `SwipeableRow` component using `ReanimatedSwipeable`
- Clear recently viewed history on sign-out (both `HeaderAvatar`
and Settings screen)
- Add `@expo/ui@55.0.2` dependency
- Swap `runOnJS` for `scheduleOnRN` from `react-native-worklets` in
gesture handler `.onEnd()` callbacks (HeroBanner, PosterCard)
- Delete custom `timeAgo` util; use `date-fns` `formatDistanceToNow`
in integration status strings
- Enhance person detail screen: formatted birth date with age/death
calculation, place of birth with MapPin icon, department label
mapping, CalendarIcon for dates
- Fix `ExpandableText` truncation detection by measuring line count
on a hidden full-text node rather than the clamped one; add
`actionColor` prop so title accent color can theme the toggle
- Polish PosterCard metadata row: remove icon opacity, tighten gaps
Replace three separate status toggle buttons (Watchlist/Watching/Completed)
with a single status button matching the web pattern: "+ Watchlist" when
inactive, "Watching"/"Completed" when active (tap to remove). Consolidate
actions into a single row with separator and star rating. Use title-accent
color for theming consistency with other native components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Introduced HeroBanner component for displaying movie or TV show details with animated press effects and navigation.
- Added OfflineBanner component to notify users of network connectivity issues, utilizing glass effect when available.
- Implemented haptic feedback utilities for Android to enhance user interactions.
Replace the conditional GlassView backdrop overlay with plain tinted
Views, and apply the liquid glass effect to the play button circle
instead when supported.
Replace hardcoded `--color-primary` references on the title detail screen
with a dedicated `--color-title-accent` / `--color-title-accent-foreground`
variable pair, allowing the poster-derived palette to style title UI
elements without polluting the global primary color.
- Add `--color-title-accent` and `--color-title-accent-foreground` to
`global.css` with default warm amber values
- Update `use-title-theme.ts` to set `--color-title-accent` instead of
`--color-primary`; switch from `useFocusEffect` to `useEffect`
- Replace all `bg-primary`, `text-primary`, `border-primary` etc. in
`title/[id].tsx` and title components with `title-accent` equivalents
- Add `accentColor` prop to `StarRating` so it can receive the resolved
CSS variable value at runtime
- Pass `iconColor={titleAccent}` to all `SectionHeader` instances on the
title detail screen
- Use platform-appropriate store icon (App Store / Google Play) for the
"Where to Watch" section header
- Switch progress bar in `ContinueWatchingBanner` from `bg-status-watching`
to `bg-title-accent`