Guest User

Untitled

a guest
Jan 29th, 2026
7
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 81.89 KB | None | 0 0
  1. // ==UserScript==
  2. // @name On'Chat Pro Ultimate
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.0.0
  5. // @description Ultimate live chat experience for onche.org - Real-time, beautiful, powerful
  6. // @author Flow
  7. // @match https://onche.org/*
  8. // @grant GM_addStyle
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_notification
  12. // @grant GM_setClipboard
  13. // @run-at document-end
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. /* global $ */
  20. /* eslint-env browser */
  21.  
  22. // ╔══════════════════════════════════════════════════════════════════════════════╗
  23. // ║ ON'CHAT PRO ULTIMATE v3.0 ║
  24. // ║ The definitive onche.org experience ║
  25. // ║ Codename: Athena ║
  26. // ╚══════════════════════════════════════════════════════════════════════════════╝
  27.  
  28. // ==================== CONFIGURATION ====================
  29. const CONFIG = {
  30. VERSION: '3.0.0',
  31. CODENAME: 'Athena',
  32. DEBUG: false,
  33.  
  34. POLLING: {
  35. ULTRA_FAST: 1000,
  36. FAST: 2000,
  37. NORMAL: 5000,
  38. SLOW: 10000,
  39. IDLE: 30000,
  40. DEFAULT: 3000,
  41. MAX_INTERVAL: 60000,
  42. BACKOFF_MULTIPLIER: 1.3
  43. },
  44.  
  45. UI: {
  46. ANIMATION_DURATION: 300,
  47. HIGHLIGHT_DURATION: 4000,
  48. TOAST_DURATION: 3000,
  49. BUTTON_SIZE: 44,
  50. PANEL_WIDTH: 320,
  51. THEME: 'dark' // 'dark', 'light', 'auto'
  52. },
  53.  
  54. SELECTORS: {
  55. MESSAGES_CONTAINER: '.messages',
  56. MESSAGE: '.message',
  57. MESSAGE_CONTENT: '.message__content',
  58. MESSAGE_AUTHOR: '.message__info__pseudo',
  59. MESSAGE_TIME: '.message__info__date',
  60. TOPIC: '#topic',
  61. TOPIC_TITLE: '.topic__title, h1',
  62. TEXTAREA: 'textarea[name="message"]',
  63. FORM: 'form:has(textarea[name="message"]), form.message-form',
  64. AVATAR: '.message__avatar img',
  65. LIKE_BUTTON: '.likeButton',
  66. PAGINATION: '.pagination'
  67. },
  68.  
  69. CLASSES: {
  70. NEW_MESSAGE: 'onchat-new',
  71. HIGHLIGHT: 'onchat-highlight',
  72. MENTION: 'onchat-mention',
  73. VISIBLE: 'visible',
  74. HIDDEN: 'hidden',
  75. ACTIVE: 'active',
  76. ERROR: 'error',
  77. PAUSED: 'paused',
  78. MINIMIZED: 'minimized'
  79. },
  80.  
  81. STORAGE_KEYS: {
  82. LIVE_MODE: 'onchat_live_mode',
  83. REFRESH_INTERVAL: 'onchat_refresh_interval',
  84. NOTIFICATIONS: 'onchat_notifications',
  85. SOUNDS: 'onchat_sounds',
  86. COMPACT_MODE: 'onchat_compact_mode',
  87. THEME: 'onchat_theme',
  88. MENTIONS_KEYWORDS: 'onchat_mentions',
  89. BLOCKED_USERS: 'onchat_blocked',
  90. FAVORITE_TOPICS: 'onchat_favorites',
  91. READ_MESSAGES: 'onchat_read',
  92. PANEL_MINIMIZED: 'onchat_panel_min',
  93. USERNAME: 'onchat_username'
  94. },
  95.  
  96. SECURITY: {
  97. MAX_REACTIONS_PER_WINDOW: 10,
  98. REACTION_WINDOW_MS: 10000,
  99. MAX_MESSAGES_PER_WINDOW: 5,
  100. MESSAGE_WINDOW_MS: 60000,
  101. MIN_TOKEN_LENGTH: 10,
  102. MAX_TOKEN_LENGTH: 200,
  103. FETCH_TIMEOUT: 30000,
  104. REDACT_SENSITIVE: true
  105. },
  106.  
  107. SOUNDS: {
  108. NEW_MESSAGE: 'data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdH2Coverage...',
  109. MENTION: 'data:audio/wav;base64,UklGRl9vT19teleQUFhYWm10IBAAAAABAAEAQB8AAMention...'
  110. }
  111. };
  112.  
  113. // ==================== THEME SYSTEM ====================
  114. const ThemeSystem = {
  115. themes: {
  116. dark: {
  117. '--onchat-bg': '#0a0a12',
  118. '--onchat-bg-secondary': '#12121c',
  119. '--onchat-bg-tertiary': '#1a1a28',
  120. '--onchat-bg-hover': '#22222f',
  121. '--onchat-border': '#2a2a3d',
  122. '--onchat-border-light': '#3a3a4d',
  123. '--onchat-text': '#e4e4e7',
  124. '--onchat-text-secondary': '#a1a1aa',
  125. '--onchat-text-muted': '#71717a',
  126. '--onchat-accent': '#6366f1',
  127. '--onchat-accent-hover': '#818cf8',
  128. '--onchat-accent-glow': 'rgba(99, 102, 241, 0.3)',
  129. '--onchat-success': '#22c55e',
  130. '--onchat-warning': '#f59e0b',
  131. '--onchat-error': '#ef4444',
  132. '--onchat-mention': '#f472b6',
  133. '--onchat-shadow': '0 4px 24px rgba(0, 0, 0, 0.5)',
  134. '--onchat-glass': 'rgba(10, 10, 18, 0.85)'
  135. },
  136. light: {
  137. '--onchat-bg': '#ffffff',
  138. '--onchat-bg-secondary': '#f4f4f5',
  139. '--onchat-bg-tertiary': '#e4e4e7',
  140. '--onchat-bg-hover': '#d4d4d8',
  141. '--onchat-border': '#d4d4d8',
  142. '--onchat-border-light': '#e4e4e7',
  143. '--onchat-text': '#18181b',
  144. '--onchat-text-secondary': '#52525b',
  145. '--onchat-text-muted': '#a1a1aa',
  146. '--onchat-accent': '#4f46e5',
  147. '--onchat-accent-hover': '#6366f1',
  148. '--onchat-accent-glow': 'rgba(79, 70, 229, 0.2)',
  149. '--onchat-success': '#16a34a',
  150. '--onchat-warning': '#d97706',
  151. '--onchat-error': '#dc2626',
  152. '--onchat-mention': '#db2777',
  153. '--onchat-shadow': '0 4px 24px rgba(0, 0, 0, 0.1)',
  154. '--onchat-glass': 'rgba(255, 255, 255, 0.9)'
  155. }
  156. },
  157.  
  158. current: 'dark',
  159.  
  160. init() {
  161. const saved = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.THEME, 'dark');
  162. this.apply(saved);
  163. },
  164.  
  165. apply(theme) {
  166. if (!this.themes[theme]) theme = 'dark';
  167. this.current = theme;
  168.  
  169. const vars = this.themes[theme];
  170. Object.entries(vars).forEach(([key, value]) => {
  171. document.documentElement.style.setProperty(key, value);
  172. });
  173.  
  174. SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.THEME, theme);
  175. },
  176.  
  177. toggle() {
  178. this.apply(this.current === 'dark' ? 'light' : 'dark');
  179. return this.current;
  180. }
  181. };
  182.  
  183. // ==================== SOUND SYSTEM ====================
  184. const SoundSystem = {
  185. enabled: true,
  186. audioContext: null,
  187.  
  188. init() {
  189. this.enabled = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.SOUNDS, true);
  190. },
  191.  
  192. async play(type) {
  193. if (!this.enabled) return;
  194.  
  195. try {
  196. if (!this.audioContext) {
  197. this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
  198. }
  199.  
  200. const oscillator = this.audioContext.createOscillator();
  201. const gainNode = this.audioContext.createGain();
  202.  
  203. oscillator.connect(gainNode);
  204. gainNode.connect(this.audioContext.destination);
  205.  
  206. if (type === 'mention') {
  207. // Double beep for mentions
  208. oscillator.frequency.setValueAtTime(880, this.audioContext.currentTime);
  209. oscillator.frequency.setValueAtTime(1100, this.audioContext.currentTime + 0.1);
  210. gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime);
  211. gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.3);
  212. oscillator.start();
  213. oscillator.stop(this.audioContext.currentTime + 0.3);
  214. } else {
  215. // Soft pop for new messages
  216. oscillator.frequency.setValueAtTime(600, this.audioContext.currentTime);
  217. gainNode.gain.setValueAtTime(0.05, this.audioContext.currentTime);
  218. gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.15);
  219. oscillator.start();
  220. oscillator.stop(this.audioContext.currentTime + 0.15);
  221. }
  222. } catch (e) {
  223. // Audio not supported or blocked
  224. }
  225. },
  226.  
  227. toggle() {
  228. this.enabled = !this.enabled;
  229. SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.SOUNDS, this.enabled);
  230. return this.enabled;
  231. }
  232. };
  233.  
  234. // ==================== NOTIFICATION SYSTEM ====================
  235. const NotificationSystem = {
  236. enabled: false,
  237. permission: 'default',
  238.  
  239. async init() {
  240. this.enabled = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.NOTIFICATIONS, false);
  241.  
  242. if (this.enabled && 'Notification' in window) {
  243. this.permission = Notification.permission;
  244. if (this.permission === 'default') {
  245. this.permission = await Notification.requestPermission();
  246. }
  247. }
  248. },
  249.  
  250. async requestPermission() {
  251. if (!('Notification' in window)) return false;
  252.  
  253. this.permission = await Notification.requestPermission();
  254. return this.permission === 'granted';
  255. },
  256.  
  257. send(title, options = {}) {
  258. if (!this.enabled || this.permission !== 'granted') return;
  259. if (document.hasFocus()) return; // Don't notify if page is focused
  260.  
  261. try {
  262. const notification = new Notification(title, {
  263. icon: 'https://onche.org/favicon.ico',
  264. badge: 'https://onche.org/favicon.ico',
  265. tag: 'onchat-' + Date.now(),
  266. renotify: false,
  267. silent: true, // We handle sounds ourselves
  268. ...options
  269. });
  270.  
  271. notification.onclick = () => {
  272. window.focus();
  273. notification.close();
  274. };
  275.  
  276. setTimeout(() => notification.close(), 5000);
  277. } catch (e) {
  278. Logger.warn('Notification failed:', e);
  279. }
  280. },
  281.  
  282. async toggle() {
  283. if (!this.enabled) {
  284. const granted = await this.requestPermission();
  285. this.enabled = granted;
  286. } else {
  287. this.enabled = false;
  288. }
  289. SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.NOTIFICATIONS, this.enabled);
  290. return this.enabled;
  291. }
  292. };
  293.  
  294. // ==================== TOAST SYSTEM ====================
  295. const ToastSystem = {
  296. container: null,
  297.  
  298. init() {
  299. this.container = document.createElement('div');
  300. this.container.id = 'onchat-toasts';
  301. this.container.innerHTML = '';
  302. document.body.appendChild(this.container);
  303. },
  304.  
  305. show(message, type = 'info', duration = CONFIG.UI.TOAST_DURATION) {
  306. const toast = document.createElement('div');
  307. toast.className = `onchat-toast onchat-toast-${type}`;
  308.  
  309. const icons = {
  310. info: '💬',
  311. success: '✓',
  312. warning: '⚠',
  313. error: '✕',
  314. mention: '📢'
  315. };
  316.  
  317. toast.innerHTML = `
  318. <span class="onchat-toast-icon">${icons[type] || icons.info}</span>
  319. <span class="onchat-toast-message">${SecurityUtils.escapeHTML(message)}</span>
  320. `;
  321.  
  322. this.container.appendChild(toast);
  323.  
  324. // Trigger animation
  325. requestAnimationFrame(() => {
  326. toast.classList.add('onchat-toast-visible');
  327. });
  328.  
  329. setTimeout(() => {
  330. toast.classList.remove('onchat-toast-visible');
  331. toast.classList.add('onchat-toast-hiding');
  332. setTimeout(() => toast.remove(), 300);
  333. }, duration);
  334. },
  335.  
  336. info(msg) { this.show(msg, 'info'); },
  337. success(msg) { this.show(msg, 'success'); },
  338. warning(msg) { this.show(msg, 'warning'); },
  339. error(msg) { this.show(msg, 'error'); },
  340. mention(msg) { this.show(msg, 'mention', 5000); }
  341. };
  342.  
  343. // ==================== MENTION SYSTEM ====================
  344. const MentionSystem = {
  345. keywords: [],
  346. username: '',
  347.  
  348. init() {
  349. this.username = this.detectUsername();
  350. const saved = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.MENTIONS_KEYWORDS, '');
  351. this.keywords = saved ? saved.split(',').map(k => k.trim().toLowerCase()).filter(Boolean) : [];
  352.  
  353. // Always include username
  354. if (this.username && !this.keywords.includes(this.username.toLowerCase())) {
  355. this.keywords.unshift(this.username.toLowerCase());
  356. }
  357. },
  358.  
  359. detectUsername() {
  360. // Try to find username from page
  361. const selectors = [
  362. '.user-menu__name',
  363. '.header__user__name',
  364. '[data-username]',
  365. '.profile-link'
  366. ];
  367.  
  368. for (const sel of selectors) {
  369. const el = document.querySelector(sel);
  370. if (el) {
  371. const name = el.dataset.username || el.textContent.trim();
  372. if (name) {
  373. SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.USERNAME, name);
  374. return name;
  375. }
  376. }
  377. }
  378.  
  379. return SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.USERNAME, '');
  380. },
  381.  
  382. setKeywords(keywords) {
  383. this.keywords = keywords.split(',').map(k => k.trim().toLowerCase()).filter(Boolean);
  384. if (this.username && !this.keywords.includes(this.username.toLowerCase())) {
  385. this.keywords.unshift(this.username.toLowerCase());
  386. }
  387. SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.MENTIONS_KEYWORDS, this.keywords.join(','));
  388. },
  389.  
  390. checkMessage(messageElement) {
  391. if (!this.keywords.length) return false;
  392.  
  393. const content = messageElement.querySelector(CONFIG.SELECTORS.MESSAGE_CONTENT);
  394. if (!content) return false;
  395.  
  396. const text = content.textContent.toLowerCase();
  397.  
  398. for (const keyword of this.keywords) {
  399. if (text.includes(keyword)) {
  400. messageElement.classList.add(CONFIG.CLASSES.MENTION);
  401. return true;
  402. }
  403. }
  404. return false;
  405. }
  406. };
  407.  
  408. // ==================== BLOCKED USERS SYSTEM ====================
  409. const BlockSystem = {
  410. blocked: new Set(),
  411.  
  412. init() {
  413. const saved = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.BLOCKED_USERS, '');
  414. this.blocked = new Set(saved ? saved.split(',').filter(Boolean) : []);
  415. },
  416.  
  417. isBlocked(username) {
  418. return this.blocked.has(username.toLowerCase());
  419. },
  420.  
  421. toggle(username) {
  422. const lower = username.toLowerCase();
  423. if (this.blocked.has(lower)) {
  424. this.blocked.delete(lower);
  425. } else {
  426. this.blocked.add(lower);
  427. }
  428. this.save();
  429. return this.blocked.has(lower);
  430. },
  431.  
  432. save() {
  433. SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.BLOCKED_USERS, Array.from(this.blocked).join(','));
  434. },
  435.  
  436. filterMessages(container) {
  437. if (!this.blocked.size) return;
  438.  
  439. container.querySelectorAll(CONFIG.SELECTORS.MESSAGE).forEach(msg => {
  440. const authorEl = msg.querySelector(CONFIG.SELECTORS.MESSAGE_AUTHOR);
  441. if (authorEl) {
  442. const author = authorEl.textContent.trim().toLowerCase();
  443. if (this.blocked.has(author)) {
  444. msg.classList.add('onchat-blocked');
  445. }
  446. }
  447. });
  448. }
  449. };
  450.  
  451. // ==================== FAVORITES SYSTEM ====================
  452. const FavoritesSystem = {
  453. favorites: new Set(),
  454.  
  455. init() {
  456. const saved = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.FAVORITE_TOPICS, '');
  457. this.favorites = new Set(saved ? saved.split(',').filter(Boolean) : []);
  458. },
  459.  
  460. isFavorite(topicId) {
  461. return this.favorites.has(String(topicId));
  462. },
  463.  
  464. toggle(topicId) {
  465. const id = String(topicId);
  466. if (this.favorites.has(id)) {
  467. this.favorites.delete(id);
  468. } else {
  469. this.favorites.add(id);
  470. }
  471. this.save();
  472. return this.favorites.has(id);
  473. },
  474.  
  475. save() {
  476. SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.FAVORITE_TOPICS, Array.from(this.favorites).join(','));
  477. },
  478.  
  479. getTopicId() {
  480. const match = window.location.pathname.match(/\/topic\/(\d+)/);
  481. return match ? match[1] : null;
  482. }
  483. };
  484.  
  485. // ==================== KEYBOARD SHORTCUTS ====================
  486. const KeyboardSystem = {
  487. shortcuts: new Map(),
  488.  
  489. init() {
  490. // Define shortcuts
  491. this.register('r', () => this.focusReply(), 'Répondre');
  492. this.register('j', () => this.nextMessage(), 'Message suivant');
  493. this.register('k', () => this.prevMessage(), 'Message précédent');
  494. this.register('g', () => this.scrollToBottom(), 'Aller en bas');
  495. this.register('t', () => this.scrollToTop(), 'Aller en haut');
  496. this.register('l', () => StateManager.toggle('liveMode'), 'Toggle Live');
  497. this.register('/', () => this.openSearch(), 'Rechercher');
  498. this.register('?', () => this.showHelp(), 'Aide raccourcis');
  499. this.register('Escape', () => this.closeModals(), 'Fermer');
  500.  
  501. document.addEventListener('keydown', (e) => this.handleKeydown(e));
  502. },
  503.  
  504. register(key, callback, description) {
  505. this.shortcuts.set(key.toLowerCase(), { callback, description, key });
  506. },
  507.  
  508. handleKeydown(e) {
  509. // Ignore when typing
  510. if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) {
  511. if (e.key === 'Escape') {
  512. e.target.blur();
  513. }
  514. return;
  515. }
  516.  
  517. const shortcut = this.shortcuts.get(e.key.toLowerCase());
  518. if (shortcut) {
  519. e.preventDefault();
  520. shortcut.callback();
  521. }
  522. },
  523.  
  524. focusReply() {
  525. const textarea = document.querySelector(CONFIG.SELECTORS.TEXTAREA);
  526. if (textarea) {
  527. textarea.scrollIntoView({ behavior: 'smooth', block: 'center' });
  528. setTimeout(() => textarea.focus(), 300);
  529. }
  530. },
  531.  
  532. currentMessageIndex: -1,
  533.  
  534. nextMessage() {
  535. const messages = document.querySelectorAll(CONFIG.SELECTORS.MESSAGE);
  536. if (!messages.length) return;
  537.  
  538. this.currentMessageIndex = Math.min(this.currentMessageIndex + 1, messages.length - 1);
  539. this.highlightMessage(messages[this.currentMessageIndex]);
  540. },
  541.  
  542. prevMessage() {
  543. const messages = document.querySelectorAll(CONFIG.SELECTORS.MESSAGE);
  544. if (!messages.length) return;
  545.  
  546. this.currentMessageIndex = Math.max(this.currentMessageIndex - 1, 0);
  547. this.highlightMessage(messages[this.currentMessageIndex]);
  548. },
  549.  
  550. highlightMessage(msg) {
  551. document.querySelectorAll('.onchat-keyboard-focus').forEach(el => {
  552. el.classList.remove('onchat-keyboard-focus');
  553. });
  554. msg.classList.add('onchat-keyboard-focus');
  555. msg.scrollIntoView({ behavior: 'smooth', block: 'center' });
  556. },
  557.  
  558. scrollToBottom() {
  559. window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
  560. },
  561.  
  562. scrollToTop() {
  563. window.scrollTo({ top: 0, behavior: 'smooth' });
  564. },
  565.  
  566. openSearch() {
  567. ToastSystem.info('Recherche: Ctrl+F');
  568. },
  569.  
  570. showHelp() {
  571. let html = '<div class="onchat-shortcuts-help">';
  572. html += '<h3>⌨️ Raccourcis clavier</h3>';
  573. html += '<div class="onchat-shortcuts-grid">';
  574.  
  575. this.shortcuts.forEach(({ key, description }) => {
  576. html += `<div class="onchat-shortcut-item">
  577. <kbd>${key}</kbd>
  578. <span>${description}</span>
  579. </div>`;
  580. });
  581.  
  582. html += '</div></div>';
  583.  
  584. // Show in modal
  585. UIManager.showModal('Raccourcis', html);
  586. },
  587.  
  588. closeModals() {
  589. UIManager.closeModal();
  590. UIManager.closePanel();
  591. }
  592. };
  593.  
  594. // ==================== SECURITY UTILITIES ====================
  595. const SecurityUtils = {
  596. init() {
  597. try {
  598. if (!Object.isFrozen(Object.prototype)) {
  599. Object.freeze(Object.prototype);
  600. }
  601. } catch (e) {
  602. Logger.warn('Could not freeze Object.prototype');
  603. }
  604. },
  605.  
  606. validateNumericId(id) {
  607. const numId = typeof id === 'string' ? parseInt(id, 10) : id;
  608. if (isNaN(numId) || numId < 1 || numId > Number.MAX_SAFE_INTEGER) return null;
  609. return numId;
  610. },
  611.  
  612. validateURL(url) {
  613. if (!url || typeof url !== 'string') return null;
  614. try {
  615. const parsed = new URL(url, window.location.origin);
  616. if (parsed.origin !== window.location.origin) return null;
  617. if (!['http:', 'https:'].includes(parsed.protocol)) return null;
  618. return parsed.href;
  619. } catch {
  620. return null;
  621. }
  622. },
  623.  
  624. escapeHTML(str) {
  625. if (!str) return '';
  626. const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
  627. return String(str).replace(/[&<>"']/g, s => map[s]);
  628. },
  629.  
  630. safeGMSetValue(key, value) {
  631. if (['__proto__', 'constructor', 'prototype'].includes(key)) return;
  632. if (!/^[a-zA-Z0-9_]+$/.test(key)) return;
  633. GM_setValue(key, value);
  634. },
  635.  
  636. safeGMGetValue(key, defaultValue) {
  637. if (['__proto__', 'constructor', 'prototype'].includes(key)) return defaultValue;
  638. return GM_getValue(key, defaultValue);
  639. },
  640.  
  641. createRateLimiter(max, windowMs) {
  642. const attempts = new Map();
  643. return function(key) {
  644. const now = Date.now();
  645. const recent = (attempts.get(key) || []).filter(t => now - t < windowMs);
  646. if (recent.length >= max) return false;
  647. recent.push(now);
  648. attempts.set(key, recent);
  649. return true;
  650. };
  651. }
  652. };
  653.  
  654. // ==================== LOGGER ====================
  655. const Logger = {
  656. prefix: `%c[OnChat ${CONFIG.VERSION}]`,
  657. style: 'color: #6366f1; font-weight: bold',
  658.  
  659. info(...args) { console.info(this.prefix, this.style, ...args); },
  660. warn(...args) { console.warn(this.prefix, this.style, ...args); },
  661. error(...args) { console.error(this.prefix, this.style, ...args); },
  662. debug(...args) {
  663. if (CONFIG.DEBUG) console.log(this.prefix, this.style, '[DEBUG]', ...args);
  664. }
  665. };
  666.  
  667. // ==================== STATE MANAGER ====================
  668. const StateManager = {
  669. state: {
  670. liveMode: true,
  671. refreshInterval: CONFIG.POLLING.DEFAULT,
  672. notifications: false,
  673. sounds: true,
  674. compactMode: false,
  675. currentPage: 1,
  676. lastMessageId: null,
  677. unreadCount: 0,
  678. isPolling: false,
  679. lastActivity: Date.now()
  680. },
  681.  
  682. listeners: new Set(),
  683.  
  684. init() {
  685. // Load from storage
  686. this.state.liveMode = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.LIVE_MODE, true);
  687. this.state.refreshInterval = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.REFRESH_INTERVAL, CONFIG.POLLING.DEFAULT);
  688. this.state.compactMode = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.COMPACT_MODE, false);
  689.  
  690. // Extract current page
  691. const match = window.location.pathname.match(/\/(\d+)$/);
  692. this.state.currentPage = match ? parseInt(match[1], 10) : 1;
  693. },
  694.  
  695. get(key) {
  696. return this.state[key];
  697. },
  698.  
  699. set(key, value) {
  700. const old = this.state[key];
  701. if (old === value) return;
  702.  
  703. this.state[key] = value;
  704. this.notify(key, value, old);
  705.  
  706. // Persist certain keys
  707. const persistKeys = {
  708. liveMode: CONFIG.STORAGE_KEYS.LIVE_MODE,
  709. refreshInterval: CONFIG.STORAGE_KEYS.REFRESH_INTERVAL,
  710. compactMode: CONFIG.STORAGE_KEYS.COMPACT_MODE
  711. };
  712.  
  713. if (persistKeys[key]) {
  714. SecurityUtils.safeGMSetValue(persistKeys[key], value);
  715. }
  716. },
  717.  
  718. toggle(key) {
  719. this.set(key, !this.get(key));
  720. return this.get(key);
  721. },
  722.  
  723. subscribe(callback) {
  724. this.listeners.add(callback);
  725. return () => this.listeners.delete(callback);
  726. },
  727.  
  728. notify(key, newVal, oldVal) {
  729. this.listeners.forEach(cb => {
  730. try { cb(key, newVal, oldVal); }
  731. catch (e) { Logger.error('State listener error:', e); }
  732. });
  733. },
  734.  
  735. updateActivity() {
  736. this.state.lastActivity = Date.now();
  737. }
  738. };
  739.  
  740. // ==================== POLLER ====================
  741. const Poller = {
  742. timeoutId: null,
  743. interval: CONFIG.POLLING.DEFAULT,
  744. missCount: 0,
  745.  
  746. start() {
  747. this.stop();
  748. this.interval = StateManager.get('refreshInterval');
  749. Logger.info(`Polling started @ ${this.interval}ms`);
  750. this.poll();
  751. },
  752.  
  753. stop() {
  754. if (this.timeoutId) {
  755. clearTimeout(this.timeoutId);
  756. this.timeoutId = null;
  757. }
  758. },
  759.  
  760. async poll() {
  761. if (!StateManager.get('liveMode')) {
  762. this.stop();
  763. return;
  764. }
  765.  
  766. try {
  767. const hasNew = await MessageManager.fetchNew();
  768.  
  769. if (hasNew) {
  770. this.interval = StateManager.get('refreshInterval');
  771. this.missCount = 0;
  772. } else {
  773. this.missCount++;
  774. // Slow down if no activity
  775. if (this.missCount > 10) {
  776. this.interval = Math.min(
  777. this.interval * CONFIG.POLLING.BACKOFF_MULTIPLIER,
  778. CONFIG.POLLING.MAX_INTERVAL
  779. );
  780. }
  781. }
  782. } catch (e) {
  783. Logger.error('Poll error:', e);
  784. this.interval = Math.min(this.interval * 2, CONFIG.POLLING.MAX_INTERVAL);
  785. }
  786.  
  787. if (StateManager.get('liveMode')) {
  788. this.timeoutId = setTimeout(() => this.poll(), this.interval);
  789. }
  790. },
  791.  
  792. setInterval(ms) {
  793. this.interval = ms;
  794. this.missCount = 0;
  795. if (StateManager.get('liveMode')) {
  796. this.start();
  797. }
  798. }
  799. };
  800.  
  801. // ==================== MESSAGE MANAGER ====================
  802. const MessageManager = {
  803. processed: new Set(),
  804. lastId: null,
  805. reactionLimiter: null,
  806. messageLimiter: null,
  807.  
  808. init() {
  809. this.reactionLimiter = SecurityUtils.createRateLimiter(
  810. CONFIG.SECURITY.MAX_REACTIONS_PER_WINDOW,
  811. CONFIG.SECURITY.REACTION_WINDOW_MS
  812. );
  813. this.messageLimiter = SecurityUtils.createRateLimiter(
  814. CONFIG.SECURITY.MAX_MESSAGES_PER_WINDOW,
  815. CONFIG.SECURITY.MESSAGE_WINDOW_MS
  816. );
  817.  
  818. // Initialize with current messages
  819. const container = document.querySelector(CONFIG.SELECTORS.MESSAGES_CONTAINER);
  820. if (container) {
  821. container.querySelectorAll(CONFIG.SELECTORS.MESSAGE).forEach(msg => {
  822. this.processed.add(msg.dataset.id);
  823. if (!this.lastId || parseInt(msg.dataset.id) > parseInt(this.lastId)) {
  824. this.lastId = msg.dataset.id;
  825. }
  826. });
  827. }
  828. },
  829.  
  830. async fetchNew() {
  831. const container = document.querySelector(CONFIG.SELECTORS.MESSAGES_CONTAINER);
  832. if (!container) return false;
  833.  
  834. const controller = new AbortController();
  835. const timeout = setTimeout(() => controller.abort(), CONFIG.SECURITY.FETCH_TIMEOUT);
  836.  
  837. try {
  838. const response = await fetch(window.location.href, {
  839. signal: controller.signal,
  840. credentials: 'same-origin'
  841. });
  842. clearTimeout(timeout);
  843.  
  844. if (!response.ok) throw new Error(`HTTP ${response.status}`);
  845.  
  846. const html = await response.text();
  847. const doc = new DOMParser().parseFromString(html, 'text/html');
  848. const newContainer = doc.querySelector(CONFIG.SELECTORS.MESSAGES_CONTAINER);
  849.  
  850. if (!newContainer) return false;
  851.  
  852. const newMessages = [];
  853. newContainer.querySelectorAll(CONFIG.SELECTORS.MESSAGE).forEach(msg => {
  854. const id = msg.dataset.id;
  855. if (!this.processed.has(id)) {
  856. newMessages.push(msg);
  857. }
  858. });
  859.  
  860. if (newMessages.length > 0) {
  861. this.insertMessages(container, newMessages);
  862. return true;
  863. }
  864.  
  865. return false;
  866. } catch (e) {
  867. clearTimeout(timeout);
  868. if (e.name !== 'AbortError') {
  869. Logger.error('Fetch error:', e);
  870. }
  871. throw e;
  872. }
  873. },
  874.  
  875. insertMessages(container, messages) {
  876. const fragment = document.createDocumentFragment();
  877.  
  878. messages.forEach(msg => {
  879. const clone = msg.cloneNode(true);
  880. const id = clone.dataset.id;
  881.  
  882. clone.classList.add(CONFIG.CLASSES.NEW_MESSAGE);
  883.  
  884. // Check for mentions
  885. const isMention = MentionSystem.checkMessage(clone);
  886.  
  887. // Add to fragment
  888. fragment.appendChild(clone);
  889. this.processed.add(id);
  890.  
  891. if (!this.lastId || parseInt(id) > parseInt(this.lastId)) {
  892. this.lastId = id;
  893. }
  894.  
  895. // Notifications
  896. if (isMention) {
  897. const author = clone.querySelector(CONFIG.SELECTORS.MESSAGE_AUTHOR)?.textContent.trim() || 'Quelqu\'un';
  898. SoundSystem.play('mention');
  899. NotificationSystem.send(`${author} vous a mentionné`, {
  900. body: clone.querySelector(CONFIG.SELECTORS.MESSAGE_CONTENT)?.textContent.slice(0, 100) || ''
  901. });
  902. ToastSystem.mention(`Mention de ${author}`);
  903. } else {
  904. SoundSystem.play('message');
  905. }
  906. });
  907.  
  908. container.appendChild(fragment);
  909.  
  910. // Apply block filter
  911. BlockSystem.filterMessages(container);
  912.  
  913. // Update unread count
  914. StateManager.set('unreadCount', StateManager.get('unreadCount') + messages.length);
  915.  
  916. // Auto-scroll if near bottom
  917. const scrolledFromBottom = document.documentElement.scrollHeight -
  918. (window.pageYOffset + document.documentElement.clientHeight);
  919.  
  920. if (scrolledFromBottom < 200) {
  921. window.scrollTo({
  922. top: document.documentElement.scrollHeight,
  923. behavior: 'smooth'
  924. });
  925. }
  926.  
  927. Logger.info(`+${messages.length} new messages`);
  928. UIManager.updateStatus();
  929. }
  930. };
  931.  
  932. // ==================== UI MANAGER ====================
  933. const UIManager = {
  934. elements: {},
  935. panelOpen: false,
  936. modalOpen: false,
  937.  
  938. init() {
  939. this.injectStyles();
  940. this.createUI();
  941. this.bindEvents();
  942. this.updateStatus();
  943.  
  944. // Apply compact mode
  945. if (StateManager.get('compactMode')) {
  946. document.body.classList.add('onchat-compact');
  947. }
  948. },
  949.  
  950. injectStyles() {
  951. GM_addStyle(`
  952. /* ═══════════════════════════════════════════════════════════════
  953. ON'CHAT PRO ULTIMATE - STYLES
  954. ═══════════════════════════════════════════════════════════════ */
  955.  
  956. :root {
  957. --onchat-font: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  958. --onchat-mono: 'SF Mono', 'Fira Code', monospace;
  959. --onchat-radius: 12px;
  960. --onchat-radius-sm: 8px;
  961. --onchat-transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  962. }
  963.  
  964. /* ═══ ANIMATIONS ═══ */
  965. @keyframes onchat-slideIn {
  966. from { opacity: 0; transform: translateY(20px) scale(0.95); }
  967. to { opacity: 1; transform: translateY(0) scale(1); }
  968. }
  969.  
  970. @keyframes onchat-fadeIn {
  971. from { opacity: 0; }
  972. to { opacity: 1; }
  973. }
  974.  
  975. @keyframes onchat-pulse {
  976. 0%, 100% { transform: scale(1); }
  977. 50% { transform: scale(1.05); }
  978. }
  979.  
  980. @keyframes onchat-glow {
  981. 0%, 100% { box-shadow: 0 0 5px var(--onchat-accent-glow); }
  982. 50% { box-shadow: 0 0 20px var(--onchat-accent-glow), 0 0 30px var(--onchat-accent-glow); }
  983. }
  984.  
  985. @keyframes onchat-shake {
  986. 0%, 100% { transform: translateX(0); }
  987. 25% { transform: translateX(-5px); }
  988. 75% { transform: translateX(5px); }
  989. }
  990.  
  991. /* ═══ NEW MESSAGE ANIMATION ═══ */
  992. .${CONFIG.CLASSES.NEW_MESSAGE} {
  993. animation: onchat-slideIn 0.4s ease-out;
  994. }
  995.  
  996. .${CONFIG.CLASSES.HIGHLIGHT}::before {
  997. content: '';
  998. position: absolute;
  999. left: 0;
  1000. top: 0;
  1001. bottom: 0;
  1002. width: 4px;
  1003. background: linear-gradient(180deg, var(--onchat-accent), var(--onchat-mention));
  1004. border-radius: 4px;
  1005. animation: onchat-glow 2s ease-in-out infinite;
  1006. }
  1007.  
  1008. .${CONFIG.CLASSES.MENTION} {
  1009. background: linear-gradient(90deg,
  1010. rgba(244, 114, 182, 0.1) 0%,
  1011. transparent 100%) !important;
  1012. border-left: 3px solid var(--onchat-mention) !important;
  1013. }
  1014.  
  1015. .onchat-blocked {
  1016. opacity: 0.3;
  1017. filter: blur(2px);
  1018. transition: all var(--onchat-transition);
  1019. }
  1020.  
  1021. .onchat-blocked:hover {
  1022. opacity: 1;
  1023. filter: none;
  1024. }
  1025.  
  1026. .onchat-keyboard-focus {
  1027. outline: 2px solid var(--onchat-accent) !important;
  1028. outline-offset: 2px;
  1029. }
  1030.  
  1031. /* ═══ CONTROL BAR ═══ */
  1032. #onchat-bar {
  1033. position: fixed;
  1034. bottom: 20px;
  1035. left: 50%;
  1036. transform: translateX(-50%);
  1037. display: flex;
  1038. align-items: center;
  1039. gap: 8px;
  1040. padding: 8px 12px;
  1041. background: var(--onchat-glass);
  1042. backdrop-filter: blur(20px) saturate(180%);
  1043. -webkit-backdrop-filter: blur(20px) saturate(180%);
  1044. border: 1px solid var(--onchat-border);
  1045. border-radius: 50px;
  1046. box-shadow: var(--onchat-shadow);
  1047. z-index: 10000;
  1048. transition: all var(--onchat-transition);
  1049. }
  1050.  
  1051. #onchat-bar:hover {
  1052. border-color: var(--onchat-border-light);
  1053. }
  1054.  
  1055. #onchat-bar.${CONFIG.CLASSES.MINIMIZED} {
  1056. padding: 6px;
  1057. gap: 0;
  1058. }
  1059.  
  1060. #onchat-bar.${CONFIG.CLASSES.MINIMIZED} .onchat-bar-expandable {
  1061. width: 0;
  1062. opacity: 0;
  1063. padding: 0;
  1064. margin: 0;
  1065. overflow: hidden;
  1066. }
  1067.  
  1068. .onchat-bar-btn {
  1069. width: 36px;
  1070. height: 36px;
  1071. border: none;
  1072. border-radius: 50%;
  1073. background: transparent;
  1074. color: var(--onchat-text-secondary);
  1075. font-size: 16px;
  1076. cursor: pointer;
  1077. display: flex;
  1078. align-items: center;
  1079. justify-content: center;
  1080. transition: all var(--onchat-transition);
  1081. position: relative;
  1082. }
  1083.  
  1084. .onchat-bar-btn:hover {
  1085. background: var(--onchat-bg-hover);
  1086. color: var(--onchat-text);
  1087. transform: scale(1.1);
  1088. }
  1089.  
  1090. .onchat-bar-btn.${CONFIG.CLASSES.ACTIVE} {
  1091. background: var(--onchat-accent);
  1092. color: white;
  1093. }
  1094.  
  1095. .onchat-bar-btn.${CONFIG.CLASSES.ACTIVE}:hover {
  1096. background: var(--onchat-accent-hover);
  1097. }
  1098.  
  1099. .onchat-bar-btn .onchat-badge {
  1100. position: absolute;
  1101. top: -4px;
  1102. right: -4px;
  1103. min-width: 18px;
  1104. height: 18px;
  1105. padding: 0 5px;
  1106. background: var(--onchat-error);
  1107. color: white;
  1108. font-size: 10px;
  1109. font-weight: 700;
  1110. border-radius: 10px;
  1111. display: flex;
  1112. align-items: center;
  1113. justify-content: center;
  1114. animation: onchat-pulse 1s ease-in-out infinite;
  1115. }
  1116.  
  1117. .onchat-bar-divider {
  1118. width: 1px;
  1119. height: 24px;
  1120. background: var(--onchat-border);
  1121. margin: 0 4px;
  1122. }
  1123.  
  1124. .onchat-bar-status {
  1125. font-size: 12px;
  1126. color: var(--onchat-text-muted);
  1127. padding: 0 8px;
  1128. white-space: nowrap;
  1129. transition: all var(--onchat-transition);
  1130. }
  1131.  
  1132. .onchat-bar-status.live {
  1133. color: var(--onchat-success);
  1134. }
  1135.  
  1136. .onchat-bar-status.live::before {
  1137. content: '';
  1138. display: inline-block;
  1139. width: 6px;
  1140. height: 6px;
  1141. background: var(--onchat-success);
  1142. border-radius: 50%;
  1143. margin-right: 6px;
  1144. animation: onchat-pulse 1.5s ease-in-out infinite;
  1145. }
  1146.  
  1147. .onchat-bar-status.paused {
  1148. color: var(--onchat-warning);
  1149. }
  1150.  
  1151. /* ═══ SETTINGS PANEL ═══ */
  1152. #onchat-panel {
  1153. position: fixed;
  1154. bottom: 80px;
  1155. left: 50%;
  1156. transform: translateX(-50%) scale(0.95);
  1157. width: ${CONFIG.UI.PANEL_WIDTH}px;
  1158. max-height: 70vh;
  1159. background: var(--onchat-bg);
  1160. border: 1px solid var(--onchat-border);
  1161. border-radius: var(--onchat-radius);
  1162. box-shadow: var(--onchat-shadow);
  1163. z-index: 10001;
  1164. opacity: 0;
  1165. visibility: hidden;
  1166. transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  1167. overflow: hidden;
  1168. }
  1169.  
  1170. #onchat-panel.${CONFIG.CLASSES.VISIBLE} {
  1171. opacity: 1;
  1172. visibility: visible;
  1173. transform: translateX(-50%) scale(1);
  1174. }
  1175.  
  1176. .onchat-panel-header {
  1177. display: flex;
  1178. align-items: center;
  1179. justify-content: space-between;
  1180. padding: 16px 20px;
  1181. border-bottom: 1px solid var(--onchat-border);
  1182. background: var(--onchat-bg-secondary);
  1183. }
  1184.  
  1185. .onchat-panel-title {
  1186. font-size: 14px;
  1187. font-weight: 600;
  1188. color: var(--onchat-text);
  1189. display: flex;
  1190. align-items: center;
  1191. gap: 8px;
  1192. }
  1193.  
  1194. .onchat-panel-version {
  1195. font-size: 10px;
  1196. color: var(--onchat-text-muted);
  1197. background: var(--onchat-bg-tertiary);
  1198. padding: 2px 6px;
  1199. border-radius: 4px;
  1200. }
  1201.  
  1202. .onchat-panel-close {
  1203. width: 28px;
  1204. height: 28px;
  1205. border: none;
  1206. border-radius: 6px;
  1207. background: transparent;
  1208. color: var(--onchat-text-muted);
  1209. font-size: 18px;
  1210. cursor: pointer;
  1211. display: flex;
  1212. align-items: center;
  1213. justify-content: center;
  1214. transition: all var(--onchat-transition);
  1215. }
  1216.  
  1217. .onchat-panel-close:hover {
  1218. background: var(--onchat-bg-hover);
  1219. color: var(--onchat-text);
  1220. }
  1221.  
  1222. .onchat-panel-content {
  1223. padding: 12px 0;
  1224. max-height: 50vh;
  1225. overflow-y: auto;
  1226. }
  1227.  
  1228. .onchat-panel-section {
  1229. padding: 8px 20px;
  1230. }
  1231.  
  1232. .onchat-panel-section-title {
  1233. font-size: 11px;
  1234. font-weight: 600;
  1235. color: var(--onchat-text-muted);
  1236. text-transform: uppercase;
  1237. letter-spacing: 0.5px;
  1238. margin-bottom: 12px;
  1239. }
  1240.  
  1241. .onchat-setting {
  1242. display: flex;
  1243. align-items: center;
  1244. justify-content: space-between;
  1245. padding: 10px 0;
  1246. }
  1247.  
  1248. .onchat-setting-info {
  1249. flex: 1;
  1250. }
  1251.  
  1252. .onchat-setting-label {
  1253. font-size: 13px;
  1254. color: var(--onchat-text);
  1255. font-weight: 500;
  1256. }
  1257.  
  1258. .onchat-setting-desc {
  1259. font-size: 11px;
  1260. color: var(--onchat-text-muted);
  1261. margin-top: 2px;
  1262. }
  1263.  
  1264. /* Toggle Switch */
  1265. .onchat-toggle {
  1266. position: relative;
  1267. width: 44px;
  1268. height: 24px;
  1269. background: var(--onchat-bg-tertiary);
  1270. border-radius: 12px;
  1271. cursor: pointer;
  1272. transition: all var(--onchat-transition);
  1273. }
  1274.  
  1275. .onchat-toggle.${CONFIG.CLASSES.ACTIVE} {
  1276. background: var(--onchat-accent);
  1277. }
  1278.  
  1279. .onchat-toggle::after {
  1280. content: '';
  1281. position: absolute;
  1282. top: 3px;
  1283. left: 3px;
  1284. width: 18px;
  1285. height: 18px;
  1286. background: white;
  1287. border-radius: 50%;
  1288. transition: all var(--onchat-transition);
  1289. box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  1290. }
  1291.  
  1292. .onchat-toggle.${CONFIG.CLASSES.ACTIVE}::after {
  1293. transform: translateX(20px);
  1294. }
  1295.  
  1296. /* Select */
  1297. .onchat-select {
  1298. background: var(--onchat-bg-tertiary);
  1299. border: 1px solid var(--onchat-border);
  1300. border-radius: var(--onchat-radius-sm);
  1301. color: var(--onchat-text);
  1302. padding: 6px 10px;
  1303. font-size: 12px;
  1304. cursor: pointer;
  1305. outline: none;
  1306. transition: all var(--onchat-transition);
  1307. }
  1308.  
  1309. .onchat-select:hover {
  1310. border-color: var(--onchat-border-light);
  1311. }
  1312.  
  1313. .onchat-select:focus {
  1314. border-color: var(--onchat-accent);
  1315. }
  1316.  
  1317. /* Input */
  1318. .onchat-input {
  1319. width: 100%;
  1320. background: var(--onchat-bg-tertiary);
  1321. border: 1px solid var(--onchat-border);
  1322. border-radius: var(--onchat-radius-sm);
  1323. color: var(--onchat-text);
  1324. padding: 10px 12px;
  1325. font-size: 13px;
  1326. outline: none;
  1327. transition: all var(--onchat-transition);
  1328. }
  1329.  
  1330. .onchat-input:focus {
  1331. border-color: var(--onchat-accent);
  1332. box-shadow: 0 0 0 3px var(--onchat-accent-glow);
  1333. }
  1334.  
  1335. .onchat-input::placeholder {
  1336. color: var(--onchat-text-muted);
  1337. }
  1338.  
  1339. /* ═══ TOASTS ═══ */
  1340. #onchat-toasts {
  1341. position: fixed;
  1342. top: 20px;
  1343. right: 20px;
  1344. display: flex;
  1345. flex-direction: column;
  1346. gap: 10px;
  1347. z-index: 10002;
  1348. pointer-events: none;
  1349. }
  1350.  
  1351. .onchat-toast {
  1352. display: flex;
  1353. align-items: center;
  1354. gap: 10px;
  1355. padding: 12px 16px;
  1356. background: var(--onchat-bg);
  1357. border: 1px solid var(--onchat-border);
  1358. border-radius: var(--onchat-radius-sm);
  1359. box-shadow: var(--onchat-shadow);
  1360. transform: translateX(120%);
  1361. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  1362. pointer-events: auto;
  1363. }
  1364.  
  1365. .onchat-toast-visible {
  1366. transform: translateX(0);
  1367. }
  1368.  
  1369. .onchat-toast-hiding {
  1370. transform: translateX(120%);
  1371. opacity: 0;
  1372. }
  1373.  
  1374. .onchat-toast-icon {
  1375. font-size: 16px;
  1376. }
  1377.  
  1378. .onchat-toast-message {
  1379. font-size: 13px;
  1380. color: var(--onchat-text);
  1381. }
  1382.  
  1383. .onchat-toast-success { border-left: 3px solid var(--onchat-success); }
  1384. .onchat-toast-warning { border-left: 3px solid var(--onchat-warning); }
  1385. .onchat-toast-error { border-left: 3px solid var(--onchat-error); }
  1386. .onchat-toast-mention {
  1387. border-left: 3px solid var(--onchat-mention);
  1388. animation: onchat-shake 0.5s ease-in-out;
  1389. }
  1390.  
  1391. /* ═══ MODAL ═══ */
  1392. #onchat-modal-overlay {
  1393. position: fixed;
  1394. inset: 0;
  1395. background: rgba(0, 0, 0, 0.6);
  1396. backdrop-filter: blur(4px);
  1397. z-index: 10003;
  1398. opacity: 0;
  1399. visibility: hidden;
  1400. transition: all 0.25s ease;
  1401. }
  1402.  
  1403. #onchat-modal-overlay.${CONFIG.CLASSES.VISIBLE} {
  1404. opacity: 1;
  1405. visibility: visible;
  1406. }
  1407.  
  1408. #onchat-modal {
  1409. position: fixed;
  1410. top: 50%;
  1411. left: 50%;
  1412. transform: translate(-50%, -50%) scale(0.9);
  1413. width: 90%;
  1414. max-width: 500px;
  1415. max-height: 80vh;
  1416. background: var(--onchat-bg);
  1417. border: 1px solid var(--onchat-border);
  1418. border-radius: var(--onchat-radius);
  1419. box-shadow: var(--onchat-shadow);
  1420. z-index: 10004;
  1421. opacity: 0;
  1422. visibility: hidden;
  1423. transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  1424. overflow: hidden;
  1425. }
  1426.  
  1427. #onchat-modal.${CONFIG.CLASSES.VISIBLE} {
  1428. opacity: 1;
  1429. visibility: visible;
  1430. transform: translate(-50%, -50%) scale(1);
  1431. }
  1432.  
  1433. .onchat-modal-header {
  1434. display: flex;
  1435. align-items: center;
  1436. justify-content: space-between;
  1437. padding: 16px 20px;
  1438. border-bottom: 1px solid var(--onchat-border);
  1439. }
  1440.  
  1441. .onchat-modal-title {
  1442. font-size: 16px;
  1443. font-weight: 600;
  1444. color: var(--onchat-text);
  1445. }
  1446.  
  1447. .onchat-modal-content {
  1448. padding: 20px;
  1449. overflow-y: auto;
  1450. max-height: 60vh;
  1451. }
  1452.  
  1453. /* Shortcuts Help */
  1454. .onchat-shortcuts-help h3 {
  1455. font-size: 14px;
  1456. color: var(--onchat-text);
  1457. margin-bottom: 16px;
  1458. }
  1459.  
  1460. .onchat-shortcuts-grid {
  1461. display: grid;
  1462. grid-template-columns: repeat(2, 1fr);
  1463. gap: 10px;
  1464. }
  1465.  
  1466. .onchat-shortcut-item {
  1467. display: flex;
  1468. align-items: center;
  1469. gap: 10px;
  1470. padding: 8px;
  1471. background: var(--onchat-bg-secondary);
  1472. border-radius: var(--onchat-radius-sm);
  1473. }
  1474.  
  1475. .onchat-shortcut-item kbd {
  1476. min-width: 28px;
  1477. height: 28px;
  1478. display: flex;
  1479. align-items: center;
  1480. justify-content: center;
  1481. background: var(--onchat-bg-tertiary);
  1482. border: 1px solid var(--onchat-border);
  1483. border-radius: 6px;
  1484. font-family: var(--onchat-mono);
  1485. font-size: 12px;
  1486. color: var(--onchat-text);
  1487. }
  1488.  
  1489. .onchat-shortcut-item span {
  1490. font-size: 12px;
  1491. color: var(--onchat-text-secondary);
  1492. }
  1493.  
  1494. /* ═══ FLOATING BUTTONS ═══ */
  1495. .onchat-fab {
  1496. position: fixed;
  1497. width: 48px;
  1498. height: 48px;
  1499. border: none;
  1500. border-radius: 50%;
  1501. background: var(--onchat-bg);
  1502. border: 1px solid var(--onchat-border);
  1503. color: var(--onchat-text-secondary);
  1504. font-size: 20px;
  1505. cursor: pointer;
  1506. box-shadow: var(--onchat-shadow);
  1507. z-index: 9999;
  1508. display: flex;
  1509. align-items: center;
  1510. justify-content: center;
  1511. transition: all var(--onchat-transition);
  1512. opacity: 0;
  1513. visibility: hidden;
  1514. transform: scale(0.8);
  1515. }
  1516.  
  1517. .onchat-fab.${CONFIG.CLASSES.VISIBLE} {
  1518. opacity: 1;
  1519. visibility: visible;
  1520. transform: scale(1);
  1521. }
  1522.  
  1523. .onchat-fab:hover {
  1524. background: var(--onchat-bg-secondary);
  1525. border-color: var(--onchat-accent);
  1526. color: var(--onchat-accent);
  1527. transform: scale(1.1);
  1528. }
  1529.  
  1530. #onchat-scroll-btn {
  1531. bottom: 90px;
  1532. right: 20px;
  1533. }
  1534.  
  1535. #onchat-compose-btn {
  1536. bottom: 90px;
  1537. right: 80px;
  1538. }
  1539.  
  1540. /* ═══ COMPACT MODE ═══ */
  1541. .onchat-compact .message {
  1542. padding: 8px 12px !important;
  1543. }
  1544.  
  1545. .onchat-compact .message__avatar {
  1546. width: 32px !important;
  1547. height: 32px !important;
  1548. }
  1549.  
  1550. .onchat-compact .message__content {
  1551. font-size: 13px !important;
  1552. }
  1553.  
  1554. /* ═══ RESPONSIVE ═══ */
  1555. @media (max-width: 768px) {
  1556. #onchat-bar {
  1557. bottom: 10px;
  1558. padding: 6px 10px;
  1559. gap: 4px;
  1560. }
  1561.  
  1562. .onchat-bar-btn {
  1563. width: 32px;
  1564. height: 32px;
  1565. font-size: 14px;
  1566. }
  1567.  
  1568. .onchat-bar-status {
  1569. display: none;
  1570. }
  1571.  
  1572. #onchat-panel {
  1573. width: 95vw;
  1574. bottom: 70px;
  1575. }
  1576.  
  1577. .onchat-fab {
  1578. width: 44px;
  1579. height: 44px;
  1580. font-size: 18px;
  1581. }
  1582.  
  1583. #onchat-scroll-btn {
  1584. bottom: 80px;
  1585. right: 15px;
  1586. }
  1587.  
  1588. #onchat-compose-btn {
  1589. bottom: 80px;
  1590. right: 70px;
  1591. }
  1592. }
  1593.  
  1594. /* ═══ SCROLLBAR ═══ */
  1595. .onchat-panel-content::-webkit-scrollbar,
  1596. .onchat-modal-content::-webkit-scrollbar {
  1597. width: 6px;
  1598. }
  1599.  
  1600. .onchat-panel-content::-webkit-scrollbar-track,
  1601. .onchat-modal-content::-webkit-scrollbar-track {
  1602. background: transparent;
  1603. }
  1604.  
  1605. .onchat-panel-content::-webkit-scrollbar-thumb,
  1606. .onchat-modal-content::-webkit-scrollbar-thumb {
  1607. background: var(--onchat-border);
  1608. border-radius: 3px;
  1609. }
  1610.  
  1611. .onchat-panel-content::-webkit-scrollbar-thumb:hover,
  1612. .onchat-modal-content::-webkit-scrollbar-thumb:hover {
  1613. background: var(--onchat-border-light);
  1614. }
  1615. `);
  1616. },
  1617.  
  1618. createUI() {
  1619. // Control Bar
  1620. const bar = document.createElement('div');
  1621. bar.id = 'onchat-bar';
  1622. bar.innerHTML = `
  1623. <button class="onchat-bar-btn" data-action="toggle-live" title="Mode Live (L)">
  1624. <span>⚡</span>
  1625. </button>
  1626. <button class="onchat-bar-btn" data-action="toggle-sound" title="Sons">
  1627. <span>🔔</span>
  1628. </button>
  1629. <button class="onchat-bar-btn" data-action="toggle-notifications" title="Notifications">
  1630. <span>📢</span>
  1631. </button>
  1632. <div class="onchat-bar-divider onchat-bar-expandable"></div>
  1633. <span class="onchat-bar-status onchat-bar-expandable">Initializing...</span>
  1634. <div class="onchat-bar-divider onchat-bar-expandable"></div>
  1635. <button class="onchat-bar-btn onchat-bar-expandable" data-action="toggle-favorite" title="Favori">
  1636. <span>☆</span>
  1637. </button>
  1638. <button class="onchat-bar-btn" data-action="toggle-settings" title="Paramètres">
  1639. <span>⚙</span>
  1640. </button>
  1641. <button class="onchat-bar-btn" data-action="toggle-minimize" title="Réduire">
  1642. <span>◀</span>
  1643. </button>
  1644. `;
  1645. document.body.appendChild(bar);
  1646. this.elements.bar = bar;
  1647.  
  1648. // Settings Panel
  1649. const panel = document.createElement('div');
  1650. panel.id = 'onchat-panel';
  1651. panel.innerHTML = `
  1652. <div class="onchat-panel-header">
  1653. <div class="onchat-panel-title">
  1654. <span>⚡ OnChat Pro</span>
  1655. <span class="onchat-panel-version">v${CONFIG.VERSION}</span>
  1656. </div>
  1657. <button class="onchat-panel-close" data-action="close-panel">×</button>
  1658. </div>
  1659. <div class="onchat-panel-content">
  1660. <div class="onchat-panel-section">
  1661. <div class="onchat-panel-section-title">Live Chat</div>
  1662. <div class="onchat-setting">
  1663. <div class="onchat-setting-info">
  1664. <div class="onchat-setting-label">Mode Live</div>
  1665. <div class="onchat-setting-desc">Actualisation automatique des messages</div>
  1666. </div>
  1667. <div class="onchat-toggle ${StateManager.get('liveMode') ? CONFIG.CLASSES.ACTIVE : ''}" data-setting="liveMode"></div>
  1668. </div>
  1669. <div class="onchat-setting">
  1670. <div class="onchat-setting-info">
  1671. <div class="onchat-setting-label">Intervalle</div>
  1672. </div>
  1673. <select class="onchat-select" data-setting="refreshInterval">
  1674. <option value="1000" ${StateManager.get('refreshInterval') === 1000 ? 'selected' : ''}>1s (Ultra)</option>
  1675. <option value="2000" ${StateManager.get('refreshInterval') === 2000 ? 'selected' : ''}>2s (Rapide)</option>
  1676. <option value="3000" ${StateManager.get('refreshInterval') === 3000 ? 'selected' : ''}>3s (Normal)</option>
  1677. <option value="5000" ${StateManager.get('refreshInterval') === 5000 ? 'selected' : ''}>5s (Éco)</option>
  1678. <option value="10000" ${StateManager.get('refreshInterval') === 10000 ? 'selected' : ''}>10s (Lent)</option>
  1679. </select>
  1680. </div>
  1681. </div>
  1682.  
  1683. <div class="onchat-panel-section">
  1684. <div class="onchat-panel-section-title">Notifications</div>
  1685. <div class="onchat-setting">
  1686. <div class="onchat-setting-info">
  1687. <div class="onchat-setting-label">Sons</div>
  1688. <div class="onchat-setting-desc">Bip pour nouveaux messages</div>
  1689. </div>
  1690. <div class="onchat-toggle ${SoundSystem.enabled ? CONFIG.CLASSES.ACTIVE : ''}" data-setting="sounds"></div>
  1691. </div>
  1692. <div class="onchat-setting">
  1693. <div class="onchat-setting-info">
  1694. <div class="onchat-setting-label">Notifications Desktop</div>
  1695. <div class="onchat-setting-desc">Alertes même onglet inactif</div>
  1696. </div>
  1697. <div class="onchat-toggle ${NotificationSystem.enabled ? CONFIG.CLASSES.ACTIVE : ''}" data-setting="notifications"></div>
  1698. </div>
  1699. </div>
  1700.  
  1701. <div class="onchat-panel-section">
  1702. <div class="onchat-panel-section-title">Mentions</div>
  1703. <div class="onchat-setting" style="flex-direction: column; align-items: stretch; gap: 8px;">
  1704. <div class="onchat-setting-info">
  1705. <div class="onchat-setting-label">Mots-clés de mention</div>
  1706. <div class="onchat-setting-desc">Séparés par des virgules (votre pseudo est inclus automatiquement)</div>
  1707. </div>
  1708. <input type="text" class="onchat-input" data-setting="mentions"
  1709. placeholder="mot1, mot2, mot3..."
  1710. value="${SecurityUtils.escapeHTML(MentionSystem.keywords.slice(1).join(', '))}">
  1711. </div>
  1712. </div>
  1713.  
  1714. <div class="onchat-panel-section">
  1715. <div class="onchat-panel-section-title">Apparence</div>
  1716. <div class="onchat-setting">
  1717. <div class="onchat-setting-info">
  1718. <div class="onchat-setting-label">Mode Compact</div>
  1719. <div class="onchat-setting-desc">Messages plus petits</div>
  1720. </div>
  1721. <div class="onchat-toggle ${StateManager.get('compactMode') ? CONFIG.CLASSES.ACTIVE : ''}" data-setting="compactMode"></div>
  1722. </div>
  1723. <div class="onchat-setting">
  1724. <div class="onchat-setting-info">
  1725. <div class="onchat-setting-label">Thème</div>
  1726. </div>
  1727. <select class="onchat-select" data-setting="theme">
  1728. <option value="dark" ${ThemeSystem.current === 'dark' ? 'selected' : ''}>🌙 Sombre</option>
  1729. <option value="light" ${ThemeSystem.current === 'light' ? 'selected' : ''}>☀️ Clair</option>
  1730. </select>
  1731. </div>
  1732. </div>
  1733.  
  1734. <div class="onchat-panel-section" style="border-top: 1px solid var(--onchat-border); padding-top: 16px; margin-top: 8px;">
  1735. <div style="display: flex; justify-content: space-between; align-items: center;">
  1736. <span style="font-size: 11px; color: var(--onchat-text-muted);">
  1737. Codename: ${CONFIG.CODENAME}
  1738. </span>
  1739. <span style="font-size: 11px; color: var(--onchat-text-muted);">
  1740. Raccourcis: <kbd style="background: var(--onchat-bg-tertiary); padding: 2px 6px; border-radius: 4px; font-size: 10px;">?</kbd>
  1741. </span>
  1742. </div>
  1743. </div>
  1744. </div>
  1745. `;
  1746. document.body.appendChild(panel);
  1747. this.elements.panel = panel;
  1748.  
  1749. // Modal
  1750. const modalOverlay = document.createElement('div');
  1751. modalOverlay.id = 'onchat-modal-overlay';
  1752. document.body.appendChild(modalOverlay);
  1753. this.elements.modalOverlay = modalOverlay;
  1754.  
  1755. const modal = document.createElement('div');
  1756. modal.id = 'onchat-modal';
  1757. modal.innerHTML = `
  1758. <div class="onchat-modal-header">
  1759. <span class="onchat-modal-title"></span>
  1760. <button class="onchat-panel-close" data-action="close-modal">×</button>
  1761. </div>
  1762. <div class="onchat-modal-content"></div>
  1763. `;
  1764. document.body.appendChild(modal);
  1765. this.elements.modal = modal;
  1766.  
  1767. // FABs
  1768. const scrollBtn = document.createElement('button');
  1769. scrollBtn.id = 'onchat-scroll-btn';
  1770. scrollBtn.className = 'onchat-fab';
  1771. scrollBtn.innerHTML = '↓';
  1772. scrollBtn.title = 'Aller en bas (G)';
  1773. document.body.appendChild(scrollBtn);
  1774. this.elements.scrollBtn = scrollBtn;
  1775.  
  1776. const composeBtn = document.createElement('button');
  1777. composeBtn.id = 'onchat-compose-btn';
  1778. composeBtn.className = 'onchat-fab';
  1779. composeBtn.innerHTML = '✎';
  1780. composeBtn.title = 'Répondre (R)';
  1781. document.body.appendChild(composeBtn);
  1782. this.elements.composeBtn = composeBtn;
  1783.  
  1784. // Apply minimized state
  1785. if (SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.PANEL_MINIMIZED, false)) {
  1786. bar.classList.add(CONFIG.CLASSES.MINIMIZED);
  1787. }
  1788. },
  1789.  
  1790. bindEvents() {
  1791. // Bar buttons
  1792. this.elements.bar.addEventListener('click', (e) => {
  1793. const btn = e.target.closest('[data-action]');
  1794. if (!btn) return;
  1795.  
  1796. const action = btn.dataset.action;
  1797.  
  1798. switch (action) {
  1799. case 'toggle-live':
  1800. const liveMode = StateManager.toggle('liveMode');
  1801. btn.classList.toggle(CONFIG.CLASSES.ACTIVE, liveMode);
  1802. if (liveMode) {
  1803. Poller.start();
  1804. ToastSystem.success('Mode Live activé');
  1805. } else {
  1806. Poller.stop();
  1807. ToastSystem.info('Mode Live désactivé');
  1808. }
  1809. this.updateStatus();
  1810. break;
  1811.  
  1812. case 'toggle-sound':
  1813. const sounds = SoundSystem.toggle();
  1814. btn.classList.toggle(CONFIG.CLASSES.ACTIVE, sounds);
  1815. ToastSystem.info(sounds ? 'Sons activés' : 'Sons désactivés');
  1816. break;
  1817.  
  1818. case 'toggle-notifications':
  1819. NotificationSystem.toggle().then(enabled => {
  1820. btn.classList.toggle(CONFIG.CLASSES.ACTIVE, enabled);
  1821. ToastSystem.info(enabled ? 'Notifications activées' : 'Notifications désactivées');
  1822. });
  1823. break;
  1824.  
  1825. case 'toggle-favorite':
  1826. const topicId = FavoritesSystem.getTopicId();
  1827. if (topicId) {
  1828. const isFav = FavoritesSystem.toggle(topicId);
  1829. btn.querySelector('span').textContent = isFav ? '★' : '☆';
  1830. ToastSystem.info(isFav ? 'Ajouté aux favoris' : 'Retiré des favoris');
  1831. }
  1832. break;
  1833.  
  1834. case 'toggle-settings':
  1835. this.togglePanel();
  1836. break;
  1837.  
  1838. case 'toggle-minimize':
  1839. const bar = this.elements.bar;
  1840. bar.classList.toggle(CONFIG.CLASSES.MINIMIZED);
  1841. const isMin = bar.classList.contains(CONFIG.CLASSES.MINIMIZED);
  1842. btn.querySelector('span').textContent = isMin ? '▶' : '◀';
  1843. SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.PANEL_MINIMIZED, isMin);
  1844. break;
  1845. }
  1846. });
  1847.  
  1848. // Panel interactions
  1849. this.elements.panel.addEventListener('click', (e) => {
  1850. const toggle = e.target.closest('.onchat-toggle');
  1851. if (toggle) {
  1852. const setting = toggle.dataset.setting;
  1853.  
  1854. switch (setting) {
  1855. case 'liveMode':
  1856. const liveMode = StateManager.toggle('liveMode');
  1857. toggle.classList.toggle(CONFIG.CLASSES.ACTIVE, liveMode);
  1858. if (liveMode) Poller.start();
  1859. else Poller.stop();
  1860. this.updateBarButton('toggle-live', liveMode);
  1861. this.updateStatus();
  1862. break;
  1863.  
  1864. case 'sounds':
  1865. const sounds = SoundSystem.toggle();
  1866. toggle.classList.toggle(CONFIG.CLASSES.ACTIVE, sounds);
  1867. this.updateBarButton('toggle-sound', sounds);
  1868. break;
  1869.  
  1870. case 'notifications':
  1871. NotificationSystem.toggle().then(enabled => {
  1872. toggle.classList.toggle(CONFIG.CLASSES.ACTIVE, enabled);
  1873. this.updateBarButton('toggle-notifications', enabled);
  1874. });
  1875. break;
  1876.  
  1877. case 'compactMode':
  1878. const compact = StateManager.toggle('compactMode');
  1879. toggle.classList.toggle(CONFIG.CLASSES.ACTIVE, compact);
  1880. document.body.classList.toggle('onchat-compact', compact);
  1881. break;
  1882. }
  1883. }
  1884.  
  1885. if (e.target.dataset.action === 'close-panel') {
  1886. this.closePanel();
  1887. }
  1888. });
  1889.  
  1890. // Panel selects
  1891. this.elements.panel.addEventListener('change', (e) => {
  1892. const setting = e.target.dataset.setting;
  1893.  
  1894. if (setting === 'refreshInterval') {
  1895. const value = parseInt(e.target.value, 10);
  1896. StateManager.set('refreshInterval', value);
  1897. Poller.setInterval(value);
  1898. ToastSystem.info(`Intervalle: ${value / 1000}s`);
  1899. }
  1900.  
  1901. if (setting === 'theme') {
  1902. ThemeSystem.apply(e.target.value);
  1903. ToastSystem.info(`Thème: ${e.target.value}`);
  1904. }
  1905. });
  1906.  
  1907. // Mentions input
  1908. const mentionsInput = this.elements.panel.querySelector('[data-setting="mentions"]');
  1909. if (mentionsInput) {
  1910. mentionsInput.addEventListener('change', (e) => {
  1911. MentionSystem.setKeywords(e.target.value);
  1912. ToastSystem.success('Mots-clés mis à jour');
  1913. });
  1914. }
  1915.  
  1916. // Modal
  1917. this.elements.modalOverlay.addEventListener('click', () => this.closeModal());
  1918. this.elements.modal.querySelector('[data-action="close-modal"]').addEventListener('click', () => this.closeModal());
  1919.  
  1920. // FABs
  1921. this.elements.scrollBtn.addEventListener('click', () => {
  1922. window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
  1923. StateManager.set('unreadCount', 0);
  1924. this.updateStatus();
  1925. });
  1926.  
  1927. this.elements.composeBtn.addEventListener('click', () => {
  1928. KeyboardSystem.focusReply();
  1929. });
  1930.  
  1931. // Scroll handler
  1932. let scrollTimeout;
  1933. window.addEventListener('scroll', () => {
  1934. clearTimeout(scrollTimeout);
  1935. scrollTimeout = setTimeout(() => this.updateFABs(), 100);
  1936. }, { passive: true });
  1937.  
  1938. // Activity tracking
  1939. ['mousedown', 'keydown', 'touchstart'].forEach(event => {
  1940. document.addEventListener(event, () => StateManager.updateActivity(), { passive: true });
  1941. });
  1942.  
  1943. // Initial FAB state
  1944. setTimeout(() => this.updateFABs(), 500);
  1945. },
  1946.  
  1947. togglePanel() {
  1948. this.panelOpen = !this.panelOpen;
  1949. this.elements.panel.classList.toggle(CONFIG.CLASSES.VISIBLE, this.panelOpen);
  1950. },
  1951.  
  1952. closePanel() {
  1953. this.panelOpen = false;
  1954. this.elements.panel.classList.remove(CONFIG.CLASSES.VISIBLE);
  1955. },
  1956.  
  1957. showModal(title, content) {
  1958. this.elements.modal.querySelector('.onchat-modal-title').textContent = title;
  1959. this.elements.modal.querySelector('.onchat-modal-content').innerHTML = content;
  1960. this.elements.modalOverlay.classList.add(CONFIG.CLASSES.VISIBLE);
  1961. this.elements.modal.classList.add(CONFIG.CLASSES.VISIBLE);
  1962. this.modalOpen = true;
  1963. },
  1964.  
  1965. closeModal() {
  1966. this.elements.modalOverlay.classList.remove(CONFIG.CLASSES.VISIBLE);
  1967. this.elements.modal.classList.remove(CONFIG.CLASSES.VISIBLE);
  1968. this.modalOpen = false;
  1969. },
  1970.  
  1971. updateStatus() {
  1972. const status = this.elements.bar.querySelector('.onchat-bar-status');
  1973. const unread = StateManager.get('unreadCount');
  1974.  
  1975. if (StateManager.get('liveMode')) {
  1976. status.className = 'onchat-bar-status onchat-bar-expandable live';
  1977. status.textContent = unread > 0 ? `Live · ${unread} nouveau${unread > 1 ? 'x' : ''}` : 'Live';
  1978. } else {
  1979. status.className = 'onchat-bar-status onchat-bar-expandable paused';
  1980. status.textContent = 'Pause';
  1981. }
  1982.  
  1983. // Update live button
  1984. this.updateBarButton('toggle-live', StateManager.get('liveMode'));
  1985.  
  1986. // Update unread badge
  1987. const liveBtn = this.elements.bar.querySelector('[data-action="toggle-live"]');
  1988. let badge = liveBtn.querySelector('.onchat-badge');
  1989.  
  1990. if (unread > 0 && !document.hasFocus()) {
  1991. if (!badge) {
  1992. badge = document.createElement('span');
  1993. badge.className = 'onchat-badge';
  1994. liveBtn.appendChild(badge);
  1995. }
  1996. badge.textContent = unread > 99 ? '99+' : unread;
  1997. } else if (badge) {
  1998. badge.remove();
  1999. }
  2000.  
  2001. // Update title
  2002. if (unread > 0) {
  2003. document.title = `(${unread}) ${document.title.replace(/^\(\d+\)\s*/, '')}`;
  2004. } else {
  2005. document.title = document.title.replace(/^\(\d+\)\s*/, '');
  2006. }
  2007. },
  2008.  
  2009. updateBarButton(action, active) {
  2010. const btn = this.elements.bar.querySelector(`[data-action="${action}"]`);
  2011. if (btn) {
  2012. btn.classList.toggle(CONFIG.CLASSES.ACTIVE, active);
  2013. }
  2014. },
  2015.  
  2016. updateFABs() {
  2017. const scrolledFromBottom = document.documentElement.scrollHeight -
  2018. (window.pageYOffset + document.documentElement.clientHeight);
  2019.  
  2020. const showFABs = scrolledFromBottom > 200;
  2021.  
  2022. this.elements.scrollBtn.classList.toggle(CONFIG.CLASSES.VISIBLE, showFABs);
  2023. this.elements.composeBtn.classList.toggle(CONFIG.CLASSES.VISIBLE, showFABs);
  2024.  
  2025. // Reset unread when at bottom
  2026. if (!showFABs && StateManager.get('unreadCount') > 0) {
  2027. StateManager.set('unreadCount', 0);
  2028. this.updateStatus();
  2029. }
  2030. }
  2031. };
  2032.  
  2033. // ==================== MAIN APPLICATION ====================
  2034. class OnChatUltimate {
  2035. constructor() {
  2036. this.initialized = false;
  2037. }
  2038.  
  2039. async init() {
  2040. if (this.initialized) return;
  2041.  
  2042. try {
  2043. Logger.info('🚀 Initializing...');
  2044.  
  2045. // Security first
  2046. SecurityUtils.init();
  2047.  
  2048. // Core systems
  2049. StateManager.init();
  2050. ThemeSystem.init();
  2051. SoundSystem.init();
  2052. await NotificationSystem.init();
  2053. MentionSystem.init();
  2054. BlockSystem.init();
  2055. FavoritesSystem.init();
  2056.  
  2057. // UI
  2058. ToastSystem.init();
  2059. UIManager.init();
  2060. KeyboardSystem.init();
  2061.  
  2062. // Message handling
  2063. MessageManager.init();
  2064.  
  2065. // Start polling if on topic page
  2066. if (this.isTopicPage()) {
  2067. if (StateManager.get('liveMode')) {
  2068. Poller.start();
  2069. }
  2070.  
  2071. // Check favorite status
  2072. const topicId = FavoritesSystem.getTopicId();
  2073. if (topicId && FavoritesSystem.isFavorite(topicId)) {
  2074. const favBtn = document.querySelector('[data-action="toggle-favorite"] span');
  2075. if (favBtn) favBtn.textContent = '★';
  2076. }
  2077. }
  2078.  
  2079. // Reset unread on focus
  2080. window.addEventListener('focus', () => {
  2081. StateManager.set('unreadCount', 0);
  2082. UIManager.updateStatus();
  2083. });
  2084.  
  2085. // Intercept form submissions
  2086. this.interceptForms();
  2087.  
  2088. this.initialized = true;
  2089. Logger.info('✅ Ready!');
  2090. ToastSystem.success(`OnChat Pro ${CONFIG.VERSION} activé`);
  2091.  
  2092. } catch (error) {
  2093. Logger.error('Init failed:', error);
  2094. ToastSystem.error('Erreur d\'initialisation');
  2095. }
  2096. }
  2097.  
  2098. isTopicPage() {
  2099. return window.location.pathname.includes('/topic/') &&
  2100. document.querySelector(CONFIG.SELECTORS.MESSAGES_CONTAINER);
  2101. }
  2102.  
  2103. interceptForms() {
  2104. const originalFetch = window.fetch;
  2105.  
  2106. window.fetch = async function(...args) {
  2107. const [url, options] = args;
  2108.  
  2109. // Message POST
  2110. if (options?.method === 'POST' && typeof url === 'string' && url.includes('/topic/')) {
  2111. Logger.info('📤 Message POST');
  2112.  
  2113. const response = await originalFetch.apply(this, args);
  2114.  
  2115. if (response.ok) {
  2116. // Trigger immediate poll
  2117. setTimeout(() => {
  2118. MessageManager.fetchNew().then(hasNew => {
  2119. if (hasNew) {
  2120. window.scrollTo({
  2121. top: document.documentElement.scrollHeight,
  2122. behavior: 'smooth'
  2123. });
  2124. }
  2125. });
  2126. }, 500);
  2127. }
  2128.  
  2129. return response;
  2130. }
  2131.  
  2132. return originalFetch.apply(this, args);
  2133. };
  2134. }
  2135. }
  2136.  
  2137. // ==================== BOOTSTRAP ====================
  2138. const app = new OnChatUltimate();
  2139.  
  2140. if (document.readyState === 'loading') {
  2141. document.addEventListener('DOMContentLoaded', () => app.init());
  2142. } else {
  2143. app.init();
  2144. }
  2145.  
  2146. })();
Add Comment
Please, Sign In to add comment