diff --git a/api_v1_channel_permissions.php b/api_v1_channel_permissions.php
index 078e7f6..4373c26 100644
--- a/api_v1_channel_permissions.php
+++ b/api_v1_channel_permissions.php
@@ -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;
}
diff --git a/api_v1_servers.php b/api_v1_servers.php
index f324857..05b064d 100644
--- a/api_v1_servers.php
+++ b/api_v1_servers.php
@@ -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);
diff --git a/assets/css/discord.css b/assets/css/discord.css
index db4b44c..77347c5 100644
--- a/assets/css/discord.css
+++ b/assets/css/discord.css
@@ -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;
+}
+
+
diff --git a/assets/js/main.js b/assets/js/main.js
index f5ee9f2..b4b3d00 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -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 = '
Loading permissions...
';
+ channelPermissionsRolesList.innerHTML = 'Loading...
';
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 = 'No more roles to add';
+ return;
+ }
+
+ availableRoles.forEach(role => {
const li = document.createElement('li');
li.innerHTML = `
@@ -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 = 'No role overrides.
';
+ function renderRoleOverridesList(channelId) {
+ channelPermissionsRolesList.innerHTML = '';
+ if (channelPermissionsData.length === 0) {
+ channelPermissionsRolesList.innerHTML = 'No overrides configured for this channel.
';
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 = `
-
-
-
-
+
+ ${p.role_name}
`;
- 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', () => {
diff --git a/includes/permissions.php b/includes/permissions.php
index 064c9e9..ab0b5ba 100644
--- a/includes/permissions.php
+++ b/includes/permissions.php
@@ -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);
}
}
diff --git a/index.php b/index.php
index 6b54385..5f3b5ab 100644
--- a/index.php
+++ b/index.php
@@ -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 '
';
+ } elseif ($isFa) {
+ return '';
} else {
return '' . htmlspecialchars($icon) . '';
}
@@ -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'] ?? '';
?>
-
+
@@ -436,7 +448,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';