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 (
Channel
{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 && (
Danger Zone
Delete Channel
)}
);
}
// ─── 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 */}
setTab('overview')} className={tabClass('overview')}>
Overview
setTab('permissions')} className={tabClass('permissions')}>
Permissions
{/* 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}
/>
>
);
}