Autosave: 20260215-151332
This commit is contained in:
parent
9c07e1ee23
commit
40f605d106
@ -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));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
128
api_v1_roles.php
128
api_v1_roles.php
@ -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
116
api_v1_rss.php
Normal 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
103
api_v1_rules.php
Normal 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
54
api_v1_tags.php
Normal 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
80
api_v1_threads.php
Normal 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;
|
||||
}
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
2
db/migrations/20260215_channel_status.sql
Normal file
2
db/migrations/20260215_channel_status.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Migration: Add status to channels for voice channels
|
||||
ALTER TABLE channels ADD COLUMN status VARCHAR(255) DEFAULT NULL;
|
||||
3
db/migrations/20260215_forum_solution.sql
Normal file
3
db/migrations/20260215_forum_solution.sql
Normal 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;
|
||||
16
db/migrations/20260215_forum_tags.sql
Normal file
16
db/migrations/20260215_forum_tags.sql
Normal 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
|
||||
);
|
||||
12
db/migrations/20260215_rss_feeds.sql
Normal file
12
db/migrations/20260215_rss_feeds.sql
Normal 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
650
index.php
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user