Skip to content

Commit

Permalink
Adds ability for mods to remove image resources
Browse files Browse the repository at this point in the history
  • Loading branch information
manuelurenah committed Apr 12, 2023
1 parent c6c6b4f commit 3e333a4
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 36 deletions.
105 changes: 85 additions & 20 deletions src/components/Image/Detail/ImageResources.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import {
ActionIcon,
Alert,
Badge,
Card,
createStyles,
CopyButton,
Group,
Rating,
Skeleton,
Stack,
Text,
Alert,
CopyButton,
createStyles,
} from '@mantine/core';
import { IconDownload, IconMessageCircle2, IconHeart, IconStar } from '@tabler/icons';
import { openConfirmModal } from '@mantine/modals';
import { IconDownload, IconHeart, IconMessageCircle2, IconStar, IconX } from '@tabler/icons';
import Link from 'next/link';
import { cloneElement, useMemo, useState } from 'react';

import { IconBadge } from '~/components/IconBadge/IconBadge';
import { useCurrentUser } from '~/hooks/useCurrentUser';
import { showErrorNotification, showSuccessNotification } from '~/utils/notifications';
import { abbreviateNumber } from '~/utils/number-helpers';
import { getDisplayName, slugit } from '~/utils/string-helpers';
import { trpc } from '~/utils/trpc';
import { cloneElement, useMemo } from 'react';

