diff --git a/src/components/GroupListViewer.tsx b/src/components/GroupListViewer.tsx index 816e2a9..1f79621 100644 --- a/src/components/GroupListViewer.tsx +++ b/src/components/GroupListViewer.tsx @@ -1,7 +1,7 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, useCallback, memo } from "react"; import { use$ } from "applesauce-react/hooks"; import { map } from "rxjs/operators"; -import { Loader2, MessageSquare } from "lucide-react"; +import { Loader2, MessageSquare, GripVertical } from "lucide-react"; import eventStore from "@/services/event-store"; import pool from "@/services/relay-pool"; import accountManager from "@/services/accounts"; @@ -12,12 +12,14 @@ import type { ProtocolIdentifier } from "@/types/chat"; import { cn } from "@/lib/utils"; import Timestamp from "./Timestamp"; import { useEffect } from "react"; +import { UserName } from "./nostr/UserName"; +import { RichText } from "./nostr/RichText"; interface GroupInfo { groupId: string; relayUrl: string; metadata?: NostrEvent; - lastMessageTimestamp?: number; + lastMessage?: NostrEvent; } /** @@ -30,7 +32,7 @@ function formatRelayForDisplay(url: string): string { /** * GroupListItem - Single group in the list */ -function GroupListItem({ +const GroupListItem = memo(function GroupListItem({ group, isSelected, onClick, @@ -50,48 +52,124 @@ function GroupListItem({ groupName = group.groupId; } - // Extract group icon - const groupIcon = - !isUnmanagedGroup && group.metadata && group.metadata.kind === 39000 - ? getTagValue(group.metadata, "picture") - : undefined; + // Get last message author and content + const lastMessageAuthor = group.lastMessage?.pubkey; + const lastMessageContent = group.lastMessage?.content; return (
-
- {groupIcon ? ( - { - e.currentTarget.style.display = "none"; - }} - /> - ) : ( -
- -
+
+
+ + {groupName} +
+ {group.lastMessage && ( + + + )}
-
-
{groupName}
- {group.lastMessageTimestamp && ( -
- -
- )} + {/* Last message preview */} + {lastMessageAuthor && lastMessageContent && ( +
+ + :{" "} + + + +
+ )} +
+ ); +}); + +/** + * ResizableDivider - Draggable divider for resizing panels + */ +function ResizableDivider({ + onResize, +}: { + onResize: (deltaX: number) => void; +}) { + const [isDragging, setIsDragging] = useState(false); + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + setIsDragging(true); + + const startX = e.clientX; + + const handleMouseMove = (moveEvent: MouseEvent) => { + const deltaX = moveEvent.clientX - startX; + onResize(deltaX); + }; + + const handleMouseUp = () => { + setIsDragging(false); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, + [onResize], + ); + + return ( +
+
+
); } +/** + * MemoizedChatViewer - Memoized chat viewer to prevent unnecessary re-renders + */ +const MemoizedChatViewer = memo( + function MemoizedChatViewer({ + groupId, + relayUrl, + }: { + groupId: string; + relayUrl: string; + }) { + return ( + + ); + }, + // Custom comparison: only re-render if group actually changed + (prev, next) => + prev.groupId === next.groupId && prev.relayUrl === next.relayUrl, +); + /** * GroupListViewer - Multi-room chat interface * @@ -108,6 +186,18 @@ export function GroupListViewer() { relayUrl: string; } | null>(null); + // State for sidebar width + const [sidebarWidth, setSidebarWidth] = useState(280); + + // Handle resize + const handleResize = useCallback((deltaX: number) => { + setSidebarWidth((prev) => { + const newWidth = prev + deltaX; + // Clamp between 200px and 500px + return Math.max(200, Math.min(500, newWidth)); + }); + }, []); + // Load user's kind 10009 (group list) event const groupListEvent = use$( () => @@ -254,14 +344,14 @@ export function GroupListViewer() { ) .pipe( map((events) => { - // Create a map of groupId -> latest message timestamp - const recencyMap = new Map(); + // Create a map of groupId -> latest message + const messageMap = new Map(); for (const evt of events) { const hTag = evt.tags.find((t) => t[0] === "h"); if (hTag && hTag[1]) { - const existing = recencyMap.get(hTag[1]); - if (!existing || evt.created_at > existing) { - recencyMap.set(hTag[1], evt.created_at); + const existing = messageMap.get(hTag[1]); + if (!existing || evt.created_at > existing.created_at) { + messageMap.set(hTag[1], evt); } } } @@ -271,13 +361,13 @@ export function GroupListViewer() { groupId: g.groupId, relayUrl: g.relayUrl, metadata: groupMetadataMap?.get(g.groupId), - lastMessageTimestamp: recencyMap.get(g.groupId), + lastMessage: messageMap.get(g.groupId), })); // Sort by recency (most recent first) groupsWithInfo.sort((a, b) => { - const aTime = a.lastMessageTimestamp || 0; - const bTime = b.lastMessageTimestamp || 0; + const aTime = a.lastMessage?.created_at || 0; + const bTime = b.lastMessage?.created_at || 0; return bTime - aTime; }); @@ -286,17 +376,6 @@ export function GroupListViewer() { ); }, [groups, groupMetadataMap]); - // Auto-select first group if none selected - useEffect(() => { - if (!selectedGroup && groupsWithRecency && groupsWithRecency.length > 0) { - const first = groupsWithRecency[0]; - setSelectedGroup({ - groupId: first.groupId, - relayUrl: first.relayUrl, - }); - } - }, [selectedGroup, groupsWithRecency]); - if (!activePubkey) { return (
@@ -334,12 +413,14 @@ export function GroupListViewer() { return (
{/* Left panel: Group list */} -
-
-
Groups
-
- {groupsWithRecency.length}{" "} - {groupsWithRecency.length === 1 ? "group" : "groups"} +
+ {/* Header matching ChatViewer style */} +
+
+
Groups
+
+ {groupsWithRecency.length} +
@@ -362,18 +443,15 @@ export function GroupListViewer() {
+ {/* Resizable divider */} + + {/* Right panel: Chat view */} -
+
{selectedGroup ? ( - ) : (