diff --git a/src/api_client/albums/user.ts b/src/api_client/albums/user.ts index 68b25448..22ddb29e 100644 --- a/src/api_client/albums/user.ts +++ b/src/api_client/albums/user.ts @@ -6,18 +6,18 @@ import { DatePhotosGroupSchema, SimpleUserSchema } from "../../actions/photosAct import i18n from "../../i18n"; import { api } from "../api"; -const UserAlbumListSchema = z - .object({ - id: z.number(), - title: z.string(), - cover_photo: PhotoSuperSimpleSchema, - photo_count: z.number(), - owner: SimpleUserSchema, - shared_to: SimpleUserSchema.array(), - created_on: z.string(), - favorited: z.boolean(), - }) - .array(); +const UserAlbumResponseSchema = z.object({ + id: z.number(), + title: z.string(), + cover_photo: PhotoSuperSimpleSchema, + photo_count: z.number(), + owner: SimpleUserSchema, + shared_to: SimpleUserSchema.array(), + created_on: z.string(), + favorited: z.boolean(), +}); + +const UserAlbumListSchema = UserAlbumResponseSchema.array(); const UserAlbumListResponseSchema = z.object({ results: UserAlbumListSchema, @@ -82,6 +82,12 @@ type SetUserAlbumCoverParams = { photo: string; }; +type ShareUserAlbumParams = { + albumId: string; + userId: string; + share: boolean; +}; + export const userAlbumsApi = api .injectEndpoints({ endpoints: builder => ({ @@ -189,6 +195,28 @@ export const userAlbumsApi = api }); }, }), + [Endpoints.shareUserAlbum]: builder.mutation({ + query: ({ albumId, userId, share }) => ({ + url: `useralbum/share/`, + method: "POST", + body: { shared: share, album_id: albumId, target_user_id: userId }, + }), + transformResponse: (response, meta, query) => { + if (query.share) { + showNotification({ + message: i18n.t("toasts.sharingalbum"), + title: i18n.t("toasts.sharingalbumtitle"), + color: "teal", + }); + } else { + showNotification({ + message: i18n.t("toasts.unsharingalbum"), + title: i18n.t("toasts.unsharingalbumtitle"), + color: "teal", + }); + } + }, + }), }), }) .enhanceEndpoints<"UserAlbums" | "UserAlbum">({ @@ -218,11 +246,16 @@ export const userAlbumsApi = api [Endpoints.addPhotoToUserAlbum]: { invalidatesTags: ["UserAlbums", "UserAlbum"], }, + [Endpoints.shareUserAlbum]: { + // TODO(sickelap): invalidate only the album that was shared + invalidatesTags: ["UserAlbums", "UserAlbum"], + }, }, }); export const { useFetchUserAlbumsQuery, + useFetchUserAlbumQuery, useLazyFetchUserAlbumQuery, useDeleteUserAlbumMutation, useRenameUserAlbumMutation, @@ -230,4 +263,5 @@ export const { useRemovePhotoFromUserAlbumMutation, useSetUserAlbumCoverMutation, useAddPhotoToUserAlbumMutation, + useShareUserAlbumMutation, } = userAlbumsApi; diff --git a/src/api_client/api.ts b/src/api_client/api.ts index f2d52de0..4f042c8e 100644 --- a/src/api_client/api.ts +++ b/src/api_client/api.ts @@ -30,8 +30,8 @@ import type { import type { RootState } from "../store/store"; import type { IUploadOptions, IUploadResponse } from "../store/upload/upload.zod"; import { UploadExistResponse, UploadResponse } from "../store/upload/upload.zod"; -import type { IApiUserListResponse, IManageUser, IUser } from "../store/user/user.zod"; -import { ManageUser, UserSchema } from "../store/user/user.zod"; +import type { IManageUser, IUser, UserList } from "../store/user/user.zod"; +import { ApiUserListResponseSchema, ManageUser, UserSchema } from "../store/user/user.zod"; import type { ServerStatsResponseType, StorageStatsResponseType } from "../store/util/util.zod"; import type { IWorkerAvailabilityResponse } from "../store/worker/worker.zod"; // eslint-disable-next-line import/no-cycle @@ -163,11 +163,12 @@ export const api = createApi({ query: userId => `/user/${userId}/`, transformResponse: (response: string) => UserSchema.parse(response), }), - [Endpoints.fetchUserList]: builder.query({ + [Endpoints.fetchUserList]: builder.query({ query: () => ({ url: "/user/", method: "GET", }), + transformResponse: (response: string) => ApiUserListResponseSchema.parse(response).results, providesTags: ["UserList"], }), [Endpoints.uploadExists]: builder.query({ diff --git a/src/components/sharing/ModalAlbumShare.module.css b/src/components/sharing/ModalAlbumShare.module.css new file mode 100644 index 00000000..9d32458c --- /dev/null +++ b/src/components/sharing/ModalAlbumShare.module.css @@ -0,0 +1,4 @@ +.title { + font-size: 2rem; + font-weight: 600; +} diff --git a/src/components/sharing/ModalAlbumShare.tsx b/src/components/sharing/ModalAlbumShare.tsx index 4412bd6c..7ab34f74 100644 --- a/src/components/sharing/ModalAlbumShare.tsx +++ b/src/components/sharing/ModalAlbumShare.tsx @@ -1,12 +1,12 @@ import { Divider, Modal, ScrollArea, Stack, TextInput, Title } from "@mantine/core"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { fetchUserAlbum } from "../../actions/albumsActions"; -import { fetchPublicUserList } from "../../actions/publicActions"; -import { useAppDispatch, useAppSelector } from "../../store/store"; -import { fuzzyMatch } from "../../util/util"; +import { useFetchUserListQuery } from "../../api_client/api"; +import { useAppSelector } from "../../store/store"; +import classes from "./ModalAlbumShare.module.css"; import { UserEntry } from "./UserEntry"; +import filterUsers from "./utils"; type Props = { isOpen: boolean; @@ -16,34 +16,15 @@ type Props = { export function ModalAlbumShare(props: Props) { const [userNameFilter, setUserNameFilter] = useState(""); - - const { pub, auth } = useAppSelector(store => store); - - const dispatch = useAppDispatch(); + const { auth } = useAppSelector(store => store); const { t } = useTranslation(); const { isOpen, onRequestClose, albumID } = props; - - useEffect(() => { - if (isOpen) { - dispatch(fetchPublicUserList()); - dispatch(fetchUserAlbum(parseInt(albumID, 10))); - } - }, [isOpen, dispatch]); - - let filteredUserList; - if (userNameFilter.length > 0) { - filteredUserList = pub.publicUserList.filter( - el => fuzzyMatch(userNameFilter, el.username) || fuzzyMatch(userNameFilter, `${el.first_name} ${el.last_name}`) - ); - } else { - filteredUserList = pub.publicUserList; - } - filteredUserList = filteredUserList.filter(el => el.id !== auth.access.user_id); + const { data: users, isFetching: isUsersFetching, isSuccess: isUsersLoaded } = useFetchUserListQuery(); return ( {t("modalphotosshare.title")}} + title={{t("modalphotosshare.title")}} onClose={() => { onRequestClose(); setUserNameFilter(""); @@ -58,12 +39,16 @@ export function ModalAlbumShare(props: Props) { placeholder={t("modalphotosshare.name")} /> - - - {filteredUserList.length > 0 && - filteredUserList.map(item => )} - - + {isUsersFetching &&
{t("modalphotosshare.loading")}
} + {isUsersLoaded && ( + + + {filterUsers(userNameFilter, auth.access?.user_id, users).map(item => ( + + ))} + + + )}
); diff --git a/src/components/sharing/ModalPhotosShare.tsx b/src/components/sharing/ModalPhotosShare.tsx index 6fcfdc2c..5dba4f96 100644 --- a/src/components/sharing/ModalPhotosShare.tsx +++ b/src/components/sharing/ModalPhotosShare.tsx @@ -1,22 +1,16 @@ import { ActionIcon, Avatar, Divider, Group, Modal, ScrollArea, Stack, Text, TextInput, Title } from "@mantine/core"; import { DateTime } from "luxon"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Share, ShareOff } from "tabler-icons-react"; import { setPhotosShared } from "../../actions/photosActions"; -import { fetchPublicUserList } from "../../actions/publicActions"; +import { useFetchUserListQuery } from "../../api_client/api"; import { serverAddress } from "../../api_client/apiClient"; import { i18nResolvedLanguage } from "../../i18n"; import { useAppDispatch, useAppSelector } from "../../store/store"; - -function fuzzyMatch(str: string, pattern: string) { - if (pattern.split("").length > 0) { - const expr = pattern.split("").reduce((a, b) => `${a}.*${b}`); - return new RegExp(expr).test(str); - } - return false; -} +import classes from "./ModalAlbumShare.module.css"; +import filterUsers from "./utils"; type Props = { selectedImageHashes: any; @@ -27,37 +21,19 @@ type Props = { export function ModalPhotosShare(props: Props) { const { t } = useTranslation(); const [userNameFilter, setUserNameFilter] = useState(""); - - const { auth, pub } = useAppSelector(store => store); + const { data: users = [], isFetching: isUsersLoading, isSuccess: isUsersLoaded } = useFetchUserListQuery(); + const { auth } = useAppSelector(store => store); const dispatch = useAppDispatch(); - const { selectedImageHashes, isOpen, onRequestClose } = props; - let filteredUserList: any[]; - if (userNameFilter.length > 0) { - filteredUserList = pub.publicUserList.filter( - (el: any) => - fuzzyMatch(el.username.toLowerCase(), userNameFilter.toLowerCase()) || - fuzzyMatch(`${el.first_name.toLowerCase()} ${el.last_name.toLowerCase()}`, userNameFilter.toLowerCase()) - ); - } else { - filteredUserList = pub.publicUserList; - } - filteredUserList = filteredUserList.filter((el: any) => el.id !== auth.access?.user_id); const selectedImageSrcs = selectedImageHashes.map( (image_hash: string) => `${serverAddress}/media/square_thumbnails/${image_hash}` ); - useEffect(() => { - if (isOpen) { - dispatch(fetchPublicUserList()); - } - }, [isOpen, dispatch]); - return ( {t("modalphotosshare.title")}} + title={{t("modalphotosshare.title")}} onClose={() => { onRequestClose(); setUserNameFilter(""); @@ -81,10 +57,11 @@ export function ModalPhotosShare(props: Props) { /> - - - {filteredUserList.length > 0 && - filteredUserList.map(item => { + {isUsersLoading &&
{t("modalphotosshare.loading")}
} + {isUsersLoaded && ( + + + {filterUsers(userNameFilter, auth.access?.user_id, users).map(item => { let displayName = item.username; if (item.first_name.length > 0 && item.last_name.length > 0) { displayName = `${item.first_name} ${item.last_name}`; @@ -123,8 +100,9 @@ export function ModalPhotosShare(props: Props) { ); })} - - +
+
+ )}
); diff --git a/src/components/sharing/utils.ts b/src/components/sharing/utils.ts new file mode 100644 index 00000000..e19049f1 --- /dev/null +++ b/src/components/sharing/utils.ts @@ -0,0 +1,23 @@ +import { IUser } from "../../store/user/user.zod"; + +function fuzzyMatch(str: string, pattern: string) { + if (pattern.split("").length > 0) { + const expr = pattern.split("").reduce((a, b) => `${a}.*${b}`); + return new RegExp(expr).test(str); + } + return false; +} + +export default function filterUsers(username: string, excludeUserId: number, users: IUser[] = []): IUser[] { + return users + .filter(user => { + if (username.length === 0) { + return true; + } + return ( + fuzzyMatch(user.username.toLowerCase(), username.toLowerCase()) || + fuzzyMatch(`${user.first_name.toLowerCase()} ${user.last_name.toLowerCase()}`, username.toLowerCase()) + ); + }) + .filter(user => user.id !== excludeUserId); +} diff --git a/src/layouts/albums/AlbumUser.tsx b/src/layouts/albums/AlbumUser.tsx index b158cd83..e1d2444a 100644 --- a/src/layouts/albums/AlbumUser.tsx +++ b/src/layouts/albums/AlbumUser.tsx @@ -6,6 +6,7 @@ import { Link } from "react-router-dom"; import { AutoSizer, Grid } from "react-virtualized"; import { Album, DotsVertical, Edit, Share, Trash, User, Users } from "tabler-icons-react"; +import { UserAlbumInfo } from "../../actions/albumActions.types"; import { useDeleteUserAlbumMutation, useFetchUserAlbumsQuery, @@ -16,7 +17,7 @@ import { ModalAlbumShare } from "../../components/sharing/ModalAlbumShare"; import { useAlbumListGridConfig } from "../../hooks/useAlbumListGridConfig"; import { HeaderComponent } from "./HeaderComponent"; -export function SharedWith({ album }: any) { +function SharedWith({ album }: { album: UserAlbumInfo }) { const [opened, { toggle, close }] = useDisclosure(false); // To-Do: Figure out, why album is an array / json <- is it still the case? return ( @@ -27,7 +28,7 @@ export function SharedWith({ album }: any) { Shared with: - {album.shared_to.map((el: { username: string }) => ( + {album.shared_to.map(el => ( {el.username} @@ -48,7 +49,7 @@ export function AlbumUser() { const [isShareDialogOpen, { open: showShareDialog, close: hideShareDialog }] = useDisclosure(false); const { t } = useTranslation(); const { data: albums, isFetching } = useFetchUserAlbumsQuery(); - const { entriesPerRow, entrySquareSize, numberOfRows, gridHeight } = useAlbumListGridConfig(albums || []); + const { entriesPerRow, entrySquareSize, numberOfRows, gridHeight } = useAlbumListGridConfig(albums ?? []); const [deleteUserAlbum] = useDeleteUserAlbumMutation(); const [renameUserAlbum] = useRenameUserAlbumMutation(); @@ -70,6 +71,10 @@ export function AlbumUser() { setAlbumTitle(title); }; + function isShared(album: UserAlbumInfo) { + return album.shared_to.length > 0; + } + function renderCell({ columnIndex, key, rowIndex, style }) { if (!albums || albums.length === 0) { return null; @@ -115,7 +120,7 @@ export function AlbumUser() {
- {album.shared_to.length > 0 && } + {isShared(album) && } {album.title} @@ -135,7 +140,7 @@ export function AlbumUser() { title={t("myalbums")} fetching={isFetching} subtitle={t("useralbum.numberof", { - number: (albums && albums.length) || 0, + number: albums?.length ?? 0, })} /> @@ -145,8 +150,7 @@ export function AlbumUser() { el.title.toLowerCase().trim()).includes(newAlbumTitle.toLowerCase().trim()) ? ( + albums?.map(el => el.title.toLowerCase().trim()).includes(newAlbumTitle.toLowerCase().trim()) ? ( <> {t("useralbum.albumalreadyexists")}, {{ name: newAlbumTitle.trim() }} @@ -166,9 +170,7 @@ export function AlbumUser() { renameUserAlbum({ id: albumID, title: albumTitle, newTitle: newAlbumTitle }); hideRenameDialog(); }} - disabled={ - albums && albums.map(el => el.title.toLowerCase().trim()).includes(newAlbumTitle.toLowerCase().trim()) - } + disabled={albums?.map(el => el.title.toLowerCase().trim()).includes(newAlbumTitle.toLowerCase().trim())} type="submit" > {t("rename")} diff --git a/src/store/user/user.zod.ts b/src/store/user/user.zod.ts index b6c21bb0..a07f2478 100644 --- a/src/store/user/user.zod.ts +++ b/src/store/user/user.zod.ts @@ -81,7 +81,8 @@ export const ApiUserListResponseSchema = z.object({ results: z.array(UserSchema), }); -export type IApiUserListResponse = z.infer; +export const UserListSchema = z.array(UserSchema); +export type UserList = z.infer; export type IUserState = { userSelfDetails: IUser;