From 2169bea03174bcd27c62fb8a394f7dace68750e7 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 5 Jan 2025 11:55:05 +0100 Subject: [PATCH 01/11] fix: add loading and refactor permissions --- .../jellyseerr/page.tsx | 38 ++++++------------- 1 file changed, 12 insertions(+), 26 deletions(-) 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..c33b1ee7 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 ? ( From 5ee1a9cabba8246c3a191bc12f3d605447ac330f Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 5 Jan 2025 11:55:22 +0100 Subject: [PATCH 02/11] fix: change keys --- app/(auth)/(tabs)/(search)/index.tsx | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index fcf8119d..92dc00c5 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( () => From b2786325819c69bc90ea7b836655821186b37059 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 5 Jan 2025 11:55:29 +0100 Subject: [PATCH 03/11] fix: height on loading button --- components/Button.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/Button.tsx b/components/Button.tsx index 1a73ad01..fb685f4c 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -61,7 +61,9 @@ export const Button: React.FC> = ({ {...props} > {loading ? ( - + + + ) : ( Date: Sun, 5 Jan 2025 11:55:41 +0100 Subject: [PATCH 04/11] fix: refactor permissions --- components/posters/JellyseerrPoster.tsx | 81 +++++++++++-------------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/components/posters/JellyseerrPoster.tsx b/components/posters/JellyseerrPoster.tsx index ad3df50f..5f363d42 100644 --- a/components/posters/JellyseerrPoster.tsx +++ b/components/posters/JellyseerrPoster.tsx @@ -1,53 +1,47 @@ -import {View, ViewProps} from "react-native"; -import {Image} from "expo-image"; -import {Text} from "@/components/common/Text"; -import {useMemo} from "react"; -import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; -import {MediaStatus, MediaType} from "@/utils/jellyseerr/server/constants/media"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import {hasPermission, Permission} from "@/utils/jellyseerr/server/lib/permissions"; -import {TouchableJellyseerrRouter} from "@/components/common/JellyseerrItemRouter"; +import { View, ViewProps } from "react-native"; +import { Image } from "expo-image"; +import { Text } from "@/components/common/Text"; +import { useMemo } from "react"; +import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; +import { + MediaStatus, + MediaType, +} from "@/utils/jellyseerr/server/constants/media"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { + hasPermission, + Permission, +} from "@/utils/jellyseerr/server/lib/permissions"; +import { TouchableJellyseerrRouter } from "@/components/common/JellyseerrItemRouter"; import JellyseerrStatusIcon from "@/components/jellyseerr/JellyseerrStatusIcon"; import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon"; +import { useJellyseerrCanRequest } from "@/utils/_jellyseerr/useJellyseerrCanRequest"; interface Props extends ViewProps { item: MovieResult | TvResult; } -const JellyseerrPoster: React.FC = ({ - 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; From cab6257fb2c92440b6bf9132eba586f15999c73a Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 5 Jan 2025 11:55:52 +0100 Subject: [PATCH 05/11] feat: hook for request permissions --- utils/_jellyseerr/useJellyseerrCanRequest.ts | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 utils/_jellyseerr/useJellyseerrCanRequest.ts 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; +}; From adfde1a7cdd71065617a9901a3e3cfe3ddaa3fc2 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 5 Jan 2025 11:56:16 +0100 Subject: [PATCH 06/11] fix: update request status in poster on search scree --- hooks/useJellyseerr.ts | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) 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!`); From e9336e9a6727e06db01c6e15fb3ff4e98e6e8235 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 5 Jan 2025 11:56:46 +0100 Subject: [PATCH 07/11] feat: new useQuery specifically for react navigation to invalidate !enabled queries on screen re-mount --- utils/useReactNavigationQuery.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 utils/useReactNavigationQuery.ts 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; +} From 39bb3a93708b92694e27a787802d9f46b43a523f Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 5 Jan 2025 12:03:56 +0100 Subject: [PATCH 08/11] fix: padding --- .../jellyseerr/[personId].tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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()} From 0773f773baf0504670819f2dd6a0acd1fa2fed05 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 5 Jan 2025 12:04:02 +0100 Subject: [PATCH 09/11] fix: padding --- .../(home,libraries,search,favorites)/jellyseerr/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c33b1ee7..b839708d 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx @@ -246,7 +246,7 @@ const Page: React.FC = () => { className="p-2 border border-neutral-800 bg-neutral-900 rounded-xl" details={details} /> - + From 0fb6f2fb308f32cce941013b96a64a084e323a51 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 5 Jan 2025 12:04:07 +0100 Subject: [PATCH 10/11] fix: padding --- app/(auth)/(tabs)/(search)/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 92dc00c5..1ae0059c 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -315,7 +315,7 @@ export default function search() { paddingRight: insets.right, }} > - + {Platform.OS === "android" && ( Date: Sun, 5 Jan 2025 12:04:11 +0100 Subject: [PATCH 11/11] fix: padding --- components/jellyseerr/Cast.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/jellyseerr/Cast.tsx b/components/jellyseerr/Cast.tsx index 8f64355d..f5474caf 100644 --- a/components/jellyseerr/Cast.tsx +++ b/components/jellyseerr/Cast.tsx @@ -13,7 +13,7 @@ const CastSlide: React.FC< details?.credits?.cast?.length && details?.credits?.cast?.length > 0 && ( - Cast + Cast } estimatedItemSize={15} keyExtractor={(item) => item?.id?.toString()} + contentContainerStyle={{ paddingHorizontal: 16 }} renderItem={({ item }) => (