diff --git a/Temp/index_new.php b/Temp/index_new.php new file mode 100644 index 0000000..63d21a5 --- /dev/null +++ b/Temp/index_new.php @@ -0,0 +1,3965 @@ +'; + } elseif ($isFa) { + return ''; + } 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 ''; + } + return '' . htmlspecialchars($icon) . ''; + } else { + return '' . htmlspecialchars($icon) . ''; + } +} + +// 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[] = '
' . $content . '
'; + 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[] = '' . $content . ''; + return $placeholder; + }, $html); + + // Bold: **text** + $html = preg_replace('/\*\*([^*]+)\*\*/', '$1', $html); + + // Italics: *text* or _text_ + $html = preg_replace('/\*([^*]+)\*/', '$1', $html); + $html = preg_replace('/_([^_]+)_/', '$1', $html); + + // Underline: __text__ + $html = preg_replace('/__([^_]+)__/', '$1', $html); + + // Strikethrough: ~~text~~ + $html = preg_replace('/~~([^~]+)~~/', '$1', $html); + + // Spoiler: ||text|| + $html = preg_replace('/\|\|([^|]+)\|\|/', '$1', $html); + + // Headers: # H1, ## H2, ### H3 (must be at start of line) + $html = preg_replace('/^# (.*$)/m', '

$1

', $html); + $html = preg_replace('/^## (.*$)/m', '

$1

', $html); + $html = preg_replace('/^### (.*$)/m', '

$1

