From f2e73e0e53e3681e7fee4b173128bd736163e1d9 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Fri, 13 Feb 2026 17:16:57 -0600 Subject: [PATCH 01/36] playlist user controls --- src/api/queries/playlist/utils/users.ts | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/api/queries/playlist/utils/users.ts diff --git a/src/api/queries/playlist/utils/users.ts b/src/api/queries/playlist/utils/users.ts new file mode 100644 index 000000000..b2178d2a5 --- /dev/null +++ b/src/api/queries/playlist/utils/users.ts @@ -0,0 +1,46 @@ +//playlist id + +import { getApi, getUser } from '@/src/stores' +import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api' + +//get playlist users +function getPlaylistUsers(playlistId: string) { + //use api + const api = getApi() + //const user = getUser(); + const playlist = getPlaylistsApi(api!) + + return playlist.getPlaylistUsers({ playlistId }) +} + +//also need user id for add and remove user functions + +function addPlaylistUser(playlistId: string, userId: string, CanEdit: boolean) { + //use api + const api = getApi() + const playlist = getPlaylistsApi(api!) + + //use dto + return playlist.updatePlaylist({ + playlistId, + updatePlaylistDto: { + Users: [ + { + UserId: userId, + CanEdit, + }, + ], + }, + }) +} + +function removePlaylistUser(playlistId: string, userId: string) { + //use api + const api = getApi() + const playlist = getPlaylistsApi(api!) + + return playlist.removeUserFromPlaylist({ + playlistId, + userId, + }) +} From 75523fe6e9dfc0e3135b8b772b6921762a06a68a Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Wed, 18 Feb 2026 17:49:14 -0600 Subject: [PATCH 02/36] updated users.ts to incorporate async function feedback --- src/api/queries/playlist/utils/users.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/api/queries/playlist/utils/users.ts b/src/api/queries/playlist/utils/users.ts index b2178d2a5..e6c13a90f 100644 --- a/src/api/queries/playlist/utils/users.ts +++ b/src/api/queries/playlist/utils/users.ts @@ -4,24 +4,28 @@ import { getApi, getUser } from '@/src/stores' import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api' //get playlist users -function getPlaylistUsers(playlistId: string) { +async function getPlaylistUsers(playlistId: string) { //use api const api = getApi() - //const user = getUser(); - const playlist = getPlaylistsApi(api!) - return playlist.getPlaylistUsers({ playlistId }) + if (!api) { + throw new Error('API Instance not set') + } + + const playlist = getPlaylistsApi(api) + + return await playlist.getPlaylistUsers({ playlistId }) } //also need user id for add and remove user functions -function addPlaylistUser(playlistId: string, userId: string, CanEdit: boolean) { +async function addPlaylistUser(playlistId: string, userId: string, CanEdit: boolean) { //use api const api = getApi() const playlist = getPlaylistsApi(api!) //use dto - return playlist.updatePlaylist({ + return await playlist.updatePlaylist({ playlistId, updatePlaylistDto: { Users: [ @@ -34,12 +38,12 @@ function addPlaylistUser(playlistId: string, userId: string, CanEdit: boolean) { }) } -function removePlaylistUser(playlistId: string, userId: string) { +async function removePlaylistUser(playlistId: string, userId: string) { //use api const api = getApi() const playlist = getPlaylistsApi(api!) - return playlist.removeUserFromPlaylist({ + return await playlist.removeUserFromPlaylist({ playlistId, userId, }) From a1f963219dbcccfcda27a9fb0f3949f9fe72ce57 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Wed, 18 Feb 2026 19:33:37 -0600 Subject: [PATCH 03/36] starting to build hooks and screen for collaborative playlist feature --- src/api/queries/playlist/index.ts | 50 +++++++++++++++++++++- src/api/queries/playlist/keys.ts | 6 +++ src/api/queries/playlist/utils/users.ts | 6 +-- src/screens/Library/add-playlist-users.tsx | 14 ++++++ src/screens/Library/index.tsx | 10 +++++ src/screens/Library/types.ts | 7 +++ 6 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 src/screens/Library/add-playlist-users.tsx diff --git a/src/api/queries/playlist/index.ts b/src/api/queries/playlist/index.ts index f1f24287b..f8084abbf 100644 --- a/src/api/queries/playlist/index.ts +++ b/src/api/queries/playlist/index.ts @@ -1,10 +1,17 @@ -import { PlaylistTracksQueryKey, PublicPlaylistsQueryKey, UserPlaylistsQueryKey } from './keys' -import { useInfiniteQuery } from '@tanstack/react-query' +import { + PlaylistTracksQueryKey, + PlaylistUsersQueryKey, + PublicPlaylistsQueryKey, + UserPlaylistsQueryKey, +} from './keys' +import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query' import { fetchUserPlaylists, fetchPublicPlaylists, fetchPlaylistTracks } from './utils' import { ApiLimits } from '../../../configs/query.config' import { getApi, getUser, useJellifyLibrary } from '../../../stores' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client' import { QueryKeys } from '../../../enums/query-keys' +import { addPlaylistUser, getPlaylistUsers, removePlaylistUser } from './utils/users' +import { ONE_MINUTE } from '@/src/constants/query-client' export const useUserPlaylists = () => { const api = getApi() @@ -53,3 +60,42 @@ export const usePublicPlaylists = () => { initialPageParam: 0, }) } + +//hooks - used in react components +//invoke user functions (getPlaylistUsers, etc) +//following react convention +export const usePlaylistUsers = (playlist: BaseItemDto) => { + return useQuery({ + queryKey: PlaylistUsersQueryKey(playlist), + queryFn: () => getPlaylistUsers(playlist.Id!), + staleTime: ONE_MINUTE * 15, //refreshes every 15mins + }) +} + +interface addPlaylistUserMutation { + playlistId: string + userId: string + CanEdit: boolean +} + +//mutations not queries for add/remove +//no params +export const useAddPlaylistUser = () => { + return useMutation({ + //playlistId: string, userId: string, CanEdit: boolean + mutationFn: (variables: addPlaylistUserMutation) => + addPlaylistUser(variables.playlistId, variables.userId, variables.CanEdit), + }) +} + +interface removePlaylistUser { + playlistId: string + userId: string +} + +export const useRemovePlaylistUser = () => { + return useMutation({ + mutationFn: (variables: removePlaylistUser) => + removePlaylistUser(variables.playlistId, variables.userId), + }) +} diff --git a/src/api/queries/playlist/keys.ts b/src/api/queries/playlist/keys.ts index 78f888221..d62253e59 100644 --- a/src/api/queries/playlist/keys.ts +++ b/src/api/queries/playlist/keys.ts @@ -5,6 +5,7 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client' enum PlaylistQueryKeys { UserPlaylists, PublicPlaylists, + PlaylistUsers, } export const UserPlaylistsQueryKey = (library: JellifyLibrary | undefined) => [ @@ -22,3 +23,8 @@ export const PublicPlaylistsQueryKey = (library: JellifyLibrary | undefined) => PlaylistQueryKeys.PublicPlaylists, library?.playlistLibraryId, ] + +export const PlaylistUsersQueryKey = (playlist: BaseItemDto) => [ + PlaylistQueryKeys.PlaylistUsers, + playlist.Id, +] diff --git a/src/api/queries/playlist/utils/users.ts b/src/api/queries/playlist/utils/users.ts index e6c13a90f..8555ef0b0 100644 --- a/src/api/queries/playlist/utils/users.ts +++ b/src/api/queries/playlist/utils/users.ts @@ -4,7 +4,7 @@ import { getApi, getUser } from '@/src/stores' import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api' //get playlist users -async function getPlaylistUsers(playlistId: string) { +export async function getPlaylistUsers(playlistId: string) { //use api const api = getApi() @@ -19,7 +19,7 @@ async function getPlaylistUsers(playlistId: string) { //also need user id for add and remove user functions -async function addPlaylistUser(playlistId: string, userId: string, CanEdit: boolean) { +export async function addPlaylistUser(playlistId: string, userId: string, CanEdit: boolean) { //use api const api = getApi() const playlist = getPlaylistsApi(api!) @@ -38,7 +38,7 @@ async function addPlaylistUser(playlistId: string, userId: string, CanEdit: bool }) } -async function removePlaylistUser(playlistId: string, userId: string) { +export async function removePlaylistUser(playlistId: string, userId: string) { //use api const api = getApi() const playlist = getPlaylistsApi(api!) diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx new file mode 100644 index 000000000..ec4def2dd --- /dev/null +++ b/src/screens/Library/add-playlist-users.tsx @@ -0,0 +1,14 @@ +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import LibraryStackParamList, { LibraryAddPlaylistUsers } from './types' +import { View } from 'tamagui' +import { usePlaylistUsers } from '@/src/api/queries/playlist' + +//screen in react native +export default function addPlaylistUsers({ + navigation, + route, +}: LibraryAddPlaylistUsers): React.JSX.Element { + const result = usePlaylistUsers(route.params.playlist) + //return component here + return +} diff --git a/src/screens/Library/index.tsx b/src/screens/Library/index.tsx index 85568a1ce..b3370b13d 100644 --- a/src/screens/Library/index.tsx +++ b/src/screens/Library/index.tsx @@ -11,6 +11,7 @@ import InstantMix from '../../components/InstantMix/component' import { getItemName } from '../../utils/formatting/item-names' import { Platform } from 'react-native' import TracksScreen from '../Tracks' +import addPlaylistUsers from './add-playlist-users' const LibraryStack = createNativeStackNavigator() @@ -82,6 +83,15 @@ export default function LibraryScreen(): React.JSX.Element { sheetAllowedDetents: Platform.OS === 'ios' ? 'fitToContents' : [0.5], }} /> + diff --git a/src/screens/Library/types.ts b/src/screens/Library/types.ts index 2b30bf904..adf086a0d 100644 --- a/src/screens/Library/types.ts +++ b/src/screens/Library/types.ts @@ -10,6 +10,9 @@ type LibraryStackParamList = BaseStackParamList & { DeletePlaylist: { playlist: BaseItemDto } + AddPlaylistUsers: { + playlist: BaseItemDto + } } export default LibraryStackParamList @@ -23,3 +26,7 @@ export type LibraryDeletePlaylistProps = NativeStackScreenProps< LibraryStackParamList, 'DeletePlaylist' > +export type LibraryAddPlaylistUsers = NativeStackScreenProps< + LibraryStackParamList, + 'AddPlaylistUsers' +> From b3bf0b74408c289332466aef2b9ad52559e6d906 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Tue, 3 Mar 2026 17:23:33 -0600 Subject: [PATCH 04/36] hook to retrieve lists of users --- src/api/queries/users/index.ts | 24 ++++++++++++++++++++++++ src/api/queries/users/keys.ts | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 src/api/queries/users/index.ts create mode 100644 src/api/queries/users/keys.ts diff --git a/src/api/queries/users/index.ts b/src/api/queries/users/index.ts new file mode 100644 index 000000000..240707a0d --- /dev/null +++ b/src/api/queries/users/index.ts @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query' +import { UserQueryKey } from './keys' +import { getApi } from '@/src/stores' +import { getUserApi } from '@jellyfin/sdk/lib/utils/api' + +//hook to get users on server +export const useUsers = () => { + //using a query to call fetchUsers for server (not playlist) + return useQuery({ queryKey: UserQueryKey, queryFn: fetchUsers }) +} + +//function to call get user API (jellyfin), no export because it's only used here +const fetchUsers = () => { + //use api (only get api when this function is called to get users) + const api = getApi() + + //check set + if (!api) { + throw new Error('API Instance not set') + } + + //return users from api + return getUserApi(api).getUsers() +} diff --git a/src/api/queries/users/keys.ts b/src/api/queries/users/keys.ts new file mode 100644 index 000000000..4ccf18fc2 --- /dev/null +++ b/src/api/queries/users/keys.ts @@ -0,0 +1,2 @@ +//key to get users (array of one string) on server +export const UserQueryKey = ['Users'] From 05bcfa14f566dd704ade36a7561a751d6e81e071 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Sat, 7 Mar 2026 12:10:50 -0600 Subject: [PATCH 05/36] logic to get playlist users and server users --- src/api/queries/playlist/utils/users.ts | 2 +- src/api/queries/users/index.ts | 4 ++-- src/screens/Library/add-playlist-users.tsx | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/api/queries/playlist/utils/users.ts b/src/api/queries/playlist/utils/users.ts index 8555ef0b0..c6cec03d7 100644 --- a/src/api/queries/playlist/utils/users.ts +++ b/src/api/queries/playlist/utils/users.ts @@ -14,7 +14,7 @@ export async function getPlaylistUsers(playlistId: string) { const playlist = getPlaylistsApi(api) - return await playlist.getPlaylistUsers({ playlistId }) + return (await playlist.getPlaylistUsers({ playlistId })).data } //also need user id for add and remove user functions diff --git a/src/api/queries/users/index.ts b/src/api/queries/users/index.ts index 240707a0d..c361e5434 100644 --- a/src/api/queries/users/index.ts +++ b/src/api/queries/users/index.ts @@ -10,7 +10,7 @@ export const useUsers = () => { } //function to call get user API (jellyfin), no export because it's only used here -const fetchUsers = () => { +const fetchUsers = async () => { //use api (only get api when this function is called to get users) const api = getApi() @@ -20,5 +20,5 @@ const fetchUsers = () => { } //return users from api - return getUserApi(api).getUsers() + return (await getUserApi(api).getUsers()).data } diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index ec4def2dd..bab080378 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -2,13 +2,27 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import LibraryStackParamList, { LibraryAddPlaylistUsers } from './types' import { View } from 'tamagui' import { usePlaylistUsers } from '@/src/api/queries/playlist' +import { useUsers } from '@/src/api/queries/users' //screen in react native export default function addPlaylistUsers({ navigation, route, }: LibraryAddPlaylistUsers): React.JSX.Element { - const result = usePlaylistUsers(route.params.playlist) + const { + data: playlistUsers, + isPending: playlistUserIsPending, + refetch: refetchPlaylistUser, + } = usePlaylistUsers(route.params.playlist) + const { data: users, isPending: useUsersIsPending, refetch: refetchUseUsers } = useUsers() + + //get string array of all playlist user IDs + const playlistUserIds = playlistUsers?.map((playlistUser) => playlistUser.UserId) + + //if user exists in playlist already, do not display + //take all users, filter any users that also appear in playlistUserIds + const otherUsers = users?.filter((user) => playlistUserIds?.includes(user.Id)) + //return component here return } From f5a10268ddb8e4746517b6df0311e6c78da514b3 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Sat, 7 Mar 2026 12:46:21 -0600 Subject: [PATCH 06/36] changing @/src to ../src --- src/api/queries/playlist/index.ts | 2 +- src/api/queries/playlist/utils/users.ts | 2 +- src/api/queries/users/index.ts | 2 +- src/screens/Library/add-playlist-users.tsx | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/queries/playlist/index.ts b/src/api/queries/playlist/index.ts index f8084abbf..0fcd413f6 100644 --- a/src/api/queries/playlist/index.ts +++ b/src/api/queries/playlist/index.ts @@ -11,7 +11,7 @@ import { getApi, getUser, useJellifyLibrary } from '../../../stores' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client' import { QueryKeys } from '../../../enums/query-keys' import { addPlaylistUser, getPlaylistUsers, removePlaylistUser } from './utils/users' -import { ONE_MINUTE } from '@/src/constants/query-client' +import { ONE_MINUTE } from '../../../constants/query-client' export const useUserPlaylists = () => { const api = getApi() diff --git a/src/api/queries/playlist/utils/users.ts b/src/api/queries/playlist/utils/users.ts index c6cec03d7..8e59f5330 100644 --- a/src/api/queries/playlist/utils/users.ts +++ b/src/api/queries/playlist/utils/users.ts @@ -1,6 +1,6 @@ //playlist id -import { getApi, getUser } from '@/src/stores' +import { getApi, getUser } from '../../../../stores' import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api' //get playlist users diff --git a/src/api/queries/users/index.ts b/src/api/queries/users/index.ts index c361e5434..d3e3c2ec9 100644 --- a/src/api/queries/users/index.ts +++ b/src/api/queries/users/index.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { UserQueryKey } from './keys' -import { getApi } from '@/src/stores' +import { getApi } from '../../../stores' import { getUserApi } from '@jellyfin/sdk/lib/utils/api' //hook to get users on server diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index bab080378..c97feaf62 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -1,8 +1,8 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import LibraryStackParamList, { LibraryAddPlaylistUsers } from './types' import { View } from 'tamagui' -import { usePlaylistUsers } from '@/src/api/queries/playlist' -import { useUsers } from '@/src/api/queries/users' +import { usePlaylistUsers } from '../../../src/api/queries/playlist' +import { useUsers } from '../../../src/api/queries/users' //screen in react native export default function addPlaylistUsers({ From fdb3787c98f7feff9b4503e59246ee5d785c76f9 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Sun, 8 Mar 2026 21:05:03 -0500 Subject: [PATCH 07/36] view for playlist collaborator screen --- src/screens/Library/add-playlist-users.tsx | 86 ++++++++++++++++++++-- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index c97feaf62..9e7322420 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -1,28 +1,104 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import LibraryStackParamList, { LibraryAddPlaylistUsers } from './types' -import { View } from 'tamagui' +import { Paragraph, Text, View, XStack, YStack } from 'tamagui' import { usePlaylistUsers } from '../../../src/api/queries/playlist' import { useUsers } from '../../../src/api/queries/users' +import ItemImage from '../../../src/components/Global/components/image' +import TextTicker from 'react-native-text-ticker' +import { TextTickerConfig } from '../../../src/components/Player/component.config' +import { getItemName } from '../../../src/utils/formatting/item-names' +import { SectionList } from 'react-native' +import Icon from '../../../src/components/Global/components/icon' //screen in react native export default function addPlaylistUsers({ navigation, route, }: LibraryAddPlaylistUsers): React.JSX.Element { + const { playlist } = route.params const { data: playlistUsers, isPending: playlistUserIsPending, refetch: refetchPlaylistUser, - } = usePlaylistUsers(route.params.playlist) + } = usePlaylistUsers(playlist) //make this playlist an easy access variable (with const variable above) const { data: users, isPending: useUsersIsPending, refetch: refetchUseUsers } = useUsers() //get string array of all playlist user IDs - const playlistUserIds = playlistUsers?.map((playlistUser) => playlistUser.UserId) + const playlistUserIds = playlistUsers?.map((playlistUser) => playlistUser.UserId) ?? [] //if user exists in playlist already, do not display //take all users, filter any users that also appear in playlistUserIds - const otherUsers = users?.filter((user) => playlistUserIds?.includes(user.Id)) + const otherUsers = users?.filter((user) => playlistUserIds?.includes(user.Id)) ?? [] + + //any user not included in listed users will get filtered out + const usersInPlaylist = users?.filter((user) => !playlistUserIds?.includes(user.Id)) ?? [] + + //use formatting for sections component later on + const playlistUserData = [ + { + title: 'Shared With', + data: usersInPlaylist, + }, + { + title: 'Users on Server', + data: otherUsers, + }, + ] //return component here - return + return ( + //return view that occupies full screen + + { + //no conditional statement here (have to have a playlist to see this view anyways) + + + + + + + {getItemName(playlist)} + + + + {/* + + {`${(source ?? tracks[0])!.ArtistItems?.map((artist) => getItemName(artist)).join(', ')}`} + + */} + + + } + + {/* conditional in react - only render if some variable meet criteria */} + { + //list of users and section list + ( + + + {user.Name ?? 'Unknown User'} + {playlistUserIds.includes(user.Id) ? ( + + ) : ( + + )} + + )} + keyExtractor={(item) => item.Id!} + /> + } + + ) } From f07b5ff64fdc9dfd497e24427b1c6552d708cba4 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Mon, 9 Mar 2026 20:32:34 -0500 Subject: [PATCH 08/36] fullscreen modal, filter out current owner of playlist, callbacks for functionality --- src/api/queries/playlist/index.ts | 56 ++++++++++++++++++---- src/api/queries/users/index.ts | 10 +++- src/screens/Library/add-playlist-users.tsx | 35 ++++++++++++-- src/screens/Library/index.tsx | 2 +- 4 files changed, 88 insertions(+), 15 deletions(-) diff --git a/src/api/queries/playlist/index.ts b/src/api/queries/playlist/index.ts index 0fcd413f6..f0498347d 100644 --- a/src/api/queries/playlist/index.ts +++ b/src/api/queries/playlist/index.ts @@ -8,10 +8,15 @@ import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query' import { fetchUserPlaylists, fetchPublicPlaylists, fetchPlaylistTracks } from './utils' import { ApiLimits } from '../../../configs/query.config' import { getApi, getUser, useJellifyLibrary } from '../../../stores' -import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client' +import { BaseItemDto, PlaylistUserPermissions, UserDto } from '@jellyfin/sdk/lib/generated-client' import { QueryKeys } from '../../../enums/query-keys' import { addPlaylistUser, getPlaylistUsers, removePlaylistUser } from './utils/users' -import { ONE_MINUTE } from '../../../constants/query-client' +import { ONE_MINUTE, queryClient } from '../../../constants/query-client' +import { User } from '@sentry/react-native' +import { triggerHaptic } from '@/src/hooks/use-haptic-feedback' +import { previous } from '@/src/hooks/player/functions/controls' +import { userEvent } from '@testing-library/react-native' +import Toast from 'react-native-toast-message' export const useUserPlaylists = () => { const api = getApi() @@ -73,8 +78,8 @@ export const usePlaylistUsers = (playlist: BaseItemDto) => { } interface addPlaylistUserMutation { - playlistId: string - userId: string + playlist: BaseItemDto + user: UserDto CanEdit: boolean } @@ -84,18 +89,53 @@ export const useAddPlaylistUser = () => { return useMutation({ //playlistId: string, userId: string, CanEdit: boolean mutationFn: (variables: addPlaylistUserMutation) => - addPlaylistUser(variables.playlistId, variables.userId, variables.CanEdit), + addPlaylistUser(variables.playlist.Id!, variables.user.Id!, variables.CanEdit), + + onSuccess: (data, variables) => { + triggerHaptic('notificationSuccess') + queryClient.setQueryData( + PlaylistUsersQueryKey(variables.playlist), + (previous: PlaylistUserPermissions[] | undefined) => { + if (previous == undefined) { + //return + return [{ userId: variables.user.Id, canEdit: true }] + } else { + return [...previous, { userId: variables.user.Id, canEdit: true }] + } + }, + ) + }, + + onError: (error, variables) => { + console.log(error) + Toast.show({ type: 'error', text1: 'Unable to add user to playlist.' }) + }, }) } interface removePlaylistUser { - playlistId: string - userId: string + playlist: BaseItemDto + user: UserDto } +//remove user as playlist collaborator export const useRemovePlaylistUser = () => { return useMutation({ mutationFn: (variables: removePlaylistUser) => - removePlaylistUser(variables.playlistId, variables.userId), + removePlaylistUser(variables.playlist.Id!, variables.user.Id!), + onSuccess: (data, variables) => { + triggerHaptic('notificationSuccess') + queryClient.setQueryData( + PlaylistUsersQueryKey(variables.playlist), + (previous: PlaylistUserPermissions[] | undefined) => { + if (previous == undefined) { + //return + return [] + } else { + return previous.filter((user) => user.UserId != variables.user.Id) + } + }, + ) + }, }) } diff --git a/src/api/queries/users/index.ts b/src/api/queries/users/index.ts index d3e3c2ec9..642b21728 100644 --- a/src/api/queries/users/index.ts +++ b/src/api/queries/users/index.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { UserQueryKey } from './keys' -import { getApi } from '../../../stores' +import { getApi, getUser } from '../../../stores' import { getUserApi } from '@jellyfin/sdk/lib/utils/api' //hook to get users on server @@ -14,11 +14,17 @@ const fetchUsers = async () => { //use api (only get api when this function is called to get users) const api = getApi() + //get owner of playlist (self) + const owner = getUser() + //check set if (!api) { throw new Error('API Instance not set') } + const usersResponse = await getUserApi(api).getUsers() + + //return users where there isn't a user with owner id in array //return users from api - return (await getUserApi(api).getUsers()).data + return usersResponse.data.filter((user) => user.Id != owner?.id) } diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index 9e7322420..db1e78a93 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -1,7 +1,11 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import LibraryStackParamList, { LibraryAddPlaylistUsers } from './types' import { Paragraph, Text, View, XStack, YStack } from 'tamagui' -import { usePlaylistUsers } from '../../../src/api/queries/playlist' +import { + useAddPlaylistUser, + usePlaylistUsers, + useRemovePlaylistUser, +} from '../../../src/api/queries/playlist' import { useUsers } from '../../../src/api/queries/users' import ItemImage from '../../../src/components/Global/components/image' import TextTicker from 'react-native-text-ticker' @@ -23,6 +27,12 @@ export default function addPlaylistUsers({ } = usePlaylistUsers(playlist) //make this playlist an easy access variable (with const variable above) const { data: users, isPending: useUsersIsPending, refetch: refetchUseUsers } = useUsers() + //invoke mutations on icon press + //add + const addUser = useAddPlaylistUser() + //remove + const removeUser = useRemovePlaylistUser() + //get string array of all playlist user IDs const playlistUserIds = playlistUsers?.map((playlistUser) => playlistUser.UserId) ?? [] @@ -89,10 +99,27 @@ export default function addPlaylistUsers({ imageOptions={{ maxWidth: 85, maxHeight: 85, quality: 90 }} /> {user.Name ?? 'Unknown User'} - {playlistUserIds.includes(user.Id) ? ( - + {playlistUserIds.includes(user.Id) ? ( //send playlist id and user id (with bang! because it likely won't be undefined) + + removeUser.mutate({ playlist: playlist, user: user }) + } + name='account-remove' + color='$warning' + /> ) : ( - + //same stuff and canEdit as true bcs you know anyone you're sharing with + + addUser.mutate({ + playlist: playlist, + user: user, + CanEdit: true, + }) + } + name='account-plus' + color='$borderColor' + /> )} )} diff --git a/src/screens/Library/index.tsx b/src/screens/Library/index.tsx index b3370b13d..2602fd4ee 100644 --- a/src/screens/Library/index.tsx +++ b/src/screens/Library/index.tsx @@ -89,7 +89,7 @@ export default function LibraryScreen(): React.JSX.Element { options={{ title: 'Add Playlist Users', presentation: 'formSheet', - sheetAllowedDetents: Platform.OS === 'ios' ? 'fitToContents' : [0.5], + sheetAllowedDetents: Platform.OS === 'ios' ? 'fitToContents' : [1.0], //screen full size }} /> From 6f5c8ed37d4fd9eca351bceb7e9bd14bc620dcb9 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Mon, 30 Mar 2026 20:05:09 -0500 Subject: [PATCH 09/36] adding button to allow share screen to swipe up --- src/components/Playlist/index.tsx | 5 +++++ src/screens/Library/add-playlist-users.tsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/Playlist/index.tsx b/src/components/Playlist/index.tsx index c536a7bc9..464fae701 100644 --- a/src/components/Playlist/index.tsx +++ b/src/components/Playlist/index.tsx @@ -36,6 +36,7 @@ import useAddToPendingDownloads, { useIsDownloading } from '../../stores/network import { useStorageContext } from '../../providers/Storage' import { queryClient } from '../../constants/query-client' import { PlaylistTracksQueryKey } from '../../api/queries/playlist/keys' +import { addPlaylistUser } from '@/src/api/queries/playlist/utils/users' export default function Playlist({ playlist, @@ -217,6 +218,10 @@ export default function Playlist({ navigation.setOptions({ headerRight: () => ( + navigation.push('addPlaylistUser')} + /> {playlistTracks && !editing && downloadActions} {canEdit && ( diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index db1e78a93..2c6030a77 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -98,7 +98,9 @@ export default function addPlaylistUsers({ height={'$12'} imageOptions={{ maxWidth: 85, maxHeight: 85, quality: 90 }} /> - {user.Name ?? 'Unknown User'} + + {user.Name ?? 'Unknown User'} + {playlistUserIds.includes(user.Id) ? ( //send playlist id and user id (with bang! because it likely won't be undefined) From 3a4a63a317a1741e3f549904b87fb2b6b60334fa Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Sat, 4 Apr 2026 11:45:28 -0500 Subject: [PATCH 10/36] add scaffold for getUserImageUrl --- src/utils/images/users.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/utils/images/users.ts diff --git a/src/utils/images/users.ts b/src/utils/images/users.ts new file mode 100644 index 000000000..2afcebe89 --- /dev/null +++ b/src/utils/images/users.ts @@ -0,0 +1,5 @@ +import { UserDto } from '@jellyfin/sdk/lib/generated-client' + +export default function getUserImageUrl(user: UserDto): string | undefined { + return undefined +} From 5096bd1c74ace27ea8c7cf6632bbff356a430b9a Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Sat, 4 Apr 2026 11:47:05 -0500 Subject: [PATCH 11/36] add additional scaffold --- src/utils/images/users.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/utils/images/users.ts b/src/utils/images/users.ts index 2afcebe89..f2e36dd71 100644 --- a/src/utils/images/users.ts +++ b/src/utils/images/users.ts @@ -1,5 +1,13 @@ +import { getApi } from '@/src/stores' import { UserDto } from '@jellyfin/sdk/lib/generated-client' +import { getImageApi } from '@jellyfin/sdk/lib/utils/api' export default function getUserImageUrl(user: UserDto): string | undefined { + const api = getApi() + + if (!api) return undefined + + const imageApi = getImageApi(api) + return undefined } From e9f2868d7ba803cdd6d3f4e4545cb47949092b38 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Sat, 4 Apr 2026 12:22:08 -0500 Subject: [PATCH 12/36] images for users and fixed navigation for share button --- src/components/Playlist/index.tsx | 6 ++++-- src/screens/Library/add-playlist-users.tsx | 16 +++++++++------- src/screens/Library/index.tsx | 9 --------- src/screens/Library/types.ts | 7 ------- src/screens/index.tsx | 11 +++++++++++ src/screens/types.d.ts | 6 ++++++ src/utils/images/users.ts | 6 +++--- 7 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/components/Playlist/index.tsx b/src/components/Playlist/index.tsx index 8942a901b..be929e562 100644 --- a/src/components/Playlist/index.tsx +++ b/src/components/Playlist/index.tsx @@ -2,7 +2,7 @@ import { ScrollView, Spinner, useTheme, XStack, YStack } from 'tamagui' import Track from '../Global/components/Track' import Icon from '../Global/components/icon' import { PlaylistProps } from './interfaces' -import { StackActions, useNavigation } from '@react-navigation/native' +import { CommonActions, StackActions, useNavigation } from '@react-navigation/native' import { RootStackParamList } from '../../screens/types' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import Sortable from 'react-native-sortables' @@ -223,7 +223,9 @@ export default function Playlist({ navigation.push('addPlaylistUser')} + onPress={() => + navigationRef.dispatch(StackActions.push('addPlaylistUsers')) + } /> {playlistTracks && !editing && downloadActions} {canEdit && ( diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index 2c6030a77..bdc93c825 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -1,5 +1,4 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' -import LibraryStackParamList, { LibraryAddPlaylistUsers } from './types' import { Paragraph, Text, View, XStack, YStack } from 'tamagui' import { useAddPlaylistUser, @@ -13,12 +12,15 @@ import { TextTickerConfig } from '../../../src/components/Player/component.confi import { getItemName } from '../../../src/utils/formatting/item-names' import { SectionList } from 'react-native' import Icon from '../../../src/components/Global/components/icon' +import TurboImage from 'react-native-turbo-image' +import getUserImageUrl from '@/src/utils/images/users' +import { AddPlaylistUsersProps } from '../types' //screen in react native export default function addPlaylistUsers({ navigation, route, -}: LibraryAddPlaylistUsers): React.JSX.Element { +}: AddPlaylistUsersProps): React.JSX.Element { const { playlist } = route.params const { data: playlistUsers, @@ -92,12 +94,12 @@ export default function addPlaylistUsers({ sections={playlistUserData} renderItem={({ item: user }) => ( - + {user.Name ?? 'Unknown User'} diff --git a/src/screens/Library/index.tsx b/src/screens/Library/index.tsx index b1cc151af..19455c88a 100644 --- a/src/screens/Library/index.tsx +++ b/src/screens/Library/index.tsx @@ -83,15 +83,6 @@ export default function LibraryScreen(): React.JSX.Element { sheetAllowedDetents: 'fitToContents', }} /> - diff --git a/src/screens/Library/types.ts b/src/screens/Library/types.ts index adf086a0d..2b30bf904 100644 --- a/src/screens/Library/types.ts +++ b/src/screens/Library/types.ts @@ -10,9 +10,6 @@ type LibraryStackParamList = BaseStackParamList & { DeletePlaylist: { playlist: BaseItemDto } - AddPlaylistUsers: { - playlist: BaseItemDto - } } export default LibraryStackParamList @@ -26,7 +23,3 @@ export type LibraryDeletePlaylistProps = NativeStackScreenProps< LibraryStackParamList, 'DeletePlaylist' > -export type LibraryAddPlaylistUsers = NativeStackScreenProps< - LibraryStackParamList, - 'AddPlaylistUsers' -> diff --git a/src/screens/index.tsx b/src/screens/index.tsx index 64ebba981..87580bd36 100644 --- a/src/screens/index.tsx +++ b/src/screens/index.tsx @@ -20,6 +20,7 @@ import SortOptionsSheet from './SortOptions' import GenreSelectionScreen from './GenreSelection' import YearSelectionScreen from './YearSelection' import MigrateDownloadsScreen from './MigrateDownloads' +import addPlaylistUsers from './Library/add-playlist-users' const RootStack = createNativeStackNavigator() @@ -154,6 +155,16 @@ export default function Root(): React.JSX.Element { headerShown: false, }} /> + + ) } diff --git a/src/screens/types.d.ts b/src/screens/types.d.ts index 73c8e7797..edd3acc62 100644 --- a/src/screens/types.d.ts +++ b/src/screens/types.d.ts @@ -89,6 +89,10 @@ export type RootStackParamList = { } MigrateDownloads: undefined + + AddPlaylistUsers: { + playlist: BaseItemDto + } } export type LoginProps = NativeStackNavigationProp @@ -112,3 +116,5 @@ export type GenresProps = { isPending: boolean isFetchingNextPage: boolean } + +export type AddPlaylistUsersProps = NativeStackScreenProps diff --git a/src/utils/images/users.ts b/src/utils/images/users.ts index f2e36dd71..d34daa4ce 100644 --- a/src/utils/images/users.ts +++ b/src/utils/images/users.ts @@ -2,12 +2,12 @@ import { getApi } from '@/src/stores' import { UserDto } from '@jellyfin/sdk/lib/generated-client' import { getImageApi } from '@jellyfin/sdk/lib/utils/api' -export default function getUserImageUrl(user: UserDto): string | undefined { +export default function getUserImageUrl(user: UserDto): string { const api = getApi() - if (!api) return undefined + if (!api) return '' const imageApi = getImageApi(api) - return undefined + return imageApi.getUserImageUrl({ Id: user.Id }) ?? '' } From 8f73a8ce2fa7c2e640e066358ad67f211c360fea Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Sat, 4 Apr 2026 12:24:30 -0500 Subject: [PATCH 13/36] fix jest --- src/screens/Library/add-playlist-users.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index bdc93c825..83b6fd81e 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -1,5 +1,4 @@ -import { NativeStackNavigationProp } from '@react-navigation/native-stack' -import { Paragraph, Text, View, XStack, YStack } from 'tamagui' +import { Paragraph, View, XStack, YStack } from 'tamagui' import { useAddPlaylistUser, usePlaylistUsers, @@ -13,7 +12,7 @@ import { getItemName } from '../../../src/utils/formatting/item-names' import { SectionList } from 'react-native' import Icon from '../../../src/components/Global/components/icon' import TurboImage from 'react-native-turbo-image' -import getUserImageUrl from '@/src/utils/images/users' +import getUserImageUrl from '../../utils/images/users' import { AddPlaylistUsersProps } from '../types' //screen in react native From 4cad4a0e99c355a358a34cae9f1a2ad91bf299f1 Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Sat, 4 Apr 2026 12:33:24 -0500 Subject: [PATCH 14/36] fix imports --- src/utils/images/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/images/users.ts b/src/utils/images/users.ts index d34daa4ce..5cfb07359 100644 --- a/src/utils/images/users.ts +++ b/src/utils/images/users.ts @@ -1,4 +1,4 @@ -import { getApi } from '@/src/stores' +import { getApi } from '../../stores' import { UserDto } from '@jellyfin/sdk/lib/generated-client' import { getImageApi } from '@jellyfin/sdk/lib/utils/api' From 174857426acc0129af32312396ed7e7b7b6fb30a Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Sat, 4 Apr 2026 12:54:28 -0500 Subject: [PATCH 15/36] update one letter and create unit tests --- src/components/Playlist/index.tsx | 2 +- src/utils/images/users.test.ts | 61 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/utils/images/users.test.ts diff --git a/src/components/Playlist/index.tsx b/src/components/Playlist/index.tsx index be929e562..d8f5f89d0 100644 --- a/src/components/Playlist/index.tsx +++ b/src/components/Playlist/index.tsx @@ -224,7 +224,7 @@ export default function Playlist({ - navigationRef.dispatch(StackActions.push('addPlaylistUsers')) + navigationRef.dispatch(StackActions.push('AddPlaylistUsers')) } /> {playlistTracks && !editing && downloadActions} diff --git a/src/utils/images/users.test.ts b/src/utils/images/users.test.ts new file mode 100644 index 000000000..8cde254d0 --- /dev/null +++ b/src/utils/images/users.test.ts @@ -0,0 +1,61 @@ +import getUserImageUrl from './users' +import { getApi } from '../../stores' +import { getImageApi } from '@jellyfin/sdk/lib/utils/api' +import { UserDto } from '@jellyfin/sdk/lib/generated-client' + +jest.mock('../../stores') +jest.mock('@jellyfin/sdk/lib/utils/api') + +const mockGetApi = getApi as jest.MockedFunction +const mockGetImageApi = getImageApi as jest.MockedFunction + +describe('getUserImageUrl', () => { + let mockUser: UserDto + + beforeEach(() => { + mockUser = { Id: 'test-user-id' } as UserDto + jest.clearAllMocks() + }) + + it('should return an empty string when getApi returns null', () => { + mockGetApi.mockReturnValue(null) + + const result = getUserImageUrl(mockUser) + + expect(result).toBe('') + expect(mockGetApi).toHaveBeenCalled() + expect(mockGetImageApi).not.toHaveBeenCalled() + }) + + it('should return the image URL when getApi returns a valid api and getUserImageUrl returns a URL', () => { + const mockApi = {} + const mockImageApi = { + getUserImageUrl: jest.fn().mockReturnValue('http://example.com/user-image.jpg'), + } + mockGetApi.mockReturnValue(mockApi) + mockGetImageApi.mockReturnValue(mockImageApi) + + const result = getUserImageUrl(mockUser) + + expect(result).toBe('http://example.com/user-image.jpg') + expect(mockGetApi).toHaveBeenCalled() + expect(mockGetImageApi).toHaveBeenCalledWith(mockApi) + expect(mockImageApi.getUserImageUrl).toHaveBeenCalledWith({ Id: mockUser.Id }) + }) + + it('should return an empty string when getUserImageUrl returns null', () => { + const mockApi = {} + const mockImageApi = { + getUserImageUrl: jest.fn().mockReturnValue(null), + } + mockGetApi.mockReturnValue(mockApi) + mockGetImageApi.mockReturnValue(mockImageApi) + + const result = getUserImageUrl(mockUser) + + expect(result).toBe('') + expect(mockGetApi).toHaveBeenCalled() + expect(mockGetImageApi).toHaveBeenCalledWith(mockApi) + expect(mockImageApi.getUserImageUrl).toHaveBeenCalledWith({ Id: mockUser.Id }) + }) +}) From f6d3fc1e38c433a87ec77e6b29ea131a7d02e043 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Sat, 4 Apr 2026 13:05:49 -0500 Subject: [PATCH 16/36] updated unit test --- src/utils/images/users.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/utils/images/users.test.ts b/src/utils/images/users.test.ts index 8cde254d0..e54f94e09 100644 --- a/src/utils/images/users.test.ts +++ b/src/utils/images/users.test.ts @@ -1,7 +1,10 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import getUserImageUrl from './users' import { getApi } from '../../stores' import { getImageApi } from '@jellyfin/sdk/lib/utils/api' import { UserDto } from '@jellyfin/sdk/lib/generated-client' +import { Api } from '@jellyfin/sdk' jest.mock('../../stores') jest.mock('@jellyfin/sdk/lib/utils/api') @@ -18,7 +21,7 @@ describe('getUserImageUrl', () => { }) it('should return an empty string when getApi returns null', () => { - mockGetApi.mockReturnValue(null) + mockGetApi.mockReturnValue(null as any) const result = getUserImageUrl(mockUser) @@ -28,10 +31,10 @@ describe('getUserImageUrl', () => { }) it('should return the image URL when getApi returns a valid api and getUserImageUrl returns a URL', () => { - const mockApi = {} + const mockApi = {} as Api const mockImageApi = { getUserImageUrl: jest.fn().mockReturnValue('http://example.com/user-image.jpg'), - } + } as any mockGetApi.mockReturnValue(mockApi) mockGetImageApi.mockReturnValue(mockImageApi) @@ -44,10 +47,10 @@ describe('getUserImageUrl', () => { }) it('should return an empty string when getUserImageUrl returns null', () => { - const mockApi = {} + const mockApi = {} as Api const mockImageApi = { getUserImageUrl: jest.fn().mockReturnValue(null), - } + } as any mockGetApi.mockReturnValue(mockApi) mockGetImageApi.mockReturnValue(mockImageApi) From 5abeeaf6ae328029dbf39e402bbe300c313bc8c0 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Sat, 4 Apr 2026 13:12:21 -0500 Subject: [PATCH 17/36] add playlist as param --- src/components/Playlist/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Playlist/index.tsx b/src/components/Playlist/index.tsx index d8f5f89d0..9200eb614 100644 --- a/src/components/Playlist/index.tsx +++ b/src/components/Playlist/index.tsx @@ -224,7 +224,9 @@ export default function Playlist({ - navigationRef.dispatch(StackActions.push('AddPlaylistUsers')) + navigationRef.dispatch( + StackActions.push('AddPlaylistUsers', { playlist }), + ) } /> {playlistTracks && !editing && downloadActions} From e388b44deb76d06297dbda94b4e3f9476ffdc1aa Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Mon, 6 Apr 2026 17:50:13 -0500 Subject: [PATCH 18/36] updating queryClient conventions/types --- src/api/queries/playlist/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/queries/playlist/index.ts b/src/api/queries/playlist/index.ts index b9e6597a4..a8d631fde 100644 --- a/src/api/queries/playlist/index.ts +++ b/src/api/queries/playlist/index.ts @@ -92,14 +92,14 @@ export const useAddPlaylistUser = () => { onSuccess: (data, variables) => { triggerHaptic('notificationSuccess') - queryClient.setQueryData( + queryClient.setQueryData( PlaylistUsersQueryKey(variables.playlist), (previous: PlaylistUserPermissions[] | undefined) => { if (previous == undefined) { //return - return [{ userId: variables.user.Id, canEdit: true }] + return [{ UserId: variables.user.Id, CanEdit: true }] } else { - return [...previous, { userId: variables.user.Id, canEdit: true }] + return [...previous, { UserId: variables.user.Id, CanEdit: true }] } }, ) @@ -124,7 +124,7 @@ export const useRemovePlaylistUser = () => { removePlaylistUser(variables.playlist.Id!, variables.user.Id!), onSuccess: (data, variables) => { triggerHaptic('notificationSuccess') - queryClient.setQueryData( + queryClient.setQueryData( PlaylistUsersQueryKey(variables.playlist), (previous: PlaylistUserPermissions[] | undefined) => { if (previous == undefined) { From 9199450aa064327a97fac74ea6850de88870e2b1 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Mon, 6 Apr 2026 18:14:54 -0500 Subject: [PATCH 19/36] inverted conditional statements for playlist user filtering --- src/screens/Library/add-playlist-users.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index 83b6fd81e..a5c8bd1a2 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -39,10 +39,10 @@ export default function addPlaylistUsers({ //if user exists in playlist already, do not display //take all users, filter any users that also appear in playlistUserIds - const otherUsers = users?.filter((user) => playlistUserIds?.includes(user.Id)) ?? [] + const otherUsers = users?.filter((user) => !playlistUserIds?.includes(user.Id)) ?? [] //any user not included in listed users will get filtered out - const usersInPlaylist = users?.filter((user) => !playlistUserIds?.includes(user.Id)) ?? [] + const usersInPlaylist = users?.filter((user) => playlistUserIds?.includes(user.Id)) ?? [] //use formatting for sections component later on const playlistUserData = [ From 4c6d67562c3a09ca464ed4d0817e381e11f94fbc Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Mon, 6 Apr 2026 18:39:17 -0500 Subject: [PATCH 20/36] section header and some formatting --- src/screens/Library/add-playlist-users.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index a5c8bd1a2..fa8ad2b92 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -14,6 +14,8 @@ import Icon from '../../../src/components/Global/components/icon' import TurboImage from 'react-native-turbo-image' import getUserImageUrl from '../../utils/images/users' import { AddPlaylistUsersProps } from '../types' +import { title } from 'node:process' +import { info } from 'node:console' //screen in react native export default function addPlaylistUsers({ @@ -59,10 +61,10 @@ export default function addPlaylistUsers({ //return component here return ( //return view that occupies full screen - + { //no conditional statement here (have to have a playlist to see this view anyways) - + {info.section.title} } renderItem={({ item: user }) => ( - + {user.Name ?? 'Unknown User'} {playlistUserIds.includes(user.Id) ? ( //send playlist id and user id (with bang! because it likely won't be undefined) From b57096db6855dbefb4543795066c95abc8c62231 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Mon, 6 Apr 2026 18:51:54 -0500 Subject: [PATCH 21/36] updated padding and marginbottom to allow full list of users to be shown --- src/screens/Library/add-playlist-users.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index fa8ad2b92..7148bdd0d 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -16,6 +16,7 @@ import getUserImageUrl from '../../utils/images/users' import { AddPlaylistUsersProps } from '../types' import { title } from 'node:process' import { info } from 'node:console' +import { useSafeAreaInsets } from 'react-native-safe-area-context' //screen in react native export default function addPlaylistUsers({ @@ -29,6 +30,7 @@ export default function addPlaylistUsers({ refetch: refetchPlaylistUser, } = usePlaylistUsers(playlist) //make this playlist an easy access variable (with const variable above) const { data: users, isPending: useUsersIsPending, refetch: refetchUseUsers } = useUsers() + const { bottom } = useSafeAreaInsets() //invoke mutations on icon press //add @@ -61,7 +63,7 @@ export default function addPlaylistUsers({ //return component here return ( //return view that occupies full screen - + { //no conditional statement here (have to have a playlist to see this view anyways) From c0290bdd3adc09ad018cfcc93ec7bebe9c05f144 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Mon, 6 Apr 2026 18:59:48 -0500 Subject: [PATCH 22/36] section header --- src/screens/Library/add-playlist-users.tsx | 26 ---------------- src/screens/index.tsx | 35 ++++++++++++++++++++-- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index 7148bdd0d..201293200 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -64,32 +64,6 @@ export default function addPlaylistUsers({ return ( //return view that occupies full screen - { - //no conditional statement here (have to have a playlist to see this view anyways) - - - - - - - {getItemName(playlist)} - - - - {/* - - {`${(source ?? tracks[0])!.ArtistItems?.map((artist) => getItemName(artist)).join(', ')}`} - - */} - - - } - {/* conditional in react - only render if some variable meet criteria */} { //list of users and section list diff --git a/src/screens/index.tsx b/src/screens/index.tsx index 87580bd36..89e153544 100644 --- a/src/screens/index.tsx +++ b/src/screens/index.tsx @@ -1,7 +1,7 @@ import Player from './Player' import Tabs from './Tabs' import { RootStackParamList } from './types' -import { Paragraph, useTheme, YStack } from 'tamagui' +import { Paragraph, useTheme, XStack, YStack } from 'tamagui' import Login from './Login' import { createNativeStackNavigator } from '@react-navigation/native-stack' import Context from './Context' @@ -21,6 +21,7 @@ import GenreSelectionScreen from './GenreSelection' import YearSelectionScreen from './YearSelection' import MigrateDownloadsScreen from './MigrateDownloads' import addPlaylistUsers from './Library/add-playlist-users' +import ItemImage from '../components/Global/components/image' const RootStack = createNativeStackNavigator() @@ -159,11 +160,12 @@ export default function Root(): React.JSX.Element { ({ title: 'Add Playlist Users', presentation: 'formSheet', sheetAllowedDetents: Platform.OS === 'ios' ? 'fitToContents' : [1.0], //screen full size - }} + header: () => addPlaylistUsersHeader(route.params.playlist), + })} /> ) @@ -188,3 +190,30 @@ function ContextSheetHeader(item: BaseItemDto): React.JSX.Element { ) } + +function addPlaylistUsersHeader(playlist: BaseItemDto): React.JSX.Element { + return ( + + + + + + + {getItemName(playlist)} + + + + {/* + + {`${(source ?? tracks[0])!.ArtistItems?.map((artist) => getItemName(artist)).join(', ')}`} + + */} + + + ) +} From a98595a9e9481a19dde8787a2650bb342fea6a06 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Mon, 6 Apr 2026 19:05:08 -0500 Subject: [PATCH 23/36] enable nestedScrolling --- src/screens/Library/add-playlist-users.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index 201293200..32028ad70 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -68,6 +68,7 @@ export default function addPlaylistUsers({ { //list of users and section list {info.section.title} } renderItem={({ item: user }) => ( From ab607c79e465a077678d376e7b6019645b1ee37d Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Mon, 6 Apr 2026 19:09:32 -0500 Subject: [PATCH 24/36] updated more margins, added title --- src/screens/index.tsx | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/screens/index.tsx b/src/screens/index.tsx index 89e153544..e05706824 100644 --- a/src/screens/index.tsx +++ b/src/screens/index.tsx @@ -193,27 +193,26 @@ function ContextSheetHeader(item: BaseItemDto): React.JSX.Element { function addPlaylistUsersHeader(playlist: BaseItemDto): React.JSX.Element { return ( - - - - - - - {getItemName(playlist)} - - - - {/* - - {`${(source ?? tracks[0])!.ArtistItems?.map((artist) => getItemName(artist)).join(', ')}`} - - */} - - + + + Add Users to Playlist + + + + + + + + {getItemName(playlist)} + + + + + ) } From fc9cd09662ecba16134a4ed58f50416f3bc6bdc9 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Mon, 6 Apr 2026 19:10:54 -0500 Subject: [PATCH 25/36] lock file --- bun.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bun.lock b/bun.lock index 174687519..41a19f903 100644 --- a/bun.lock +++ b/bun.lock @@ -19,9 +19,9 @@ "@shopify/flash-list": "2.3.1", "@tamagui/animations-reanimated": "2.0.0-rc.34", "@tamagui/config": "2.0.0-rc.34", - "@tanstack/query-async-storage-persister": "5.95.2", - "@tanstack/react-query": "5.95.2", - "@tanstack/react-query-persist-client": "5.95.2", + "@tanstack/query-async-storage-persister": "5.96.2", + "@tanstack/react-query": "5.96.2", + "@tanstack/react-query-persist-client": "5.96.2", "@testing-library/react-native": "13.3.3", "@typedigital/telemetrydeck-react": "0.4.1", "axios": "1.13.6", @@ -905,15 +905,15 @@ "@tamagui/z-index-stack": ["@tamagui/z-index-stack@2.0.0-rc.34", "", { "peerDependencies": { "react": ">=19" } }, "sha512-aUmYEWfLhDv82BTrHvRF1UsfmkeDSjjWNZU7X9994fiOShF2KEXCzZybR8xo8ZGqyYhJdLlrXhbypoCMrdMxyw=="], - "@tanstack/query-async-storage-persister": ["@tanstack/query-async-storage-persister@5.95.2", "", { "dependencies": { "@tanstack/query-core": "5.95.2", "@tanstack/query-persist-client-core": "5.95.2" } }, "sha512-ZhPIHH8J833OVZhEWwwdOk0uhY94d9Wgdnq97JoQx4Ui4xx4Dh6e7WPUrjlUWo88Yqi4Ij+T1o/VR7Vlbnkbjw=="], + "@tanstack/query-async-storage-persister": ["@tanstack/query-async-storage-persister@5.96.2", "", { "dependencies": { "@tanstack/query-core": "5.96.2", "@tanstack/query-persist-client-core": "5.96.2" } }, "sha512-lYJm+TwzOEUVkxCJapLSzRXPzmPpv7Vy3zSB1RXYQ6+vznEgXBqLjn+ZwBRvHpkRda9VXis64wv44rPIi9nCwg=="], - "@tanstack/query-core": ["@tanstack/query-core@5.95.2", "", {}, "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ=="], + "@tanstack/query-core": ["@tanstack/query-core@5.96.2", "", {}, "sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA=="], - "@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.95.2", "", { "dependencies": { "@tanstack/query-core": "5.95.2" } }, "sha512-Opfj34WZ594YXpEcZEs8WBiyPGrjrKlGILfk/Ss283uwWQ36C5nX3tRY/bBiXmM82KWauUuNvahwGwiyco/8cQ=="], + "@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.96.2", "", { "dependencies": { "@tanstack/query-core": "5.96.2" } }, "sha512-BYsP8folbvxzZsNnWJxSenEAdepGNfv809150U78D84yt/THi33EwfUCcdKWFbma5XKwlaFQGWMJKeWnVJ6GVA=="], - "@tanstack/react-query": ["@tanstack/react-query@5.95.2", "", { "dependencies": { "@tanstack/query-core": "5.95.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA=="], + "@tanstack/react-query": ["@tanstack/react-query@5.96.2", "", { "dependencies": { "@tanstack/query-core": "5.96.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA=="], - "@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.95.2", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.95.2" }, "peerDependencies": { "@tanstack/react-query": "^5.95.2", "react": "^18 || ^19" } }, "sha512-i3fvzD8gaLgQyFvRc/+iSUr60aL31tMN+5QM11zdPRg0K9CirIQjHD7WgXFBnD29KJDvcjcv7OrIBaPwZ+H9xw=="], + "@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.96.2", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.96.2" }, "peerDependencies": { "@tanstack/react-query": "^5.96.2", "react": "^18 || ^19" } }, "sha512-smQ38oVPlnvkG+G7R60IAD9X6azJLRjHEd7twml9XBLYM31ncPDP0tUKy/Gv/4ItVmKTtjZ5VabXpVZxnaWSww=="], "@telemetrydeck/sdk": ["@telemetrydeck/sdk@2.0.4", "", {}, "sha512-x4S83AqSo6wvLJ6nRYdyJEqd9qmblUdBgsTRrjH5z++b9pnf2NMc8NpVAa48KIB1pRuP/GTGzXxVYdNoie/DVg=="], From ffd98c5cc3c11bef05c45dc98025cb65516c373b Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:23:23 -0500 Subject: [PATCH 26/36] sorry aria --- src/screens/index.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/screens/index.tsx b/src/screens/index.tsx index 390a61114..d5f8139ee 100644 --- a/src/screens/index.tsx +++ b/src/screens/index.tsx @@ -21,7 +21,11 @@ import YearSelectionScreen from './YearSelection' import MigrateDownloadsScreen from './MigrateDownloads' import addPlaylistUsers from './Library/add-playlist-users' import ItemImage from '../components/Global/components/image' -import { bottomSheetPresentation, playerSheetPresentation } from '../utils/navigating/form-sheet' +import { + bottomSheetPresentation, + canUseFormSheet, + playerSheetPresentation, +} from '../utils/navigating/form-sheet' const RootStack = createNativeStackNavigator() @@ -164,8 +168,8 @@ export default function Root(): React.JSX.Element { component={addPlaylistUsers} options={({ route }) => ({ title: 'Add Playlist Users', - presentation: 'formSheet', - sheetAllowedDetents: Platform.OS === 'ios' ? 'fitToContents' : [1.0], //screen full size + presentation: bottomSheetPresentation, + sheetAllowedDetents: canUseFormSheet ? 'fitToContents' : undefined, //screen full size header: () => addPlaylistUsersHeader(route.params.playlist), })} /> From 5c6d750f472810b9827293b6b307703743ceedf5 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Sun, 19 Apr 2026 18:33:01 -0500 Subject: [PATCH 27/36] restrict playlist share button to playlist owner --- src/components/Playlist/index.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/Playlist/index.tsx b/src/components/Playlist/index.tsx index 531e885f9..a83274650 100644 --- a/src/components/Playlist/index.tsx +++ b/src/components/Playlist/index.tsx @@ -221,14 +221,16 @@ export default function Playlist({ navigation.setOptions({ headerRight: () => ( - - navigationRef.dispatch( - StackActions.push('AddPlaylistUsers', { playlist }), - ) - } - /> + {playlist.canDelete && ( + + navigationRef.dispatch( + StackActions.push('AddPlaylistUsers', { playlist }), + ) + } + /> + )} {playlistTracks && !editing && downloadActions} {canEdit && ( From 8c85d0b805bc22a6bcf4ee3b8c7f18c559ebd24b Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Sun, 19 Apr 2026 18:33:33 -0500 Subject: [PATCH 28/36] fixed typo --- src/components/Playlist/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Playlist/index.tsx b/src/components/Playlist/index.tsx index a83274650..7a12eaf44 100644 --- a/src/components/Playlist/index.tsx +++ b/src/components/Playlist/index.tsx @@ -221,7 +221,7 @@ export default function Playlist({ navigation.setOptions({ headerRight: () => ( - {playlist.canDelete && ( + {playlist.CanDelete && ( From d95902a855730c38421ff92dc474c8b1f1d6bb33 Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:54:27 -0500 Subject: [PATCH 29/36] fix issue where the userId wasn't set when creating a playlist --- src/api/mutations/playlists.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/mutations/playlists.ts b/src/api/mutations/playlists.ts index 6fd6c2de9..cb5f1bb09 100644 --- a/src/api/mutations/playlists.ts +++ b/src/api/mutations/playlists.ts @@ -183,6 +183,7 @@ export async function createPlaylist( Name: name, IsPublic: false, MediaType: MediaType.Audio, + UserId: user.id, }, }) .then(() => { From 7ac905d31f9277410d5a631aee808dcedb52c708 Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:05:55 -0500 Subject: [PATCH 30/36] fix: add playlist users screen presentation --- src/screens/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/screens/index.tsx b/src/screens/index.tsx index d5f8139ee..159ccefd2 100644 --- a/src/screens/index.tsx +++ b/src/screens/index.tsx @@ -168,8 +168,7 @@ export default function Root(): React.JSX.Element { component={addPlaylistUsers} options={({ route }) => ({ title: 'Add Playlist Users', - presentation: bottomSheetPresentation, - sheetAllowedDetents: canUseFormSheet ? 'fitToContents' : undefined, //screen full size + presentation: 'modal', header: () => addPlaylistUsersHeader(route.params.playlist), })} /> From 25b0f9b068270a7da10ef627441ed86888ff6819 Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:19:22 -0500 Subject: [PATCH 31/36] restore swipe to dismiss on android --- src/screens/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/screens/index.tsx b/src/screens/index.tsx index 159ccefd2..11a1baf7f 100644 --- a/src/screens/index.tsx +++ b/src/screens/index.tsx @@ -168,7 +168,9 @@ export default function Root(): React.JSX.Element { component={addPlaylistUsers} options={({ route }) => ({ title: 'Add Playlist Users', - presentation: 'modal', + presentation: bottomSheetPresentation, + sheetAllowedDetents: canUseFormSheet ? [1.0] : undefined, + sheetGrabberVisible: true, header: () => addPlaylistUsersHeader(route.params.playlist), })} /> From 25eb3341e4a7587c359070b9a46495ce7d82a8e3 Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Sun, 19 Apr 2026 19:31:56 -0500 Subject: [PATCH 32/36] search bar, baby! --- src/screens/Library/add-playlist-users.tsx | 42 ++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/screens/Library/add-playlist-users.tsx b/src/screens/Library/add-playlist-users.tsx index 32028ad70..aa8c1d1ca 100644 --- a/src/screens/Library/add-playlist-users.tsx +++ b/src/screens/Library/add-playlist-users.tsx @@ -1,21 +1,17 @@ -import { Paragraph, View, XStack, YStack } from 'tamagui' +import { Paragraph, View, XStack } from 'tamagui' +import { useState } from 'react' +import Input from '../../components/Global/helpers/input' import { useAddPlaylistUser, usePlaylistUsers, useRemovePlaylistUser, } from '../../../src/api/queries/playlist' import { useUsers } from '../../../src/api/queries/users' -import ItemImage from '../../../src/components/Global/components/image' -import TextTicker from 'react-native-text-ticker' -import { TextTickerConfig } from '../../../src/components/Player/component.config' -import { getItemName } from '../../../src/utils/formatting/item-names' import { SectionList } from 'react-native' import Icon from '../../../src/components/Global/components/icon' import TurboImage from 'react-native-turbo-image' import getUserImageUrl from '../../utils/images/users' import { AddPlaylistUsersProps } from '../types' -import { title } from 'node:process' -import { info } from 'node:console' import { useSafeAreaInsets } from 'react-native-safe-area-context' //screen in react native @@ -23,6 +19,7 @@ export default function addPlaylistUsers({ navigation, route, }: AddPlaylistUsersProps): React.JSX.Element { + const [searchQuery, setSearchQuery] = useState('') const { playlist } = route.params const { data: playlistUsers, @@ -32,6 +29,16 @@ export default function addPlaylistUsers({ const { data: users, isPending: useUsersIsPending, refetch: refetchUseUsers } = useUsers() const { bottom } = useSafeAreaInsets() + //filter users based on search query + const filterUsersBySearch = (userList: typeof users) => { + if (!searchQuery.trim()) return userList ?? [] + return ( + userList?.filter((user) => + user.Name?.toLowerCase().includes(searchQuery.toLowerCase()), + ) ?? [] + ) + } + //invoke mutations on icon press //add const addUser = useAddPlaylistUser() @@ -48,22 +55,37 @@ export default function addPlaylistUsers({ //any user not included in listed users will get filtered out const usersInPlaylist = users?.filter((user) => playlistUserIds?.includes(user.Id)) ?? [] + //apply search filter + const filteredOtherUsers = filterUsersBySearch(otherUsers) + const filteredInPlaylistUsers = filterUsersBySearch(usersInPlaylist) + //use formatting for sections component later on const playlistUserData = [ { title: 'Shared With', - data: usersInPlaylist, + data: filteredInPlaylistUsers, }, { title: 'Users on Server', - data: otherUsers, + data: filteredOtherUsers, }, ] //return component here return ( //return view that occupies full screen - + + {/* search bar */} + + + + {/* conditional in react - only render if some variable meet criteria */} { //list of users and section list From e378f52c0435d1ec98e0fb2f40eaa581f5faaf8c Mon Sep 17 00:00:00 2001 From: arijohn723 Date: Sun, 19 Apr 2026 19:46:53 -0500 Subject: [PATCH 33/36] unit testing --- jest/functional/playlist-users.test.ts | 299 +++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 jest/functional/playlist-users.test.ts diff --git a/jest/functional/playlist-users.test.ts b/jest/functional/playlist-users.test.ts new file mode 100644 index 000000000..756c3c42e --- /dev/null +++ b/jest/functional/playlist-users.test.ts @@ -0,0 +1,299 @@ +import { getApi } from '../../src/stores' +import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api' +import { + addPlaylistUser, + getPlaylistUsers, + removePlaylistUser, +} from '../../src/api/queries/playlist/utils/users' +import { BaseItemDto, PlaylistUserPermissions, UserDto } from '@jellyfin/sdk/lib/generated-client' + +jest.mock('../../src/stores') +jest.mock('@jellyfin/sdk/lib/utils/api') + +describe('Playlist Users API Functions', () => { + const mockPlaylistId = 'playlist-123' + const mockUserId = 'user-456' + const mockApi = { basePath: 'http://test' } + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('getPlaylistUsers', () => { + it('fetches playlist users successfully', async () => { + const mockUsers: PlaylistUserPermissions[] = [ + { UserId: 'user-1', CanEdit: true }, + { UserId: 'user-2', CanEdit: false }, + ] + + const mockPlaylistApi = { + getPlaylistUsers: jest.fn().mockResolvedValue({ data: mockUsers }), + } + + ;(getApi as jest.Mock).mockReturnValue(mockApi) + ;(getPlaylistsApi as jest.Mock).mockReturnValue(mockPlaylistApi) + + const result = await getPlaylistUsers(mockPlaylistId) + + expect(result).toEqual(mockUsers) + expect(getPlaylistsApi).toHaveBeenCalledWith(mockApi) + expect(mockPlaylistApi.getPlaylistUsers).toHaveBeenCalledWith({ + playlistId: mockPlaylistId, + }) + }) + + it('throws error when API instance is not set', async () => { + ;(getApi as jest.Mock).mockReturnValue(null) + + await expect(getPlaylistUsers(mockPlaylistId)).rejects.toThrow('API Instance not set') + }) + }) + + describe('addPlaylistUser', () => { + it('adds a user to playlist with correct permissions', async () => { + const mockPlaylistApi = { + updatePlaylist: jest.fn().mockResolvedValue({}), + } + + ;(getApi as jest.Mock).mockReturnValue(mockApi) + ;(getPlaylistsApi as jest.Mock).mockReturnValue(mockPlaylistApi) + + await addPlaylistUser(mockPlaylistId, mockUserId, true) + + expect(mockPlaylistApi.updatePlaylist).toHaveBeenCalledWith({ + playlistId: mockPlaylistId, + updatePlaylistDto: { + Users: [ + { + UserId: mockUserId, + CanEdit: true, + }, + ], + }, + }) + }) + + it('adds a user to playlist with read-only permission', async () => { + const mockPlaylistApi = { + updatePlaylist: jest.fn().mockResolvedValue({}), + } + + ;(getApi as jest.Mock).mockReturnValue(mockApi) + ;(getPlaylistsApi as jest.Mock).mockReturnValue(mockPlaylistApi) + + await addPlaylistUser(mockPlaylistId, mockUserId, false) + + expect(mockPlaylistApi.updatePlaylist).toHaveBeenCalledWith({ + playlistId: mockPlaylistId, + updatePlaylistDto: { + Users: [ + { + UserId: mockUserId, + CanEdit: false, + }, + ], + }, + }) + }) + }) + + describe('removePlaylistUser', () => { + it('removes a user from playlist', async () => { + const mockPlaylistApi = { + removeUserFromPlaylist: jest.fn().mockResolvedValue({}), + } + + ;(getApi as jest.Mock).mockReturnValue(mockApi) + ;(getPlaylistsApi as jest.Mock).mockReturnValue(mockPlaylistApi) + + await removePlaylistUser(mockPlaylistId, mockUserId) + + expect(mockPlaylistApi.removeUserFromPlaylist).toHaveBeenCalledWith({ + playlistId: mockPlaylistId, + userId: mockUserId, + }) + }) + }) +}) + +describe('Playlist Users Query Client Updates', () => { + const mockPlaylist: BaseItemDto = { + Id: 'playlist-123', + Name: 'Test Playlist', + Type: 'Playlist', + } + + const mockUser: UserDto = { + Id: 'user-456', + Name: 'Test User', + } + + describe('useAddPlaylistUser onSuccess', () => { + it('should add new user to empty playlist users cache', () => { + const previousData: PlaylistUserPermissions[] | undefined = undefined + const newUser: PlaylistUserPermissions = { + UserId: mockUser.Id, + CanEdit: true, + } + + // Simulate the query update function + const updateFn = (previous: PlaylistUserPermissions[] | undefined) => { + if (previous == undefined) { + return [newUser] + } else { + return [...previous, newUser] + } + } + + const result = updateFn(previousData) + expect(result).toEqual([newUser]) + expect(result).toHaveLength(1) + }) + + it('should add new user to existing playlist users list', () => { + const existingUser: PlaylistUserPermissions = { + UserId: 'user-existing', + CanEdit: true, + } + const previousData: PlaylistUserPermissions[] = [existingUser] + const newUser: PlaylistUserPermissions = { + UserId: mockUser.Id, + CanEdit: false, + } + + // Simulate the query update function + const updateFn = (previous: PlaylistUserPermissions[] | undefined) => { + if (previous == undefined) { + return [newUser] + } else { + return [...previous, newUser] + } + } + + const result = updateFn(previousData) + expect(result).toEqual([existingUser, newUser]) + expect(result).toHaveLength(2) + }) + + it('should respect CanEdit permission when adding user', () => { + const previousData: PlaylistUserPermissions[] | undefined = undefined + const canEditValue = false + const newUser: PlaylistUserPermissions = { + UserId: mockUser.Id, + CanEdit: canEditValue, + } + + // Simulate the query update function + const updateFn = (previous: PlaylistUserPermissions[] | undefined) => { + if (previous == undefined) { + return [newUser] + } else { + return [...previous, newUser] + } + } + + const result = updateFn(previousData) + expect(result[0].CanEdit).toBe(false) + }) + }) + + describe('useRemovePlaylistUser onSuccess', () => { + it('should remove user from playlist users cache', () => { + const userToRemove = 'user-456' + const previousData: PlaylistUserPermissions[] = [ + { UserId: 'user-1', CanEdit: true }, + { UserId: userToRemove, CanEdit: false }, + { UserId: 'user-3', CanEdit: true }, + ] + + // Simulate the query update function + const updateFn = (previous: PlaylistUserPermissions[] | undefined) => { + if (previous == undefined) { + return [] + } else { + return previous.filter((user) => user.UserId != userToRemove) + } + } + + const result = updateFn(previousData) + expect(result).toEqual([ + { UserId: 'user-1', CanEdit: true }, + { UserId: 'user-3', CanEdit: true }, + ]) + expect(result).toHaveLength(2) + }) + + it('should handle removing from empty list', () => { + const userToRemove = 'user-456' + const previousData: PlaylistUserPermissions[] | undefined = undefined + + // Simulate the query update function + const updateFn = (previous: PlaylistUserPermissions[] | undefined) => { + if (previous == undefined) { + return [] + } else { + return previous.filter((user) => user.UserId != userToRemove) + } + } + + const result = updateFn(previousData) + expect(result).toEqual([]) + expect(result).toHaveLength(0) + }) + + it('should handle removing non-existent user gracefully', () => { + const userToRemove = 'user-nonexistent' + const previousData: PlaylistUserPermissions[] = [ + { UserId: 'user-1', CanEdit: true }, + { UserId: 'user-2', CanEdit: false }, + ] + + // Simulate the query update function + const updateFn = (previous: PlaylistUserPermissions[] | undefined) => { + if (previous == undefined) { + return [] + } else { + return previous.filter((user) => user.UserId != userToRemove) + } + } + + const result = updateFn(previousData) + expect(result).toEqual(previousData) + expect(result).toHaveLength(2) + }) + }) + + describe('Mutation Variables Validation', () => { + it('should use correct CanEdit value from mutation variables', () => { + const variables = { + playlist: mockPlaylist, + user: mockUser, + CanEdit: false, + } + + // Simulate adding with the CanEdit value from variables + const newUser: PlaylistUserPermissions = { + UserId: variables.user.Id, + CanEdit: variables.CanEdit, + } + + expect(newUser.CanEdit).toBe(false) + expect(newUser.UserId).toBe(mockUser.Id) + }) + + it('should maintain user ID from variables in cache', () => { + const variables = { + playlist: mockPlaylist, + user: mockUser, + CanEdit: true, + } + + const newUser: PlaylistUserPermissions = { + UserId: variables.user.Id, + CanEdit: variables.CanEdit, + } + + expect(newUser.UserId).toBe('user-456') + }) + }) +}) From 6be0cc20a9c0488644e759b8c9940212d2746c32 Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Wed, 22 Apr 2026 06:49:21 -0500 Subject: [PATCH 34/36] add post-merge husky hook --- .husky/post-merge | 5 +++++ 1 file changed, 5 insertions(+) create mode 100755 .husky/post-merge diff --git a/.husky/post-merge b/.husky/post-merge new file mode 100755 index 000000000..0b2a3a1fd --- /dev/null +++ b/.husky/post-merge @@ -0,0 +1,5 @@ +if git diff-tree --no-commit-id -r --name-only ORIG_HEAD HEAD | grep -qE "package\.json|bun\.lockb"; then + echo "📦 Dependencies changed, running module install..." + bun i + echo "✅ Module install completed, ready to rock 🪨!" +fi From 61ed1fe0a247958515e490c1a479380d3897894b Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Sat, 2 May 2026 08:32:52 -0500 Subject: [PATCH 35/36] apply prettier --- src/screens/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/index.tsx b/src/screens/index.tsx index e83211e03..3f89b8d91 100644 --- a/src/screens/index.tsx +++ b/src/screens/index.tsx @@ -24,7 +24,7 @@ import ItemImage from '../components/Global/components/image' import { addToPlaylistSheetPresentation, bottomSheetPresentation, - canUseFormSheet, + canUseFormSheet, playerSheetPresentation, } from '../utils/navigating/form-sheet' From efa2bfb00979c7cbde24dba1b04ab13b75e74cb4 Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Fri, 22 May 2026 07:03:13 -0500 Subject: [PATCH 36/36] Fix build --- jest/functional/playlist-users.test.ts | 2 +- src/api/queries/playlist/utils/users.ts | 2 +- src/api/queries/users/index.ts | 2 +- src/utils/images/users.test.ts | 2 +- src/utils/images/users.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jest/functional/playlist-users.test.ts b/jest/functional/playlist-users.test.ts index 756c3c42e..787709c18 100644 --- a/jest/functional/playlist-users.test.ts +++ b/jest/functional/playlist-users.test.ts @@ -1,4 +1,4 @@ -import { getApi } from '../../src/stores' +import { getApi } from '../../src/stores/auth/utils' import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api' import { addPlaylistUser, diff --git a/src/api/queries/playlist/utils/users.ts b/src/api/queries/playlist/utils/users.ts index 8e59f5330..3ff7bbfa6 100644 --- a/src/api/queries/playlist/utils/users.ts +++ b/src/api/queries/playlist/utils/users.ts @@ -1,6 +1,6 @@ //playlist id -import { getApi, getUser } from '../../../../stores' +import { getApi } from '../../../../stores/auth/utils' import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api' //get playlist users diff --git a/src/api/queries/users/index.ts b/src/api/queries/users/index.ts index 642b21728..86fa81833 100644 --- a/src/api/queries/users/index.ts +++ b/src/api/queries/users/index.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { UserQueryKey } from './keys' -import { getApi, getUser } from '../../../stores' +import { getApi, getUser } from '../../../stores/auth/utils' import { getUserApi } from '@jellyfin/sdk/lib/utils/api' //hook to get users on server diff --git a/src/utils/images/users.test.ts b/src/utils/images/users.test.ts index e54f94e09..3dba1c6d8 100644 --- a/src/utils/images/users.test.ts +++ b/src/utils/images/users.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import getUserImageUrl from './users' -import { getApi } from '../../stores' +import { getApi } from '../../stores/auth/utils' import { getImageApi } from '@jellyfin/sdk/lib/utils/api' import { UserDto } from '@jellyfin/sdk/lib/generated-client' import { Api } from '@jellyfin/sdk' diff --git a/src/utils/images/users.ts b/src/utils/images/users.ts index 5cfb07359..bebb2ac1d 100644 --- a/src/utils/images/users.ts +++ b/src/utils/images/users.ts @@ -1,4 +1,4 @@ -import { getApi } from '../../stores' +import { getApi } from '../../stores/auth/utils' import { UserDto } from '@jellyfin/sdk/lib/generated-client' import { getImageApi } from '@jellyfin/sdk/lib/utils/api'