# et roles

This commit is contained in:
Flatlogic Bot 2026-02-15 21:22:03 +00:00
parent e3984686cb
commit 5494f1e4ee
6 changed files with 518 additions and 168 deletions

View File

@ -9,6 +9,24 @@ $data = json_decode(file_get_contents('php://input'), true) ?? $_POST;
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$channel_id = $_GET['channel_id'] ?? 0;
// Get server_id for this channel
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$channel = $stmt->fetch();
$server_id = $channel['server_id'] ?? 0;
// Ensure @everyone role exists for this server
$stmt = db()->prepare("SELECT id FROM roles WHERE server_id = ? AND (name = '@everyone' OR name = 'Everyone') LIMIT 1");
$stmt->execute([$server_id]);
$everyone = $stmt->fetch();
if (!$everyone && $server_id) {
$stmt = db()->prepare("INSERT INTO roles (server_id, name, color, permissions, position) VALUES (?, '@everyone', '#99aab5', 0, 0)");
$stmt->execute([$server_id]);
$everyone_role_id = db()->lastInsertId();
} else {
$everyone_role_id = $everyone['id'] ?? 0;
}
// Fetch permissions for this channel
$stmt = db()->prepare("
SELECT cp.*, r.name as role_name, r.color as role_color
@ -17,7 +35,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
WHERE cp.channel_id = ?
");
$stmt->execute([$channel_id]);
echo json_encode(['success' => true, 'permissions' => $stmt->fetchAll()]);
$permissions = $stmt->fetchAll();
// Check if @everyone is in permissions, if not add it manually to show up by default
$has_everyone = false;
foreach($permissions as $p) {
if ($p['role_id'] == $everyone_role_id) {
$has_everyone = true;
break;
}
}
if (!$has_everyone && $everyone_role_id) {
$stmt = db()->prepare("SELECT name, color FROM roles WHERE id = ?");
$stmt->execute([$everyone_role_id]);
$r = $stmt->fetch();
$permissions[] = [
'channel_id' => (int)$channel_id,
'role_id' => (int)$everyone_role_id,
'allow_permissions' => 0,
'deny_permissions' => 0,
'role_name' => $r['name'],
'role_color' => $r['color']
];
}
echo json_encode(['success' => true, 'permissions' => $permissions]);
exit;
}

View File

@ -68,6 +68,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Create default channel
$stmt = $db->prepare("INSERT INTO channels (server_id, name, type) VALUES (?, 'general', 'text')");
$stmt->execute([$server_id]);
// Create default @everyone role
$stmt = $db->prepare("INSERT INTO roles (server_id, name, color, permissions, position) VALUES (?, '@everyone', '#99aab5', 0, 0)");
$stmt->execute([$server_id]);
$db->commit();
header('Location: index.php?server_id=' . $server_id);

View File

