diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/[personId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/[personId].tsx index 2dc2a478..5a930982 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/[personId].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/[personId].tsx @@ -223,12 +223,14 @@ export default function page() { mediaType={item.mediaType as "movie" | "tv"} /> {/*{item.title}*/} - - as {item.character} - + {item.character && ( + + as {item.character} + + )} )} keyExtractor={(item) => item.id.toString()} diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx index 678680c7..b839708d 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx @@ -42,22 +42,18 @@ import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; import DetailFacts from "@/components/jellyseerr/DetailFacts"; import { ItemActions } from "@/components/series/SeriesActions"; import Cast from "@/components/jellyseerr/Cast"; +import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; const Page: React.FC = () => { const insets = useSafeAreaInsets(); const params = useLocalSearchParams(); - const { - mediaTitle, - releaseYear, - canRequest: canRequestString, - posterSrc, - ...result - } = params as unknown as { - mediaTitle: string; - releaseYear: number; - canRequest: string; - posterSrc: string; - } & Partial; + const { mediaTitle, releaseYear, posterSrc, ...result } = + params as unknown as { + mediaTitle: string; + releaseYear: number; + canRequest: string; + posterSrc: string; + } & Partial; const navigation = useNavigation(); const { jellyseerrApi, requestMedia } = useJellyseerr(); @@ -87,19 +83,7 @@ const Page: React.FC = () => { }, }); - const canRequest = useMemo(() => { - const pendingRequests = details?.mediaInfo?.requests?.some( - (r: MediaRequest) => - r.status == MediaRequestStatus.PENDING || - r.status == MediaRequestStatus.APPROVED - ); - - return ( - (details?.mediaInfo?.status === MediaStatus.UNKNOWN && - !pendingRequests) || - (!details?.mediaInfo?.status && canRequestString === "true") - ); - }, [canRequestString, details]); + const canRequest = useJellyseerrCanRequest(details); const renderBackdrop = useCallback( (props: BottomSheetBackdropProps) => ( @@ -225,7 +209,9 @@ const Page: React.FC = () => { g.name) || []} /> - {canRequest ? ( + {isLoading || isFetching ? ( + + ) : canRequest ? ( @@ -260,7 +246,7 @@ const Page: React.FC = () => { className="p-2 border border-neutral-800 bg-neutral-900 rounded-xl" details={details} /> - + diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index fcf8119d..1ae0059c 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -31,13 +31,18 @@ import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useDebounce } from "use-debounce"; import { useJellyseerr } from "@/hooks/useJellyseerr"; -import {MovieResult, PersonResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; +import { + MovieResult, + PersonResult, + TvResult, +} from "@/utils/jellyseerr/server/models/Search"; import { MediaType } from "@/utils/jellyseerr/server/constants/media"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; import { Tag } from "@/components/GenreTags"; import DiscoverSlide from "@/components/jellyseerr/DiscoverSlide"; import { sortBy } from "lodash"; import PersonPoster from "@/components/jellyseerr/PersonPoster"; +import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery"; type SearchType = "Library" | "Discover"; @@ -150,8 +155,8 @@ export default function search() { enabled: searchType === "Library" && debouncedSearch.length > 0, }); - const { data: jellyseerrResults, isFetching: j1 } = useQuery({ - queryKey: ["search", "jellyseerrResults", debouncedSearch], + const { data: jellyseerrResults, isFetching: j1 } = useReactNavigationQuery({ + queryKey: ["search", "jellyseerr", "results", debouncedSearch], queryFn: async () => { const response = await jellyseerrApi?.search({ query: new URLSearchParams(debouncedSearch).toString(), @@ -167,14 +172,15 @@ export default function search() { debouncedSearch.length > 0, }); - const { data: jellyseerrDiscoverSettings, isFetching: j2 } = useQuery({ - queryKey: ["search", "jellyseerrDiscoverSettings", debouncedSearch], - queryFn: async () => jellyseerrApi?.discoverSettings(), - enabled: - !!jellyseerrApi && - searchType === "Discover" && - debouncedSearch.length == 0, - }); + const { data: jellyseerrDiscoverSettings, isFetching: j2 } = + useReactNavigationQuery({ + queryKey: ["search", "jellyseerr", "discoverSettings", debouncedSearch], + queryFn: async () => jellyseerrApi?.discoverSettings(), + enabled: + !!jellyseerrApi && + searchType === "Discover" && + debouncedSearch.length == 0, + }); const jellyseerrMovieResults: MovieResult[] | undefined = useMemo( () => @@ -309,7 +315,7 @@ export default function search() { paddingRight: insets.right, }} > - + {Platform.OS === "android" && ( > = ({ {...props} > {loading ? ( - + + + ) : ( 0 && ( - Cast + Cast } estimatedItemSize={15} keyExtractor={(item) => item?.id?.toString()} + contentContainerStyle={{ paddingHorizontal: 16 }} renderItem={({ item }) => ( = ({ - item, - ...props -}) => { - const {jellyseerrUser, jellyseerrApi} = useJellyseerr(); - // const imageSource = +const JellyseerrPoster: React.FC = ({ item, ...props }) => { + const { jellyseerrApi } = useJellyseerr(); const imageSrc = useMemo( - () => jellyseerrApi?.imageProxy(item.posterPath, 'w300_and_h450_face'), + () => jellyseerrApi?.imageProxy(item.posterPath, "w300_and_h450_face"), [item, jellyseerrApi] - ) - const title = useMemo(() => item.mediaType === MediaType.MOVIE ? item.title : item.name, [item]) - const releaseYear = useMemo(() => - new Date(item.mediaType === MediaType.MOVIE ? item.releaseDate : item.firstAirDate).getFullYear(), + ); + const title = useMemo( + () => (item.mediaType === MediaType.MOVIE ? item.title : item.name), [item] - ) + ); + const releaseYear = useMemo( + () => + new Date( + item.mediaType === MediaType.MOVIE + ? item.releaseDate + : item.firstAirDate + ).getFullYear(), + [item] + ); - const showRequestButton = useMemo(() => - jellyseerrUser && hasPermission( - [ - Permission.REQUEST, - item.mediaType === 'movie' - ? Permission.REQUEST_MOVIE - : Permission.REQUEST_TV, - ], - jellyseerrUser.permissions, - {type: 'or'} - ), - [item, jellyseerrUser] - ) - - const canRequest = useMemo(() => { - const status = item?.mediaInfo?.status - return showRequestButton && !status || status === MediaStatus.UNKNOWN - }, [item]) + const canRequest = useJellyseerrCanRequest(item); return ( = ({ = ({ - ) -} + ); +}; - -export default JellyseerrPoster; \ No newline at end of file +export default JellyseerrPoster; diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index 4546cee1..815510fa 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -30,8 +30,9 @@ import { writeErrorLog } from "@/utils/log"; import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; import { CombinedCredit, - PersonDetails + PersonDetails, } from "@/utils/jellyseerr/server/models/Person"; +import { useQueryClient } from "@tanstack/react-query"; interface SearchParams { query: string; @@ -220,7 +221,12 @@ export class JellyseerrApi { async personCombinedCredits(id: number | string): Promise { return this.axios - ?.get(Endpoints.API_V1 + Endpoints.PERSON + `/${id}` + Endpoints.COMBINED_CREDITS) + ?.get( + Endpoints.API_V1 + + Endpoints.PERSON + + `/${id}` + + Endpoints.COMBINED_CREDITS + ) .then((response) => { return response?.data; }); @@ -260,15 +266,20 @@ export class JellyseerrApi { }); } - imageProxy(path?: string, tmdbPath: string = 'original', width: number = 1920, quality: number = 75) { - return path ? ( - this.axios.defaults.baseURL + - `/_next/image?` + - new URLSearchParams( - `url=https://image.tmdb.org/t/p/${tmdbPath}/${path}&w=${width}&q=${quality}` - ).toString() - ) : - this.axios?.defaults.baseURL + `/images/overseerr_poster_not_found_logo_top.png`; + imageProxy( + path?: string, + tmdbPath: string = "original", + width: number = 1920, + quality: number = 75 + ) { + return path + ? this.axios.defaults.baseURL + + `/_next/image?` + + new URLSearchParams( + `url=https://image.tmdb.org/t/p/${tmdbPath}/${path}&w=${width}&q=${quality}` + ).toString() + : this.axios?.defaults.baseURL + + `/images/overseerr_poster_not_found_logo_top.png`; } async submitIssue(mediaId: number, issueType: IssueType, message: string) { @@ -344,6 +355,7 @@ const jellyseerrUserAtom = atom(storage.get(JELLYSEERR_USER)); export const useJellyseerr = () => { const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom); const [settings, updateSettings] = useSettings(); + const queryClient = useQueryClient(); const jellyseerrApi = useMemo(() => { const cookies = storage.get(JELLYSEERR_COOKIES); @@ -361,12 +373,16 @@ export const useJellyseerr = () => { const requestMedia = useCallback( (title: string, request: MediaRequestBody, onSuccess?: () => void) => { - jellyseerrApi?.request?.(request)?.then((mediaRequest) => { + jellyseerrApi?.request?.(request)?.then(async (mediaRequest) => { + await queryClient.invalidateQueries({ + queryKey: ["search", "jellyseerr"], + }); + switch (mediaRequest.status) { case MediaRequestStatus.PENDING: case MediaRequestStatus.APPROVED: toast.success(`Requested ${title}!`); - onSuccess?.() + onSuccess?.(); break; case MediaRequestStatus.DECLINED: toast.error(`You don't have permission to request!`); diff --git a/utils/_jellyseerr/useJellyseerrCanRequest.ts b/utils/_jellyseerr/useJellyseerrCanRequest.ts new file mode 100644 index 00000000..ba692df3 --- /dev/null +++ b/utils/_jellyseerr/useJellyseerrCanRequest.ts @@ -0,0 +1,52 @@ +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { + MediaRequestStatus, + MediaStatus, +} from "@/utils/jellyseerr/server/constants/media"; +import { + hasPermission, + Permission, +} from "@/utils/jellyseerr/server/lib/permissions"; +import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; +import { useMemo } from "react"; +import MediaRequest from "../jellyseerr/server/entity/MediaRequest"; +import { MovieDetails } from "../jellyseerr/server/models/Movie"; +import { TvDetails } from "../jellyseerr/server/models/Tv"; + +export const useJellyseerrCanRequest = ( + item?: MovieResult | TvResult | MovieDetails | TvDetails +) => { + const { jellyseerrUser } = useJellyseerr(); + + const canRequest = useMemo(() => { + if (!jellyseerrUser || !item) return false; + + const canNotRequest = + item?.mediaInfo?.requests?.some( + (r: MediaRequest) => + r.status == MediaRequestStatus.PENDING || + r.status == MediaRequestStatus.APPROVED + ) || + item.mediaInfo?.status === MediaStatus.AVAILABLE || + item.mediaInfo?.status === MediaStatus.BLACKLISTED || + item.mediaInfo?.status === MediaStatus.PENDING || + item.mediaInfo?.status === MediaStatus.PROCESSING; + + if (canNotRequest) return false; + + const userHasPermission = hasPermission( + [ + Permission.REQUEST, + item?.mediaInfo?.mediaType + ? Permission.REQUEST_MOVIE + : Permission.REQUEST_TV, + ], + jellyseerrUser.permissions, + { type: "or" } + ); + + return userHasPermission && !canNotRequest; + }, [item, jellyseerrUser]); + + return canRequest; +}; diff --git a/utils/useReactNavigationQuery.ts b/utils/useReactNavigationQuery.ts new file mode 100644 index 00000000..a0c5b307 --- /dev/null +++ b/utils/useReactNavigationQuery.ts @@ -0,0 +1,32 @@ +import { useFocusEffect } from "@react-navigation/core"; +import { + QueryKey, + useQuery, + UseQueryOptions, + UseQueryResult, +} from "@tanstack/react-query"; +import { useCallback } from "react"; + +export function useReactNavigationQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +>( + options: UseQueryOptions +): UseQueryResult { + const useQueryReturn = useQuery(options); + + useFocusEffect( + useCallback(() => { + if ( + ((options.refetchOnWindowFocus && useQueryReturn.isStale) || + options.refetchOnWindowFocus === "always") && + options.enabled !== false + ) + useQueryReturn.refetch(); + }, [options.enabled, options.refetchOnWindowFocus]) + ); + + return useQueryReturn; +}