mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
fix: address review feedback on API reorganization
- Fix updateRating invalidation in native app — was only invalidating title queries, now calls invalidateTitleQueries() to also refresh tracking.userInfo (drives the rating UI) - Make unwatchMovie status revert consistent with unwatchSeries — revert any non-watchlist status, not just "completed" - Add sync invariant comment on handleWatch/handleUnwatch loops - Remove unused queryClient import in native use-title-actions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,14 +51,14 @@ export default function DashboardScreen() {
|
||||
"--color-status-completed",
|
||||
]) as [string, string, string, string];
|
||||
|
||||
const stats = useQuery(orpc.tracking.stats.queryOptions());
|
||||
const libraryStats = useQuery(orpc.library.stats.queryOptions());
|
||||
const movieHistory = useQuery(
|
||||
orpc.tracking.history.queryOptions({
|
||||
orpc.tracking.stats.queryOptions({
|
||||
input: { type: "movie", period: moviePeriod },
|
||||
}),
|
||||
);
|
||||
const episodeHistory = useQuery(
|
||||
orpc.tracking.history.queryOptions({
|
||||
orpc.tracking.stats.queryOptions({
|
||||
input: { type: "episode", period: episodePeriod },
|
||||
}),
|
||||
);
|
||||
@@ -67,7 +67,7 @@ export default function DashboardScreen() {
|
||||
const recommendations = useQuery(orpc.discover.recommendations.queryOptions());
|
||||
|
||||
const isRefreshing =
|
||||
stats.isRefetching ||
|
||||
libraryStats.isRefetching ||
|
||||
continueWatching.isRefetching ||
|
||||
library.isRefetching ||
|
||||
movieHistory.isRefetching ||
|
||||
@@ -82,8 +82,8 @@ export default function DashboardScreen() {
|
||||
const hasContinueWatching = (continueWatching.data?.items?.length ?? 0) > 0;
|
||||
const hasRecommendations = (recommendations.data?.items?.length ?? 0) > 0;
|
||||
|
||||
const movieCount = movieHistory.data?.count ?? stats.data?.moviesThisMonth;
|
||||
const episodeCount = episodeHistory.data?.count ?? stats.data?.episodesThisWeek;
|
||||
const movieCount = movieHistory.data?.count;
|
||||
const episodeCount = episodeHistory.data?.count;
|
||||
|
||||
const periodLabels: Record<TimePeriod, string> = {
|
||||
today: t`today`,
|
||||
@@ -144,7 +144,7 @@ export default function DashboardScreen() {
|
||||
<View className="flex-row gap-3">
|
||||
<StatsCard
|
||||
label={t`In Library`}
|
||||
value={stats.data?.librarySize}
|
||||
value={libraryStats.data?.size}
|
||||
icon={IconBooks}
|
||||
color="text-status-watchlist"
|
||||
tintColor={watchlistColor}
|
||||
@@ -152,7 +152,7 @@ export default function DashboardScreen() {
|
||||
/>
|
||||
<StatsCard
|
||||
label={t`Completed`}
|
||||
value={stats.data?.completed}
|
||||
value={libraryStats.data?.completed}
|
||||
icon={IconCheck}
|
||||
color="text-status-completed"
|
||||
tintColor={completedColor}
|
||||
|
||||
@@ -246,9 +246,12 @@ export default function LibraryScreen() {
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const { quickAdd } = useTitleActions();
|
||||
const handleQuickAdd = useCallback((id: string) => quickAdd.mutate({ id }), [quickAdd]);
|
||||
const addingId = quickAdd.isPending ? (quickAdd.variables?.id ?? null) : null;
|
||||
const { updateStatus } = useTitleActions();
|
||||
const handleQuickAdd = useCallback(
|
||||
(id: string) => updateStatus.mutate({ id, status: "watchlist" }),
|
||||
[updateStatus],
|
||||
);
|
||||
const addingId = updateStatus.isPending ? (updateStatus.variables?.id ?? null) : null;
|
||||
|
||||
const allItems = useMemo(
|
||||
() => libraryQuery.data?.pages.flatMap((page) => page.items) ?? [],
|
||||
|
||||
@@ -32,13 +32,13 @@ export default function SearchScreen() {
|
||||
}),
|
||||
});
|
||||
|
||||
const { quickAdd: quickAddMutation } = useTitleActions();
|
||||
const { updateStatus } = useTitleActions();
|
||||
|
||||
const handleQuickAdd = useCallback(
|
||||
(id: string) => {
|
||||
quickAddMutation.mutate({ id });
|
||||
updateStatus.mutate({ id, status: "watchlist" });
|
||||
},
|
||||
[quickAddMutation],
|
||||
[updateStatus],
|
||||
);
|
||||
|
||||
// Memoize mapped results to maintain stable references
|
||||
@@ -57,7 +57,7 @@ export default function SearchScreen() {
|
||||
[searchResults.data?.pages],
|
||||
);
|
||||
|
||||
const addingId = quickAddMutation.isPending ? (quickAddMutation.variables?.id ?? null) : null;
|
||||
const addingId = updateStatus.isPending ? (updateStatus.variables?.id ?? null) : null;
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: SearchResultItem }) => (
|
||||
|
||||
@@ -62,12 +62,12 @@ export default function PersonDetailScreen() {
|
||||
const mutedForeground = useCSSVariable("--color-muted-foreground") as string;
|
||||
const primaryColor = useCSSVariable("--color-primary") as string;
|
||||
|
||||
const { quickAdd } = useTitleActions();
|
||||
const { updateStatus } = useTitleActions();
|
||||
const handleQuickAdd = useCallback(
|
||||
(titleId: string) => quickAdd.mutate({ id: titleId }),
|
||||
[quickAdd],
|
||||
(titleId: string) => updateStatus.mutate({ id: titleId, status: "watchlist" }),
|
||||
[updateStatus],
|
||||
);
|
||||
const addingKey = quickAdd.isPending ? (quickAdd.variables?.id ?? null) : null;
|
||||
const addingKey = updateStatus.isPending ? (updateStatus.variables?.id ?? null) : null;
|
||||
|
||||
const { data, isPending, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useInfiniteQuery(
|
||||
|
||||
@@ -108,12 +108,7 @@ export default function TitleDetailScreen() {
|
||||
const userInfo = useQuery(orpc.tracking.userInfo.queryOptions({ input: { id } }));
|
||||
const recommendations = useQuery(orpc.titles.similar.queryOptions({ input: { id } }));
|
||||
|
||||
const {
|
||||
updateStatus,
|
||||
updateRating,
|
||||
watchMovie,
|
||||
quickAdd: quickAddMutation,
|
||||
} = useTitleActions({
|
||||
const { updateStatus, updateRating, watchMovie } = useTitleActions({
|
||||
toasts: {
|
||||
watchMovie: () => {
|
||||
const name = title?.title;
|
||||
@@ -413,14 +408,12 @@ export default function TitleDetailScreen() {
|
||||
currentStatus={userInfo.data?.status ?? null}
|
||||
onStatusChange={(status) => {
|
||||
if (status === "in_watchlist") {
|
||||
quickAddMutation.mutate({ id });
|
||||
updateStatus.mutate({ id, status: "watchlist" });
|
||||
} else {
|
||||
updateStatus.mutate({ id, status: null });
|
||||
}
|
||||
}}
|
||||
isPending={
|
||||
updateStatus.isPending || quickAddMutation.isPending || watchMovie.isPending
|
||||
}
|
||||
isPending={updateStatus.isPending || watchMovie.isPending}
|
||||
/>
|
||||
|
||||
{title.type === "movie" && (
|
||||
|
||||
@@ -29,9 +29,12 @@ export function HorizontalPosterRow({
|
||||
items: PosterRowItem[];
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const { quickAdd } = useTitleActions();
|
||||
const handleQuickAdd = useCallback((id: string) => quickAdd.mutate({ id }), [quickAdd]);
|
||||
const addingKey = quickAdd.isPending ? (quickAdd.variables?.id ?? null) : null;
|
||||
const { updateStatus } = useTitleActions();
|
||||
const handleQuickAdd = useCallback(
|
||||
(id: string) => updateStatus.mutate({ id, status: "watchlist" }),
|
||||
[updateStatus],
|
||||
);
|
||||
const addingKey = updateStatus.isPending ? (updateStatus.variables?.id ?? null) : null;
|
||||
const keyExtractor = useCallback((item: PosterRowItem) => item.id, []);
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: PosterRowItem }) => (
|
||||
|
||||
@@ -131,7 +131,7 @@ export function HeroBanner({ item }: { item: HeroBannerItem }) {
|
||||
<Link.MenuAction
|
||||
title={t`Add to Watchlist`}
|
||||
icon="bookmark"
|
||||
onPress={() => titleActions.quickAdd(item.id, item.title)}
|
||||
onPress={() => titleActions.addToWatchlist(item.id, item.title)}
|
||||
/>
|
||||
</Link.Menu>
|
||||
</Link>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useLingui } from "@lingui/react/macro";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { orpc } from "@/lib/orpc";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { invalidateTitleQueries } from "@/lib/title-actions";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
@@ -25,7 +24,6 @@ interface WatchInput {
|
||||
|
||||
interface UseTitleActionsOptions {
|
||||
toasts?: {
|
||||
quickAdd?: ToastOverride<{ id: string }>;
|
||||
updateStatus?: ToastOverride<{ id: string; status: string | null }>;
|
||||
watchMovie?: ToastOverride<WatchInput>;
|
||||
updateRating?: ToastOverride<{ id: string; stars: number }>;
|
||||
@@ -43,20 +41,6 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
const { t } = useLingui();
|
||||
const toastOverrides = options?.toasts;
|
||||
|
||||
const quickAdd = useMutation(
|
||||
orpc.tracking.quickAdd.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
toast.success(resolveToast(toastOverrides?.quickAdd, t`Added to watchlist`, input));
|
||||
invalidateTitleQueries();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t`Failed to add to watchlist`);
|
||||
// Refetch so optimistic local status reverts on failure
|
||||
queryClient.invalidateQueries({ queryKey: orpc.titles.key() });
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const updateStatus = useMutation(
|
||||
orpc.tracking.updateStatus.mutationOptions({
|
||||
onSuccess: (_data, input) => {
|
||||
@@ -92,8 +76,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
? t`Rated ${plural(stars, { one: "# star", other: "# stars" })}`
|
||||
: t`Rating removed`;
|
||||
toast.success(resolveToast(toastOverrides?.updateRating, defaultMsg, input));
|
||||
// Rating only invalidates title queries, not tracking
|
||||
queryClient.invalidateQueries({ queryKey: orpc.titles.key() });
|
||||
invalidateTitleQueries();
|
||||
},
|
||||
onError: () => toast.error(t`Failed to update rating`),
|
||||
}),
|
||||
@@ -130,7 +113,6 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
||||
);
|
||||
|
||||
return {
|
||||
quickAdd,
|
||||
updateStatus,
|
||||
watchMovie,
|
||||
updateRating,
|
||||
|
||||
@@ -27,9 +27,9 @@ export function invalidateTitleQueries() {
|
||||
* Each method calls the RPC, shows a toast, and invalidates the relevant queries.
|
||||
*/
|
||||
export const titleActions = {
|
||||
async quickAdd(id: string, titleName?: string) {
|
||||
async addToWatchlist(id: string, titleName?: string) {
|
||||
try {
|
||||
await client.tracking.quickAdd({ id });
|
||||
await client.tracking.updateStatus({ id, status: "watchlist" });
|
||||
toast.success(
|
||||
titleName
|
||||
? i18n._(msg`Added "${titleName}" to watchlist`)
|
||||
|
||||
Reference in New Issue
Block a user