2983 lines
205 KiB
PHP
2983 lines
205 KiB
PHP
<?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('/^> (.*$)/m', '<blockquote>$1</blockquote>', $html);
|
||
$html = preg_replace('/^>>> ([\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('/<(https?:\/\/[^&]+)>/', '<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();
|
||
}
|
||
}
|
||
|
||
// Always fetch tags if it's a forum channel
|
||
if ($channel_type === 'forum') {
|
||
$stmt_tags = db()->prepare("SELECT * FROM forum_tags WHERE channel_id = ? ORDER BY name ASC");
|
||
$stmt_tags->execute([$active_channel_id]);
|
||
$forum_tags = $stmt_tags->fetchAll();
|
||
|
||
$selected_tag_ids = [];
|
||
if (!empty($_GET['tags'])) {
|
||
$selected_tag_ids = array_filter(explode(',', $_GET['tags']), 'is_numeric');
|
||
}
|
||
}
|
||
|
||
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') {
|
||
$tag_where = "";
|
||
$query_params = [$active_server_id, $active_server_id, $active_channel_id];
|
||
|
||
if (!empty($selected_tag_ids)) {
|
||
$placeholders = implode(',', array_fill(0, count($selected_tag_ids), '?'));
|
||
$tag_where = " AND EXISTS (SELECT 1 FROM thread_tags tt WHERE tt.thread_id = t.id AND tt.tag_id IN ($placeholders))";
|
||
foreach ($selected_tag_ids as $tid) $query_params[] = $tid;
|
||
}
|
||
|
||
$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 = ? " . $tag_where . "
|
||
ORDER BY t.is_pinned DESC, t.last_activity_at DESC, t.created_at DESC
|
||
");
|
||
$stmt->execute($query_params);
|
||
$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">
|
||
<img src="img/voip_icon.png" alt="Home" style="width: 100%; height: 100%; object-fit: cover; border-radius: inherit;">
|
||
</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' && !$active_thread): ?>
|
||
<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 flex-wrap gap-1">
|
||
<?php
|
||
$s_id = $active_server_id;
|
||
$c_id = $active_channel_id;
|
||
?>
|
||
<a href="?server_id=<?php echo $s_id; ?>&channel_id=<?php echo $c_id; ?>" class="btn btn-outline-secondary <?php echo empty($selected_tag_ids) ? 'active' : ''; ?>">Tous</a>
|
||
<?php foreach($forum_tags as $tag):
|
||
$is_active = in_array($tag['id'], $selected_tag_ids);
|
||
if ($is_active) {
|
||
$new_tags = array_diff($selected_tag_ids, [$tag['id']]);
|
||
} else {
|
||
$new_tags = array_merge($selected_tag_ids, [$tag['id']]);
|
||
}
|
||
$tags_query = !empty($new_tags) ? '&tags=' . implode(',', $new_tags) : '';
|
||
$tag_url = "?server_id=$s_id&channel_id=$c_id$tags_query";
|
||
?>
|
||
<a href="<?php echo $tag_url; ?>" class="btn btn-outline-secondary <?php echo $is_active ? 'active' : ''; ?>" style="<?php echo $is_active ? "background-color: {$tag['color']}; border-color: {$tag['color']}; color: white;" : ""; ?>">
|
||
<?php echo htmlspecialchars($tag['name']); ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</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 !empty($selected_tag_ids) ? '&tags='.implode(',', $selected_tag_ids) : ''; ?>" 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 ($active_thread): ?>
|
||
<div class="p-3 border-bottom border-secondary d-flex justify-content-between align-items-center bg-dark bg-opacity-25 sticky-top" style="z-index: 10;">
|
||
<div>
|
||
<h4 class="mb-0">
|
||
<?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; ?>
|
||
<?php echo htmlspecialchars($active_thread['title']); ?>
|
||
</h4>
|
||
<div class="small text-muted mt-1">
|
||
Par <?php echo htmlspecialchars($active_thread['username']); ?> • Dans #<?php echo htmlspecialchars($current_channel_name); ?>
|
||
</div>
|
||
</div>
|
||
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $active_channel_id; ?><?php echo !empty($selected_tag_ids) ? '&tags='.implode(',', $selected_tag_ids) : ''; ?>" class="btn btn-outline-secondary btn-sm">
|
||
<i class="fa-solid fa-arrow-left me-1"></i> Retour au forum
|
||
</a>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?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>
|
||
<button type="submit" class="btn border-0 p-2 ms-1" id="chat-send-btn" title="Envoyer (Entrée)" style="color: #5865f2; <?php echo (isset($active_thread) && $active_thread['is_locked']) ? 'display: none;' : ''; ?>">
|
||
<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="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></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'] ?? ''); ?>">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary w-100 mt-2" onclick="document.getElementById('avatar-upload-input').click()">
|
||
<i class="fas fa-upload me-1"></i> Importer
|
||
</button>
|
||
<input type="file" id="avatar-upload-input" style="display: none;" accept="image/*">
|
||
</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>
|
||
<button class="btn btn-secondary px-3" type="button" id="refresh-avatar-btn" title="Rafraîchir les propositions"><i class="fas fa-sync-alt"></i></button>
|
||
</div>
|
||
<div id="avatar-results" class="d-grid gap-2 overflow-auto p-2 rounded settings-inner-bg" style="max-height: 220px; grid-template-columns: repeat(13, 1fr);">
|
||
<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>
|
||
</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-mode-switch')?.checked ? '1' : '0';
|
||
const soundNotifications = document.getElementById('sound-notifications-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); ?>">
|
||
<div class="d-flex gap-2 justify-content-center">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" id="search-server-icon-btn">Rechercher</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="document.getElementById('server-icon-upload-input').click()">
|
||
<i class="fas fa-upload me-1"></i> Importer
|
||
</button>
|
||
<input type="file" id="server-icon-upload-input" style="display: none;" accept="image/*">
|
||
</div>
|
||
</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>
|
||
<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" style="background-color: #2b2d31; overflow: visible;">
|
||
<!-- Sidebar: Roles & Members -->
|
||
<div class="col-4 border-end border-secondary d-flex flex-column" style="background-color: #2b2d31; border-top-left-radius: 0.375rem; border-bottom-left-radius: 0.375rem;">
|
||
<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" data-bs-boundary="viewport" 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; border-top-right-radius: 0.375rem; border-bottom-right-radius: 0.375rem;">
|
||
<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>
|
||
</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 Editor Modal -->
|
||
<div class="modal fade" id="roleEditorModal" 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>
|