diff --git a/api_v1_channels.php b/api_v1_channels.php
index 62b2dbf..cadc6d3 100644
--- a/api_v1_channels.php
+++ b/api_v1_channels.php
@@ -17,6 +17,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ // Handle JSON input
+ $json = json_decode(file_get_contents('php://input'), true);
+ if ($json) {
+ $action = $json['action'] ?? '';
+ if ($action === 'reorder') {
+ $server_id = $json['server_id'] ?? 0;
+ $orders = $json['orders'] ?? []; // Array of {id, position, category_id}
+ $user_id = $_SESSION['user_id'];
+
+ if (Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_CHANNELS)) {
+ $stmt = db()->prepare("UPDATE channels SET position = ?, category_id = ? WHERE id = ? AND server_id = ?");
+ foreach ($orders as $o) {
+ $stmt->execute([$o['position'], $o['category_id'] ?: null, $o['id'], $server_id]);
+ }
+ echo json_encode(['success' => true]);
+ } else {
+ echo json_encode(['success' => false, 'error' => 'Permission denied']);
+ }
+ exit;
+ }
+ }
+
$action = $_POST['action'] ?? 'create';
$server_id = $_POST['server_id'] ?? 0;
$user_id = $_SESSION['user_id'];
@@ -30,6 +52,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message_limit = !empty($_POST['message_limit']) ? (int)$_POST['message_limit'] : null;
$theme_color = $_POST['theme_color'] ?? null;
if ($theme_color === '') $theme_color = null;
+ $icon = $_POST['icon'] ?? null;
+ if ($icon === '') $icon = null;
+ $category_id = !empty($_POST['category_id']) ? (int)$_POST['category_id'] : null;
// Check if user has permission to manage channels
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
@@ -38,8 +63,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($chan && Permissions::hasPermission($user_id, $chan['server_id'], Permissions::MANAGE_CHANNELS)) {
$name = strtolower(preg_replace('/[^a-zA-Z0-9\-]/', '-', $name));
- $stmt = db()->prepare("UPDATE channels SET name = ?, type = ?, status = ?, allow_file_sharing = ?, theme_color = ?, message_limit = ? WHERE id = ?");
- $stmt->execute([$name, $type, $status, $allow_file_sharing, $theme_color, $message_limit, $channel_id]);
+ $stmt = db()->prepare("UPDATE channels SET name = ?, type = ?, status = ?, allow_file_sharing = ?, theme_color = ?, message_limit = ?, icon = ?, category_id = ? WHERE id = ?");
+ $stmt->execute([$name, $type, $status, $allow_file_sharing, $theme_color, $message_limit, $icon, $category_id, $channel_id]);
}
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
exit;
@@ -72,9 +97,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message_limit = !empty($_POST['message_limit']) ? (int)$_POST['message_limit'] : null;
$theme_color = $_POST['theme_color'] ?? null;
if ($theme_color === '') $theme_color = null;
+ $icon = $_POST['icon'] ?? null;
+ if ($icon === '') $icon = null;
+ $category_id = !empty($_POST['category_id']) ? (int)$_POST['category_id'] : null;
- $stmt = db()->prepare("INSERT INTO channels (server_id, name, type, allow_file_sharing, theme_color, message_limit) VALUES (?, ?, ?, ?, ?, ?)");
- $stmt->execute([$server_id, $name, $type, $allow_file_sharing, $theme_color, $message_limit]);
+ $stmt = db()->prepare("INSERT INTO channels (server_id, name, type, allow_file_sharing, theme_color, message_limit, icon, category_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$server_id, $name, $type, $allow_file_sharing, $theme_color, $message_limit, $icon, $category_id]);
$channel_id = db()->lastInsertId();
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
diff --git a/assets/css/discord.css b/assets/css/discord.css
index f3498b6..13dcd35 100644
--- a/assets/css/discord.css
+++ b/assets/css/discord.css
@@ -178,17 +178,6 @@ body {
color: var(--text-primary);
}
-.channel-item::before {
- content: "#";
- font-size: 1.2em;
- font-weight: 300;
-}
-
-.voice-item::before {
- content: "๐";
- font-size: 0.9em;
-}
-
.server-icon.add-btn {
color: #23a559;
}
@@ -203,13 +192,48 @@ body {
font-size: 0.75em;
text-transform: uppercase;
font-weight: bold;
- margin-bottom: 8px;
- padding-left: 8px;
+ margin-bottom: 4px;
+ padding: 16px 8px 4px 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
+.channel-category .channel-settings-btn,
+.channel-category .add-channel-btn {
+ opacity: 0;
+ transition: opacity 0.2s;
+ font-size: 1.2em;
+ cursor: pointer;
+}
+
+.channel-category:hover .channel-settings-btn,
+.channel-category:hover .add-channel-btn {
+ opacity: 1;
+}
+
+.channel-item-container .channel-settings-btn {
+ opacity: 0;
+ transition: opacity 0.2s;
+}
+
+.channel-item-container:hover .channel-settings-btn {
+ opacity: 1;
+}
+
+.category-group .channel-item-container {
+ margin-left: 8px;
+}
+
+.category-group {
+ min-height: 5px;
+}
+
+.sortable-ghost {
+ background-color: var(--hover) !important;
+ opacity: 0.5;
+}
+
.add-channel-btn {
cursor: pointer;
font-size: 1.2em;
@@ -400,7 +424,27 @@ body {
width: 240px;
background-color: var(--bg-members);
padding: 24px 8px;
- display: none; /* Hidden on mobile/small screens */
+ display: flex;
+ flex-direction: column;
+}
+
+.members-sidebar.hidden {
+ display: none;
+}
+
+@media (max-width: 992px) {
+ .members-sidebar {
+ display: none;
+ }
+ .members-sidebar.show {
+ display: flex;
+ position: absolute;
+ right: 0;
+ top: 48px;
+ bottom: 0;
+ z-index: 100;
+ box-shadow: -2px 0 10px rgba(0,0,0,0.5);
+ }
}
/* Reactions */
diff --git a/assets/js/main.js b/assets/js/main.js
index 0b16feb..f05e60f 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -53,18 +53,7 @@ document.addEventListener('DOMContentLoaded', () => {
new Notification(`Mention in #${window.currentChannelName}`, {
body: `${data.username}: ${data.content}`,
icon: data.avatar_url || ''
- if (e.target.classList.contains('move-rule-btn')) {
- const id = e.target.dataset.id;
- const dir = e.target.dataset.dir;
- const resp = await fetch('api_v1_rules.php', {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id, dir })
- });
- if ((await resp.json()).success) location.reload();
- }
- });
-
+ });
}
}
}
@@ -594,10 +583,21 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
- document.addEventListener('click', (e) => {
+ document.addEventListener('click', async (e) => {
if (!e.target.closest('.search-container')) {
searchResults.style.display = 'none';
}
+
+ if (e.target.classList.contains('move-rule-btn')) {
+ const id = e.target.dataset.id;
+ const dir = e.target.dataset.dir;
+ const resp = await fetch('api_v1_rules.php', {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ id, dir })
+ });
+ if ((await resp.json()).success) location.reload();
+ }
});
// Roles Management
@@ -615,6 +615,7 @@ document.addEventListener('DOMContentLoaded', () => {
modal.querySelector('#edit-channel-limit').value = btn.dataset.limit || '';
modal.querySelector('#edit-channel-status').value = btn.dataset.status || '';
modal.querySelector('#edit-channel-theme').value = btn.dataset.theme || '#5865f2';
+ modal.querySelector('#edit-channel-icon').value = btn.dataset.icon || '';
modal.querySelector('#delete-channel-id').value = channelId;
// Show/Hide RSS tab
@@ -1348,6 +1349,19 @@ document.addEventListener('DOMContentLoaded', () => {
} catch (e) { console.error(e); }
});
+ // Toggle members sidebar
+ const toggleMembersBtn = document.getElementById('toggle-members-btn');
+ const membersSidebar = document.querySelector('.members-sidebar');
+ if (toggleMembersBtn && membersSidebar) {
+ toggleMembersBtn.addEventListener('click', () => {
+ if (window.innerWidth > 992) {
+ membersSidebar.classList.toggle('hidden');
+ } else {
+ membersSidebar.classList.toggle('show');
+ }
+ });
+ }
+
// User Settings - Save
const saveSettingsBtn = document.getElementById('save-settings-btn');
saveSettingsBtn?.addEventListener('click', async () => {
diff --git a/assets/pasted-20260215-151928-c94822be.png b/assets/pasted-20260215-151928-c94822be.png
new file mode 100644
index 0000000..c6b60fa
Binary files /dev/null and b/assets/pasted-20260215-151928-c94822be.png differ
diff --git a/assets/pasted-20260215-153522-763a8478.png b/assets/pasted-20260215-153522-763a8478.png
new file mode 100644
index 0000000..1acd0a1
Binary files /dev/null and b/assets/pasted-20260215-153522-763a8478.png differ
diff --git a/includes/permissions.php b/includes/permissions.php
index 9e467f5..064c9e9 100644
--- a/includes/permissions.php
+++ b/includes/permissions.php
@@ -9,6 +9,11 @@ class Permissions {
const ADMINISTRATOR = 32;
public static function hasPermission($user_id, $server_id, $permission) {
+ $stmt = db()->prepare("SELECT is_admin FROM users WHERE id = ?");
+ $stmt->execute([$user_id]);
+ $user = $stmt->fetch();
+ if ($user && $user['is_admin']) return true;
+
$stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmt->execute([$server_id]);
$server = $stmt->fetch();
diff --git a/index.php b/index.php
index ca8469c..cd80336 100644
--- a/index.php
+++ b/index.php
@@ -63,7 +63,7 @@ if ($is_dm_view) {
$active_server_id = $_GET['server_id'] ?? ($servers[0]['id'] ?? 1);
// Fetch channels
- $stmt = db()->prepare("SELECT * FROM channels WHERE server_id = ?");
+ $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);
@@ -192,8 +192,10 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+
+