forked from administration/panel
553 lines
20 KiB
TypeScript
553 lines
20 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { Button, buttonVariants } from "../../ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "../../ui/dropdown-menu";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "../../ui/alert-dialog";
|
|
import { Input } from "../../ui/input";
|
|
import {
|
|
banUser,
|
|
closeReportsByUser,
|
|
resetBotToken,
|
|
sendAlert,
|
|
suspendUser,
|
|
transferBot,
|
|
unsuspendUser,
|
|
updateBotDiscoverability,
|
|
updateUserBadges,
|
|
wipeUserProfile,
|
|
} from "@/lib/actions";
|
|
import { useRef, useState } from "react";
|
|
import { useToast } from "../../ui/use-toast";
|
|
import { Bot, User } from "revolt-api";
|
|
import { Card, CardHeader } from "../../ui/card";
|
|
import { cn } from "@/lib/utils";
|
|
import { decodeTime } from "ulid";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import UserSelector from "@/components/ui/user-selector";
|
|
|
|
const badges = [1, 2, 4, 8, 16, 32, 128, 0, 256, 512, 1024];
|
|
|
|
export function UserActions({ user, bot }: { user: User; bot?: Bot }) {
|
|
const alertMessage = useRef("");
|
|
const { toast } = useToast();
|
|
|
|
const [userDraft, setUserDraft] = useState(user);
|
|
const [botDraft, setBotDraft] = useState(bot);
|
|
const [wipeDraft, setWipeDraft] = useState({
|
|
banner: false,
|
|
avatar: false,
|
|
bio: false,
|
|
displayName: false,
|
|
status: false,
|
|
});
|
|
const [transferTarget, setTransferTarget] = useState<User | null>(null);
|
|
const [transferResetToken, setTransferResetToken] = useState(true);
|
|
|
|
const userInaccessible = userDraft.flags === 4 || userDraft.flags === 2;
|
|
|
|
return (
|
|
<>
|
|
<Card>
|
|
<CardHeader className="w-full flex flex-row gap-2 justify-center items-center flex-wrap">
|
|
{badges.map((badge) => {
|
|
const sysBadge =
|
|
(badge === 0 && user._id === "01EX2NCWQ0CHS3QJF0FEQS1GR4") ||
|
|
(badge === 256 && decodeTime(user._id) < 1629638578431);
|
|
return (
|
|
// eslint-disable-next-line
|
|
<img
|
|
key={badge}
|
|
src={`/badges/${badge === 8 ? 2048 : badge}.svg`}
|
|
className={cn(
|
|
"w-7 h-7 transition-all !m-0",
|
|
sysBadge
|
|
? ""
|
|
: (userDraft.badges ?? 0) & badge
|
|
? "cursor-pointer"
|
|
: "cursor-pointer opacity-40 hover:opacity-60 grayscale hover:grayscale-0"
|
|
)}
|
|
onClick={async () => {
|
|
if (sysBadge) return;
|
|
|
|
try {
|
|
const badges =
|
|
(decodeTime(user._id) < 1629638578431 ? 256 : 0) |
|
|
((userDraft.badges ?? 0) ^ badge);
|
|
await updateUserBadges(user._id, badges);
|
|
setUserDraft((user) => ({ ...user!, badges }));
|
|
toast({
|
|
title: "Updated user badges",
|
|
});
|
|
} catch (err) {
|
|
toast({
|
|
title: "Failed to update user badges",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
</CardHeader>
|
|
</Card>
|
|
|
|
<div className="flex gap-2">
|
|
{bot ? (
|
|
botDraft!.discoverable ? (
|
|
<Button
|
|
className="flex-1"
|
|
onClick={async () => {
|
|
try {
|
|
await updateBotDiscoverability(bot._id, false);
|
|
setBotDraft((bot) => ({ ...bot!, discoverable: false }));
|
|
toast({
|
|
title: "Removed bot from Discover",
|
|
});
|
|
} catch (err) {
|
|
toast({
|
|
title: "Failed to remove bot from Discover",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
Remove from Discover
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
className="flex-1"
|
|
onClick={async () => {
|
|
try {
|
|
await updateBotDiscoverability(bot._id, true);
|
|
setBotDraft((bot) => ({ ...bot!, discoverable: true }));
|
|
toast({
|
|
title: "Added bot to Discover",
|
|
});
|
|
} catch (err) {
|
|
toast({
|
|
title: "Failed to add bot to Discover",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
Add to Discover
|
|
</Button>
|
|
)
|
|
) : (
|
|
<Link
|
|
className={`flex-1 ${buttonVariants()}`}
|
|
href={`/panel/inspect/account/${user._id}`}
|
|
>
|
|
Account
|
|
</Link>
|
|
)}
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
className="flex-1 bg-orange-400 hover:bg-orange-300"
|
|
disabled={userInaccessible}
|
|
>
|
|
{userDraft.flags === 1 ? "Unsuspend" : "Suspend"}
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
Are you sure you want to{" "}
|
|
{userDraft.flags === 1 ? "unsuspend" : "suspend"} this user?
|
|
</AlertDialogTitle>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() =>
|
|
userDraft.flags === 1
|
|
? unsuspendUser(user._id)
|
|
.then(() => {
|
|
setUserDraft((user) => ({ ...user, flags: 0 }));
|
|
toast({ title: "Unsuspended user" });
|
|
})
|
|
.catch((err) =>
|
|
toast({
|
|
title: "Failed to unsuspend user!",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
})
|
|
)
|
|
: suspendUser(user._id)
|
|
.then(() => {
|
|
setUserDraft((user) => ({ ...user, flags: 1 }));
|
|
toast({ title: "Suspended user" });
|
|
})
|
|
.catch((err) =>
|
|
toast({
|
|
title: "Failed to suspend user!",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
})
|
|
)
|
|
}
|
|
>
|
|
{userDraft.flags === 1 ? "Unsuspend" : "Suspend"}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
className="flex-1"
|
|
variant="destructive"
|
|
disabled={userInaccessible}
|
|
>
|
|
{userDraft.flags === 4 ? "Banned" : "Ban"}
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
Are you sure you want to ban this user?
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
All messages sent by this user will be deleted immediately.
|
|
<br className="text-base/8" />
|
|
<span className="text-red-700">
|
|
This action is irreversible!
|
|
</span>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
className="hover:bg-red-700 transition-all"
|
|
onClick={() =>
|
|
banUser(user._id)
|
|
.then(() => {
|
|
setUserDraft((user) => ({ ...user, flags: 4 }));
|
|
toast({ title: "Banned user" });
|
|
})
|
|
.catch((err) =>
|
|
toast({
|
|
title: "Failed to ban user!",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
})
|
|
)
|
|
}
|
|
>
|
|
Ban
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button className="flex-1 bg-yellow-600">Bees</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Release the bees</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to send the bees?
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction onClick={() => toast({ title: "🐝" })}>
|
|
Deploy
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" className="flex-1">
|
|
More Options
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="flex flex-col">
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="ghost">Send Alert</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Send Alert</AlertDialogTitle>
|
|
<AlertDialogDescription className="flex flex-col gap-2">
|
|
<span>
|
|
This will send a message from the Platform Moderation
|
|
account.
|
|
</span>
|
|
<Input
|
|
placeholder="Enter a message..."
|
|
name="message"
|
|
onChange={(e) =>
|
|
(alertMessage.current = e.currentTarget.value)
|
|
}
|
|
/>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => {
|
|
if (!alertMessage.current) return;
|
|
|
|
sendAlert(user._id, alertMessage.current)
|
|
.then(() => {
|
|
toast({ title: "Sent Alert" });
|
|
alertMessage.current = "";
|
|
})
|
|
.catch((err) =>
|
|
toast({
|
|
title: "Failed to send alert!",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
})
|
|
);
|
|
}}
|
|
>
|
|
Send
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="ghost">Reset profile</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Reset user profile</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
<Checkbox
|
|
checked={wipeDraft.avatar}
|
|
onChange={(e) =>
|
|
setWipeDraft({ ...wipeDraft, avatar: e == true })
|
|
}
|
|
>
|
|
Avatar
|
|
</Checkbox>
|
|
<Checkbox
|
|
checked={wipeDraft.banner}
|
|
onChange={(e) =>
|
|
setWipeDraft({ ...wipeDraft, banner: e == true })
|
|
}
|
|
>
|
|
Profile Banner
|
|
</Checkbox>
|
|
<Checkbox
|
|
checked={wipeDraft.displayName}
|
|
onChange={(e) =>
|
|
setWipeDraft({ ...wipeDraft, displayName: e == true })
|
|
}
|
|
>
|
|
Display Name
|
|
</Checkbox>
|
|
<Checkbox
|
|
checked={wipeDraft.bio}
|
|
onChange={(e) =>
|
|
setWipeDraft({ ...wipeDraft, bio: e == true })
|
|
}
|
|
>
|
|
Bio
|
|
</Checkbox>
|
|
<Checkbox
|
|
checked={wipeDraft.status}
|
|
onChange={(e) =>
|
|
setWipeDraft({ ...wipeDraft, status: e == true })
|
|
}
|
|
>
|
|
Status
|
|
</Checkbox>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
disabled={!Object.values(wipeDraft).filter((i) => i).length}
|
|
onClick={() => {
|
|
wipeUserProfile(user._id, wipeDraft)
|
|
.then(() => {
|
|
toast({ title: "Wiped selected fields" });
|
|
window.location.reload();
|
|
})
|
|
.catch((e) =>
|
|
toast({
|
|
title: "Failed to wipe profile",
|
|
description: String(e),
|
|
variant: "destructive",
|
|
})
|
|
)
|
|
.finally(() =>
|
|
setWipeDraft({
|
|
avatar: false,
|
|
banner: false,
|
|
bio: false,
|
|
displayName: false,
|
|
status: false,
|
|
})
|
|
);
|
|
}}
|
|
>
|
|
Reset
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="ghost" disabled={!user.bot?.owner}>Reset bot token</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Reset token</AlertDialogTitle>
|
|
<AlertDialogDescription className="flex flex-col gap-2">
|
|
<span>
|
|
Re-roll this bot's authentication token. This will not disconnect active connections.
|
|
</span>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => resetBotToken(user._id)
|
|
.then(() => toast({
|
|
title: "Reset bot token",
|
|
}))
|
|
.catch((e) => toast({
|
|
title: "Failed to reset token",
|
|
description: String(e),
|
|
variant: "destructive",
|
|
}))
|
|
}
|
|
>
|
|
Reset
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="ghost" disabled={!user.bot?.owner}>Transfer bot</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Transfer bot</AlertDialogTitle>
|
|
<AlertDialogDescription className="flex flex-col gap-2">
|
|
<span>
|
|
Transfer this bot to a new owner.
|
|
</span>
|
|
<UserSelector
|
|
onChange={setTransferTarget}
|
|
/>
|
|
<Checkbox
|
|
checked={transferResetToken}
|
|
onChange={(e) => setTransferResetToken(!!e)}
|
|
>
|
|
Also reset token
|
|
</Checkbox>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
disabled={!transferTarget}
|
|
onClick={() => transferBot(user._id, transferTarget!._id, transferResetToken)
|
|
.then(() => toast({
|
|
title: "Reset bot token",
|
|
}))
|
|
.catch((e) => toast({
|
|
title: "Failed to reset token",
|
|
description: String(e),
|
|
variant: "destructive",
|
|
}))
|
|
.finally(() => {
|
|
setTransferResetToken(true);
|
|
setTransferTarget(null);
|
|
})
|
|
}
|
|
>
|
|
Transfer
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="ghost">Close Open Reports</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Close Open Reports</AlertDialogTitle>
|
|
<AlertDialogDescription className="flex flex-col gap-2">
|
|
<span>
|
|
This will close all reports still open by this user.
|
|
</span>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() =>
|
|
closeReportsByUser(user._id)
|
|
.then((reports) =>
|
|
toast({ title: `Closed ${reports} Reports` })
|
|
)
|
|
.catch((err) =>
|
|
toast({
|
|
title: "Failed to close reports!",
|
|
description: String(err),
|
|
variant: "destructive",
|
|
})
|
|
)
|
|
}
|
|
>
|
|
Continue
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
{/* <DropdownMenuItem>
|
|
Clear ({counts.pending}) Friend Requests
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem>
|
|
Clear All ({counts.all}) Relations
|
|
</DropdownMenuItem> */}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|