refactor: rename Server → Space across entire codebase
Renames all domain terminology from "Server" to "Space" throughout the application — database schema, API routes, shared types, stores, components, and UI strings. Files renamed: ServerSidebar → SpaceSidebar, CreateServer → CreateSpace, JoinServer → JoinSpace, ServerSettings → SpaceSettings, serverStore → spaceStore, routes/servers → routes/spaces.
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { ServerSidebar } from './ServerSidebar';
|
||||
import { SpaceSidebar } from './SpaceSidebar';
|
||||
import { ChannelSidebar } from './ChannelSidebar';
|
||||
import { MainContent } from './MainContent';
|
||||
import { RightPanel } from './RightPanel';
|
||||
import { MobileNav } from './MobileNav';
|
||||
import { ImagePreview } from '../chat/ImagePreview';
|
||||
import { CreateServerModal } from '../modals/CreateServer';
|
||||
import { JoinServerModal } from '../modals/JoinServer';
|
||||
import { CreateSpaceModal } from '../modals/CreateSpace';
|
||||
import { JoinSpaceModal } from '../modals/JoinSpace';
|
||||
import { CreateChannelModal } from '../modals/CreateChannel';
|
||||
import { InviteModal } from '../modals/InviteModal';
|
||||
import { UserSettingsModal } from '../modals/UserSettings';
|
||||
import { ServerSettingsModal } from '../modals/ServerSettings';
|
||||
import { SpaceSettingsModal } from '../modals/SpaceSettings';
|
||||
import { ChannelSettingsModal } from '../modals/ChannelSettingsModal';
|
||||
import { NewDmModal } from '../modals/NewDmModal';
|
||||
import { AddDmMemberModal } from '../modals/AddDmMemberModal';
|
||||
@@ -25,14 +25,14 @@ import { useAuth } from '../../hooks/useAuth';
|
||||
import { useWebSocket } from '../../hooks/useWebSocket';
|
||||
import { useFederationToasts } from '../../hooks/useFederationToasts';
|
||||
import { useLiveKit } from '../../hooks/useLiveKit';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
import { useSpaceStore } from '../../stores/spaceStore';
|
||||
import { useChatStore } from '../../stores/chatStore';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { useVoiceStore } from '../../stores/voiceStore';
|
||||
import { AudioManager } from '../../audio/AudioManager';
|
||||
|
||||
export function AppLayout() {
|
||||
const { serverId, channelId, inviteCode } = useParams<{ serverId?: string; channelId?: string; inviteCode?: string }>();
|
||||
const { spaceId, channelId, inviteCode } = useParams<{ spaceId?: string; channelId?: string; inviteCode?: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Global interaction handler to resume AudioContext
|
||||
@@ -83,8 +83,8 @@ export function AppLayout() {
|
||||
}, [outputDeviceId]);
|
||||
|
||||
const { user, isLoading } = useAuth();
|
||||
const setCurrentServer = useServerStore((s) => s.setCurrentServer);
|
||||
const loadServerDetail = useServerStore((s) => s.loadServerDetail);
|
||||
const setCurrentSpace = useSpaceStore((s) => s.setCurrentSpace);
|
||||
const loadSpaceDetail = useSpaceStore((s) => s.loadSpaceDetail);
|
||||
const setCurrentChannel = useChatStore((s) => s.setCurrentChannel);
|
||||
const loadMessages = useChatStore((s) => s.loadMessages);
|
||||
const setIsMobile = useUIStore((s) => s.setIsMobile);
|
||||
@@ -96,7 +96,7 @@ export function AppLayout() {
|
||||
const userProfilePopout = useUIStore((s) => s.userProfilePopout);
|
||||
const closeUserProfile = useUIStore((s) => s.closeUserProfile);
|
||||
|
||||
const channels = useServerStore((s) => s.channels);
|
||||
const channels = useSpaceStore((s) => s.channels);
|
||||
|
||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
|
||||
@@ -185,22 +185,22 @@ export function AppLayout() {
|
||||
|
||||
// Handle route params
|
||||
useEffect(() => {
|
||||
if (serverId === '@me') {
|
||||
if (spaceId === '@me') {
|
||||
setShowDms(true);
|
||||
setCurrentServer(null);
|
||||
} else if (serverId) {
|
||||
setCurrentSpace(null);
|
||||
} else if (spaceId) {
|
||||
setShowDms(false);
|
||||
setCurrentServer(serverId);
|
||||
loadServerDetail(serverId);
|
||||
setCurrentSpace(spaceId);
|
||||
loadSpaceDetail(spaceId);
|
||||
} else {
|
||||
setShowDms(false);
|
||||
setCurrentServer(null);
|
||||
setCurrentSpace(null);
|
||||
}
|
||||
}, [serverId, setCurrentServer, loadServerDetail, setShowDms]);
|
||||
}, [spaceId, setCurrentSpace, loadSpaceDetail, setShowDms]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inviteCode) {
|
||||
openModal('joinServer');
|
||||
openModal('joinSpace');
|
||||
}
|
||||
}, [inviteCode, openModal]);
|
||||
|
||||
@@ -208,35 +208,35 @@ export function AppLayout() {
|
||||
if (channelId) {
|
||||
setCurrentChannel(channelId);
|
||||
loadMessages(channelId);
|
||||
if (serverId && serverId !== '@me') {
|
||||
useUIStore.getState().setLastChannel(serverId, channelId);
|
||||
if (spaceId && spaceId !== '@me') {
|
||||
useUIStore.getState().setLastChannel(spaceId, channelId);
|
||||
}
|
||||
} else {
|
||||
setCurrentChannel(null);
|
||||
}
|
||||
}, [channelId, serverId, setCurrentChannel, loadMessages]);
|
||||
}, [channelId, spaceId, setCurrentChannel, loadMessages]);
|
||||
|
||||
// Auto-select last visited (or first) channel when opening a server without a channelId
|
||||
useEffect(() => {
|
||||
if (!serverId || serverId === '@me' || channelId) return;
|
||||
if (!spaceId || spaceId === '@me' || channelId) return;
|
||||
if (channels.length === 0) return;
|
||||
|
||||
const firstChannel = channels[0];
|
||||
if (!firstChannel) return;
|
||||
|
||||
// Guard: only redirect when channels belong to the target server.
|
||||
// `channels` is shared per-view state set by loadServerDetail (async).
|
||||
// `channels` is shared per-view state set by loadSpaceDetail (async).
|
||||
// Without this check, stale channels from a previously viewed server
|
||||
// would cause a wrong redirect during the fetch window.
|
||||
const { channelToServerMap } = useServerStore.getState();
|
||||
if (channelToServerMap.get(firstChannel.id) !== serverId) return;
|
||||
const { channelToSpaceMap } = useSpaceStore.getState();
|
||||
if (channelToSpaceMap.get(firstChannel.id) !== spaceId) return;
|
||||
|
||||
const lastId = useUIStore.getState().lastChannelPerServer[serverId];
|
||||
const lastId = useUIStore.getState().lastChannelPerSpace[spaceId];
|
||||
const target = (lastId && channels.find((c) => c.id === lastId)) || firstChannel;
|
||||
if (target) {
|
||||
navigate(`/channels/${serverId}/${target.id}`, { replace: true });
|
||||
navigate(`/channels/${spaceId}/${target.id}`, { replace: true });
|
||||
}
|
||||
}, [serverId, channelId, channels, navigate]);
|
||||
}, [spaceId, channelId, channels, navigate]);
|
||||
|
||||
if (isLoading || !user) {
|
||||
return (
|
||||
@@ -254,9 +254,9 @@ export function AppLayout() {
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col md:grid md:grid-cols-[312px_1fr] md:grid-rows-[minmax(0,1fr)] bg-surface-base overflow-hidden">
|
||||
{/* Server sidebar - always visible on desktop, toggled on mobile */}
|
||||
{/* Space sidebar - always visible on desktop, toggled on mobile */}
|
||||
<div className={`fixed inset-y-0 left-0 z-40 flex w-[312px] transition-transform duration-200 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'} md:static md:z-auto md:w-auto md:transform-none`}>
|
||||
<ServerSidebar />
|
||||
<SpaceSidebar />
|
||||
<ChannelSidebar />
|
||||
</div>
|
||||
|
||||
@@ -267,12 +267,12 @@ export function AppLayout() {
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<CreateServerModal />
|
||||
<JoinServerModal />
|
||||
<CreateSpaceModal />
|
||||
<JoinSpaceModal />
|
||||
<CreateChannelModal />
|
||||
<InviteModal />
|
||||
<UserSettingsModal />
|
||||
<ServerSettingsModal />
|
||||
<SpaceSettingsModal />
|
||||
<ChannelSettingsModal />
|
||||
<NewDmModal />
|
||||
<AddDmMemberModal />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useServerStore, getChannelOrigin } from '../../stores/serverStore';
|
||||
import { useSpaceStore, getChannelOrigin } from '../../stores/spaceStore';
|
||||
import { useChatStore } from '../../stores/chatStore';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
@@ -18,16 +18,16 @@ import { parseFederatedUsername, isSelf } from '../../utils/identity';
|
||||
import { joinVoiceChannel } from '../../utils/voice';
|
||||
|
||||
export function ChannelSidebar() {
|
||||
const servers = useServerStore((s) => s.servers);
|
||||
const currentServerId = useServerStore((s) => s.currentServerId);
|
||||
const channels = useServerStore((s) => s.channels);
|
||||
const dmChannels = useServerStore((s) => s.dmChannels);
|
||||
const spaces = useSpaceStore((s) => s.spaces);
|
||||
const currentSpaceId = useSpaceStore((s) => s.currentSpaceId);
|
||||
const channels = useSpaceStore((s) => s.channels);
|
||||
const dmChannels = useSpaceStore((s) => s.dmChannels);
|
||||
const currentChannelId = useChatStore((s) => s.currentChannelId);
|
||||
const setCurrentChannel = useChatStore((s) => s.setCurrentChannel);
|
||||
const unreadChannels = useChatStore((s) => s.unreadChannels);
|
||||
const openModal = useUIStore((s) => s.openModal);
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const members = useServerStore((s) => s.members);
|
||||
const members = useSpaceStore((s) => s.members);
|
||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
|
||||
const isMuted = useVoiceStore((s) => s.isMuted);
|
||||
@@ -72,26 +72,26 @@ export function ChannelSidebar() {
|
||||
}
|
||||
};
|
||||
|
||||
const serverPermissions = useServerStore((s) => s.serverPermissions);
|
||||
const server = servers.find(s => s.id === currentServerId);
|
||||
const myServerPerms = currentServerId ? serverPermissions.get(currentServerId) : undefined;
|
||||
const spacePermissions = useSpaceStore((s) => s.spacePermissions);
|
||||
const space = spaces.find(s => s.id === currentSpaceId);
|
||||
const mySpacePerms = currentSpaceId ? spacePermissions.get(currentSpaceId) : undefined;
|
||||
|
||||
const federationInstances = useInstanceStore((s) => s.instances);
|
||||
const instanceLabel = useMemo(() => {
|
||||
const origin = (server as any)?._instanceOrigin;
|
||||
const origin = (space as any)?._instanceOrigin;
|
||||
if (!origin) return null;
|
||||
const inst = federationInstances.find(i => i.origin === origin);
|
||||
if (inst) return inst.label;
|
||||
try { return new URL(origin).host; } catch { return origin; }
|
||||
}, [server, federationInstances]);
|
||||
const canManageChannels = hasPermissionBit(myServerPerms, PermissionBits.MANAGE_CHANNELS);
|
||||
}, [space, federationInstances]);
|
||||
const canManageChannels = hasPermissionBit(mySpacePerms, PermissionBits.MANAGE_CHANNELS);
|
||||
|
||||
const textChannels = channels.filter(c => c.type === 'text');
|
||||
const voiceChannels = channels.filter(c => c.type === 'voice' || c.type === 'video');
|
||||
|
||||
const handleChannelClick = (channelId: string) => {
|
||||
setCurrentChannel(channelId);
|
||||
navigate(`/channels/${currentServerId || '@me'}/${channelId}`);
|
||||
navigate(`/channels/${currentSpaceId || '@me'}/${channelId}`);
|
||||
};
|
||||
|
||||
const handleHomeClick = () => {
|
||||
@@ -102,11 +102,11 @@ export function ChannelSidebar() {
|
||||
const handleVoiceJoin = (channelId: string) => {
|
||||
// Don't re-join the same channel — prevents duplicate LiveKit connections
|
||||
if (currentVoiceChannelId === channelId) {
|
||||
navigate(`/channels/${currentServerId}/${channelId}`);
|
||||
navigate(`/channels/${currentSpaceId}/${channelId}`);
|
||||
return;
|
||||
}
|
||||
joinVoiceChannel(channelId);
|
||||
navigate(`/channels/${currentServerId}/${channelId}`);
|
||||
navigate(`/channels/${currentSpaceId}/${channelId}`);
|
||||
};
|
||||
|
||||
// Floating bottom panel — shared between DM view and server view
|
||||
@@ -130,7 +130,7 @@ export function ChannelSidebar() {
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
if (!server) {
|
||||
if (!space) {
|
||||
return (
|
||||
<>
|
||||
<div className="w-60 md:w-full bg-surface-channel flex flex-col flex-shrink-0 select-none md:pl-[72px] border-r border-border-hard">
|
||||
@@ -260,7 +260,7 @@ export function ChannelSidebar() {
|
||||
navigate('/channels/@me');
|
||||
setCurrentChannel(null);
|
||||
}
|
||||
useServerStore.getState().closeDm(dm.id);
|
||||
useSpaceStore.getState().closeDm(dm.id);
|
||||
}}
|
||||
className="opacity-0 group-hover:opacity-100 text-txt-tertiary hover:text-txt-primary transition-opacity flex-shrink-0 ml-1"
|
||||
title="Close DM"
|
||||
@@ -287,14 +287,14 @@ export function ChannelSidebar() {
|
||||
return (
|
||||
<>
|
||||
<div className="w-60 md:w-full bg-surface-channel flex flex-col flex-shrink-0 select-none md:pl-[72px] border-r border-border-hard">
|
||||
{/* Server header */}
|
||||
{/* Space header */}
|
||||
<div className="h-12 flex items-center border-b border-border-hard z-10 group/header">
|
||||
<button
|
||||
onClick={() => openModal('serverSettings')}
|
||||
onClick={() => openModal('spaceSettings')}
|
||||
className="flex-1 h-full px-4 flex items-center justify-between hover:bg-interactive-hover transition-colors min-w-0"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<span className="font-bold text-[15px] tracking-[-0.02em] text-txt-primary truncate leading-tight block">{server.name}</span>
|
||||
<span className="font-bold text-[15px] tracking-[-0.02em] text-txt-primary truncate leading-tight block">{space.name}</span>
|
||||
{instanceLabel && (
|
||||
<span className="text-[10px] text-txt-tertiary font-medium truncate block leading-tight">
|
||||
{instanceLabel}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
import { useSpaceStore } from '../../stores/spaceStore';
|
||||
import { useChatStore } from '../../stores/chatStore';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
@@ -20,9 +20,9 @@ import { joinVoiceChannel } from '../../utils/voice';
|
||||
|
||||
export function MainContent() {
|
||||
// 1. ALL HOOKS AT THE TOP
|
||||
const channels = useServerStore((s) => s.channels);
|
||||
const channels = useSpaceStore((s) => s.channels);
|
||||
const currentChannelId = useChatStore((s) => s.currentChannelId);
|
||||
const currentServerId = useServerStore((s) => s.currentServerId);
|
||||
const currentSpaceId = useSpaceStore((s) => s.currentSpaceId);
|
||||
const voiceChatOpen = useUIStore((s) => s.voiceChatOpen);
|
||||
const voiceFullscreen = useUIStore((s) => s.voiceFullscreen);
|
||||
const setVoiceFullscreen = useUIStore((s) => s.setVoiceFullscreen);
|
||||
@@ -35,7 +35,7 @@ export function MainContent() {
|
||||
const isExplorePage = location.pathname === '/explore';
|
||||
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
|
||||
const outgoingCall = useVoiceStore((s) => s.outgoingCall);
|
||||
const dmChannels = useServerStore((s) => s.dmChannels);
|
||||
const dmChannels = useSpaceStore((s) => s.dmChannels);
|
||||
const authUser = useAuthStore((s) => s.user);
|
||||
const openModal = useUIStore((s) => s.openModal);
|
||||
|
||||
@@ -64,7 +64,7 @@ export function MainContent() {
|
||||
const channel = channels.find(c => c.id === currentChannelId);
|
||||
const isVoiceChannel = channel?.type === 'voice' || channel?.type === 'video';
|
||||
|
||||
if (showDms || isExplorePage || !currentServerId) {
|
||||
if (showDms || isExplorePage || !currentSpaceId) {
|
||||
if (!currentChannelId) {
|
||||
if (isExplorePage) return <ExplorePage />;
|
||||
return <FriendsPage />;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { MemberWithUser } from '@backspace/shared';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
import { useSpaceStore } from '../../stores/spaceStore';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { Avatar } from '../ui/Avatar';
|
||||
import { Username } from '../ui/Username';
|
||||
@@ -42,14 +42,14 @@ function getMemberGroup(member: MemberWithUser, ownerId: string | undefined) {
|
||||
}
|
||||
|
||||
export function MemberSidebar() {
|
||||
const members = useServerStore((s) => s.members);
|
||||
const servers = useServerStore((s) => s.servers);
|
||||
const currentServerId = useServerStore((s) => s.currentServerId);
|
||||
const members = useSpaceStore((s) => s.members);
|
||||
const spaces = useSpaceStore((s) => s.spaces);
|
||||
const currentSpaceId = useSpaceStore((s) => s.currentSpaceId);
|
||||
const memberListOpen = useUIStore((s) => s.memberListOpen);
|
||||
const openUserProfile = useUIStore((s) => s.openUserProfile);
|
||||
|
||||
const server = servers.find(s => s.id === currentServerId);
|
||||
const ownerId = server?.ownerId;
|
||||
const space = spaces.find(s => s.id === currentSpaceId);
|
||||
const ownerId = space?.ownerId;
|
||||
|
||||
const { roleGroups, offlineMembers } = useMemo(() => {
|
||||
const online = members.filter(m => m.user.status !== 'offline');
|
||||
|
||||
@@ -2,13 +2,13 @@ import React from 'react';
|
||||
import { MemberSidebar } from './MemberSidebar';
|
||||
import { ActivityPanel } from './ActivityPanel';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
import { useSpaceStore } from '../../stores/spaceStore';
|
||||
|
||||
export function RightPanel() {
|
||||
const showDms = useUIStore((s) => s.showDms);
|
||||
const currentServerId = useServerStore((s) => s.currentServerId);
|
||||
const currentSpaceId = useSpaceStore((s) => s.currentSpaceId);
|
||||
|
||||
if (showDms || !currentServerId) {
|
||||
if (showDms || !currentSpaceId) {
|
||||
return <ActivityPanel />;
|
||||
}
|
||||
|
||||
|
||||
+57
-57
@@ -1,11 +1,11 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
import { useSpaceStore } from '../../stores/spaceStore';
|
||||
import { useChatStore } from '../../stores/chatStore';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { useInstanceStore } from '../../stores/instanceStore';
|
||||
|
||||
import { getServerGradient, HOME_GRADIENT } from '../../utils/gradients';
|
||||
import { getSpaceGradient, HOME_GRADIENT } from '../../utils/gradients';
|
||||
|
||||
interface SidebarItemProps {
|
||||
id: string;
|
||||
@@ -13,13 +13,13 @@ interface SidebarItemProps {
|
||||
icon?: string | null;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
type?: 'server' | 'dm' | 'action';
|
||||
type?: 'space' | 'dm' | 'action';
|
||||
actionType?: 'add' | 'join' | 'explore';
|
||||
hasUnread?: boolean;
|
||||
dimmed?: boolean;
|
||||
}
|
||||
|
||||
function SidebarItem({ id, name, icon, active, onClick, type = 'server', actionType, hasUnread, dimmed }: SidebarItemProps) {
|
||||
function SidebarItem({ id, name, icon, active, onClick, type = 'space', actionType, hasUnread, dimmed }: SidebarItemProps) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const firstLetter = name.charAt(0).toUpperCase();
|
||||
|
||||
@@ -41,11 +41,11 @@ function SidebarItem({ id, name, icon, active, onClick, type = 'server', actionT
|
||||
return { background: HOME_GRADIENT.gradient };
|
||||
}
|
||||
|
||||
// Server type — if it has a custom icon image, no gradient needed
|
||||
// Space type — if it has a custom icon image, no gradient needed
|
||||
if (icon) return undefined;
|
||||
|
||||
const serverGrad = getServerGradient(id, name);
|
||||
return { background: serverGrad.gradient };
|
||||
const spaceGrad = getSpaceGradient(id, name);
|
||||
return { background: spaceGrad.gradient };
|
||||
}, [type, id, name, icon, isHovered]);
|
||||
|
||||
const getButtonClasses = () => {
|
||||
@@ -73,7 +73,7 @@ function SidebarItem({ id, name, icon, active, onClick, type = 'server', actionT
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* Pill Indicator */}
|
||||
{(type === 'server' || type === 'dm') && (
|
||||
{(type === 'space' || type === 'dm') && (
|
||||
<div className="absolute -left-0 w-2 h-10 flex items-center">
|
||||
<div
|
||||
className={`bg-white rounded-r-full transition-all duration-200 origin-left ${getPillHeight()} w-1`}
|
||||
@@ -112,12 +112,12 @@ function SidebarItem({ id, name, icon, active, onClick, type = 'server', actionT
|
||||
);
|
||||
}
|
||||
|
||||
export function ServerSidebar() {
|
||||
const servers = useServerStore((s) => s.servers);
|
||||
const currentServerId = useServerStore((s) => s.currentServerId);
|
||||
const setCurrentServer = useServerStore((s) => s.setCurrentServer);
|
||||
const channelToServerMap = useServerStore((s) => s.channelToServerMap);
|
||||
const dmChannels = useServerStore((s) => s.dmChannels);
|
||||
export function SpaceSidebar() {
|
||||
const spaces = useSpaceStore((s) => s.spaces);
|
||||
const currentSpaceId = useSpaceStore((s) => s.currentSpaceId);
|
||||
const setCurrentSpace = useSpaceStore((s) => s.setCurrentSpace);
|
||||
const channelToSpaceMap = useSpaceStore((s) => s.channelToSpaceMap);
|
||||
const dmChannels = useSpaceStore((s) => s.dmChannels);
|
||||
const showDms = useUIStore((s) => s.showDms);
|
||||
const setShowDms = useUIStore((s) => s.setShowDms);
|
||||
const openModal = useUIStore((s) => s.openModal);
|
||||
@@ -128,11 +128,11 @@ export function ServerSidebar() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// Group servers by origin
|
||||
const groupedServers = useMemo(() => {
|
||||
const home = servers.filter(s => !(s as any)._instanceOrigin);
|
||||
const remoteMap = new Map<string, typeof servers>();
|
||||
for (const s of servers) {
|
||||
// Group spaces by origin
|
||||
const groupedSpaces = useMemo(() => {
|
||||
const home = spaces.filter(s => !(s as any)._instanceOrigin);
|
||||
const remoteMap = new Map<string, typeof spaces>();
|
||||
for (const s of spaces) {
|
||||
const origin = (s as any)._instanceOrigin;
|
||||
if (!origin) continue;
|
||||
const list = remoteMap.get(origin) || [];
|
||||
@@ -140,7 +140,7 @@ export function ServerSidebar() {
|
||||
remoteMap.set(origin, list);
|
||||
}
|
||||
return { home, remoteGroups: Array.from(remoteMap.entries()) };
|
||||
}, [servers]);
|
||||
}, [spaces]);
|
||||
|
||||
// Set of disconnected origins
|
||||
const disconnectedOrigins = useMemo(() => {
|
||||
@@ -153,15 +153,15 @@ export function ServerSidebar() {
|
||||
return set;
|
||||
}, [instances]);
|
||||
|
||||
// Compute which servers have unread channels
|
||||
const unreadServerIds = useMemo(() => {
|
||||
// Compute which spaces have unread channels
|
||||
const unreadSpaceIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
for (const channelId of unreadChannels) {
|
||||
const serverId = channelToServerMap.get(channelId);
|
||||
if (serverId) ids.add(serverId);
|
||||
const spaceId = channelToSpaceMap.get(channelId);
|
||||
if (spaceId) ids.add(spaceId);
|
||||
}
|
||||
return ids;
|
||||
}, [unreadChannels, channelToServerMap]);
|
||||
}, [unreadChannels, channelToSpaceMap]);
|
||||
|
||||
// Check if any DM channels are unread
|
||||
const hasDmUnread = useMemo(() => {
|
||||
@@ -171,28 +171,28 @@ export function ServerSidebar() {
|
||||
return false;
|
||||
}, [unreadChannels, dmChannels]);
|
||||
|
||||
const handleServerClick = (serverId: string) => {
|
||||
const server = servers.find(s => s.id === serverId);
|
||||
const origin = (server as any)?._instanceOrigin;
|
||||
const handleSpaceClick = (spaceId: string) => {
|
||||
const space = spaces.find(s => s.id === spaceId);
|
||||
const origin = (space as any)?._instanceOrigin;
|
||||
if (origin && disconnectedOrigins.has(origin)) {
|
||||
const inst = instances.find(i => i.origin === origin);
|
||||
addToast(`Reconnecting to ${inst?.label || 'remote instance'}...`, 'warning', 4000);
|
||||
return;
|
||||
}
|
||||
setCurrentServer(serverId);
|
||||
setCurrentSpace(spaceId);
|
||||
setShowDms(false);
|
||||
navigate(`/channels/${serverId}`);
|
||||
navigate(`/channels/${spaceId}`);
|
||||
};
|
||||
|
||||
const handleDmClick = () => {
|
||||
setShowDms(true);
|
||||
setCurrentServer(null);
|
||||
setCurrentSpace(null);
|
||||
setCurrentChannel(null);
|
||||
navigate('/channels/@me');
|
||||
};
|
||||
|
||||
const handleExploreClick = () => {
|
||||
setCurrentServer(null);
|
||||
setCurrentSpace(null);
|
||||
setCurrentChannel(null);
|
||||
navigate('/explore');
|
||||
};
|
||||
@@ -210,21 +210,21 @@ export function ServerSidebar() {
|
||||
|
||||
<div className="w-8 h-[2px] bg-interactive-muted rounded-full mb-1.5" />
|
||||
|
||||
{/* Home servers */}
|
||||
{groupedServers.home.map((server) => (
|
||||
{/* Home spaces */}
|
||||
{groupedSpaces.home.map((space) => (
|
||||
<SidebarItem
|
||||
key={server.id}
|
||||
id={server.id}
|
||||
name={server.name}
|
||||
icon={server.icon}
|
||||
active={currentServerId === server.id}
|
||||
onClick={() => handleServerClick(server.id)}
|
||||
hasUnread={unreadServerIds.has(server.id)}
|
||||
key={space.id}
|
||||
id={space.id}
|
||||
name={space.name}
|
||||
icon={space.icon}
|
||||
active={currentSpaceId === space.id}
|
||||
onClick={() => handleSpaceClick(space.id)}
|
||||
hasUnread={unreadSpaceIds.has(space.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Remote instance groups */}
|
||||
{groupedServers.remoteGroups.map(([origin, groupServers]) => {
|
||||
{groupedSpaces.remoteGroups.map(([origin, groupSpaces]) => {
|
||||
const inst = instances.find(i => i.origin === origin);
|
||||
const label = inst?.label || (() => { try { return new URL(origin).host; } catch { return '?'; } })();
|
||||
const isDimmed = disconnectedOrigins.has(origin);
|
||||
@@ -234,15 +234,15 @@ export function ServerSidebar() {
|
||||
<div className="text-[9px] text-txt-tertiary font-medium uppercase tracking-wider mb-1 truncate max-w-[52px] text-center" title={label}>
|
||||
{label}
|
||||
</div>
|
||||
{groupServers.map((server) => (
|
||||
{groupSpaces.map((space) => (
|
||||
<SidebarItem
|
||||
key={server.id}
|
||||
id={server.id}
|
||||
name={server.name}
|
||||
icon={server.icon}
|
||||
active={currentServerId === server.id}
|
||||
onClick={() => handleServerClick(server.id)}
|
||||
hasUnread={unreadServerIds.has(server.id)}
|
||||
key={space.id}
|
||||
id={space.id}
|
||||
name={space.name}
|
||||
icon={space.icon}
|
||||
active={currentSpaceId === space.id}
|
||||
onClick={() => handleSpaceClick(space.id)}
|
||||
hasUnread={unreadSpaceIds.has(space.id)}
|
||||
dimmed={isDimmed}
|
||||
/>
|
||||
))}
|
||||
@@ -253,26 +253,26 @@ export function ServerSidebar() {
|
||||
<div className="w-8 h-[2px] bg-interactive-muted rounded-full mb-1.5" />
|
||||
|
||||
<SidebarItem
|
||||
id="add-server"
|
||||
name="Add a Server"
|
||||
id="add-space"
|
||||
name="Add a Space"
|
||||
active={false}
|
||||
onClick={() => openModal('createServer')}
|
||||
onClick={() => openModal('createSpace')}
|
||||
type="action"
|
||||
actionType="add"
|
||||
/>
|
||||
|
||||
<SidebarItem
|
||||
id="join-server"
|
||||
name="Join a Server"
|
||||
id="join-space"
|
||||
name="Join a Space"
|
||||
active={false}
|
||||
onClick={() => openModal('joinServer')}
|
||||
onClick={() => openModal('joinSpace')}
|
||||
type="action"
|
||||
actionType="join"
|
||||
/>
|
||||
|
||||
<SidebarItem
|
||||
id="explore"
|
||||
name="Explore Servers"
|
||||
name="Explore Spaces"
|
||||
active={location.pathname === '/explore'}
|
||||
onClick={handleExploreClick}
|
||||
type="action"
|
||||
Reference in New Issue
Block a user