feat: right-click context menus for channels, groups, and servers
ContextMenu: reusable hook-based component that positions a menu at
the cursor and auto-closes on outside click or Escape.
ChannelList:
- Right-click channel -> Edit Name, Permissions, Delete Channel
- Right-click group header -> Edit Name, Create Channel Here, Delete Group
- Inline rename: replacing name text with an input, commits on Enter/blur
- CreateChannelModal now accepts defaultGroupId to pre-select a group
ServerBar:
- Right-click server icon -> Server Settings, Invite People, Leave Server
- Leave calls DELETE /servers/{id}/members/me
Backend:
- Added LeaveServer handler (DELETE /servers/{serverID}/members/me)
- Server owner cannot leave; must transfer or delete
This commit is contained in:
@@ -4,6 +4,10 @@ import { useChannelStore } from '../stores/channel.ts';
|
||||
import { useLayoutStore } from '../stores/layout.ts';
|
||||
import { CreateServerModal } from './CreateServerModal.tsx';
|
||||
import { JoinServerModal } from './JoinServerModal.tsx';
|
||||
import { ServerSettingsModal } from './ServerSettingsModal.tsx';
|
||||
import { InviteModal } from './InviteModal.tsx';
|
||||
import { useContextMenu } from './ContextMenu.tsx';
|
||||
import { api } from '../lib/api.ts';
|
||||
|
||||
function getInitials(name: string): string {
|
||||
const words = name.trim().split(/\s+/);
|
||||
@@ -22,6 +26,10 @@ export function ServerBar() {
|
||||
const { isDM, setDM } = useLayoutStore();
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [showJoin, setShowJoin] = useState(false);
|
||||
const [settingsServerId, setSettingsServerId] = useState<string | null>(null);
|
||||
const [inviteServer, setInviteServer] = useState<{ id: string; name: string } | null>(null);
|
||||
|
||||
const { showMenu, MenuPortal } = useContextMenu();
|
||||
|
||||
useEffect(() => {
|
||||
fetchServers();
|
||||
@@ -39,6 +47,26 @@ export function ServerBar() {
|
||||
setDM(true);
|
||||
};
|
||||
|
||||
const handleServerContextMenu = (e: React.MouseEvent, server: { id: string; name: string }) => {
|
||||
showMenu(e, [
|
||||
{ label: 'Server Settings', icon: '⚙', onClick: () => setSettingsServerId(server.id) },
|
||||
{ label: 'Invite People', icon: '📨', onClick: () => setInviteServer({ id: server.id, name: server.name }) },
|
||||
{ label: 'Leave Server', icon: '🚪', danger: true, onClick: () => leaveServer(server.id, server.name) },
|
||||
]);
|
||||
};
|
||||
|
||||
const leaveServer = async (serverId: string, serverName: string) => {
|
||||
if (!confirm(`Leave "${serverName}"? You'll need an invite to rejoin.`)) return;
|
||||
try {
|
||||
await api.delete(`/servers/${serverId}/members/me`);
|
||||
fetchServers();
|
||||
if (activeServerId === serverId) {
|
||||
setActiveServer(null);
|
||||
setDM(true);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full w-16 bg-gb-bg-h border-r border-gb-bg-t flex flex-col items-center py-3 gap-2 overflow-y-auto">
|
||||
@@ -57,6 +85,7 @@ export function ServerBar() {
|
||||
<button
|
||||
key={server.id}
|
||||
onClick={() => handleSelect(server.id)}
|
||||
onContextMenu={(e) => handleServerContextMenu(e, server)}
|
||||
className={'w-11 h-11 flex items-center justify-center font-mono text-sm border ' +
|
||||
(server.id === activeServerId && !isDM
|
||||
? 'bg-gb-bg-t text-gb-orange border-gb-orange'
|
||||
@@ -89,6 +118,13 @@ export function ServerBar() {
|
||||
</div>
|
||||
{showCreate && <CreateServerModal onClose={() => setShowCreate(false)} />}
|
||||
{showJoin && <JoinServerModal onClose={() => setShowJoin(false)} />}
|
||||
{settingsServerId && (
|
||||
<ServerSettingsModal serverId={settingsServerId} onClose={() => setSettingsServerId(null)} />
|
||||
)}
|
||||
{inviteServer && (
|
||||
<InviteModal serverId={inviteServer.id} serverName={inviteServer.name} onClose={() => setInviteServer(null)} />
|
||||
)}
|
||||
{MenuPortal}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user