Autosave: 20260215-151332

This commit is contained in:
Flatlogic Bot 2026-02-15 15:13:32 +00:00
parent 9c07e1ee23
commit 40f605d106
16 changed files with 1621 additions and 240 deletions

View File

@ -1,6 +1,7 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
@ -23,20 +24,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($action === 'update') {
$channel_id = $_POST['channel_id'] ?? 0;
$name = $_POST['name'] ?? '';
$type = $_POST['type'] ?? 'chat';
$status = $_POST['status'] ?? null;
$allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0;
$message_limit = !empty($_POST['message_limit']) ? (int)$_POST['message_limit'] : null;
$theme_color = $_POST['theme_color'] ?? null;
if ($theme_color === '') $theme_color = null;
// Check if user is owner of the server
$stmt = db()->prepare("SELECT s.owner_id FROM servers s JOIN channels c ON s.id = c.server_id WHERE c.id = ?");
// Check if user has permission to manage channels
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$server = $stmt->fetch();
if ($server && $server['owner_id'] == $user_id) {
$chan = $stmt->fetch();
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 = ?, allow_file_sharing = ?, theme_color = ?, message_limit = ? WHERE id = ?");
$stmt->execute([$name, $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 = ? WHERE id = ?");
$stmt->execute([$name, $type, $status, $allow_file_sharing, $theme_color, $message_limit, $channel_id]);
}
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
exit;
@ -44,16 +47,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($action === 'delete') {
$channel_id = $_POST['channel_id'] ?? 0;
// Check if user is owner
$stmt = db()->prepare("SELECT s.owner_id, s.id as server_id FROM servers s JOIN channels c ON s.id = c.server_id WHERE c.id = ?");
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$server = $stmt->fetch();
$chan = $stmt->fetch();
if ($server && $server['owner_id'] == $user_id) {
if ($chan && Permissions::hasPermission($user_id, $chan['server_id'], Permissions::MANAGE_CHANNELS)) {
$stmt = db()->prepare("DELETE FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
}
header('Location: index.php?server_id=' . ($server['server_id'] ?? ''));
header('Location: index.php?server_id=' . ($chan['server_id'] ?? ''));
exit;
}
@ -61,11 +63,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$type = $_POST['type'] ?? 'text';
$user_id = $_SESSION['user_id'];
// Check if user is member of the server
$stmt = db()->prepare("SELECT 1 FROM server_members WHERE server_id = ? AND user_id = ?");
$stmt->execute([$server_id, $user_id]);
if ($stmt->fetch() && $name) {
// Check if user has permission to manage channels
if (Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_CHANNELS) && $name) {
try {
// Basic sanitization for channel name
$name = strtolower(preg_replace('/[^a-zA-Z0-9\-]/', '-', $name));

View File

@ -32,8 +32,8 @@ $stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmt->execute([$server_id]);
$server = $stmt->fetch();
if ($server["owner_id"] != $_SESSION["user_id"]) {
echo json_encode(["success" => false, "error" => "Only the server owner can clear history"]);
if (!Permissions::hasPermission($_SESSION["user_id"], $server_id, Permissions::MANAGE_CHANNELS)) {
echo json_encode(["success" => false, "error" => "Only moderators or admins can clear history"]);
exit;
}

View File

@ -38,14 +38,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
if ($pinned) {
try {
// Get server_id for the channel
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$server_id = $stmt->fetchColumn();
$stmt = db()->prepare("
SELECT m.*, u.username, u.avatar_url
SELECT m.*, u.username, u.avatar_url,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color
FROM messages m
JOIN users u ON m.user_id = u.id
WHERE m.channel_id = ? AND m.is_pinned = 1
ORDER BY m.created_at DESC
");
$stmt->execute([$channel_id]);
$stmt->execute([$server_id ?: 0, $channel_id]);
$msgs = $stmt->fetchAll();
foreach ($msgs as &$m) {
@ -120,15 +126,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
$content = '';
$channel_id = 0;
$thread_id = null;
$attachment_url = null;
if (strpos($_SERVER['CONTENT_TYPE'] ?? '', 'application/json') !== false) {
$data = json_decode(file_get_contents('php://input'), true);
$content = $data['content'] ?? '';
$channel_id = $data['channel_id'] ?? 0;
$thread_id = !empty($data['thread_id']) ? (int)$data['thread_id'] : null;
} else {
$content = $_POST['content'] ?? '';
$channel_id = $_POST['channel_id'] ?? 0;
$thread_id = !empty($_POST['thread_id']) ? (int)$_POST['thread_id'] : null;
// Check if file sharing is allowed in this channel
$stmt = db()->prepare("SELECT allow_file_sharing FROM channels WHERE id = ?");
@ -185,8 +194,8 @@ if (!empty($content)) {
}
try {
$stmt = db()->prepare("INSERT INTO messages (channel_id, user_id, content, attachment_url, metadata) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$channel_id, $user_id, $content, $attachment_url, $metadata]);
$stmt = db()->prepare("INSERT INTO messages (channel_id, thread_id, user_id, content, attachment_url, metadata) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$channel_id, $thread_id, $user_id, $content, $attachment_url, $metadata]);
$last_id = db()->lastInsertId();
// Enforce message limit if set
@ -211,9 +220,20 @@ try {
$stmt->execute([$channel_id, $channel_id, $limit]);
}
// Fetch message with username for the response
$stmt = db()->prepare("SELECT m.*, u.username, u.avatar_url FROM messages m JOIN users u ON m.user_id = u.id WHERE m.id = ?");
$stmt->execute([$last_id]);
// Get server_id for the channel
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$server_id = $stmt->fetchColumn();
// Fetch message with username and role color for the response
$stmt = db()->prepare("
SELECT m.*, u.username, u.avatar_url,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color
FROM messages m
JOIN users u ON m.user_id = u.id
WHERE m.id = ?
");
$stmt->execute([$server_id ?: 0, $last_id]);
$msg = $stmt->fetch();
echo json_encode([
@ -223,6 +243,7 @@ try {
'user_id' => $msg['user_id'],
'username' => $msg['username'],
'avatar_url' => $msg['avatar_url'],
'role_color' => $msg['role_color'],
'content' => $msg['content'],
'attachment_url' => $msg['attachment_url'],
'metadata' => $msg['metadata'] ? json_decode($msg['metadata']) : null,

View File

@ -1,5 +1,7 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
$user_id = $_SESSION['user_id'];
@ -7,21 +9,68 @@ $data = json_decode(file_get_contents('php://input'), true) ?? $_POST;
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$server_id = $_GET['server_id'] ?? 0;
if (!$server_id) {
echo json_encode(['success' => false, 'error' => 'Missing server_id']);
exit;
}
// Verify user is in server
$stmt = db()->prepare("SELECT * FROM server_members WHERE server_id = ? AND user_id = ?");
$stmt->execute([$server_id, $user_id]);
if (!$stmt->fetch()) {
echo json_encode(['success' => false, 'error' => 'Access denied']);
exit;
}
$stmt = db()->prepare("SELECT * FROM roles WHERE server_id = ? ORDER BY position DESC");
$stmt->execute([$server_id]);
echo json_encode(['success' => true, 'roles' => $stmt->fetchAll()]);
$roles = $stmt->fetchAll();
// Fetch members and their roles
$stmt = db()->prepare("
SELECT u.id, u.username, u.avatar_url,
GROUP_CONCAT(r.id) as role_ids,
GROUP_CONCAT(r.name) as role_names,
(SELECT r2.color FROM roles r2 JOIN user_roles ur2 ON r2.id = ur2.role_id WHERE ur2.user_id = u.id AND r2.server_id = ? ORDER BY r2.position DESC LIMIT 1) as role_color
FROM users u
JOIN server_members sm ON u.id = sm.user_id
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id AND r.server_id = ?
WHERE sm.server_id = ?
GROUP BY u.id
");
$stmt->execute([$server_id, $server_id, $server_id]);
$members = $stmt->fetchAll();
echo json_encode([
'success' => true,
'roles' => $roles,
'members' => $members,
'permissions_list' => [
['value' => 1, 'name' => 'View Channels'],
['value' => 2, 'name' => 'Send Messages'],
['value' => 4, 'name' => 'Manage Messages'],
['value' => 8, 'name' => 'Manage Channels'],
['value' => 16, 'name' => 'Manage Server'],
['value' => 32, 'name' => 'Administrator']
]
]);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $data['action'] ?? '';
$server_id = $data['server_id'] ?? 0;
$action = $data['action'] ?? 'create';
// Check if user is owner of server
// Permissions check: Owner or MANAGE_SERVER
$stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmt->execute([$server_id]);
$server = $stmt->fetch();
if (!$server || $server['owner_id'] != $user_id) {
$is_owner = ($server && $server['owner_id'] == $user_id);
$can_manage = Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_SERVER) || Permissions::hasPermission($user_id, $server_id, Permissions::ADMINISTRATOR);
if (!$is_owner && !$can_manage) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
@ -29,58 +78,47 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($action === 'create') {
$name = $data['name'] ?? 'New Role';
$color = $data['color'] ?? '#99aab5';
$stmt = db()->prepare("INSERT INTO roles (server_id, name, color) VALUES (?, ?, ?)");
$stmt->execute([$server_id, $name, $color]);
$perms = $data['permissions'] ?? 0;
$stmt = db()->prepare("INSERT INTO roles (server_id, name, color, permissions) VALUES (?, ?, ?, ?)");
$stmt->execute([$server_id, $name, $color, $perms]);
echo json_encode(['success' => true, 'role_id' => db()->lastInsertId()]);
} elseif ($action === 'update') {
$role_id = $data['id'] ?? 0;
$name = $data['name'] ?? '';
$color = $data['color'] ?? '';
$perms = $data['permissions'] ?? 0;
$stmt = db()->prepare("UPDATE roles SET name = ?, color = ?, permissions = ? WHERE id = ? AND server_id = ?");
$stmt->execute([$name, $color, $perms, $role_id, $server_id]);
echo json_encode(['success' => true]);
} elseif ($action === 'delete') {
$role_id = $data['id'] ?? 0;
$stmt = db()->prepare("DELETE FROM roles WHERE id = ? AND server_id = ?");
$stmt->execute([$role_id, $server_id]);
echo json_encode(['success' => true]);
} elseif ($action === 'assign') {
$target_user_id = $data['user_id'] ?? 0;
$role_id = $data['role_id'] ?? 0;
// Verify role belongs to server
$stmt = db()->prepare("SELECT id FROM roles WHERE id = ? AND server_id = ?");
$stmt->execute([$role_id, $server_id]);
if (!$stmt->fetch()) {
echo json_encode(['success' => false, 'error' => 'Invalid role']);
exit;
}
$stmt = db()->prepare("INSERT IGNORE INTO user_roles (user_id, role_id) VALUES (?, ?)");
$stmt->execute([$target_user_id, $role_id]);
echo json_encode(['success' => true]);
} elseif ($action === 'unassign') {
$target_user_id = $data['user_id'] ?? 0;
$role_id = $data['role_id'] ?? 0;
$stmt = db()->prepare("DELETE FROM user_roles WHERE user_id = ? AND role_id = ?");
$stmt->execute([$target_user_id, $role_id]);
$stmt = db()->prepare("DELETE ur FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = ? AND ur.role_id = ? AND r.server_id = ?");
$stmt->execute([$target_user_id, $role_id, $server_id]);
echo json_encode(['success' => true]);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$role_id = $data['id'] ?? 0;
$name = $data['name'] ?? '';
$color = $data['color'] ?? '';
$permissions = $data['permissions'] ?? null;
// Check server ownership via role
$stmt = db()->prepare("SELECT s.owner_id FROM servers s JOIN roles r ON s.id = r.server_id WHERE r.id = ?");
$stmt->execute([$role_id]);
$server = $stmt->fetch();
if ($server && $server['owner_id'] == $user_id) {
$stmt = db()->prepare("UPDATE roles SET name = ?, color = ?, permissions = ? WHERE id = ?");
$stmt->execute([$name, $color, $permissions, $role_id]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
$role_id = $data['id'] ?? 0;
$stmt = db()->prepare("SELECT s.owner_id FROM servers s JOIN roles r ON s.id = r.server_id WHERE r.id = ?");
$stmt->execute([$role_id]);
$server = $stmt->fetch();
if ($server && $server['owner_id'] == $user_id) {
$stmt = db()->prepare("DELETE FROM roles WHERE id = ?");
$stmt->execute([$role_id]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
}
exit;
}

116
api_v1_rss.php Normal file
View File

@ -0,0 +1,116 @@
<?php
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
$user = getCurrentUser();
$action = $_REQUEST['action'] ?? '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$channel_id = $_POST['channel_id'] ?? 0;
// Permission check: must have manage channels permission
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$chan = $stmt->fetch();
if (!$chan || !Permissions::hasPermission($user['id'], $chan['server_id'], Permissions::MANAGE_CHANNELS)) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($action === 'add') {
$url = $_POST['url'] ?? '';
if (!filter_var($url, FILTER_VALIDATE_URL)) {
echo json_encode(['success' => false, 'error' => 'Invalid URL']);
exit;
}
$stmt = db()->prepare("INSERT INTO channel_rss_feeds (channel_id, url) VALUES (?, ?)");
$stmt->execute([$channel_id, $url]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete') {
$feed_id = $_POST['feed_id'] ?? 0;
$stmt = db()->prepare("DELETE FROM channel_rss_feeds WHERE id = ? AND channel_id = ?");
$stmt->execute([$feed_id, $channel_id]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'sync') {
// Fetch feeds for this channel
$stmt = db()->prepare("SELECT * FROM channel_rss_feeds WHERE channel_id = ?");
$stmt->execute([$channel_id]);
$feeds = $stmt->fetchAll();
$new_items_count = 0;
foreach ($feeds as $feed) {
$rss_content = @file_get_contents($feed['url']);
if (!$rss_content) continue;
$xml = @simplexml_load_string($rss_content);
if (!$xml) continue;
$items = [];
if (isset($xml->channel->item)) { // RSS 2.0
$items = $xml->channel->item;
} elseif (isset($xml->entry)) { // Atom
$items = $xml->entry;
}
foreach ($items as $item) {
$guid = (string)($item->guid ?? ($item->id ?? $item->link));
$title = (string)$item->title;
$link = (string)($item->link['href'] ?? $item->link);
$description = strip_tags((string)($item->description ?? $item->summary));
// Check if already exists
$stmt_check = db()->prepare("SELECT id FROM messages WHERE channel_id = ? AND rss_guid = ?");
$stmt_check->execute([$channel_id, $guid]);
if ($stmt_check->fetch()) continue;
// Insert as message from a special "RSS Bot" user or system
// Let's find or create an RSS Bot user
$stmt_bot = db()->prepare("SELECT id FROM users WHERE username = 'RSS Bot' AND is_bot = 1");
$stmt_bot->execute();
$bot = $stmt_bot->fetch();
if (!$bot) {
$stmt_create_bot = db()->prepare("INSERT INTO users (username, is_bot, status) VALUES ('RSS Bot', 1, 'online')");
$stmt_create_bot->execute();
$bot_id = db()->lastInsertId();
} else {
$bot_id = $bot['id'];
}
$content = "**" . $title . "**\n" . $description . "\n" . $link;
// For announcements, we might want to use metadata for a rich embed
$metadata = json_encode([
'title' => $title,
'description' => mb_substr($description, 0, 200) . (mb_strlen($description) > 200 ? '...' : ''),
'url' => $link,
'site_name' => parse_url($feed['url'], PHP_URL_HOST)
]);
$stmt_msg = db()->prepare("INSERT INTO messages (channel_id, user_id, content, metadata, rss_guid) VALUES (?, ?, ?, ?, ?)");
$stmt_msg->execute([$channel_id, $bot_id, $content, $metadata, $guid]);
$new_items_count++;
}
$stmt_update_feed = db()->prepare("UPDATE channel_rss_feeds SET last_fetched_at = CURRENT_TIMESTAMP WHERE id = ?");
$stmt_update_feed->execute([$feed['id']]);
}
echo json_encode(['success' => true, 'new_items' => $new_items_count]);
exit;
}
} else {
// GET: List feeds
$channel_id = $_GET['channel_id'] ?? 0;
$stmt = db()->prepare("SELECT * FROM channel_rss_feeds WHERE channel_id = ? ORDER BY created_at DESC");
$stmt->execute([$channel_id]);
echo json_encode(['success' => true, 'feeds' => $stmt->fetchAll()]);
exit;
}

103
api_v1_rules.php Normal file
View File

@ -0,0 +1,103 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
$user_id = $_SESSION['user_id'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$channel_id = $_POST['channel_id'] ?? 0;
$content = $_POST['content'] ?? '';
// Check if user has permission to manage channels
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$chan = $stmt->fetch();
if (!$chan || !Permissions::hasPermission($user_id, $chan['server_id'], Permissions::MANAGE_CHANNELS)) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
try {
// Get max position
$stmt = db()->prepare("SELECT MAX(position) FROM channel_rules WHERE channel_id = ?");
$stmt->execute([$channel_id]);
$pos = (int)$stmt->fetchColumn() + 1;
$stmt = db()->prepare("INSERT INTO channel_rules (channel_id, content, position) VALUES (?, ?, ?)");
$stmt->execute([$channel_id, $content, $pos]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
$id = $_GET['id'] ?? 0;
$stmt = db()->prepare("SELECT c.server_id FROM channels c JOIN channel_rules r ON c.id = r.channel_id WHERE r.id = ?");
$stmt->execute([$id]);
$res = $stmt->fetch();
if ($res && Permissions::hasPermission($user_id, $res['server_id'], Permissions::MANAGE_CHANNELS)) {
$stmt = db()->prepare("DELETE FROM channel_rules WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'PATCH') {
$data = json_decode(file_get_contents('php://input'), true);
$id = $data['id'] ?? 0;
$dir = $data['dir'] ?? 'up';
$stmt = db()->prepare("SELECT channel_id, position FROM channel_rules WHERE id = ?");
$stmt->execute([$id]);
$current = $stmt->fetch();
if ($current) {
$channel_id = $current['channel_id'];
$pos = $current['position'];
if ($dir === 'up') {
$stmt = db()->prepare("SELECT id, position FROM channel_rules WHERE channel_id = ? AND position < ? ORDER BY position DESC LIMIT 1");
} else {
$stmt = db()->prepare("SELECT id, position FROM channel_rules WHERE channel_id = ? AND position > ? ORDER BY position ASC LIMIT 1");
}
$stmt->execute([$channel_id, $pos]);
$other = $stmt->fetch();
if ($other) {
db()->prepare("UPDATE channel_rules SET position = ? WHERE id = ?")->execute([$other['position'], $id]);
db()->prepare("UPDATE channel_rules SET position = ? WHERE id = ?")->execute([$pos, $other['id']]);
}
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Rule not found']);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$data = json_decode(file_get_contents('php://input'), true);
$id = $data['id'] ?? 0;
$content = $data['content'] ?? '';
$stmt = db()->prepare("SELECT c.server_id FROM channels c JOIN channel_rules r ON c.id = r.channel_id WHERE r.id = ?");
$stmt->execute([$id]);
$res = $stmt->fetch();
if ($res && Permissions::hasPermission($user_id, $res['server_id'], Permissions::MANAGE_CHANNELS)) {
$stmt = db()->prepare("UPDATE channel_rules SET content = ? WHERE id = ?");
$stmt->execute([$content, $id]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
}
exit;
}

54
api_v1_tags.php Normal file
View File

@ -0,0 +1,54 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
$user_id = $_SESSION['user_id'];
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$channel_id = $_GET['channel_id'] ?? 0;
if (!$channel_id) {
echo json_encode(['success' => false, 'error' => 'Missing channel_id']);
exit;
}
$stmt = db()->prepare("SELECT * FROM forum_tags WHERE channel_id = ?");
$stmt->execute([$channel_id]);
echo json_encode(['success' => true, 'tags' => $stmt->fetchAll()]);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) $data = $_POST;
$action = $data['action'] ?? 'create';
$channel_id = $data['channel_id'] ?? 0;
// Check permissions
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$chan = $stmt->fetch();
if (!$chan || !Permissions::hasPermission($user_id, $chan['server_id'], Permissions::MANAGE_CHANNELS)) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($action === 'create') {
$name = $data['name'] ?? '';
$color = $data['color'] ?? '#7289da';
if (!$name) {
echo json_encode(['success' => false, 'error' => 'Missing name']);
exit;
}
$stmt = db()->prepare("INSERT INTO forum_tags (channel_id, name, color) VALUES (?, ?, ?)");
$stmt->execute([$channel_id, $name, $color]);
echo json_encode(['success' => true, 'tag_id' => db()->lastInsertId()]);
} elseif ($action === 'delete') {
$tag_id = $data['tag_id'] ?? 0;
$stmt = db()->prepare("DELETE FROM forum_tags WHERE id = ? AND channel_id = ?");
$stmt->execute([$tag_id, $channel_id]);
echo json_encode(['success' => true]);
}
exit;
}

80
api_v1_threads.php Normal file
View File

@ -0,0 +1,80 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$channel_id = $_POST['channel_id'] ?? 0;
$title = $_POST['title'] ?? '';
$user_id = $_SESSION['user_id'];
if (!$channel_id || !$title) {
echo json_encode(['success' => false, 'error' => 'Missing data']);
exit;
}
$tag_ids = $_POST['tag_ids'] ?? [];
if (is_string($tag_ids)) {
$tag_ids = array_filter(explode(',', $tag_ids));
}
try {
db()->beginTransaction();
$stmt = db()->prepare("INSERT INTO forum_threads (channel_id, user_id, title) VALUES (?, ?, ?)");
$stmt->execute([$channel_id, $user_id, $title]);
$thread_id = db()->lastInsertId();
if (!empty($tag_ids)) {
$stmtTag = db()->prepare("INSERT INTO thread_tags (thread_id, tag_id) VALUES (?, ?)");
foreach ($tag_ids as $tag_id) {
if ($tag_id) $stmtTag->execute([$thread_id, $tag_id]);
}
}
db()->commit();
echo json_encode(['success' => true, 'thread_id' => $thread_id]);
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'PATCH' || (isset($_GET['action']) && $_GET['action'] === 'solve')) {
$data = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$thread_id = $data['thread_id'] ?? 0;
$message_id = $data['message_id'] ?? null; // null to unsolve
$user_id = $_SESSION['user_id'];
if (!$thread_id) {
echo json_encode(['success' => false, 'error' => 'Missing thread_id']);
exit;
}
// Verify permission (thread owner or admin)
$stmt = db()->prepare("SELECT t.*, c.server_id FROM forum_threads t JOIN channels c ON t.channel_id = c.id WHERE t.id = ?");
$stmt->execute([$thread_id]);
$thread = $stmt->fetch();
if (!$thread) {
echo json_encode(['success' => false, 'error' => 'Thread not found']);
exit;
}
$stmtServer = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmtServer->execute([$thread['server_id']]);
$server = $stmtServer->fetch();
if ($thread['user_id'] != $user_id && $server['owner_id'] != $user_id) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
try {
$stmt = db()->prepare("UPDATE forum_threads SET solution_message_id = ? WHERE id = ?");
$stmt->execute([$message_id, $thread_id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}

View File

@ -12,11 +12,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? $user['username'];
$avatar_url = $_POST['avatar_url'] ?? $user['avatar_url'];
$dnd_mode = isset($_POST['dnd_mode']) ? (int)$_POST['dnd_mode'] : (int)($user['dnd_mode'] ?? 0);
$sound_notifications = isset($_POST['sound_notifications']) ? (int)$_POST['sound_notifications'] : (int)($user['sound_notifications'] ?? 0);
$theme = $_POST['theme'] ?? $user['theme'] ?? 'dark';
try {
$stmt = db()->prepare("UPDATE users SET username = ?, avatar_url = ?, dnd_mode = ?, theme = ? WHERE id = ?");
$stmt->execute([$username, $avatar_url, $dnd_mode, $theme, $user['id']]);
$stmt = db()->prepare("UPDATE users SET username = ?, avatar_url = ?, dnd_mode = ?, sound_notifications = ?, theme = ? WHERE id = ?");
$stmt->execute([$username, $avatar_url, $dnd_mode, $sound_notifications, $theme, $user['id']]);
$_SESSION['username'] = $username; // Update session if stored (though getCurrentUser fetches from DB)

View File

@ -785,3 +785,77 @@ body {
.action-btn.pin.active {
color: var(--blurple);
}
/* Announcement Style */
.announcement-style {
background-color: rgba(88, 101, 242, 0.05);
border: 1px solid rgba(88, 101, 242, 0.1);
border-radius: 8px;
margin-bottom: 8px;
padding: 12px !important;
}
.announcement-style .message-text {
font-size: 1.1em;
font-weight: 500;
}
/* Rules Style */
.rule-item {
transition: transform 0.2s;
cursor: default;
}
.rule-item:hover {
transform: translateX(5px);
}
/* Forum Style */
.thread-item {
transition: background-color 0.2s;
}
.thread-item:hover {
background-color: #35373c !important;
}
/* YouTube Embed */
.youtube-embed {
overflow: hidden;
position: relative;
max-width: 560px;
}
.youtube-embed iframe {
width: 100%;
aspect-ratio: 16 / 9;
}
/* Solution Style */
.message-item.is-solution {
background-color: rgba(35, 165, 89, 0.05);
border-left: 2px solid #23a559;
}
.action-btn.mark-solution.active {
color: #23a559;
}
/* Forum Filters */
.forum-filters .btn {
border-radius: 20px;
margin-right: 5px;
border: none;
background-color: var(--bg-servers);
color: var(--text-muted);
font-size: 0.8em;
padding: 4px 12px;
}
.forum-filters .btn:hover {
background-color: var(--hover);
color: var(--text-primary);
}
.forum-filters .btn.active {
background-color: var(--active);
color: var(--text-primary);
}

View File

@ -6,12 +6,13 @@ document.addEventListener('DOMContentLoaded', () => {
const typingIndicator = document.getElementById('typing-indicator');
// Emoji list for reactions
const EMOJIS = ['👍', '❤️', '😂', '😮', '😢', '🔥', '✅', '🚀'];
const EMOJIS = ['👍', '❤️', '😂', '😮', '😢', '🔥', '✅', '🚀', '❓', '💡', '📌', '💯'];
// Scroll to bottom
messagesList.scrollTop = messagesList.scrollHeight;
const currentChannel = new URLSearchParams(window.location.search).get('channel_id') || 1;
const currentThread = new URLSearchParams(window.location.search).get('thread_id');
let typingTimeout;
// Notification Permission
@ -52,7 +53,18 @@ 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();
}
});
}
}
}
@ -114,6 +126,9 @@ document.addEventListener('DOMContentLoaded', () => {
const formData = new FormData();
formData.append('content', content);
formData.append('channel_id', currentChannel);
if (currentThread) {
formData.append('thread_id', currentThread);
}
const progressContainer = document.getElementById('upload-progress-container');
const progressBar = document.getElementById('upload-progress-bar');
@ -383,11 +398,12 @@ document.addEventListener('DOMContentLoaded', () => {
const div = document.createElement('div');
div.className = 'message-item p-2 border-bottom border-secondary';
div.style.backgroundColor = 'transparent';
const authorStyle = msg.role_color ? `color: ${msg.role_color};` : '';
div.innerHTML = `
<div class="d-flex align-items-start">
<div class="message-avatar" style="width: 32px; height: 32px; margin-right: 10px; ${msg.avatar_url ? `background-image: url('${msg.avatar_url}');` : ''}"></div>
<div style="flex: 1;">
<div class="message-author" style="font-size: 0.85em;">
<div class="message-author" style="font-size: 0.85em; ${authorStyle}">
${escapeHTML(msg.username)}
<span class="message-time">${msg.time}</span>
</div>
@ -589,15 +605,145 @@ document.addEventListener('DOMContentLoaded', () => {
channelSettingsBtns.forEach(btn => {
btn.addEventListener('click', () => {
const modal = document.getElementById('editChannelModal');
modal.querySelector('#edit-channel-id').value = btn.dataset.id;
const channelId = btn.dataset.id;
const channelType = btn.dataset.type || 'chat';
modal.querySelector('#edit-channel-id').value = channelId;
modal.querySelector('#edit-channel-name').value = btn.dataset.name;
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-theme').value = btn.dataset.theme || '#5865f2';
modal.querySelector('#delete-channel-id').value = btn.dataset.id;
modal.querySelector('#delete-channel-id').value = channelId;
// Show/Hide RSS tab
const rssTabNav = document.getElementById('rss-tab-nav');
const statusContainer = document.getElementById('edit-channel-status-container');
if (channelType === 'announcement') {
rssTabNav.style.display = 'block';
} 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();
bootstrap.Tab.getOrCreateInstance(modal.querySelector('[data-bs-target="#edit-channel-general"]')).show();
}
}
if (channelType === 'voice') {
statusContainer.style.display = 'block';
} else {
statusContainer.style.display = 'none';
}
});
});
// RSS Management
const editChannelType = document.getElementById('edit-channel-type');
editChannelType?.addEventListener('change', () => {
const type = editChannelType.value;
const rssTabNav = document.getElementById('rss-tab-nav');
const statusContainer = document.getElementById('edit-channel-status-container');
rssTabNav.style.display = (type === 'announcement') ? 'block' : 'none';
statusContainer.style.display = (type === 'voice') ? 'block' : 'none';
});
// RSS Management
const rssTabBtn = document.getElementById('rss-tab-btn');
const rssFeedsList = document.getElementById('rss-feeds-list');
const addRssBtn = document.getElementById('add-rss-btn');
const syncRssBtn = document.getElementById('sync-rss-btn');
rssTabBtn?.addEventListener('click', loadRssFeeds);
async function loadRssFeeds() {
const channelId = document.getElementById('edit-channel-id').value;
rssFeedsList.innerHTML = '<div class="text-center p-3 text-muted small">Loading feeds...</div>';
try {
const resp = await fetch(`api_v1_rss.php?channel_id=${channelId}`);
const data = await resp.json();
if (data.success) {
renderRssFeeds(data.feeds);
}
} catch (e) { console.error(e); }
}
function renderRssFeeds(feeds) {
rssFeedsList.innerHTML = '';
if (feeds.length === 0) {
rssFeedsList.innerHTML = '<div class="text-center p-3 text-muted small">No RSS feeds configured.</div>';
return;
}
feeds.forEach(feed => {
const item = document.createElement('div');
item.className = 'list-group-item bg-transparent text-white border-secondary p-2 mb-1';
item.innerHTML = `
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small text-truncate" style="max-width: 80%;">${feed.url}</span>
<button class="btn btn-sm text-danger delete-rss-btn" data-id="${feed.id}">×</button>
</div>
<div class="small text-muted" style="font-size: 0.7em;">Last fetched: ${feed.last_fetched_at || 'Never'}</div>
`;
rssFeedsList.appendChild(item);
});
}
addRssBtn?.addEventListener('click', async () => {
const channelId = document.getElementById('edit-channel-id').value;
const url = document.getElementById('new-rss-url').value.trim();
if (!url) return;
const formData = new FormData();
formData.append('action', 'add');
formData.append('channel_id', channelId);
formData.append('url', url);
const resp = await fetch('api_v1_rss.php', { method: 'POST', body: formData });
if ((await resp.json()).success) {
document.getElementById('new-rss-url').value = '';
loadRssFeeds();
}
});
syncRssBtn?.addEventListener('click', async () => {
const channelId = document.getElementById('edit-channel-id').value;
syncRssBtn.disabled = true;
syncRssBtn.textContent = 'Syncing...';
const formData = new FormData();
formData.append('action', 'sync');
formData.append('channel_id', channelId);
try {
const resp = await fetch('api_v1_rss.php', { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
alert(`Sync complete! Found ${result.new_items} new items.`);
loadRssFeeds();
}
} catch (e) { console.error(e); }
syncRssBtn.disabled = false;
syncRssBtn.textContent = 'Sync Now';
});
rssFeedsList?.addEventListener('click', async (e) => {
if (e.target.classList.contains('delete-rss-btn')) {
const channelId = document.getElementById('edit-channel-id').value;
const feedId = e.target.dataset.id;
const formData = new FormData();
formData.append('action', 'delete');
formData.append('channel_id', channelId);
formData.append('feed_id', feedId);
await fetch('api_v1_rss.php', { method: 'POST', body: formData });
loadRssFeeds();
}
});
// Clear Channel History
const clearHistoryBtn = document.getElementById('clear-channel-history-btn');
clearHistoryBtn?.addEventListener('click', async () => {
@ -624,33 +770,47 @@ document.addEventListener('DOMContentLoaded', () => {
const rolesTabBtn = document.getElementById('roles-tab-btn');
const rolesList = document.getElementById('roles-list');
const addRoleBtn = document.getElementById('add-role-btn');
const membersTabBtn = document.getElementById('members-tab-btn');
const membersList = document.getElementById('server-members-list');
const activeServerId = new URLSearchParams(window.location.search).get('server_id') || 1;
let serverRoles = [];
let serverPermissions = [];
rolesTabBtn?.addEventListener('click', loadRoles);
membersTabBtn?.addEventListener('click', loadRoles); // Both tabs need roles data
async function loadRoles() {
rolesList.innerHTML = '<div class="text-center p-3 text-muted">Loading roles...</div>';
if (rolesList) rolesList.innerHTML = '<div class="text-center p-3 text-muted">Loading...</div>';
if (membersList) membersList.innerHTML = '<div class="text-center p-3 text-muted">Loading...</div>';
try {
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`);
const data = await resp.json();
if (data.success) {
renderRoles(data.roles);
serverRoles = data.roles;
serverPermissions = data.permissions_list;
if (rolesList) renderRoles(data.roles);
if (membersList) renderMembers(data.members);
}
} catch (e) { console.error(e); }
}
function renderRoles(roles) {
rolesList.innerHTML = '';
if (roles.length === 0) {
rolesList.innerHTML = '<div class="text-center p-3 text-muted">No roles created yet.</div>';
}
roles.forEach(role => {
const item = document.createElement('div');
item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2';
item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-1 rounded';
item.innerHTML = `
<div class="d-flex align-items-center">
<div style="width: 12px; height: 12px; border-radius: 50%; background-color: ${role.color}; margin-right: 10px;"></div>
<span>${role.name}</span>
<div style="width: 14px; height: 14px; border-radius: 50%; background-color: ${role.color}; margin-right: 12px; box-shadow: 0 0 5px ${role.color}88;"></div>
<span class="fw-medium">${role.name}</span>
</div>
<div>
<button class="btn btn-sm btn-outline-light edit-role-btn" data-id="${role.id}">Edit</button>
<button class="btn btn-sm btn-outline-light edit-role-btn-v2" data-id="${role.id}" data-name="${role.name}" data-color="${role.color}" data-perms="${role.permissions}">Edit</button>
<button class="btn btn-sm btn-outline-danger delete-role-btn" data-id="${role.id}">×</button>
</div>
`;
@ -658,16 +818,123 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
addRoleBtn?.addEventListener('click', async () => {
const name = prompt('Role name:');
if (!name) return;
const color = prompt('Role color (hex):', '#99aab5');
function renderMembers(members) {
membersList.innerHTML = '';
members.forEach(member => {
const memberRoles = member.role_ids ? member.role_ids.split(',') : [];
const item = document.createElement('div');
item.className = 'list-group-item bg-transparent text-white border-secondary d-flex justify-content-between align-items-center p-2 mb-2 rounded bg-dark';
let rolesHtml = '';
serverRoles.forEach(role => {
const isAssigned = memberRoles.includes(role.id.toString());
rolesHtml += `
<div class="form-check form-check-inline">
<input class="form-check-input role-assign-check" type="checkbox"
data-user-id="${member.id}" data-role-id="${role.id}"
${isAssigned ? 'checked' : ''}>
<label class="form-check-label small" style="color: ${role.color}">${role.name}</label>
</div>
`;
});
item.innerHTML = `
<div class="d-flex align-items-center flex-grow-1">
<div class="message-avatar me-2" style="width: 32px; height: 32px; ${member.avatar_url ? `background-image: url('${member.avatar_url}');` : ''}"></div>
<div class="flex-grow-1">
<div class="fw-bold small" style="color: ${member.role_color || 'inherit'}">${member.username}</div>
<div class="member-roles-assign-list">
${rolesHtml || '<span class="text-muted small">No roles available</span>'}
</div>
</div>
</div>
`;
membersList.appendChild(item);
});
}
// Role Editing Modal Logic
rolesList?.addEventListener('click', (e) => {
if (e.target.classList.contains('edit-role-btn-v2')) {
const role = e.target.dataset;
document.getElementById('edit-role-id').value = role.id;
document.getElementById('edit-role-name').value = role.name;
document.getElementById('edit-role-color').value = role.color;
const permsContainer = document.getElementById('role-permissions-checkboxes');
permsContainer.innerHTML = '';
const currentPerms = parseInt(role.perms);
serverPermissions.forEach(p => {
const isChecked = (currentPerms & p.value) === p.value;
permsContainer.innerHTML += `
<div class="form-check mb-1">
<input class="form-check-input perm-check" type="checkbox" value="${p.value}" id="perm-${p.value}" ${isChecked ? 'checked' : ''}>
<label class="form-check-label text-white small" for="perm-${p.value}">${p.name}</label>
</div>
`;
});
const modal = new bootstrap.Modal(document.getElementById('roleEditorModal'));
modal.show();
}
});
document.getElementById('save-role-btn')?.addEventListener('click', async () => {
const id = document.getElementById('edit-role-id').value;
const name = document.getElementById('edit-role-name').value;
const color = document.getElementById('edit-role-color').value;
let permissions = 0;
document.querySelectorAll('.perm-check:checked').forEach(cb => {
permissions |= parseInt(cb.value);
});
try {
const resp = await fetch('api_v1_roles.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'create', server_id: activeServerId, name, color })
body: JSON.stringify({ action: 'update', server_id: activeServerId, id, name, color, permissions })
});
const data = await resp.json();
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('roleEditorModal')).hide();
loadRoles();
}
} catch (e) { console.error(e); }
});
membersList?.addEventListener('change', async (e) => {
if (e.target.classList.contains('role-assign-check')) {
const userId = e.target.dataset.userId;
const roleId = e.target.dataset.roleId;
const action = e.target.checked ? 'assign' : 'unassign';
try {
const resp = await fetch('api_v1_roles.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, server_id: activeServerId, user_id: userId, role_id: roleId })
});
const data = await resp.json();
if (!data.success) {
alert(data.error || 'Failed to update role');
e.target.checked = !e.target.checked;
}
} catch (e) { console.error(e); }
}
});
addRoleBtn?.addEventListener('click', async () => {
const name = prompt('Role name:');
if (!name) return;
const color = '#99aab5';
try {
const resp = await fetch('api_v1_roles.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'create', server_id: activeServerId, name, color, permissions: 0 })
});
if ((await resp.json()).success) loadRoles();
} catch (e) { console.error(e); }
@ -678,23 +945,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (!confirm('Delete this role?')) return;
const roleId = e.target.dataset.id;
const resp = await fetch('api_v1_roles.php', {
method: 'DELETE',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: roleId })
});
if ((await resp.json()).success) loadRoles();
}
if (e.target.classList.contains('edit-role-btn')) {
const roleId = e.target.dataset.id;
const name = prompt('New name:');
const color = prompt('New color (hex):');
if (!name || !color) return;
const resp = await fetch('api_v1_roles.php', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: roleId, name, color, permissions: 0 })
body: JSON.stringify({ action: 'delete', server_id: activeServerId, id: roleId })
});
if ((await resp.json()).success) loadRoles();
}
@ -854,6 +1107,216 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// Forum: New Thread
const newThreadBtn = document.getElementById('new-thread-btn');
const newThreadModal = document.getElementById('newThreadModal') ? new bootstrap.Modal(document.getElementById('newThreadModal')) : null;
let selectedTagIds = [];
newThreadBtn?.addEventListener('click', async () => {
if (!newThreadModal) return;
// Load tags for this channel
const tagsList = document.getElementById('new-thread-tags-list');
tagsList.innerHTML = '<div class="text-muted small">Loading tags...</div>';
selectedTagIds = [];
try {
const resp = await fetch(`api_v1_tags.php?channel_id=${currentChannel}`);
const data = await resp.json();
tagsList.innerHTML = '';
if (data.success && data.tags.length > 0) {
data.tags.forEach(tag => {
const span = document.createElement('span');
span.className = 'badge rounded-pill p-2 border border-secondary';
span.style.cursor = 'pointer';
span.style.backgroundColor = 'transparent';
span.dataset.id = tag.id;
span.dataset.color = tag.color;
span.textContent = tag.name;
span.onclick = () => {
if (selectedTagIds.includes(tag.id)) {
selectedTagIds = selectedTagIds.filter(id => id !== tag.id);
span.style.backgroundColor = 'transparent';
} else {
selectedTagIds.push(tag.id);
span.style.backgroundColor = tag.color;
}
};
tagsList.appendChild(span);
});
} else {
tagsList.innerHTML = '<div class="text-muted small">No tags available.</div>';
}
} catch (e) { console.error(e); }
newThreadModal.show();
});
document.getElementById('submit-new-thread-btn')?.addEventListener('click', async () => {
const title = document.getElementById('new-thread-title').value.trim();
if (!title) return;
try {
const formData = new FormData();
formData.append('channel_id', currentChannel);
formData.append('title', title);
formData.append('tag_ids', selectedTagIds.join(','));
const resp = await fetch('api_v1_threads.php', { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
window.location.href = `?server_id=${activeServerId}&channel_id=${currentChannel}&thread_id=${result.thread_id}`;
} else {
alert(result.error || 'Failed to create thread');
}
} catch (e) { console.error(e); }
});
// Forum: Mark as Solution
document.addEventListener('click', async (e) => {
const solBtn = e.target.closest('.action-btn.mark-solution');
if (solBtn) {
const threadId = solBtn.dataset.threadId;
const messageId = solBtn.classList.contains('active') ? null : solBtn.dataset.messageId;
try {
const resp = await fetch('api_v1_threads.php?action=solve', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ thread_id: threadId, message_id: messageId })
});
const result = await resp.json();
if (result.success) {
location.reload();
} else {
alert(result.error || 'Failed to update solution');
}
} catch (e) { console.error(e); }
}
});
// Forum: Manage Tags
const manageTagsBtn = document.getElementById('manage-tags-btn');
const manageTagsModal = document.getElementById('manageTagsModal') ? new bootstrap.Modal(document.getElementById('manageTagsModal')) : null;
manageTagsBtn?.addEventListener('click', async () => {
if (!manageTagsModal) return;
loadForumAdminTags();
manageTagsModal.show();
});
async function loadForumAdminTags() {
const list = document.getElementById('forum-tags-admin-list');
list.innerHTML = '<div class="text-center p-3 text-muted small">Loading tags...</div>';
try {
const resp = await fetch(`api_v1_tags.php?channel_id=${currentChannel}`);
const data = await resp.json();
list.innerHTML = '';
if (data.success && data.tags.length > 0) {
data.tags.forEach(tag => {
const div = document.createElement('div');
div.className = 'd-flex justify-content-between align-items-center mb-2 p-2 bg-dark rounded';
div.innerHTML = `
<div class="d-flex align-items-center">
<div style="width: 12px; height: 12px; border-radius: 50%; background-color: ${tag.color}; margin-right: 8px;"></div>
<span>${tag.name}</span>
</div>
<button class="btn btn-sm text-danger delete-forum-tag-btn" data-id="${tag.id}">×</button>
`;
list.appendChild(div);
});
} else {
list.innerHTML = '<div class="text-center p-3 text-muted small">No tags created yet.</div>';
}
} catch (e) { console.error(e); }
}
document.getElementById('add-forum-tag-btn')?.addEventListener('click', async () => {
const name = document.getElementById('new-tag-name').value.trim();
const color = document.getElementById('new-tag-color').value;
if (!name) return;
try {
const resp = await fetch('api_v1_tags.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'create', channel_id: currentChannel, name, color })
});
if ((await resp.json()).success) {
document.getElementById('new-tag-name').value = '';
loadForumAdminTags();
}
} catch (e) { console.error(e); }
});
document.getElementById('forum-tags-admin-list')?.addEventListener('click', async (e) => {
if (e.target.classList.contains('delete-forum-tag-btn')) {
const tagId = e.target.dataset.id;
try {
const resp = await fetch('api_v1_tags.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'delete', channel_id: currentChannel, tag_id: tagId })
});
if ((await resp.json()).success) loadForumAdminTags();
} catch (e) { console.error(e); }
}
});
// Rules: Add Rule
const addRuleBtn = document.getElementById('add-rule-btn');
addRuleBtn?.addEventListener('click', async () => {
const content = prompt('Rule Content:');
if (!content) return;
try {
const formData = new FormData();
formData.append('channel_id', currentChannel);
formData.append('content', content);
const resp = await fetch('api_v1_rules.php', { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
location.reload();
} else {
alert(result.error || 'Failed to add rule');
}
} catch (e) { console.error(e); }
});
// Rules: Delete/Edit
document.addEventListener('click', async (e) => {
if (e.target.classList.contains('delete-rule-btn')) {
if (!confirm('Delete this rule?')) return;
const id = e.target.dataset.id;
const resp = await fetch(`api_v1_rules.php?id=${id}`, { method: 'DELETE' });
if ((await resp.json()).success) location.reload();
}
if (e.target.classList.contains('edit-rule-btn')) {
const id = e.target.dataset.id;
const oldContent = e.target.closest('.rule-item').querySelector('.rule-content').innerText;
const newContent = prompt('Edit Rule:', oldContent);
if (!newContent || newContent === oldContent) return;
const resp = await fetch('api_v1_rules.php', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, content: newContent })
});
if ((await resp.json()).success) location.reload();
}
});
// Channel Selection Type
const addChannelBtns = document.querySelectorAll('.add-channel-btn');
addChannelBtns.forEach(btn => {
btn.addEventListener('click', () => {
const type = btn.dataset.type;
const select = document.getElementById('channel-type-select');
if (select) {
select.value = type === 'voice' ? 'voice' : 'chat';
}
});
});
// User Settings - Avatar Search
const avatarSearchBtn = document.getElementById('search-avatar-btn');
const avatarSearchQuery = document.getElementById('avatar-search-query');
@ -946,8 +1409,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
const isMe = msg.user_id == window.currentUserId || msg.username == window.currentUsername;
// Check if user is server owner (could be passed in window object)
const isOwner = window.isServerOwner || false;
const hasManageRights = window.canManageChannels || window.isServerOwner || false;
const pinHtml = `
<span class="action-btn pin ${msg.is_pinned ? 'active' : ''}" title="${msg.is_pinned ? 'Unpin' : 'Pin'}" data-id="${msg.id}" data-pinned="${msg.is_pinned ? '1' : '0'}">
@ -955,7 +1417,7 @@ document.addEventListener('DOMContentLoaded', () => {
</span>
`;
const actionsHtml = (isMe || isOwner) ? `
const actionsHtml = (isMe || hasManageRights) ? `
<div class="message-actions-menu">
${pinHtml}
${isMe ? `
@ -982,10 +1444,19 @@ document.addEventListener('DOMContentLoaded', () => {
}
if (msg.is_pinned) div.classList.add('pinned');
const ytRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
const ytMatch = msg.content.match(ytRegex);
let ytHtml = '';
if (ytMatch && ytMatch[1]) {
ytHtml = `<div class="youtube-embed mt-2"><iframe width="100%" height="315" src="https://www.youtube.com/embed/${ytMatch[1]}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="border-radius: 8px; max-width: 560px;"></iframe></div>`;
}
const authorStyle = msg.role_color ? `color: ${msg.role_color};` : '';
div.innerHTML = `
<div class="message-avatar" style="${avatarStyle}"></div>
<div class="message-content">
<div class="message-author">
<div class="message-author" style="${authorStyle}">
${escapeHTML(msg.username)}
<span class="message-time">${msg.time}</span>
${pinnedBadge}
@ -994,7 +1465,8 @@ document.addEventListener('DOMContentLoaded', () => {
<div class="message-text">
${escapeHTML(msg.content).replace(/\n/g, '<br>').replace(mentionRegex, `<span class="mention">@${window.currentUsername}</span>`)}
${attachmentHtml}
${embedHtml}
${ytHtml}
${ytHtml ? '' : embedHtml}
</div>
<div class="message-reactions mt-1" data-message-id="${msg.id}">
<span class="add-reaction-btn" title="Add Reaction">+</span>

View File

@ -0,0 +1,2 @@
-- Migration: Add status to channels for voice channels
ALTER TABLE channels ADD COLUMN status VARCHAR(255) DEFAULT NULL;

View File

@ -0,0 +1,3 @@
-- Add solution support to forum threads
ALTER TABLE forum_threads ADD COLUMN solution_message_id INT NULL DEFAULT NULL;
ALTER TABLE forum_threads ADD FOREIGN KEY (solution_message_id) REFERENCES messages(id) ON DELETE SET NULL;

View File

@ -0,0 +1,16 @@
-- Add forum tags support
CREATE TABLE IF NOT EXISTS forum_tags (
id INT AUTO_INCREMENT PRIMARY KEY,
channel_id INT NOT NULL,
name VARCHAR(50) NOT NULL,
color VARCHAR(20) DEFAULT '#7289da',
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS thread_tags (
thread_id INT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (thread_id, tag_id),
FOREIGN KEY (thread_id) REFERENCES forum_threads(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES forum_tags(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,12 @@
-- Migration: Add RSS support for announcement channels
CREATE TABLE IF NOT EXISTS channel_rss_feeds (
id INT AUTO_INCREMENT PRIMARY KEY,
channel_id INT NOT NULL,
url VARCHAR(255) NOT NULL,
last_fetched_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
);
ALTER TABLE messages ADD COLUMN IF NOT EXISTS rss_guid VARCHAR(255) NULL;
CREATE INDEX IF NOT EXISTS idx_rss_guid ON messages(rss_guid);

650
index.php
View File

@ -67,7 +67,7 @@ if ($is_dm_view) {
$stmt->execute([$active_server_id]);
$channels = $stmt->fetchAll();
$active_channel_id = $_GET['channel_id'] ?? ($channels[0]['id'] ?? 1);
// Fetch active channel details for theme
$active_channel = null;
foreach($channels as $c) {
@ -76,32 +76,99 @@ if ($is_dm_view) {
break;
}
}
require_once 'includes/permissions.php';
$is_owner = false;
$can_manage_channels = false;
$can_manage_server = false;
foreach($servers as $s) {
if($s['id'] == $active_server_id) {
$is_owner = ($s['owner_id'] == $current_user_id);
$can_manage_channels = Permissions::hasPermission($current_user_id, $active_server_id, Permissions::MANAGE_CHANNELS) || $is_owner;
$can_manage_server = Permissions::hasPermission($current_user_id, $active_server_id, Permissions::MANAGE_SERVER) || Permissions::hasPermission($current_user_id, $active_server_id, Permissions::ADMINISTRATOR) || $is_owner;
break;
}
}
$channel_theme = $active_channel['theme_color'] ?? null;
// Fetch messages
$display_limit = !empty($active_channel['message_limit']) ? (int)$active_channel['message_limit'] : 50;
$stmt = db()->prepare("
SELECT m.*, u.username, u.avatar_url
FROM messages m
JOIN users u ON m.user_id = u.id
WHERE m.channel_id = ?
ORDER BY m.created_at ASC
LIMIT " . $display_limit . "
");
$stmt->execute([$active_channel_id]);
$messages = $stmt->fetchAll();
$channel_type = $active_channel['type'] ?? 'chat';
$active_thread_id = $_GET['thread_id'] ?? null;
$active_thread = null;
if ($active_thread_id) {
$stmt = db()->prepare("SELECT t.*, u.username FROM forum_threads t JOIN users u ON t.user_id = u.id WHERE t.id = ?");
$stmt->execute([$active_thread_id]);
$active_thread = $stmt->fetch();
if ($active_thread) {
$stmt = db()->prepare("
SELECT m.*, u.username, u.avatar_url,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color
FROM messages m
JOIN users u ON m.user_id = u.id
WHERE m.thread_id = ?
ORDER BY m.created_at ASC
");
$stmt->execute([$active_server_id, $active_thread_id]);
$messages = $stmt->fetchAll();
}
}
if (!$active_thread && $channel_type === 'rules') {
$stmt = db()->prepare("SELECT * FROM channel_rules WHERE channel_id = ? ORDER BY position ASC");
$stmt->execute([$active_channel_id]);
$rules = $stmt->fetchAll();
} elseif ($channel_type === 'forum') {
$filter_status = $_GET['status'] ?? 'all';
$status_where = "";
if ($filter_status === 'resolved') {
$status_where = " AND t.solution_message_id IS NOT NULL";
} elseif ($filter_status === 'unresolved') {
$status_where = " AND t.solution_message_id IS NULL";
}
$stmt = db()->prepare("
SELECT t.*, u.username, u.avatar_url,
(SELECT COUNT(*) FROM messages m WHERE m.thread_id = t.id) as message_count,
(SELECT MAX(created_at) FROM messages m WHERE m.thread_id = t.id) as last_activity,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color,
(SELECT GROUP_CONCAT(CONCAT(ft.name, ':', ft.color) SEPARATOR '|') FROM thread_tags tt JOIN forum_tags ft ON tt.tag_id = ft.id WHERE tt.thread_id = t.id) as tags
FROM forum_threads t
JOIN users u ON t.user_id = u.id
WHERE t.channel_id = ? " . $status_where . "
ORDER BY last_activity DESC, t.created_at DESC
");
$stmt->execute([$active_server_id, $active_channel_id]);
$threads = $stmt->fetchAll();
} else {
// Fetch messages
$display_limit = !empty($active_channel['message_limit']) ? (int)$active_channel['message_limit'] : 50;
$stmt = db()->prepare("
SELECT m.*, u.username, u.avatar_url,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color
FROM messages m
JOIN users u ON m.user_id = u.id
WHERE m.channel_id = ?
ORDER BY m.created_at ASC
LIMIT " . $display_limit . "
");
$stmt->execute([$active_server_id, $active_channel_id]);
$messages = $stmt->fetchAll();
}
$current_channel_name = 'general';
foreach($channels as $c) if($c['id'] == $active_channel_id) $current_channel_name = $c['name'];
// Fetch members
$stmt = db()->prepare("
SELECT u.id, u.username, u.avatar_url, u.status
SELECT u.id, u.username, u.avatar_url, u.status,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color
FROM users u
JOIN server_members sm ON u.id = sm.user_id
WHERE sm.server_id = ?
");
$stmt->execute([$active_server_id]);
$stmt->execute([$active_server_id, $active_server_id]);
$members = $stmt->fetchAll();
}
@ -129,10 +196,14 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<link rel="stylesheet" href="assets/css/discord.css?v=<?php echo time(); ?>">
<script>
window.currentUserId = <?php echo $current_user_id; ?>;
window.activeServerId = "<?php echo $active_server_id; ?>";
window.currentUsername = "<?php echo addslashes($user['username']); ?>";
window.isServerOwner = <?php echo ($is_owner ?? false) ? 'true' : 'false'; ?>;
window.canManageServer = <?php echo ($can_manage_server ?? false) ? 'true' : 'false'; ?>;
window.canManageChannels = <?php echo ($can_manage_channels ?? false) ? 'true' : 'false'; ?>;
window.currentChannelName = "<?php echo addslashes($current_channel_name); ?>";
window.isDndMode = <?php echo ($user['dnd_mode'] ?? 0) ? 'true' : 'false'; ?>;
window.soundNotifications = <?php echo ($user['sound_notifications'] ?? 0) ? 'true' : 'false'; ?>;
</script>
<style>
:root {
@ -180,16 +251,14 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
echo "Direct Messages";
} else {
$active_server_name = 'Server';
$is_owner = false;
foreach($servers as $s) {
if($s['id'] == $active_server_id) {
$active_server_name = $s['name'];
$is_owner = ($s['owner_id'] == $current_user_id);
break;
}
}
echo htmlspecialchars($active_server_name);
if ($is_owner): ?>
if ($is_owner || $can_manage_server): ?>
<span class="ms-auto" style="cursor: pointer;" data-bs-toggle="modal" data-bs-target="#serverSettingsModal">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></svg>
</span>
@ -211,21 +280,33 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<?php else: ?>
<div class="channel-category">
<span>Text Channels</span>
<span class="add-channel-btn" title="Create Channel" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="text">+</span>
<?php if ($can_manage_channels): ?>
<span class="add-channel-btn" title="Create Channel" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="chat">+</span>
<?php endif; ?>
</div>
<?php foreach($channels as $c): if($c['type'] !== 'text') continue; ?>
<?php foreach($channels as $c): if($c['type'] === 'voice') continue; ?>
<div class="channel-item-container d-flex align-items-center">
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $c['id']; ?>"
class="channel-item flex-grow-1 <?php echo $c['id'] == $active_channel_id ? 'active' : ''; ?>">
<span class="me-1">
<?php
if ($c['type'] === 'announcement') echo '📢';
elseif ($c['type'] === 'rules') echo '📜';
elseif ($c['type'] === 'forum') echo '🏛️';
else echo '#';
?>
</span>
<?php echo htmlspecialchars($c['name']); ?>
</a>
<?php if ($is_owner): ?>
<?php if ($can_manage_channels): ?>
<span class="channel-settings-btn ms-1" style="cursor: pointer; color: var(--text-muted);"
data-bs-toggle="modal" data-bs-target="#editChannelModal"
data-id="<?php echo $c['id']; ?>"
data-name="<?php echo htmlspecialchars($c['name']); ?>"
data-type="<?php echo $c['type']; ?>"
data-files="<?php echo $c['allow_file_sharing']; ?>"
data-limit="<?php echo $c['message_limit']; ?>"
data-status="<?php echo htmlspecialchars($c['status'] ?? ''); ?>"
data-theme="<?php echo $c['theme_color']; ?>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</span>
@ -235,20 +316,27 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<div class="channel-category" style="margin-top: 16px;">
<span>Voice Channels</span>
<span class="add-channel-btn" title="Create Channel" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="voice">+</span>
<?php if ($can_manage_channels): ?>
<span class="add-channel-btn" title="Create Channel" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="voice">+</span>
<?php endif; ?>
</div>
<?php foreach($channels as $c): if($c['type'] !== 'voice') continue; ?>
<div class="channel-item-container d-flex align-items-center">
<div class="channel-item voice-item flex-grow-1" data-channel-id="<?php echo $c['id']; ?>">
<?php echo htmlspecialchars($c['name']); ?>
<span>🔊 <?php echo htmlspecialchars($c['name']); ?></span>
<?php if (!empty($c['status'])): ?>
<div class="channel-status small text-muted ms-4" style="font-size: 0.75em; margin-top: -2px;"><?php echo htmlspecialchars($c['status']); ?></div>
<?php endif; ?>
</div>
<?php if ($is_owner): ?>
<?php if ($can_manage_channels): ?>
<span class="channel-settings-btn ms-1" style="cursor: pointer; color: var(--text-muted);"
data-bs-toggle="modal" data-bs-target="#editChannelModal"
data-id="<?php echo $c['id']; ?>"
data-name="<?php echo htmlspecialchars($c['name']); ?>"
data-type="<?php echo $c['type']; ?>"
data-files="<?php echo $c['allow_file_sharing']; ?>"
data-limit="<?php echo $c['message_limit']; ?>"
data-status="<?php echo htmlspecialchars($c['status'] ?? ''); ?>"
data-theme="<?php echo $c['theme_color']; ?>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</span>
@ -302,117 +390,265 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
<div class="messages-list" id="messages-list">
<?php if(empty($messages)): ?>
<div style="text-align: center; color: var(--text-muted); margin-top: 40px;">
<h4>Welcome to #<?php echo htmlspecialchars($current_channel_name); ?>!</h4>
<p>This is the start of the #<?php echo htmlspecialchars($current_channel_name); ?> channel.</p>
</div>
<?php endif; ?>
<?php foreach($messages as $m):
$mention_pattern = '/@' . preg_quote($user['username'], '/') . '\b/';
$is_mentioned = preg_match($mention_pattern, $m['content']);
?>
<div class="message-item <?php echo $is_mentioned ? 'mentioned' : ''; ?> <?php echo $m['is_pinned'] ? 'pinned' : ''; ?>" data-id="<?php echo $m['id']; ?>">
<div class="message-avatar" style="<?php echo $m['avatar_url'] ? "background-image: url('{$m['avatar_url']}');" : ""; ?>"></div>
<div class="message-content">
<div class="message-author">
<?php echo htmlspecialchars($m['username']); ?>
<span class="message-time"><?php echo date('H:i', strtotime($m['created_at'])); ?></span>
<?php if ($m['is_pinned']): ?>
<span class="pinned-badge ms-2" title="Pinned Message">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path></svg>
Pinned
</span>
<?php endif; ?>
<?php if ($m['user_id'] == $current_user_id || ($active_server_id != 'dms' && $is_owner)): ?>
<div class="message-actions-menu">
<span class="action-btn pin <?php echo $m['is_pinned'] ? 'active' : ''; ?>" title="<?php echo $m['is_pinned'] ? 'Unpin' : 'Pin'; ?>" data-id="<?php echo $m['id']; ?>" data-pinned="<?php echo $m['is_pinned']; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
</span>
<?php if ($m['user_id'] == $current_user_id): ?>
<span class="action-btn edit" title="Edit" data-id="<?php echo $m['id']; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
</span>
<span class="action-btn delete" title="Delete" data-id="<?php echo $m['id']; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="message-text">
<?php
$msg_content = htmlspecialchars($m['content']);
$msg_content = preg_replace($mention_pattern, '<span class="mention">@' . htmlspecialchars($user['username']) . '</span>', $msg_content);
echo nl2br($msg_content);
?>
<?php if ($m['attachment_url']): ?>
<div class="message-attachment mt-2">
<?php
$ext = strtolower(pathinfo($m['attachment_url'], PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])):
?>
<img src="<?php echo htmlspecialchars($m['attachment_url']); ?>" class="img-fluid rounded message-img-preview" alt="Attachment" style="max-height: 300px; cursor: pointer;" onclick="window.open(this.src)">
<?php else: ?>
<a href="<?php echo htmlspecialchars($m['attachment_url']); ?>" target="_blank" class="attachment-link d-inline-flex align-items-center p-2 rounded bg-dark text-white text-decoration-none">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
<?php echo basename($m['attachment_url']); ?>
</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($m['metadata'])):
$meta = json_decode($m['metadata'], true);
if ($meta): ?>
<div class="rich-embed mt-2 p-3 rounded" style="background: rgba(0,0,0,0.1); border-left: 4px solid var(--blurple); max-width: 520px;">
<?php if (!empty($meta['site_name'])): ?>
<div class="embed-site-name mb-1" style="font-size: 0.75em; color: var(--text-muted); text-transform: uppercase; font-weight: bold;"><?php echo htmlspecialchars($meta['site_name']); ?></div>
<?php endif; ?>
<?php if (!empty($meta['title'])): ?>
<a href="<?php echo htmlspecialchars($meta['url']); ?>" target="_blank" class="embed-title d-block mb-1 text-decoration-none" style="font-weight: 600; color: #00a8fc;"><?php echo htmlspecialchars($meta['title']); ?></a>
<?php endif; ?>
<?php if (!empty($meta['description'])): ?>
<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);"><?php echo htmlspecialchars($meta['description']); ?></div>
<?php endif; ?>
<?php if (!empty($meta['image'])): ?>
<div class="embed-image">
<img src="<?php echo htmlspecialchars($meta['image']); ?>" class="rounded" style="max-width: 100%; max-height: 300px; object-fit: contain;">
<?php if($active_thread): ?>
<div class="thread-view-container p-4">
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $active_channel_id; ?><?php echo isset($_GET['status']) ? '&status='.htmlspecialchars($_GET['status']) : ''; ?>" class="btn btn-sm btn-outline-secondary mb-3"> Back to Forum</a>
<h3>Discussion: <?php echo htmlspecialchars($active_thread['title']); ?></h3>
<?php
// Fetch tags for this thread
$stmt_t = db()->prepare("SELECT ft.* FROM forum_tags ft JOIN thread_tags tt ON ft.id = tt.tag_id WHERE tt.thread_id = ?");
$stmt_t->execute([$active_thread['id']]);
$thread_tags = $stmt_t->fetchAll();
if ($thread_tags):
foreach ($thread_tags as $t):
?>
<span class="badge rounded-pill me-1" style="background-color: <?php echo htmlspecialchars($t['color']); ?>;"><?php echo htmlspecialchars($t['name']); ?></span>
<?php endforeach; endif; ?>
<div class="messages-list-inner mt-4">
<?php foreach($messages as $m):
$mention_pattern = '/@' . preg_quote($user['username'], '/') . '\b/';
$is_mentioned = preg_match($mention_pattern, $m['content']);
$is_solution = ($active_thread['solution_message_id'] == $m['id']);
?>
<!-- Message rendering code (reused) -->
<div class="message-item <?php echo $is_mentioned ? 'mentioned' : ''; ?> <?php echo $is_solution ? 'is-solution' : ''; ?>" data-id="<?php echo $m['id']; ?>">
<div class="message-avatar" style="<?php echo $m['avatar_url'] ? "background-image: url('{$m['avatar_url']}');" : ""; ?>"></div>
<div class="message-content">
<div class="message-author" style="<?php echo !empty($m['role_color']) ? "color: {$m['role_color']};" : ""; ?>">
<?php echo htmlspecialchars($m['username']); ?>
<span class="message-time"><?php echo date('H:i', strtotime($m['created_at'])); ?></span>
<?php if ($is_solution): ?>
<span class="badge bg-success ms-2">SOLUTION</span>
<?php endif; ?>
<div class="message-actions-menu">
<?php if (($active_thread['user_id'] == $current_user_id || $can_manage_server) && $m['user_id'] != $active_thread['user_id']): ?>
<span class="action-btn mark-solution <?php echo $is_solution ? 'active' : ''; ?>" title="<?php echo $is_solution ? 'Unmark as Solution' : 'Mark as Solution'; ?>" data-thread-id="<?php echo $active_thread['id']; ?>" data-message-id="<?php echo $m['id']; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="message-text">
<?php echo nl2br(htmlspecialchars($m['content'])); ?>
</div>
<div class="message-reactions mt-1" data-message-id="<?php echo $m['id']; ?>">
<?php
// Fetch reactions for this message
$stmt_react = db()->prepare("SELECT emoji, COUNT(*) as count, GROUP_CONCAT(user_id) as users FROM message_reactions WHERE message_id = ? GROUP BY emoji");
$stmt_react->execute([$m['id']]);
$reactions = $stmt_react->fetchAll();
foreach ($reactions as $r):
$reacted = in_array($current_user_id, explode(',', $r['users']));
?>
<span class="reaction-badge <?php echo $reacted ? 'active' : ''; ?>" data-emoji="<?php echo htmlspecialchars($r['emoji']); ?>">
<?php echo htmlspecialchars($r['emoji']); ?> <span class="count"><?php echo $r['count']; ?></span>
</span>
<?php endforeach; ?>
<span class="add-reaction-btn" title="Add Reaction">+</span>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<div class="message-reactions mt-1" data-message-id="<?php echo $m['id']; ?>">
<?php
// Fetch reactions for this message
$stmt_react = db()->prepare("SELECT emoji, COUNT(*) as count, GROUP_CONCAT(user_id) as users FROM message_reactions WHERE message_id = ? GROUP BY emoji");
$stmt_react->execute([$m['id']]);
$reactions = $stmt_react->fetchAll();
foreach ($reactions as $r):
$reacted = in_array($current_user_id, explode(',', $r['users']));
?>
<span class="reaction-badge <?php echo $reacted ? 'active' : ''; ?>" data-emoji="<?php echo htmlspecialchars($r['emoji']); ?>">
<?php echo htmlspecialchars($r['emoji']); ?> <span class="count"><?php echo $r['count']; ?></span>
</span>
<?php endforeach; ?>
<span class="add-reaction-btn" title="Add Reaction">+</span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
<?php elseif($channel_type === 'rules'): ?>
<div class="rules-container p-4">
<h2 class="mb-4">📜 <?php echo htmlspecialchars($current_channel_name); ?></h2>
<div id="rules-list-sortable">
<?php foreach($rules as $rule): ?>
<div class="rule-item mb-3 p-3 rounded bg-dark border-start border-4 border-primary d-flex justify-content-between align-items-center" data-id="<?php echo $rule['id']; ?>">
<div class="rule-content flex-grow-1">
<?php echo nl2br(htmlspecialchars($rule['content'])); ?>
</div>
<?php if($can_manage_channels): ?>
<div class="rule-actions ms-3 d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary move-rule-btn" data-id="<?php echo $rule['id']; ?>" data-dir="up"></button>
<button class="btn btn-sm btn-outline-secondary move-rule-btn" data-id="<?php echo $rule['id']; ?>" data-dir="down"></button>
<button class="btn btn-sm btn-outline-light edit-rule-btn" data-id="<?php echo $rule['id']; ?>">Edit</button>
<button class="btn btn-sm btn-outline-danger delete-rule-btn" data-id="<?php echo $rule['id']; ?>">×</button>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php if($can_manage_channels): ?>
<button class="btn btn-primary mt-3" id="add-rule-btn">+ Add a Rule</button>
<?php endif; ?>
</div>
<?php elseif($channel_type === 'forum'): ?>
<div class="forum-container p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-2">🏛️ <?php echo htmlspecialchars($current_channel_name); ?></h2>
<div class="btn-group btn-group-sm forum-filters">
<?php
$s_id = $active_server_id;
$c_id = $active_channel_id;
$curr_status = $_GET['status'] ?? 'all';
?>
<a href="?server_id=<?php echo $s_id; ?>&channel_id=<?php echo $c_id; ?>&status=all" class="btn btn-outline-secondary <?php echo $curr_status === 'all' ? 'active' : ''; ?>">All</a>
<a href="?server_id=<?php echo $s_id; ?>&channel_id=<?php echo $c_id; ?>&status=unresolved" class="btn btn-outline-secondary <?php echo $curr_status === 'unresolved' ? 'active' : ''; ?>">Unresolved</a>
<a href="?server_id=<?php echo $s_id; ?>&channel_id=<?php echo $c_id; ?>&status=resolved" class="btn btn-outline-secondary <?php echo $curr_status === 'resolved' ? 'active' : ''; ?>">Resolved</a>
</div>
</div>
<div class="d-flex gap-2">
<?php if($can_manage_channels): ?>
<button class="btn btn-outline-secondary" id="manage-tags-btn">Manage Tags</button>
<?php endif; ?>
<button class="btn btn-primary" id="new-thread-btn">New Discussion</button>
</div>
</div>
<div class="thread-list">
<?php if(empty($threads)): ?>
<div class="text-center text-muted mt-5">No discussions yet. Start one!</div>
<?php endif; ?>
<?php foreach($threads as $thread): ?>
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $active_channel_id; ?>&thread_id=<?php echo $thread['id']; ?><?php echo isset($_GET['status']) ? '&status='.htmlspecialchars($_GET['status']) : ''; ?>" class="thread-item d-flex align-items-center p-3 mb-2 rounded bg-dark text-decoration-none text-white border-start border-4 border-secondary">
<div class="thread-icon me-3">💬</div>
<div class="thread-info flex-grow-1">
<div class="thread-title fw-bold">
<?php if($thread['solution_message_id']): ?>
<span class="text-success me-1" title="Solved"></span>
<?php endif; ?>
<?php echo htmlspecialchars($thread['title']); ?>
<?php if($thread['tags']):
$tag_list = explode('|', $thread['tags']);
foreach($tag_list as $tag_data):
list($t_name, $t_color) = explode(':', $tag_data);
?>
<span class="badge rounded-pill ms-1" style="background-color: <?php echo htmlspecialchars($t_color); ?>; font-size: 0.6em;"><?php echo htmlspecialchars($t_name); ?></span>
<?php endforeach; endif; ?>
</div>
<div class="thread-meta small text-muted">Started by <?php echo htmlspecialchars($thread['username']); ?> • <?php echo $thread['message_count']; ?> messages</div>
</div>
<div class="thread-activity text-end small text-muted">
<?php if($thread['last_activity']): ?>
Last active: <?php echo date('H:i', strtotime($thread['last_activity'])); ?>
<?php endif; ?>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<?php if(empty($messages)): ?>
<div style="text-align: center; color: var(--text-muted); margin-top: 40px;">
<h4>Welcome to #<?php echo htmlspecialchars($current_channel_name); ?>!</h4>
<p>This is the start of the #<?php echo htmlspecialchars($current_channel_name); ?> channel.</p>
</div>
<?php endif; ?>
<?php foreach($messages as $m):
$mention_pattern = '/@' . preg_quote($user['username'], '/') . '\b/';
$is_mentioned = preg_match($mention_pattern, $m['content']);
?>
<div class="message-item <?php echo $is_mentioned ? 'mentioned' : ''; ?> <?php echo $m['is_pinned'] ? 'pinned' : ''; ?> <?php echo $channel_type === 'announcement' ? 'announcement-style' : ''; ?>" data-id="<?php echo $m['id']; ?>">
<div class="message-avatar" style="<?php echo $m['avatar_url'] ? "background-image: url('{$m['avatar_url']}');" : ""; ?>"></div>
<div class="message-content">
<div class="message-author" style="<?php echo !empty($m['role_color']) ? "color: {$m['role_color']};" : ""; ?>">
<?php echo htmlspecialchars($m['username']); ?>
<span class="message-time"><?php echo date('H:i', strtotime($m['created_at'])); ?></span>
<?php if ($m['is_pinned']): ?>
<span class="pinned-badge ms-2" title="Pinned Message">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path></svg>
Pinned
</span>
<?php endif; ?>
<?php if ($m['user_id'] == $current_user_id || ($active_server_id != 'dms' && $can_manage_channels)): ?>
<div class="message-actions-menu">
<span class="action-btn pin <?php echo $m['is_pinned'] ? 'active' : ''; ?>" title="<?php echo $m['is_pinned'] ? 'Unpin' : 'Pin'; ?>" data-id="<?php echo $m['id']; ?>" data-pinned="<?php echo $m['is_pinned']; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
</span>
<?php if ($m['user_id'] == $current_user_id): ?>
<span class="action-btn edit" title="Edit" data-id="<?php echo $m['id']; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
</span>
<span class="action-btn delete" title="Delete" data-id="<?php echo $m['id']; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="message-text">
<?php
$msg_content = htmlspecialchars($m['content']);
$msg_content = preg_replace($mention_pattern, '<span class="mention">@' . htmlspecialchars($user['username']) . '</span>', $msg_content);
echo nl2br($msg_content);
?>
<?php if ($m['attachment_url']): ?>
<div class="message-attachment mt-2">
<?php
$ext = strtolower(pathinfo($m['attachment_url'], PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])):
?>
<img src="<?php echo htmlspecialchars($m['attachment_url']); ?>" class="img-fluid rounded message-img-preview" alt="Attachment" style="max-height: 300px; cursor: pointer;" onclick="window.open(this.src)">
<?php else: ?>
<a href="<?php echo htmlspecialchars($m['attachment_url']); ?>" target="_blank" class="attachment-link d-inline-flex align-items-center p-2 rounded bg-dark text-white text-decoration-none">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
<?php echo basename($m['attachment_url']); ?>
</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($m['metadata'])):
$meta = json_decode($m['metadata'], true);
if ($meta): ?>
<div class="rich-embed mt-2 p-3 rounded" style="background: rgba(0,0,0,0.1); border-left: 4px solid var(--blurple); max-width: 520px;">
<?php if (!empty($meta['site_name'])): ?>
<div class="embed-site-name mb-1" style="font-size: 0.75em; color: var(--text-muted); text-transform: uppercase; font-weight: bold;"><?php echo htmlspecialchars($meta['site_name']); ?></div>
<?php endif; ?>
<?php if (!empty($meta['title'])): ?>
<a href="<?php echo htmlspecialchars($meta['url']); ?>" target="_blank" class="embed-title d-block mb-1 text-decoration-none" style="font-weight: 600; color: #00a8fc;"><?php echo htmlspecialchars($meta['title']); ?></a>
<?php endif; ?>
<?php if (!empty($meta['description'])): ?>
<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);"><?php echo htmlspecialchars($meta['description']); ?></div>
<?php endif; ?>
<?php if (!empty($meta['image'])): ?>
<div class="embed-image">
<img src="<?php echo htmlspecialchars($meta['image']); ?>" class="rounded" style="max-width: 100%; max-height: 300px; object-fit: contain;">
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<div class="message-reactions mt-1" data-message-id="<?php echo $m['id']; ?>">
<?php
// Fetch reactions for this message
$stmt_react = db()->prepare("SELECT emoji, COUNT(*) as count, GROUP_CONCAT(user_id) as users FROM message_reactions WHERE message_id = ? GROUP BY emoji");
$stmt_react->execute([$m['id']]);
$reactions = $stmt_react->fetchAll();
foreach ($reactions as $r):
$reacted = in_array($current_user_id, explode(',', $r['users']));
?>
<span class="reaction-badge <?php echo $reacted ? 'active' : ''; ?>" data-emoji="<?php echo htmlspecialchars($r['emoji']); ?>">
<?php echo htmlspecialchars($r['emoji']); ?> <span class="count"><?php echo $r['count']; ?></span>
</span>
<?php endforeach; ?>
<span class="add-reaction-btn" title="Add Reaction">+</span>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<div id="typing-indicator" class="typing-indicator"></div>
<div class="chat-input-container">
<?php
$allow_files = true;
foreach($channels as $c) {
if($c['id'] == $active_channel_id) {
$allow_files = (bool)$c['allow_file_sharing'];
break;
$show_input = true;
if ($channel_type === 'rules') $show_input = false;
if ($channel_type === 'forum' && !$active_thread) $show_input = false;
if ($channel_type === 'announcement' && !$can_manage_channels) $show_input = false;
if ($show_input):
$allow_files = true;
foreach($channels as $c) {
if($c['id'] == $active_channel_id) {
$allow_files = (bool)$c['allow_file_sharing'];
break;
}
}
}
?>
<div id="upload-progress-container" class="upload-progress-container" style="display: none;">
<div class="progress" style="height: 4px; background-color: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden;">
@ -453,7 +689,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<div style="position: absolute; bottom: 0; right: 0; width: 10px; height: 10px; background-color: #23a559; border-radius: 50%; border: 2px solid var(--bg-members);"></div>
<?php endif; ?>
</div>
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; <?php echo !empty($m['role_color']) ? "color: {$m['role_color']};" : ""; ?>">
<?php echo htmlspecialchars($m['username']); ?>
</span>
</div>
@ -489,6 +725,11 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<label class="form-check-label text-white" for="dnd-switch">Do Not Disturb</label>
<div class="form-text text-muted" style="font-size: 0.8em;">Mute all desktop notifications.</div>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" name="sound_notifications" id="sound-switch" value="1" <?php echo ($user['sound_notifications'] ?? 0) ? 'checked' : ''; ?>>
<label class="form-check-label text-white" for="sound-switch">Sound Notifications</label>
<div class="form-text text-muted" style="font-size: 0.8em;">Play a sound when you are mentioned.</div>
</div>
<div class="mb-2">
<label class="form-label text-white d-block" style="font-size: 0.9em;">Appearance</label>
<div class="btn-group w-100" role="group">
@ -511,8 +752,10 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
</div>
</form>
</div>
</form>
<?php endif; ?>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link text-white text-decoration-none" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="save-settings-btn" class="btn btn-primary" style="background-color: var(--blurple); border: none; padding: 10px 24px;">Save Changes</button>
@ -540,6 +783,9 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="webhooks-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-webhooks" type="button">Webhooks</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="members-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-members" type="button">Members</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="stats-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-stats" type="button">Stats</button>
</li>
@ -607,6 +853,12 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<!-- Webhooks will be loaded here -->
</div>
</div>
<div class="tab-pane fade" id="settings-members">
<h6 class="mb-3">Server Members</h6>
<div id="server-members-list" class="list-group list-group-flush bg-transparent">
<!-- Members will be loaded here -->
</div>
</div>
<div class="tab-pane fade" id="settings-stats">
<div id="stats-content">
<div class="row text-center mb-4">
@ -695,8 +947,18 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
<form action="api_v1_channels.php" method="POST">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<input type="hidden" name="type" id="channel-type-input" value="text">
<input type="hidden" name="type" id="channel-type-input" value="chat">
<div class="modal-body">
<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" class="form-select bg-dark text-white border-secondary mb-3" id="channel-type-select">
<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>
</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">
@ -740,6 +1002,9 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<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>
</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>
</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>
</li>
@ -751,11 +1016,28 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<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>
</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>
<input type="text" name="name" id="edit-channel-name" class="form-control" required>
</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>
<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>
@ -783,6 +1065,22 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<button type="submit" class="btn btn-danger w-100">Delete Channel</button>
</form>
</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="input-group">
<input type="url" id="new-rss-url" class="form-control bg-dark text-white border-secondary" placeholder="https://example.com/rss.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>
</div>
<div id="rss-feeds-list" class="list-group list-group-flush bg-transparent">
<!-- 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>
@ -818,12 +1116,104 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
</div>
</div>
<!-- Role Editor Modal -->
<div class="modal fade" id="roleEditorModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Role</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-role-id">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Role Name</label>
<input type="text" id="edit-role-name" class="form-control">
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Role Color</label>
<input type="color" id="edit-role-color" class="form-control form-control-color w-100">
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Permissions</label>
<div id="role-permissions-checkboxes" class="p-2 bg-dark rounded">
<!-- Permission checkboxes here -->
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="save-role-btn" class="btn btn-primary">Save Role</button>
</div>
</div>
</div>
</div>
<!-- New Thread Modal -->
<div class="modal fade" id="newThreadModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New Discussion</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Thread Title</label>
<input type="text" id="new-thread-title" class="form-control" placeholder="What's on your mind?">
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Tags</label>
<div id="new-thread-tags-list" class="d-flex flex-wrap gap-2 p-2 bg-dark rounded">
<!-- Tags loaded here -->
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="submit-new-thread-btn" class="btn btn-primary">Create Thread</button>
</div>
</div>
</div>
</div>
<!-- Manage Tags Modal -->
<div class="modal fade" id="manageTagsModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Manage Forum Tags</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="forum-tags-admin-list" class="mb-4">
<!-- List of tags with delete buttons -->
</div>
<hr class="border-secondary">
<h6>Add New Tag</h6>
<div class="row g-2">
<div class="col-7">
<input type="text" id="new-tag-name" class="form-control form-control-sm" placeholder="Tag Name">
</div>
<div class="col-3">
<input type="color" id="new-tag-color" class="form-control form-control-sm form-control-color w-100" value="#7289da">
</div>
<div class="col-2">
<button class="btn btn-sm btn-primary w-100" id="add-forum-tag-btn">+</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
window.currentUserId = <?php echo $current_user_id; ?>;
window.currentUsername = "<?php echo addslashes($user['username']); ?>";
window.currentChannelName = "<?php echo addslashes($current_channel_name); ?>";
window.isServerOwner = <?php echo ($is_owner ?? false) ? 'true' : 'false'; ?>;
window.canManageServer = <?php echo ($can_manage_server ?? false) ? 'true' : 'false'; ?>;
window.isDndMode = <?php echo ($user['dnd_mode'] ?? 0) ? 'true' : 'false'; ?>;
</script>
<script src="assets/js/voice.js?v=<?php echo time(); ?>"></script>