', $html); + + // Subtext: -# text (must be at start of line) + $html = preg_replace('/^-# (.*$)/m', '$1', $html); + + // Blockquotes: > text or >>> text + $html = preg_replace('/^> (.*$)/m', '
$1
', $html); + $html = preg_replace('/^>>> ([\s\S]*$)/', '
$1
', $html); + + // Hyperlinks: [text](url) + $html = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '$1', $html); + + // Pure links: + $html = preg_replace('/<(https?:\/\/[^&]+)>/', '$1', $html); + + // Newlines to
(only those not inside placeholders) + $html = nl2br($html); + + // Remove extra space around headers and blockquotes added by nl2br + $html = preg_replace('/(\s*)\s*(|
)/i', '$2', $html); + $html = preg_replace('/(<\/h[1-3]>|<\/blockquote>)\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, '@' . htmlspecialchars($username_to_mention) . '', $result); + } + + foreach ($custom_emotes_cache as $ce) { + $emote_html = '' . htmlspecialchars($ce['name']) . ''; + $result = str_replace($ce['code'], $emote_html, $result); + } + return $result; +} +requireLogin(); + +$user = getCurrentUser(); +if ($user) { + db()->prepare("UPDATE users SET status = 'online' WHERE id = ?")->execute([$user['id']]); + $user['status'] = 'online'; // Update local variable too +} +$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 c.*, (SELECT COUNT(*) FROM channel_events e WHERE e.channel_id = c.id AND (e.is_permanent = 1 OR CONCAT(e.end_date, ' ', e.end_time) >= NOW())) as event_count FROM channels c WHERE c.server_id = ? ORDER BY c.position ASC, c.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; + + // Event permissions + $can_create_event = Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::CREATE_EVENT); + $can_edit_event = Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::EDIT_EVENT); + $can_delete_event = Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::DELETE_EVENT); + $can_create_announcement = Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::CREATE_ANNOUNCEMENT); + $can_create_poll = Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::CREATE_POLL); + + 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.*, (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, 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_t_ids = db()->prepare("SELECT tag_id FROM thread_tags WHERE thread_id = ?"); + $stmt_t_ids->execute([$active_thread_id]); + $active_thread_tag_ids = $stmt_t_ids->fetchAll(PDO::FETCH_COLUMN); + $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.*, (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, u.display_name as username, u.avatar_url, + (SELECT COUNT(*) FROM messages m WHERE m.thread_id = t.id) as message_count, + (SELECT MAX(created_at) FROM messages m WHERE m.thread_id = t.id) as last_message_at, + (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 forum_threads t + JOIN users u ON t.user_id = u.id + WHERE t.channel_id = ? $tag_where + ORDER BY t.is_pinned DESC, last_message_at DESC + "); + $stmt->execute($query_params); + $threads = $stmt->fetchAll(); + } elseif ($channel_type === 'event') { + // Cleanup expired temporary events + try { + $now = date('Y-m-d H:i:s'); + $stmt_cleanup = db()->prepare("DELETE FROM channel_events WHERE channel_id = ? AND is_permanent = 0 AND CONCAT(end_date, ' ', end_time) < ?"); + $stmt_cleanup->execute([$active_channel_id, $now]); + + $stmt = db()->prepare(" + SELECT e.*, u.display_name as username, u.avatar_url, + (SELECT COUNT(*) FROM event_participations WHERE event_id = e.id) as participation_count, + (SELECT COUNT(*) FROM event_participations WHERE event_id = e.id AND user_id = ?) as is_participating + FROM channel_events e + JOIN users u ON e.user_id = u.id + WHERE e.channel_id = ? + ORDER BY e.is_permanent DESC, e.start_date ASC, e.start_time ASC + "); + $stmt->execute([$current_user_id, $active_channel_id]); + $events = $stmt->fetchAll(); + } catch (Exception $e) { + $events = []; + } + } elseif ($channel_type === 'poll') { + $display_limit = !empty($active_channel['message_limit']) ? (int)$active_channel['message_limit'] : 50; + $stmt = db()->prepare(" + SELECT m.*, u.display_name as username, 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 DESC + LIMIT " . $display_limit . " + "); + $stmt->execute([$active_server_id, $active_server_id, $active_channel_id]); + $messages = $stmt->fetchAll(); + + // Fetch votes for each poll message + foreach ($messages as &$m) { + $stmt_v = db()->prepare("SELECT pv.option_index, COUNT(*) as vote_count, GROUP_CONCAT(u.display_name SEPARATOR ', ') as user_names, GROUP_CONCAT(pv.user_id) as user_ids FROM poll_votes pv JOIN users u ON pv.user_id = u.id WHERE pv.message_id = ? GROUP BY pv.option_index"); + $stmt_v->execute([$m['id']]); + $m['votes_data'] = $stmt_v->fetchAll(); + } + unset($m); + } 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']; + if ($c['type'] === 'event' && !empty($c['event_count'])) { + $current_channel_name .= " (" . $c['event_count'] . ")"; + } + } + } + + // 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, + (SELECT GROUP_CONCAT(CONCAT(sb.name, '|', sb.image_url) SEPARATOR ':::') FROM member_badges mb JOIN server_badges sb ON mb.badge_id = sb.id WHERE mb.user_id = u.id AND mb.server_id = ?) as badge_data + 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, $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'] ?? 'Corvara - Messaging App'; +$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; +?> + + + + + + #<?php echo htmlspecialchars($current_channel_name); ?> | <?php echo htmlspecialchars($projectDescription); ?> + + + + + + + + + + + + + + + + + + + +
+ +
+ + Home + +
+ + "> + + + + + + +
+ + +
+
+ Messages privés'; + } else { + $active_server_name = 'Serveur'; + foreach($servers as $s) { + if($s['id'] == $active_server_id) { + $active_server_name = $s['name']; + break; + } + } + echo '' . htmlspecialchars($active_server_name) . ''; + ?> +
+ + + + + + + + + + +
+ +
+ +
+ + +
+
+ PHP | +
+
+ + +
+
+ + '; + elseif ($active_channel['type'] === 'rules') echo ''; + elseif ($active_channel['type'] === 'autorole') echo ''; + elseif ($active_channel['type'] === 'forum') echo ''; + elseif ($active_channel['type'] === 'voice') echo ''; + elseif ($active_channel['type'] === 'poll') echo ''; + else echo ''; + + if (!empty($active_channel['icon'])) { + echo ' ' . renderRoleIcon($active_channel['icon'], '16px') . ''; + } + } + ?> + + + +
+ + +
+
+ + +
+
+
+
+
+
+ +
+
+ ← Retour au forum +
+ + + + + + + + + + + + +
+
+

+ + + Discussion : +

+ 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): + ?> + + +
+ + +
+
">
+
+
"> + + + +
+ + + + + + + + + + + + + +
+ + SOLUTION + +
+
+ +
+
+ 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'])); + ?> + + + + + + +
+
+
+ +
+
+ +
+
+
+

Sondages

+

Participez aux sondages de la communauté.

+
+ + + +
+ +
+ +
+
+ +
+

Aucun sondage pour le moment.

+ +

Soyez le premier à poser une question !

