This commit is contained in:
Flatlogic Bot 2026-02-15 11:24:55 +00:00
parent 0911f86785
commit 1e73419ffb
11 changed files with 1064 additions and 162 deletions

View 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;
}

View File

@ -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);

View File

@ -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']) {

View File

@ -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
View 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()]);
}

View File

@ -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)

View File

@ -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);
}

View File

@ -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);
}

View 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';

View File

@ -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
View File

@ -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>