Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion desktop/biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.6/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.11/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand Down
57 changes: 57 additions & 0 deletions desktop/src/features/forum/ui/DeleteActionMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { MoreHorizontal, Trash2 } from "lucide-react";
import * as React from "react";

import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";

import { DeleteConfirmDialog } from "./DeleteConfirmDialog";

type DeleteActionMenuProps = {
label: string;
onConfirm: () => void;
iconSize?: "sm" | "md";
};

export function DeleteActionMenu({
label,
onConfirm,
iconSize = "md",
}: DeleteActionMenuProps) {
const [isOpen, setIsOpen] = React.useState(false);
const iconClass = iconSize === "sm" ? "h-3.5 w-3.5" : "h-4 w-4";

return (
<div className="ml-auto opacity-0 transition-opacity group-hover:opacity-100">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
tabIndex={-1}
type="button"
>
<MoreHorizontal className={iconClass} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => setIsOpen(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete {label}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DeleteConfirmDialog
label={label}
onConfirm={onConfirm}
onOpenChange={setIsOpen}
open={isOpen}
/>
</div>
);
}
48 changes: 48 additions & 0 deletions desktop/src/features/forum/ui/DeleteConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/shared/ui/alert-dialog";
import { Button } from "@/shared/ui/button";

export function DeleteConfirmDialog({
open,
onOpenChange,
onConfirm,
label,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
label: string;
}) {
return (
<AlertDialog onOpenChange={onOpenChange} open={open}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete {label}?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete this {label} and cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button onClick={onConfirm} type="button" variant="destructive">
Delete {label}
</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
95 changes: 20 additions & 75 deletions desktop/src/features/forum/ui/ForumPostCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { MessageSquare, MoreHorizontal, Trash2 } from "lucide-react";
import * as React from "react";
import { MessageSquare } from "lucide-react";

import {
resolveUserLabel,
Expand All @@ -9,26 +8,10 @@ import { UserAvatar } from "@/shared/ui/UserAvatar";
import type { ForumPost } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { resolveMentionNames } from "@/shared/lib/resolveMentionNames";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/shared/ui/alert-dialog";
import { Button } from "@/shared/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { Markdown } from "@/shared/ui/markdown";

import { formatRelativeTime } from "../lib/time";
import { DeleteActionMenu } from "./DeleteActionMenu";

type ForumPostCardProps = {
post: ForumPost;
Expand All @@ -51,7 +34,6 @@ export function ForumPostCard({
onClick,
onDelete,
}: ForumPostCardProps) {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false);
const authorLabel = resolveUserLabel({
pubkey: post.pubkey,
currentPubkey,
Expand All @@ -67,14 +49,22 @@ export function ForumPostCard({
: post.content;

return (
<button
// biome-ignore lint/a11y/useSemanticElements: Cannot use <button> because DeleteActionMenu renders a nested <button> via DropdownMenuTrigger, which is invalid HTML
<div
role="button"
tabIndex={0}
className={cn(
"group w-full cursor-pointer rounded-xl border border-border/60 bg-card p-4 text-left transition-colors hover:border-border hover:bg-accent/40",
isActive && "border-primary/40 bg-accent/60",
isDeleting && "pointer-events-none opacity-50",
)}
onClick={() => onClick(post)}
type="button"
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onClick(post);
}
}}
>
<div className="flex items-center gap-2">
<UserAvatar avatarUrl={avatarUrl} displayName={authorLabel} size="sm" />
Expand All @@ -86,61 +76,16 @@ export function ForumPostCard({
</span>

{canDelete && onDelete ? (
// biome-ignore lint/a11y/noStaticElementInteractions: presentation wrapper only stops click propagation to parent card link
<div
className="ml-auto opacity-0 transition-opacity group-hover:opacity-100"
onClickCapture={(e) => e.stopPropagation()}
className="ml-auto"
onClick={(e) => e.stopPropagation()}
role="presentation"
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
tabIndex={-1}
type="button"
>
<MoreHorizontal className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => setIsDeleteDialogOpen(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete post
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

<AlertDialog
onOpenChange={setIsDeleteDialogOpen}
open={isDeleteDialogOpen}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete post?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete this post and cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button
onClick={() => onDelete(post.eventId)}
type="button"
variant="destructive"
>
Delete post
</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<DeleteActionMenu
label="post"
onConfirm={() => onDelete(post.eventId)}
/>
</div>
) : null}
</div>
Expand Down Expand Up @@ -168,6 +113,6 @@ export function ForumPostCard({
) : null}
</div>
) : null}
</button>
</div>
);
}
Loading