Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moderation #123

Merged
merged 48 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
a073dcc
add schemas for moderation and reports
WillCorrigan Sep 13, 2024
4b9750f
add moderation page
WillCorrigan Sep 20, 2024
dbca7ab
no unused imports
WillCorrigan Sep 20, 2024
e3f38ee
update card to use children, we're allys here
WillCorrigan Sep 20, 2024
b75666d
add client component, start adding actions and checks for roles
WillCorrigan Sep 21, 2024
f8606dd
remove extra div on mod page
WillCorrigan Sep 22, 2024
06f8fb6
delete generated migration
WillCorrigan Sep 23, 2024
19d9dcc
Merge branch 'main' into will/un-71-frontpage-moderation
WillCorrigan Sep 23, 2024
51a32bb
update report to reflect schema, start working on actual page components
WillCorrigan Sep 23, 2024
4b5c2ef
update readme typos
WillCorrigan Sep 23, 2024
db46d92
remove unused imports for build
WillCorrigan Sep 23, 2024
4bd324f
generate schema, migrate
WillCorrigan Sep 25, 2024
1419f5e
Merge branch 'main' into will/un-71-frontpage-moderation
WillCorrigan Sep 25, 2024
3f35e59
add status to schema, start chunking out actions, get forms and actio…
WillCorrigan Sep 26, 2024
5343c2b
Merge branch 'main' into will/un-71-frontpage-moderation
WillCorrigan Sep 26, 2024
020922b
moderate the things
WillCorrigan Sep 27, 2024
6c4603b
Merge branch 'main' into will/un-71-frontpage-moderation
WillCorrigan Sep 27, 2024
281276d
update packages
WillCorrigan Sep 28, 2024
ac5e365
add dropdown menu to posts wip
WillCorrigan Sep 29, 2024
f667367
Merge branch 'main' into will/un-71-frontpage-moderation
WillCorrigan Sep 29, 2024
b845fe0
why
WillCorrigan Sep 29, 2024
18ca032
remove unused
WillCorrigan Sep 29, 2024
87f43bb
add confirmation dialogs
WillCorrigan Sep 29, 2024
1796b6c
get ellipsis working for posts, create moderator actions in db, add e…
WillCorrigan Sep 30, 2024
e64d956
better server actions
WillCorrigan Sep 30, 2024
fb90c03
reusable ellipsis component takes children
WillCorrigan Oct 1, 2024
bc96907
lock file
WillCorrigan Oct 1, 2024
d83c2a0
update page query to filter hidden users
WillCorrigan Oct 1, 2024
f4737eb
left join to prevent query being weird
WillCorrigan Oct 1, 2024
1c65d78
report button on card and profile working, add logging to db for reports
WillCorrigan Oct 2, 2024
09bd437
cache reports
WillCorrigan Oct 2, 2024
7879b26
lower case the things
WillCorrigan Oct 2, 2024
47aa95b
deduplicate, convert messy ifs to switch
WillCorrigan Oct 2, 2024
6decdef
fix suggested changes
WillCorrigan Oct 2, 2024
c66e427
get rid of enum, add some DID types to things
WillCorrigan Oct 3, 2024
c3fa544
remove scroll area, enforce admin for sensitive actions
WillCorrigan Oct 3, 2024
47a6aea
make suggested changes, add migration for times to labelled profiles
WillCorrigan Oct 3, 2024
2ae8ef1
styling tweaks, re-add missing rkey and cid
WillCorrigan Oct 4, 2024
e7842b4
Lazy load admin button
tom-sherman Oct 4, 2024
ee14457
Early return
tom-sherman Oct 4, 2024
6895913
Promise.all
tom-sherman Oct 4, 2024
45f1983
update schema datetimes and discord webhook fields
WillCorrigan Oct 4, 2024
b813a1e
get rooturl based on env
WillCorrigan Oct 4, 2024
22a8631
redo schema for completeness
WillCorrigan Oct 4, 2024
79f5ecc
Improve createReport input
tom-sherman Oct 4, 2024
b97ce7e
Type safe form
tom-sherman Oct 4, 2024
49d2649
No need for state
tom-sherman Oct 4, 2024
fcb2f67
Refactor report button
tom-sherman Oct 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/frontpage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

Frontpage AppView and frontend client.

## Running locally
<!-- ## Running locally -->

If you just need to work on the app in a logged-out state, then you just need to run the following:

```bash
pnpm run dev
```

If you need to login, you need to setup some additional env vars and serve your dev server over the public internet. You can do this with `cloudflared` altho other options are available eg. `ngrok` or `tailscale`:
If you need to login, you need to setup some additional env vars and serve your dev server over the public internet. You can do this with `cloudflared` although other options are available eg. `ngrok` or `tailscale`:

```bash
pnpm exec tsx ./scripts/generate-jwk.mts # Copy this output into .env.local
Expand Down
67 changes: 67 additions & 0 deletions packages/frontpage/app/(app)/_components/delete-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"use client";

import {
AlertDialogHeader,
AlertDialogFooter,
} from "@/lib/components/ui/alert-dialog";
import { toast } from "@/lib/components/ui/use-toast";
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
} from "@/lib/components/ui/alert-dialog";
import { DropdownMenuItem } from "@/lib/components/ui/dropdown-menu";
import { useEllipsisDropdownContext } from "./ellipsis-dropdown";
import { TrashIcon } from "@radix-ui/react-icons";

type DeleteButtonProps = {
deleteAction: () => Promise<void>;
};

export function DeleteButton({ deleteAction }: DeleteButtonProps) {
const ellipsisDropdownContext = useEllipsisDropdownContext();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<DropdownMenuItem
className="text-red-600"
onSelect={(e) => {
e.preventDefault();
}}
>
<TrashIcon className="mr-2 scale-125" />
Delete
</DropdownMenuItem>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will delete your entry. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="mt-3">
<AlertDialogAction
onClick={() => {
void deleteAction();
toast({
title: "Your entry will be deleted shortly",
type: "foreground",
});
ellipsisDropdownContext.close();
}}
damiensedgwick marked this conversation as resolved.
Show resolved Hide resolved
>
Confirm
</AlertDialogAction>
<AlertDialogCancel onClick={() => ellipsisDropdownContext.close()}>
Cancel
</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
50 changes: 50 additions & 0 deletions packages/frontpage/app/(app)/_components/ellipsis-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client";

import { Button } from "@/lib/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/lib/components/ui/dropdown-menu";
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import React, { ReactNode } from "react";

interface EllipsisDropdownProps {
children: ReactNode;
}

const EllipsisDropdownContext = React.createContext<null | {
close: () => void;
}>(null);

export function EllipsisDropdown({ children }: EllipsisDropdownProps) {
const [open, setOpen] = React.useState(false);

return (
<EllipsisDropdownContext.Provider value={{ close: () => setOpen(false) }}>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<DotsHorizontalIcon className="scale-125" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuGroup>{children}</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</EllipsisDropdownContext.Provider>
);
}

export const useEllipsisDropdownContext = () => {
const context = React.useContext(EllipsisDropdownContext);
if (!context) {
throw new Error(
"useEllipsisDropdownContext must be used within a EllipsisDropdown",
);
}
return context;
};
68 changes: 65 additions & 3 deletions packages/frontpage/app/(app)/_components/post-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import { getVoteForPost } from "@/lib/data/db/vote";
import { ensureUser, getUser } from "@/lib/data/user";
import { TimeAgo } from "@/lib/components/time-ago";
import { VoteButton } from "./vote-button";
import { PostCollection } from "@/lib/data/atproto/post";
import { PostCollection, deletePost } from "@/lib/data/atproto/post";
import { getVerifiedHandle } from "@/lib/data/atproto/identity";
import { UserHoverCard } from "@/lib/components/user-hover-card";
import type { DID } from "@/lib/data/atproto/did";
import { parseReportForm } from "@/lib/data/db/report-shared";
import { createReport } from "@/lib/data/db/report";
import { EllipsisDropdown } from "./ellipsis-dropdown";
import { revalidatePath } from "next/cache";
import { ReportDialogDropdownButton } from "./report-dialog";
import { DeleteButton } from "./delete-button";

type PostProps = {
id: number;
Expand All @@ -34,7 +40,10 @@ export async function PostCard({
cid,
isUpvoted,
}: PostProps) {
const handle = await getVerifiedHandle(author);
const [handle, user] = await Promise.all([
getVerifiedHandle(author),
getUser(),
]);
const postHref = `/post/${handle}/${rkey}`;

return (
Expand Down Expand Up @@ -87,7 +96,7 @@ export async function PostCard({
</h2>
<div className="flex flex-wrap text-gray-500 dark:text-gray-400 sm:gap-4">
<div className="flex gap-2 flex-wrap md:flex-nowrap">
<div className="flex gap-2">
<div className="flex gap-2 items-center">
<span aria-hidden>•</span>
<UserHoverCard did={author} asChild>
<Link href={`/profile/${handle}`} className="hover:underline">
Expand All @@ -108,8 +117,61 @@ export async function PostCard({
</Link>
</div>
</div>

{user ? (
<div className="ml-auto">
<EllipsisDropdown>
<ReportDialogDropdownButton
reportAction={reportPostAction.bind(null, {
rkey,
cid,
author,
})}
/>
{user?.did === author ? (
<DeleteButton
deleteAction={deletePostAction.bind(null, rkey)}
/>
) : null}
</EllipsisDropdown>
</div>
) : null}
</div>
</div>
</article>
);
}

export async function deletePostAction(rkey: string) {
"use server";
await ensureUser();
await deletePost(rkey);

revalidatePath("/");
}

export async function reportPostAction(
input: {
rkey: string;
cid: string;
author: DID;
},
formData: FormData,
) {
"use server";
await ensureUser();

const formResult = parseReportForm(formData);
if (!formResult.success) {
throw new Error("Invalid form data");
}

await createReport({
...formResult.data,
subjectUri: `at://${input.author}/${PostCollection}/${input.rkey}`,
subjectDid: input.author,
subjectCollection: PostCollection,
subjectRkey: input.rkey,
subjectCid: input.cid,
});
}
131 changes: 131 additions & 0 deletions packages/frontpage/app/(app)/_components/report-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"use client";