+ +
+ + +
+
+
+
">
+
+
+
Par
+
+
+
+ + + + +
+
+
+ +
+
+ $opt): + $count = 0; + $user_voted = false; + $voters_list = ''; + if (isset($m['votes_data'])) { + foreach($m['votes_data'] as $v) { + if ($v['option_index'] == $idx) { + $count = (int)$v['vote_count']; + $voters_list = $v['user_names'] ?? ''; + $user_ids = explode(',', $v['user_ids'] ?? ''); + if (in_array($current_user_id, $user_ids)) $user_voted = true; + break; + } + } + } + $percent = $total_votes > 0 ? round(($count / $total_votes) * 100) : 0; + $emote = $emotes[$idx] ?? ''; + ?> +
" + style="cursor: ; position: relative; overflow: hidden; border-radius: 8px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); transition: all 0.2s;"> + +
+ +
+
+ + + + + + + +
+
+ % + + () + +
+
+
+ +
+ +
+ + +
+
+ +
+
+
+

Événements

+

Découvrez et gérez les événements à venir.

+
+ + + +
+ +
+ +
+
+ +
+

Aucun événement prévu pour le moment.

+ +

Cliquez sur "Ajouter un événement" pour commencer.

+ +
+ + +
+
+
">
+
+
+
+
+ +
+
+ + + à + +
+ +
+ + + Fin: à + +
+ + +
+ + + + +
+ +
+
+ +
+
Voir plus
+ + +
+
+ Participants () +
+ prepare("SELECT u.avatar_url, u.display_name FROM event_participations ep JOIN users u ON ep.user_id = u.id WHERE ep.event_id = ? LIMIT 5"); + $stmt_p->execute([$event['id']]); + $participants = $stmt_p->fetchAll(); + $p_idx = 0; + foreach ($participants as $p): + ?> +
; margin-left: 0 ? '-8px' : '0'; ?>; position: relative; z-index: ;">
+ + 5): ?> +
+
+ +
+
+ +
+ +
+ +
+
+ + +
+
+ +
+

📜

+
+ +
+
+ . + +
+ +
+ + + + +
+ +
+ +
+ + + + + + + prepare("SELECT 1 FROM rule_acceptances WHERE user_id = ? AND channel_id = ?"); + $stmtAcc->execute([$current_user_id, $active_channel_id]); + $has_accepted = $stmtAcc->fetch(); + ?> +
+ +
+ Vous avez accepté les règles. +
+
+ +
+ +

Veuillez accepter les règles pour obtenir l'accès complet.

+ + +
+ +
+ +
+

🛡️

+

Cliquez sur un bouton pour vous attribuer ou vous retirer un rôle.

+ +
+ prepare("SELECT 1 FROM user_roles WHERE user_id = ? AND role_id = ?"); + $stmtHasRole->execute([$current_user_id, $ar['role_id']]); + $has_role = $stmtHasRole->fetch(); + ?> +
+ + +
+ +
+ + + + + +
+
+ +
+ +
+ + +
+ +
+ +
+ +
+
+
+

🏛️

+
+ + Tous + + "> + + + +
+
+
+ + + + + + +
+
+ +
+ + +
+
+

+ + + +

+
+ Par • Dans # +
+
+ + Retour au forum + +
+ + +
+

Bienvenue dans # !

+

C'est le début du salon #.

