v6
This commit is contained in:
parent
0911f86785
commit
1e73419ffb
66
api_v1_channel_permissions.php
Normal file
66
api_v1_channel_permissions.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once 'auth/session.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true) ?? $_POST;
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||||
|
$channel_id = $_GET['channel_id'] ?? 0;
|
||||||
|
|
||||||
|
// Fetch permissions for this channel
|
||||||
|
$stmt = db()->prepare("
|
||||||
|
SELECT cp.*, r.name as role_name, r.color as role_color
|
||||||
|
FROM channel_permissions cp
|
||||||
|
JOIN roles r ON cp.role_id = r.id
|
||||||
|
WHERE cp.channel_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$channel_id]);
|
||||||
|
echo json_encode(['success' => true, 'permissions' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$channel_id = $data['channel_id'] ?? 0;
|
||||||
|
$role_id = $data['role_id'] ?? 0;
|
||||||
|
$allow = $data['allow'] ?? 0;
|
||||||
|
$deny = $data['deny'] ?? 0;
|
||||||
|
|
||||||
|
// 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 = ?");
|
||||||
|
$stmt->execute([$channel_id]);
|
||||||
|
$server = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($server && $server['owner_id'] == $user_id) {
|
||||||
|
$stmt = db()->prepare("
|
||||||
|
INSERT INTO channel_permissions (channel_id, role_id, allow_permissions, deny_permissions)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE allow_permissions = VALUES(allow_permissions), deny_permissions = VALUES(deny_permissions)
|
||||||
|
");
|
||||||
|
$stmt->execute([$channel_id, $role_id, $allow, $deny]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
|
||||||
|
$channel_id = $data['channel_id'] ?? 0;
|
||||||
|
$role_id = $data['role_id'] ?? 0;
|
||||||
|
|
||||||
|
// Check if user is owner
|
||||||
|
$stmt = db()->prepare("SELECT s.owner_id FROM servers s JOIN channels c ON s.id = c.server_id WHERE c.id = ?");
|
||||||
|
$stmt->execute([$channel_id]);
|
||||||
|
$server = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($server && $server['owner_id'] == $user_id) {
|
||||||
|
$stmt = db()->prepare("DELETE FROM channel_permissions WHERE channel_id = ? AND role_id = ?");
|
||||||
|
$stmt->execute([$channel_id, $role_id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@ -24,6 +24,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$channel_id = $_POST['channel_id'] ?? 0;
|
$channel_id = $_POST['channel_id'] ?? 0;
|
||||||
$name = $_POST['name'] ?? '';
|
$name = $_POST['name'] ?? '';
|
||||||
$allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0;
|
$allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0;
|
||||||
|
$theme_color = $_POST['theme_color'] ?? null;
|
||||||
|
if ($theme_color === '') $theme_color = null;
|
||||||
|
|
||||||
// Check if user is owner of the server
|
// 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 = ?");
|
$stmt = db()->prepare("SELECT s.owner_id FROM servers s JOIN channels c ON s.id = c.server_id WHERE c.id = ?");
|
||||||
@ -32,8 +34,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
if ($server && $server['owner_id'] == $user_id) {
|
if ($server && $server['owner_id'] == $user_id) {
|
||||||
$name = strtolower(preg_replace('/[^a-zA-Z0-9\-]/', '-', $name));
|
$name = strtolower(preg_replace('/[^a-zA-Z0-9\-]/', '-', $name));
|
||||||
$stmt = db()->prepare("UPDATE channels SET name = ?, allow_file_sharing = ? WHERE id = ?");
|
$stmt = db()->prepare("UPDATE channels SET name = ?, allow_file_sharing = ?, theme_color = ? WHERE id = ?");
|
||||||
$stmt->execute([$name, $allow_file_sharing, $channel_id]);
|
$stmt->execute([$name, $allow_file_sharing, $theme_color, $channel_id]);
|
||||||
}
|
}
|
||||||
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
|
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
|
||||||
exit;
|
exit;
|
||||||
@ -67,9 +69,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
// Basic sanitization for channel name
|
// Basic sanitization for channel name
|
||||||
$name = strtolower(preg_replace('/[^a-zA-Z0-9\-]/', '-', $name));
|
$name = strtolower(preg_replace('/[^a-zA-Z0-9\-]/', '-', $name));
|
||||||
$allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0;
|
$allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0;
|
||||||
|
$theme_color = $_POST['theme_color'] ?? null;
|
||||||
|
if ($theme_color === '') $theme_color = null;
|
||||||
|
|
||||||
$stmt = db()->prepare("INSERT INTO channels (server_id, name, type, allow_file_sharing) VALUES (?, ?, ?, ?)");
|
$stmt = db()->prepare("INSERT INTO channels (server_id, name, type, allow_file_sharing, theme_color) VALUES (?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([$server_id, $name, $type, $allow_file_sharing]);
|
$stmt->execute([$server_id, $name, $type, $allow_file_sharing, $theme_color]);
|
||||||
$channel_id = db()->lastInsertId();
|
$channel_id = db()->lastInsertId();
|
||||||
|
|
||||||
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
|
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ header('Content-Type: application/json');
|
|||||||
require_once 'auth/session.php';
|
require_once 'auth/session.php';
|
||||||
require_once 'includes/opengraph.php';
|
require_once 'includes/opengraph.php';
|
||||||
require_once 'includes/ai_filtering.php';
|
require_once 'includes/ai_filtering.php';
|
||||||
|
require_once 'includes/permissions.php';
|
||||||
|
|
||||||
// Check for Bot token in headers
|
// Check for Bot token in headers
|
||||||
$headers = getallheaders();
|
$headers = getallheaders();
|
||||||
@ -31,17 +32,59 @@ if ($bot_token) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||||
|
$channel_id = $_GET['channel_id'] ?? 0;
|
||||||
|
$pinned = isset($_GET['pinned']) && $_GET['pinned'] == 1;
|
||||||
|
|
||||||
|
if ($pinned) {
|
||||||
|
try {
|
||||||
|
$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 = ? AND m.is_pinned = 1
|
||||||
|
ORDER BY m.created_at DESC
|
||||||
|
");
|
||||||
|
$stmt->execute([$channel_id]);
|
||||||
|
$msgs = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach ($msgs as &$m) {
|
||||||
|
$m['time'] = date('H:i', strtotime($m['created_at']));
|
||||||
|
$m['metadata'] = $m['metadata'] ? json_decode($m['metadata']) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'messages' => $msgs]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
|
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
$message_id = $data['id'] ?? 0;
|
$message_id = $data['id'] ?? 0;
|
||||||
$content = $data['content'] ?? '';
|
$content = $data['content'] ?? '';
|
||||||
|
$action = $data['action'] ?? 'edit';
|
||||||
if (empty($content)) {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Content cannot be empty']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($action === 'pin') {
|
||||||
|
$stmt = db()->prepare("UPDATE messages SET is_pinned = 1 WHERE id = ?");
|
||||||
|
$stmt->execute([$message_id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($action === 'unpin') {
|
||||||
|
$stmt = db()->prepare("UPDATE messages SET is_pinned = 0 WHERE id = ?");
|
||||||
|
$stmt->execute([$message_id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($content)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Content cannot be empty']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
$stmt = db()->prepare("UPDATE messages SET content = ? WHERE id = ? AND user_id = ?");
|
$stmt = db()->prepare("UPDATE messages SET content = ? WHERE id = ? AND user_id = ?");
|
||||||
$stmt->execute([$content, $message_id, $user_id]);
|
$stmt->execute([$content, $message_id, $user_id]);
|
||||||
|
|
||||||
@ -115,6 +158,12 @@ if (empty($content) && empty($attachment_url)) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check granular permissions
|
||||||
|
if (!Permissions::canSendInChannel($user_id, $channel_id)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'You do not have permission to send messages in this channel.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($content)) {
|
if (!empty($content)) {
|
||||||
$moderation = moderateContent($content);
|
$moderation = moderateContent($content);
|
||||||
if (!$moderation['is_safe']) {
|
if (!$moderation['is_safe']) {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ requireLogin();
|
|||||||
|
|
||||||
$user_id = $_SESSION['user_id'];
|
$user_id = $_SESSION['user_id'];
|
||||||
$query = $_GET['q'] ?? '';
|
$query = $_GET['q'] ?? '';
|
||||||
|
$type = $_GET['type'] ?? 'messages'; // messages or users
|
||||||
$channel_id = $_GET['channel_id'] ?? 0;
|
$channel_id = $_GET['channel_id'] ?? 0;
|
||||||
|
|
||||||
if (empty($query)) {
|
if (empty($query)) {
|
||||||
@ -13,31 +14,42 @@ if (empty($query)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$sql = "SELECT m.*, u.username, u.avatar_url
|
if ($type === 'users') {
|
||||||
FROM messages m
|
$stmt = db()->prepare("
|
||||||
JOIN users u ON m.user_id = u.id
|
SELECT id, username, avatar_url, status
|
||||||
WHERE m.content LIKE ? ";
|
FROM users
|
||||||
$params = ["%" . $query . "%"];
|
WHERE username LIKE ?
|
||||||
|
LIMIT 20
|
||||||
if ($channel_id > 0) {
|
");
|
||||||
$sql .= " AND m.channel_id = ?";
|
$stmt->execute(["%" . $query . "%"]);
|
||||||
$params[] = $channel_id;
|
$results = $stmt->fetchAll();
|
||||||
} else {
|
} else {
|
||||||
// Search in all channels user has access to
|
$sql = "SELECT m.*, u.username, u.avatar_url
|
||||||
$sql .= " AND m.channel_id IN (
|
FROM messages m
|
||||||
SELECT c.id FROM channels c
|
JOIN users u ON m.user_id = u.id
|
||||||
LEFT JOIN server_members sm ON c.server_id = sm.server_id
|
WHERE m.content LIKE ? ";
|
||||||
LEFT JOIN channel_members cm ON c.id = cm.channel_id
|
$params = ["%" . $query . "%"];
|
||||||
WHERE sm.user_id = ? OR cm.user_id = ?
|
|
||||||
)";
|
|
||||||
$params[] = $user_id;
|
|
||||||
$params[] = $user_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql .= " ORDER BY m.created_at DESC LIMIT 50";
|
if ($channel_id > 0) {
|
||||||
$stmt = db()->prepare($sql);
|
$sql .= " AND m.channel_id = ?";
|
||||||
$stmt->execute($params);
|
$params[] = $channel_id;
|
||||||
$results = $stmt->fetchAll();
|
} else {
|
||||||
|
// Search in all channels user has access to
|
||||||
|
$sql .= " AND m.channel_id IN (
|
||||||
|
SELECT c.id FROM channels c
|
||||||
|
LEFT JOIN server_members sm ON c.server_id = sm.server_id
|
||||||
|
LEFT JOIN channel_members cm ON c.id = cm.channel_id
|
||||||
|
WHERE sm.user_id = ? OR cm.user_id = ?
|
||||||
|
)";
|
||||||
|
$params[] = $user_id;
|
||||||
|
$params[] = $user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY m.created_at DESC LIMIT 50";
|
||||||
|
$stmt = db()->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$results = $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
echo json_encode(['success' => true, 'results' => $results]);
|
echo json_encode(['success' => true, 'results' => $results]);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
|||||||
74
api_v1_stats.php
Normal file
74
api_v1_stats.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once 'auth/session.php';
|
||||||
|
|
||||||
|
$server_id = $_GET['server_id'] ?? 0;
|
||||||
|
if (!$server_id) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Server ID required']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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()) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Total members
|
||||||
|
$stmt = db()->prepare("SELECT COUNT(*) as count FROM server_members WHERE server_id = ?");
|
||||||
|
$stmt->execute([$server_id]);
|
||||||
|
$total_members = $stmt->fetch()['count'];
|
||||||
|
|
||||||
|
// Total messages in all channels of this server
|
||||||
|
$stmt = db()->prepare("
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM messages m
|
||||||
|
JOIN channels c ON m.channel_id = c.id
|
||||||
|
WHERE c.server_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$server_id]);
|
||||||
|
$total_messages = $stmt->fetch()['count'];
|
||||||
|
|
||||||
|
// Messages per day (last 7 days)
|
||||||
|
$stmt = db()->prepare("
|
||||||
|
SELECT DATE(m.created_at) as date, COUNT(*) as count
|
||||||
|
FROM messages m
|
||||||
|
JOIN channels c ON m.channel_id = c.id
|
||||||
|
WHERE c.server_id = ? AND m.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||||||
|
GROUP BY DATE(m.created_at)
|
||||||
|
ORDER BY date ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$server_id]);
|
||||||
|
$history = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Top active users
|
||||||
|
$stmt = db()->prepare("
|
||||||
|
SELECT u.username, COUNT(*) as message_count
|
||||||
|
FROM messages m
|
||||||
|
JOIN channels c ON m.channel_id = c.id
|
||||||
|
JOIN users u ON m.user_id = u.id
|
||||||
|
WHERE c.server_id = ?
|
||||||
|
GROUP BY m.user_id
|
||||||
|
ORDER BY message_count DESC
|
||||||
|
LIMIT 5
|
||||||
|
");
|
||||||
|
$stmt->execute([$server_id]);
|
||||||
|
$top_users = $stmt->fetchAll();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'stats' => [
|
||||||
|
'total_members' => $total_members,
|
||||||
|
'total_messages' => $total_messages,
|
||||||
|
'history' => $history,
|
||||||
|
'top_users' => $top_users
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@ -11,10 +11,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
$username = $_POST['username'] ?? $user['username'];
|
$username = $_POST['username'] ?? $user['username'];
|
||||||
$avatar_url = $_POST['avatar_url'] ?? $user['avatar_url'];
|
$avatar_url = $_POST['avatar_url'] ?? $user['avatar_url'];
|
||||||
|
$dnd_mode = isset($_POST['dnd_mode']) ? (int)$_POST['dnd_mode'] : (int)($user['dnd_mode'] ?? 0);
|
||||||
|
$theme = $_POST['theme'] ?? $user['theme'] ?? 'dark';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = db()->prepare("UPDATE users SET username = ?, avatar_url = ? WHERE id = ?");
|
$stmt = db()->prepare("UPDATE users SET username = ?, avatar_url = ?, dnd_mode = ?, theme = ? WHERE id = ?");
|
||||||
$stmt->execute([$username, $avatar_url, $user['id']]);
|
$stmt->execute([$username, $avatar_url, $dnd_mode, $theme, $user['id']]);
|
||||||
|
|
||||||
$_SESSION['username'] = $username; // Update session if stored (though getCurrentUser fetches from DB)
|
$_SESSION['username'] = $username; // Update session if stored (though getCurrentUser fetches from DB)
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,75 @@
|
|||||||
--active: #3f4147;
|
--active: #3f4147;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] {
|
||||||
|
--bg-servers: #e3e5e8;
|
||||||
|
--bg-channels: #f2f3f5;
|
||||||
|
--bg-chat: #ffffff;
|
||||||
|
--bg-members: #f2f3f5;
|
||||||
|
--text-primary: #313338;
|
||||||
|
--text-muted: #5c5e66;
|
||||||
|
--hover: #e8e9eb;
|
||||||
|
--active: #dbdee1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .chat-input-wrapper {
|
||||||
|
background-color: #ebedef;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .message-item:hover {
|
||||||
|
background-color: rgba(0,0,0,0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .user-panel {
|
||||||
|
background-color: #ebedef;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .form-control {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #dbdee1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .modal-content {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #313338;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .modal-header, [data-theme="light"] .modal-footer {
|
||||||
|
border-color: #dbdee1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .btn-close {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .search-input {
|
||||||
|
background-color: #dbdee1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .search-results-dropdown {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .channel-category {
|
||||||
|
color: #5c5e66;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .dm-status-indicator {
|
||||||
|
border-color: #f2f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] hr {
|
||||||
|
border-color: #dbdee1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .upload-progress-container {
|
||||||
|
background-color: #f2f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .rich-embed {
|
||||||
|
background: rgba(0,0,0,0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -663,3 +732,56 @@ body {
|
|||||||
.channel-settings-btn:hover {
|
.channel-settings-btn:hover {
|
||||||
color: var(--text-normal) !important;
|
color: var(--text-normal) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Progress Bar */
|
||||||
|
.upload-progress-container {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: rgba(0,0,0,0.1);
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mentions */
|
||||||
|
.mention {
|
||||||
|
background-color: rgba(88, 101, 242, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention:hover {
|
||||||
|
background-color: var(--blurple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.mentioned {
|
||||||
|
background-color: rgba(250, 166, 26, 0.05);
|
||||||
|
border-left: 2px solid #faa61a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pinned Messages */
|
||||||
|
.message-item.pinned {
|
||||||
|
background-color: rgba(88, 101, 242, 0.05);
|
||||||
|
border-left: 2px solid var(--blurple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinned-badge {
|
||||||
|
font-size: 0.7em;
|
||||||
|
color: var(--blurple);
|
||||||
|
background-color: rgba(88, 101, 242, 0.1);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.pin.active {
|
||||||
|
color: var(--blurple);
|
||||||
|
}
|
||||||
|
|||||||
@ -14,6 +14,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const currentChannel = new URLSearchParams(window.location.search).get('channel_id') || 1;
|
const currentChannel = new URLSearchParams(window.location.search).get('channel_id') || 1;
|
||||||
let typingTimeout;
|
let typingTimeout;
|
||||||
|
|
||||||
|
// Notification Permission
|
||||||
|
if ("Notification" in window && Notification.permission === "default") {
|
||||||
|
Notification.requestPermission();
|
||||||
|
}
|
||||||
|
|
||||||
// WebSocket for real-time
|
// WebSocket for real-time
|
||||||
let ws;
|
let ws;
|
||||||
let voiceHandler;
|
let voiceHandler;
|
||||||
@ -40,6 +45,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (data.channel_id == currentChannel) {
|
if (data.channel_id == currentChannel) {
|
||||||
appendMessage(data);
|
appendMessage(data);
|
||||||
messagesList.scrollTop = messagesList.scrollHeight;
|
messagesList.scrollTop = messagesList.scrollHeight;
|
||||||
|
|
||||||
|
// Desktop Notifications for mentions
|
||||||
|
if (data.content.includes(`@${window.currentUsername}`) && data.user_id != window.currentUserId) {
|
||||||
|
if (Notification.permission === "granted" && !window.isDndMode) {
|
||||||
|
new Notification(`Mention in #${window.currentChannelName}`, {
|
||||||
|
body: `${data.username}: ${data.content}`,
|
||||||
|
icon: data.avatar_url || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === 'typing') {
|
} else if (msg.type === 'typing') {
|
||||||
if (msg.channel_id == currentChannel && msg.user_id != window.currentUserId) {
|
if (msg.channel_id == currentChannel && msg.user_id != window.currentUserId) {
|
||||||
@ -89,7 +104,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
chatForm.addEventListener('submit', async (e) => {
|
chatForm.addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const content = chatInput.value.trim();
|
const content = chatInput.value.trim();
|
||||||
const file = fileUpload.files[0];
|
const file = fileUpload.files[0];
|
||||||
@ -99,35 +114,64 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('content', content);
|
formData.append('content', content);
|
||||||
formData.append('channel_id', currentChannel);
|
formData.append('channel_id', currentChannel);
|
||||||
|
|
||||||
|
const progressContainer = document.getElementById('upload-progress-container');
|
||||||
|
const progressBar = document.getElementById('upload-progress-bar');
|
||||||
|
const progressPercent = document.getElementById('upload-percentage');
|
||||||
|
const progressFilename = document.getElementById('upload-filename');
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
fileUpload.value = ''; // Clear file input
|
fileUpload.value = ''; // Clear file input
|
||||||
|
|
||||||
|
// Show progress bar
|
||||||
|
progressContainer.style.display = 'block';
|
||||||
|
progressFilename.textContent = `Uploading: ${file.name}`;
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
progressPercent.textContent = '0%';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const xhr = new XMLHttpRequest();
|
||||||
const response = await fetch('api_v1_messages.php', {
|
xhr.open('POST', 'api_v1_messages.php', true);
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
xhr.upload.onprogress = (ev) => {
|
||||||
if (result.success) {
|
if (ev.lengthComputable && file) {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
const percent = Math.round((ev.loaded / ev.total) * 100);
|
||||||
ws.send(JSON.stringify({
|
progressBar.style.width = percent + '%';
|
||||||
type: 'message',
|
progressPercent.textContent = percent + '%';
|
||||||
data: JSON.stringify({
|
}
|
||||||
...result.message,
|
};
|
||||||
channel_id: currentChannel
|
|
||||||
})
|
xhr.onload = () => {
|
||||||
}));
|
if (xhr.status === 200) {
|
||||||
|
const result = JSON.parse(xhr.responseText);
|
||||||
|
if (result.success) {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
data: JSON.stringify({
|
||||||
|
...result.message,
|
||||||
|
channel_id: currentChannel
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
appendMessage(result.message);
|
||||||
|
messagesList.scrollTop = messagesList.scrollHeight;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
appendMessage(result.message);
|
alert(result.error || 'Failed to send message');
|
||||||
messagesList.scrollTop = messagesList.scrollHeight;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
progressContainer.style.display = 'none';
|
||||||
console.error('Failed to send message:', err);
|
};
|
||||||
}
|
|
||||||
|
xhr.onerror = () => {
|
||||||
|
console.error('XHR Error');
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
alert('An error occurred during the upload.');
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(formData);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle Reaction Clicks
|
// Handle Reaction Clicks
|
||||||
@ -306,6 +350,61 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pinBtn = e.target.closest('.action-btn.pin');
|
||||||
|
if (pinBtn) {
|
||||||
|
const msgId = pinBtn.dataset.id;
|
||||||
|
const isPinned = pinBtn.dataset.pinned == '1';
|
||||||
|
const action = isPinned ? 'unpin' : 'pin';
|
||||||
|
|
||||||
|
const resp = await fetch('api_v1_messages.php', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: msgId, action: action })
|
||||||
|
});
|
||||||
|
const result = await resp.json();
|
||||||
|
if (result.success) {
|
||||||
|
location.reload(); // Simplest way to reflect changes across UI
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinnedMessagesBtn = document.getElementById('pinned-messages-btn');
|
||||||
|
if (e.target.closest('#pinned-messages-btn')) {
|
||||||
|
const container = document.getElementById('pinned-messages-container');
|
||||||
|
container.innerHTML = '<div class="p-3 text-center text-muted">Loading pinned messages...</div>';
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('pinnedMessagesModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
const resp = await fetch(`api_v1_messages.php?channel_id=${currentChannel}&pinned=1`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success && data.messages.length > 0) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
data.messages.forEach(msg => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'message-item p-2 border-bottom border-secondary';
|
||||||
|
div.style.backgroundColor = 'transparent';
|
||||||
|
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;">
|
||||||
|
${escapeHTML(msg.username)}
|
||||||
|
<span class="message-time">${msg.time}</span>
|
||||||
|
</div>
|
||||||
|
<div class="message-text" style="font-size: 0.9em;">
|
||||||
|
${escapeHTML(msg.content).replace(/\n/g, '<br>')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(div);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
container.innerHTML = '<div class="p-3 text-center text-muted">No pinned messages in this channel.</div>';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Start DM
|
// Start DM
|
||||||
const dmBtn = e.target.closest('.start-dm-btn');
|
const dmBtn = e.target.closest('.start-dm-btn');
|
||||||
if (dmBtn) {
|
if (dmBtn) {
|
||||||
@ -322,31 +421,50 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Global Search
|
// Global Search
|
||||||
const searchInput = document.getElementById('global-search');
|
const searchInput = document.getElementById('global-search');
|
||||||
|
const searchType = document.getElementById('search-type');
|
||||||
const searchResults = document.getElementById('search-results');
|
const searchResults = document.getElementById('search-results');
|
||||||
|
|
||||||
searchInput.addEventListener('input', async () => {
|
searchInput?.addEventListener('input', async () => {
|
||||||
const q = searchInput.value.trim();
|
const q = searchInput.value.trim();
|
||||||
|
const type = searchType.value;
|
||||||
if (q.length < 2) {
|
if (q.length < 2) {
|
||||||
searchResults.style.display = 'none';
|
searchResults.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await fetch(`api_v1_search.php?q=${encodeURIComponent(q)}&channel_id=${currentChannel}`);
|
const resp = await fetch(`api_v1_search.php?q=${encodeURIComponent(q)}&type=${type}&channel_id=${currentChannel}`);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
|
|
||||||
if (data.success && data.results.length > 0) {
|
if (data.success && data.results.length > 0) {
|
||||||
searchResults.innerHTML = '';
|
searchResults.innerHTML = '';
|
||||||
data.results.forEach(res => {
|
data.results.forEach(res => {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'search-result-item';
|
item.className = 'search-result-item d-flex align-items-center gap-2';
|
||||||
item.innerHTML = `
|
if (type === 'users') {
|
||||||
<div class="search-result-author">${res.username}</div>
|
item.innerHTML = `
|
||||||
<div class="search-result-text">${res.content}</div>
|
<div class="message-avatar" style="width: 24px; height: 24px; ${res.avatar_url ? `background-image: url('${res.avatar_url}');` : ''}"></div>
|
||||||
`;
|
<div class="flex-grow-1">
|
||||||
item.onclick = () => {
|
<div class="search-result-author">${res.username}</div>
|
||||||
// Logic to scroll to message would go here
|
<div class="small text-muted" style="font-size: 0.7em;">Click to start conversation</div>
|
||||||
searchResults.style.display = 'none';
|
</div>
|
||||||
};
|
`;
|
||||||
|
item.onclick = () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('user_id', res.id);
|
||||||
|
fetch('api_v1_dms.php', { method: 'POST', body: formData })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(resDM => {
|
||||||
|
if (resDM.success) window.location.href = `?server_id=dms&channel_id=${resDM.channel_id}`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="search-result-author">${res.username}</div>
|
||||||
|
<div class="search-result-text">${res.content}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
searchResults.appendChild(item);
|
searchResults.appendChild(item);
|
||||||
});
|
});
|
||||||
searchResults.style.display = 'block';
|
searchResults.style.display = 'block';
|
||||||
@ -356,6 +474,110 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Channel Permissions Management
|
||||||
|
const channelPermissionsTabBtn = document.getElementById('channel-permissions-tab-btn');
|
||||||
|
const channelPermissionsList = document.getElementById('channel-permissions-list');
|
||||||
|
const addPermRoleList = document.getElementById('add-permission-role-list');
|
||||||
|
|
||||||
|
channelPermissionsTabBtn?.addEventListener('click', async () => {
|
||||||
|
const channelId = document.getElementById('edit-channel-id').value;
|
||||||
|
loadChannelPermissions(channelId);
|
||||||
|
loadRolesForPermissions(channelId);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadChannelPermissions(channelId) {
|
||||||
|
channelPermissionsList.innerHTML = '<div class="text-center p-3 text-muted small">Loading permissions...</div>';
|
||||||
|
const resp = await fetch(`api_v1_channel_permissions.php?channel_id=${channelId}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success) {
|
||||||
|
renderChannelPermissions(channelId, data.permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRolesForPermissions(channelId) {
|
||||||
|
addPermRoleList.innerHTML = '';
|
||||||
|
const resp = await fetch(`api_v1_roles.php?server_id=${activeServerId}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success) {
|
||||||
|
data.roles.forEach(role => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.innerHTML = `<a class="dropdown-item d-flex align-items-center gap-2" href="#">
|
||||||
|
<div style="width: 10px; height: 10px; border-radius: 50%; background-color: ${role.color};"></div>
|
||||||
|
${role.name}
|
||||||
|
</a>`;
|
||||||
|
li.onclick = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await fetch('api_v1_channel_permissions.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ channel_id: channelId, role_id: role.id, allow: 0, deny: 0 })
|
||||||
|
});
|
||||||
|
loadChannelPermissions(channelId);
|
||||||
|
};
|
||||||
|
addPermRoleList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChannelPermissions(channelId, permissions) {
|
||||||
|
channelPermissionsList.innerHTML = '';
|
||||||
|
if (permissions.length === 0) {
|
||||||
|
channelPermissionsList.innerHTML = '<div class="text-center p-3 text-muted small">No role overrides.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
permissions.forEach(p => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'list-group-item bg-transparent text-white border-secondary p-2';
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div style="width: 10px; height: 10px; border-radius: 50%; background-color: ${p.role_color}; margin-right: 8px;"></div>
|
||||||
|
<span class="small fw-bold">${p.role_name}</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm text-danger remove-perm-btn" data-role-id="${p.role_id}">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<select class="form-select form-select-sm bg-dark text-white border-secondary perm-select" data-role-id="${p.role_id}">
|
||||||
|
<option value="allow" ${p.allow_permissions ? 'selected' : ''}>Allow Sending Messages</option>
|
||||||
|
<option value="deny" ${p.deny_permissions ? 'selected' : ''}>Deny Sending Messages</option>
|
||||||
|
<option value="neutral" ${!p.allow_permissions && !p.deny_permissions ? 'selected' : ''}>Neutral</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
channelPermissionsList.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
channelPermissionsList?.addEventListener('click', async (e) => {
|
||||||
|
const channelId = document.getElementById('edit-channel-id').value;
|
||||||
|
if (e.target.classList.contains('remove-perm-btn')) {
|
||||||
|
const roleId = e.target.dataset.roleId;
|
||||||
|
await fetch('api_v1_channel_permissions.php', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ channel_id: channelId, role_id: roleId })
|
||||||
|
});
|
||||||
|
loadChannelPermissions(channelId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
channelPermissionsList?.addEventListener('change', async (e) => {
|
||||||
|
if (e.target.classList.contains('perm-select')) {
|
||||||
|
const channelId = document.getElementById('edit-channel-id').value;
|
||||||
|
const roleId = e.target.dataset.roleId;
|
||||||
|
const val = e.target.value;
|
||||||
|
let allow = 0, deny = 0;
|
||||||
|
if (val === 'allow') allow = 1;
|
||||||
|
if (val === 'deny') deny = 1;
|
||||||
|
|
||||||
|
await fetch('api_v1_channel_permissions.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ channel_id: channelId, role_id: roleId, allow, deny })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (!e.target.closest('.search-container')) {
|
if (!e.target.closest('.search-container')) {
|
||||||
searchResults.style.display = 'none';
|
searchResults.style.display = 'none';
|
||||||
@ -370,6 +592,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
modal.querySelector('#edit-channel-id').value = btn.dataset.id;
|
modal.querySelector('#edit-channel-id').value = btn.dataset.id;
|
||||||
modal.querySelector('#edit-channel-name').value = btn.dataset.name;
|
modal.querySelector('#edit-channel-name').value = btn.dataset.name;
|
||||||
modal.querySelector('#edit-channel-files').checked = btn.dataset.files == '1';
|
modal.querySelector('#edit-channel-files').checked = btn.dataset.files == '1';
|
||||||
|
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 = btn.dataset.id;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -532,6 +755,49 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Stats Management
|
||||||
|
const statsTabBtn = document.getElementById('stats-tab-btn');
|
||||||
|
statsTabBtn?.addEventListener('click', loadStats);
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`api_v1_stats.php?server_id=${activeServerId}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('stat-members').textContent = data.stats.total_members;
|
||||||
|
document.getElementById('stat-messages').textContent = data.stats.total_messages;
|
||||||
|
|
||||||
|
const topUsersList = document.getElementById('top-users-list');
|
||||||
|
topUsersList.innerHTML = '';
|
||||||
|
data.stats.top_users.forEach(user => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'd-flex justify-content-between align-items-center mb-1 p-2 bg-dark rounded';
|
||||||
|
item.innerHTML = `<span>${user.username}</span><span class="badge bg-primary">${user.message_count} msgs</span>`;
|
||||||
|
topUsersList.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activity = document.getElementById('activity-chart-placeholder');
|
||||||
|
activity.innerHTML = '';
|
||||||
|
data.stats.history.forEach(day => {
|
||||||
|
const bar = document.createElement('div');
|
||||||
|
bar.className = 'd-flex align-items-center mb-1';
|
||||||
|
const percent = Math.min(100, (day.count / 100) * 100); // Normalize to 100 for visual
|
||||||
|
bar.innerHTML = `
|
||||||
|
<div style="width: 80px;" class="small">${day.date}</div>
|
||||||
|
<div class="flex-grow-1 mx-2" style="height: 10px; background: #1e1f22; border-radius: 5px;">
|
||||||
|
<div style="width: ${percent}%; height: 100%; background: var(--blurple); border-radius: 5px;"></div>
|
||||||
|
</div>
|
||||||
|
<div style="width: 30px;" class="small text-end">${day.count}</div>
|
||||||
|
`;
|
||||||
|
activity.appendChild(bar);
|
||||||
|
});
|
||||||
|
if (data.stats.history.length === 0) {
|
||||||
|
activity.innerHTML = '<div class="text-muted">No activity in the last 7 days.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
// Server Settings
|
// Server Settings
|
||||||
const searchServerIconBtn = document.getElementById('search-server-icon-btn');
|
const searchServerIconBtn = document.getElementById('search-server-icon-btn');
|
||||||
const serverIconResults = document.getElementById('server-icon-search-results');
|
const serverIconResults = document.getElementById('server-icon-search-results');
|
||||||
@ -564,67 +830,153 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
serverIconResults.innerHTML = '<div class="text-danger small">Error fetching icons</div>';
|
serverIconResults.innerHTML = '<div class="text-danger small">Error fetching icons</div>';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// User Settings - Avatar Search
|
||||||
|
const avatarSearchBtn = document.getElementById('search-avatar-btn');
|
||||||
|
const avatarSearchQuery = document.getElementById('avatar-search-query');
|
||||||
|
const avatarResults = document.getElementById('avatar-results');
|
||||||
|
const avatarPreview = document.getElementById('settings-avatar-preview');
|
||||||
|
const avatarUrlInput = document.getElementById('settings-avatar-url');
|
||||||
|
|
||||||
|
avatarSearchBtn?.addEventListener('click', async () => {
|
||||||
|
const q = avatarSearchQuery.value.trim();
|
||||||
|
if (!q) return;
|
||||||
|
avatarResults.innerHTML = '<div class="text-muted small">Searching...</div>';
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`api/pexels.php?action=search&query=${encodeURIComponent(q)}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
avatarResults.innerHTML = '';
|
||||||
|
data.forEach(photo => {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = photo.url;
|
||||||
|
img.className = 'avatar-pick';
|
||||||
|
img.style.width = '60px';
|
||||||
|
img.style.height = '60px';
|
||||||
|
img.style.cursor = 'pointer';
|
||||||
|
img.onclick = () => {
|
||||||
|
avatarUrlInput.value = photo.url;
|
||||||
|
avatarPreview.style.backgroundImage = `url('${photo.url}')`;
|
||||||
|
};
|
||||||
|
avatarResults.appendChild(img);
|
||||||
|
});
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// User Settings - Save
|
||||||
|
const saveSettingsBtn = document.getElementById('save-settings-btn');
|
||||||
|
saveSettingsBtn?.addEventListener('click', async () => {
|
||||||
|
const form = document.getElementById('user-settings-form');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const dndMode = document.getElementById('dnd-switch').checked ? '1' : '0';
|
||||||
|
formData.append('dnd_mode', dndMode);
|
||||||
|
|
||||||
|
const theme = form.querySelector('input[name="theme"]:checked').value;
|
||||||
|
document.body.setAttribute('data-theme', theme);
|
||||||
|
|
||||||
|
const resp = await fetch('api_v1_user.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const result = await resp.json();
|
||||||
|
if (result.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(result.error || 'Failed to save settings');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function appendMessage(msg) {
|
function escapeHTML(str) {
|
||||||
const messagesList = document.getElementById('messages-list');
|
const div = document.createElement('div');
|
||||||
const div = document.createElement('div');
|
div.textContent = str;
|
||||||
div.className = 'message-item';
|
return div.innerHTML;
|
||||||
div.dataset.id = msg.id;
|
|
||||||
const avatarStyle = msg.avatar_url ? `background-image: url('${msg.avatar_url}');` : '';
|
|
||||||
|
|
||||||
let attachmentHtml = '';
|
|
||||||
if (msg.attachment_url) {
|
|
||||||
const ext = msg.attachment_url.split('.').pop().toLowerCase();
|
|
||||||
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) {
|
|
||||||
attachmentHtml = `<div class="message-attachment mt-2"><img src="${msg.attachment_url}" class="img-fluid rounded message-img-preview" alt="Attachment" style="max-height: 300px; cursor: pointer;" onclick="window.open(this.src)"></div>`;
|
|
||||||
} else {
|
|
||||||
attachmentHtml = `<div class="message-attachment mt-2"><a href="${msg.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>${msg.attachment_url.split('/').pop()}</a></div>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let embedHtml = '';
|
function appendMessage(msg) {
|
||||||
if (msg.metadata) {
|
const messagesList = document.getElementById('messages-list');
|
||||||
const meta = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata;
|
const div = document.createElement('div');
|
||||||
embedHtml = `
|
div.className = 'message-item';
|
||||||
<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;">
|
div.dataset.id = msg.id;
|
||||||
${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;">${meta.site_name}</div>` : ''}
|
const avatarStyle = msg.avatar_url ? `background-image: url('${msg.avatar_url}');` : '';
|
||||||
${meta.title ? `<a href="${meta.url}" target="_blank" class="embed-title d-block mb-1 text-decoration-none" style="font-weight: 600; color: #00a8fc;">${meta.title}</a>` : ''}
|
|
||||||
${meta.description ? `<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);">${meta.description}</div>` : ''}
|
let attachmentHtml = '';
|
||||||
${meta.image ? `<div class="embed-image"><img src="${meta.image}" class="rounded" style="max-width: 100%; max-height: 300px; object-fit: contain;"></div>` : ''}
|
if (msg.attachment_url) {
|
||||||
|
const ext = msg.attachment_url.split('.').pop().toLowerCase();
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) {
|
||||||
|
attachmentHtml = `<div class="message-attachment mt-2"><img src="${msg.attachment_url}" class="img-fluid rounded message-img-preview" alt="Attachment" style="max-height: 300px; cursor: pointer;" onclick="window.open(this.src)"></div>`;
|
||||||
|
} else {
|
||||||
|
attachmentHtml = `<div class="message-attachment mt-2"><a href="${msg.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>${msg.attachment_url.split('/').pop()}</a></div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let embedHtml = '';
|
||||||
|
if (msg.metadata) {
|
||||||
|
const meta = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata;
|
||||||
|
embedHtml = `
|
||||||
|
<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;">
|
||||||
|
${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;">${escapeHTML(meta.site_name)}</div>` : ''}
|
||||||
|
${meta.title ? `<a href="${meta.url}" target="_blank" class="embed-title d-block mb-1 text-decoration-none" style="font-weight: 600; color: #00a8fc;">${escapeHTML(meta.title)}</a>` : ''}
|
||||||
|
${meta.description ? `<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);">${escapeHTML(meta.description)}</div>` : ''}
|
||||||
|
${meta.image ? `<div class="embed-image"><img src="${meta.image}" class="rounded" style="max-width: 100%; max-height: 300px; object-fit: contain;"></div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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'}">
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const actionsHtml = (isMe || isOwner) ? `
|
||||||
|
<div class="message-actions-menu">
|
||||||
|
${pinHtml}
|
||||||
|
${isMe ? `
|
||||||
|
<span class="action-btn edit" title="Edit" data-id="${msg.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="${msg.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>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
const pinnedBadge = msg.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>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
const mentionRegex = new RegExp(`@${window.currentUsername}\\b`, 'g');
|
||||||
|
if (msg.content.match(mentionRegex)) {
|
||||||
|
div.classList.add('mentioned');
|
||||||
|
}
|
||||||
|
if (msg.is_pinned) div.classList.add('pinned');
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="message-avatar" style="${avatarStyle}"></div>
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message-author">
|
||||||
|
${escapeHTML(msg.username)}
|
||||||
|
<span class="message-time">${msg.time}</span>
|
||||||
|
${pinnedBadge}
|
||||||
|
${actionsHtml}
|
||||||
|
</div>
|
||||||
|
<div class="message-text">
|
||||||
|
${escapeHTML(msg.content).replace(/\n/g, '<br>').replace(mentionRegex, `<span class="mention">@${window.currentUsername}</span>`)}
|
||||||
|
${attachmentHtml}
|
||||||
|
${embedHtml}
|
||||||
|
</div>
|
||||||
|
<div class="message-reactions mt-1" data-message-id="${msg.id}">
|
||||||
|
<span class="add-reaction-btn" title="Add Reaction">+</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
messagesList.appendChild(div);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMe = msg.user_id == window.currentUserId || msg.username == window.currentUsername;
|
|
||||||
const actionsHtml = isMe ? `
|
|
||||||
<div class="message-actions-menu">
|
|
||||||
<span class="action-btn edit" title="Edit" data-id="${msg.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="${msg.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>
|
|
||||||
</div>
|
|
||||||
` : '';
|
|
||||||
|
|
||||||
div.innerHTML = `
|
|
||||||
<div class="message-avatar" style="${avatarStyle}"></div>
|
|
||||||
<div class="message-content">
|
|
||||||
<div class="message-author">
|
|
||||||
${msg.username}
|
|
||||||
<span class="message-time">${msg.time}</span>
|
|
||||||
${actionsHtml}
|
|
||||||
</div>
|
|
||||||
<div class="message-text">
|
|
||||||
${msg.content.replace(/\n/g, '<br>')}
|
|
||||||
${attachmentHtml}
|
|
||||||
${embedHtml}
|
|
||||||
</div>
|
|
||||||
<div class="message-reactions mt-1" data-message-id="${msg.id}">
|
|
||||||
<span class="add-reaction-btn" title="Add Reaction">+</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
messagesList.appendChild(div);
|
|
||||||
}
|
|
||||||
|
|||||||
12
db/migrations/20260215_granular_roles_and_themes.sql
Normal file
12
db/migrations/20260215_granular_roles_and_themes.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
-- Migration: Add channel permissions and user theme preference
|
||||||
|
CREATE TABLE IF NOT EXISTS channel_permissions (
|
||||||
|
channel_id INT NOT NULL,
|
||||||
|
role_id INT NOT NULL,
|
||||||
|
allow_permissions INT DEFAULT 0,
|
||||||
|
deny_permissions INT DEFAULT 0,
|
||||||
|
PRIMARY KEY (channel_id, role_id),
|
||||||
|
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS theme VARCHAR(20) DEFAULT 'dark';
|
||||||
@ -9,6 +9,11 @@ class Permissions {
|
|||||||
const ADMINISTRATOR = 32;
|
const ADMINISTRATOR = 32;
|
||||||
|
|
||||||
public static function hasPermission($user_id, $server_id, $permission) {
|
public static function hasPermission($user_id, $server_id, $permission) {
|
||||||
|
$stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
|
||||||
|
$stmt->execute([$server_id]);
|
||||||
|
$server = $stmt->fetch();
|
||||||
|
if ($server && $server['owner_id'] == $user_id) return true;
|
||||||
|
|
||||||
$stmt = db()->prepare("
|
$stmt = db()->prepare("
|
||||||
SELECT SUM(r.permissions) as total_perms
|
SELECT SUM(r.permissions) as total_perms
|
||||||
FROM roles r
|
FROM roles r
|
||||||
@ -22,4 +27,35 @@ class Permissions {
|
|||||||
if ($perms & self::ADMINISTRATOR) return true;
|
if ($perms & self::ADMINISTRATOR) return true;
|
||||||
return ($perms & $permission) === $permission;
|
return ($perms & $permission) === $permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function canSendInChannel($user_id, $channel_id) {
|
||||||
|
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
|
||||||
|
$stmt->execute([$channel_id]);
|
||||||
|
$c = $stmt->fetch();
|
||||||
|
if (!$c) return false;
|
||||||
|
$server_id = $c['server_id'];
|
||||||
|
|
||||||
|
// Check if owner
|
||||||
|
$stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
|
||||||
|
$stmt->execute([$server_id]);
|
||||||
|
$s = $stmt->fetch();
|
||||||
|
if ($s && $s['owner_id'] == $user_id) return true;
|
||||||
|
|
||||||
|
// Check overrides
|
||||||
|
$stmt = db()->prepare("
|
||||||
|
SELECT cp.allow_permissions, cp.deny_permissions
|
||||||
|
FROM channel_permissions cp
|
||||||
|
JOIN user_roles ur ON cp.role_id = ur.role_id
|
||||||
|
WHERE ur.user_id = ? AND cp.channel_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$user_id, $channel_id]);
|
||||||
|
$overrides = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach($overrides as $o) {
|
||||||
|
if ($o['deny_permissions'] & 1) return false; // Bit 1 for SEND_MESSAGES in overrides
|
||||||
|
if ($o['allow_permissions'] & 1) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::hasPermission($user_id, $server_id, self::SEND_MESSAGES);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
247
index.php
247
index.php
@ -31,6 +31,7 @@ if ($is_dm_view) {
|
|||||||
$dm_channels = $stmt->fetchAll();
|
$dm_channels = $stmt->fetchAll();
|
||||||
|
|
||||||
$active_channel_id = $_GET['channel_id'] ?? ($dm_channels[0]['id'] ?? 0);
|
$active_channel_id = $_GET['channel_id'] ?? ($dm_channels[0]['id'] ?? 0);
|
||||||
|
$channel_theme = null; // DMs don't have custom themes for now
|
||||||
|
|
||||||
if ($active_channel_id) {
|
if ($active_channel_id) {
|
||||||
// Fetch DM messages
|
// Fetch DM messages
|
||||||
@ -67,6 +68,16 @@ if ($is_dm_view) {
|
|||||||
$channels = $stmt->fetchAll();
|
$channels = $stmt->fetchAll();
|
||||||
$active_channel_id = $_GET['channel_id'] ?? ($channels[0]['id'] ?? 1);
|
$active_channel_id = $_GET['channel_id'] ?? ($channels[0]['id'] ?? 1);
|
||||||
|
|
||||||
|
// Fetch active channel details for theme
|
||||||
|
$active_channel = null;
|
||||||
|
foreach($channels as $c) {
|
||||||
|
if($c['id'] == $active_channel_id) {
|
||||||
|
$active_channel = $c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$channel_theme = $active_channel['theme_color'] ?? null;
|
||||||
|
|
||||||
// Fetch messages
|
// Fetch messages
|
||||||
$stmt = db()->prepare("
|
$stmt = db()->prepare("
|
||||||
SELECT m.*, u.username, u.avatar_url
|
SELECT m.*, u.username, u.avatar_url
|
||||||
@ -115,8 +126,30 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="assets/css/discord.css?v=<?php echo time(); ?>">
|
<link rel="stylesheet" href="assets/css/discord.css?v=<?php echo time(); ?>">
|
||||||
|
<script>
|
||||||
|
window.currentUserId = <?php echo $current_user_id; ?>;
|
||||||
|
window.currentUsername = "<?php echo addslashes($user['username']); ?>";
|
||||||
|
window.isServerOwner = <?php echo ($is_owner ?? false) ? 'true' : 'false'; ?>;
|
||||||
|
window.currentChannelName = "<?php echo addslashes($current_channel_name); ?>";
|
||||||
|
window.isDndMode = <?php echo ($user['dnd_mode'] ?? 0) ? 'true' : 'false'; ?>;
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
<?php if ($channel_theme): ?>
|
||||||
|
--blurple: <?php echo $channel_theme; ?>;
|
||||||
|
<?php endif; ?>
|
||||||
|
}
|
||||||
|
<?php if ($channel_theme): ?>
|
||||||
|
.mention {
|
||||||
|
background-color: <?php echo $channel_theme; ?>4D; /* 30% opacity */
|
||||||
|
}
|
||||||
|
.mention:hover {
|
||||||
|
background-color: <?php echo $channel_theme; ?>;
|
||||||
|
}
|
||||||
|
<?php endif; ?>
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-theme="<?php echo htmlspecialchars($user['theme'] ?? 'dark'); ?>">
|
||||||
|
|
||||||
<div class="discord-app">
|
<div class="discord-app">
|
||||||
<!-- Servers Sidebar -->
|
<!-- Servers Sidebar -->
|
||||||
@ -190,7 +223,8 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
data-bs-toggle="modal" data-bs-target="#editChannelModal"
|
data-bs-toggle="modal" data-bs-target="#editChannelModal"
|
||||||
data-id="<?php echo $c['id']; ?>"
|
data-id="<?php echo $c['id']; ?>"
|
||||||
data-name="<?php echo htmlspecialchars($c['name']); ?>"
|
data-name="<?php echo htmlspecialchars($c['name']); ?>"
|
||||||
data-files="<?php echo $c['allow_file_sharing']; ?>">
|
data-files="<?php echo $c['allow_file_sharing']; ?>"
|
||||||
|
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>
|
<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>
|
</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@ -211,7 +245,8 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
data-bs-toggle="modal" data-bs-target="#editChannelModal"
|
data-bs-toggle="modal" data-bs-target="#editChannelModal"
|
||||||
data-id="<?php echo $c['id']; ?>"
|
data-id="<?php echo $c['id']; ?>"
|
||||||
data-name="<?php echo htmlspecialchars($c['name']); ?>"
|
data-name="<?php echo htmlspecialchars($c['name']); ?>"
|
||||||
data-files="<?php echo $c['allow_file_sharing']; ?>">
|
data-files="<?php echo $c['allow_file_sharing']; ?>"
|
||||||
|
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>
|
<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>
|
</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@ -245,11 +280,22 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
<span style="color: var(--text-muted); margin-right: 8px;"><?php echo $is_dm_view ? '@' : '#'; ?></span>
|
<span style="color: var(--text-muted); margin-right: 8px;"><?php echo $is_dm_view ? '@' : '#'; ?></span>
|
||||||
<?php echo htmlspecialchars($current_channel_name); ?>
|
<span class="flex-grow-1"><?php echo htmlspecialchars($current_channel_name); ?></span>
|
||||||
|
|
||||||
<div class="search-container">
|
<div class="d-flex align-items-center">
|
||||||
<input type="text" id="global-search" class="search-input" placeholder="Search messages..." autocomplete="off">
|
<button id="pinned-messages-btn" class="btn btn-link text-muted p-1 me-2" title="Pinned Messages">
|
||||||
<div id="search-results" class="search-results-dropdown"></div>
|
<svg width="20" height="20" 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>
|
||||||
|
</button>
|
||||||
|
<div class="search-container">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<select id="search-type" class="form-select bg-dark text-muted border-0" style="width: auto; max-width: 80px; font-size: 0.7em;">
|
||||||
|
<option value="messages">Chat</option>
|
||||||
|
<option value="users">Users</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="global-search" class="search-input" placeholder="Search..." autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div id="search-results" class="search-results-dropdown"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="messages-list" id="messages-list">
|
<div class="messages-list" id="messages-list">
|
||||||
@ -259,26 +305,44 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
<p>This is the start of the #<?php echo htmlspecialchars($current_channel_name); ?> channel.</p>
|
<p>This is the start of the #<?php echo htmlspecialchars($current_channel_name); ?> channel.</p>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php foreach($messages as $m): ?>
|
<?php foreach($messages as $m):
|
||||||
<div class="message-item" data-id="<?php echo $m['id']; ?>">
|
$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-avatar" style="<?php echo $m['avatar_url'] ? "background-image: url('{$m['avatar_url']}');" : ""; ?>"></div>
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
<div class="message-author">
|
<div class="message-author">
|
||||||
<?php echo htmlspecialchars($m['username']); ?>
|
<?php echo htmlspecialchars($m['username']); ?>
|
||||||
<span class="message-time"><?php echo date('H:i', strtotime($m['created_at'])); ?></span>
|
<span class="message-time"><?php echo date('H:i', strtotime($m['created_at'])); ?></span>
|
||||||
<?php if ($m['user_id'] == $current_user_id): ?>
|
<?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">
|
<div class="message-actions-menu">
|
||||||
<span class="action-btn edit" title="Edit" data-id="<?php echo $m['id']; ?>">
|
<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="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>
|
<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>
|
|
||||||
<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>
|
</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>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-text">
|
<div class="message-text">
|
||||||
<?php echo nl2br(htmlspecialchars($m['content'])); ?>
|
<?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']): ?>
|
<?php if ($m['attachment_url']): ?>
|
||||||
<div class="message-attachment mt-2">
|
<div class="message-attachment mt-2">
|
||||||
<?php
|
<?php
|
||||||
@ -347,6 +411,15 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
<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;">
|
||||||
|
<div id="upload-progress-bar" class="progress-bar" role="progressbar" style="width: 0%; background-color: var(--blurple);"></div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mt-1">
|
||||||
|
<span id="upload-filename" class="small text-muted" style="font-size: 0.7em;">Uploading...</span>
|
||||||
|
<span id="upload-percentage" class="small text-muted" style="font-size: 0.7em;">0%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form id="chat-form" enctype="multipart/form-data">
|
<form id="chat-form" enctype="multipart/form-data">
|
||||||
<div class="chat-input-wrapper">
|
<div class="chat-input-wrapper">
|
||||||
<?php if ($allow_files): ?>
|
<?php if ($allow_files): ?>
|
||||||
@ -406,6 +479,23 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Username</label>
|
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Username</label>
|
||||||
<input type="text" name="username" class="form-control" value="<?php echo htmlspecialchars($user['username']); ?>" required>
|
<input type="text" name="username" class="form-control" value="<?php echo htmlspecialchars($user['username']); ?>" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Settings</label>
|
||||||
|
<div class="form-check form-switch mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox" name="dnd_mode" id="dnd-switch" value="1" <?php echo ($user['dnd_mode'] ?? 0) ? 'checked' : ''; ?>>
|
||||||
|
<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="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">
|
||||||
|
<input type="radio" class="btn-check" name="theme" id="theme-dark" value="dark" <?php echo ($user['theme'] ?? 'dark') == 'dark' ? 'checked' : ''; ?>>
|
||||||
|
<label class="btn btn-outline-secondary btn-sm" for="theme-dark">Dark</label>
|
||||||
|
<input type="radio" class="btn-check" name="theme" id="theme-light" value="light" <?php echo ($user['theme'] ?? 'dark') == 'light' ? 'checked' : ''; ?>>
|
||||||
|
<label class="btn btn-outline-secondary btn-sm" for="theme-light">Light</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Search Avatars</label>
|
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Search Avatars</label>
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
@ -447,6 +537,9 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
<li class="nav-item">
|
<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>
|
<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>
|
||||||
|
<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>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content p-3">
|
<div class="tab-content p-3">
|
||||||
<div class="tab-pane fade show active" id="settings-general">
|
<div class="tab-pane fade show active" id="settings-general">
|
||||||
@ -511,6 +604,33 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
<!-- Webhooks will be loaded here -->
|
<!-- Webhooks will be loaded here -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-pane fade" id="settings-stats">
|
||||||
|
<div id="stats-content">
|
||||||
|
<div class="row text-center mb-4">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="p-3 rounded bg-dark">
|
||||||
|
<div class="small text-muted text-uppercase">Members</div>
|
||||||
|
<div class="h4 mb-0" id="stat-members">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="p-3 rounded bg-dark">
|
||||||
|
<div class="small text-muted text-uppercase">Messages</div>
|
||||||
|
<div class="h4 mb-0" id="stat-messages">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h6 class="text-uppercase small text-muted mb-2">Top Active Users</h6>
|
||||||
|
<div id="top-users-list" class="mb-4">
|
||||||
|
<!-- Top users here -->
|
||||||
|
</div>
|
||||||
|
<h6 class="text-uppercase small text-muted mb-2">Activity (Last 7 Days)</h6>
|
||||||
|
<div id="activity-chart-placeholder" class="small text-muted text-center p-4 border border-secondary rounded">
|
||||||
|
<!-- Simplistic chart or list -->
|
||||||
|
Loading activity...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -585,6 +705,10 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
<input class="form-check-input" type="checkbox" name="allow_file_sharing" id="add-channel-files" value="1" checked>
|
<input class="form-check-input" type="checkbox" name="allow_file_sharing" id="add-channel-files" value="1" checked>
|
||||||
<label class="form-check-label text-white" for="add-channel-files">Allow File Sharing</label>
|
<label class="form-check-label text-white" for="add-channel-files">Allow File Sharing</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Theme Color</label>
|
||||||
|
<input type="color" name="theme_color" class="form-control form-control-color w-100" value="#5865f2" title="Choose channel theme color">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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" class="btn btn-link text-white text-decoration-none" data-bs-dismiss="modal">Cancel</button>
|
||||||
@ -603,31 +727,77 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
<h5 class="modal-title">Channel Settings</h5>
|
<h5 class="modal-title">Channel Settings</h5>
|
||||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body p-0">
|
||||||
<form action="api_v1_channels.php" method="POST">
|
<ul class="nav nav-tabs nav-fill" id="editChannelTabs" role="tablist">
|
||||||
<input type="hidden" name="action" value="update">
|
<li class="nav-item">
|
||||||
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
|
<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>
|
||||||
<input type="hidden" name="channel_id" id="edit-channel-id">
|
</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>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content p-3">
|
||||||
|
<div class="tab-pane fade show active" id="edit-channel-general">
|
||||||
|
<form action="api_v1_channels.php" method="POST">
|
||||||
|
<input type="hidden" name="action" value="update">
|
||||||
|
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
|
||||||
|
<input type="hidden" name="channel_id" id="edit-channel-id">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Channel Name</label>
|
<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>
|
<input type="text" name="name" id="edit-channel-name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check form-switch mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" name="allow_file_sharing" id="edit-channel-files" value="1">
|
||||||
|
<label class="form-check-label text-white" for="edit-channel-files">Allow File Sharing</label>
|
||||||
|
<div class="form-text text-muted" style="font-size: 0.8em;">When disabled, users cannot upload files in this channel.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Theme Color</label>
|
||||||
|
<input type="color" name="theme_color" id="edit-channel-theme" class="form-control form-control-color w-100" value="#5865f2" title="Choose channel theme color">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-100 mb-2">Save Changes</button>
|
||||||
|
</form>
|
||||||
|
<form action="api_v1_channels.php" method="POST" onsubmit="return confirm('Are you sure you want to delete this channel?');">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
|
||||||
|
<input type="hidden" name="channel_id" id="delete-channel-id">
|
||||||
|
<button type="submit" class="btn btn-danger w-100">Delete Channel</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-pane fade" id="edit-channel-permissions">
|
||||||
<div class="form-check form-switch mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<input class="form-check-input" type="checkbox" name="allow_file_sharing" id="edit-channel-files" value="1">
|
<h6 class="mb-0">Role Overrides</h6>
|
||||||
<label class="form-check-label text-white" for="edit-channel-files">Allow File Sharing</label>
|
<div class="dropdown">
|
||||||
<div class="form-text text-muted" style="font-size: 0.8em;">When disabled, users cannot upload files in this channel.</div>
|
<button class="btn btn-sm btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">Add Role</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-dark" id="add-permission-role-list">
|
||||||
|
<!-- Server roles loaded here -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="channel-permissions-list" class="list-group list-group-flush bg-transparent">
|
||||||
|
<!-- Channel permissions loaded here -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary w-100 mb-2">Save Changes</button>
|
<!-- Pinned Messages Modal -->
|
||||||
</form>
|
<div class="modal fade" id="pinnedMessagesModal" tabindex="-1">
|
||||||
<form action="api_v1_channels.php" method="POST" onsubmit="return confirm('Are you sure you want to delete this channel?');">
|
<div class="modal-dialog modal-md">
|
||||||
<input type="hidden" name="action" value="delete">
|
<div class="modal-content">
|
||||||
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
|
<div class="modal-header">
|
||||||
<input type="hidden" name="channel_id" id="delete-channel-id">
|
<h5 class="modal-title">Pinned Messages</h5>
|
||||||
<button type="submit" class="btn btn-danger w-100">Delete Channel</button>
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
</form>
|
</div>
|
||||||
|
<div class="modal-body p-0" id="pinned-messages-container" style="max-height: 500px; overflow-y: auto; background-color: var(--bg-chat);">
|
||||||
|
<div class="p-3 text-center text-muted">No pinned messages yet.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -637,6 +807,9 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
<script>
|
<script>
|
||||||
window.currentUserId = <?php echo $current_user_id; ?>;
|
window.currentUserId = <?php echo $current_user_id; ?>;
|
||||||
window.currentUsername = "<?php echo addslashes($user['username']); ?>";
|
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.isDndMode = <?php echo ($user['dnd_mode'] ?? 0) ? 'true' : 'false'; ?>;
|
||||||
</script>
|
</script>
|
||||||
<script src="assets/js/voice.js?v=<?php echo time(); ?>"></script>
|
<script src="assets/js/voice.js?v=<?php echo time(); ?>"></script>
|
||||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user