diff --git a/desktop/biome.json b/desktop/biome.json index eed2cf8a2..8535f3d59 100644 --- a/desktop/biome.json +++ b/desktop/biome.json @@ -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", diff --git a/desktop/src/features/forum/ui/DeleteActionMenu.tsx b/desktop/src/features/forum/ui/DeleteActionMenu.tsx new file mode 100644 index 000000000..e6ce8b3d3 --- /dev/null +++ b/desktop/src/features/forum/ui/DeleteActionMenu.tsx @@ -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 ( +
+ + + + + + setIsOpen(true)} + > + + Delete {label} + + + + +
+ ); +} diff --git a/desktop/src/features/forum/ui/DeleteConfirmDialog.tsx b/desktop/src/features/forum/ui/DeleteConfirmDialog.tsx new file mode 100644 index 000000000..02eacc4f7 --- /dev/null +++ b/desktop/src/features/forum/ui/DeleteConfirmDialog.tsx @@ -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 ( + + + + Delete {label}? + + This will permanently delete this {label} and cannot be undone. + + + + + + + + + + + + + ); +} diff --git a/desktop/src/features/forum/ui/ForumPostCard.tsx b/desktop/src/features/forum/ui/ForumPostCard.tsx index ec34e2e79..4f6113aaa 100644 --- a/desktop/src/features/forum/ui/ForumPostCard.tsx +++ b/desktop/src/features/forum/ui/ForumPostCard.tsx @@ -1,5 +1,4 @@ -import { MessageSquare, MoreHorizontal, Trash2 } from "lucide-react"; -import * as React from "react"; +import { MessageSquare } from "lucide-react"; import { resolveUserLabel, @@ -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; @@ -51,7 +34,6 @@ export function ForumPostCard({ onClick, onDelete, }: ForumPostCardProps) { - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false); const authorLabel = resolveUserLabel({ pubkey: post.pubkey, currentPubkey, @@ -67,14 +49,22 @@ export function ForumPostCard({ : post.content; return ( - - - - setIsDeleteDialogOpen(true)} - > - - Delete post - - - - - - - - Delete post? - - This will permanently delete this post and cannot be undone. - - - - - - - - - - - - + onDelete(post.eventId)} + /> ) : null} @@ -168,6 +113,6 @@ export function ForumPostCard({ ) : null} ) : null} - + ); } diff --git a/desktop/src/features/forum/ui/ForumThreadPanel.tsx b/desktop/src/features/forum/ui/ForumThreadPanel.tsx index 7d11a8a1b..1706f3d45 100644 --- a/desktop/src/features/forum/ui/ForumThreadPanel.tsx +++ b/desktop/src/features/forum/ui/ForumThreadPanel.tsx @@ -1,4 +1,4 @@ -import { ArrowLeft, MessageSquare, MoreHorizontal, Trash2 } from "lucide-react"; +import { ArrowLeft, MessageSquare } from "lucide-react"; import * as React from "react"; import { @@ -10,27 +10,12 @@ import type { ForumThreadResponse, ThreadReply } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext"; 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 { Skeleton } from "@/shared/ui/skeleton"; import { formatRelativeTime } from "../lib/time"; +import { DeleteActionMenu } from "./DeleteActionMenu"; import { ForumComposer } from "./ForumComposer"; type ForumThreadPanelProps = { @@ -62,43 +47,6 @@ function canDeleteReply( return reply.pubkey.toLowerCase() === currentPubkey.toLowerCase(); } -function DeleteConfirmDialog({ - open, - onOpenChange, - onConfirm, - label, -}: { - open: boolean; - onOpenChange: (open: boolean) => void; - onConfirm: () => void; - label: string; -}) { - return ( - - - - Delete {label}? - - This will permanently delete this {label} and cannot be undone. - - - - - - - - - - - - - ); -} - function ReplyRow({ reply, currentPubkey, @@ -112,7 +60,6 @@ function ReplyRow({ channelNames?: string[]; onDelete?: (eventId: string) => void; }) { - const [isDeleteOpen, setIsDeleteOpen] = React.useState(false); const replyAuthorLabel = resolveUserLabel({ pubkey: reply.pubkey, currentPubkey, @@ -140,33 +87,11 @@ function ReplyRow({ {showDelete ? ( -
- - - - - - setIsDeleteOpen(true)} - > - - Delete reply - - - - onDelete(reply.eventId)} - onOpenChange={setIsDeleteOpen} - open={isDeleteOpen} - /> -
+ onDelete(reply.eventId)} + /> ) : null}
@@ -198,7 +123,6 @@ export function ForumThreadPanel({ targetEventId, }: ForumThreadPanelProps) { const scrollRef = React.useRef(null); - const [isDeletePostOpen, setIsDeletePostOpen] = React.useState(false); const { channels } = useChannelNavigation(); const channelNames = React.useMemo( () => channels.filter((c) => c.channelType !== "dm").map((c) => c.name), @@ -298,33 +222,10 @@ export function ForumThreadPanel({
{canDeletePost && onDeletePost ? ( -
- - - - - - setIsDeletePostOpen(true)} - > - - Delete post - - - - onDeletePost(post.eventId)} - onOpenChange={setIsDeletePostOpen} - open={isDeletePostOpen} - /> -
+ onDeletePost(post.eventId)} + /> ) : null}
diff --git a/mobile/lib/features/forum/forum_post_card.dart b/mobile/lib/features/forum/forum_post_card.dart index 5597b42a2..308c65141 100644 --- a/mobile/lib/features/forum/forum_post_card.dart +++ b/mobile/lib/features/forum/forum_post_card.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../shared/theme/theme.dart'; +import '../channels/message_content.dart'; import '../profile/user_cache_provider.dart'; import '../profile/user_profile.dart'; import 'forum_models.dart'; @@ -33,6 +34,11 @@ class ForumPostCard extends ConsumerWidget { ref.watch(userCacheProvider.select((cache) => cache[pk])) ?? ref.read(userCacheProvider.notifier).get(pk); final displayName = profile?.label ?? _shortPubkey(post.pubkey); + final mentionNames = ref.watch( + userCacheProvider.select( + (cache) => _buildMentionNames(post.mentionPubkeys, cache), + ), + ); final preview = post.content.length > 200 ? '${post.content.substring(0, 200)}...' : post.content; @@ -94,13 +100,23 @@ class ForumPostCard extends ConsumerWidget { const SizedBox(height: Grid.xxs), // Content preview - Text( - preview, - style: context.textTheme.bodyMedium?.copyWith( - color: context.colors.onSurface, + ShaderMask( + shaderCallback: (bounds) => const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.white, Colors.transparent], + stops: [0.0, 0.75, 1.0], + ).createShader(bounds), + blendMode: BlendMode.dstIn, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 120), + child: IgnorePointer( + child: MessageContent( + content: preview, + mentionNames: mentionNames, + ), + ), ), - maxLines: 4, - overflow: TextOverflow.ellipsis, ), // Thread summary @@ -251,3 +267,17 @@ String _shortPubkey(String pubkey) { if (pubkey.length > 12) return '${pubkey.substring(0, 8)}\u2026'; return pubkey; } + +Map _buildMentionNames( + List mentionPubkeys, + Map userCache, +) { + final names = {}; + for (final pk in mentionPubkeys) { + final p = userCache[pk.toLowerCase()]; + if (p?.displayName != null) { + names[pk.toLowerCase()] = p!.displayName!; + } + } + return names; +}