Add a quiet "Powered by Sofa" footer to the logged-out splash screen,
linking to sofa.watch. The footer sits in normal flow below the hero so
it can't overlap the buttons on short screens, and clears the bottom
safe-area inset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuild the home page hero, feature sections, and footer with a new layout, integration logos component, and an app screenshot. Drop the scroll indicator in favour of the new structure.
Default analytics to off, and name screen events from route patterns instead of pathnames and params so they carry no content IDs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Render the filter strip and a pull-to-refresh ScrollView for the pending,
error and empty states instead of replacing the whole screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exclude library titles and order by lastFetchedAt in SQL, and stamp
lastFetchedAt when a non-library refresh fails so it moves to the back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pin all @orpc/* catalog entries to 2.0.0-beta.42; @sofa/api now depends on @orpc/openapi
- Contract routes move from .route() to .meta(openapi(...)); schemas gain meta ids so the
OpenAPI document hoists them as named components; publicInfo overrides document-level security
- OpenAPI generator/handler use v2 APIs (converters, base, spec plugin option, OpenAPI 3.1.1 output)
- Add restoreRootFileType routing interceptor so root-level File inputs (avatar upload, backup
restore) keep their MIME type under Bun.serve
- Web/native links use origin + path url; native error classification derives HTTP status from
COMMON_ERROR_STATUS_MAP / MalformedResponseError since ORPCError.status is gone
- router-auth test reads v2 orderedMiddlewares; regenerated docs/public/openapi.json
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
updateStatus now returns { alreadyAdded }. Native toasts say so instead of
claiming success, and the Explore hero hides Add to Watchlist for library titles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Window starts now equal the first chart bucket, and year buckets are built from the 1st of each month so the 29th-31st no longer repeat or skip months.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds tracking.history (cursor-paginated, movies and episodes merged), a
last_watched library sort, and a /history web page.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Declare the app's 12 languages to iOS and Android via expo-localization,
resolve the language from the device's preferred languages at start-up and
on Android runtime changes, and make Settings > Language open the system
settings page. Drops the MMKV-stored locale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raise ios.deploymentTarget from 16.4 to 17.0 (app and widget extension) and state the requirement in the mobile app docs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Bypass the 5-minute session cookie cache when refetching after name/avatar edits, keep taps on Save/Cancel working with the keyboard open, add Return-to-save, trim the name, and guard against double submit.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replace the session-rejected / server-change / cached-session-seeded flags with a single
session-end reason that deliberate sign-outs mark before signing out. The session-loss
effect now toasts only when no reason was recorded.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With the React Compiler, hasServerUrl read from MMKV during render was
memoized forever at the root. Read it through useSyncExternalStore instead.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Extract the job loop into runImportJob (with tests), hold its UI state in
useImportJob, and move the importing/done steps and stat/option components
into import-dialog-parts. Both dialogs now close when an import continues in
the background or the connection drops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Search queries that look like an IMDb ID (e.g. tt0133093) are now resolved
through TMDB's find endpoint, respecting the movie/tv type filter.
Supersedes #60.
Co-authored-by: Gonçalo Fonseca <gcare.fonseca99@outlook.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Android tab bar only declared four tabs with hard-coded English
labels, so the Library route was never registered and "See all" on Home
did nothing. Match the iOS tab set and order and use LingUI labels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- invalidateTitleQueries no longer invalidates titles.get and returns a promise for tracking/library refetches; mutations return it from onSuccess so isPending covers the refetch
- per-episode pending guard (disabled + dimmed row) and disabled season "Mark All Watched" while pending
- add usePullToRefresh so RefreshControl only reflects user-initiated pulls (Home, Explore, Library, Upcoming)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Trakt/Simkl sign-in dialog polled with setInterval and no in-flight
guard, so while the first "authorized" poll was still fetching the
library, the next tick re-sent the redeemed device code. Trakt answers
409, which was mapped to "expired", producing a false "Device code
expired" error.
- public-api: map Trakt 409 (already redeemed) to pending
- web: add a single-flight device-code poller that drops late responses
after stop() and aborts the in-flight request
- web: use it in the imports dialog and ignore device-code responses
that arrive after the dialog was closed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Probe the health endpoint while unreachable, refetch errored queries when the server
returns, and keep gcTime at least as long as the persister maxAge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session-loss effect never ran because sessionLost was computed in a
render that also called setState, so the committed value was always false.
Compare against a ref inside the effect instead, re-validate the session on
401s, stop retrying 4xx errors, and show the change-server link on
OIDC-only login screens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the client IP from the TCP peer plus trusted-proxy X-Forwarded-For hops
(TRUSTED_PROXIES) and hand it to Better Auth in an internal header, so spoofed
forwarding headers can no longer rotate sign-in rate-limit buckets. The Expo
authorization proxy now only redirects to the OIDC provider's authorization endpoint.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Helpers that took Lingui's `t` as a parameter rendered empty strings because
the macro only transforms `t` imported from the macro. Use msg descriptors
plus i18n._ instead, so mapped error toasts and native server-URL errors are
real, extractable strings. Also map Better Auth error codes to localized
messages instead of showing raw server text, and send a REGISTRATION_CLOSED
code from the auth hook.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
readImportJob now selects only progress columns instead of the multi-MB
payload, processImportJob empties the payload once a job reaches a terminal
status, and server startup clears payloads of previously finished jobs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
expo-widgets rebuilds each widget timeline from the stored entries when it
ends, so five entries 15 minutes apart left the widgets stuck on the last
entry (the 5th show, or the furthest-away release) after an hour.
Continue Watching now rotates through up to five shows every 30 minutes for
12 hours, then rests on the top show. Upcoming writes one entry per local
midnight at which the shown item or its Today/Tomorrow label changes, moves
on after each air date and ends on the empty state once everything has aired.
All widget text is now produced in widgets.ts through LingUI and passed to the
widget extension as string props, replacing the hard-coded English labels and
month table. The widget catalogs gain "S{seasonNum} E{epNum}", "Nothing to
watch" and "Nothing upcoming"; the rest reuse existing messages. The refresh
now waits for the locale catalog so English labels are not baked in first.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
UIGraphicsImageRenderer(size:) uses the screen scale by default, so the cached JPEGs came out about 1395px wide instead of the intended 465px. WidgetKit refuses to render images whose pixel area is far beyond the widget's, and the extension has a tight memory limit, so oversized art could blank the widget.
Render at scale 1 into a center-cropped 510px square (systemSmall is at most 170pt, or 510px at 3x) and bump JPEG quality to 0.8 now that the files are much smaller.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
iOS 17+ rendered 'Please adopt containerBackground API' because expo-widgets 56 dropped its hard-coded container background and neither widget applied the modifier. Artwork also drew at its raw pixel size because the images were never resizable, and the layout assumed a fixed 155pt box with default content margins.
Apply containerBackground, make the artwork resizable, aspect-filled and clipped, replace the flat overlay with a gradient scrim, left-align the text block, use a native linear ProgressView, and disable content margins via the plugin's ios config. Also correct the Continue Watching description, since the feed only contains TV episodes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Follow Trakt's pagination headers, slim items to the fields Sofa reads,
keep the most recent plays when the payload would exceed Vercel's 4.5 MB
response limit, surface failed/truncated lists as warnings, and return a
readable fetch_error instead of an oversized response for every provider.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>