ed50815902
- Channel store saves {serverId, channelId} to localStorage on select
- ServerBar restores last server+channel after fetching servers
- Fetches channels for the saved server, then sets the channel if valid
- Gracefully handles missing/deleted servers or channels
155 lines
6.1 KiB
TypeScript
155 lines
6.1 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useServerStore } from "../stores/server.ts";
|
|
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+/);
|
|
if (words.length === 1) {
|
|
return words[0].slice(0, 2).toUpperCase();
|
|
}
|
|
return (words[0][0] + words[words.length - 1][0]).toUpperCase();
|
|
}
|
|
|
|
export function ServerBar() {
|
|
const servers = useServerStore((state) => state.servers);
|
|
const activeServerId = useServerStore((state) => state.activeServerId);
|
|
const fetchServers = useServerStore((state) => state.fetchServers);
|
|
const setActiveServer = useServerStore((state) => state.setActiveServer);
|
|
const setActiveChannel = useChannelStore((state) => state.setActiveChannel);
|
|
const { isDM, setDM } = useLayoutStore();
|
|
const navigate = useNavigate();
|
|
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().then(() => {
|
|
// ponytail: restore last active server + channel from localStorage
|
|
try {
|
|
const raw = localStorage.getItem('dumpster:lastChannel');
|
|
if (raw) {
|
|
const { serverId, channelId } = JSON.parse(raw);
|
|
const srv = useServerStore.getState().servers.find((s) => s.id === serverId);
|
|
if (srv) {
|
|
setActiveServer(serverId);
|
|
setDM(false);
|
|
// Fetch channels then restore the channel
|
|
useChannelStore.getState().fetchChannels(serverId).then(() => {
|
|
const ch = useChannelStore.getState().channelsByServer[serverId]?.find((c) => c.id === channelId);
|
|
if (ch) {
|
|
setActiveChannel(channelId);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} catch { /* ignore bad localStorage */ }
|
|
});
|
|
}, [fetchServers, setActiveServer, setActiveChannel, setDM]);
|
|
|
|
const handleSelect = (id: string) => {
|
|
setActiveServer(id);
|
|
setActiveChannel(null);
|
|
setDM(false);
|
|
navigate('/');
|
|
};
|
|
|
|
const handleDM = () => {
|
|
setActiveServer(null);
|
|
setActiveChannel(null);
|
|
setDM(true);
|
|
navigate('/dm');
|
|
};
|
|
|
|
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">
|
|
<button
|
|
onClick={handleDM}
|
|
className={'w-11 h-11 flex items-center justify-center font-mono text-sm border ' +
|
|
(isDM
|
|
? 'bg-gb-bg-t text-gb-orange border-gb-orange'
|
|
: 'bg-gb-bg-s text-gb-fg border-gb-bg-t hover:border-gb-fg-t hover:text-gb-aqua')}
|
|
title="Direct messages"
|
|
>
|
|
[@]
|
|
</button>
|
|
<div className="w-8 h-px bg-gb-bg-t" />
|
|
{servers.map((server) => (
|
|
<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'
|
|
: 'bg-gb-bg-s text-gb-fg border-gb-bg-t hover:border-gb-fg-t hover:text-gb-aqua')}
|
|
title={server.name}
|
|
>
|
|
{server.unread && server.id !== activeServerId ? (
|
|
<span className="text-gb-aqua">[{getInitials(server.name)}]</span>
|
|
) : (
|
|
'[' + getInitials(server.name) + ']'
|
|
)}
|
|
</button>
|
|
))}
|
|
<div className="mt-2 flex flex-col gap-2 items-center">
|
|
<button
|
|
onClick={() => setShowCreate(true)}
|
|
className="w-11 h-11 flex items-center justify-center font-mono text-sm border bg-gb-bg-s text-gb-fg border-gb-bg-t hover:border-gb-fg-t hover:text-gb-aqua"
|
|
title="Create server"
|
|
>
|
|
[+]
|
|
</button>
|
|
<button
|
|
onClick={() => setShowJoin(true)}
|
|
className="w-11 h-11 flex items-center justify-center font-mono text-xs border bg-gb-bg-s text-gb-fg border-gb-bg-t hover:border-gb-fg-t hover:text-gb-aqua"
|
|
title="Join server"
|
|
>
|
|
[#]
|
|
</button>
|
|
</div>
|
|
</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}
|
|
</>
|
|
);
|
|
}
|