1
0
Fork 0

feat: server invite list

user-stream
Lea 2023-08-10 21:57:05 +02:00 committed by insert
parent 9249b4e58d
commit a04a10f492
6 changed files with 256 additions and 5 deletions

View File

@ -0,0 +1,23 @@
import { ServerCard } from "@/components/cards/ServerCard";
import { NavigationToolbar } from "@/components/common/NavigationToolbar";
import ServerInviteList from "@/components/pages/inspector/InviteList";
import { fetchInvites, fetchServerById } from "@/lib/db";
import { notFound } from "next/navigation";
export default async function ServerInvites({ params }: { params: { id: string } }) {
const server = await fetchServerById(params.id);
if (!server) return notFound();
const { invites, channels, users } = await fetchInvites({
type: "Server",
server: params.id,
});
return (
<div className="flex flex-col gap-2">
<NavigationToolbar>Inspecting Server Invites</NavigationToolbar>
<ServerCard server={server} subtitle={`${invites.length} invite${invites.length == 1 ? '' : 's'}`} />
<ServerInviteList invites={invites} server={server} channels={channels} users={users} />
</div>
)
}

View File

@ -0,0 +1,51 @@
"use client"
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { ChannelInvite } from "@/lib/db";
import Link from "next/link";
import { Channel, User } from "revolt-api";
export function InviteCard({
invite,
channel,
user,
}: {
invite: ChannelInvite;
channel?: Channel;
user?: User;
}) {
return (
<Card className="my-2">
<CardHeader>
<CardTitle className="flex items-center">
<span className="font-extralight mr-0.5 select-none">rvlt.gg/</span>{invite._id}
<span className="select-none">{" "}</span> {/* looks better like this when for some reason the css doesnt load */}
{invite.vanity
? <span
className="select-none ml-2 p-1.5 bg-gray-400 text-white rounded-md font-normal text-base"
>
Vanity
</span>
: <></>}
</CardTitle>
<CardDescription>
{invite.type}
{" • "}
<Link href={`/panel/inspect/channel/${invite.channel}`}>
{(
channel &&
channel.channel_type != "DirectMessage" &&
channel.channel_type != "SavedMessages"
)
? `#${channel.name}`
: <i>Unknown Channel</i>}
</Link>
{" • "}
<Link href={`/panel/inspect/user/${invite.creator}`}>
{user ? `${user.username}#${user.discriminator}` : <i>Unknown Creator</i>}
</Link>
</CardDescription>
</CardHeader>
</Card>
);
}

View File