@ -929,3 +929,79 @@ body {
background-color: var(--active);
color: var(--text-primary);
}
/* Permission Tri-state Toggles */
.perm-tri-state {
background-color: #1e1f22;
padding: 2px;
border-radius: 4px;
display: inline-flex;
}
.perm-tri-state .btn {
padding: 4px 10px;
font-size: 0.85rem;
border: none !important;
border-radius: 4px !important;
color: #b5bac1;
transition: all 0.2s;
}
.perm-tri-state .btn:hover {
background-color: #35373c;
color: white;
}
.perm-tri-state .btn-outline-danger:checked + label {
background-color: #f23f42 !important;
color: white !important;
}
.perm-tri-state .btn-outline-secondary:checked + label {
background-color: #4e5058 !important;
color: white !important;
}
.perm-tri-state .btn-outline-success:checked + label {
background-color: #23a559 !important;
color: white !important;
}
#channel-permissions-roles-list .list-group-item.active {
background-color: rgba(78, 80, 88, 0.6) !important;
color: white !important;
}
#channel-permissions-roles-list .list-group-item {
border-radius: 4px !important;
margin: 0 8px;
width: auto;
color: #dbdee1 !important;
}
/* Modal text visibility fixes */
.modal-content .text-muted {
color: #dbdee1 !important; /* Lighter than before */
}
.modal-content .small {
color: #dbdee1;
}
.modal-content label {
color: #ffffff; /* Even brighter */
}
.modal-content .fw-bold {
color: #ffffff;
}
.permission-item {
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.permission-item:last-child {
border-bottom: none;
}

View File

@ -745,21 +745,33 @@ document.addEventListener('DOMContentLoaded', () => {
// Channel Permissions Management
const channelPermissionsTabBtn = document.getElementById('channel-permissions-tab-btn');
const channelPermissionsList = document.getElementById('channel-permissions-list');
const channelPermissionsRolesList = document.getElementById('channel-permissions-roles-list');
const addPermRoleList = document.getElementById('add-permission-role-list');
const channelPermissionsSettings = document.getElementById('channel-permissions-settings');
const noRoleSelectedView = document.getElementById('no-role-selected-view');
const selectedPermRoleName = document.getElementById('selected-perm-role-name');
const removeSelectedPermRole = document.getElementById('remove-selected-perm-role');
const permissionsTogglesContainer = document.getElementById('permissions-toggles-container');
let currentSelectedOverrideRole = null;
let channelPermissionsData = [];
channelPermissionsTabBtn?.addEventListener('click', async () => {
const channelId = document.getElementById('edit-channel-id').value;
loadChannelPermissions(channelId);
loadRolesForPermissions(channelId);
currentSelectedOverrideRole = null;
channelPermissionsSettings.style.display = 'none';
noRoleSelectedView.style.display = 'flex';
await loadChannelPermissions(channelId);
await loadRolesForPermissions(channelId);
});
async function loadChannelPermissions(channelId) {
channelPermissionsList.innerHTML = '<div class="text-center p-3 text-muted small">Loading permissions...</div>';
channelPermissionsRolesList.innerHTML = '<div class="text-center p-3 text-muted small">Loading...</div>';
const resp = await fetch(`api_v1_channel_permissions.php?channel_id=${channelId}`);
const data = await resp.json();
if (data.success) {
renderChannelPermissions(channelId, data.permissions);
channelPermissionsData = data.permissions;
renderRoleOverridesList(channelId);
}
}
@ -768,7 +780,16 @@ document.addEventListener('DOMContentLoaded', () => {
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`);
const data = await resp.json();
if (data.success) {
data.roles.forEach(role => {
// Filter out roles already in overrides
const existingRoleIds = channelPermissionsData.map(p => parseInt(p.role_id));
const availableRoles = data.roles.filter(role => !existingRoleIds.includes(parseInt(role.id)));
if (availableRoles.length === 0) {
addPermRoleList.innerHTML = '<li><span class="dropdown-item disabled">No more roles to add</span></li>';
return;
}
availableRoles.forEach(role => {
const li = document.createElement('li');
li.innerHTML = `<a class="dropdown-item d-flex align-items-center gap-2" href="#">
<div style="width: 10px; height: 10px; border-radius: 50%; background-color: ${role.color};"></div>
@ -781,69 +802,122 @@ document.addEventListener('DOMContentLoaded', () => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel_id: channelId, role_id: role.id, allow: 0, deny: 0 })
});
loadChannelPermissions(channelId);
await loadChannelPermissions(channelId);
selectOverrideRole(role.id, role.name);
};
addPermRoleList.appendChild(li);
});
}
}
function renderChannelPermissions(channelId, permissions) {
channelPermissionsList.innerHTML = '';
if (permissions.length === 0) {
channelPermissionsList.innerHTML = '<div class="text-center p-3 text-muted small">No role overrides.</div>';
function renderRoleOverridesList(channelId) {
channelPermissionsRolesList.innerHTML = '';
if (channelPermissionsData.length === 0) {
channelPermissionsRolesList.innerHTML = '<div class="text-center p-3 small" style="color: #dbdee1;">No overrides configured for this channel.</div>';
return;
}
permissions.forEach(p => {
// Sort: @everyone always at top, then by name
const sortedData = [...channelPermissionsData].sort((a, b) => {
const isAEveryone = a.role_name.toLowerCase().includes('everyone');
const isBEveryone = b.role_name.toLowerCase().includes('everyone');
if (isAEveryone && !isBEveryone) return -1;
if (!isAEveryone && isBEveryone) return 1;
return a.role_name.localeCompare(b.role_name);
});
sortedData.forEach(p => {
const item = document.createElement('div');
item.className = 'list-group-item bg-transparent text-white border-secondary p-2';
item.className = `list-group-item list-group-item-action bg-transparent text-white border-0 mb-1 p-2 small d-flex align-items-center ${currentSelectedOverrideRole == p.role_id ? 'active' : ''}`;
item.style.cursor = 'pointer';
item.innerHTML = `
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="d-flex align-items-center">
<div style="width: 10px; height: 10px; border-radius: 50%; background-color: ${p.role_color}; margin-right: 8px;"></div>
<span class="small fw-bold">${p.role_name}</span>
</div>
<button class="btn btn-sm text-danger remove-perm-btn" data-role-id="${p.role_id}">×</button>
</div>
<div class="d-flex gap-2">
<select class="form-select form-select-sm bg-dark text-white border-secondary perm-select" data-role-id="${p.role_id}">
<option value="allow" ${p.allow_permissions ? 'selected' : ''}>Allow Sending Messages</option>
<option value="deny" ${p.deny_permissions ? 'selected' : ''}>Deny Sending Messages</option>
<option value="neutral" ${!p.allow_permissions && !p.deny_permissions ? 'selected' : ''}>Neutral</option>
</select>
</div>
<div style="width: 8px; height: 8px; border-radius: 50%; background-color: ${p.role_color}; margin-right: 8px; flex-shrink: 0;"></div>
<span class="flex-grow-1 text-truncate">${p.role_name}</span>
`;
channelPermissionsList.appendChild(item);
item.onclick = () => selectOverrideRole(p.role_id, p.role_name);
channelPermissionsRolesList.appendChild(item);
});
}
channelPermissionsList?.addEventListener('click', async (e) => {
function selectOverrideRole(roleId, roleName) {
currentSelectedOverrideRole = roleId;
const channelId = document.getElementById('edit-channel-id').value;
if (e.target.classList.contains('remove-perm-btn')) {
const roleId = e.target.dataset.roleId;
await fetch('api_v1_channel_permissions.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel_id: channelId, role_id: roleId })
});
loadChannelPermissions(channelId);
// Update list active state
renderRoleOverridesList(channelId);
selectedPermRoleName.textContent = roleName;
noRoleSelectedView.style.display = 'none';
channelPermissionsSettings.style.display = 'block';
// Load existing permissions for this role
const p = channelPermissionsData.find(perm => perm.role_id == roleId) || { allow_permissions: 0, deny_permissions: 0 };
// Update toggles (for now only bit 1: View Channel)
updateToggleUI(1, p.allow_permissions, p.deny_permissions);
}
function updateToggleUI(bit, allowPerms, denyPerms) {
const group = document.querySelector(`.perm-tri-state[data-perm-bit="${bit}"]`);
if (!group) return;
if (allowPerms & bit) {
group.querySelector('input[value="allow"]').checked = true;
} else if (denyPerms & bit) {
group.querySelector('input[value="deny"]').checked = true;
} else {
group.querySelector('input[value="neutral"]').checked = true;
}
}
removeSelectedPermRole?.addEventListener('click', async () => {
if (!currentSelectedOverrideRole) return;
const channelId = document.getElementById('edit-channel-id').value;
await fetch('api_v1_channel_permissions.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel_id: channelId, role_id: currentSelectedOverrideRole })
});
currentSelectedOverrideRole = null;
channelPermissionsSettings.style.display = 'none';
noRoleSelectedView.style.display = 'flex';
loadChannelPermissions(channelId);
});
channelPermissionsList?.addEventListener('change', async (e) => {
if (e.target.classList.contains('perm-select')) {
const channelId = document.getElementById('edit-channel-id').value;
const roleId = e.target.dataset.roleId;
permissionsTogglesContainer?.addEventListener('change', async (e) => {
if (e.target.type === 'radio') {
const group = e.target.closest('.perm-tri-state');
const bit = parseInt(group.dataset.permBit);
const val = e.target.value;
let allow = 0, deny = 0;
if (val === 'allow') allow = 1;
if (val === 'deny') deny = 1;
const channelId = document.getElementById('edit-channel-id').value;
const roleId = currentSelectedOverrideRole;
let p = channelPermissionsData.find(perm => perm.role_id == roleId);
if (!p) {
p = { role_id: roleId, allow_permissions: 0, deny_permissions: 0 };
}
let allow = parseInt(p.allow_permissions);
let deny = parseInt(p.deny_permissions);
// Clear current bit
allow &= ~bit;
deny &= ~bit;
if (val === 'allow') allow |= bit;
if (val === 'deny') deny |= bit;
await fetch('api_v1_channel_permissions.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel_id: channelId, role_id: roleId, allow, deny })
});
// Update local data
p.allow_permissions = allow;
p.deny_permissions = deny;
}
});
@ -871,16 +945,22 @@ document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('editChannelModal');
const channelId = btn.dataset.id;
const channelType = btn.dataset.type || 'chat';
const channelName = btn.dataset.name;
modal.querySelector('#edit-channel-id').value = channelId;
modal.querySelector('#edit-channel-name').value = btn.dataset.name;
modal.querySelector('#edit-channel-name').value = channelName;
modal.querySelector('#header-channel-name').textContent = channelName;
modal.querySelector('#edit-channel-type').value = channelType;
modal.querySelector('#edit-channel-files').checked = btn.dataset.files == '1';
modal.querySelector('#edit-channel-limit').value = btn.dataset.limit || '';
modal.querySelector('#edit-channel-status').value = btn.dataset.status || '';
modal.querySelector('#edit-channel-icon').value = btn.dataset.icon || '';
modal.querySelector('#edit-channel-category-id').value = btn.dataset.category || '';
modal.querySelector('#delete-channel-id').value = channelId;
// Reset delete zone
document.getElementById('delete-confirm-zone').style.display = 'none';
// Show/Hide RSS tab
const rssTabNav = document.getElementById('rss-tab-nav');
const statusContainer = document.getElementById('edit-channel-status-container');
@ -890,8 +970,8 @@ document.addEventListener('DOMContentLoaded', () => {
} else {
rssTabNav.style.display = 'none';
// Switch to General tab if we were on RSS
if (document.getElementById('rss-tab-btn').classList.contains('active')) {
bootstrap.Tab.getInstance(modal.querySelector('.nav-link.active')).hide();
const rssTabBtn = document.getElementById('rss-tab-btn');
if (rssTabBtn && rssTabBtn.classList.contains('active')) {
bootstrap.Tab.getOrCreateInstance(modal.querySelector('[data-bs-target="#edit-channel-general"]')).show();
}
}
@ -904,6 +984,10 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
document.getElementById('delete-channel-trigger')?.addEventListener('click', () => {
document.getElementById('delete-confirm-zone').style.display = 'block';
});
// RSS Management
const editChannelType = document.getElementById('edit-channel-type');
editChannelType?.addEventListener('change', () => {

View File

@ -33,6 +33,56 @@ class Permissions {
return ($perms & $permission) === $permission;
}
public static function canViewChannel($user_id, $channel_id) {
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$c = $stmt->fetch();
if (!$c) return false;
$server_id = $c['server_id'];
// Check if owner or admin
if (self::hasPermission($user_id, $server_id, self::ADMINISTRATOR)) return true;
// Fetch overrides for all roles the user has in this server
$stmt = db()->prepare("
SELECT cp.allow_permissions, cp.deny_permissions
FROM channel_permissions cp
JOIN user_roles ur ON cp.role_id = ur.role_id
WHERE ur.user_id = ? AND cp.channel_id = ?
");
$stmt->execute([$user_id, $channel_id]);
$overrides = $stmt->fetchAll();
// Check @everyone override specifically (even if user has no roles assigned)
$stmt = db()->prepare("SELECT id FROM roles WHERE server_id = ? AND (name = '@everyone' OR name = 'Everyone') LIMIT 1");
$stmt->execute([$server_id]);
$everyone_role = $stmt->fetch();
if ($everyone_role) {
$stmt = db()->prepare("SELECT allow_permissions, deny_permissions FROM channel_permissions WHERE channel_id = ? AND role_id = ?");
$stmt->execute([$channel_id, $everyone_role['id']]);
$eo = $stmt->fetch();
if ($eo) {
$overrides[] = $eo;
}
}
if (empty($overrides)) {
return true; // Default to yes
}
$allow = false;
$deny = false;
foreach($overrides as $o) {
if ($o['allow_permissions'] & self::VIEW_CHANNEL) $allow = true;
if ($o['deny_permissions'] & self::VIEW_CHANNEL) $deny = true;
}
if ($allow) return true;
if ($deny) return false;
return true; // Default to yes
}
public static function canSendInChannel($user_id, $channel_id) {
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
@ -40,11 +90,8 @@ class Permissions {
if (!$c) return false;
$server_id = $c['server_id'];
// Check if owner
$stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmt->execute([$server_id]);
$s = $stmt->fetch();
if ($s && $s['owner_id'] == $user_id) return true;
// Check if owner or admin
if (self::hasPermission($user_id, $server_id, self::ADMINISTRATOR)) return true;
// Check overrides
$stmt = db()->prepare("
@ -56,11 +103,29 @@ class Permissions {
$stmt->execute([$user_id, $channel_id]);
$overrides = $stmt->fetchAll();
foreach($overrides as $o) {
if ($o['deny_permissions'] & 1) return false; // Bit 1 for SEND_MESSAGES in overrides
if ($o['allow_permissions'] & 1) return true;
// Check @everyone override
$stmt = db()->prepare("SELECT id FROM roles WHERE server_id = ? AND (name = '@everyone' OR name = 'Everyone') LIMIT 1");
$stmt->execute([$server_id]);
$everyone_role = $stmt->fetch();
if ($everyone_role) {
$stmt = db()->prepare("SELECT allow_permissions, deny_permissions FROM channel_permissions WHERE channel_id = ? AND role_id = ?");
$stmt->execute([$channel_id, $everyone_role['id']]);
$eo = $stmt->fetch();
if ($eo) {
$overrides[] = $eo;
}
}
$allow = false;
$deny = false;
foreach($overrides as $o) {
if ($o['allow_permissions'] & self::SEND_MESSAGES) $allow = true;
if ($o['deny_permissions'] & self::SEND_MESSAGES) $deny = true;
}
if ($allow) return true;
if ($deny) return false;
return self::hasPermission($user_id, $server_id, self::SEND_MESSAGES);
}
}

302
index.php
View File

@ -4,8 +4,12 @@ require_once 'auth/session.php';
function renderRoleIcon($icon, $size = '12px') {
if (empty($icon)) return '';
$isUrl = (strpos($icon, 'http') === 0 || strpos($icon, '/') === 0);
$isFa = (strpos($icon, 'fa-') === 0);
if ($isUrl) {
return '<img src="' . htmlspecialchars($icon) . '" class="role-icon ms-1" style="width: '.$size.'; height: '.$size.'; vertical-align: middle; object-fit: contain;">';
} elseif ($isFa) {
return '<i class="fa-solid ' . htmlspecialchars($icon) . ' ms-1" style="font-size: '.$size.'; vertical-align: middle;"></i>';
} else {
return '<span class="ms-1" style="font-size: '.$size.'; vertical-align: middle;">' . htmlspecialchars($icon) . '</span>';
}
@ -75,8 +79,17 @@ if ($is_dm_view) {
// Fetch channels
$stmt = db()->prepare("SELECT * FROM channels WHERE server_id = ? ORDER BY position ASC, id ASC");
$stmt->execute([$active_server_id]);
$channels = $stmt->fetchAll();
$active_channel_id = $_GET['channel_id'] ?? ($channels[0]['id'] ?? 1);
$all_channels = $stmt->fetchAll();
require_once 'includes/permissions.php';
$channels = [];
foreach($all_channels as $c) {
if (Permissions::canViewChannel($current_user_id, $c['id'])) {
$channels[] = $c;
}
}
$active_channel_id = $_GET['channel_id'] ?? ($channels[0]['id'] ?? 0);
// Fetch active channel details for theme
$active_channel = null;
@ -87,7 +100,6 @@ if ($is_dm_view) {
}
}
require_once 'includes/permissions.php';
$is_owner = false;
$can_manage_channels = false;
$can_manage_server = false;
@ -340,7 +352,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
</span>
<?php if (!empty($c['icon'])): ?>
<span class="me-1 custom-channel-icon"><i class="fa-solid <?php echo htmlspecialchars($c['icon']); ?>"></i></span>
<span class="me-1" style="font-size: 14px;"><?php echo renderRoleIcon($c['icon'], '14px'); ?></span>
<?php endif; ?>
<span class="channel-name-text"><?php echo htmlspecialchars($c['name']); ?></span>
</span>
@ -436,7 +448,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<!-- Chat Area -->
<div class="chat-container">
<div class="chat-header">
<span style="color: var(--text-muted); margin-right: 8px; width: 20px; display: inline-block; text-align: center;">
<span style="color: var(--text-muted); margin-right: 8px; width: auto; display: inline-block; text-align: center;">
<?php
if ($is_dm_view) {
echo '@';
@ -446,12 +458,13 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
elseif ($active_channel['type'] === 'forum') echo '<i class="fa-solid fa-comments"></i>';
elseif ($active_channel['type'] === 'voice') echo '<i class="fa-solid fa-volume-up"></i>';
else echo '<i class="fa-solid fa-hashtag"></i>';
if (!empty($active_channel['icon'])) {
echo ' <span class="ms-1">' . renderRoleIcon($active_channel['icon'], '16px') . '</span>';
}
}
?>
</span>
<?php if (!$is_dm_view && !empty($active_channel['icon'])): ?>
<span class="me-2 custom-channel-icon"><i class="fa-solid <?php echo htmlspecialchars($active_channel['icon']); ?>"></i></span>
<?php endif; ?>
<span class="flex-grow-1"><?php echo htmlspecialchars($current_channel_name); ?></span>
<div class="d-flex align-items-center">
@ -1109,152 +1122,217 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<!-- Edit Channel Modal -->
<div class="modal fade" id="editChannelModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Channel Settings</h5>
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow-lg" style="background-color: #313338;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">Channel Settings #<span id="header-channel-name"></span></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<ul class="nav nav-tabs nav-fill" id="editChannelTabs" role="tablist">
<ul class="nav nav-tabs nav-fill px-3 border-bottom border-secondary" id="editChannelTabs" role="tablist" style="background-color: #2b2d31;">
<li class="nav-item">
<button class="nav-link active text-white border-0 bg-transparent" data-bs-toggle="tab" data-bs-target="#edit-channel-general" type="button">General</button>
<button class="nav-link active text-white border-0 bg-transparent py-3" data-bs-toggle="tab" data-bs-target="#edit-channel-general" type="button">Overview</button>
</li>
<li class="nav-item" id="rss-tab-nav" style="display: none;">
<button class="nav-link text-white border-0 bg-transparent" id="rss-tab-btn" data-bs-toggle="tab" data-bs-target="#edit-channel-rss" type="button">RSS Feeds</button>
<button class="nav-link text-white border-0 bg-transparent py-3" id="rss-tab-btn" data-bs-toggle="tab" data-bs-target="#edit-channel-rss" type="button">RSS Feeds</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="channel-permissions-tab-btn" data-bs-toggle="tab" data-bs-target="#edit-channel-permissions" type="button">Permissions</button>
<button class="nav-link text-white border-0 bg-transparent py-3" id="channel-permissions-tab-btn" data-bs-toggle="tab" data-bs-target="#edit-channel-permissions" type="button">Permissions</button>
</li>
</ul>
<div class="tab-content p-3">
<div class="tab-content p-4" style="min-height: 450px;">
<div class="tab-pane fade show active" id="edit-channel-general">
<form action="api_v1_channels.php" method="POST">
<input type="hidden" name="action" value="update">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<input type="hidden" name="channel_id" id="edit-channel-id">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Type</label>
<select name="type" id="edit-channel-type" class="form-select bg-dark text-white border-secondary mb-3">
<option value="chat">Traditional Chat</option>
<option value="announcement">Announcements</option>
<option value="rules">Rules</option>
<option value="forum">Forum</option>
<option value="voice">Voice Channel</option>
<option value="category">Category (Separator)</option>
</select>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Parent Category</label>
<select name="category_id" id="edit-channel-category-id" class="form-select bg-dark text-white border-secondary">
<option value="">No Category (Top level)</option>
<?php
foreach($channels as $cat) {
if ($cat['type'] === 'category') {
echo '<option value="'.$cat['id'].'">'.htmlspecialchars($cat['name']).'</option>';
}
}
?>
</select>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Name</label>
<div class="input-group">
<span class="input-group-text bg-dark border-0 text-muted" id="edit-channel-prefix">#</span>
<input type="text" name="name" id="edit-channel-name" class="form-control" required>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Name</label>
<div class="input-group">
<span class="input-group-text bg-dark border-secondary text-muted">#</span>
<input type="text" name="name" id="edit-channel-name" class="form-control bg-dark text-white border-secondary" required>
</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Type</label>
<select name="type" id="edit-channel-type" class="form-select bg-dark text-white border-secondary">
<option value="chat">Text Channel</option>
<option value="announcement">Announcements</option>
<option value="rules">Rules</option>
<option value="forum">Forum</option>
<option value="voice">Voice Channel</option>
<option value="category">Category</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Category</label>
<select name="category_id" id="edit-channel-category-id" class="form-select bg-dark text-white border-secondary">
<option value="">No Category</option>
<?php
foreach($all_channels as $cat) {
if ($cat['type'] === 'category') {
echo '<option value="'.$cat['id'].'">'.htmlspecialchars($cat['name']).'</option>';
}
}
?>
</select>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Icon</label>
<select name="icon" id="edit-channel-icon" class="form-select bg-dark text-white border-secondary">
<option value="">Aucune icône personnalisée</option>
<option value="fa-hashtag"># Hashtag</option>
<option value="fa-volume-up">🔊 Voice</option>
<option value="fa-bullhorn">📢 Announcements</option>
<option value="fa-gavel">🔨 Rules</option>
<option value="fa-comments">💬 Forum</option>
<option value="fa-lock">🔒 Private</option>
<option value="fa-star"> Star</option>
<option value="fa-heart">❤️ Heart</option>
<option value="fa-gamepad">🎮 Gaming</option>
<option value="fa-music">🎵 Music</option>
<option value="fa-video">📹 Video</option>
<option value="fa-info-circle"> Info</option>
<option value="fa-question-circle"> Help</option>
<option value="fa-book">📖 Library</option>
<option value="fa-gift">🎁 Giveaways</option>
<option value="fa-code">💻 Coding</option>
<option value="fa-terminal">⌨️ Bot</option>
</select>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Icon</label>
<select name="icon" id="edit-channel-icon" class="form-select bg-dark text-white border-secondary mb-3">
<option value="">Aucune icône personnalisée</option>
<option value="fa-hashtag"># Hashtag</option>
<option value="fa-volume-up">🔊 Voice</option>
<option value="fa-bullhorn">📢 Announcements</option>
<option value="fa-gavel">🔨 Rules</option>
<option value="fa-comments">💬 Forum</option>
<option value="fa-lock">🔒 Private</option>
<option value="fa-star"> Star</option>
<option value="fa-heart">❤️ Heart</option>
<option value="fa-gamepad">🎮 Gaming</option>
<option value="fa-music">🎵 Music</option>
<option value="fa-video">📹 Video</option>
<option value="fa-info-circle"> Info</option>
<option value="fa-question-circle"> Help</option>
<option value="fa-book">📖 Library</option>
<option value="fa-gift">🎁 Giveaways</option>
<option value="fa-code">💻 Coding</option>
<option value="fa-terminal">⌨️ Bot</option>
</select>
<div class="row mt-2">
<div class="col-12">
<div class="mb-3" id="edit-channel-status-container" style="display: none;">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Voice Status</label>
<input type="text" name="status" id="edit-channel-status" class="form-control bg-dark text-white border-secondary" placeholder="What's happening?">
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Message Retention</label>
<input type="number" name="message_limit" id="edit-channel-limit" class="form-control bg-dark text-white border-secondary" placeholder="Keep all messages">
</div>
<div class="form-check form-switch mb-4">
<input class="form-check-input" type="checkbox" name="allow_file_sharing" id="edit-channel-files" value="1">
<label class="form-check-label text-white" for="edit-channel-files">Allow users to upload files</label>
</div>
</div>
</div>
<div class="mb-3" id="edit-channel-status-container" style="display: none;">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Status (Voice only)</label>
<input type="text" name="status" id="edit-channel-status" class="form-control" placeholder="What's happening?">
<div class="form-text text-muted" style="font-size: 0.8em;">Visible below the channel name.</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary flex-grow-1">Save Changes</button>
<button type="button" id="clear-channel-history-btn" class="btn btn-outline-warning">Clear Chat</button>
<button type="button" class="btn btn-outline-danger" id="delete-channel-trigger">Delete</button>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="allow_file_sharing" id="edit-channel-files" value="1">
<label class="form-check-label text-white" for="edit-channel-files">Allow File Sharing</label>
<div class="form-text text-muted" style="font-size: 0.8em;">When disabled, users cannot upload files in this channel.</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Message Limit</label>
<input type="number" name="message_limit" id="edit-channel-limit" class="form-control" placeholder="No limit">
<div class="form-text text-muted" style="font-size: 0.8em;">Keep only the most recent messages.</div>
</div>
<button type="submit" class="btn btn-primary w-100 mb-2">Save Changes</button>
</form>
<button type="button" id="clear-channel-history-btn" class="btn btn-warning w-100 mb-2">Vider l'historique</button>
<form action="api_v1_channels.php" method="POST" onsubmit="return confirm('Are you sure you want to delete this channel?');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<input type="hidden" name="channel_id" id="delete-channel-id">
<button type="submit" class="btn btn-danger w-100">Delete Channel</button>
</form>
<div id="delete-confirm-zone" style="display: none;" class="mt-3 p-3 border border-danger rounded">
<p class="text-danger small mb-2">Are you sure? This cannot be undone.</p>
<form action="api_v1_channels.php" method="POST" class="d-flex gap-2">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<input type="hidden" name="channel_id" id="delete-channel-id">
<button type="submit" class="btn btn-danger btn-sm">Yes, Delete Channel</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="document.getElementById('delete-confirm-zone').style.display='none'">Cancel</button>
</form>
</div>
</div>
<div class="tab-pane fade" id="edit-channel-rss">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Add New RSS Feed</label>
<div class="mb-4">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Add RSS Feed</label>
<div class="input-group">
<input type="url" id="new-rss-url" class="form-control bg-dark text-white border-secondary" placeholder="https://example.com/rss.xml">
<input type="url" id="new-rss-url" class="form-control bg-dark text-white border-secondary" placeholder="https://example.com/feed.xml">
<button class="btn btn-primary" type="button" id="add-rss-btn">Add</button>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label text-uppercase fw-bold mb-0" style="font-size: 0.7em; color: var(--text-muted);">Active Feeds</label>
<button class="btn btn-sm btn-outline-info" id="sync-rss-btn">Sync Now</button>
<label class="form-label text-uppercase fw-bold mb-0" style="font-size: 0.7em; color: var(--text-muted);">Active Subscriptions</label>
<button class="btn btn-sm btn-outline-info" id="sync-rss-btn">Sync All</button>
</div>
<div id="rss-feeds-list" class="list-group list-group-flush bg-transparent">
<div id="rss-feeds-list" class="list-group list-group-flush bg-transparent border rounded border-secondary" style="max-height: 250px; overflow-y: auto;">
<!-- RSS feeds loaded here -->
</div>
</div>
<div class="tab-pane fade" id="edit-channel-permissions">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Role Overrides</h6>
<div class="dropdown">
<button class="btn btn-sm btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">Add Role</button>
<ul class="dropdown-menu dropdown-menu-dark" id="add-permission-role-list">
<!-- Server roles loaded here -->
</ul>
<div class="tab-pane fade h-100" id="edit-channel-permissions">
<div class="row h-100 g-0 border rounded border-secondary overflow-hidden" style="background-color: #2b2d31;">
<!-- Sidebar: Roles & Members -->
<div class="col-4 border-end border-secondary d-flex flex-column" style="background-color: #2b2d31;">
<div class="p-3 border-bottom border-secondary d-flex justify-content-between align-items-center">
<span class="small fw-bold text-uppercase" style="font-size: 0.7em; color: #dbdee1;">Roles / Members</span>
<div class="dropdown">
<button class="btn btn-sm btn-link text-white p-0" type="button" data-bs-toggle="dropdown" style="text-decoration: none;">
<i class="fa-solid fa-plus-circle"></i>
</button>
<ul class="dropdown-menu dropdown-menu-dark shadow" id="add-permission-role-list" style="max-height: 300px; overflow-y: auto;">
<!-- Roles loaded here -->
</ul>
</div>
</div>
<div id="channel-permissions-roles-list" class="list-group list-group-flush overflow-auto flex-grow-1" style="max-height: 350px; overflow-x: hidden;">
<!-- List of roles with overrides -->
</div>
</div>
<!-- Main: Permission Settings -->
<div class="col-8 d-flex flex-column" style="background-color: #313338;">
<div id="channel-permissions-settings" style="display: none;" class="h-100 d-flex flex-column">
<div class="p-3 border-bottom border-secondary d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold" id="selected-perm-role-name">Role Name</h6>
<button class="btn btn-sm btn-outline-danger py-0 px-2" id="remove-selected-perm-role" style="font-size: 0.75em;">Clear Overrides</button>
</div>
<div class="p-3 overflow-auto flex-grow-1" id="permissions-toggles-container" style="max-height: 350px; overflow-x: hidden;">
<div class="permission-item mb-3 p-2 rounded" style="background: rgba(255,255,255,0.02);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">View Channel</div>
<div style="font-size: 0.75em; color: #b5bac1;">Allows members to view this channel.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="1">
<input type="radio" class="btn-check" name="perm_1" id="perm_1_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_1_deny" title="Deny"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_1" id="perm_1_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_1_neutral" title="Neutral">/</label>
<input type="radio" class="btn-check" name="perm_1" id="perm_1_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_1_allow" title="Allow"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<!-- More permissions can be added here -->
</div>
</div>
<div class="h-100 d-flex align-items-center justify-content-center text-muted p-4" id="no-role-selected-view">
<div class="text-center">
<i class="fa-solid fa-lock mb-3" style="font-size: 2.5rem; opacity: 0.2;"></i>
<p class="small mb-0">Select a role or member on the left to configure their specific permissions for this channel.</p>
</div>
</div>
</div>
</div>
<div id="channel-permissions-list" class="list-group list-group-flush bg-transparent">
<!-- Channel permissions loaded here -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Pinned Messages Modal -->
<div class="modal fade" id="pinnedMessagesModal" tabindex="-1">