+
+ + +
+
">
+
+
+ "> + + + + + +
+ + + + + + + + + + + +
+ + + + + Épinglé + + +
+
+ + +
+ + Attachment + + + + + + +
+ + + +
+ +
+ + + + + +
+ + + +
+ +
+ + +
+ +
+ + +
+ + + + + +
+ +
+ +
+ + +
+
+ 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'])); + ?> + + + + + + +
+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+ + + +
+
+ + Vous ne disposez pas de la permission d\'envoyer des messages dans ce salon. +
'; + } else { + $allow_files = true; + foreach($channels as $c) { + if($c['id'] == $active_channel_id) { + $allow_files = (bool)$c['allow_file_sharing']; + break; + } + } + ?> + +
+
+ + + > + +
+ +
+ + + + + + + + + + + + +
+
+ +
+ +
+ + +
+
+ Membres — +
+ +
+
"> + +
+ +
+ +
+ "> + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Temp/voice_tab.html b/Temp/voice_tab.html new file mode 100644 index 0000000..0057774 --- /dev/null +++ b/Temp/voice_tab.html @@ -0,0 +1,106 @@ + +
+
Paramètres de voix
+ +
+
+ + +
+
+ + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+ > + +
+
Réduit l'écho causé par vos haut-parleurs captés par votre micro.
+
+
+
+ > + +
+
Filtre les bruits de fond comme les ventilateurs ou les clics de clavier.
+
+
+
+ > + +
+
Active des filtres supplémentaires (passe-haut, gain auto optimisé) pour une qualité vocale supérieure sur Chrome.
+
+
+ +
+ +
+
+ onchange="togglePTTParamètres('vox')"> + +
+
+ onchange="togglePTTParamètres('ptt')"> + +
+
+ +
+
+ + +
Click the box and press any key to set your PTT shortcut.
+
+
+ +
+
+ +
+
+
+
+ +
+ Sensitive + Loud Only +
+
+
+
+ +
+
+ +
+
Microphone Access
+
Voice channels require microphone permission. If you don't hear anything, check your browser's site settings.
+
+
+
+
diff --git a/api_v1_user.php b/api_v1_user.php index 03732da..7f01427 100644 --- a/api_v1_user.php +++ b/api_v1_user.php @@ -28,10 +28,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $voice_vox_threshold = isset($_POST['voice_vox_threshold']) ? (float)$_POST['voice_vox_threshold'] : ($user['voice_vox_threshold'] ?? 0.1); $voice_echo_cancellation = isset($_POST['voice_echo_cancellation']) ? (int)$_POST['voice_echo_cancellation'] : ($user['voice_echo_cancellation'] ?? 1); $voice_noise_suppression = isset($_POST['voice_noise_suppression']) ? (int)$_POST['voice_noise_suppression'] : ($user['voice_noise_suppression'] ?? 1); + $voice_advanced_filters = isset($_POST['voice_advanced_filters']) ? (int)$_POST['voice_advanced_filters'] : ($user['voice_advanced_filters'] ?? 1); try { - $stmt = db()->prepare("UPDATE users SET display_name = ?, avatar_url = ?, dnd_mode = ?, sound_notifications = ?, theme = ?, voice_mode = ?, voice_ptt_key = ?, voice_vox_threshold = ?, voice_echo_cancellation = ?, voice_noise_suppression = ? WHERE id = ?"); - $stmt->execute([$display_name, $avatar_url, $dnd_mode, $sound_notifications, $theme, $voice_mode, $voice_ptt_key, $voice_vox_threshold, $voice_echo_cancellation, $voice_noise_suppression, $user['id']]); + $stmt = db()->prepare("UPDATE users SET display_name = ?, avatar_url = ?, dnd_mode = ?, sound_notifications = ?, theme = ?, voice_mode = ?, voice_ptt_key = ?, voice_vox_threshold = ?, voice_echo_cancellation = ?, voice_noise_suppression = ?, voice_advanced_filters = ? WHERE id = ?"); + $stmt->execute([$display_name, $avatar_url, $dnd_mode, $sound_notifications, $theme, $voice_mode, $voice_ptt_key, $voice_vox_threshold, $voice_echo_cancellation, $voice_noise_suppression, $voice_advanced_filters, $user['id']]); echo json_encode(['success' => true]); } catch (Exception $e) { @@ -40,4 +41,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { exit; } -echo json_encode(['success' => false, 'error' => 'Invalid request']); +echo json_encode(['success' => false, 'error' => 'Invalid request']); \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index a8c7104..ddb7f8f 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -480,7 +480,7 @@ document.addEventListener('DOMContentLoaded', () => { let voiceHandler; if (typeof VoiceChannel !== 'undefined') { - voiceHandler = new VoiceChannel(null, window.voiceSettings); + voiceHandler = new VoiceChannel(null, window.voiceParamètres); window.voiceHandler = voiceHandler; console.log('VoiceHandler initialized'); diff --git a/assets/js/voice.js b/assets/js/voice.js index 704299b..ad5d644 100644 --- a/assets/js/voice.js +++ b/assets/js/voice.js @@ -12,7 +12,8 @@ class VoiceChannel { inputVolume: 1.0, outputVolume: 1.0, echoCancellation: true, - noiseSuppression: true + noiseSuppression: true, + advancedFilters: true }; console.log('VoiceChannel constructor called with settings:', this.settings); this.localStream = null; @@ -69,6 +70,36 @@ class VoiceChannel { }, 200); } + getAudioConstraints() { + const useAdvanced = this.settings.advancedFilters !== false; + + const constraints = { + echoCancellation: { ideal: this.settings.echoCancellation }, + noiseSuppression: { ideal: this.settings.noiseSuppression }, + autoGainControl: { ideal: useAdvanced }, + // Chromium-specific flags + googEchoCancellation: { ideal: this.settings.echoCancellation }, + googAutoGainControl: { ideal: useAdvanced }, + googNoiseSuppression: { ideal: this.settings.noiseSuppression }, + googHighpassFilter: { ideal: useAdvanced }, + googTypingNoiseDetection: { ideal: true }, + googAudioMirroring: { ideal: false }, + googNoiseReduction: { ideal: this.settings.noiseSuppression }, + googAutoGainControl2: { ideal: useAdvanced }, + googAudioMirroring: { ideal: false }, + // Standard constraints + channelCount: { ideal: 1 }, + sampleRate: { ideal: 48000 }, + sampleSize: { ideal: 16 } + }; + + if (this.settings.inputDevice !== 'default') { + constraints.deviceId = { exact: this.settings.inputDevice }; + } + + return constraints; + } + setupPTTListeners() { window.addEventListener('keydown', (e) => { // Ignore if in input field @@ -197,16 +228,9 @@ class VoiceChannel { try { console.log('Requesting microphone access with device:', this.settings.inputDevice); const constraints = { - audio: { - echoCancellation: this.settings.echoCancellation, - noiseSuppression: this.settings.noiseSuppression, - autoGainControl: true - }, + audio: this.getAudioConstraints(), video: false }; - if (this.settings.inputDevice !== 'default') { - constraints.audio.deviceId = { exact: this.settings.inputDevice }; - } this.localStream = await navigator.mediaDevices.getUserMedia(constraints); console.log('Microphone access granted'); this.setMute(false); // Join unmuted by default (self-mute off) @@ -685,16 +709,9 @@ class VoiceChannel { this.settings.inputDevice = deviceId; if (this.currentChannelId && this.localStream) { const constraints = { - audio: { - echoCancellation: this.settings.echoCancellation, - noiseSuppression: this.settings.noiseSuppression, - autoGainControl: true - }, + audio: this.getAudioConstraints(), video: false }; - if (deviceId !== 'default') { - constraints.audio.deviceId = { exact: deviceId }; - } const newStream = await navigator.mediaDevices.getUserMedia(constraints); const newTrack = newStream.getAudioTracks()[0]; @@ -721,18 +738,11 @@ class VoiceChannel { async updateAudioConstraints() { if (this.currentChannelId && this.localStream) { - console.log('Updating audio constraints:', this.settings.echoCancellation, this.settings.noiseSuppression); + console.log('Updating audio constraints:', this.settings.echoCancellation, this.settings.noiseSuppression, this.settings.advancedFilters); const constraints = { - audio: { - echoCancellation: this.settings.echoCancellation, - noiseSuppression: this.settings.noiseSuppression, - autoGainControl: true - }, + audio: this.getAudioConstraints(), video: false }; - if (this.settings.inputDevice !== 'default') { - constraints.audio.deviceId = { exact: this.settings.inputDevice }; - } try { const newStream = await navigator.mediaDevices.getUserMedia(constraints); const newTrack = newStream.getAudioTracks()[0]; diff --git a/db/migrations/20260325_add_voice_advanced_filters.sql b/db/migrations/20260325_add_voice_advanced_filters.sql new file mode 100644 index 0000000..119c915 --- /dev/null +++ b/db/migrations/20260325_add_voice_advanced_filters.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN voice_advanced_filters TINYINT(1) DEFAULT 1; diff --git a/index.php b/index.php index 9899109..63d21a5 100644 --- a/index.php +++ b/index.php @@ -482,6 +482,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; voxThreshold: , echoCancellation: , noiseSuppression: , + advancedFilters: , inputDevice: localStorage.getItem('voice_input_device') || 'default', outputDevice: localStorage.getItem('voice_output_device') || 'default', inputVolume: parseFloat(localStorage.getItem('voice_input_volume') || 1.0), @@ -1861,6 +1862,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; +
Paramètres de voix
@@ -1904,6 +1906,13 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
Filtre les bruits de fond comme les ventilateurs ou les clics de clavier.
+
+
+ > + +
+
Active des filtres supplémentaires (passe-haut, gain auto optimisé) pour une qualité vocale supérieure sur Chrome.
+
@@ -1959,8 +1968,6 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
- -
Whisper Configurations
@@ -2182,11 +2189,13 @@ async function handleSaveUserParamètres(btn) { 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'; + const advancedFilters = document.getElementById('advanced-filters-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); + formData.set('voice_advanced_filters', advancedFilters); // Explicitly get theme and voice_mode to ensure they are captured const themeInput = form.querySelector('input[name="theme"]:checked'); @@ -2221,6 +2230,7 @@ async function handleSaveUserParamètres(btn) { window.voiceHandler.settings.voxThreshold = voxThreshold; window.voiceHandler.settings.echoCancellation = echoCancellation === '1'; window.voiceHandler.settings.noiseSuppression = noiseSuppression === '1'; + window.voiceHandler.settings.advancedFilters = advancedFilters === '1'; // New settings const inputDevice = document.getElementById('voice_input_device')?.value;