@ -0,0 +1,143 @@
"use client"
import { InviteCard } from "@/components/cards/InviteCard";
import { Button } from "@/components/ui/button";
import { Command, CommandItem } from "@/components/ui/command";
import { Input } from "@/components/ui/input";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
import { ChannelInvite } from "@/lib/db";
import { ChevronsUpDown } from "lucide-react";
import { useMemo, useState } from "react";
import { Channel, Server, User } from "revolt-api";
export default function ServerInviteList({ server, invites, channels, users }: {
server: Server,
invites: ChannelInvite[],
channels?: Channel[],
users?: User[],
}) {
const [selectVanityOnly, setSelectVanityOnly] = useState(false);
const [selectChannel, setSelectChannel] = useState(false);
const [selectUser, setSelectUser] = useState(false);
const [vanityOnly, setVanityOnly] = useState(false);
const [channelFilter, setChannelFilter] = useState("");
const [userFilter, setUserFilter] = useState("");
const filteredInvites = useMemo(() => {
return invites
?.filter(invite => vanityOnly ? invite.vanity : true)
?.filter(invite => channelFilter ? invite.channel == channelFilter : true)
?.filter(invite => userFilter ? invite.creator == userFilter : true);
}, [vanityOnly, channelFilter, userFilter, invites]);
return (
<div>
<div className="flex gap-2">
<Popover open={selectVanityOnly} onOpenChange={setSelectVanityOnly}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={selectVanityOnly}
className="flex-1 justify-between"
>
{vanityOnly ? "Vanity" : "All"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
{[
{ value: false, label: "All" },
{ value: true, label: "Vanity" },
].map((option) => (
<CommandItem
key={String(option.value)}
onSelect={async () => {
setSelectVanityOnly(false);
setVanityOnly(option.value);
}}
>{option.label}</CommandItem>
))}
</Command>
</PopoverContent>
</Popover>
<Popover open={selectChannel} onOpenChange={setSelectChannel}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={selectChannel}
className="flex-1 justify-between"
>
{channelFilter
? (channels?.find(c => c._id == channelFilter) as any)?.name
: "Select channel"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandItem onSelect={async () => {
setSelectChannel(false);
setChannelFilter("");
}}
>All channels</CommandItem>
{channels?.map((channel) => (
<CommandItem
key={String(channel._id)}
onSelect={async () => {
setSelectChannel(false);
setChannelFilter(channel._id);
}}
>{'#' + (channel as any).name}</CommandItem>
))}
</Command>
</PopoverContent>
</Popover>
<Popover open={selectUser} onOpenChange={setSelectUser}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={selectUser}
className="flex-1 justify-between"
>
{userFilter
? `${users?.find(c => c._id == userFilter)?.username}#${users?.find(c => c._id == userFilter)?.discriminator}`
: "Select user"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandItem onSelect={async () => {
setSelectUser(false);
setUserFilter("");
}}
>All users</CommandItem>
{users?.map((user) => (
<CommandItem
key={String(user._id)}
onSelect={async () => {
setSelectUser(false);
setUserFilter(user._id);
}}
>{user.username}#{user.discriminator}</CommandItem>
))}
</Command>
</PopoverContent>
</Popover>
</div>
{filteredInvites.map(invite => (<InviteCard
invite={invite}
channel={channels?.find(c => c._id == invite.channel)}
user={users?.find(c => c._id == invite.creator)}
key={invite._id}
/>))}
</div>
)
}

View File

@ -1,12 +1,9 @@
"use client"; "use client";
import { Server } from "revolt-api"; import { Server } from "revolt-api";
import { Button } from "../../ui/button"; import { Button, buttonVariants } from "../../ui/button";
import { import {
Command, Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem, CommandItem,
} from "@/components/ui/command"; } from "@/components/ui/command";
import { import {
@ -19,6 +16,7 @@ import { cn } from "@/lib/utils";
import { useState } from "react"; import { useState } from "react";
import { updateServerDiscoverability, updateServerFlags } from "@/lib/actions"; import { updateServerDiscoverability, updateServerFlags } from "@/lib/actions";
import { useToast } from "../../ui/use-toast"; import { useToast } from "../../ui/use-toast";
import Link from "next/link";
export function ServerActions({ server }: { server: Server }) { export function ServerActions({ server }: { server: Server }) {
const [selectBadges, setSelectBadges] = useState(false); const [selectBadges, setSelectBadges] = useState(false);
@ -130,6 +128,13 @@ export function ServerActions({ server }: { server: Server }) {
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<Link
className={`flex-1 ${buttonVariants()}`}
href={`/panel/inspect/server/${server._id}/invites`}
>
Invites
</Link>
<Button className="flex-1" variant="destructive"> <Button className="flex-1" variant="destructive">
Quarantine Quarantine
</Button> </Button>

View File

@ -16,7 +16,7 @@ type Permission =
| "" | ""
| `/fetch${"" | "/by-id" | "/by-user"}` | `/fetch${"" | "/by-id" | "/by-user"}`
| `/update${"" | "/discoverability"}`}` | `/update${"" | "/discoverability"}`}`
| `channels${"" | `/fetch${"" | "/by-id" | "/dm"}` | `/create${"" | "/dm"}`}` | `channels${"" | `/fetch${"" | "/by-id" | "/by-server" | "/dm" | "/invites"}` | `/create${"" | "/dm"}`}`
| `messages${"" | `/fetch${"" | "/by-id" | "/by-user"}`}` | `messages${"" | `/fetch${"" | "/by-id" | "/by-user"}`}`
| `reports${ | `reports${
| "" | ""
@ -125,6 +125,7 @@ const PermissionSets = {
"messages/fetch/by-id", "messages/fetch/by-id",
"channels/fetch/by-id", "channels/fetch/by-id",
"channels/fetch/dm", "channels/fetch/dm",
"channels/fetch/invites",
"channels/create/dm", "channels/create/dm",
"reports/fetch/related/by-user", "reports/fetch/related/by-user",

View File

@ -5,6 +5,7 @@ import type {
AccountStrike, AccountStrike,
Bot, Bot,
Channel, Channel,
Invite,
Message, Message,
Report, Report,
Server, Server,
@ -286,6 +287,33 @@ export async function fetchServers(query: Filter<Server>) {
.toArray(); .toArray();
} }
// `vanity` should eventually be added to the backend as well
export type ChannelInvite = Invite & { vanity?: boolean }
export async function fetchInvites(query: Filter<ChannelInvite>) {
await checkPermission("channels/fetch/invites", query);
const invites = await mongo()
.db("revolt")
.collection<ChannelInvite>("channel_invites")
.find(query)
.toArray();
const channels = await mongo()
.db("revolt")
.collection<Channel>("channels")
.find({ _id: { $in: invites.map((invite) => invite.channel) } })
.toArray();
const users = await mongo()
.db("revolt")
.collection<User>("users")
.find({ _id: { $in: invites.map((invite) => invite.creator) } })
.toArray();
return { invites, channels, users }
}
export async function fetchMessageById(id: string) { export async function fetchMessageById(id: string) {
await checkPermission("messages/fetch/by-id", id); await checkPermission("messages/fetch/by-id", id);