import React, { useState, useMemo } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useSpaceStore } from '../../stores/spaceStore'; import { useChatStore } from '../../stores/chatStore'; import { useUIStore } from '../../stores/uiStore'; import { useInstanceStore } from '../../stores/instanceStore'; import { Tooltip } from '../ui/Tooltip'; import { getSpaceGradient, HOME_GRADIENT } from '../../utils/gradients'; interface SidebarItemProps { id: string; name: string; icon?: string | null; active: boolean; onClick: () => void; type?: 'space' | 'dm' | 'action'; actionType?: 'add' | 'join' | 'explore'; hasUnread?: boolean; dimmed?: boolean; federationBadge?: boolean; federationDisconnected?: boolean; tooltipText?: string; } function SidebarItem({ id, name, icon, active, onClick, type = 'space', actionType, hasUnread, dimmed, federationBadge, federationDisconnected, tooltipText }: SidebarItemProps) { const [isHovered, setIsHovered] = useState(false); const firstLetter = name.charAt(0).toUpperCase(); const getPillHeight = () => { if (active) return 'h-8'; if (isHovered) return 'h-4'; if (hasUnread && !active) return 'h-2'; return 'h-2 scale-0'; }; const backgroundStyle = useMemo((): React.CSSProperties | undefined => { if (type === 'action') { return { background: isHovered ? 'rgba(134, 239, 172, 0.12)' : 'rgba(255, 255, 255, 0.04)', }; } if (type === 'dm') { return { background: HOME_GRADIENT.gradient }; } // Space type — if it has a custom icon image, no gradient needed if (icon) return undefined; const spaceGrad = getSpaceGradient(id, name); return { background: spaceGrad.gradient }; }, [type, id, name, icon, isHovered]); const getButtonClasses = () => { const base = 'w-10 h-10 flex items-center justify-center duration-200 overflow-hidden [transition:border-radius_0.2s,background_0.2s,color_0.2s]'; if (type === 'dm') { return `${base} text-white ${active ? 'rounded-[13px]' : 'rounded-[20px] hover:rounded-[13px]'}`; } if (type === 'action') { return `${base} rounded-[20px] hover:rounded-[13px] text-accent-mint`; } if (icon) { return `${base} ${active ? 'rounded-[13px]' : 'rounded-[20px] hover:rounded-[13px]'}`; } return `${base} text-white ${active ? 'rounded-[13px]' : 'rounded-[20px] hover:rounded-[13px]'}`; }; const buttonContent = ( ); const innerContent = (
{buttonContent} {federationBadge && (
{federationDisconnected ? (
) : ( )}
)}
); return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {/* Pill Indicator */} {(type === 'space' || type === 'dm') && (
)} {tooltipText ? ( {innerContent} ) : ( innerContent )}
); } function InstanceDivider({ label, disconnected }: { label: string; disconnected: boolean }) { return (
); } 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); const addToast = useUIStore((s) => s.addToast); const setCurrentChannel = useChatStore((s) => s.setCurrentChannel); const unreadChannels = useChatStore((s) => s.unreadChannels); const instances = useInstanceStore((s) => s.instances); const navigate = useNavigate(); const location = useLocation(); // Group spaces by origin const groupedSpaces = useMemo(() => { const home = spaces.filter(s => !(s as any)._instanceOrigin); const remoteMap = new Map(); for (const s of spaces) { const origin = (s as any)._instanceOrigin; if (!origin) continue; const list = remoteMap.get(origin) || []; list.push(s); remoteMap.set(origin, list); } return { home, remoteGroups: Array.from(remoteMap.entries()) }; }, [spaces]); // Set of disconnected origins const disconnectedOrigins = useMemo(() => { const set = new Set(); for (const inst of instances) { if (inst.status === 'disconnected' || inst.status === 'error') { set.add(inst.origin); } } return set; }, [instances]); // Compute which spaces have unread channels const unreadSpaceIds = useMemo(() => { const ids = new Set(); for (const channelId of unreadChannels) { const spaceId = channelToSpaceMap.get(channelId); if (spaceId) ids.add(spaceId); } return ids; }, [unreadChannels, channelToSpaceMap]); // Check if any DM channels are unread const hasDmUnread = useMemo(() => { for (const dm of dmChannels) { if (unreadChannels.has(dm.id)) return true; } return false; }, [unreadChannels, dmChannels]); 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; } setCurrentSpace(spaceId); setShowDms(false); navigate(`/channels/${spaceId}`); }; const handleDmClick = () => { setShowDms(true); setCurrentSpace(null); setCurrentChannel(null); navigate('/channels/@me'); }; const handleExploreClick = () => { setCurrentSpace(null); setCurrentChannel(null); navigate('/explore'); }; return ( ); }