38443-vm/index.php
2026-02-19 18:09:08 +00:00

3127 lines
221 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
require_once 'auth/session.php';
function renderRoleIcon($icon, $size = '14px') {
if (empty($icon)) return '';
$isUrl = (strpos($icon, 'http') === 0 || strpos($icon, '/') === 0);
$isFa = (strpos($icon, 'fa-') === 0);
$isCustomEmote = (strpos($icon, ':') === 0 && substr($icon, -1) === ':');
if ($isUrl) {
return '<img src="' . htmlspecialchars($icon) . '" class="role-icon ms-1" style="width: '.$size.'; height: '.$size.'; vertical-align: middle; object-fit: contain;">';
} elseif ($isFa) {
return '<i class="fa-solid ' . htmlspecialchars($icon) . ' ms-1" style="font-size: '.$size.'; vertical-align: middle;"></i>';
} elseif ($isCustomEmote) {
// Fetch emote path
static $ce_icons_cache;
if ($ce_icons_cache === null) {
try { $ce_icons_cache = db()->query("SELECT code, path FROM custom_emotes")->fetchAll(PDO::FETCH_KEY_PAIR); } catch (Exception $e) { $ce_icons_cache = []; }
}
if (isset($ce_icons_cache[$icon])) {
return '<img src="' . htmlspecialchars($ce_icons_cache[$icon]) . '" class="role-icon ms-1" style="width: '.$size.'; height: '.$size.'; vertical-align: middle; object-fit: contain;">';
}
return '<span class="ms-1" style="font-size: '.$size.'; vertical-align: middle;">' . htmlspecialchars($icon) . '</span>';
} else {
return '<span class="ms-1" style="font-size: '.$size.'; vertical-align: middle;">' . htmlspecialchars($icon) . '</span>';
}
}
// Helper to parse markdown in content
function parse_markdown($text) {
if (empty($text)) return "";
// First escape HTML
$html = htmlspecialchars($text);
// Code blocks: ```language\ncontent```
$code_blocks = [];
$html = preg_replace_callback('/```(?:(\w+)\n)?([\s\S]*?)```/', function($matches) use (&$code_blocks) {
$lang = $matches[1] ?? 'text';
$content = $matches[2];
$placeholder = "__CODE_BLOCK_" . count($code_blocks) . "__";
$code_blocks[] = '<pre class="code-block"><code class="language-' . htmlspecialchars($lang) . '">' . $content . '</code></pre>';
return $placeholder;
}, $html);
// Inline code: `content`
$inline_codes = [];
$html = preg_replace_callback('/`([^`\n]+)`/', function($matches) use (&$inline_codes) {
$content = $matches[1];
$placeholder = "__INLINE_CODE_" . count($inline_codes) . "__";
$inline_codes[] = '<code>' . $content . '</code>';
return $placeholder;
}, $html);
// Bold: **text**
$html = preg_replace('/\*\*([^*]+)\*\*/', '<strong>$1</strong>', $html);
// Italics: *text* or _text_
$html = preg_replace('/\*([^*]+)\*/', '<em>$1</em>', $html);
$html = preg_replace('/_([^_]+)_/', '<em>$1</em>', $html);
// Underline: __text__
$html = preg_replace('/__([^_]+)__/', '<u>$1</u>', $html);
// Strikethrough: ~~text~~
$html = preg_replace('/~~([^~]+)~~/', '<del>$1</del>', $html);
// Spoiler: ||text||
$html = preg_replace('/\|\|([^|]+)\|\|/', '<span class="spoiler" onclick="this.classList.toggle(\'revealed\')">$1</span>', $html);
// Headers: # H1, ## H2, ### H3 (must be at start of line)
$html = preg_replace('/^# (.*$)/m', '<h1>$1</h1>', $html);
$html = preg_replace('/^## (.*$)/m', '<h2>$1</h2>', $html);
$html = preg_replace('/^### (.*$)/m', '<h3>$1</h3>', $html);
// Subtext: -# text (must be at start of line)
$html = preg_replace('/^-# (.*$)/m', '<small class="d-block" style="font-size: 0.8em; color: var(--text-muted);">$1</small>', $html);
// Blockquotes: > text or >>> text
$html = preg_replace('/^&gt; (.*$)/m', '<blockquote>$1</blockquote>', $html);
$html = preg_replace('/^&gt;&gt;&gt; ([\s\S]*$)/', '<blockquote>$1</blockquote>', $html);
// Hyperlinks: [text](url)
$html = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2" target="_blank">$1</a>', $html);
// Pure links: <url>
$html = preg_replace('/&lt;(https?:\/\/[^&]+)&gt;/', '<a href="$1" target="_blank">$1</a>', $html);
// Newlines to <br> (only those not inside placeholders)
$html = nl2br($html);
// Remove extra space around headers and blockquotes added by nl2br
$html = preg_replace('/(<br\s*\/?>\s*)\s*(<h[1-3]>|<blockquote>)/i', '$2', $html);
$html = preg_replace('/(<\/h[1-3]>|<\/blockquote>)\s*(<br\s*\/?>\s*)/i', '$1', $html);
// Re-insert inline code
foreach ($inline_codes as $i => $code) {
$html = str_replace("__INLINE_CODE_$i" . "__", $code, $html);
}
// Re-insert code blocks
foreach ($code_blocks as $i => $block) {
$html = str_replace("__CODE_BLOCK_$i" . "__", $block, $html);
}
return $html;
}
// Helper to parse emotes in content
function parse_emotes($content, $username_to_mention = null) {
static $custom_emotes_cache;
if ($custom_emotes_cache === null) {
try {
$custom_emotes_cache = db()->query("SELECT name, path, code FROM custom_emotes")->fetchAll();
} catch (Exception $e) {
$custom_emotes_cache = [];
}
}
$result = parse_markdown($content);
// Parse mentions if username provided
if ($username_to_mention) {
$mention_pattern = '/@' . preg_quote($username_to_mention, '/') . '\b/';
$result = preg_replace($mention_pattern, '<span class="mention">@' . htmlspecialchars($username_to_mention) . '</span>', $result);
}
foreach ($custom_emotes_cache as $ce) {
$emote_html = '<img src="' . htmlspecialchars($ce['path']) . '" alt="' . htmlspecialchars($ce['name']) . '" title="' . htmlspecialchars($ce['code']) . '" style="width: 24px; height: 24px; vertical-align: middle; object-fit: contain;">';
$result = str_replace($ce['code'], $emote_html, $result);
}
return $result;
}
requireLogin();
$user = getCurrentUser();
$current_user_id = $user['id'];
$messages = []; // Initialize messages array
$threads = [];
$rules = [];
$autoroles = [];
// Fetch servers user is member of
$stmt = db()->prepare("
SELECT s.* FROM servers s
JOIN server_members sm ON s.id = sm.server_id
WHERE sm.user_id = ?
LIMIT 20
");
$stmt->execute([$current_user_id]);
$servers = $stmt->fetchAll();
$is_dm_view = (isset($_GET['server_id']) && $_GET['server_id'] == 'dms') || !isset($_GET['server_id']) && empty($servers);
if ($is_dm_view) {
$active_server_id = 'dms';
// Fetch DM channels
$stmt = db()->prepare("
SELECT c.id, u.display_name as other_user, u.avatar_url, u.status, u.id as other_user_id
FROM channels c
JOIN channel_members cm1 ON c.id = cm1.channel_id
JOIN channel_members cm2 ON c.id = cm2.channel_id
JOIN users u ON cm2.user_id = u.id
WHERE c.type = 'dm' AND cm1.user_id = ? AND cm2.user_id != ?
");
$stmt->execute([$current_user_id, $current_user_id]);
$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
$stmt = db()->prepare("
SELECT m.*, u.display_name as username, u.username as login_name, u.avatar_url
FROM messages m
JOIN users u ON m.user_id = u.id
WHERE m.channel_id = ?
ORDER BY m.created_at ASC
LIMIT 50
");
$stmt->execute([$active_channel_id]);
$messages = $stmt->fetchAll();
$current_channel_name = 'Message privé';
foreach($dm_channels as $dm) {
if ($dm['id'] == $active_channel_id) {
$current_channel_name = $dm['other_user'];
break;
}
}
} else {
$messages = [];
$current_channel_name = 'Messages privés';
}
$channels = [];
$members = []; // Members list is different for DMs or hidden
} else {
$active_server_id = $_GET['server_id'] ?? ($servers[0]['id'] ?? 1);
// Fetch channels
$stmt = db()->prepare("SELECT * FROM channels WHERE server_id = ? ORDER BY position ASC, id ASC");
$stmt->execute([$active_server_id]);
$all_channels = $stmt->fetchAll();
require_once 'includes/permissions.php';
$channels = [];
foreach($all_channels as $c) {
if (Permissions::canViewChannel($current_user_id, $c['id'])) {
$channels[] = $c;
}
}
$active_channel_id = $_GET['channel_id'] ?? ($channels[0]['id'] ?? 0);
// Fetch active channel details for theme
$active_channel = null;
foreach($channels as $c) {
if($c['id'] == $active_channel_id) {
$active_channel = $c;
break;
}
}
$is_owner = false;
$can_manage_channels = false;
$can_manage_server = false;
$active_server = null;
foreach($servers as $s) {
if($s['id'] == $active_server_id) {
$active_server = $s;
$is_owner = ($s['owner_id'] == $current_user_id);
$can_manage_channels = Permissions::hasPermission($current_user_id, $active_server_id, Permissions::MANAGE_CHANNELS) || $is_owner;
$can_manage_server = Permissions::hasPermission($current_user_id, $active_server_id, Permissions::MANAGE_SERVER) ||
Permissions::hasPermission($current_user_id, $active_server_id, Permissions::MANAGE_MESSAGES) ||
Permissions::hasPermission($current_user_id, $active_server_id, Permissions::ADMINISTRATOR) ||
$is_owner;
break;
}
}
$channel_theme = $active_server['theme_color'] ?? null;
$channel_type = $active_channel['type'] ?? 'chat';
$active_thread_id = $_GET['thread_id'] ?? null;
$active_thread = null;
if ($active_thread_id) {
$stmt = db()->prepare("SELECT t.*, u.display_name as username, u.username as login_name FROM forum_threads t JOIN users u ON t.user_id = u.id WHERE t.id = ?");
$stmt->execute([$active_thread_id]);
$active_thread = $stmt->fetch();
if ($active_thread) {
$stmt = db()->prepare("
SELECT m.*, u.display_name as username, u.username as login_name, u.avatar_url,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color,
(SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon
FROM messages m
JOIN users u ON m.user_id = u.id
WHERE m.thread_id = ?
ORDER BY m.created_at ASC
");
$stmt->execute([$active_server_id, $active_server_id, $active_thread_id]);
$messages = $stmt->fetchAll();
}
}
if ($active_thread) {
// Thread messages already fetched above
} elseif ($channel_type === 'rules') {
$stmt = db()->prepare("SELECT * FROM channel_rules WHERE channel_id = ? ORDER BY position ASC");
$stmt->execute([$active_channel_id]);
$rules = $stmt->fetchAll();
} elseif ($channel_type === 'autorole') {
$stmt = db()->prepare("SELECT ca.*, r.name as role_name FROM channel_autoroles ca JOIN roles r ON ca.role_id = r.id WHERE ca.channel_id = ? ORDER BY ca.id ASC");
$stmt->execute([$active_channel_id]);
$autoroles = $stmt->fetchAll();
} elseif ($channel_type === 'forum') {
$filter_status = $_GET['status'] ?? 'all';
$status_where = "";
if ($filter_status === 'resolved') {
$status_where = " AND t.solution_message_id IS NOT NULL";
} elseif ($filter_status === 'unresolved') {
$status_where = " AND t.solution_message_id IS NULL";
}
$stmt = db()->prepare("
SELECT t.*, u.display_name as username, u.username as login_name, u.avatar_url,
(SELECT COUNT(*) FROM messages m WHERE m.thread_id = t.id) as message_count,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color,
(SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon,
(SELECT GROUP_CONCAT(CONCAT(ft.name, ':', ft.color) SEPARATOR '|') FROM thread_tags tt JOIN forum_tags ft ON tt.tag_id = ft.id WHERE tt.thread_id = t.id) as tags
FROM forum_threads t
JOIN users u ON t.user_id = u.id
WHERE t.channel_id = ? " . $status_where . "
ORDER BY t.is_pinned DESC, t.last_activity_at DESC, t.created_at DESC
");
$stmt->execute([$active_server_id, $active_server_id, $active_channel_id]);
$threads = $stmt->fetchAll();
} else {
// Fetch messages for normal chat channels
$display_limit = !empty($active_channel['message_limit']) ? (int)$active_channel['message_limit'] : 50;
$stmt = db()->prepare("
SELECT m.*, u.display_name as username, u.username as login_name, u.avatar_url,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color,
(SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon
FROM messages m
JOIN users u ON m.user_id = u.id
WHERE m.channel_id = ? AND m.thread_id IS NULL
ORDER BY m.created_at ASC
LIMIT " . $display_limit . "
");
$stmt->execute([$active_server_id, $active_server_id, $active_channel_id]);
$messages = $stmt->fetchAll();
}
$current_channel_name = 'general';
foreach($channels as $c) if($c['id'] == $active_channel_id) $current_channel_name = $c['name'];
// Fetch voice sessions for the sidebar
$stmt_vs = db()->prepare("
SELECT vs.channel_id, vs.user_id, vs.is_muted, vs.is_deafened, u.username, u.display_name, u.avatar_url
FROM voice_sessions vs
JOIN users u ON vs.user_id = u.id
WHERE vs.last_seen > ?
");
$stale_db_time = (int) floor(microtime(true) * 1000) - 15000;
$stmt_vs->execute([$stale_db_time]);
$voice_sessions = $stmt_vs->fetchAll();
$voice_users_by_channel = [];
foreach($voice_sessions as $vs) {
$voice_users_by_channel[$vs['channel_id']][] = $vs;
}
// Fetch members
$stmt = db()->prepare("
SELECT u.id, u.display_name as username, u.username as login_name, u.avatar_url, u.status,
(SELECT GROUP_CONCAT(r.id) FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ?) as role_ids,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color,
(SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon
FROM users u
JOIN server_members sm ON u.id = sm.user_id
WHERE sm.server_id = ?
");
$stmt->execute([$active_server_id, $active_server_id, $active_server_id, $active_server_id]);
$all_server_members = $stmt->fetchAll();
$members = [];
foreach($all_server_members as $m) {
if (Permissions::canViewChannel($m['id'], $active_channel_id)) {
$members[] = $m;
}
}
// Fetch all server roles
$stmt = db()->prepare("SELECT * FROM roles WHERE server_id = ? ORDER BY position DESC");
$stmt->execute([$active_server_id]);
$server_roles = $stmt->fetchAll();
}
// SEO & Env tags
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Discord-like messaging app built with PHP';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>#<?php echo htmlspecialchars($current_channel_name); ?> | <?php echo htmlspecialchars($projectDescription); ?></title>
<meta name="description" content="<?php echo htmlspecialchars($projectDescription); ?>">
<meta property="og:description" content="<?php echo htmlspecialchars($projectDescription); ?>">
<meta property="twitter:description" content="<?php echo htmlspecialchars($projectDescription); ?>">
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?php echo htmlspecialchars($projectImageUrl); ?>">
<meta property="twitter:image" content="<?php echo htmlspecialchars($projectImageUrl); ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<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 src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script>
window.currentUserId = <?php echo $current_user_id; ?>;
window.activeServerId = "<?php echo $active_server_id; ?>";
window.currentUsername = "<?php echo addslashes($user['display_name'] ?? $user['username']); ?>";
window.isServerOwner = <?php echo ($is_owner ?? false) ? 'true' : 'false'; ?>;
window.canManageServer = <?php echo ($can_manage_server ?? false) ? 'true' : 'false'; ?>;
window.canManageChannels = <?php echo ($can_manage_channels ?? false) ? 'true' : 'false'; ?>;
window.activeChannelId = <?php echo $active_channel_id; ?>;
window.activeChannelType = "<?php echo $channel_type ?? 'chat'; ?>";
window.currentChannelName = "<?php echo addslashes($current_channel_name); ?>";
window.isDndMode = <?php echo ($user['dnd_mode'] ?? 0) ? 'true' : 'false'; ?>;
window.soundNotifications = <?php echo ($user['sound_notifications'] ?? 0) ? 'true' : 'false'; ?>;
window.voiceParamètres = {
mode: "<?php echo $user['voice_mode'] ?? 'vox'; ?>",
pttKey: "<?php echo addslashes($user['voice_ptt_key'] ?? 'v'); ?>",
voxThreshold: <?php echo $user['voice_vox_threshold'] ?? 0.1; ?>,
echoCancellation: <?php echo ($user['voice_echo_cancellation'] ?? 1) ? 'true' : 'false'; ?>,
noiseSuppression: <?php echo ($user['voice_noise_suppression'] ?? 1) ? 'true' : 'false'; ?>,
inputDevice: localStorage.getItem('voice_input_device') || 'default',
outputDevice: localStorage.getItem('voice_output_device') || 'default',
inputVolume: parseFloat(localStorage.getItem('voice_input_volume') || 1.0),
outputVolume: parseFloat(localStorage.getItem('voice_output_volume') || 1.0)
};
</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; ?>
.role-emoji-item:hover {
background-color: var(--separator);
transform: scale(1.2);
transition: transform 0.1s;
}
</style>
</head>
<body data-theme="<?php echo htmlspecialchars($user['theme'] ?: 'dark'); ?>">
<div class="discord-app">
<!-- Servers Sidebar -->
<div class="servers-sidebar">
<a href="index.php?server_id=dms" class="server-icon <?php echo $active_server_id == 'dms' ? 'active' : ''; ?>" title="Messages privés">
<svg width="28" height="20" viewBox="0 0 28 20" fill="currentColor"><path d="M23.0212 1.67671C21.3107 0.883335 19.4805 0.314845 17.5566 0C17.3137 0.434033 17.0423 1.00252 16.8488 1.46581C14.8193 1.16016 12.8016 1.16016 10.8011 1.46581C10.6076 1.00252 10.3255 0.434033 10.0827 0C8.14811 0.314845 6.31792 0.883335 4.60741 1.67671C1.14775 6.84711 0.210418 11.8962 0.67293 16.8681C2.9723 18.5677 5.19143 19.5997 7.37191 20C7.91578 19.2558 8.3897 18.4616 8.79155 17.6166C7.99616 17.3148 7.2343 16.9416 6.51603 16.505C6.70881 16.3639 6.89745 16.2125 7.07923 16.052C11.4116 18.0494 16.1264 18.0494 20.4137 16.052C20.597 16.2125 20.7856 16.3639 20.9784 16.505C20.2586 16.9416 19.4967 17.3148 18.7013 17.6166C19.1031 18.4616 19.577 19.2558 20.1209 20C22.3014 19.5997 24.5205 18.5677 26.8199 16.8681C27.3693 11.127 25.9189 6.13063 23.0212 1.67671ZM9.51636 13.6749C8.21405 13.6749 7.14188 12.4839 7.14188 11.026C7.14188 9.56816 8.19284 8.3771 9.51636 8.3771C10.8399 8.3771 11.912 9.56816 11.8908 11.026C11.8908 12.4839 10.8399 13.6749 9.51636 13.6749ZM18.0051 13.6749C16.7028 13.6749 15.6306 12.4839 15.6306 11.026C15.6306 9.56816 16.6815 8.3771 18.0051 8.3771C19.3286 8.3771 20.4008 9.56816 20.3796 11.026C20.3796 12.4839 19.3286 13.6749 18.0051 13.6749Z"/></svg>
</a>
<hr style="width: 32px; border-color: var(--separator); margin: 4px 0;">
<?php foreach($servers as $s): ?>
<a href="?server_id=<?php echo $s['id']; ?>"
class="server-icon <?php echo $s['id'] == $active_server_id ? 'active' : ''; ?>"
title="<?php echo htmlspecialchars($s['name']); ?>"
style="<?php
if (!empty($s['icon_url'])) {
echo "background-image: url('{$s['icon_url']}'); background-size: cover;";
} else if (!empty($s['theme_color'])) {
echo "background-color: {$s['theme_color']};";
}
?>">
<?php echo empty($s['icon_url']) ? mb_substr($s['name'], 0, 1) : ''; ?>
</a>
<?php endforeach; ?>
<a href="#" class="server-icon add-btn" title="Ajouter un serveur" data-bs-toggle="modal" data-bs-target="#addServerModal">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</a>
</div>
<!-- Channels Sidebar -->
<div class="channels-sidebar">
<div class="channels-header">
<?php
if ($is_dm_view) {
echo '<span class="text-truncate flex-grow-1" style="min-width: 0;">Messages privés</span>';
} else {
$active_server_name = 'Serveur';
foreach($servers as $s) {
if($s['id'] == $active_server_id) {
$active_server_name = $s['name'];
break;
}
}
echo '<span class="text-truncate flex-grow-1" style="min-width: 0;">' . htmlspecialchars($active_server_name) . '</span>';
?>
<div class="d-flex align-items-center ps-2">
<?php if ($can_manage_channels): ?>
<span class="add-channel-btn me-2" style="cursor: pointer; opacity: 0.7;" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="chat" data-category-id="" title="Créer un salon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</span>
<?php endif; ?>
<?php if ($is_owner || $can_manage_server): ?>
<span style="cursor: pointer; opacity: 0.7;" data-bs-toggle="modal" data-bs-target="#serverParamètresModal" title="Paramètres du serveur">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></svg>
</span>
<?php endif; ?>
</div>
<?php
}
?>
</div>
<div class="channels-list" id="sidebar-channels-list">
<?php if ($is_dm_view): ?>
<?php foreach($dm_channels as $dm): ?>
<a href="?server_id=dms&channel_id=<?php echo $dm['id']; ?>"
class="dm-user-item <?php echo $dm['id'] == $active_channel_id ? 'active' : ''; ?>">
<div class="message-avatar" style="width: 32px; height: 32px; <?php echo $dm['avatar_url'] ? "background-image: url('{$dm['avatar_url']}');" : ""; ?>">
<div class="dm-status-indicator dm-status-<?php echo $dm['status']; ?>" style="position: absolute; bottom: 0; right: 0;"></div>
</div>
<span><?php echo htmlspecialchars($dm['other_user']); ?></span>
</a>
<?php endforeach; ?>
<?php else: ?>
<?php
// Helper to render a channel item
function renderChannelItem($c, $active_channel_id, $active_server_id, $can_manage_channels) {
global $voice_users_by_channel;
if ($c['type'] === 'separator') {
?>
<div class="channel-item-container separator-item d-flex align-items-center justify-content-between px-2 py-1" data-id="<?php echo $c['id']; ?>" data-type="separator" style="min-height: 24px;">
<div class="flex-grow-1" style="height: 1px; background: var(--separator); margin: 10px 0;"></div>
<?php if ($can_manage_channels): ?>
<span class="channel-settings-btn ms-2" style="cursor: pointer; color: var(--text-muted); opacity: 0; transition: opacity 0.2s;"
data-bs-toggle="modal" data-bs-target="#editChannelModal"
data-id="<?php echo $c['id']; ?>"
data-name="separator"
data-type="separator"
data-files="0"
data-limit="0"
data-status=""
data-icon="<?php echo htmlspecialchars($c['icon'] ?? ''); ?>"
data-rules-role="<?php echo $c['rules_role_id'] ?? ''; ?>"
data-category="<?php echo $c['category_id'] ?? ''; ?>">
<svg width="12" height="12" 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; ?>
</div>
<?php
return;
}
?>
<div class="channel-item-container" data-id="<?php echo $c['id']; ?>">
<div class="d-flex align-items-center">
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $c['id']; ?>"
class="channel-item flex-grow-1 <?php echo ($c['id'] == $active_channel_id) ? 'active' : ''; ?> <?php echo ($c['type'] === 'voice') ? 'voice-item' : ''; ?>" <?php echo ($c['type'] === 'voice') ? 'data-channel-id="'.$c['id'].'"' : ''; ?>>
<span class="d-flex align-items-center">
<span class="me-1" style="width: 20px; display: inline-block; text-align: center;">
<?php
if ($c['type'] === 'announcement') echo '<i class="fa-solid fa-bullhorn"></i>';
elseif ($c['type'] === 'rules') echo '<i class="fa-solid fa-gavel"></i>';
elseif ($c['type'] === 'autorole') echo '<i class="fa-solid fa-shield-halved"></i>';
elseif ($c['type'] === 'forum') echo '<i class="fa-solid fa-comments"></i>';
elseif ($c['type'] === 'voice') echo '<i class="fa-solid fa-volume-up"></i>';
else echo '<i class="fa-solid fa-hashtag"></i>';
?>
</span>
<?php if (!empty($c['icon'])): ?>
<span class="me-1" style="font-size: 14px;"><?php echo renderRoleIcon($c['icon'], '14px'); ?></span>
<?php endif; ?>
<span class="channel-name-text"><?php echo htmlspecialchars($c['name']); ?></span>
</span>
<?php if ($c['type'] === 'voice' && !empty($c['status'])): ?>
<div class="channel-status small text-muted ms-4" style="font-size: 0.75em; margin-top: -2px;"><?php echo htmlspecialchars($c['status']); ?></div>
<?php endif; ?>
</a>
<?php if ($can_manage_channels): ?>
<span class="channel-settings-btn ms-1" style="cursor: pointer; color: var(--text-muted);"
data-bs-toggle="modal" data-bs-target="#editChannelModal"
data-id="<?php echo $c['id']; ?>"
data-name="<?php echo htmlspecialchars($c['name']); ?>"
data-type="<?php echo $c['type']; ?>"
data-files="<?php echo $c['allow_file_sharing']; ?>"
data-limit="<?php echo $c['message_limit']; ?>"
data-status="<?php echo htmlspecialchars($c['status'] ?? ''); ?>"
data-icon="<?php echo htmlspecialchars($c['icon'] ?? ''); ?>"
data-rules-role="<?php echo $c['rules_role_id'] ?? ''; ?>"
data-category="<?php echo $c['category_id'] ?? ''; ?>">
<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; ?>
</div>
<?php if ($c['type'] === 'voice'): ?>
<div class="voice-users-list ms-4 mb-1">
<?php if (isset($voice_users_by_channel[$c['id']])): ?>
<?php foreach($voice_users_by_channel[$c['id']] as $v_user): ?>
<div class="voice-user small text-muted d-flex align-items-center mb-1" data-user-id="<?php echo $v_user['user_id']; ?>">
<div class="message-avatar me-2" style="width: 16px; height: 16px; <?php echo $v_user['avatar_url'] ? "background-image: url('{$v_user['avatar_url']}');" : ""; ?>"></div>
<span class="text-truncate" style="font-size: 13px; max-width: 100px;"><?php echo htmlspecialchars($v_user['display_name'] ?? $v_user['username']); ?></span>
<?php if ($v_user['is_deafened']): ?>
<i class="fa-solid fa-volume-xmark ms-auto text-danger" style="font-size: 10px;"></i>
<?php elseif ($v_user['is_muted']): ?>
<i class="fa-solid fa-microphone-slash ms-auto text-danger" style="font-size: 10px;"></i>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php
}
$category_ids = array_column(array_filter($channels, function($c) { return $c['type'] === 'category'; }), 'id');
foreach($channels as $item) {
// Skip channels that have a parent category (they will be rendered inside their category)
if ($item['type'] !== 'category' && !empty($item['category_id']) && in_array($item['category_id'], $category_ids)) {
continue;
}
if ($item['type'] === 'category') {
// Render category and its children
?>
<div class="category-wrapper" data-id="<?php echo $item['id']; ?>">
<div class="channel-category d-flex align-items-center" data-id="<?php echo $item['id']; ?>" style="cursor: pointer;">
<span class="category-collapse-toggle me-1" style="width: 12px; display: inline-block; transition: transform 0.2s; font-size: 0.7em;">
<i class="fa-solid fa-chevron-down"></i>
</span>
<?php if (!empty($item['icon'])): ?>
<span class="me-1" style="font-size: 14px;"><?php echo renderRoleIcon($item['icon'], '14px'); ?></span>
<?php endif; ?>
<span class="category-name flex-grow-1 text-uppercase fw-bold" style="font-size: 0.85em; color: var(--text-muted);"><?php echo htmlspecialchars($item['name']); ?></span>
<?php if ($can_manage_channels): ?>
<span class="channel-settings-btn ms-1" style="cursor: pointer; color: var(--text-muted);"
data-bs-toggle="modal" data-bs-target="#editChannelModal"
data-id="<?php echo $item['id']; ?>"
data-name="<?php echo htmlspecialchars($item['name']); ?>"
data-type="category"
data-files="0"
data-limit="0"
data-status=""
data-icon="<?php echo htmlspecialchars($item['icon'] ?? ''); ?>"
data-rules-role="<?php echo $item['rules_role_id'] ?? ''; ?>"
data-category=""
data-theme="">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</span>
<span class="add-channel-btn ms-1" title="Créer un salon" data-bs-toggle="modal" data-bs-target="#addChannelModal" data-type="chat" data-category-id="<?php echo $item['id']; ?>">+</span>
<?php endif; ?>
</div>
<div class="category-group" data-category-id="<?php echo $item['id']; ?>">
<?php
foreach($channels as $c) {
if ($c['type'] !== 'category' && $c['category_id'] == $item['id']) {
renderChannelItem($c, $active_channel_id, $active_server_id, $can_manage_channels);
}
}
?>
</div>
</div>
<?php
} else {
// Render top level channel
renderChannelItem($item, $active_channel_id, $active_server_id, $can_manage_channels);
}
}
?>
<?php endif; ?>
</div>
<div class="user-panel">
<div class="user-info" data-bs-toggle="modal" data-bs-target="#userParamètresModal">
<div class="message-avatar" style="width: 32px; height: 32px; <?php echo $user['avatar_url'] ? "background-image: url('{$user['avatar_url']}');" : ""; ?>"></div>
<div style="flex: 1; min-width: 0;">
<div style="font-weight: bold; font-size: 0.85em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?>
</div>
<div style="color: var(--text-muted); font-size: 0.75em;">@<?php echo htmlspecialchars($user['username']); ?> #<?php echo str_pad($user['id'], 4, '0', STR_PAD_LEFT); ?></div>
</div>
</div>
<div class="user-actions d-flex align-items-center">
<button class="btn btn-link p-1 text-muted border-0" id="btn-panel-mute" title="Mute/Unmute" onclick="if(window.voiceHandler) window.voiceHandler.toggleMute()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>
</button>
<button class="btn btn-link p-1 text-muted border-0" id="btn-panel-deafen" title="Deafen/Undeafen" onclick="if(window.voiceHandler) window.voiceHandler.toggleDeafen()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path></svg>
</button>
<a href="#" title="Paramètres" class="p-1 text-muted d-inline-flex" data-bs-toggle="modal" data-bs-target="#userParamètresModal">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
</a>
<a href="auth/logout.php" class="p-1 text-muted d-inline-flex" onclick="if(window.voiceHandler) window.voiceHandler.leave(); sessionStorage.clear();" title="Déconnexion">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
</a>
</div>
</div>
<div style="padding: 10px; font-size: 10px; color: #4e5058; border-top: 1px solid #1e1f22;">
PHP <?php echo PHP_VERSION; ?> | <?php echo date('H:i'); ?>
</div>
</div>
<!-- Chat Area -->
<div class="chat-container">
<div class="chat-header">
<span style="color: var(--text-muted); margin-right: 8px; width: auto; display: inline-block; text-align: center;">
<?php
if ($is_dm_view) {
echo '@';
} else {
if ($active_channel['type'] === 'announcement') echo '<i class="fa-solid fa-bullhorn"></i>';
elseif ($active_channel['type'] === 'rules') echo '<i class="fa-solid fa-gavel"></i>';
elseif ($active_channel['type'] === 'autorole') echo '<i class="fa-solid fa-shield-halved"></i>';
elseif ($active_channel['type'] === 'forum') echo '<i class="fa-solid fa-comments"></i>';
elseif ($active_channel['type'] === 'voice') echo '<i class="fa-solid fa-volume-up"></i>';
else echo '<i class="fa-solid fa-hashtag"></i>';
if (!empty($active_channel['icon'])) {
echo ' <span class="ms-1">' . renderRoleIcon($active_channel['icon'], '16px') . '</span>';
}
}
?>
</span>
<span class="flex-grow-1 text-truncate me-2" style="min-width: 0;"><?php echo htmlspecialchars($current_channel_name); ?></span>
<div class="d-flex align-items-center">
<button id="toggle-members-btn" class="btn btn-link text-muted p-1 me-2" title="Afficher/Masquer la liste des membres">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
</button>
<button id="pinned-messages-btn" class="btn btn-link text-muted p-1 me-2" title="Messages épinglés">
<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">Utilisateurs</option>
</select>
<input type="text" id="global-search" class="search-input" placeholder="Rechercher..." autocomplete="off">
</div>
<div id="search-results" class="search-results-dropdown"></div>
</div>
</div>
</div>
<div class="messages-list" id="messages-list">
<?php if($active_thread): ?>
<div class="thread-view-container p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $active_channel_id; ?><?php echo isset($_GET['status']) ? '&status='.htmlspecialchars($_GET['status']) : ''; ?>" class="btn btn-sm btn-outline-secondary">← Retour au forum</a>
<div class="d-flex gap-2">
<?php if (Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::PIN_THREADS)): ?>
<button class="btn btn-sm <?php echo $active_thread['is_pinned'] ? 'btn-primary' : 'btn-outline-primary'; ?>" id="toggle-pin-thread" data-id="<?php echo $active_thread['id']; ?>" data-pinned="<?php echo $active_thread['is_pinned']; ?>">
<i class="fa-solid fa-thumbtack me-1"></i> <?php echo $active_thread['is_pinned'] ? 'Désépingler' : 'Épingler'; ?>
</button>
<?php endif; ?>
<?php if (Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::LOCK_THREADS)): ?>
<button class="btn btn-sm <?php echo $active_thread['is_locked'] ? 'btn-warning' : 'btn-outline-warning'; ?>" id="toggle-lock-thread" data-id="<?php echo $active_thread['id']; ?>" data-locked="<?php echo $active_thread['is_locked']; ?>">
<i class="fa-solid <?php echo $active_thread['is_locked'] ? 'fa-unlock' : 'fa-lock'; ?> me-1"></i> <?php echo $active_thread['is_locked'] ? 'Déverrouiller' : 'Verrouiller'; ?>
</button>
<?php endif; ?>
<?php if ($active_thread['user_id'] == $current_user_id || $can_manage_server): ?>
<button class="btn btn-sm btn-outline-danger" id="delete-thread-btn" data-id="<?php echo $active_thread['id']; ?>" data-channel-id="<?php echo $active_channel_id; ?>" data-server-id="<?php echo $active_server_id; ?>">
<i class="fa-solid fa-trash me-1"></i> Supprimer
</button>
<?php endif; ?>
</div>
</div>
<h3>
<?php if($active_thread['is_pinned']): ?><i class="fa-solid fa-thumbtack text-primary me-2 small"></i><?php endif; ?>
<?php if($active_thread['is_locked']): ?><i class="fa-solid fa-lock text-warning me-2 small"></i><?php endif; ?>
Discussion : <?php echo htmlspecialchars($active_thread['title']); ?>
</h3>
<?php
// Fetch tags for this thread
$stmt_t = db()->prepare("SELECT ft.* FROM forum_tags ft JOIN thread_tags tt ON ft.id = tt.tag_id WHERE tt.thread_id = ?");
$stmt_t->execute([$active_thread['id']]);
$thread_tags = $stmt_t->fetchAll();
if ($thread_tags):
foreach ($thread_tags as $t):
?>
<span class="badge rounded-pill me-1" style="background-color: <?php echo htmlspecialchars($t['color']); ?>;"><?php echo htmlspecialchars($t['name']); ?></span>
<?php endforeach; endif; ?>
<div class="messages-list-inner mt-4">
<?php foreach($messages as $m):
$mention_pattern = '/@' . preg_quote($user['username'], '/') . '\b/';
$is_mentioned = preg_match($mention_pattern, $m['content']);
$is_solution = ($active_thread['solution_message_id'] == $m['id']);
?>
<!-- Message rendering code (reused) -->
<div class="message-item <?php echo $is_mentioned ? 'mentioned' : ''; ?> <?php echo $is_solution ? 'is-solution' : ''; ?>" data-id="<?php echo $m['id']; ?>" data-raw-content="<?php echo htmlspecialchars($m['content']); ?>">
<div class="message-avatar" style="<?php echo $m['avatar_url'] ? "background-image: url('{$m['avatar_url']}');" : ""; ?>"></div>
<div class="message-content">
<div class="message-author" style="<?php echo !empty($m['role_color']) ? "color: {$m['role_color']};" : ""; ?>">
<?php echo htmlspecialchars($m['username']); ?>
<?php echo renderRoleIcon($m['role_icon'], '14px'); ?>
<span class="message-time"><?php echo date('H:i', strtotime($m['created_at'])); ?></span>
<?php if ($is_solution): ?>
<span class="badge bg-success ms-2">SOLUTION</span>
<?php endif; ?>
<div class="message-actions-menu">
<?php if (($active_thread['user_id'] == $current_user_id || $can_manage_server) && $m['user_id'] != $active_thread['user_id']): ?>
<span class="action-btn mark-solution <?php echo $is_solution ? 'active' : ''; ?>" title="<?php echo $is_solution ? 'Retirer comme solution' : 'Marquer comme solution'; ?>" data-thread-id="<?php echo $active_thread['id']; ?>" data-message-id="<?php echo $m['id']; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>
</span>
<?php endif; ?>
<?php if ($m['user_id'] == $current_user_id): ?>
<span class="action-btn edit" title="Modifier" 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="Supprimer" data-id="<?php echo $m['id']; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
</span>
<?php endif; ?>
</div>
</div>
<div class="message-text">
<?php echo parse_emotes($m['content'], $user['username']); ?>
</div>
<div class="message-reactions mt-1" data-message-id="<?php echo $m['id']; ?>">
<?php
// Fetch reactions for this message
$stmt_react = db()->prepare("SELECT emoji, COUNT(*) as count, GROUP_CONCAT(user_id) as users FROM message_reactions WHERE message_id = ? GROUP BY emoji");
$stmt_react->execute([$m['id']]);
$reactions = $stmt_react->fetchAll();
foreach ($reactions as $r):
$reacted = in_array($current_user_id, explode(',', $r['users']));
?>
<span class="reaction-badge <?php echo $reacted ? 'active' : ''; ?>" data-emoji="<?php echo htmlspecialchars($r['emoji']); ?>">
<?php echo htmlspecialchars($r['emoji']); ?> <span class="count"><?php echo $r['count']; ?></span>
</span>
<?php endforeach; ?>
<span class="add-reaction-btn" title="Ajouter une réaction">+</span>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php elseif($channel_type === 'rules'): ?>
<div class="rules-container p-4">
<h2 class="mb-4">📜 <?php echo htmlspecialchars($current_channel_name); ?></h2>
<div id="rules-list-sortable">
<?php $i = 1; foreach($rules as $rule): ?>
<div class="rule-item mb-3 p-3 rounded bg-dark border-start border-4 border-primary d-flex justify-content-between align-items-center" data-id="<?php echo $rule['id']; ?>">
<div class="rule-content flex-grow-1">
<span class="rule-number fw-bold me-2"><?php echo $i++; ?>.</span>
<?php echo parse_emotes($rule['content']); ?>
</div>
<?php if($can_manage_channels): ?>
<div class="rule-actions ms-3 d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary move-rule-btn" data-id="<?php echo $rule['id']; ?>" data-dir="up">↑</button>
<button class="btn btn-sm btn-outline-secondary move-rule-btn" data-id="<?php echo $rule['id']; ?>" data-dir="down">↓</button>
<button class="btn btn-sm btn-outline-light edit-rule-btn" data-id="<?php echo $rule['id']; ?>">Modifier</button>
<button class="btn btn-sm btn-outline-danger delete-rule-btn" data-id="<?php echo $rule['id']; ?>">×</button>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php if($can_manage_channels): ?>
<div id="add-rule-form" style="display: none;" class="mt-3 p-3 rounded bg-dark border border-secondary">
<textarea id="new-rule-content" class="form-control bg-dark text-white mb-2" placeholder="Saisissez la règle ici..." rows="3"></textarea>
<div class="d-flex gap-2">
<button class="btn btn-success btn-sm" id="save-new-rule-btn">Enregistrer</button>
<button class="btn btn-secondary btn-sm" id="cancel-new-rule-btn">Annuler</button>
</div>
</div>
<button class="btn btn-primary mt-3" id="add-rule-btn">+ Ajouter une règle</button>
<?php endif; ?>
<?php if (!empty($active_channel['rules_role_id'])): ?>
<?php
$stmtAcc = db()->prepare("SELECT 1 FROM rule_acceptances WHERE user_id = ? AND channel_id = ?");
$stmtAcc->execute([$current_user_id, $active_channel_id]);
$has_accepted = $stmtAcc->fetch();
?>
<div class="mt-5 pt-4 border-top border-secondary text-center" id="rules-acceptance-container">
<?php if ($has_accepted): ?>
<div class="alert alert-success d-inline-block">
<i class="fa-solid fa-check-circle me-2"></i> Vous avez accepté les règles.
</div>
<div class="mt-2">
<button class="btn btn-sm btn-outline-danger" id="withdraw-rules-btn">
<i class="fa-solid fa-undo me-1"></i> Retirer mon acceptation
</button>
</div>
<?php else: ?>
<p class="text-muted mb-3">Veuillez accepter les règles pour obtenir l'accès complet.</p>
<button class="btn btn-lg btn-success px-5" id="accept-rules-btn">
<i class="fa-solid fa-check me-2"></i> J'accepte les règles
</button>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php elseif($channel_type === 'autorole'): ?>
<div class="autoroles-container p-4">
<h2 class="mb-4">🛡️ <?php echo htmlspecialchars($current_channel_name); ?></h2>
<p class="text-muted mb-4">Cliquez sur un bouton pour vous attribuer ou vous retirer un rôle.</p>
<div class="d-flex flex-wrap gap-3" id="autorole-buttons-list">
<?php foreach($autoroles as $ar):
// Check if user has this role
$stmtHasRole = db()->prepare("SELECT 1 FROM user_roles WHERE user_id = ? AND role_id = ?");
$stmtHasRole->execute([$current_user_id, $ar['role_id']]);
$has_role = $stmtHasRole->fetch();
?>
<div class="autorole-card p-1 rounded" style="background-color: #2b2d31; border: 1px solid #4e5058; min-width: 200px;">
<button class="btn autorole-toggle-btn d-flex align-items-center gap-2 px-4 py-3 w-100 <?php echo $has_role ? 'btn-primary' : 'btn-outline-secondary'; ?>"
data-role-id="<?php echo $ar['role_id']; ?>"
data-id="<?php echo $ar['id']; ?>"
style="<?php echo $has_role ? 'background-color: var(--blurple); border: none;' : 'background-color: #2b2d31; border: none; color: white;'; ?>">
<span style="font-size: 1.5em;"><?php echo parse_emotes($ar['icon']); ?></span>
<div class="text-start">
<div class="fw-bold"><?php echo htmlspecialchars($ar['title']); ?></div>
<div class="small opacity-75"><?php echo htmlspecialchars($ar['role_name']); ?></div>
</div>
</button>
<?php if($can_manage_channels): ?>
<div class="d-flex justify-content-end gap-2 px-2 pb-2 border-top border-secondary pt-2 mt-1">
<button type="button" class="btn btn-sm btn-link text-info p-0 edit-autorole-btn"
data-id="<?php echo $ar['id']; ?>"
data-icon="<?php echo htmlspecialchars($ar['icon']); ?>"
data-title="<?php echo htmlspecialchars($ar['title']); ?>"
data-role-id="<?php echo $ar['role_id']; ?>"
data-bs-toggle="modal" data-bs-target="#editAutoroleModal">
<i class="fa-solid fa-pen-to-square"></i> Modifier
</button>
<form action="api_v1_autoroles.php" method="POST" class="m-0">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?php echo $ar['id']; ?>">
<input type="hidden" name="channel_id" value="<?php echo $active_channel_id; ?>">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<button type="submit" class="btn btn-sm btn-link text-danger p-0 ms-1" title="Supprimer Autorole" onclick="return confirm('Supprimer cet autorole ?')">
<i class="fa-solid fa-trash"></i> Supprimer
</button>
</form>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php if($can_manage_channels): ?>
<div class="mt-5 pt-4 border-top border-secondary">
<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#addAutoroleModal">
<i class="fa-solid fa-plus me-2"></i> Ajouter un autorole
</button>
</div>
<?php endif; ?>
</div>
<?php elseif($channel_type === 'forum'): ?>
<div class="forum-container p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-2">🏛️ <?php echo htmlspecialchars($current_channel_name); ?></h2>
<div class="btn-group btn-group-sm forum-filters">
<?php
$s_id = $active_server_id;
$c_id = $active_channel_id;
$curr_status = $_GET['status'] ?? 'all';
?>
<a href="?server_id=<?php echo $s_id; ?>&channel_id=<?php echo $c_id; ?>&status=all" class="btn btn-outline-secondary <?php echo $curr_status === 'all' ? 'active' : ''; ?>">Tous</a>
<a href="?server_id=<?php echo $s_id; ?>&channel_id=<?php echo $c_id; ?>&status=unresolved" class="btn btn-outline-secondary <?php echo $curr_status === 'unresolved' ? 'active' : ''; ?>">Non résolus</a>
<a href="?server_id=<?php echo $s_id; ?>&channel_id=<?php echo $c_id; ?>&status=resolved" class="btn btn-outline-secondary <?php echo $curr_status === 'resolved' ? 'active' : ''; ?>">Résolus</a>
</div>
</div>
<div class="d-flex gap-2">
<?php if(Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::MANAGE_TAGS)): ?>
<button class="btn btn-outline-secondary" id="manage-tags-btn">Gérer les tags</button>
<?php endif; ?>
<?php if(Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::CREATE_THREAD)): ?>
<button class="btn btn-primary" id="new-thread-btn">Nouvelle Discussion</button>
<?php endif; ?>
</div>
</div>
<div class="thread-list">
<?php if(empty($threads)): ?>
<div class="text-center text-muted mt-5">Pas encore de discussions. Commencez-en une !</div>
<?php endif; ?>
<?php foreach($threads as $thread): ?>
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $active_channel_id; ?>&thread_id=<?php echo $thread['id']; ?><?php echo isset($_GET['status']) ? '&status='.htmlspecialchars($_GET['status']) : ''; ?>" class="thread-item d-flex align-items-center p-3 mb-2 rounded bg-dark text-decoration-none text-white border-start border-4 <?php echo $thread['is_pinned'] ? 'border-primary' : 'border-secondary'; ?>">
<div class="thread-icon me-3">
<?php if($thread['is_pinned']): ?>
<i class="fa-solid fa-thumbtack text-primary"></i>
<?php elseif($thread['is_locked']): ?>
<i class="fa-solid fa-lock text-warning"></i>
<?php else: ?>
💬
<?php endif; ?>
</div>
<div class="thread-info flex-grow-1">
<div class="thread-title fw-bold">
<?php if($thread['solution_message_id']): ?>
<span class="text-success me-1" title="Résolu">✔</span>
<?php endif; ?>
<?php if($thread['is_locked'] && !$thread['is_pinned']): ?>
<i class="fa-solid fa-lock small me-1 text-muted"></i>
<?php endif; ?>
<?php echo htmlspecialchars($thread['title']); ?>
<?php if($thread['tags']):
$tag_list = explode('|', $thread['tags']);
foreach($tag_list as $tag_data):
list($t_name, $t_color) = explode(':', $tag_data);
?>
<span class="badge rounded-pill ms-1" style="background-color: <?php echo htmlspecialchars($t_color); ?>; font-size: 0.6em;"><?php echo htmlspecialchars($t_name); ?></span>
<?php endforeach; endif; ?>
</div>
<div class="thread-meta small text-muted">
Lancé par <span style="<?php echo !empty($thread['role_color']) ? "color: {$thread['role_color']};" : ""; ?>"><?php echo htmlspecialchars($thread['username']); ?></span>
<?php echo renderRoleIcon($thread['role_icon'], '11px'); ?>
• <?php echo $thread['message_count']; ?> messages
</div>
</div>
<div class="thread-activity text-end small text-muted">
<?php if($thread['last_activity_at']): ?>
Dernière activité : <?php echo date('H:i', strtotime($thread['last_activity_at'])); ?>
<?php endif; ?>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<?php if(empty($messages)): ?>
<div style="text-align: center; color: var(--text-muted); margin-top: 40px;">
<h4>Bienvenue dans #<?php echo htmlspecialchars($current_channel_name); ?> !</h4>
<p>C'est le début du salon #<?php echo htmlspecialchars($current_channel_name); ?>.</p>
</div>
<?php endif; ?>
<?php foreach($messages as $m):
$mention_pattern = '/@' . preg_quote($user['username'], '/') . '\b/';
$is_mentioned = preg_match($mention_pattern, $m['content']);
?>
<div class="message-item <?php echo $is_mentioned ? 'mentioned' : ''; ?> <?php echo $m['is_pinned'] ? 'pinned' : ''; ?> <?php echo $channel_type === 'announcement' ? 'announcement-style' : ''; ?>" data-id="<?php echo $m['id']; ?>" data-raw-content="<?php echo htmlspecialchars($m['content']); ?>">
<div class="message-avatar" style="<?php echo $m['avatar_url'] ? "background-image: url('{$m['avatar_url']}');" : ""; ?>"></div>
<div class="message-content">
<div class="message-header">
<span class="message-author" style="<?php echo !empty($m['role_color']) ? "color: {$m['role_color']};" : ""; ?>">
<?php echo htmlspecialchars($m['username']); ?>
<?php echo renderRoleIcon($m['role_icon'], '14px'); ?>
</span>
<span class="message-time"><?php echo date('H:i', strtotime($m['created_at'])); ?></span>
<?php if ($m['is_pinned']): ?>
<span class="pinned-badge ms-2" title="Message épinglé">
<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>
Épinglé
</span>
<?php endif; ?>
</div>
<div class="message-text">
<?php echo parse_emotes($m['content'], $user['username']); ?>
<?php if ($m['attachment_url']): ?>
<div class="message-attachment mt-2">
<?php
$ext = strtolower(pathinfo($m['attachment_url'], PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])):
?>
<img src="<?php echo htmlspecialchars($m['attachment_url']); ?>" class="img-fluid rounded message-img-preview" alt="Attachment" style="max-height: 300px; cursor: pointer;" onclick="window.open(this.src)">
<?php else: ?>
<a href="<?php echo htmlspecialchars($m['attachment_url']); ?>" target="_blank" class="attachment-link d-inline-flex align-items-center p-2 rounded bg-dark text-white text-decoration-none">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
<?php echo basename($m['attachment_url']); ?>
</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($m['metadata'])):
$meta = json_decode($m['metadata'], true);
if ($meta): ?>
<div class="rich-embed mt-2 p-3 rounded" style="background: rgba(0,0,0,0.1); border-left: 4px solid var(--blurple); max-width: 520px;">
<?php if (!empty($meta['site_name']) && empty($meta['is_rss'])): ?>
<div class="embed-site-name mb-1" style="font-size: 0.75em; color: var(--text-muted); text-transform: uppercase; font-weight: bold;"><?php echo htmlspecialchars($meta['site_name']); ?></div>
<?php endif; ?>
<?php if (!empty($meta['title'])): ?>
<a href="<?php echo htmlspecialchars($meta['url']); ?>" target="_blank" class="embed-title d-block mb-1 text-decoration-none" style="font-weight: 600; color: #00a8fc; font-size: 1.1em;"><?php echo htmlspecialchars($meta['title']); ?></a>
<?php endif; ?>
<?php if (!empty($meta['is_rss'])): ?>
<div class="embed-meta mb-2" style="font-size: 0.8em; color: var(--text-muted);">
<?php
$parts = [];
if (!empty($meta['category'])) $parts[] = htmlspecialchars($meta['category']);
if (!empty($meta['date'])) $parts[] = htmlspecialchars($meta['date']);
if (!empty($meta['author'])) $parts[] = htmlspecialchars($meta['author']);
echo implode(' · ', $parts);
?>
</div>
<?php endif; ?>
<?php if (!empty($meta['description'])): ?>
<div class="embed-description mb-2" style="font-size: 0.9em; color: var(--text-normal);"><?php echo htmlspecialchars($meta['description']); ?></div>
<?php endif; ?>
<?php if (!empty($meta['image'])): ?>
<div class="embed-image">
<img src="<?php echo htmlspecialchars($meta['image']); ?>" class="rounded" style="max-width: 100%; max-height: 300px; object-fit: contain;">
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<div class="message-reactions mt-1" data-message-id="<?php echo $m['id']; ?>">
<?php
// Fetch reactions for this message
$stmt_react = db()->prepare("SELECT emoji, COUNT(*) as count, GROUP_CONCAT(user_id) as users FROM message_reactions WHERE message_id = ? GROUP BY emoji");
$stmt_react->execute([$m['id']]);
$reactions = $stmt_react->fetchAll();
foreach ($reactions as $r):
$reacted = in_array($current_user_id, explode(',', $r['users']));
?>
<span class="reaction-badge <?php echo $reacted ? 'active' : ''; ?>" data-emoji="<?php echo htmlspecialchars($r['emoji']); ?>">
<?php echo parse_emotes($r['emoji']); ?> <span class="count"><?php echo $r['count']; ?></span>
</span>
<?php endforeach; ?>
<span class="add-reaction-btn" title="Ajouter une réaction">+</span>
</div>
</div>
<?php if ($m['user_id'] == $current_user_id || ($active_server_id != 'dms' && $can_manage_channels)): ?>
<div class="message-actions-menu">
<span class="action-btn pin <?php echo $m['is_pinned'] ? 'active' : ''; ?>" title="<?php echo $m['is_pinned'] ? 'Désépingler' : 'Épingler'; ?>" data-id="<?php echo $m['id']; ?>" data-pinned="<?php echo $m['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>
<?php if ($m['user_id'] == $current_user_id): ?>
<span class="action-btn edit" title="Modifier" 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="Supprimer" 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>
<?php endforeach; ?>
<?php endif; ?>
</div>
<div id="typing-indicator" class="typing-indicator"></div>
<div class="chat-input-container">
<?php
require_once 'includes/permissions.php';
if ($active_thread) {
$can_send = Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::SEND_MESSAGES_IN_THREADS);
} else {
$can_send = Permissions::canSendInChannel($current_user_id, $active_channel_id);
}
$show_input = true;
if ($channel_type === 'rules') $show_input = false;
if ($channel_type === 'forum' && !$active_thread) $show_input = false;
if (($channel_type === 'announcement' || $channel_type === 'text') && !$can_manage_channels) $show_input = false;
if ($show_input):
if (!$can_send) {
echo '<div class="chat-input-wrapper justify-content-center p-3 text-muted small" style="background-color: #2b2d31; border-radius: 8px;">
<i class="fa-solid fa-lock me-2"></i> Vous ne disposez pas de la permission d\'envoyer des messages dans ce salon.
</div>';
} else {
$allow_files = true;
foreach($channels as $c) {
if($c['id'] == $active_channel_id) {
$allow_files = (bool)$c['allow_file_sharing'];
break;
}
}
?>
<div id="upload-progress-container" class="upload-progress-container" style="display: none;">
<div class="progress" style="height: 4px; background-color: var(--separator); 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): ?>
<label for="file-upload" class="upload-btn-label" title="Télécharger un fichier" <?php echo (isset($active_thread) && $active_thread['is_locked']) ? 'style="opacity: 0.3; cursor: not-allowed;"' : ''; ?>>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
</label>
<input type="file" id="file-upload" style="display: none;" <?php echo (isset($active_thread) && $active_thread['is_locked']) ? 'disabled' : ''; ?>>
<?php else: ?>
<div class="upload-btn-label disabled" title="Partage de fichiers désactivé" style="opacity: 0.3; cursor: not-allowed;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="8" x2="16" y2="16"></line><line x1="16" y1="8" x2="8" y2="16"></line></svg>
</div>
<?php endif; ?>
<?php if (isset($active_thread) && $active_thread['is_locked']): ?>
<textarea id="chat-input" class="chat-input" placeholder="Cette discussion est verrouillée." autocomplete="off" rows="1" disabled style="background-color: rgba(0,0,0,0.1); cursor: not-allowed;"></textarea>
<?php else: ?>
<textarea id="chat-input" class="chat-input" placeholder="Envoyer un message dans #<?php echo htmlspecialchars($current_channel_name); ?>" autocomplete="off" rows="1"></textarea>
<?php endif; ?>
<button type="button" class="btn border-0 text-muted p-2" id="chat-emoji-btn" title="Sélecteur d'emojis" <?php echo (isset($active_thread) && $active_thread['is_locked']) ? 'disabled style="opacity: 0.5;"' : ''; ?>>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M8 14s1.5 2 4 2 4-2 4-2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg>
</button>
</div>
</form>
<?php } ?>
<?php endif; ?>
</div>
</div>
<!-- Members Sidebar -->
<div class="members-sidebar">
<div style="color: var(--text-muted); font-size: 0.75em; text-transform: uppercase; font-weight: bold; margin-bottom: 16px;">
Membres — <?php echo count($members); ?>
</div>
<?php foreach($members as $m): ?>
<div class="channel-item member-item" data-user-id="<?php echo $m['id']; ?>" data-username="<?php echo htmlspecialchars($m['username']); ?>" data-avatar="<?php echo htmlspecialchars($m['avatar_url'] ?? ''); ?>" data-role-ids="<?php echo $m['role_ids'] ?? ''; ?>" style="color: var(--text-primary); margin-bottom: 8px; cursor: pointer;">
<div class="message-avatar" style="width: 32px; height: 32px; background-color: <?php echo $m['status'] == 'online' ? '#23a559' : '#80848e'; ?>; position: relative; <?php echo $m['avatar_url'] ? "background-image: url('{$m['avatar_url']}');" : ""; ?>">
<?php if($m['status'] == 'online'): ?>
<div style="position: absolute; bottom: 0; right: 0; width: 10px; height: 10px; background-color: #23a559; border-radius: 50%; border: 2px solid var(--bg-members);"></div>
<?php endif; ?>
</div>
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; <?php echo !empty($m['role_color']) ? "color: {$m['role_color']};" : ""; ?>">
<?php echo htmlspecialchars($m['username']); ?>
<?php echo renderRoleIcon($m['role_icon'], '14px'); ?>
</span>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Paramètres utilisateur Modal -->
<div class="modal fade" id="userParamètresModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content border-0 shadow-lg" style="min-height: 500px;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">Paramètres utilisateur</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<div class="d-flex flex-row h-100" style="min-height: 450px;">
<!-- Paramètres Sidebar -->
<div class="settings-nav-sidebar p-3 border-end border-secondary" style="width: 200px;">
<ul class="nav flex-column nav-pills" id="userParamètresTabs" role="tablist">
<li class="nav-item mb-1">
<button class="nav-link active w-100 text-start border-0 py-2 px-3" data-bs-toggle="pill" data-bs-target="#settings-profile" type="button">Mon profil</button>
</li>
<li class="nav-item mb-1">
<button class="nav-link w-100 text-start border-0 py-2 px-3" data-bs-toggle="pill" data-bs-target="#settings-appearance" type="button">Apparence</button>
</li>
<li class="nav-item mb-1">
<button class="nav-link w-100 text-start border-0 py-2 px-3" data-bs-toggle="pill" data-bs-target="#settings-voice" type="button">Voix & Vidéo</button>
</li>
<li class="nav-item mb-1">
<button class="nav-link w-100 text-start border-0 py-2 px-3" data-bs-toggle="pill" data-bs-target="#settings-whispers" type="button" onclick="loadWhisperParamètres()">Whispers (TeamSpeak Style)</button>
</li>
<li class="nav-item mb-1">
<button class="nav-link w-100 text-start border-0 py-2 px-3" data-bs-toggle="pill" data-bs-target="#settings-notifications" type="button">Notifications</button>
</li>
</ul>
</div>
<!-- Paramètres Content -->
<div class="flex-grow-1 p-4 overflow-auto custom-scrollbar">
<form id="user-settings-form">
<div class="tab-content" id="userParamètresContent">
<!-- Profile Tab -->
<div class="tab-pane fade show active" id="settings-profile" role="tabpanel">
<h5 class="mb-4 fw-bold text-uppercase" style="font-size: 0.8em; color: var(--text-muted);">Profil utilisateur</h5>
<div class="row align-items-center mb-4 p-3 rounded settings-section-bg">
<div class="col-md-3 text-center">
<div class="message-avatar mx-auto mb-2" id="settings-avatar-preview" style="width: 80px; height: 80px; <?php echo $user['avatar_url'] ? "background-image: url('{$user['avatar_url']}');" : ""; ?>"></div>
<input type="hidden" name="avatar_url" id="settings-avatar-url" value="<?php echo htmlspecialchars($user['avatar_url'] ?? ''); ?>">
</div>
<div class="col-md-9">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Nom d'affichage</label>
<input type="text" name="display_name" class="form-control bg-dark text-white border-0" value="<?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?>" required>
</div>
</div>
</div>
<div class="mb-4">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Rechercher des avatars</label>
<div class="input-group mb-2 shadow-sm">
<input type="text" id="avatar-search-query" class="form-control bg-dark border-0 text-white" placeholder="ex: chat, abstrait, gamer">
<button class="btn btn-primary px-3" type="button" id="search-avatar-btn">Rechercher</button>
</div>
<div id="avatar-results" class="d-flex flex-wrap gap-2 overflow-auto p-2 rounded settings-inner-bg" style="max-height: 180px;">
<div class="w-100 text-center text-muted py-3 small">Recherchez des images pour changer votre avatar.</div>
</div>
</div>
</div>
<!-- Apparence Tab -->
<div class="tab-pane fade" id="settings-appearance" role="tabpanel">
<h5 class="mb-4 fw-bold text-uppercase" style="font-size: 0.8em; color: var(--text-muted);">Paramètres d'apparence</h5>
<div class="row g-3">
<div class="col-6">
<input type="radio" class="btn-check" name="theme" id="theme-dark" value="dark" <?php echo ($user['theme'] ?? 'dark') == 'dark' ? 'checked' : ''; ?> onchange="document.body.setAttribute('data-theme', 'dark')">
<label class="btn btn-outline-secondary w-100 py-4 d-flex flex-column align-items-center" for="theme-dark">
<i class="fa-solid fa-moon mb-2 fs-3"></i>
<span>Thème sombre</span>
</label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="theme" id="theme-light" value="light" <?php echo ($user['theme'] ?? 'dark') == 'light' ? 'checked' : ''; ?> onchange="document.body.setAttribute('data-theme', 'light')">
<label class="btn btn-outline-secondary w-100 py-4 d-flex flex-column align-items-center" for="theme-light">
<i class="fa-solid fa-sun mb-2 fs-3"></i>
<span>Thème clair</span>
</label>
</div>
</div>
</div>
<!-- Voice Tab -->
<div class="tab-pane fade" id="settings-voice" role="tabpanel">
<h5 class="mb-4 fw-bold text-uppercase" style="font-size: 0.8em; color: var(--text-muted);">Paramètres de voix</h5>
<div class="row mb-4">
<div class="col-md-6 mb-3">
<label class="form-label text-uppercase fw-bold mb-2" style="font-size: 0.7em; color: var(--text-muted);">Périphérique d'entrée</label>
<select name="voice_input_device" id="voice_input_device" class="form-select bg-dark text-white border-0">
<option value="default">Défaut</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label text-uppercase fw-bold mb-2" style="font-size: 0.7em; color: var(--text-muted);">Périphérique de sortie</label>
<select name="voice_output_device" id="voice_output_device" class="form-select bg-dark text-white border-0">
<option value="default">Défaut</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label text-uppercase fw-bold mb-2" style="font-size: 0.7em; color: var(--text-muted);">Volume d'entrée</label>
<input type="range" name="voice_input_volume" id="voice_input_volume" class="form-range" min="0" max="1" step="0.01" value="1.0">
<script>document.getElementById('voice_input_volume').value = localStorage.getItem('voice_input_volume') || 1.0;</script>
</div>
<div class="col-md-6 mb-3">
<label class="form-label text-uppercase fw-bold mb-2" style="font-size: 0.7em; color: var(--text-muted);">Volume de sortie</label>
<input type="range" name="voice_output_volume" id="voice_output_volume" class="form-range" min="0" max="2" step="0.01" value="1.0">
<script>document.getElementById('voice_output_volume').value = localStorage.getItem('voice_output_volume') || 1.0;</script>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" name="voice_echo_cancellation" id="echo-cancellation-switch" value="1" <?php echo ($user['voice_echo_cancellation'] ?? 1) ? 'checked' : ''; ?>>
<label class="form-check-label" for="echo-cancellation-switch">Annulation de l'écho</label>
</div>
<div class="form-text text-muted small mb-3">Réduit l'écho causé par vos haut-parleurs captés par votre micro.</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" name="voice_noise_suppression" id="noise-suppression-switch" value="1" <?php echo ($user['voice_noise_suppression'] ?? 1) ? 'checked' : ''; ?>>
<label class="form-check-label" for="noise-suppression-switch">Suppression du bruit</label>
</div>
<div class="form-text text-muted small mb-3">Filtre les bruits de fond comme les ventilateurs ou les clics de clavier.</div>
</div>
</div>
<div class="p-3 rounded mb-4 settings-section-bg">
<label class="form-label text-uppercase fw-bold mb-3" style="font-size: 0.7em; color: var(--text-muted);">Mode d'entrée</label>
<div class="d-flex gap-3 mb-4">
<div class="form-check custom-radio-card flex-grow-1">
<input class="form-check-input d-none" type="radio" name="voice_mode" id="voice-mode-vox" value="vox" <?php echo ($user['voice_mode'] ?? 'vox') == 'vox' ? 'checked' : ''; ?> onchange="togglePTTParamètres('vox')">
<label class="form-check-label w-100 p-3 rounded border border-secondary text-center cursor-pointer" for="voice-mode-vox" style="cursor: pointer;">
<i class="fa-solid fa-microphone mb-2 d-block"></i>
Activité vocale
</label>
</div>
<div class="form-check custom-radio-card flex-grow-1">
<input class="form-check-input d-none" type="radio" name="voice_mode" id="voice-mode-ptt" value="ptt" <?php echo ($user['voice_mode'] ?? 'vox') == 'ptt' ? 'checked' : ''; ?> onchange="togglePTTParamètres('ptt')">
<label class="form-check-label w-100 p-3 rounded border border-secondary text-center cursor-pointer" for="voice-mode-ptt" style="cursor: pointer;">
<i class="fa-solid fa-keyboard mb-2 d-block"></i>
Appuyer pour parler
</label>
</div>
</div>
<div id="ptt-settings-container" style="<?php echo ($user['voice_mode'] ?? 'vox') == 'ptt' ? '' : 'display: none;'; ?>">
<div class="mb-3">
<label class="form-label small fw-bold">Raccourci clavier</label>
<input type="text" name="voice_ptt_key" id="voice_ptt_key_input" class="form-control bg-dark text-white border-0" value="<?php echo htmlspecialchars($user['voice_ptt_key'] ?? 'v'); ?>" placeholder="Click and press a key..." readonly style="cursor: pointer; caret-color: transparent;">
<div class="form-text text-muted" style="font-size: 0.8em;">Click the box and press any key to set your PTT shortcut.</div>
</div>
</div>
<div id="vox-settings-container" style="<?php echo ($user['voice_mode'] ?? 'vox') == 'vox' ? '' : 'display: none;'; ?>">
<div class="mb-3">
<label class="form-label small fw-bold">Input Sensitivity</label>
<div class="voice-meter-container mb-2" style="height: 8px; background: var(--bg-servers); border-radius: 4px; overflow: hidden; position: relative;">
<div id="voice-meter-bar" style="height: 100%; width: 0%; background: #23a559; transition: width 0.1s;"></div>
<div id="voice-meter-threshold" style="position: absolute; top: 0; bottom: 0; width: 2px; background: #f23f43; z-index: 2;"></div>
</div>
<input type="range" name="voice_vox_threshold" id="vox_threshold_input" class="form-range" min="0" max="1" step="0.01" value="<?php echo $user['voice_vox_threshold'] ?? 0.1; ?>">
<div class="d-flex justify-content-between small text-muted mt-1">
<span>Sensitive</span>
<span>Loud Only</span>
</div>
</div>
</div>
</div>
<div class="p-3 rounded border border-info border-opacity-25" style="background-color: rgba(0, 168, 252, 0.05);">
<div class="d-flex">
<i class="fa-solid fa-circle-info text-info me-3 mt-1"></i>
<div>
<div class="fw-bold text-info small mb-1">Microphone Access</div>
<div class="text-muted small">Voice channels require microphone permission. If you don't hear anything, check your browser's site settings.</div>
</div>
</div>
</div>
</div>
<!-- Whispers Tab -->
<div class="tab-pane fade" id="settings-whispers" role="tabpanel">
<h5 class="mb-4 fw-bold text-uppercase" style="font-size: 0.8em; color: var(--text-muted);">Whisper Configurations</h5>
<div class="p-3 rounded mb-4 settings-section-bg">
<p class="small text-muted mb-3">Whisper allows you to talk to specific users or entire channels regardless of which channel you are currently in. This only works in Push-to-Talk mode for the whisper itself.</p>
<div id="whisper-list" class="mb-4">
<!-- Whisper entries will be loaded here -->
<div class="text-center py-3 text-muted small">Loading whispers...</div>
</div>
<hr class="border-secondary my-4">
<h6 class="small fw-bold mb-3">Add New Whisper</h6>
<div class="row g-2">
<div class="col-md-4">
<label class="form-label small text-muted">Target Type</label>
<select id="new-whisper-type" class="form-select bg-dark text-white border-0" onchange="updateWhisperTargetOptions()">
<option value="user">User</option>
<option value="channel">Channel</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label small text-muted">Target</label>
<select id="new-whisper-target" class="form-select bg-dark text-white border-0">
<!-- Options populated dynamically -->
</select>
</div>
<div class="col-md-3">
<label class="form-label small text-muted">Hotkeys</label>
<input type="text" id="new-whisper-key" class="form-control bg-dark text-white border-0" placeholder="Press a key..." readonly style="cursor: pointer; caret-color: transparent;">
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="button" class="btn btn-primary w-100" onclick="addWhisperSetting()"><i class="fa-solid fa-plus"></i></button>
</div>
</div>
</div>
<div class="p-3 rounded border border-warning border-opacity-25" style="background-color: rgba(255, 170, 0, 0.05);">
<div class="d-flex">
<i class="fa-solid fa-triangle-exclamation text-warning me-3 mt-1"></i>
<div>
<div class="fw-bold text-warning small mb-1">Whisper Notice</div>
<div class="text-muted small">Whispering uses additional bandwidth and connections. Avoid setting too many whisper hotkeys if you have a slow connection.</div>
</div>
</div>
</div>
</div>
<!-- Notifications Tab -->
<div class="tab-pane fade" id="settings-notifications" role="tabpanel">
<h5 class="mb-4 fw-bold text-uppercase" style="font-size: 0.8em; color: var(--text-muted);">Paramètres de notifications</h5>
<div class="mb-4 p-3 rounded settings-section-bg">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="dnd_mode" id="dnd-mode-switch" value="1" <?php echo ($user['dnd_mode'] ?? 0) ? 'checked' : ''; ?>>
<label class="form-check-label fw-bold" for="dnd-mode-switch">Mode Ne pas déranger</label>
<div class="form-text text-muted small">Désactive toutes les notifications sonores et visuelles.</div>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="sound_notifications" id="sound-notifications-switch" value="1" <?php echo ($user['sound_notifications'] ?? 1) ? 'checked' : ''; ?>>
<label class="form-check-label fw-bold" for="sound-notifications-switch">Notifications sonores</label>
<div class="form-text text-muted small">Joue un son lors de la réception d'un nouveau message.</div>
</div>
</div>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="sound_notifications" id="sound-switch" value="1" <?php echo ($user['sound_notifications'] ?? 0) ? 'checked' : ''; ?>>
<label class="form-check-label" for="sound-switch">Sound Notifications</label>
<div class="form-text text-muted" style="font-size: 0.8em;">Play a sound when you are mentioned.</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="modal-footer border-0 settings-footer-bg">
<button type="button" class="btn btn-link text-decoration-none settings-cancel-btn" data-bs-dismiss="modal">Cancel</button>
<button type="button" onclick="handleSaveUserParamètres(this)" class="btn btn-primary" style="background-color: var(--blurple); border: none; padding: 10px 32px; font-weight: 600;">Save Changes</button>
</div>
</div>
</div>
</div>
<style>
.custom-radio-card input:checked + label {
background-color: var(--blurple) !important;
border-color: var(--blurple) !important;
color: white !important;
}
.custom-radio-card label:hover {
background-color: rgba(255,255,255,0.05);
}
.cursor-pointer { cursor: pointer; }
</style>
<script>
function togglePTTParamètres(mode) {
console.log('Toggling voice mode to:', mode);
const pttContainer = document.getElementById('ptt-settings-container');
const voxContainer = document.getElementById('vox-settings-container');
if (pttContainer) pttContainer.style.display = (mode === 'ptt' ? 'block' : 'none');
if (voxContainer) voxContainer.style.display = (mode === 'vox' ? 'block' : 'none');
}
// Special handler for PTT key input to make it more intuitive
document.addEventListener('DOMContentLoaded', () => {
const pttInput = document.getElementById('voice_ptt_key_input');
if (pttInput) {
pttInput.addEventListener('keydown', (e) => {
e.preventDefault();
pttInput.value = e.key;
});
}
// Voice meter update
const voxThresholdInput = document.getElementById('vox_threshold_input');
const meterThreshold = document.getElementById('voice-meter-threshold');
const meterBar = document.getElementById('voice-meter-bar');
// Handle voice tab activation for mic preview and device list
const voiceTabBtn = document.querySelector('[data-bs-target="#settings-voice"]');
if (voiceTabBtn) {
voiceTabBtn.addEventListener('shown.bs.tab', async () => {
// Populate devices
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const inputSelect = document.getElementById('voice_input_device');
const outputSelect = document.getElementById('voice_output_device');
if (inputSelect) {
const current = inputSelect.value;
inputSelect.innerHTML = '';
devices.filter(d => d.kind === 'audioinput').forEach(d => {
const opt = document.createElement('option');
opt.value = d.deviceId;
opt.text = d.label || `Microphone (${d.deviceId.slice(0, 5)}...)`;
if (d.deviceId === current || d.deviceId === (localStorage.getItem('voice_input_device') || 'default')) opt.selected = true;
inputSelect.add(opt);
});
}
if (outputSelect) {
const current = outputSelect.value;
outputSelect.innerHTML = '';
devices.filter(d => d.kind === 'audiooutput').forEach(d => {
const opt = document.createElement('option');
opt.value = d.deviceId;
opt.text = d.label || `Speaker (${d.deviceId.slice(0, 5)}...)`;
if (d.deviceId === current || d.deviceId === (localStorage.getItem('voice_output_device') || 'default')) opt.selected = true;
outputSelect.add(opt);
});
}
} catch (e) {
console.error('Failed to enumerate devices:', e);
}
if (window.voiceHandler) {
if (!window.voiceHandler.localStream) {
try {
console.log('Voice tab active, requesting mic for preview...');
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
window.voiceHandler.localStream = stream;
window.voiceHandler.setupVOX();
} catch (e) {
console.error('Failed to get mic for preview:', e);
}
} else {
console.log('Voice tab active, using existing localStream for preview');
window.voiceHandler.setupVOX();
}
}
});
}
if (voxThresholdInput && meterThreshold) {
const updateThresholdPos = () => {
// Threshold input is 0 (loud) to 1 (quiet)
// But actually 1.0 means high threshold, so quiet needs more voice
// 0.1 means low threshold, easy to trigger.
// Meter is 0 to 100%.
meterThreshold.style.left = (voxThresholdInput.value * 100) + '%';
};
voxThresholdInput.addEventListener('input', updateThresholdPos);
updateThresholdPos();
}
setInterval(() => {
if (window.voiceHandler && meterBar && document.getElementById('settings-voice').classList.contains('active')) {
const volume = window.voiceHandler.getVolume(); // 0 to 1
meterBar.style.width = (volume * 100) + '%';
// Color feedback
const threshold = parseFloat(voxThresholdInput.value);
if (volume > threshold) {
meterBar.style.backgroundColor = '#23a559'; // Green
} else {
meterBar.style.backgroundColor = '#4f545c'; // Grey
}
}
}, 50);
});
function handlePTTKeyCapture(e, input) {
e.preventDefault();
input.value = e.key;
}
async function handleSaveUserParamètres(btn) {
const originalContent = btn.innerHTML;
const form = document.getElementById('user-settings-form');
if (!form) return;
if (!form.reportValidity()) return;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> Saving...';
const formData = new FormData(form);
// Ensure switches are correctly sent as 1/0
const dndMode = document.getElementById('dnd-switch')?.checked ? '1' : '0';
const soundNotifications = document.getElementById('sound-switch')?.checked ? '1' : '0';
const echoCancellation = document.getElementById('echo-cancellation-switch')?.checked ? '1' : '0';
const noiseSuppression = document.getElementById('noise-suppression-switch')?.checked ? '1' : '0';
formData.set('dnd_mode', dndMode);
formData.set('sound_notifications', soundNotifications);
formData.set('voice_echo_cancellation', echoCancellation);
formData.set('voice_noise_suppression', noiseSuppression);
// Explicitly get theme and voice_mode to ensure they are captured
const themeInput = form.querySelector('input[name="theme"]:checked');
if (themeInput) formData.set('theme', themeInput.value);
const voiceModeInput = form.querySelector('input[name="voice_mode"]:checked');
if (voiceModeInput) formData.set('voice_mode', voiceModeInput.value);
try {
const resp = await fetch('api_v1_user.php?v=' + Date.now(), {
method: 'POST',
body: formData
});
const result = await resp.json();
if (result.success) {
btn.innerHTML = '<i class="fa-solid fa-check me-2"></i> Saved!';
// Update current username in window
if (formData.get('display_name')) {
window.currentUsername = formData.get('display_name');
}
// Update local voiceHandler settings without reload
if (window.voiceHandler) {
const mode = formData.get('voice_mode');
const pttKey = formData.get('voice_ptt_key');
const voxThreshold = parseFloat(formData.get('voice_vox_threshold'));
window.voiceHandler.settings.mode = mode;
window.voiceHandler.settings.pttKey = pttKey;
window.voiceHandler.settings.voxThreshold = voxThreshold;
window.voiceHandler.settings.echoCancellation = echoCancellation === '1';
window.voiceHandler.settings.noiseSuppression = noiseSuppression === '1';
// New settings
const inputDevice = document.getElementById('voice_input_device')?.value;
const outputDevice = document.getElementById('voice_output_device')?.value;
const inputVol = document.getElementById('voice_input_volume')?.value;
const outputVol = document.getElementById('voice_output_volume')?.value;
if (inputDevice) window.voiceHandler.setInputDevice(inputDevice);
if (outputDevice) window.voiceHandler.setOutputDevice(outputDevice);
if (inputVol) window.voiceHandler.setInputVolume(inputVol);
if (outputVol) window.voiceHandler.setOutputVolume(outputVol);
// Re-apply constraints if echo/noise changed
window.voiceHandler.updateAudioConstraints();
// Persist client-side settings
localStorage.setItem('voice_input_device', inputDevice);
localStorage.setItem('voice_output_device', outputDevice);
localStorage.setItem('voice_input_volume', inputVol);
localStorage.setItem('voice_output_volume', outputVol);
console.log('Voice settings updated locally:', window.voiceHandler.settings);
if (mode === 'vox' && !window.voiceHandler.audioContext) {
window.voiceHandler.setupVOX();
}
window.voiceHandler.updateVoiceUI();
}
setTimeout(() => {
btn.innerHTML = originalContent;
btn.disabled = false;
// Optional: close modal after save?
// bootstrap.Modal.getInstance(document.getElementById('userParamètresModal')).hide();
}, 1500);
} else {
alert('Error: ' + (result.error || 'Unknown error'));
btn.disabled = false;
btn.innerHTML = originalContent;
}
} catch (e) {
console.error('Save error:', e);
alert('Connection error. Please try again.');
btn.disabled = false;
btn.innerHTML = originalContent;
}
}
// Whisper Logic
async function loadWhisperParamètres() {
const listEl = document.getElementById('whisper-list');
listEl.innerHTML = '<div class="text-center py-3 text-muted small"><span class="spinner-border spinner-border-sm me-2"></span> Loading whispers...</div>';
try {
const [whispersResp, usersResp, channelsResp] = await Promise.all([
fetch('api_v1_voice.php?action=get_whispers'),
fetch('api_v1_user.php?action=list_all'),
fetch('api_v1_channels.php?action=list_all')
]);
const whispers = await whispersResp.json();
const users = await usersResp.json();
const channels = await channelsResp.json();
// Populate target selector
updateWhisperTargetOptions(users.users || [], channels.channels || []);
// Store globally for mapping names
window.whisperUtilisateursMap = {};
if (users.users) users.users.forEach(u => window.whisperUtilisateursMap[u.id] = u.display_name || u.username);
window.whisperChannelsMap = {};
if (channels.channels) channels.channels.forEach(c => window.whisperChannelsMap[c.id] = c.name);
if (whispers.success && whispers.whispers.length > 0) {
listEl.innerHTML = '';
whispers.whispers.forEach(w => {
const targetName = w.target_type === 'user' ? (window.whisperUtilisateursMap[w.target_id] || 'User #'+w.target_id) : ('#' + (window.whisperChannelsMap[w.target_id] || w.target_id));
const row = document.createElement('div');
row.className = 'd-flex justify-content-between align-items-center p-2 mb-1 rounded bg-dark border-start border-3 border-info';
row.innerHTML = `
<div class="d-flex align-items-center">
<span class="badge bg-info me-2 text-uppercase" style="font-size: 0.7em;">${w.target_type}</span>
<span class="text-white small fw-bold">${targetName}</span>
<span class="text-muted small ms-3">Key: <span class="badge bg-secondary">${w.whisper_key}</span></span>
</div>
<button class="btn btn-sm text-danger border-0 p-1" onclick="deleteWhisperSetting(${w.id})"><i class="fa-solid fa-trash-can"></i></button>
`;
listEl.appendChild(row);
});
} else {
listEl.innerHTML = '<div class="text-center py-3 text-muted small">No whispers configured yet.</div>';
}
// Re-initialize whisper handlers in voiceHandler if active
if (window.voiceHandler) {
window.voiceHandler.whisperParamètres = whispers.whispers || [];
window.voiceHandler.setupWhisperListeners();
}
} catch (e) {
console.error('Failed to load whispers:', e);
listEl.innerHTML = '<div class="text-center py-3 text-danger small">Error loading settings.</div>';
}
}
function updateWhisperTargetOptions(users, channels) {
const type = document.getElementById('new-whisper-type').value;
const targetSelect = document.getElementById('new-whisper-target');
targetSelect.innerHTML = '';
if (type === 'user') {
const usersList = users || Object.values(window.whisperUtilisateursMap || {}).map((name, id) => ({id, username: name}));
usersList.forEach(u => {
if (u.id == window.currentUserId) return;
const opt = document.createElement('option');
opt.value = u.id;
opt.text = u.display_name || u.username;
targetSelect.add(opt);
});
} else {
const channelsList = channels || Object.values(window.whisperChannelsMap || {}).map((name, id) => ({id, name}));
channelsList.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.text = '#' + c.name;
targetSelect.add(opt);
});
}
}
async function addWhisperSetting() {
const type = document.getElementById('new-whisper-type').value;
const targetId = document.getElementById('new-whisper-target').value;
const key = document.getElementById('new-whisper-key').value;
if (!targetId || !key) return alert('Please select a target and press a key.');
try {
const resp = await fetch('api_v1_voice.php?action=save_whisper&target_type='+type+'&target_id='+targetId+'&key='+encodeURIComponent(key));
const data = await resp.json();
if (data.success) {
document.getElementById('new-whisper-key').value = '';
loadWhisperParamètres();
} else {
alert('Error: ' + data.error);
}
} catch (e) { console.error(e); }
}
async function deleteWhisperSetting(id) {
if (!confirm('Supprimer this whisper configuration?')) return;
try {
const resp = await fetch('api_v1_voice.php?action=delete_whisper&id='+id);
const data = await resp.json();
if (data.success) {
loadWhisperParamètres();
}
} catch (e) { console.error(e); }
}
// Hotkey capture for new whisper
document.addEventListener('DOMContentLoaded', () => {
const whisperKeyInput = document.getElementById('new-whisper-key');
if (whisperKeyInput) {
whisperKeyInput.addEventListener('keydown', (e) => {
e.preventDefault();
whisperKeyInput.value = e.key;
});
}
});
</script>
<!-- Paramètres du serveur Modal -->
<!-- Paramètres du serveur Modal -->
<div class="modal fade" id="serverParamètresModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Paramètres du serveur</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<ul class="nav nav-tabs nav-fill" id="serverParamètresTabs" role="tablist">
<li class="nav-item">
<button class="nav-link active text-white border-0 bg-transparent" data-bs-toggle="tab" data-bs-target="#settings-general" type="button">Général</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="emotes-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-emotes" type="button">Emotes</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="roles-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-roles" type="button">Rôles</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="webhooks-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-webhooks" type="button">Webhooks</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="members-tab-btn" data-bs-toggle="tab" data-bs-target="#settings-members" type="button">Membres</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">
<form action="api_v1_servers.php" method="POST" id="server-settings-form">
<input type="hidden" name="action" value="update">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<div class="mb-3 text-center">
<?php
$active_icon = '';
foreach($servers as $s) if($s['id'] == $active_server_id) $active_icon = $s['icon_url'];
?>
<div class="message-avatar mx-auto mb-2" id="server-icon-preview" style="width: 80px; height: 80px; <?php echo $active_icon ? "background-image: url('{$active_icon}');" : ""; ?>"></div>
<input type="hidden" name="icon_url" id="server-icon-url" value="<?php echo htmlspecialchars($active_icon); ?>">
<button type="button" class="btn btn-sm btn-outline-secondary" id="search-server-icon-btn">Changer l'icône</button>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Nom du serveur</label>
<input type="text" name="name" class="form-control" value="<?php echo htmlspecialchars($active_server_name ?? ''); ?>" required>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Couleur du thème</label>
<input type="color" name="theme_color" class="form-control form-control-color w-100" value="<?php echo $active_server['theme_color'] ?? '#5865f2'; ?>" title="Choisir la couleur du thème du serveur">
</div>
<div id="server-icon-search-results" class="d-flex flex-wrap gap-2 mb-3 overflow-auto" style="max-height: 150px;"></div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Code d'invitation</label>
<?php
$invite = '';
$expires_at = '';
foreach($servers as $s) {
if($s['id'] == $active_server_id) {
$invite = $s['invite_code'];
$expires_at = $s['invite_code_expires_at'] ? date('c', strtotime($s['invite_code_expires_at'])) : '';
}
}
?>
<div class="input-group mb-2">
<input type="text" id="server-invite-code" class="form-control bg-dark text-white border-0" value="<?php echo $invite; ?>" readonly>
<button class="btn btn-secondary" type="button" onclick="navigator.clipboard.writeText(document.getElementById('server-invite-code').value)">Copier</button>
<button class="btn btn-primary" type="button" id="refresh-invite-code-btn">Rafraîchir</button>
</div>
<div id="invite-code-timer" class="small text-muted" data-expires="<?php echo $expires_at; ?>">
<?php if ($expires_at): ?>
Expire dans : <span id="invite-timer-display">--:--</span>
<?php else: ?>
Pas d'expiration définie.
<?php endif; ?>
</div>
</div>
<hr class="border-secondary">
<button type="submit" class="btn btn-primary w-100 mb-2">Enregistrer les modifications</button>
</form>
<form action="api_v1_servers.php" method="POST" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer ce serveur ? Cette action est irréversible.');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<button type="submit" class="btn btn-danger w-100">Supprimer le serveur</button>
</form>
</div>
<div class="tab-pane fade" id="settings-roles">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Rôles du serveur</h6>
<button class="btn btn-sm btn-primary" id="add-role-btn">+ Ajouter un rôle</button>
</div>
<div id="roles-list" class="list-group list-group-flush bg-transparent">
<!-- Roles will be loaded here -->
</div>
</div>
<div class="tab-pane fade" id="settings-webhooks">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Webhooks</h6>
<button class="btn btn-sm btn-primary" id="add-webhook-btn">+ Créer un Webhook</button>
</div>
<div id="webhooks-list" class="list-group list-group-flush bg-transparent">
<!-- Webhooks will be loaded here -->
</div>
</div>
<div class="tab-pane fade" id="settings-members">
<h6 class="mb-3">Membres du serveur</h6>
<div id="server-members-list" class="list-group list-group-flush bg-transparent">
<!-- Members will be loaded here -->
</div>
</div>
<div class="tab-pane fade" id="settings-stats">
<div id="stats-content">
<div class="row text-center mb-4">
<div class="col-6">
<div class="p-3 rounded bg-dark">
<div class="small text-muted text-uppercase">Membres</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">Utilisateurs les plus actifs</h6>
<div id="top-users-list" class="mb-4">
<!-- Top users here -->
</div>
<h6 class="text-uppercase small text-muted mb-2">Activité (7 derniers jours)</h6>
<div id="activity-chart-placeholder" class="small text-muted text-center p-4 border border-secondary rounded">
<!-- Simplistic chart or list -->
Chargement de l'activité...
</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">Utilisateurs les plus actifs</h6>
<div id="top-users-list" class="mb-4">
<!-- Top users here -->
</div>
<h6 class="text-uppercase small text-muted mb-2">Activité (7 derniers jours)</h6>
<div id="activity-chart-placeholder" class="small text-muted text-center p-4 border border-secondary rounded">
<!-- Simplistic chart or list -->
Chargement de l'activité...
</div>
</div>
</div>
<div class="tab-pane fade" id="settings-emotes">
<div class="row g-0" style="height: 450px;">
<div class="col-3 border-end border-secondary overflow-auto custom-scrollbar" id="settings-emotes-sidebar" style="background-color: #2b2d31;">
<!-- Categories will be loaded here -->
</div>
<div class="col-9 d-flex flex-column" style="background-color: #313338;">
<div class="p-2 border-bottom border-secondary d-flex gap-2">
<input type="text" id="settings-emotes-search" class="form-control form-control-sm bg-dark border-secondary text-white" placeholder="Rechercher une emote...">
<div id="custom-emote-upload-zone" class="d-none">
<button class="btn btn-primary btn-sm" onclick="document.getElementById('emote-upload-input').click()">
<i class="fas fa-plus"></i> Ajouter
</button>
<input type="file" id="emote-upload-input" class="d-none" accept="image/png,image/jpeg">
</div>
</div>
<div id="settings-emotes-grid" class="flex-grow-1 overflow-auto p-3 custom-scrollbar" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); grid-auto-rows: min-content; align-content: start; gap: 10px;">
<!-- Emojis will be loaded here -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Server Modal -->
<div class="modal fade" id="addServerModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create or Join a server</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<ul class="nav nav-tabs nav-fill" id="serverTabs" role="tablist">
<li class="nav-item">
<button class="nav-link active text-white border-0 bg-transparent" id="create-tab" data-bs-toggle="tab" data-bs-target="#create-pane" type="button">Créer</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent" id="join-tab" data-bs-toggle="tab" data-bs-target="#join-pane" type="button">Rejoindre</button>
</li>
</ul>
<div class="tab-content p-3">
<div class="tab-pane fade show active" id="create-pane">
<form action="api_v1_servers.php" method="POST">
<p style="color: var(--text-muted); font-size: 0.9em;">Donnez une personnalité à votre nouveau serveur avec un nom.</p>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Nom du serveur</label>
<input type="text" name="name" class="form-control" placeholder="Mon super serveur" required>
</div>
<button type="submit" class="btn btn-primary w-100" style="background-color: var(--blurple); border: none;">Créer le serveur</button>
</form>
</div>
<div class="tab-pane fade" id="join-pane">
<form action="api_v1_servers.php" method="POST">
<input type="hidden" name="action" value="join">
<p style="color: var(--text-muted); font-size: 0.9em;">Entrez un code d'invitation pour rejoindre un serveur existant.</p>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Code d'invitation</label>
<input type="text" name="invite_code" class="form-control" placeholder="Ex: aB1!c2D3@4" required>
</div>
<button type="submit" class="btn btn-success w-100" style="background-color: #23a559; border: none;">Rejoindre le serveur</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Channel Modal -->
<div class="modal fade" id="addChannelModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Créer un salon</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="api_v1_channels.php" method="POST">
<input type="hidden" name="action" value="create">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<div class="modal-body">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Type de salon</label>
<select name="type" class="form-select bg-dark text-white border-secondary mb-3" id="add-channel-type">
<option value="chat">Salon textuel classique</option>
<option value="announcement">Annonces</option>
<option value="rules">Règles</option>
<option value="forum">Forum</option>
<option value="autorole">Autorôles</option>
<option value="voice">Salon vocal</option>
<option value="separator">Séparateur</option>
<option value="category">Catégorie</option>
</select>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Nom du salon</label>
<div class="input-group">
<span class="input-group-text bg-dark border-0 text-muted" id="add-channel-prefix">#</span>
<input type="text" name="name" class="form-control" placeholder="nouveau-salon" required>
</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Icône du salon</label>
<select name="icon" class="form-select bg-dark text-white border-secondary mb-3">
<option value="">Aucune icône personnalisée</option>
<option value="fa-hashtag"># Hashtag</option>
<option value="fa-volume-up">🔊 Voix</option>
<option value="fa-bullhorn">📢 Annonces</option>
<option value="fa-gavel">🔨 Règles</option>
<option value="fa-comments">💬 Forum</option>
<option value="fa-lock">🔒 Privé</option>
<option value="fa-star">⭐ Étoile</option>
<option value="fa-heart">❤️ Cœur</option>
<option value="fa-gamepad">🎮 Jeux</option>
<option value="fa-music">🎵 Musique</option>
<option value="fa-video">📹 Vidéo</option>
<option value="fa-info-circle"> Info</option>
<option value="fa-question-circle">❓ Aide</option>
<option value="fa-book">📖 Bibliothèque</option>
<option value="fa-gift">🎁 Giveaways</option>
<option value="fa-code">💻 Programmation</option>
<option value="fa-terminal">⌨️ Bot</option>
</select>
</div>
<div class="mb-3" id="add-channel-rules-role-container" style="display: none;">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Rôle accordé après acceptation</label>
<select name="rules_role_id" class="form-select bg-dark text-white border-secondary">
<option value="">Aucun rôle attribué</option>
<?php foreach($server_roles as $role): ?>
<option value="<?php echo $role['id']; ?>"><?php echo htmlspecialchars($role['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div id="add-channel-files-container">
<div class="form-check form-switch mb-3">
<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">Autoriser le partage de fichiers</label>
</div>
</div>
<div id="add-channel-limit-container">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Limite de messages</label>
<input type="number" name="message_limit" class="form-control" placeholder="ex: 50 (Laissez vide pour aucune limite)">
<div class="form-text text-muted" style="font-size: 0.8em;">Conserve automatiquement seulement les X derniers messages de ce salon.</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link text-white text-decoration-none" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary" style="background-color: var(--blurple); border: none; padding: 10px 24px;">Créer un salon</button>
</div>
</form>
</div>
</div>
</div>
<!-- Add Autorole Modal -->
<div class="modal fade" id="addAutoroleModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ajouter un Autorole</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="api_v1_autoroles.php" method="POST">
<input type="hidden" name="action" value="create">
<input type="hidden" name="channel_id" value="<?php echo $active_channel_id; ?>">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<div class="modal-body">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Icône (Emoji)</label>
<div class="d-flex align-items-center mb-2">
<div id="add-autorole-emoji-preview" class="d-flex align-items-center justify-content-center border rounded me-2" style="width: 48px; height: 48px; font-size: 24px; background: #1e1f22;">🚀</div>
<input type="hidden" name="icon" id="add-autorole-icon" value="🚀">
<button type="button" class="btn btn-outline-secondary" id="add-autorole-emoji-btn">Choisir un emoji...</button>
</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Titre du bouton</label>
<input type="text" name="title" class="form-control" placeholder="Ex: Joueur" required>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Rôle à attribuer</label>
<select name="role_id" class="form-select bg-dark text-white border-secondary" required>
<option value="">Sélectionnez un rôle</option>
<?php foreach($server_roles as $role): ?>
<option value="<?php echo $role['id']; ?>"><?php echo htmlspecialchars($role['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link text-white text-decoration-none" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary" style="background-color: var(--blurple); border: none;">Créer le bouton</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modifier Autorole Modal -->
<div class="modal fade" id="editAutoroleModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modifier l'Autorole</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="api_v1_autoroles.php" method="POST">
<input type="hidden" name="action" value="update">
<input type="hidden" name="id" id="edit-autorole-id">
<input type="hidden" name="channel_id" value="<?php echo $active_channel_id; ?>">
<input type="hidden" name="server_id" value="<?php echo $active_server_id; ?>">
<div class="modal-body">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Icône (Emoji)</label>
<div class="d-flex align-items-center mb-2">
<div id="edit-autorole-emoji-preview" class="d-flex align-items-center justify-content-center border rounded me-2" style="width: 48px; height: 48px; font-size: 24px; background: #1e1f22;"></div>
<input type="hidden" name="icon" id="edit-autorole-icon">
<button type="button" class="btn btn-outline-secondary" id="edit-autorole-emoji-btn">Changer l'emoji...</button>
</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Titre du bouton</label>
<input type="text" name="title" id="edit-autorole-title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Rôle à attribuer</label>
<select name="role_id" id="edit-autorole-role-id" class="form-select bg-dark text-white border-secondary" required>
<option value="">Sélectionnez un rôle</option>
<?php foreach($server_roles as $role): ?>
<option value="<?php echo $role['id']; ?>"><?php echo htmlspecialchars($role['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link text-white text-decoration-none" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary" style="background-color: var(--blurple); border: none;">Enregistrer les modifications</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modifier Channel Modal -->
<div class="modal fade" id="editChannelModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow-lg" style="background-color: #313338;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">Paramètres du salon — #<span id="header-channel-name"></span></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<ul class="nav nav-tabs nav-fill px-3 border-bottom border-secondary" id="editChannelTabs" role="tablist" style="background-color: #2b2d31;">
<li class="nav-item">
<button class="nav-link active text-white border-0 bg-transparent py-3" data-bs-toggle="tab" data-bs-target="#edit-channel-general" type="button">Vue d'ensemble</button>
</li>
<li class="nav-item" id="rss-tab-nav" style="display: none;">
<button class="nav-link text-white border-0 bg-transparent py-3" id="rss-tab-btn" data-bs-toggle="tab" data-bs-target="#edit-channel-rss" type="button">Flux RSS</button>
</li>
<li class="nav-item">
<button class="nav-link text-white border-0 bg-transparent py-3" 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-4" style="min-height: 450px;">
<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="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Nom du salon</label>
<div class="input-group">
<span class="input-group-text bg-dark border-secondary text-muted">#</span>
<input type="text" name="name" id="edit-channel-name" class="form-control bg-dark text-white border-secondary" required>
</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Type de salon</label>
<select name="type" id="edit-channel-type" class="form-select bg-dark text-white border-secondary">
<option value="chat">Salon textuel</option>
<option value="announcement">Annonces</option>
<option value="rules">Règles</option>
<option value="forum">Forum</option>
<option value="autorole">Autorôles</option>
<option value="voice">Salon vocal</option>
<option value="category">Catégorie</option>
<option value="separator">Séparateur</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Catégorie</label>
<select name="category_id" id="edit-channel-category-id" class="form-select bg-dark text-white border-secondary">
<option value="">Aucune catégorie</option>
<?php
foreach($all_channels as $cat) {
if ($cat['type'] === 'category') {
echo '<option value="'.$cat['id'].'">'.htmlspecialchars($cat['name']).'</option>';
}
}
?>
</select>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Icône</label>
<select name="icon" id="edit-channel-icon" class="form-select bg-dark text-white border-secondary">
<option value="">Aucune icône personnalisée</option>
<option value="fa-hashtag"># Hashtag</option>
<option value="fa-volume-up">🔊 Voix</option>
<option value="fa-bullhorn">📢 Annonces</option>
<option value="fa-gavel">🔨 Règles</option>
<option value="fa-comments">💬 Forum</option>
<option value="fa-lock">🔒 Privé</option>
<option value="fa-star">⭐ Étoile</option>
<option value="fa-heart">❤️ Cœur</option>
<option value="fa-gamepad">🎮 Jeux</option>
<option value="fa-music">🎵 Musique</option>
<option value="fa-video">📹 Vidéo</option>
<option value="fa-info-circle"> Info</option>
<option value="fa-question-circle">❓ Aide</option>
<option value="fa-book">📖 Bibliothèque</option>
<option value="fa-gift">🎁 Giveaways</option>
<option value="fa-code">💻 Programmation</option>
<option value="fa-terminal">⌨️ Bot</option>
</select>
</div>
<div class="mb-3" id="edit-channel-rules-role-container" style="display: none;">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Rôle accordé après acceptation</label>
<select name="rules_role_id" id="edit-channel-rules-role" class="form-select bg-dark text-white border-secondary">
<option value="">Aucun rôle attribué</option>
<?php foreach($server_roles as $role): ?>
<option value="<?php echo $role['id']; ?>"><?php echo htmlspecialchars($role['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<div class="mb-3" id="edit-channel-status-container" style="display: none;">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Statut vocal</label>
<input type="text" name="status" id="edit-channel-status" class="form-control bg-dark text-white border-secondary" placeholder="Que se passe-t-il ?">
</div>
<div id="edit-channel-limit-container">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Rétention des messages</label>
<input type="number" name="message_limit" id="edit-channel-limit" class="form-control bg-dark text-white border-secondary" placeholder="Conserver tous les messages">
</div>
</div>
<div id="edit-channel-files-container">
<div class="form-check form-switch mb-4">
<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">Autoriser les utilisateurs à télécharger des fichiers</label>
</div>
</div>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary flex-grow-1">Enregistrer les modifications</button>
<button type="button" id="clear-channel-history-btn" class="btn btn-outline-warning">Effacer le chat</button>
<button type="button" class="btn btn-outline-danger" id="delete-channel-trigger">Supprimer</button>
</div>
</form>
<div id="delete-confirm-zone" style="display: none;" class="mt-3 p-3 border border-danger rounded">
<p class="text-danger small mb-2">Êtes-vous sûr ? Cette action est irréversible.</p>
<form action="api_v1_channels.php" method="POST" class="d-flex gap-2">
<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 btn-sm">Oui, supprimer le salon</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="document.getElementById('delete-confirm-zone').style.display='none'">Annuler</button>
</form>
</div>
</div>
<div class="tab-pane fade" id="edit-channel-rss">
<div class="mb-4">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Ajouter un flux RSS</label>
<div class="input-group">
<input type="url" id="new-rss-url" class="form-control bg-dark text-white border-secondary" placeholder="https://exemple.com/feed.xml">
<button class="btn btn-primary" type="button" id="add-rss-btn">Ajouter</button>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label text-uppercase fw-bold mb-0" style="font-size: 0.7em; color: var(--text-muted);">Abonnements actifs</label>
<button class="btn btn-sm btn-outline-info" id="sync-rss-btn">Tout synchroniser</button>
</div>
<div id="rss-feeds-list" class="list-group list-group-flush bg-transparent border rounded border-secondary" style="max-height: 250px; overflow-y: auto;">
<!-- RSS feeds loaded here -->
</div>
</div>
<div class="tab-pane fade h-100" id="edit-channel-permissions">
<div class="row h-100 g-0 border rounded border-secondary overflow-hidden" style="background-color: #2b2d31;">
<!-- Sidebar: Roles & Members -->
<div class="col-4 border-end border-secondary d-flex flex-column" style="background-color: #2b2d31;">
<div class="p-3 border-bottom border-secondary d-flex justify-content-between align-items-center">
<span class="small fw-bold text-uppercase" style="font-size: 0.7em; color: #dbdee1;">Rôles / Membres</span>
<div class="d-flex align-items-center gap-2">
<div class="dropdown">
<button class="btn btn-sm btn-link text-white p-0" type="button" data-bs-toggle="dropdown" title="Ajouter un rôle ou un membre" style="text-decoration: none; opacity: 0.8;">
<i class="fa-solid fa-plus-circle" style="font-size: 1.1rem;"></i>
</button>
<ul class="dropdown-menu dropdown-menu-dark shadow border-secondary" id="add-permission-role-list" style="max-height: 300px; overflow-y: auto; min-width: 200px;">
<!-- Roles loaded here -->
</ul>
</div>
<div class="search-container position-relative" style="width: 80px;">
<input type="text" id="search-channel-perms" class="form-control form-control-sm bg-dark text-white border-0 py-0" style="font-size: 0.75em; border-radius: 4px;" placeholder="@everyone">
</div>
</div>
</div>
<div id="channel-permissions-roles-list" class="list-group list-group-flush overflow-auto flex-grow-1" style="max-height: 350px; overflow-x: hidden;">
<!-- List of roles with overrides -->
</div>
</div>
<!-- Main: Permission Paramètres -->
<div class="col-8 d-flex flex-column" style="background-color: #313338;">
<div id="channel-permissions-settings" class="h-100 d-flex flex-column d-none">
<div class="p-3 border-bottom border-secondary d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold" id="selected-perm-role-name">Nom du rôle</h6>
<button class="btn btn-sm btn-outline-danger py-0 px-2" id="remove-selected-perm-role" style="font-size: 0.75em;">Effacer les surcharges</button>
</div>
<div class="p-3 overflow-auto flex-grow-1" id="permissions-toggles-container" style="max-height: 350px; overflow-x: hidden;">
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Voir le salon</div>
<div style="font-size: 0.75em; color: #b5bac1;">Permet aux membres de voir ce salon.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="1">
<input type="radio" class="btn-check" name="perm_1" id="perm_1_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_1_deny" title="Refuser"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_1" id="perm_1_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_1_neutral" title="Neutre">/</label>
<input type="radio" class="btn-check" name="perm_1" id="perm_1_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_1_allow" title="Autoriser"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Envoyer des messages</div>
<div style="font-size: 0.75em; color: #b5bac1;">Permet aux membres d'envoyer des messages dans ce salon.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="2">
<input type="radio" class="btn-check" name="perm_2" id="perm_2_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_2_deny" title="Refuser"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_2" id="perm_2_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_2_neutral" title="Neutre">/</label>
<input type="radio" class="btn-check" name="perm_2" id="perm_2_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_2_allow" title="Autoriser"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Créer une discussion</div>
<div style="font-size: 0.75em; color: #b5bac1;">Permet aux membres de créer de nouvelles discussions.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="64">
<input type="radio" class="btn-check" name="perm_64" id="perm_64_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_64_deny" title="Refuser"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_64" id="perm_64_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_64_neutral" title="Neutre">/</label>
<input type="radio" class="btn-check" name="perm_64" id="perm_64_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_64_allow" title="Autoriser"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Gérer les tags</div>
<div style="font-size: 0.75em; color: #b5bac1;">Permet d'ajouter/modifier les tags du forum.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="128">
<input type="radio" class="btn-check" name="perm_128" id="perm_128_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_128_deny" title="Refuser"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_128" id="perm_128_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_128_neutral" title="Neutre">/</label>
<input type="radio" class="btn-check" name="perm_128" id="perm_128_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_128_allow" title="Autoriser"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Épingler des discussions</div>
<div style="font-size: 0.75em; color: #b5bac1;">Permet d'épingler/désépingler des discussions.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="256">
<input type="radio" class="btn-check" name="perm_256" id="perm_256_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_256_deny" title="Refuser"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_256" id="perm_256_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_256_neutral" title="Neutre">/</label>
<input type="radio" class="btn-check" name="perm_256" id="perm_256_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_256_allow" title="Autoriser"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Verrouiller des discussions</div>
<div style="font-size: 0.75em; color: #b5bac1;">Permet de verrouiller/déverrouiller des discussions.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="512">
<input type="radio" class="btn-check" name="perm_512" id="perm_512_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_512_deny" title="Refuser"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_512" id="perm_512_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_512_neutral" title="Neutre">/</label>
<input type="radio" class="btn-check" name="perm_512" id="perm_512_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_512_allow" title="Autoriser"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Poster dans les discussions</div>
<div style="font-size: 0.75em; color: #b5bac1;">Permet de poster dans les discussions non verrouillées.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="1024">
<input type="radio" class="btn-check" name="perm_1024" id="perm_1024_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_1024_deny" title="Refuser"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_1024" id="perm_1024_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_1024_neutral" title="Neutre">/</label>
<input type="radio" class="btn-check" name="perm_1024" id="perm_1024_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_1024_allow" title="Autoriser"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Parler</div>
<div style="font-size: 0.75em; color: #b5bac1;">Permet aux membres de parler dans ce salon vocal.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="2048">
<input type="radio" class="btn-check" name="perm_2048" id="perm_2048_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_2048_deny" title="Refuser"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_2048" id="perm_2048_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_2048_neutral" title="Neutre">/</label>
<input type="radio" class="btn-check" name="perm_2048" id="perm_2048_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_2048_allow" title="Autoriser"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<!-- More permissions can be added here -->
</div>
</div>
<div class="h-100 d-flex align-items-center justify-content-center text-muted p-4" id="no-role-selected-view">
<div class="text-center">
<i class="fa-solid fa-lock mb-3" style="font-size: 2.5rem; opacity: 0.2;"></i>
<p class="small mb-0">Sélectionnez un rôle ou un membre sur la gauche pour configurer ses permissions spécifiques pour ce salon.</p>
</div>
</div>
</div>
</div>
</div>
<div class="search-container position-relative" style="width: 80px;">
<input type="text" id="search-channel-perms" class="form-control form-control-sm bg-dark text-white border-0 py-0" style="font-size: 0.75em; border-radius: 4px;" placeholder="@everyone">
</div>
</div>
</div>
<div id="channel-permissions-roles-list" class="list-group list-group-flush overflow-auto flex-grow-1" style="max-height: 350px; overflow-x: hidden;">
<!-- List of roles with overrides -->
</div>
</div>
<!-- Main: Permission Paramètres -->
<div class="col-8 d-flex flex-column" style="background-color: #313338;">
<div id="channel-permissions-settings" class="h-100 d-flex flex-column d-none">
<div class="p-3 border-bottom border-secondary d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold" id="selected-perm-role-name">Role Name</h6>
<button class="btn btn-sm btn-outline-danger py-0 px-2" id="remove-selected-perm-role" style="font-size: 0.75em;">Clear Overrides</button>
</div>
<div class="p-3 overflow-auto flex-grow-1" id="permissions-toggles-container" style="max-height: 350px; overflow-x: hidden;">
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">View Channel</div>
<div style="font-size: 0.75em; color: #b5bac1;">Allows members to view this channel.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="1">
<input type="radio" class="btn-check" name="perm_1" id="perm_1_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_1_deny" title="Deny"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_1" id="perm_1_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_1_neutral" title="Neutral">/</label>
<input type="radio" class="btn-check" name="perm_1" id="perm_1_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_1_allow" title="Allow"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Send Messages</div>
<div style="font-size: 0.75em; color: #b5bac1;">Allows members to send messages in this channel.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="2">
<input type="radio" class="btn-check" name="perm_2" id="perm_2_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_2_deny" title="Deny"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_2" id="perm_2_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_2_neutral" title="Neutral">/</label>
<input type="radio" class="btn-check" name="perm_2" id="perm_2_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_2_allow" title="Allow"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Create Thread</div>
<div style="font-size: 0.75em; color: #b5bac1;">Allows members to create new discussions.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="64">
<input type="radio" class="btn-check" name="perm_64" id="perm_64_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_64_deny" title="Deny"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_64" id="perm_64_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_64_neutral" title="Neutral">/</label>
<input type="radio" class="btn-check" name="perm_64" id="perm_64_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_64_allow" title="Allow"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Manage Tags</div>
<div style="font-size: 0.75em; color: #b5bac1;">Allows adding/modifying forum tags.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="128">
<input type="radio" class="btn-check" name="perm_128" id="perm_128_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_128_deny" title="Deny"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_128" id="perm_128_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_128_neutral" title="Neutral">/</label>
<input type="radio" class="btn-check" name="perm_128" id="perm_128_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_128_allow" title="Allow"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Épingler Discussions</div>
<div style="font-size: 0.75em; color: #b5bac1;">Allows pinning/unpinning threads.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="256">
<input type="radio" class="btn-check" name="perm_256" id="perm_256_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_256_deny" title="Deny"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_256" id="perm_256_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_256_neutral" title="Neutral">/</label>
<input type="radio" class="btn-check" name="perm_256" id="perm_256_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_256_allow" title="Allow"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Verrouiller Discussions</div>
<div style="font-size: 0.75em; color: #b5bac1;">Allows locking/unlocking threads.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="512">
<input type="radio" class="btn-check" name="perm_512" id="perm_512_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_512_deny" title="Deny"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_512" id="perm_512_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_512_neutral" title="Neutral">/</label>
<input type="radio" class="btn-check" name="perm_512" id="perm_512_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_512_allow" title="Allow"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Send Messages in Discussions</div>
<div style="font-size: 0.75em; color: #b5bac1;">Allows posting in unlocked threads.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="1024">
<input type="radio" class="btn-check" name="perm_1024" id="perm_1024_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_1024_deny" title="Deny"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_1024" id="perm_1024_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_1024_neutral" title="Neutral">/</label>
<input type="radio" class="btn-check" name="perm_1024" id="perm_1024_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_1024_allow" title="Allow"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<div class="permission-item mb-3 p-2 rounded" style="background: var(--separator-soft);">
<div class="d-flex justify-content-between align-items-center">
<div class="pe-3">
<div class="fw-bold" style="color: #ffffff; font-size: 0.9em;">Speak</div>
<div style="font-size: 0.75em; color: #b5bac1;">Allows members to speak in this vocal channel.</div>
</div>
<div class="btn-group btn-group-sm perm-tri-state" data-perm-bit="2048">
<input type="radio" class="btn-check" name="perm_2048" id="perm_2048_deny" value="deny">
<label class="btn btn-outline-danger border-0" for="perm_2048_deny" title="Deny"><i class="fa-solid fa-xmark"></i></label>
<input type="radio" class="btn-check" name="perm_2048" id="perm_2048_neutral" value="neutral" checked>
<label class="btn btn-outline-secondary border-0" for="perm_2048_neutral" title="Neutral">/</label>
<input type="radio" class="btn-check" name="perm_2048" id="perm_2048_allow" value="allow">
<label class="btn btn-outline-success border-0" for="perm_2048_allow" title="Allow"><i class="fa-solid fa-check"></i></label>
</div>
</div>
</div>
<!-- More permissions can be added here -->
</div>
</div>
<div class="h-100 d-flex align-items-center justify-content-center text-muted p-4" id="no-role-selected-view">
<div class="text-center">
<i class="fa-solid fa-lock mb-3" style="font-size: 2.5rem; opacity: 0.2;"></i>
<p class="small mb-0">Select a role or member on the left to configure their specific permissions for this channel.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Messages épinglés 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">Messages épinglés</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">Pas encore de messages épinglés.</div>
</div>
</div>
</div>
</div>
<!-- Role Modifieror Modal -->
<div class="modal fade" id="roleModifierorModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modifier le rôle</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-role-id">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Nom du rôle</label>
<input type="text" id="edit-role-name" class="form-control">
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Couleur du rôle</label>
<input type="color" id="edit-role-color" class="form-control form-control-color w-100">
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Icône du rôle</label>
<div class="d-flex align-items-center mb-2">
<div id="selected-role-emoji-preview" class="d-flex align-items-center justify-content-center border rounded" style="width: 48px; height: 48px; font-size: 24px; background: #1e1f22;"></div>
<input type="hidden" id="edit-role-icon">
<button type="button" class="btn btn-sm btn-outline-primary ms-2" id="role-emoji-select-btn">Choisir un Emoji</button>
<button type="button" class="btn btn-sm btn-outline-danger ms-2" onclick="document.getElementById('edit-role-icon').value=''; document.getElementById('selected-role-emoji-preview').textContent='';">Supprimer l'icône</button>
</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Permissions</label>
<div id="role-permissions-checkboxes" class="p-2 bg-dark rounded">
<!-- Permission checkboxes here -->
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="button" id="save-role-btn" class="btn btn-primary">Enregistrer le rôle</button>
</div>
</div>
</div>
</div>
<!-- New Thread Modal -->
<div class="modal fade" id="newThreadModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Nouvelle discussion</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Titre de la discussion</label>
<input type="text" id="new-thread-title" class="form-control" placeholder="À quoi pensez-vous ?">
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Tags</label>
<div id="new-thread-tags-list" class="d-flex flex-wrap gap-2 p-2 bg-dark rounded">
<!-- Tags loaded here -->
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="button" id="submit-new-thread-btn" class="btn btn-primary">Créer la discussion</button>
</div>
</div>
</div>
</div>
<!-- Manage Tags Modal -->
<div class="modal fade" id="manageTagsModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Gérer les tags du forum</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="forum-tags-admin-list" class="mb-4">
<!-- List of tags with delete buttons -->
</div>
<hr class="border-secondary">
<h6>Ajouter un nouveau tag</h6>
<div class="row g-2">
<div class="col-7">
<input type="text" id="new-tag-name" class="form-control form-control-sm" placeholder="Nom du tag">
</div>
<div class="col-3">
<input type="color" id="new-tag-color" class="form-control form-control-sm form-control-color w-100" value="#7289da">
</div>
<div class="col-2">
<button class="btn btn-sm btn-primary w-100" id="add-forum-tag-btn">+</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modifier User Roles Modal -->
<div class="modal fade" id="editUserRolesModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modifier les rôles du membre</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="d-flex align-items-center mb-4">
<div id="edit-user-roles-avatar" class="message-avatar me-3" style="width: 48px; height: 48px;"></div>
<div>
<h5 id="edit-user-roles-username" class="mb-0">Nom d'utilisateur</h5>
<div class="text-muted small">Sélectionnez les rôles à attribuer à ce membre</div>
</div>
</div>
<input type="hidden" id="edit-user-roles-user-id">
<div id="user-roles-selection-list" class="list-group list-group-flush bg-dark rounded">
<!-- Roles checkboxes populated by JS -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="button" id="save-user-roles-btn" class="btn btn-primary">Enregistrer les modifications</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/voice.js?v=<?php echo time(); ?>"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
<script>
// Handle channel type and icon in modals
const addChannelType = document.getElementById('add-channel-type');
const addChannelPrefix = document.getElementById('add-channel-prefix');
const addChannelIcon = document.querySelector('#addChannelModal select[name="icon"]');
const editChannelType = document.getElementById('edit-channel-type');
const editChannelPrefix = document.getElementById('edit-channel-prefix');
const editChannelIcon = document.getElementById('edit-channel-icon');
function getPrefixForType(type) {
if (type === 'voice') return '<i class="fa-solid fa-volume-up"></i>';
if (type === 'announcement') return '<i class="fa-solid fa-bullhorn"></i>';
if (type === 'rules') return '<i class="fa-solid fa-gavel"></i>';
if (type === 'forum') return '<i class="fa-solid fa-comments"></i>';
if (type === 'separator') return '—';
return '#';
}
function updatePrefix(typeSelect, iconSelect, prefixSpan) {
if (!prefixSpan || !typeSelect) return;
// Handle name input visibility for separator and category
const modal = typeSelect.closest('.modal');
const nameInputContainer = modal.querySelector('input[name="name"]')?.closest('.mb-3');
const iconSelectContainer = modal.querySelector('select[name="icon"]')?.closest('.mb-3');
const fileSharingContainer = modal.querySelector('input[name="allow_file_sharing"]')?.closest('.mb-3') || modal.querySelector('input[name="allow_file_sharing"]')?.closest('.form-check');
const limitContainer = modal.querySelector('input[name="message_limit"]')?.closest('.mb-3');
const categoryContainer = modal.querySelector('select[name="category_id"]')?.closest('.mb-3');
if (typeSelect.value === 'separator') {
if (nameInputContainer) nameInputContainer.style.display = 'none';
if (iconSelectContainer) iconSelectContainer.style.display = 'none';
if (fileSharingContainer) fileSharingContainer.style.display = 'none';
if (limitContainer) limitContainer.style.display = 'none';
if (categoryContainer) categoryContainer.style.display = 'none';
if (modal.querySelector('input[name="name"]')) modal.querySelector('input[name="name"]').required = false;
} else if (typeSelect.value === 'category') {
if (nameInputContainer) nameInputContainer.style.display = 'block';
if (iconSelectContainer) iconSelectContainer.style.display = 'block';
if (fileSharingContainer) fileSharingContainer.style.display = 'none';
if (limitContainer) limitContainer.style.display = 'none';
if (categoryContainer) categoryContainer.style.display = 'none';
if (modal.querySelector('input[name="name"]')) modal.querySelector('input[name="name"]').required = true;
} else {
if (nameInputContainer) nameInputContainer.style.display = 'block';
if (iconSelectContainer) iconSelectContainer.style.display = 'block';
if (fileSharingContainer) fileSharingContainer.style.display = 'block';
if (limitContainer) limitContainer.style.display = 'block';
if (categoryContainer) categoryContainer.style.display = 'block';
if (modal.querySelector('input[name="name"]')) modal.querySelector('input[name="name"]').required = true;
}
let prefix = getPrefixForType(typeSelect.value);
if (iconSelect && iconSelect.value) {
prefix += ` <i class="fa-solid ${iconSelect.value}"></i>`;
}
prefixSpan.innerHTML = prefix;
}
document.querySelectorAll('.add-channel-btn').forEach(btn => {
btn.addEventListener('click', function() {
const type = this.getAttribute('data-type');
const categoryId = this.getAttribute('data-category-id');
if (addChannelType) {
addChannelType.value = type;
updatePrefix(addChannelType, addChannelIcon, addChannelPrefix);
}
// Add category_id hidden field or select it if we add it to addChannelModal
// Let's add a hidden field to addChannelModal for category_id
let catInput = document.getElementById('add-channel-category-id');
if (!catInput) {
catInput = document.createElement('input');
catInput.type = 'hidden';
catInput.name = 'category_id';
catInput.id = 'add-channel-category-id';
document.querySelector('#addChannelModal form').appendChild(catInput);
}
catInput.value = categoryId || '';
});
});
if (addChannelType) {
addChannelType.addEventListener('change', () => updatePrefix(addChannelType, addChannelIcon, addChannelPrefix));
}
if (addChannelIcon) {
addChannelIcon.addEventListener('change', () => updatePrefix(addChannelType, addChannelIcon, addChannelPrefix));
}
if (editChannelType) {
editChannelType.addEventListener('change', () => updatePrefix(editChannelType, editChannelIcon, editChannelPrefix));
}
if (editChannelIcon) {
editChannelIcon.addEventListener('change', () => updatePrefix(editChannelType, editChannelIcon, editChannelPrefix));
}
// Initial update when opening edit modal
document.addEventListener('click', function(e) {
const btn = e.target.closest('.channel-settings-btn');
if (btn) {
// Fill basic fields to ensure they are present even if main.js fails
const modal = document.getElementById('editChannelModal');
if (modal) {
const idInput = document.getElementById('edit-channel-id');
const nameInput = document.getElementById('edit-channel-name');
const typeSelect = document.getElementById('edit-channel-type');
const iconSelect = document.getElementById('edit-channel-icon');
const categorySelect = document.getElementById('edit-channel-category-id');
if (idInput) idInput.value = btn.dataset.id || '';
if (nameInput) nameInput.value = btn.dataset.name || '';
if (typeSelect) typeSelect.value = btn.dataset.type || 'chat';
if (iconSelect) iconSelect.value = btn.dataset.icon || '';
if (categorySelect) categorySelect.value = btn.dataset.category || '';
// Force switch to Overview tab
const overviewTabBtn = modal.querySelector('[data-bs-target="#edit-channel-general"]');
if (overviewTabBtn && typeof bootstrap !== 'undefined') {
bootstrap.Tab.getOrCreateInstance(overviewTabBtn).show();
}
// Also fill delete ID
const deleteIdInput = document.getElementById('delete-channel-id');
if (deleteIdInput) deleteIdInput.value = btn.dataset.id || '';
}
setTimeout(() => updatePrefix(editChannelType, editChannelIcon, editChannelPrefix), 100);
}
});
// SortableJS Implementation for Channels
<?php if ($can_manage_channels): ?>
document.addEventListener('DOMContentLoaded', function() {
// Sortable for groups (channels inside categories)
const groups = document.querySelectorAll('.category-group');
groups.forEach(group => {
new Sortable(group, {
group: 'channels',
draggable: '.channel-item-container',
animation: 150,
ghostClass: 'sortable-ghost',
onEnd: function() {
saveChannelOrders();
}
});
});
// Sortable for categories themselves and top-level channels
const sidebar = document.getElementById('sidebar-channels-list');
new Sortable(sidebar, {
group: 'channels',
animation: 150,
draggable: '.category-wrapper, .channel-item-container:not(.category-group .channel-item-container)',
ghostClass: 'sortable-ghost',
onEnd: function() {
saveChannelOrders();
}
});
});
async function saveChannelOrders() {
const orders = [];
let position = 0;
const sidebar = document.getElementById('sidebar-channels-list');
// Iterate over top-level items
const topLevelItems = sidebar.children;
Array.from(topLevelItems).forEach(item => {
const itemId = item.dataset.id;
if (!itemId) return;
if (item.classList.contains('category-wrapper')) {
// It's a category
orders.push({
id: itemId,
position: position++,
category_id: null
});
// Now add all channels inside this category
const subChannels = item.querySelectorAll('.category-group .channel-item-container');
subChannels.forEach(sub => {
if (sub.dataset.id) {
orders.push({
id: sub.dataset.id,
position: position++,
category_id: itemId
});
}
});
} else if (item.classList.contains('channel-item-container')) {
// It's a top level channel or separator
orders.push({
id: itemId,
position: position++,
category_id: null
});
}
});
try {
const resp = await fetch('api_v1_channels.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'reorder',
server_id: "<?php echo $active_server_id; ?>",
orders: orders
})
});
const data = await resp.json();
if (!data.success) {
console.error('Failed to save channel order:', data.error);
}
} catch (e) {
console.error('Error saving channel order:', e);
}
}
<?php endif; ?>
</script>
</body>
</html>