refactor: use react-resizable-panels for GroupListViewer

Changes:
- Replace custom resizable divider with react-resizable-panels
  - Uses ResizablePanelGroup, ResizablePanel, and ResizableHandle
  - Better UX with built-in resizing behavior
- Remove all icons from group list items
- Hide images and event embeds in last message preview
  - Pass showImages: false and showEventEmbeds: false to RichText
  - Cleaner message previews focusing on text content
- Remove group list header for cleaner appearance

Dependencies:
- Add react-resizable-panels package
This commit is contained in:
Claude
2026-01-13 08:56:44 +00:00
parent faa4028bd5
commit 9350c46774
3 changed files with 39 additions and 88 deletions

11
package-lock.json generated
View File

@@ -61,6 +61,7 @@
"react-markdown": "^10.1.0",
"react-medium-image-zoom": "^5.4.0",
"react-mosaic-component": "^6.1.1",
"react-resizable-panels": "^4.4.0",
"react-router": "^7.1.0",
"react-virtuoso": "^4.17.0",
"remark-gfm": "^4.0.1",
@@ -10346,6 +10347,16 @@
}
}
},
"node_modules/react-resizable-panels": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.4.0.tgz",
"integrity": "sha512-vGH1rIhyDOL4RSWYTx3eatjDohDFIRxJCAXUOaeL9HyamptUnUezqndjMtBo9hQeaq1CIP0NBbc7ZV3lBtlgxA==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/react-router": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",

View File

@@ -69,6 +69,7 @@
"react-markdown": "^10.1.0",
"react-medium-image-zoom": "^5.4.0",
"react-mosaic-component": "^6.1.1",
"react-resizable-panels": "^4.4.0",
"react-router": "^7.1.0",
"react-virtuoso": "^4.17.0",
"remark-gfm": "^4.0.1",

View File

@@ -1,7 +1,12 @@
import { useState, useMemo, useCallback, memo } from "react";
import { useState, useMemo, memo } from "react";
import { use$ } from "applesauce-react/hooks";
import { map } from "rxjs/operators";
import { Loader2, MessageSquare, GripVertical } from "lucide-react";
import { Loader2 } from "lucide-react";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "react-resizable-panels";
import eventStore from "@/services/event-store";
import pool from "@/services/relay-pool";
import accountManager from "@/services/accounts";
@@ -65,26 +70,30 @@ const GroupListItem = memo(function GroupListItem({
onClick={onClick}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0 flex-1">
<MessageSquare className="size-4 flex-shrink-0 text-muted-foreground" />
<span className="text-sm font-medium truncate">{groupName}</span>
</div>
<span className="text-sm font-medium truncate">{groupName}</span>
{group.lastMessage && (
<span className="text-xs text-muted-foreground flex-shrink-0">
<Timestamp timestamp={group.lastMessage.created_at} />
</span>
)}
</div>
{/* Last message preview */}
{/* Last message preview - hide images and event embeds */}
{lastMessageAuthor && lastMessageContent && (
<div className="text-xs text-muted-foreground pl-6 truncate">
<div className="text-xs text-muted-foreground truncate">
<UserName
pubkey={lastMessageAuthor}
className="text-xs font-medium"
/>
:{" "}
<span className="inline truncate">
<RichText content={lastMessageContent} className="inline" />
<RichText
content={lastMessageContent}
className="inline"
options={{
showImages: false,
showEventEmbeds: false,
}}
/>
</span>
</div>
)}
@@ -92,55 +101,6 @@ const GroupListItem = memo(function GroupListItem({
);
});
/**
* 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 (
<div
className={cn(
"w-1 bg-border hover:bg-primary/50 cursor-col-resize flex items-center justify-center transition-colors relative group",
isDragging && "bg-primary",
)}
onMouseDown={handleMouseDown}
>
<div className="absolute inset-y-0 left-1/2 -translate-x-1/2 w-4 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<GripVertical className="size-3 text-muted-foreground" />
</div>
</div>
);
}
/**
* MemoizedChatViewer - Memoized chat viewer to prevent unnecessary re-renders
*/
@@ -186,18 +146,6 @@ 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$(
() =>
@@ -411,19 +359,10 @@ export function GroupListViewer() {
}
return (
<div className="flex h-full">
<ResizablePanelGroup direction="horizontal" className="h-full">
{/* Left panel: Group list */}
<div className="flex flex-col border-r" style={{ width: sidebarWidth }}>
{/* Header matching ChatViewer style */}
<div className="pl-4 pr-4 border-b w-full py-0.5">
<div className="flex items-center justify-between gap-3">
<div className="text-sm font-semibold">Groups</div>
<div className="text-xs text-muted-foreground">
{groupsWithRecency.length}
</div>
</div>
</div>
<div className="flex-1 overflow-y-auto">
<ResizablePanel defaultSize={25} minSize={15} maxSize={40}>
<div className="flex h-full flex-col overflow-y-auto">
{groupsWithRecency.map((group) => (
<GroupListItem
key={`${group.relayUrl}'${group.groupId}`}
@@ -441,13 +380,13 @@ export function GroupListViewer() {
/>
))}
</div>
</div>
</ResizablePanel>
{/* Resizable divider */}
<ResizableDivider onResize={handleResize} />
{/* Resizable handle */}
<ResizableHandle withHandle />
{/* Right panel: Chat view */}
<div className="flex-1 min-w-0">
<ResizablePanel defaultSize={75}>
{selectedGroup ? (
<MemoizedChatViewer
groupId={selectedGroup.groupId}
@@ -458,7 +397,7 @@ export function GroupListViewer() {
Select a group to view chat
</div>
)}
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
);
}