chore: remove 62 tsc emit artifacts from src/, add noEmit to tsconfig

tsc was emitting compiled .js files directly into packages/web/src/
alongside the .tsx source files because noEmit was not set. These
artifacts were never used — Vite compiles from .tsx source directly.

- Add noEmit: true to packages/web/tsconfig.json (tsc = type-check only)
- Delete all 62 orphaned .js files from src/ (-6,129 lines)
- Add packages/web/src/**/*.js to .gitignore as safeguard
This commit is contained in:
Jannis Braun
2026-02-23 00:54:51 +01:00
parent 5747267a6b
commit 176f4db27e
64 changed files with 3 additions and 6129 deletions
@@ -1,39 +0,0 @@
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useEffect, useMemo } from 'react';
import { useSocialStore } from '../../stores/socialStore';
import { useUIStore } from '../../stores/uiStore';
import { Avatar } from '../ui/Avatar';
export function ActivityPanel() {
const friends = useSocialStore((s) => s.friends);
const loadFriends = useSocialStore((s) => s.loadFriends);
const memberListOpen = useUIStore((s) => s.memberListOpen);
const openUserProfile = useUIStore((s) => s.openUserProfile);
useEffect(() => {
loadFriends();
}, [loadFriends]);
const { onlineFriends, offlineFriends } = useMemo(() => {
const online = friends.filter(f => f.status !== 'offline');
const offline = friends.filter(f => f.status === 'offline');
return { onlineFriends: online, offlineFriends: offline };
}, [friends]);
if (!memberListOpen)
return null;
const handleFriendClick = (e, friend) => {
e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect();
openUserProfile({
id: friend.id,
username: friend.username,
displayName: friend.displayName,
avatar: friend.avatar,
status: friend.status,
customStatus: friend.customStatus,
createdAt: friend.createdAt,
}, {
top: Math.min(rect.top, window.innerHeight - 450),
left: rect.left - 316,
});
};
const renderFriend = (friend, isOffline = false) => (_jsxs("div", { onClick: (e) => handleFriendClick(e, friend), className: "flex items-center gap-3 px-2 py-1.5 rounded-[4px] hover:bg-discord-modifier-hover cursor-pointer group transition-colors", children: [_jsx(Avatar, { src: friend.avatar, name: friend.displayName ?? friend.username, size: 32, status: isOffline ? 'offline' : friend.status, className: isOffline ? 'opacity-60' : undefined }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: `text-[15px] font-medium truncate ${isOffline ? 'text-discord-text-muted' : 'text-discord-text-primary'}`, children: friend.displayName ?? friend.username }), !isOffline && friend.customStatus && (_jsx("div", { className: "text-[12px] text-discord-text-muted truncate", children: friend.customStatus }))] })] }, friend.id));
return (_jsx("div", { className: "w-60 bg-discord-bg-secondary flex-shrink-0 overflow-y-auto select-none no-scrollbar", children: _jsxs("div", { className: "p-3", children: [_jsx("h3", { className: "text-[20px] font-bold text-discord-text-header mb-4 px-2", children: "Active Now" }), onlineFriends.length === 0 && offlineFriends.length === 0 ? (_jsxs("div", { className: "text-center py-8", children: [_jsx("div", { className: "text-[16px] font-bold text-discord-text-header mb-1", children: "It's quiet for now..." }), _jsx("div", { className: "text-[14px] text-discord-text-muted max-w-[200px] mx-auto", children: "When a friend starts an activity\u2014like playing a game or hanging out on voice\u2014we'll show it here!" })] })) : (_jsxs(_Fragment, { children: [onlineFriends.length > 0 && (_jsxs("div", { className: "mb-4", children: [_jsxs("h3", { className: "text-[12px] font-bold text-discord-text-muted uppercase tracking-wider px-2 mb-1", children: ["ONLINE \u2014 ", onlineFriends.length] }), onlineFriends.map(f => renderFriend(f))] })), offlineFriends.length > 0 && (_jsxs("div", { children: [_jsxs("h3", { className: "text-[12px] font-bold text-discord-text-muted uppercase tracking-wider px-2 mb-1", children: ["OFFLINE \u2014 ", offlineFriends.length] }), offlineFriends.map(f => renderFriend(f, true))] }))] }))] }) }));
}
@@ -1,188 +0,0 @@
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { ServerSidebar } from './ServerSidebar';
import { ChannelSidebar } from './ChannelSidebar';
import { MainContent } from './MainContent';
import { RightPanel } from './RightPanel';
import { ImagePreview } from '../chat/ImagePreview';
import { CreateServerModal } from '../modals/CreateServer';
import { JoinServerModal } from '../modals/JoinServer';
import { CreateChannelModal } from '../modals/CreateChannel';
import { InviteModal } from '../modals/InviteModal';
import { UserSettingsModal } from '../modals/UserSettings';
import { ServerSettingsModal } from '../modals/ServerSettings';
import { NewDmModal } from '../modals/NewDmModal';
import { IncomingCallModal } from '../voice/IncomingCallModal';
import { PictureInPicture } from '../voice/PictureInPicture';
import { SoundController } from '../voice/SoundController';
import { GlobalAudioRenderer } from '../voice/GlobalAudioRenderer';
import { UserProfilePopout } from '../ui/UserProfilePopout';
import { useAuth } from '../../hooks/useAuth';
import { useWebSocket } from '../../hooks/useWebSocket';
import { useLiveKit } from '../../hooks/useLiveKit';
import { useServerStore } from '../../stores/serverStore';
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();
// Global interaction handler to resume AudioContext
useEffect(() => {
const resume = () => {
AudioManager.getInstance().resumeContext().then(() => {
window.removeEventListener('click', resume);
window.removeEventListener('keydown', resume);
window.removeEventListener('touchstart', resume);
});
};
window.addEventListener('click', resume);
window.addEventListener('keydown', resume);
window.addEventListener('touchstart', resume);
return () => {
window.removeEventListener('click', resume);
window.removeEventListener('keydown', resume);
window.removeEventListener('touchstart', resume);
};
}, []);
// MutationObserver: neutralize rogue LiveKit <audio> elements that bypass our Web Audio pipeline.
// LiveKit can re-attach hidden <audio> elements after .detach(), causing full-volume playback
// that ignores our volume/mute controls. Any <audio> without data-opencord is immediately killed.
useEffect(() => {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
mutation.addedNodes.forEach((node) => {
if (node instanceof HTMLAudioElement && !node.dataset.opencord) {
node.muted = true;
node.volume = 0;
node.pause();
node.srcObject = null;
}
});
}
});
observer.observe(document.body, { childList: true, subtree: true });
return () => observer.disconnect();
}, []);
const outputDeviceId = useVoiceStore((s) => s.outputDeviceId);
useEffect(() => {
AudioManager.getInstance().setOutputDevice(outputDeviceId);
}, [outputDeviceId]);
const { user, isLoading } = useAuth();
const setCurrentServer = useServerStore((s) => s.setCurrentServer);
const loadServerDetail = useServerStore((s) => s.loadServerDetail);
const setCurrentChannel = useChatStore((s) => s.setCurrentChannel);
const loadMessages = useChatStore((s) => s.loadMessages);
const setIsMobile = useUIStore((s) => s.setIsMobile);
const setShowDms = useUIStore((s) => s.setShowDms);
const openModal = useUIStore((s) => s.openModal);
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
const isMobile = useUIStore((s) => s.isMobile);
const userProfilePopout = useUIStore((s) => s.userProfilePopout);
const closeUserProfile = useUIStore((s) => s.closeUserProfile);
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
const setParticipants = useVoiceStore((s) => s.setParticipants);
const { connect: connectVoice, connectDm: connectDmVoice, disconnect: disconnectVoice, participants: voiceParticipants, isConnected: isVoiceConnected, isConnecting: isVoiceConnecting, connectedChannelId, } = useLiveKit();
// Initialize WebSocket
const { isConnected: isWsConnected } = useWebSocket();
// Sync participants to store
useEffect(() => {
setParticipants(voiceParticipants);
}, [voiceParticipants, setParticipants]);
// Track the last channel we attempted to connect to, to prevent effect loops
const lastAttemptedRef = React.useRef(null);
// Manage voice connection
useEffect(() => {
if (isLoading || !user || !isWsConnected)
return;
const manageConnection = async () => {
// Determine what we SHOULD be connected to
const targetChannelId = activeDmCall
? `dm-${activeDmCall.dmChannelId}`
: currentVoiceChannelId;
// 1. If we have a target
if (targetChannelId) {
// If we're not connected to the RIGHT place, trigger connect.
// We IGNORE isVoiceConnecting here to allow "interrupting" a connection
// or switching rooms immediately.
if (connectedChannelId !== targetChannelId) {
// Prevent spamming the same connection attempt if React re-renders
if (lastAttemptedRef.current === targetChannelId && isVoiceConnecting) {
return;
}
console.log(`[AppLayout] Switching/Connecting to: ${targetChannelId}`);
lastAttemptedRef.current = targetChannelId;
if (activeDmCall) {
await connectDmVoice(activeDmCall.dmChannelId);
}
else {
await connectVoice(targetChannelId);
}
}
else {
// We are connected to the right place. Reset ref.
lastAttemptedRef.current = null;
}
return;
}
// 2. No target — ensure disconnected
if (connectedChannelId !== null || isVoiceConnected || isVoiceConnecting) {
console.log('[AppLayout] Leaving voice (no target)');
lastAttemptedRef.current = null;
await disconnectVoice();
}
};
manageConnection();
}, [
currentVoiceChannelId,
activeDmCall,
connectedChannelId,
isVoiceConnected,
isVoiceConnecting,
isWsConnected,
isLoading,
user,
connectVoice,
connectDmVoice,
disconnectVoice
]);
// Responsive detection
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, [setIsMobile]);
// Handle route params
useEffect(() => {
if (serverId === '@me') {
setShowDms(true);
setCurrentServer(null);
}
else if (serverId) {
setShowDms(false);
setCurrentServer(serverId);
loadServerDetail(serverId);
}
}, [serverId, setCurrentServer, loadServerDetail, setShowDms]);
useEffect(() => {
if (inviteCode) {
openModal('joinServer');
}
}, [inviteCode, openModal]);
useEffect(() => {
if (channelId) {
setCurrentChannel(channelId);
loadMessages(channelId);
}
else {
setCurrentChannel(null);
}
}, [channelId, setCurrentChannel, loadMessages]);
if (isLoading || !user) {
return (_jsx("div", { className: "h-screen flex items-center justify-center bg-discord-bg-primary", children: _jsxs("div", { className: "text-center", children: [_jsxs("svg", { className: "animate-spin w-10 h-10 text-discord-blurple mx-auto mb-4", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), _jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" })] }), _jsx("p", { className: "text-discord-text-muted", children: "Loading Opencord..." })] }) }));
}
return (_jsxs("div", { className: "h-screen flex bg-discord-bg-tertiary overflow-hidden", children: [_jsxs("div", { className: `${isMobile ? `fixed z-40 h-full transition-transform ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}` : 'flex h-full'}`, children: [_jsx(ServerSidebar, {}), _jsx(ChannelSidebar, {})] }), _jsxs("div", { className: "flex-1 flex min-w-0 bg-discord-bg-primary relative", children: [_jsx(MainContent, {}), _jsx(RightPanel, {})] }), _jsx(CreateServerModal, {}), _jsx(JoinServerModal, {}), _jsx(CreateChannelModal, {}), _jsx(InviteModal, {}), _jsx(UserSettingsModal, {}), _jsx(ServerSettingsModal, {}), _jsx(NewDmModal, {}), _jsx(IncomingCallModal, {}), _jsx(ImagePreview, {}), _jsx(PictureInPicture, {}), _jsx(SoundController, {}), _jsx(GlobalAudioRenderer, {}), userProfilePopout.user && userProfilePopout.position && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-[45]", onClick: closeUserProfile }), _jsx(UserProfilePopout, { user: userProfilePopout.user, onClose: closeUserProfile, position: userProfilePopout.position })] }))] }));
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,7 +0,0 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { useUIStore } from '../../stores/uiStore';
export function MemberListToggleButton() {
const toggleMemberList = useUIStore((s) => s.toggleMemberList);
const memberListOpen = useUIStore((s) => s.memberListOpen);
return (_jsx("button", { onClick: toggleMemberList, className: `w-8 h-8 flex items-center justify-center transition-colors rounded-[4px] hover:bg-discord-modifier-hover ${memberListOpen ? 'text-discord-text-primary' : 'text-discord-text-muted hover:text-discord-text-secondary'}`, title: "Toggle Member List", children: _jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M14 8.00598C14 10.211 12.206 12.006 10 12.006C7.795 12.006 6 10.211 6 8.00598C6 5.80098 7.794 4.00598 10 4.00598C12.206 4.00598 14 5.80098 14 8.00598ZM2 19.006C2 15.473 5.29 13.006 10 13.006C14.711 13.006 18 15.473 18 19.006V20.006H2V19.006ZM20 20.006H22V19.006C22 16.451 20.178 14.471 17.532 13.471C19.461 14.601 20 16.561 20 19.006V20.006Z" }) }) }));
}
@@ -1,53 +0,0 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useMemo } from 'react';
import { useServerStore } from '../../stores/serverStore';
import { useUIStore } from '../../stores/uiStore';
import { Avatar } from '../ui/Avatar';
const ROLE_ORDER = { owner: 0, admin: 1, member: 2 };
const ROLE_LABELS = { owner: 'OWNER', admin: 'ADMIN', member: 'MEMBER' };
export function MemberSidebar() {
const members = useServerStore((s) => s.members);
const memberListOpen = useUIStore((s) => s.memberListOpen);
const openUserProfile = useUIStore((s) => s.openUserProfile);
const { roleGroups, offlineMembers } = useMemo(() => {
const online = members.filter(m => m.user.status !== 'offline');
const offline = members.filter(m => m.user.status === 'offline');
// Group online members by role
const groups = new Map();
for (const m of online) {
const role = m.role || 'member';
if (!groups.has(role))
groups.set(role, []);
groups.get(role).push(m);
}
// Sort groups by role hierarchy
const sorted = [...groups.entries()].sort((a, b) => (ROLE_ORDER[a[0]] ?? 99) - (ROLE_ORDER[b[0]] ?? 99));
return { roleGroups: sorted, offlineMembers: offline };
}, [members]);
if (!memberListOpen)
return null;
const roleColors = {
owner: 'text-discord-red',
admin: 'text-discord-blurple',
member: 'text-discord-text-primary',
};
const getMemberColor = (member) => {
if (member.roles && member.roles.length > 0) {
return { color: member.roles[0].color };
}
return undefined;
};
const handleMemberClick = (e, user) => {
e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect();
openUserProfile(user, {
top: Math.min(rect.top, window.innerHeight - 450),
left: rect.left - 316,
});
};
const renderMember = (member, isOffline = false) => {
const displayName = member.user.displayName ?? member.user.username;
return (_jsxs("div", { onClick: (e) => handleMemberClick(e, member.user), className: "flex items-center gap-3 px-2 py-1.5 rounded-[4px] hover:bg-discord-modifier-hover cursor-pointer group transition-colors", children: [_jsx(Avatar, { src: member.user.avatar, name: displayName, size: 32, status: isOffline ? 'offline' : member.user.status, className: isOffline ? 'opacity-60' : undefined }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: `text-[15px] font-medium truncate ${isOffline ? 'text-discord-text-muted' : (!getMemberColor(member) ? (roleColors[member.role] ?? 'text-discord-text-primary') : '')}`, style: isOffline ? undefined : getMemberColor(member), children: displayName }), !isOffline && member.user.customStatus && (_jsx("div", { className: "text-[12px] text-discord-text-muted truncate", children: member.user.customStatus }))] })] }, member.userId));
};
return (_jsx("div", { className: "w-60 bg-discord-bg-members flex-shrink-0 overflow-y-auto select-none no-scrollbar", children: _jsxs("div", { className: "p-3", children: [roleGroups.map(([role, groupMembers]) => (_jsxs("div", { className: "mb-4", children: [_jsxs("h3", { className: "text-[12px] font-bold text-discord-text-muted uppercase tracking-wider px-2 mb-1", children: [ROLE_LABELS[role] ?? role.toUpperCase(), " \u2014 ", groupMembers.length] }), groupMembers.map((m) => renderMember(m))] }, role))), offlineMembers.length > 0 && (_jsxs("div", { children: [_jsxs("h3", { className: "text-[12px] font-bold text-discord-text-muted uppercase tracking-wider px-2 mb-1", children: ["OFFLINE \u2014 ", offlineMembers.length] }), offlineMembers.map((m) => renderMember(m, true))] }))] }) }));
}
@@ -1,10 +0,0 @@
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { useUIStore } from '../../stores/uiStore';
export function MobileNav() {
const isMobile = useUIStore((s) => s.isMobile);
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
if (!isMobile)
return null;
return (_jsxs(_Fragment, { children: [_jsx("button", { onClick: toggleSidebar, className: "fixed top-3 left-3 z-40 p-1.5 rounded bg-discord-bg-secondary text-discord-text-primary md:hidden", children: _jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: sidebarOpen ? (_jsx("path", { d: "M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" })) : (_jsx("path", { d: "M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" })) }) }), sidebarOpen && (_jsx("div", { className: "fixed inset-0 bg-black/50 z-30 md:hidden", onClick: toggleSidebar }))] }));
}
@@ -1,13 +0,0 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { MemberSidebar } from './MemberSidebar';
import { ActivityPanel } from './ActivityPanel';
import { useUIStore } from '../../stores/uiStore';
import { useServerStore } from '../../stores/serverStore';
export function RightPanel() {
const showDms = useUIStore((s) => s.showDms);
const currentServerId = useServerStore((s) => s.currentServerId);
if (showDms || !currentServerId) {
return _jsx(ActivityPanel, {});
}
return _jsx(MemberSidebar, {});
}
@@ -1,72 +0,0 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useServerStore } from '../../stores/serverStore';
import { useChatStore } from '../../stores/chatStore';
import { useUIStore } from '../../stores/uiStore';
import { Tooltip } from '../ui/Tooltip';
function SidebarItem({ name, icon, active, onClick, type = 'server', actionType, hasUnread }) {
const [isHovered, setIsHovered] = useState(false);
const firstLetter = name.charAt(0).toUpperCase();
const getPillHeight = () => {
if (active)
return 'h-10';
if (isHovered)
return 'h-5';
if (hasUnread && !active)
return 'h-2';
return 'h-2 scale-0';
};
const getButtonClasses = () => {
const base = 'w-12 h-12 flex items-center justify-center transition-all duration-200 overflow-hidden relative group';
if (type === 'dm') {
return `${base} ${active ? 'bg-discord-blurple rounded-[16px] text-white' : 'bg-discord-bg-primary rounded-[24px] hover:rounded-[16px] text-discord-text-primary hover:bg-discord-blurple hover:text-white'}`;
}
if (type === 'action') {
return `${base} bg-discord-bg-primary rounded-[24px] hover:rounded-[16px] text-discord-green hover:bg-discord-green hover:text-white`;
}
return `${base} ${active ? 'bg-discord-blurple rounded-[16px] text-white' : 'bg-discord-bg-primary rounded-[24px] hover:rounded-[16px] text-discord-text-primary hover:bg-discord-blurple hover:text-white'}`;
};
return (_jsxs("div", { className: "relative flex items-center mb-2 w-full justify-center", onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [(type === 'server' || type === 'dm') && (_jsx("div", { className: "absolute -left-0 w-2 h-12 flex items-center", children: _jsx("div", { className: `bg-white rounded-r-full transition-all duration-200 origin-left ${getPillHeight()} w-1` }) })), _jsx(Tooltip, { content: name, position: "right", children: _jsx("button", { onClick: onClick, className: getButtonClasses(), children: type === 'dm' ? (_jsx("svg", { width: "28", height: "20", viewBox: "0 0 28 20", fill: "currentColor", children: _jsx("path", { d: "M23.0212 1.67671C21.3107 0.879656 19.5079 0.318797 17.6584 0C17.4062 0.461742 17.1749 0.934541 16.9708 1.4184C15.003 1.12145 12.9974 1.12145 11.0292 1.4184C10.8251 0.934541 10.5938 0.461742 10.3416 0C8.49215 0.318797 6.68934 0.879656 4.97882 1.67671C0.665804 8.44726 -0.364554 15.0614 0.225316 21.5765C2.41849 23.2105 4.70543 24.3115 7.04773 25.043C7.60419 24.2941 8.09868 23.4944 8.52321 22.6521C7.71966 22.3602 6.9466 21.9905 6.21274 21.5543C6.39845 21.4212 6.58011 21.2838 6.75775 21.1429C12.7568 23.8968 19.2811 23.8968 25.2422 21.1429C25.4199 21.2838 25.6015 21.4212 25.7873 21.5543C25.0534 21.9905 24.2804 22.3602 23.4768 22.6521C23.9013 23.4944 24.3958 24.2941 24.9523 25.043C27.2946 24.3115 29.5815 23.2105 31.7747 21.5765C32.4517 14.0051 30.5663 7.45459 26.0212 1.67671H23.0212Z", transform: "scale(0.85) translate(0, 0)" }) })) : type === 'action' ? (actionType === 'add' ? (_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z" }) })) : actionType === 'explore' ? (_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zm-3.146-5.351l2.78-1.042 1.042-2.78-2.78 1.042-1.042 2.78zM14.5 7.5l-2.5 5-5 2.5 2.5-5 5-2.5z" }) })) : (_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M12 2L4.5 20.29l.71.71L12 18l6.79 3 .71-.71z" }) }))) : icon ? (_jsx("img", { src: icon.startsWith('http') ? icon : `/api/uploads/${icon}`, alt: name, className: "w-full h-full object-cover" })) : (_jsx("span", { className: "text-[16px] font-medium", children: firstLetter })) }) })] }));
}
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);
const showDms = useUIStore((s) => s.showDms);
const setShowDms = useUIStore((s) => s.setShowDms);
const openModal = useUIStore((s) => s.openModal);
const unreadChannels = useChatStore((s) => s.unreadChannels);
const navigate = useNavigate();
// Compute which servers have unread channels
const unreadServerIds = useMemo(() => {
const ids = new Set();
for (const channelId of unreadChannels) {
const serverId = channelToServerMap.get(channelId);
if (serverId)
ids.add(serverId);
}
return ids;
}, [unreadChannels, channelToServerMap]);
// 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 handleServerClick = (serverId) => {
setCurrentServer(serverId);
setShowDms(false);
navigate(`/channels/${serverId}`);
};
const handleDmClick = () => {
setShowDms(true);
setCurrentServer(null);
navigate('/channels/@me');
};
return (_jsxs("nav", { className: "w-[72px] bg-discord-bg-server flex flex-col items-center py-3 overflow-y-auto flex-shrink-0 no-scrollbar select-none", children: [_jsx(SidebarItem, { id: "@me", name: "Direct Messages", active: showDms, onClick: handleDmClick, type: "dm", hasUnread: hasDmUnread }), _jsx("div", { className: "w-8 h-[2px] bg-discord-modifier-accent rounded-full mb-2" }), servers.map((server) => (_jsx(SidebarItem, { id: server.id, name: server.name, icon: server.icon, active: currentServerId === server.id, onClick: () => handleServerClick(server.id), hasUnread: unreadServerIds.has(server.id) }, server.id))), _jsx(SidebarItem, { id: "add-server", name: "Add a Server", active: false, onClick: () => openModal('createServer'), type: "action", actionType: "add" }), _jsx(SidebarItem, { id: "join-server", name: "Join a Server", active: false, onClick: () => openModal('joinServer'), type: "action", actionType: "join" }), _jsx(SidebarItem, { id: "explore", name: "Explore Discoverable Servers", active: false, onClick: () => { }, type: "action", actionType: "explore" })] }));
}