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;
|
||||
$name = $_POST['name'] ?? '';
|
||||
$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
|
||||
$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) {
|
||||
$name = strtolower(preg_replace('/[^a-zA-Z0-9\-]/', '-', $name));
|
||||
$stmt = db()->prepare("UPDATE channels SET name = ?, allow_file_sharing = ? WHERE id = ?");
|
||||
$stmt->execute([$name, $allow_file_sharing, $channel_id]);
|
||||
$stmt = db()->prepare("UPDATE channels SET name = ?, allow_file_sharing = ?, theme_color = ? WHERE id = ?");
|
||||
$stmt->execute([$name, $allow_file_sharing, $theme_color, $channel_id]);
|
||||
}
|
||||
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
|
||||
exit;
|
||||
@ -67,9 +69,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Basic sanitization for channel name
|
||||
$name = strtolower(preg_replace('/[^a-zA-Z0-9\-]/', '-', $name));
|
||||
$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->execute([$server_id, $name, $type, $allow_file_sharing]);
|
||||
$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, $theme_color]);
|
||||
$channel_id = db()->lastInsertId();
|
||||
|
||||
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 'includes/opengraph.php';
|
||||
require_once 'includes/ai_filtering.php';
|
||||
require_once 'includes/permissions.php';
|
||||
|
||||
// Check for Bot token in headers
|
||||
$headers = getallheaders();
|
||||
@ -31,17 +32,59 @@ if ($bot_token) {
|
||||
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') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$message_id = $data['id'] ?? 0;
|
||||
$content = $data['content'] ?? '';
|
||||
|
||||
if (empty($content)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Content cannot be empty']);
|
||||
exit;
|
||||
}
|
||||
$action = $data['action'] ?? 'edit';
|
||||
|
||||
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->execute([$content, $message_id, $user_id]);
|
||||
|
||||
@ -115,6 +158,12 @@ if (empty($content) && empty($attachment_url)) {
|
||||
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)) {
|
||||
$moderation = moderateContent($content);
|
||||
if (!$moderation['is_safe']) {
|
||||
|
||||
@ -5,6 +5,7 @@ requireLogin();
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$query = $_GET['q'] ?? '';
|
||||
$type = $_GET['type'] ?? 'messages'; // messages or users
|
||||
$channel_id = $_GET['channel_id'] ?? 0;
|
||||
|
||||
if (empty($query)) {
|
||||
@ -13,31 +14,42 @@ if (empty($query)) {
|
||||
}
|
||||
|
||||
try {
|
||||
$sql = "SELECT m.*, u.username, u.avatar_url
|
||||
FROM messages m
|
||||
JOIN users u ON m.user_id = u.id
|
||||
WHERE m.content LIKE ? ";
|
||||
$params = ["%" . $query . "%"];
|
||||
|
||||
if ($channel_id > 0) {
|
||||
$sql .= " AND m.channel_id = ?";
|
||||
$params[] = $channel_id;
|
||||
if ($type === 'users') {
|
||||
$stmt = db()->prepare("
|
||||
SELECT id, username, avatar_url, status
|
||||
FROM users
|
||||
WHERE username LIKE ?
|
||||
LIMIT 20
|
||||
");
|
||||
$stmt->execute(["%" . $query . "%"]);
|
||||
$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 = "SELECT m.*, u.username, u.avatar_url
|
||||
FROM messages m
|
||||
JOIN users u ON m.user_id = u.id
|
||||
WHERE m.content LIKE ? ";
|
||||
$params = ["%" . $query . "%"];
|
||||
|
||||
$sql .= " ORDER BY m.created_at DESC LIMIT 50";
|
||||
$stmt = db()->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$results = $stmt->fetchAll();
|
||||
if ($channel_id > 0) {
|
||||
$sql .= " AND m.channel_id = ?";
|
||||
$params[] = $channel_id;
|
||||
} 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]);
|
||||
} 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'];
|
||||
$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 {
|
||||
$stmt = db()->prepare("UPDATE users SET username = ?, avatar_url = ? WHERE id = ?");
|
||||
$stmt->execute([$username, $avatar_url, $user['id']]);
|
||||
$stmt = db()->prepare("UPDATE users SET username = ?, avatar_url = ?, dnd_mode = ?, theme = ? WHERE id = ?");
|
||||
$stmt->execute([$username, $avatar_url, $dnd_mode, $theme, $user['id']]);
|
||||
|
||||
$_SESSION['username'] = $username; // Update session if stored (though getCurrentUser fetches from DB)
|
||||
|
||||
|
||||
@ -10,6 +10,75 @@
|
||||
--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 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@ -663,3 +732,56 @@ body {
|
||||
.channel-settings-btn:hover {
|
||||
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;
|
||||
let typingTimeout;
|
||||
|
||||
// Notification Permission
|
||||
if ("Notification" in window && Notification.permission === "default") {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
|
||||
// WebSocket for real-time
|
||||
let ws;
|
||||
let voiceHandler;
|
||||
@ -40,6 +45,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (data.channel_id == currentChannel) {
|
||||
appendMessage(data);
|
||||
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') {
|
||||
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();
|
||||
const content = chatInput.value.trim();
|
||||
const file = fileUpload.files[0];
|
||||
@ -99,35 +114,64 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('content', content);
|
||||
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) {
|
||||
formData.append('file', file);
|
||||
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 response = await fetch('api_v1_messages.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', 'api_v1_messages.php', true);
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'message',
|
||||
data: JSON.stringify({
|
||||
...result.message,
|
||||
channel_id: currentChannel
|
||||
})
|
||||
}));
|
||||
xhr.upload.onprogress = (ev) => {
|
||||
if (ev.lengthComputable && file) {
|
||||
const percent = Math.round((ev.loaded / ev.total) * 100);
|
||||
progressBar.style.width = percent + '%';
|
||||
progressPercent.textContent = percent + '%';
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
appendMessage(result.message);
|
||||
messagesList.scrollTop = messagesList.scrollHeight;
|
||||
alert(result.error || 'Failed to send message');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to send message:', err);
|
||||
}
|
||||
progressContainer.style.display = 'none';
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
console.error('XHR Error');
|
||||
progressContainer.style.display = 'none';
|
||||
alert('An error occurred during the upload.');
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
|
||||
// Handle Reaction Clicks
|
||||
@ -305,6 +349,61 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
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
|
||||
const dmBtn = e.target.closest('.start-dm-btn');
|
||||
@ -322,31 +421,50 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Global Search
|
||||
const searchInput = document.getElementById('global-search');
|
||||
const searchType = document.getElementById('search-type');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
|
||||
searchInput.addEventListener('input', async () => {
|
||||
searchInput?.addEventListener('input', async () => {
|
||||
const q = searchInput.value.trim();
|
||||
const type = searchType.value;
|
||||
if (q.length < 2) {
|
||||
searchResults.style.display = 'none';
|
||||
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();
|
||||
|
||||
if (data.success && data.results.length > 0) {
|
||||
searchResults.innerHTML = '';
|
||||
data.results.forEach(res => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'search-result-item';
|
||||
item.innerHTML = `
|
||||
<div class="search-result-author">${res.username}</div>
|
||||
<div class="search-result-text">${res.content}</div>
|
||||
`;
|
||||
item.onclick = () => {
|
||||
// Logic to scroll to message would go here
|
||||
searchResults.style.display = 'none';
|
||||
};
|
||||
item.className = 'search-result-item d-flex align-items-center gap-2';
|
||||
if (type === 'users') {
|
||||
item.innerHTML = `
|
||||
<div class="message-avatar" style="width: 24px; height: 24px; ${res.avatar_url ? `background-image: url('${res.avatar_url}');` : ''}"></div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="search-result-author">${res.username}</div>
|
||||
<div class="small text-muted" style="font-size: 0.7em;">Click to start conversation</div>
|
||||
</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.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) => {
|
||||
if (!e.target.closest('.search-container')) {
|
||||
searchResults.style.display = 'none';
|
||||
@ -370,6 +592,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
modal.querySelector('#edit-channel-id').value = btn.dataset.id;
|
||||
modal.querySelector('#edit-channel-name').value = btn.dataset.name;
|
||||
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;
|
||||
});
|
||||
});
|
||||
@ -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
|
||||
const searchServerIconBtn = document.getElementById('search-server-icon-btn');
|
||||
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>';
|
||||
}
|
||||
});
|
||||
|
||||
// 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) {
|
||||
const messagesList = document.getElementById('messages-list');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message-item';
|
||||
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>`;
|
||||
}
|
||||
function escapeHTML(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
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;">${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;">${meta.title}</a>` : ''}
|
||||
${meta.description ? `<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);">${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>` : ''}
|
||||
function appendMessage(msg) {
|
||||
const messagesList = document.getElementById('messages-list');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message-item';
|
||||
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 = '';
|
||||
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>
|
||||
`;
|
||||
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;
|
||||
|
||||
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("
|
||||
SELECT SUM(r.permissions) as total_perms
|
||||
FROM roles r
|
||||
@ -22,4 +27,35 @@ class Permissions {
|
||||
if ($perms & self::ADMINISTRATOR) return true;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
251
index.php
251
index.php
@ -31,6 +31,7 @@ if ($is_dm_view) {
|
||||
$dm_channels = $stmt->fetchAll();
|
||||
|
||||
$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) {
|
||||
// Fetch DM messages
|
||||
@ -67,6 +68,16 @@ if ($is_dm_view) {
|
||||
$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) {
|
||||
if($c['id'] == $active_channel_id) {
|
||||
$active_channel = $c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$channel_theme = $active_channel['theme_color'] ?? null;
|
||||
|
||||
// Fetch messages
|
||||
$stmt = db()->prepare("
|
||||
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://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(); ?>">
|
||||
<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>
|
||||
<body>
|
||||
<body data-theme="<?php echo htmlspecialchars($user['theme'] ?? 'dark'); ?>">
|
||||
|
||||
<div class="discord-app">
|
||||
<!-- Servers Sidebar -->
|
||||
@ -190,7 +223,8 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
data-bs-toggle="modal" data-bs-target="#editChannelModal"
|
||||
data-id="<?php echo $c['id']; ?>"
|
||||
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>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
@ -211,7 +245,8 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
data-bs-toggle="modal" data-bs-target="#editChannelModal"
|
||||
data-id="<?php echo $c['id']; ?>"
|
||||
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>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
@ -245,11 +280,22 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
<div class="chat-container">
|
||||
<div class="chat-header">
|
||||
<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">
|
||||
<input type="text" id="global-search" class="search-input" placeholder="Search messages..." autocomplete="off">
|
||||
<div id="search-results" class="search-results-dropdown"></div>
|
||||
<div class="d-flex align-items-center">
|
||||
<button id="pinned-messages-btn" class="btn btn-link text-muted p-1 me-2" title="Pinned Messages">
|
||||
<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 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>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php foreach($messages as $m): ?>
|
||||
<div class="message-item" data-id="<?php echo $m['id']; ?>">
|
||||
<?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['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">
|
||||
<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 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 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']): ?>
|
||||
<div class="message-attachment mt-2">
|
||||
<?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">
|
||||
<div class="chat-input-wrapper">
|
||||
<?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>
|
||||
<input type="text" name="username" class="form-control" value="<?php echo htmlspecialchars($user['username']); ?>" required>
|
||||
</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">
|
||||
<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">
|
||||
@ -447,6 +537,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="stats-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-stats" type="button">Stats</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content p-3">
|
||||
<div class="tab-pane fade show active" id="settings-general">
|
||||
@ -511,6 +604,33 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
<!-- Webhooks 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">
|
||||
<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>
|
||||
@ -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>
|
||||
<label class="form-check-label text-white" for="add-channel-files">Allow File Sharing</label>
|
||||
</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 class="modal-footer">
|
||||
<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>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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">
|
||||
<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="modal-body p-0">
|
||||
<ul class="nav nav-tabs nav-fill" id="editChannelTabs" role="tablist">
|
||||
<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">
|
||||
<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">
|
||||
<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="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="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>
|
||||
|
||||
<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 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 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>
|
||||
<div class="dropdown">
|
||||
<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>
|
||||
|
||||
<!-- Pinned Messages Modal -->
|
||||
<div class="modal fade" id="pinnedMessagesModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-md">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Pinned Messages</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</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>
|
||||
@ -637,6 +807,9 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
<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.isDndMode = <?php echo ($user['dnd_mode'] ?? 0) ? 'true' : 'false'; ?>;
|
||||
</script>
|
||||
<script src="assets/js/voice.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