import {
AlertDialogHeader,
AlertDialogFooter,
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogCancel,
} from "@/lib/components/ui/alert-dialog";
import { Button } from "@/lib/components/ui/button";
import { Textarea } from "@/lib/components/ui/textarea";
import { toast } from "@/lib/components/ui/use-toast";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/lib/components/ui/select";
import { ReportReasons } from "@/lib/data/db/report-shared";
import { ReactNode, useState } from "react";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { useEllipsisDropdownContext } from "./ellipsis-dropdown";
import { DropdownMenuItem } from "@/lib/components/ui/dropdown-menu";

type ReportDialogProps = {
reportAction: (formData: FormData) => Promise<void>;
children: ReactNode;
};

function ReportDialog({ reportAction, children }: ReportDialogProps) {
const [open, setOpen] = useState(false);
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will report the entry. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<form
action={async (formData) => {
try {
await reportAction(formData);
} catch (_) {
toast({
title: "Something went wrong",
type: "foreground",
});
return;
}
toast({
title: "Report submitted",
type: "foreground",
});
setOpen(false);
}}
>
<Select name="reportReason">
<SelectTrigger className="w-44">
<SelectValue placeholder="Please select one" />
</SelectTrigger>
<SelectContent>
{ReportReasons.map((reason) => (
<SelectItem key={reason} value={reason}>
{reason}
</SelectItem>
))}
</SelectContent>
</Select>
<Textarea
id="textArea"
name="creatorComment"
placeholder="Enter your report reason here..."
className="resize-y my-3"
maxLength={250}
/>
<AlertDialogFooter>
<Button variant="success" type="submit">
Confirm
</Button>
<AlertDialogCancel type="button">Cancel</AlertDialogCancel>
</AlertDialogFooter>
</form>
</AlertDialogContent>
</AlertDialog>
);
}

export function ReportDialogIcon({
reportAction,
}: Pick<ReportDialogProps, "reportAction">) {
return (
<ReportDialog reportAction={reportAction}>
<Button size="icon" variant="outline">
<ExclamationTriangleIcon className="h-4 w-4 text-red-500" />
</Button>
</ReportDialog>
);
}

export function ReportDialogDropdownButton({
reportAction,
}: Pick<ReportDialogProps, "reportAction">) {
const ellipsisDropdownContext = useEllipsisDropdownContext();
return (
<ReportDialog
reportAction={async (formData: FormData) => {
await reportAction(formData);
ellipsisDropdownContext.close();
}}
>
<DropdownMenuItem
//this prevents the alert dialog from closing when the dropdown item is clicked
//alternatively wrap the whole dropdown in the dialog as per https://github.com/radix-ui/primitives/issues/1836
//but that defeats the purpose of this reusable component
onSelect={(e) => {
e.preventDefault();
}}
>
<ExclamationTriangleIcon className="mr-2 h-4 w-4" />
Report
</DropdownMenuItem>
</ReportDialog>
);
}
13 changes: 12 additions & 1 deletion packages/frontpage/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { deleteAuthCookie, getSession, signOut } from "@/lib/auth";
import Link from "next/link";
import { Suspense } from "react";
import { Button } from "@/lib/components/ui/button";
import { isBetaUser } from "@/lib/data/user";
import { isAdmin, isBetaUser } from "@/lib/data/user";
import { OpenInNewWindowIcon } from "@radix-ui/react-icons";
import { ThemeToggle } from "./_components/theme-toggle";
import {
Expand Down Expand Up @@ -123,6 +123,17 @@ async function LoginOrLogout() {
Profile
</Link>
</DropdownMenuItem>
<Suspense fallback={null}>
{isAdmin().then((isAdmin) =>
isAdmin ? (
<DropdownMenuItem asChild>
<Link href="/moderation" className="cursor-pointer">
Moderation
</Link>
</DropdownMenuItem>
) : null,
)}
</Suspense>
<DropdownMenuSeparator />
<form
action={async () => {
Expand Down
Loading