import React, { useState, useEffect, useCallback } from 'react'; import { Modal } from '../ui/Modal'; import { ConfirmDialog } from '../ui/ConfirmDialog'; import { useUIStore } from '../../stores/uiStore'; import { useSpaceStore, getApiForOrigin } from '../../stores/spaceStore'; import { PermissionBits, permissionsToString, stringToPermissions, hasPermissionBit } from '../../utils/permissions'; import { Toggle } from '../ui/Toggle'; import { PermissionsEditor } from '../ui/PermissionsEditor'; import type { PermissionDef } from '../ui/OverrideEntry'; // ─── Permission Definitions for Channel Overrides ────────────────────────────── const TEXT_CHANNEL_PERMISSIONS: PermissionDef[] = [ { key: 'VIEW_CHANNEL', label: 'View Channel', bit: PermissionBits.VIEW_CHANNEL }, { key: 'SEND_MESSAGES', label: 'Send Messages', bit: PermissionBits.SEND_MESSAGES }, { key: 'MANAGE_MESSAGES', label: 'Manage Messages', bit: PermissionBits.MANAGE_MESSAGES }, { key: 'ATTACH_FILES', label: 'Attach Files', bit: PermissionBits.ATTACH_FILES }, { key: 'READ_MESSAGE_HISTORY', label: 'Read Message History', bit: PermissionBits.READ_MESSAGE_HISTORY }, { key: 'ADD_REACTIONS', label: 'Add Reactions', bit: PermissionBits.ADD_REACTIONS }, ]; const VOICE_CHANNEL_PERMISSIONS: PermissionDef[] = [ { key: 'VIEW_CHANNEL', label: 'View Channel', bit: PermissionBits.VIEW_CHANNEL }, { key: 'CONNECT', label: 'Connect', bit: PermissionBits.CONNECT }, { key: 'SPEAK', label: 'Speak', bit: PermissionBits.SPEAK }, { key: 'STREAM', label: 'Stream', bit: PermissionBits.STREAM }, { key: 'MUTE_MEMBERS', label: 'Mute Members', bit: PermissionBits.MUTE_MEMBERS }, { key: 'DEAFEN_MEMBERS', label: 'Deafen Members', bit: PermissionBits.DEAFEN_MEMBERS }, { key: 'MOVE_MEMBERS', label: 'Move Members', bit: PermissionBits.MOVE_MEMBERS }, { key: 'DISCONNECT_MEMBERS', label: 'Disconnect Members', bit: PermissionBits.DISCONNECT_MEMBERS }, ]; // ─── Overview Tab ─────────────────────────────────────────────────────────────── function OverviewTab({ channelId, channelName, channelType, isPrivate, isFetching, isLoading, error, canManageChannels, onTogglePrivate, onDeleteChannel, }: { channelId: string; channelName: string; channelType: string; isPrivate: boolean; isFetching: boolean; isLoading: boolean; error: string; canManageChannels: boolean; onTogglePrivate: () => void; onDeleteChannel: () => void; }) { return (
{isPrivate ? ( ) : ( )} {channelName}
{error && (
{error}
)}
Private Channel
Only selected members and roles will be able to view this channel.
{isPrivate && !isFetching && (
This channel is hidden from members without explicit access. Users with the Administrator permission or space owners can always see all channels.
)} {canManageChannels && (
)}
); } // ─── Main Modal ───────────────────────────────────────────────────────────────── export function ChannelSettingsModal() { const activeModal = useUIStore((s) => s.activeModal); const modalData = useUIStore((s) => s.modalData); const closeModal = useUIStore((s) => s.closeModal); const currentSpaceId = useSpaceStore((s) => s.currentSpaceId); const channels = useSpaceStore((s) => s.channels); const spaces = useSpaceStore((s) => s.spaces); const spacePermissions = useSpaceStore((s) => s.spacePermissions); const [tab, setTab] = useState<'overview' | 'permissions'>('overview'); const [isPrivate, setIsPrivate] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isFetching, setIsFetching] = useState(true); const [error, setError] = useState(''); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const isOpen = activeModal === 'channelSettings'; const channelId = modalData?.channelId as string | undefined; const channel = channels.find(c => c.id === channelId); const myPerms = currentSpaceId ? spacePermissions.get(currentSpaceId) : undefined; const canManageChannels = myPerms !== undefined && hasPermissionBit(myPerms, PermissionBits.MANAGE_CHANNELS); const canManageRoles = myPerms !== undefined && hasPermissionBit(myPerms, PermissionBits.MANAGE_ROLES); // Reset state when modal closes useEffect(() => { if (!isOpen) { setShowDeleteConfirm(false); setIsDeleting(false); setTab('overview'); } }, [isOpen]); // Fetch overrides for the private toggle (overview tab) const fetchPrivateState = useCallback(() => { if (!channelId || !currentSpaceId) return; setIsFetching(true); setError(''); const space = spaces.find(s => s.id === currentSpaceId); const channelApi = getApiForOrigin(space?._instanceOrigin ?? ''); channelApi.channels.getOverrides(channelId) .then((data: { targetType: string; targetId: string; allow: string; deny: string }[]) => { // Check if @everyone role (id === spaceId) has VIEW_CHANNEL denied const everyoneOverride = data.find( o => o.targetType === 'role' && o.targetId === currentSpaceId ); if (everyoneOverride) { const denyBits = stringToPermissions(everyoneOverride.deny); setIsPrivate((denyBits & PermissionBits.VIEW_CHANNEL) !== 0n); } else { setIsPrivate(false); } }) .catch((err: Error) => { setError(err.message || 'Failed to load channel overrides'); }) .finally(() => { setIsFetching(false); }); }, [channelId, currentSpaceId, spaces]); useEffect(() => { if (isOpen && channelId && currentSpaceId) { fetchPrivateState(); } else { setIsFetching(false); } }, [isOpen, channelId, currentSpaceId, fetchPrivateState]); if (!isOpen || !channel || !channelId || !currentSpaceId) return null; const space = spaces.find(s => s.id === currentSpaceId); const handleToggle = async () => { setError(''); setIsLoading(true); const channelApi = getApiForOrigin(space?._instanceOrigin ?? ''); try { if (!isPrivate) { // Make private: deny VIEW_CHANNEL for @everyone role await channelApi.channels.putOverride(channelId, { targetType: 'role', targetId: currentSpaceId, allow: '0', deny: permissionsToString(PermissionBits.VIEW_CHANNEL), }); setIsPrivate(true); } else { // Make public: remove the @everyone VIEW_CHANNEL deny override await channelApi.channels.deleteOverride(channelId, 'role', currentSpaceId); setIsPrivate(false); } // Re-fetch to keep in sync fetchPrivateState(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update channel privacy'); } finally { setIsLoading(false); } }; const handleDeleteChannel = async () => { if (!channelId || !currentSpaceId) return; setIsDeleting(true); try { const channelApi = getApiForOrigin(space?._instanceOrigin ?? ''); await channelApi.channels.delete(channelId); closeModal(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete channel'); setIsDeleting(false); } }; const showTabs = canManageRoles; const tabClass = (t: typeof tab) => `w-full text-left px-2.5 py-1.5 rounded text-sm transition-colors ${ tab === t ? 'bg-interactive-selected text-txt-primary' : 'text-txt-tertiary hover:text-txt-secondary hover:bg-interactive-hover' }`; return ( <> {showTabs ? (
{/* Tabs */}
{/* Content */}
{tab === 'overview' && ( setShowDeleteConfirm(true)} /> )} {tab === 'permissions' && ( { const channelApi = getApiForOrigin(space?._instanceOrigin ?? ''); return channelApi.channels.getOverrides(channelId); }} putOverride={(data) => { const channelApi = getApiForOrigin(space?._instanceOrigin ?? ''); return channelApi.channels.putOverride(channelId, data); }} deleteOverride={(targetType, targetId) => { const channelApi = getApiForOrigin(space?._instanceOrigin ?? ''); return channelApi.channels.deleteOverride(channelId, targetType, targetId); }} /> )}
) : ( setShowDeleteConfirm(true)} /> )}
setShowDeleteConfirm(false)} onConfirm={handleDeleteChannel} title={`Delete #${channel.name}?`} description={<> This will permanently delete #{channel.name} and all of its messages. {channel.type === 'voice' && ' Any users currently in this voice channel will be disconnected.'} {' '}This action cannot be undone. } confirmLabel="Delete Channel" variant="danger" loading={isDeleting} /> ); }