import React, { useState, useEffect } from 'react'; import { Modal } from '../ui/Modal'; import { ConfirmDialog } from '../ui/ConfirmDialog'; import { useUIStore } from '../../stores/uiStore'; import { useSpaceStore, getApiForOrigin } from '../../stores/spaceStore'; import { api } from '../../api/client'; import { PermissionBits, permissionsToString, stringToPermissions, hasPermissionBit } from '../../utils/permissions'; interface ChannelOverride { channelId: string; targetType: string; targetId: string; allow: string; deny: string; } 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 [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); // Reset delete state when modal closes useEffect(() => { if (!isOpen) { setShowDeleteConfirm(false); setIsDeleting(false); } }, [isOpen]); // Fetch overrides when modal opens useEffect(() => { if (!isOpen || !channelId || !currentSpaceId) { setIsFetching(false); return; } setIsFetching(true); setError(''); api.channels.getOverrides(channelId) .then((overrides: ChannelOverride[]) => { // Check if @everyone role (id === spaceId) has VIEW_CHANNEL denied const everyoneOverride = overrides.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); }); }, [isOpen, channelId, currentSpaceId]); if (!isOpen || !channel || !channelId || !currentSpaceId) return null; const handleToggle = async () => { setError(''); setIsLoading(true); try { if (!isPrivate) { // Make private: deny VIEW_CHANNEL for @everyone role await api.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 api.channels.deleteOverride(channelId, 'role', currentSpaceId); setIsPrivate(false); } } 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 space = spaces.find(s => s.id === currentSpaceId); 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); } }; return ( <>
{channel.name}
{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 && (
)}
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} /> ); }