const useStyles = createStyles(() => ({
statBadge: {
Expand All @@ -30,6 +33,10 @@ const useStyles = createStyles(() => ({
export function ImageResources({ imageId }: { imageId: number }) {
const currentUser = useCurrentUser();
const { classes, theme } = useStyles();
const queryUtils = trpc.useContext();

const [selectedResource, setSelectedResource] = useState<number | null>(null);

const { data, isLoading } = trpc.image.getResources.useQuery({ id: imageId });

const { data: { Favorite: favoriteModels = [] } = { Favorite: [] } } =
Expand Down Expand Up @@ -62,6 +69,41 @@ export function ImageResources({ imageId }: { imageId: number }) {
return resources;
}, [data, favoriteModels]);

const { mutate, isLoading: removingResource } = trpc.image.removeResource.useMutation();
const handleRemoveResource = (resourceId: number) => {
setSelectedResource(resourceId);
openConfirmModal({
centered: true,
title: 'Remove Resource',
children:
'Are you sure you want to remove this resource from this image? This action is destructive and cannot be reverted.',
labels: { confirm: 'Yes, remove it', cancel: 'Cancel' },
confirmProps: { color: 'red' },
onConfirm: () =>
mutate(
{ imageId, resourceId },
{
async onSuccess() {
showSuccessNotification({
title: 'Successfully removed resource',
message: 'The resource was removed from the image',
});
await queryUtils.image.getResources.invalidate({ id: imageId });
},
onError(error) {
showErrorNotification({
title: 'Unable to remove resource',
error: new Error(error.message),
});
},
onSettled() {
setSelectedResource(null);
},
}
),
});
};

return (
<Stack spacing={4}>
{isLoading ? (
Expand All @@ -71,11 +113,16 @@ export function ImageResources({ imageId }: { imageId: number }) {
</Stack>
) : !!resources.length ? (
resources.map(({ key, isFavorite, isAvailable, ...resource }) => {
const removing = selectedResource === resource.id && removingResource;

return (
<Wrapper resource={resource} key={key}>
<Card
p={8}
sx={{ backgroundColor: theme.colors.dark[7], opacity: isAvailable ? 1 : 0.3 }}
sx={{
backgroundColor: theme.colors.dark[7],
opacity: removing ? 0.3 : isAvailable ? 1 : 0.3,
}}
withBorder
>
<Stack spacing="xs">
Expand All @@ -88,20 +135,38 @@ export function ImageResources({ imageId }: { imageId: number }) {
Unavailable
</Badge>
)}
{resource.modelType && (
<Badge radius="sm" size="sm">
{getDisplayName(resource.modelType)}
</Badge>
)}
{!isAvailable && resource.hash && (
<CopyButton value={resource.hash}>
{({ copy, copied }) => (
<Badge onClick={copy} radius="sm" size="sm" sx={{ cursor: 'pointer' }}>
{copied ? 'Copied...' : resource.hash}
</Badge>
)}
</CopyButton>
)}
<Group spacing={4}>
{resource.modelType && (
<Badge radius="sm" size="sm">
{getDisplayName(resource.modelType)}
</Badge>
)}
{!isAvailable && resource.hash && (
<CopyButton value={resource.hash}>
{({ copy, copied }) => (
<Badge onClick={copy} radius="sm" size="sm" sx={{ cursor: 'pointer' }}>
{copied ? 'Copied...' : resource.hash}
</Badge>
)}
</CopyButton>
)}
{currentUser?.isModerator && (
<ActionIcon
size="xs"
color="red"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
e.nativeEvent.stopImmediatePropagation();

handleRemoveResource(resource.id);
}}
disabled={removing}
>
<IconX size={14} stroke={1.5} />
</ActionIcon>
)}
</Group>
</Group>
{isAvailable && (
<Group spacing={0} position="apart">
Expand Down
27 changes: 12 additions & 15 deletions src/components/ResourceReview/ResourceReviewMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import { ActionIcon, MantineNumberSize, Menu, MenuProps, Text, Loader } from '@mantine/core';
import { closeAllModals, closeModal, openConfirmModal } from '@mantine/modals';
import { ActionIcon, Loader, MantineNumberSize, Menu, MenuProps, Text } from '@mantine/core';
import { closeAllModals, openConfirmModal } from '@mantine/modals';
import {
IconBan,
IconCalculator,
IconCalculatorOff,
IconDotsVertical,
IconEdit,
IconFlag,
IconLock,
IconLockOpen,
IconSwitchHorizontal,
IconTrash,
} from '@tabler/icons';
import { SessionUser } from 'next-auth';
import { ToggleLockComments } from '~/components/CommentsV2';

import { ToggleLockComments } from '~/components/CommentsV2';
import { LoginRedirect } from '~/components/LoginRedirect/LoginRedirect';
import { useCurrentUser } from '~/hooks/useCurrentUser';
import { openContext } from '~/providers/CustomModalsProvider';
import { closeRoutedContext, openRoutedContext } from '~/providers/RoutedContextProvider';
import { closeRoutedContext } from '~/providers/RoutedContextProvider';
import { ReportEntity } from '~/server/schema/report.schema';
import { ReviewGetAllItem } from '~/types/router';
import { showErrorNotification } from '~/utils/notifications';
import { trpc } from '~/utils/trpc';

Expand Down Expand Up @@ -125,12 +120,14 @@ export function ResourceReviewMenu({
>
Delete review
</Menu.Item>
<Menu.Item
icon={<IconEdit size={14} stroke={1.5} />}
onClick={() => openContext('resourceReviewEdit', review)}
>
Edit review
</Menu.Item>
{!isMuted && (
<Menu.Item
icon={<IconEdit size={14} stroke={1.5} />}
onClick={() => openContext('resourceReviewEdit', review)}
>
Edit review
</Menu.Item>
)}
</>
)}
{isMod && (
Expand Down
5 changes: 5 additions & 0 deletions src/server/routers/image.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
updateImageSchema,
getInfiniteImagesSchema,
imageModerationSchema,
removeImageResourceSchema,
} from './../schema/image.schema';
import {
deleteImageHandler,
Expand Down Expand Up @@ -40,6 +41,7 @@ import {
} from '~/server/trpc';
import { throwAuthorizationError } from '~/server/utils/errorHandling';
import { applyUserPreferences } from '~/server/middleware.trpc';
import { removeImageResource } from '~/server/services/image.service';

const isOwnerOrModerator = middleware(async ({ ctx, next, input = {} }) => {
if (!ctx.user) throw throwAuthorizationError();
Expand Down Expand Up @@ -106,4 +108,7 @@ export const imageRouter = router({
.query(getImagesAsPostsInfiniteHandler),
get: publicProcedure.input(getByIdSchema).query(getImageHandler),
getResources: publicProcedure.input(getByIdSchema).query(getImageResourcesHandler),
removeResource: protectedProcedure
.input(getByIdSchema)
.mutation(({ input, ctx }) => removeImageResource({ ...input, user: ctx.user })),
});
6 changes: 6 additions & 0 deletions src/server/schema/image.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,9 @@ export const getImageSchema = z.object({
// browsingMode: z.nativeEnum(BrowsingMode).optional(),
});
// #endregion

export type RemoveImageResourceSchema = z.infer<typeof removeImageResourceSchema>;
export const removeImageResourceSchema = z.object({
imageId: z.number(),
resourceId: z.number(),
});
25 changes: 24 additions & 1 deletion src/server/services/image.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ingestImageSchema,
GetInfiniteImagesInput,
GetImageInput,
RemoveImageResourceSchema,
} from './../schema/image.schema';
import {
CosmeticSource,
Expand Down Expand Up @@ -34,12 +35,17 @@ import { getEdgeUrl } from '~/client-utils/cf-images-utils';
import { decreaseDate } from '~/utils/date-helpers';
import { simpleTagSelect, imageTagSelect, ImageTag } from '~/server/selectors/tag.selector';
import { getImageV2Select, ImageV2Model } from '~/server/selectors/imagev2.selector';
import { throwAuthorizationError } from '~/server/utils/errorHandling';
import {
throwAuthorizationError,
throwDbError,
throwNotFoundError,
} from '~/server/utils/errorHandling';
import { VotableTagModel } from '~/libs/tags';
import { UserWithCosmetics, userWithCosmeticsSelect } from '~/server/selectors/user.selector';
import { getTagsNeedingReview } from '~/server/services/system-cache';
import { redis } from '~/server/redis/client';
import { hashify } from '~/utils/string-helpers';
import { TRPCError } from '@trpc/server';

export const getModelVersionImages = async ({ modelVersionId }: { modelVersionId: number }) => {
const result = await dbRead.imagesOnModels.findMany({
Expand Down Expand Up @@ -989,6 +995,7 @@ export const getImageResources = async ({ id }: GetByIdInput) => {
return await dbRead.imageResourceHelper.findMany({
where: { imageId: id, OR: [{ hash: { not: null } }, { modelVersionId: { not: null } }] },
select: {
id: true,
reviewId: true,
reviewRating: true,
reviewDetails: true,
Expand Down Expand Up @@ -1229,3 +1236,19 @@ export const getImagesForPosts = async ({
// ).sort((a, b) => b.postCount - a.postCount);
// };
// #endregion

export const removeImageResource = async ({ id, user }: GetByIdInput & { user?: SessionUser }) => {
if (!user?.isModerator) throw throwAuthorizationError();

try {
const resource = await dbWrite.imageResource.delete({
where: { id },
});
if (!resource) throw throwNotFoundError(`No image resource with id ${id}`);

return resource;
} catch (error) {
if (error instanceof TRPCError) throw error;
throw throwDbError(error);
}
};

0 comments on commit 3e333a4

Please sign in to comment.