Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name On'Chat Pro Ultimate
- // @namespace http://tampermonkey.net/
- // @version 3.0.0
- // @description Ultimate live chat experience for onche.org - Real-time, beautiful, powerful
- // @author Flow
- // @match https://onche.org/*
- // @grant GM_addStyle
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_notification
- // @grant GM_setClipboard
- // @run-at document-end
- // ==/UserScript==
- (function() {
- 'use strict';
- /* global $ */
- /* eslint-env browser */
- // ╔══════════════════════════════════════════════════════════════════════════════╗
- // ║ ON'CHAT PRO ULTIMATE v3.0 ║
- // ║ The definitive onche.org experience ║
- // ║ Codename: Athena ║
- // ╚══════════════════════════════════════════════════════════════════════════════╝
- // ==================== CONFIGURATION ====================
- const CONFIG = {
- VERSION: '3.0.0',
- CODENAME: 'Athena',
- DEBUG: false,
- POLLING: {
- ULTRA_FAST: 1000,
- FAST: 2000,
- NORMAL: 5000,
- SLOW: 10000,
- IDLE: 30000,
- DEFAULT: 3000,
- MAX_INTERVAL: 60000,
- BACKOFF_MULTIPLIER: 1.3
- },
- UI: {
- ANIMATION_DURATION: 300,
- HIGHLIGHT_DURATION: 4000,
- TOAST_DURATION: 3000,
- BUTTON_SIZE: 44,
- PANEL_WIDTH: 320,
- THEME: 'dark' // 'dark', 'light', 'auto'
- },
- SELECTORS: {
- MESSAGES_CONTAINER: '.messages',
- MESSAGE: '.message',
- MESSAGE_CONTENT: '.message__content',
- MESSAGE_AUTHOR: '.message__info__pseudo',
- MESSAGE_TIME: '.message__info__date',
- TOPIC: '#topic',
- TOPIC_TITLE: '.topic__title, h1',
- TEXTAREA: 'textarea[name="message"]',
- FORM: 'form:has(textarea[name="message"]), form.message-form',
- AVATAR: '.message__avatar img',
- LIKE_BUTTON: '.likeButton',
- PAGINATION: '.pagination'
- },
- CLASSES: {
- NEW_MESSAGE: 'onchat-new',
- HIGHLIGHT: 'onchat-highlight',
- MENTION: 'onchat-mention',
- VISIBLE: 'visible',
- HIDDEN: 'hidden',
- ACTIVE: 'active',
- ERROR: 'error',
- PAUSED: 'paused',
- MINIMIZED: 'minimized'
- },
- STORAGE_KEYS: {
- LIVE_MODE: 'onchat_live_mode',
- REFRESH_INTERVAL: 'onchat_refresh_interval',
- NOTIFICATIONS: 'onchat_notifications',
- SOUNDS: 'onchat_sounds',
- COMPACT_MODE: 'onchat_compact_mode',
- THEME: 'onchat_theme',
- MENTIONS_KEYWORDS: 'onchat_mentions',
- BLOCKED_USERS: 'onchat_blocked',
- FAVORITE_TOPICS: 'onchat_favorites',
- READ_MESSAGES: 'onchat_read',
- PANEL_MINIMIZED: 'onchat_panel_min',
- USERNAME: 'onchat_username'
- },
- SECURITY: {
- MAX_REACTIONS_PER_WINDOW: 10,
- REACTION_WINDOW_MS: 10000,
- MAX_MESSAGES_PER_WINDOW: 5,
- MESSAGE_WINDOW_MS: 60000,
- MIN_TOKEN_LENGTH: 10,
- MAX_TOKEN_LENGTH: 200,
- FETCH_TIMEOUT: 30000,
- REDACT_SENSITIVE: true
- },
- SOUNDS: {
- NEW_MESSAGE: 'data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdH2Coverage...',
- MENTION: 'data:audio/wav;base64,UklGRl9vT19teleQUFhYWm10IBAAAAABAAEAQB8AAMention...'
- }
- };
- // ==================== THEME SYSTEM ====================
- const ThemeSystem = {
- themes: {
- dark: {
- '--onchat-bg': '#0a0a12',
- '--onchat-bg-secondary': '#12121c',
- '--onchat-bg-tertiary': '#1a1a28',
- '--onchat-bg-hover': '#22222f',
- '--onchat-border': '#2a2a3d',
- '--onchat-border-light': '#3a3a4d',
- '--onchat-text': '#e4e4e7',
- '--onchat-text-secondary': '#a1a1aa',
- '--onchat-text-muted': '#71717a',
- '--onchat-accent': '#6366f1',
- '--onchat-accent-hover': '#818cf8',
- '--onchat-accent-glow': 'rgba(99, 102, 241, 0.3)',
- '--onchat-success': '#22c55e',
- '--onchat-warning': '#f59e0b',
- '--onchat-error': '#ef4444',
- '--onchat-mention': '#f472b6',
- '--onchat-shadow': '0 4px 24px rgba(0, 0, 0, 0.5)',
- '--onchat-glass': 'rgba(10, 10, 18, 0.85)'
- },
- light: {
- '--onchat-bg': '#ffffff',
- '--onchat-bg-secondary': '#f4f4f5',
- '--onchat-bg-tertiary': '#e4e4e7',
- '--onchat-bg-hover': '#d4d4d8',
- '--onchat-border': '#d4d4d8',
- '--onchat-border-light': '#e4e4e7',
- '--onchat-text': '#18181b',
- '--onchat-text-secondary': '#52525b',
- '--onchat-text-muted': '#a1a1aa',
- '--onchat-accent': '#4f46e5',
- '--onchat-accent-hover': '#6366f1',
- '--onchat-accent-glow': 'rgba(79, 70, 229, 0.2)',
- '--onchat-success': '#16a34a',
- '--onchat-warning': '#d97706',
- '--onchat-error': '#dc2626',
- '--onchat-mention': '#db2777',
- '--onchat-shadow': '0 4px 24px rgba(0, 0, 0, 0.1)',
- '--onchat-glass': 'rgba(255, 255, 255, 0.9)'
- }
- },
- current: 'dark',
- init() {
- const saved = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.THEME, 'dark');
- this.apply(saved);
- },
- apply(theme) {
- if (!this.themes[theme]) theme = 'dark';
- this.current = theme;
- const vars = this.themes[theme];
- Object.entries(vars).forEach(([key, value]) => {
- document.documentElement.style.setProperty(key, value);
- });
- SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.THEME, theme);
- },
- toggle() {
- this.apply(this.current === 'dark' ? 'light' : 'dark');
- return this.current;
- }
- };
- // ==================== SOUND SYSTEM ====================
- const SoundSystem = {
- enabled: true,
- audioContext: null,
- init() {
- this.enabled = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.SOUNDS, true);
- },
- async play(type) {
- if (!this.enabled) return;
- try {
- if (!this.audioContext) {
- this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
- }
- const oscillator = this.audioContext.createOscillator();
- const gainNode = this.audioContext.createGain();
- oscillator.connect(gainNode);
- gainNode.connect(this.audioContext.destination);
- if (type === 'mention') {
- // Double beep for mentions
- oscillator.frequency.setValueAtTime(880, this.audioContext.currentTime);
- oscillator.frequency.setValueAtTime(1100, this.audioContext.currentTime + 0.1);
- gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime);
- gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.3);
- oscillator.start();
- oscillator.stop(this.audioContext.currentTime + 0.3);
- } else {
- // Soft pop for new messages
- oscillator.frequency.setValueAtTime(600, this.audioContext.currentTime);
- gainNode.gain.setValueAtTime(0.05, this.audioContext.currentTime);
- gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.15);
- oscillator.start();
- oscillator.stop(this.audioContext.currentTime + 0.15);
- }
- } catch (e) {
- // Audio not supported or blocked
- }
- },
- toggle() {
- this.enabled = !this.enabled;
- SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.SOUNDS, this.enabled);
- return this.enabled;
- }
- };
- // ==================== NOTIFICATION SYSTEM ====================
- const NotificationSystem = {
- enabled: false,
- permission: 'default',
- async init() {
- this.enabled = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.NOTIFICATIONS, false);
- if (this.enabled && 'Notification' in window) {
- this.permission = Notification.permission;
- if (this.permission === 'default') {
- this.permission = await Notification.requestPermission();
- }
- }
- },
- async requestPermission() {
- if (!('Notification' in window)) return false;
- this.permission = await Notification.requestPermission();
- return this.permission === 'granted';
- },
- send(title, options = {}) {
- if (!this.enabled || this.permission !== 'granted') return;
- if (document.hasFocus()) return; // Don't notify if page is focused
- try {
- const notification = new Notification(title, {
- icon: 'https://onche.org/favicon.ico',
- badge: 'https://onche.org/favicon.ico',
- tag: 'onchat-' + Date.now(),
- renotify: false,
- silent: true, // We handle sounds ourselves
- ...options
- });
- notification.onclick = () => {
- window.focus();
- notification.close();
- };
- setTimeout(() => notification.close(), 5000);
- } catch (e) {
- Logger.warn('Notification failed:', e);
- }
- },
- async toggle() {
- if (!this.enabled) {
- const granted = await this.requestPermission();
- this.enabled = granted;
- } else {
- this.enabled = false;
- }
- SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.NOTIFICATIONS, this.enabled);
- return this.enabled;
- }
- };
- // ==================== TOAST SYSTEM ====================
- const ToastSystem = {
- container: null,
- init() {
- this.container = document.createElement('div');
- this.container.id = 'onchat-toasts';
- this.container.innerHTML = '';
- document.body.appendChild(this.container);
- },
- show(message, type = 'info', duration = CONFIG.UI.TOAST_DURATION) {
- const toast = document.createElement('div');
- toast.className = `onchat-toast onchat-toast-${type}`;
- const icons = {
- info: '💬',
- success: '✓',
- warning: '⚠',
- error: '✕',
- mention: '📢'
- };
- toast.innerHTML = `
- <span class="onchat-toast-icon">${icons[type] || icons.info}</span>
- <span class="onchat-toast-message">${SecurityUtils.escapeHTML(message)}</span>
- `;
- this.container.appendChild(toast);
- // Trigger animation
- requestAnimationFrame(() => {
- toast.classList.add('onchat-toast-visible');
- });
- setTimeout(() => {
- toast.classList.remove('onchat-toast-visible');
- toast.classList.add('onchat-toast-hiding');
- setTimeout(() => toast.remove(), 300);
- }, duration);
- },
- info(msg) { this.show(msg, 'info'); },
- success(msg) { this.show(msg, 'success'); },
- warning(msg) { this.show(msg, 'warning'); },
- error(msg) { this.show(msg, 'error'); },
- mention(msg) { this.show(msg, 'mention', 5000); }
- };
- // ==================== MENTION SYSTEM ====================
- const MentionSystem = {
- keywords: [],
- username: '',
- init() {
- this.username = this.detectUsername();
- const saved = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.MENTIONS_KEYWORDS, '');
- this.keywords = saved ? saved.split(',').map(k => k.trim().toLowerCase()).filter(Boolean) : [];
- // Always include username
- if (this.username && !this.keywords.includes(this.username.toLowerCase())) {
- this.keywords.unshift(this.username.toLowerCase());
- }
- },
- detectUsername() {
- // Try to find username from page
- const selectors = [
- '.user-menu__name',
- '.header__user__name',
- '[data-username]',
- '.profile-link'
- ];
- for (const sel of selectors) {
- const el = document.querySelector(sel);
- if (el) {
- const name = el.dataset.username || el.textContent.trim();
- if (name) {
- SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.USERNAME, name);
- return name;
- }
- }
- }
- return SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.USERNAME, '');
- },
- setKeywords(keywords) {
- this.keywords = keywords.split(',').map(k => k.trim().toLowerCase()).filter(Boolean);
- if (this.username && !this.keywords.includes(this.username.toLowerCase())) {
- this.keywords.unshift(this.username.toLowerCase());
- }
- SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.MENTIONS_KEYWORDS, this.keywords.join(','));
- },
- checkMessage(messageElement) {
- if (!this.keywords.length) return false;
- const content = messageElement.querySelector(CONFIG.SELECTORS.MESSAGE_CONTENT);
- if (!content) return false;
- const text = content.textContent.toLowerCase();
- for (const keyword of this.keywords) {
- if (text.includes(keyword)) {
- messageElement.classList.add(CONFIG.CLASSES.MENTION);
- return true;
- }
- }
- return false;
- }
- };
- // ==================== BLOCKED USERS SYSTEM ====================
- const BlockSystem = {
- blocked: new Set(),
- init() {
- const saved = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.BLOCKED_USERS, '');
- this.blocked = new Set(saved ? saved.split(',').filter(Boolean) : []);
- },
- isBlocked(username) {
- return this.blocked.has(username.toLowerCase());
- },
- toggle(username) {
- const lower = username.toLowerCase();
- if (this.blocked.has(lower)) {
- this.blocked.delete(lower);
- } else {
- this.blocked.add(lower);
- }
- this.save();
- return this.blocked.has(lower);
- },
- save() {
- SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.BLOCKED_USERS, Array.from(this.blocked).join(','));
- },
- filterMessages(container) {
- if (!this.blocked.size) return;
- container.querySelectorAll(CONFIG.SELECTORS.MESSAGE).forEach(msg => {
- const authorEl = msg.querySelector(CONFIG.SELECTORS.MESSAGE_AUTHOR);
- if (authorEl) {
- const author = authorEl.textContent.trim().toLowerCase();
- if (this.blocked.has(author)) {
- msg.classList.add('onchat-blocked');
- }
- }
- });
- }
- };
- // ==================== FAVORITES SYSTEM ====================
- const FavoritesSystem = {
- favorites: new Set(),
- init() {
- const saved = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.FAVORITE_TOPICS, '');
- this.favorites = new Set(saved ? saved.split(',').filter(Boolean) : []);
- },
- isFavorite(topicId) {
- return this.favorites.has(String(topicId));
- },
- toggle(topicId) {
- const id = String(topicId);
- if (this.favorites.has(id)) {
- this.favorites.delete(id);
- } else {
- this.favorites.add(id);
- }
- this.save();
- return this.favorites.has(id);
- },
- save() {
- SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.FAVORITE_TOPICS, Array.from(this.favorites).join(','));
- },
- getTopicId() {
- const match = window.location.pathname.match(/\/topic\/(\d+)/);
- return match ? match[1] : null;
- }
- };
- // ==================== KEYBOARD SHORTCUTS ====================
- const KeyboardSystem = {
- shortcuts: new Map(),
- init() {
- // Define shortcuts
- this.register('r', () => this.focusReply(), 'Répondre');
- this.register('j', () => this.nextMessage(), 'Message suivant');
- this.register('k', () => this.prevMessage(), 'Message précédent');
- this.register('g', () => this.scrollToBottom(), 'Aller en bas');
- this.register('t', () => this.scrollToTop(), 'Aller en haut');
- this.register('l', () => StateManager.toggle('liveMode'), 'Toggle Live');
- this.register('/', () => this.openSearch(), 'Rechercher');
- this.register('?', () => this.showHelp(), 'Aide raccourcis');
- this.register('Escape', () => this.closeModals(), 'Fermer');
- document.addEventListener('keydown', (e) => this.handleKeydown(e));
- },
- register(key, callback, description) {
- this.shortcuts.set(key.toLowerCase(), { callback, description, key });
- },
- handleKeydown(e) {
- // Ignore when typing
- if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) {
- if (e.key === 'Escape') {
- e.target.blur();
- }
- return;
- }
- const shortcut = this.shortcuts.get(e.key.toLowerCase());
- if (shortcut) {
- e.preventDefault();
- shortcut.callback();
- }
- },
- focusReply() {
- const textarea = document.querySelector(CONFIG.SELECTORS.TEXTAREA);
- if (textarea) {
- textarea.scrollIntoView({ behavior: 'smooth', block: 'center' });
- setTimeout(() => textarea.focus(), 300);
- }
- },
- currentMessageIndex: -1,
- nextMessage() {
- const messages = document.querySelectorAll(CONFIG.SELECTORS.MESSAGE);
- if (!messages.length) return;
- this.currentMessageIndex = Math.min(this.currentMessageIndex + 1, messages.length - 1);
- this.highlightMessage(messages[this.currentMessageIndex]);
- },
- prevMessage() {
- const messages = document.querySelectorAll(CONFIG.SELECTORS.MESSAGE);
- if (!messages.length) return;
- this.currentMessageIndex = Math.max(this.currentMessageIndex - 1, 0);
- this.highlightMessage(messages[this.currentMessageIndex]);
- },
- highlightMessage(msg) {
- document.querySelectorAll('.onchat-keyboard-focus').forEach(el => {
- el.classList.remove('onchat-keyboard-focus');
- });
- msg.classList.add('onchat-keyboard-focus');
- msg.scrollIntoView({ behavior: 'smooth', block: 'center' });
- },
- scrollToBottom() {
- window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
- },
- scrollToTop() {
- window.scrollTo({ top: 0, behavior: 'smooth' });
- },
- openSearch() {
- ToastSystem.info('Recherche: Ctrl+F');
- },
- showHelp() {
- let html = '<div class="onchat-shortcuts-help">';
- html += '<h3>⌨️ Raccourcis clavier</h3>';
- html += '<div class="onchat-shortcuts-grid">';
- this.shortcuts.forEach(({ key, description }) => {
- html += `<div class="onchat-shortcut-item">
- <kbd>${key}</kbd>
- <span>${description}</span>
- </div>`;
- });
- html += '</div></div>';
- // Show in modal
- UIManager.showModal('Raccourcis', html);
- },
- closeModals() {
- UIManager.closeModal();
- UIManager.closePanel();
- }
- };
- // ==================== SECURITY UTILITIES ====================
- const SecurityUtils = {
- init() {
- try {
- if (!Object.isFrozen(Object.prototype)) {
- Object.freeze(Object.prototype);
- }
- } catch (e) {
- Logger.warn('Could not freeze Object.prototype');
- }
- },
- validateNumericId(id) {
- const numId = typeof id === 'string' ? parseInt(id, 10) : id;
- if (isNaN(numId) || numId < 1 || numId > Number.MAX_SAFE_INTEGER) return null;
- return numId;
- },
- validateURL(url) {
- if (!url || typeof url !== 'string') return null;
- try {
- const parsed = new URL(url, window.location.origin);
- if (parsed.origin !== window.location.origin) return null;
- if (!['http:', 'https:'].includes(parsed.protocol)) return null;
- return parsed.href;
- } catch {
- return null;
- }
- },
- escapeHTML(str) {
- if (!str) return '';
- const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
- return String(str).replace(/[&<>"']/g, s => map[s]);
- },
- safeGMSetValue(key, value) {
- if (['__proto__', 'constructor', 'prototype'].includes(key)) return;
- if (!/^[a-zA-Z0-9_]+$/.test(key)) return;
- GM_setValue(key, value);
- },
- safeGMGetValue(key, defaultValue) {
- if (['__proto__', 'constructor', 'prototype'].includes(key)) return defaultValue;
- return GM_getValue(key, defaultValue);
- },
- createRateLimiter(max, windowMs) {
- const attempts = new Map();
- return function(key) {
- const now = Date.now();
- const recent = (attempts.get(key) || []).filter(t => now - t < windowMs);
- if (recent.length >= max) return false;
- recent.push(now);
- attempts.set(key, recent);
- return true;
- };
- }
- };
- // ==================== LOGGER ====================
- const Logger = {
- prefix: `%c[OnChat ${CONFIG.VERSION}]`,
- style: 'color: #6366f1; font-weight: bold',
- info(...args) { console.info(this.prefix, this.style, ...args); },
- warn(...args) { console.warn(this.prefix, this.style, ...args); },
- error(...args) { console.error(this.prefix, this.style, ...args); },
- debug(...args) {
- if (CONFIG.DEBUG) console.log(this.prefix, this.style, '[DEBUG]', ...args);
- }
- };
- // ==================== STATE MANAGER ====================
- const StateManager = {
- state: {
- liveMode: true,
- refreshInterval: CONFIG.POLLING.DEFAULT,
- notifications: false,
- sounds: true,
- compactMode: false,
- currentPage: 1,
- lastMessageId: null,
- unreadCount: 0,
- isPolling: false,
- lastActivity: Date.now()
- },
- listeners: new Set(),
- init() {
- // Load from storage
- this.state.liveMode = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.LIVE_MODE, true);
- this.state.refreshInterval = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.REFRESH_INTERVAL, CONFIG.POLLING.DEFAULT);
- this.state.compactMode = SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.COMPACT_MODE, false);
- // Extract current page
- const match = window.location.pathname.match(/\/(\d+)$/);
- this.state.currentPage = match ? parseInt(match[1], 10) : 1;
- },
- get(key) {
- return this.state[key];
- },
- set(key, value) {
- const old = this.state[key];
- if (old === value) return;
- this.state[key] = value;
- this.notify(key, value, old);
- // Persist certain keys
- const persistKeys = {
- liveMode: CONFIG.STORAGE_KEYS.LIVE_MODE,
- refreshInterval: CONFIG.STORAGE_KEYS.REFRESH_INTERVAL,
- compactMode: CONFIG.STORAGE_KEYS.COMPACT_MODE
- };
- if (persistKeys[key]) {
- SecurityUtils.safeGMSetValue(persistKeys[key], value);
- }
- },
- toggle(key) {
- this.set(key, !this.get(key));
- return this.get(key);
- },
- subscribe(callback) {
- this.listeners.add(callback);
- return () => this.listeners.delete(callback);
- },
- notify(key, newVal, oldVal) {
- this.listeners.forEach(cb => {
- try { cb(key, newVal, oldVal); }
- catch (e) { Logger.error('State listener error:', e); }
- });
- },
- updateActivity() {
- this.state.lastActivity = Date.now();
- }
- };
- // ==================== POLLER ====================
- const Poller = {
- timeoutId: null,
- interval: CONFIG.POLLING.DEFAULT,
- missCount: 0,
- start() {
- this.stop();
- this.interval = StateManager.get('refreshInterval');
- Logger.info(`Polling started @ ${this.interval}ms`);
- this.poll();
- },
- stop() {
- if (this.timeoutId) {
- clearTimeout(this.timeoutId);
- this.timeoutId = null;
- }
- },
- async poll() {
- if (!StateManager.get('liveMode')) {
- this.stop();
- return;
- }
- try {
- const hasNew = await MessageManager.fetchNew();
- if (hasNew) {
- this.interval = StateManager.get('refreshInterval');
- this.missCount = 0;
- } else {
- this.missCount++;
- // Slow down if no activity
- if (this.missCount > 10) {
- this.interval = Math.min(
- this.interval * CONFIG.POLLING.BACKOFF_MULTIPLIER,
- CONFIG.POLLING.MAX_INTERVAL
- );
- }
- }
- } catch (e) {
- Logger.error('Poll error:', e);
- this.interval = Math.min(this.interval * 2, CONFIG.POLLING.MAX_INTERVAL);
- }
- if (StateManager.get('liveMode')) {
- this.timeoutId = setTimeout(() => this.poll(), this.interval);
- }
- },
- setInterval(ms) {
- this.interval = ms;
- this.missCount = 0;
- if (StateManager.get('liveMode')) {
- this.start();
- }
- }
- };
- // ==================== MESSAGE MANAGER ====================
- const MessageManager = {
- processed: new Set(),
- lastId: null,
- reactionLimiter: null,
- messageLimiter: null,
- init() {
- this.reactionLimiter = SecurityUtils.createRateLimiter(
- CONFIG.SECURITY.MAX_REACTIONS_PER_WINDOW,
- CONFIG.SECURITY.REACTION_WINDOW_MS
- );
- this.messageLimiter = SecurityUtils.createRateLimiter(
- CONFIG.SECURITY.MAX_MESSAGES_PER_WINDOW,
- CONFIG.SECURITY.MESSAGE_WINDOW_MS
- );
- // Initialize with current messages
- const container = document.querySelector(CONFIG.SELECTORS.MESSAGES_CONTAINER);
- if (container) {
- container.querySelectorAll(CONFIG.SELECTORS.MESSAGE).forEach(msg => {
- this.processed.add(msg.dataset.id);
- if (!this.lastId || parseInt(msg.dataset.id) > parseInt(this.lastId)) {
- this.lastId = msg.dataset.id;
- }
- });
- }
- },
- async fetchNew() {
- const container = document.querySelector(CONFIG.SELECTORS.MESSAGES_CONTAINER);
- if (!container) return false;
- const controller = new AbortController();
- const timeout = setTimeout(() => controller.abort(), CONFIG.SECURITY.FETCH_TIMEOUT);
- try {
- const response = await fetch(window.location.href, {
- signal: controller.signal,
- credentials: 'same-origin'
- });
- clearTimeout(timeout);
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
- const html = await response.text();
- const doc = new DOMParser().parseFromString(html, 'text/html');
- const newContainer = doc.querySelector(CONFIG.SELECTORS.MESSAGES_CONTAINER);
- if (!newContainer) return false;
- const newMessages = [];
- newContainer.querySelectorAll(CONFIG.SELECTORS.MESSAGE).forEach(msg => {
- const id = msg.dataset.id;
- if (!this.processed.has(id)) {
- newMessages.push(msg);
- }
- });
- if (newMessages.length > 0) {
- this.insertMessages(container, newMessages);
- return true;
- }
- return false;
- } catch (e) {
- clearTimeout(timeout);
- if (e.name !== 'AbortError') {
- Logger.error('Fetch error:', e);
- }
- throw e;
- }
- },
- insertMessages(container, messages) {
- const fragment = document.createDocumentFragment();
- messages.forEach(msg => {
- const clone = msg.cloneNode(true);
- const id = clone.dataset.id;
- clone.classList.add(CONFIG.CLASSES.NEW_MESSAGE);
- // Check for mentions
- const isMention = MentionSystem.checkMessage(clone);
- // Add to fragment
- fragment.appendChild(clone);
- this.processed.add(id);
- if (!this.lastId || parseInt(id) > parseInt(this.lastId)) {
- this.lastId = id;
- }
- // Notifications
- if (isMention) {
- const author = clone.querySelector(CONFIG.SELECTORS.MESSAGE_AUTHOR)?.textContent.trim() || 'Quelqu\'un';
- SoundSystem.play('mention');
- NotificationSystem.send(`${author} vous a mentionné`, {
- body: clone.querySelector(CONFIG.SELECTORS.MESSAGE_CONTENT)?.textContent.slice(0, 100) || ''
- });
- ToastSystem.mention(`Mention de ${author}`);
- } else {
- SoundSystem.play('message');
- }
- });
- container.appendChild(fragment);
- // Apply block filter
- BlockSystem.filterMessages(container);
- // Update unread count
- StateManager.set('unreadCount', StateManager.get('unreadCount') + messages.length);
- // Auto-scroll if near bottom
- const scrolledFromBottom = document.documentElement.scrollHeight -
- (window.pageYOffset + document.documentElement.clientHeight);
- if (scrolledFromBottom < 200) {
- window.scrollTo({
- top: document.documentElement.scrollHeight,
- behavior: 'smooth'
- });
- }
- Logger.info(`+${messages.length} new messages`);
- UIManager.updateStatus();
- }
- };
- // ==================== UI MANAGER ====================
- const UIManager = {
- elements: {},
- panelOpen: false,
- modalOpen: false,
- init() {
- this.injectStyles();
- this.createUI();
- this.bindEvents();
- this.updateStatus();
- // Apply compact mode
- if (StateManager.get('compactMode')) {
- document.body.classList.add('onchat-compact');
- }
- },
- injectStyles() {
- GM_addStyle(`
- /* ═══════════════════════════════════════════════════════════════
- ON'CHAT PRO ULTIMATE - STYLES
- ═══════════════════════════════════════════════════════════════ */
- :root {
- --onchat-font: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- --onchat-mono: 'SF Mono', 'Fira Code', monospace;
- --onchat-radius: 12px;
- --onchat-radius-sm: 8px;
- --onchat-transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- }
- /* ═══ ANIMATIONS ═══ */
- @keyframes onchat-slideIn {
- from { opacity: 0; transform: translateY(20px) scale(0.95); }
- to { opacity: 1; transform: translateY(0) scale(1); }
- }
- @keyframes onchat-fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
- }
- @keyframes onchat-pulse {
- 0%, 100% { transform: scale(1); }
- 50% { transform: scale(1.05); }
- }
- @keyframes onchat-glow {
- 0%, 100% { box-shadow: 0 0 5px var(--onchat-accent-glow); }
- 50% { box-shadow: 0 0 20px var(--onchat-accent-glow), 0 0 30px var(--onchat-accent-glow); }
- }
- @keyframes onchat-shake {
- 0%, 100% { transform: translateX(0); }
- 25% { transform: translateX(-5px); }
- 75% { transform: translateX(5px); }
- }
- /* ═══ NEW MESSAGE ANIMATION ═══ */
- .${CONFIG.CLASSES.NEW_MESSAGE} {
- animation: onchat-slideIn 0.4s ease-out;
- }
- .${CONFIG.CLASSES.HIGHLIGHT}::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 4px;
- background: linear-gradient(180deg, var(--onchat-accent), var(--onchat-mention));
- border-radius: 4px;
- animation: onchat-glow 2s ease-in-out infinite;
- }
- .${CONFIG.CLASSES.MENTION} {
- background: linear-gradient(90deg,
- rgba(244, 114, 182, 0.1) 0%,
- transparent 100%) !important;
- border-left: 3px solid var(--onchat-mention) !important;
- }
- .onchat-blocked {
- opacity: 0.3;
- filter: blur(2px);
- transition: all var(--onchat-transition);
- }
- .onchat-blocked:hover {
- opacity: 1;
- filter: none;
- }
- .onchat-keyboard-focus {
- outline: 2px solid var(--onchat-accent) !important;
- outline-offset: 2px;
- }
- /* ═══ CONTROL BAR ═══ */
- #onchat-bar {
- position: fixed;
- bottom: 20px;
- left: 50%;
- transform: translateX(-50%);
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px 12px;
- background: var(--onchat-glass);
- backdrop-filter: blur(20px) saturate(180%);
- -webkit-backdrop-filter: blur(20px) saturate(180%);
- border: 1px solid var(--onchat-border);
- border-radius: 50px;
- box-shadow: var(--onchat-shadow);
- z-index: 10000;
- transition: all var(--onchat-transition);
- }
- #onchat-bar:hover {
- border-color: var(--onchat-border-light);
- }
- #onchat-bar.${CONFIG.CLASSES.MINIMIZED} {
- padding: 6px;
- gap: 0;
- }
- #onchat-bar.${CONFIG.CLASSES.MINIMIZED} .onchat-bar-expandable {
- width: 0;
- opacity: 0;
- padding: 0;
- margin: 0;
- overflow: hidden;
- }
- .onchat-bar-btn {
- width: 36px;
- height: 36px;
- border: none;
- border-radius: 50%;
- background: transparent;
- color: var(--onchat-text-secondary);
- font-size: 16px;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all var(--onchat-transition);
- position: relative;
- }
- .onchat-bar-btn:hover {
- background: var(--onchat-bg-hover);
- color: var(--onchat-text);
- transform: scale(1.1);
- }
- .onchat-bar-btn.${CONFIG.CLASSES.ACTIVE} {
- background: var(--onchat-accent);
- color: white;
- }
- .onchat-bar-btn.${CONFIG.CLASSES.ACTIVE}:hover {
- background: var(--onchat-accent-hover);
- }
- .onchat-bar-btn .onchat-badge {
- position: absolute;
- top: -4px;
- right: -4px;
- min-width: 18px;
- height: 18px;
- padding: 0 5px;
- background: var(--onchat-error);
- color: white;
- font-size: 10px;
- font-weight: 700;
- border-radius: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- animation: onchat-pulse 1s ease-in-out infinite;
- }
- .onchat-bar-divider {
- width: 1px;
- height: 24px;
- background: var(--onchat-border);
- margin: 0 4px;
- }
- .onchat-bar-status {
- font-size: 12px;
- color: var(--onchat-text-muted);
- padding: 0 8px;
- white-space: nowrap;
- transition: all var(--onchat-transition);
- }
- .onchat-bar-status.live {
- color: var(--onchat-success);
- }
- .onchat-bar-status.live::before {
- content: '';
- display: inline-block;
- width: 6px;
- height: 6px;
- background: var(--onchat-success);
- border-radius: 50%;
- margin-right: 6px;
- animation: onchat-pulse 1.5s ease-in-out infinite;
- }
- .onchat-bar-status.paused {
- color: var(--onchat-warning);
- }
- /* ═══ SETTINGS PANEL ═══ */
- #onchat-panel {
- position: fixed;
- bottom: 80px;
- left: 50%;
- transform: translateX(-50%) scale(0.95);
- width: ${CONFIG.UI.PANEL_WIDTH}px;
- max-height: 70vh;
- background: var(--onchat-bg);
- border: 1px solid var(--onchat-border);
- border-radius: var(--onchat-radius);
- box-shadow: var(--onchat-shadow);
- z-index: 10001;
- opacity: 0;
- visibility: hidden;
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
- overflow: hidden;
- }
- #onchat-panel.${CONFIG.CLASSES.VISIBLE} {
- opacity: 1;
- visibility: visible;
- transform: translateX(-50%) scale(1);
- }
- .onchat-panel-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 16px 20px;
- border-bottom: 1px solid var(--onchat-border);
- background: var(--onchat-bg-secondary);
- }
- .onchat-panel-title {
- font-size: 14px;
- font-weight: 600;
- color: var(--onchat-text);
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .onchat-panel-version {
- font-size: 10px;
- color: var(--onchat-text-muted);
- background: var(--onchat-bg-tertiary);
- padding: 2px 6px;
- border-radius: 4px;
- }
- .onchat-panel-close {
- width: 28px;
- height: 28px;
- border: none;
- border-radius: 6px;
- background: transparent;
- color: var(--onchat-text-muted);
- font-size: 18px;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all var(--onchat-transition);
- }
- .onchat-panel-close:hover {
- background: var(--onchat-bg-hover);
- color: var(--onchat-text);
- }
- .onchat-panel-content {
- padding: 12px 0;
- max-height: 50vh;
- overflow-y: auto;
- }
- .onchat-panel-section {
- padding: 8px 20px;
- }
- .onchat-panel-section-title {
- font-size: 11px;
- font-weight: 600;
- color: var(--onchat-text-muted);
- text-transform: uppercase;
- letter-spacing: 0.5px;
- margin-bottom: 12px;
- }
- .onchat-setting {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 0;
- }
- .onchat-setting-info {
- flex: 1;
- }
- .onchat-setting-label {
- font-size: 13px;
- color: var(--onchat-text);
- font-weight: 500;
- }
- .onchat-setting-desc {
- font-size: 11px;
- color: var(--onchat-text-muted);
- margin-top: 2px;
- }
- /* Toggle Switch */
- .onchat-toggle {
- position: relative;
- width: 44px;
- height: 24px;
- background: var(--onchat-bg-tertiary);
- border-radius: 12px;
- cursor: pointer;
- transition: all var(--onchat-transition);
- }
- .onchat-toggle.${CONFIG.CLASSES.ACTIVE} {
- background: var(--onchat-accent);
- }
- .onchat-toggle::after {
- content: '';
- position: absolute;
- top: 3px;
- left: 3px;
- width: 18px;
- height: 18px;
- background: white;
- border-radius: 50%;
- transition: all var(--onchat-transition);
- box-shadow: 0 2px 4px rgba(0,0,0,0.2);
- }
- .onchat-toggle.${CONFIG.CLASSES.ACTIVE}::after {
- transform: translateX(20px);
- }
- /* Select */
- .onchat-select {
- background: var(--onchat-bg-tertiary);
- border: 1px solid var(--onchat-border);
- border-radius: var(--onchat-radius-sm);
- color: var(--onchat-text);
- padding: 6px 10px;
- font-size: 12px;
- cursor: pointer;
- outline: none;
- transition: all var(--onchat-transition);
- }
- .onchat-select:hover {
- border-color: var(--onchat-border-light);
- }
- .onchat-select:focus {
- border-color: var(--onchat-accent);
- }
- /* Input */
- .onchat-input {
- width: 100%;
- background: var(--onchat-bg-tertiary);
- border: 1px solid var(--onchat-border);
- border-radius: var(--onchat-radius-sm);
- color: var(--onchat-text);
- padding: 10px 12px;
- font-size: 13px;
- outline: none;
- transition: all var(--onchat-transition);
- }
- .onchat-input:focus {
- border-color: var(--onchat-accent);
- box-shadow: 0 0 0 3px var(--onchat-accent-glow);
- }
- .onchat-input::placeholder {
- color: var(--onchat-text-muted);
- }
- /* ═══ TOASTS ═══ */
- #onchat-toasts {
- position: fixed;
- top: 20px;
- right: 20px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- z-index: 10002;
- pointer-events: none;
- }
- .onchat-toast {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 12px 16px;
- background: var(--onchat-bg);
- border: 1px solid var(--onchat-border);
- border-radius: var(--onchat-radius-sm);
- box-shadow: var(--onchat-shadow);
- transform: translateX(120%);
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- pointer-events: auto;
- }
- .onchat-toast-visible {
- transform: translateX(0);
- }
- .onchat-toast-hiding {
- transform: translateX(120%);
- opacity: 0;
- }
- .onchat-toast-icon {
- font-size: 16px;
- }
- .onchat-toast-message {
- font-size: 13px;
- color: var(--onchat-text);
- }
- .onchat-toast-success { border-left: 3px solid var(--onchat-success); }
- .onchat-toast-warning { border-left: 3px solid var(--onchat-warning); }
- .onchat-toast-error { border-left: 3px solid var(--onchat-error); }
- .onchat-toast-mention {
- border-left: 3px solid var(--onchat-mention);
- animation: onchat-shake 0.5s ease-in-out;
- }
- /* ═══ MODAL ═══ */
- #onchat-modal-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.6);
- backdrop-filter: blur(4px);
- z-index: 10003;
- opacity: 0;
- visibility: hidden;
- transition: all 0.25s ease;
- }
- #onchat-modal-overlay.${CONFIG.CLASSES.VISIBLE} {
- opacity: 1;
- visibility: visible;
- }
- #onchat-modal {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%) scale(0.9);
- width: 90%;
- max-width: 500px;
- max-height: 80vh;
- background: var(--onchat-bg);
- border: 1px solid var(--onchat-border);
- border-radius: var(--onchat-radius);
- box-shadow: var(--onchat-shadow);
- z-index: 10004;
- opacity: 0;
- visibility: hidden;
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
- overflow: hidden;
- }
- #onchat-modal.${CONFIG.CLASSES.VISIBLE} {
- opacity: 1;
- visibility: visible;
- transform: translate(-50%, -50%) scale(1);
- }
- .onchat-modal-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 16px 20px;
- border-bottom: 1px solid var(--onchat-border);
- }
- .onchat-modal-title {
- font-size: 16px;
- font-weight: 600;
- color: var(--onchat-text);
- }
- .onchat-modal-content {
- padding: 20px;
- overflow-y: auto;
- max-height: 60vh;
- }
- /* Shortcuts Help */
- .onchat-shortcuts-help h3 {
- font-size: 14px;
- color: var(--onchat-text);
- margin-bottom: 16px;
- }
- .onchat-shortcuts-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 10px;
- }
- .onchat-shortcut-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px;
- background: var(--onchat-bg-secondary);
- border-radius: var(--onchat-radius-sm);
- }
- .onchat-shortcut-item kbd {
- min-width: 28px;
- height: 28px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--onchat-bg-tertiary);
- border: 1px solid var(--onchat-border);
- border-radius: 6px;
- font-family: var(--onchat-mono);
- font-size: 12px;
- color: var(--onchat-text);
- }
- .onchat-shortcut-item span {
- font-size: 12px;
- color: var(--onchat-text-secondary);
- }
- /* ═══ FLOATING BUTTONS ═══ */
- .onchat-fab {
- position: fixed;
- width: 48px;
- height: 48px;
- border: none;
- border-radius: 50%;
- background: var(--onchat-bg);
- border: 1px solid var(--onchat-border);
- color: var(--onchat-text-secondary);
- font-size: 20px;
- cursor: pointer;
- box-shadow: var(--onchat-shadow);
- z-index: 9999;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all var(--onchat-transition);
- opacity: 0;
- visibility: hidden;
- transform: scale(0.8);
- }
- .onchat-fab.${CONFIG.CLASSES.VISIBLE} {
- opacity: 1;
- visibility: visible;
- transform: scale(1);
- }
- .onchat-fab:hover {
- background: var(--onchat-bg-secondary);
- border-color: var(--onchat-accent);
- color: var(--onchat-accent);
- transform: scale(1.1);
- }
- #onchat-scroll-btn {
- bottom: 90px;
- right: 20px;
- }
- #onchat-compose-btn {
- bottom: 90px;
- right: 80px;
- }
- /* ═══ COMPACT MODE ═══ */
- .onchat-compact .message {
- padding: 8px 12px !important;
- }
- .onchat-compact .message__avatar {
- width: 32px !important;
- height: 32px !important;
- }
- .onchat-compact .message__content {
- font-size: 13px !important;
- }
- /* ═══ RESPONSIVE ═══ */
- @media (max-width: 768px) {
- #onchat-bar {
- bottom: 10px;
- padding: 6px 10px;
- gap: 4px;
- }
- .onchat-bar-btn {
- width: 32px;
- height: 32px;
- font-size: 14px;
- }
- .onchat-bar-status {
- display: none;
- }
- #onchat-panel {
- width: 95vw;
- bottom: 70px;
- }
- .onchat-fab {
- width: 44px;
- height: 44px;
- font-size: 18px;
- }
- #onchat-scroll-btn {
- bottom: 80px;
- right: 15px;
- }
- #onchat-compose-btn {
- bottom: 80px;
- right: 70px;
- }
- }
- /* ═══ SCROLLBAR ═══ */
- .onchat-panel-content::-webkit-scrollbar,
- .onchat-modal-content::-webkit-scrollbar {
- width: 6px;
- }
- .onchat-panel-content::-webkit-scrollbar-track,
- .onchat-modal-content::-webkit-scrollbar-track {
- background: transparent;
- }
- .onchat-panel-content::-webkit-scrollbar-thumb,
- .onchat-modal-content::-webkit-scrollbar-thumb {
- background: var(--onchat-border);
- border-radius: 3px;
- }
- .onchat-panel-content::-webkit-scrollbar-thumb:hover,
- .onchat-modal-content::-webkit-scrollbar-thumb:hover {
- background: var(--onchat-border-light);
- }
- `);
- },
- createUI() {
- // Control Bar
- const bar = document.createElement('div');
- bar.id = 'onchat-bar';
- bar.innerHTML = `
- <button class="onchat-bar-btn" data-action="toggle-live" title="Mode Live (L)">
- <span>⚡</span>
- </button>
- <button class="onchat-bar-btn" data-action="toggle-sound" title="Sons">
- <span>🔔</span>
- </button>
- <button class="onchat-bar-btn" data-action="toggle-notifications" title="Notifications">
- <span>📢</span>
- </button>
- <div class="onchat-bar-divider onchat-bar-expandable"></div>
- <span class="onchat-bar-status onchat-bar-expandable">Initializing...</span>
- <div class="onchat-bar-divider onchat-bar-expandable"></div>
- <button class="onchat-bar-btn onchat-bar-expandable" data-action="toggle-favorite" title="Favori">
- <span>☆</span>
- </button>
- <button class="onchat-bar-btn" data-action="toggle-settings" title="Paramètres">
- <span>⚙</span>
- </button>
- <button class="onchat-bar-btn" data-action="toggle-minimize" title="Réduire">
- <span>◀</span>
- </button>
- `;
- document.body.appendChild(bar);
- this.elements.bar = bar;
- // Settings Panel
- const panel = document.createElement('div');
- panel.id = 'onchat-panel';
- panel.innerHTML = `
- <div class="onchat-panel-header">
- <div class="onchat-panel-title">
- <span>⚡ OnChat Pro</span>
- <span class="onchat-panel-version">v${CONFIG.VERSION}</span>
- </div>
- <button class="onchat-panel-close" data-action="close-panel">×</button>
- </div>
- <div class="onchat-panel-content">
- <div class="onchat-panel-section">
- <div class="onchat-panel-section-title">Live Chat</div>
- <div class="onchat-setting">
- <div class="onchat-setting-info">
- <div class="onchat-setting-label">Mode Live</div>
- <div class="onchat-setting-desc">Actualisation automatique des messages</div>
- </div>
- <div class="onchat-toggle ${StateManager.get('liveMode') ? CONFIG.CLASSES.ACTIVE : ''}" data-setting="liveMode"></div>
- </div>
- <div class="onchat-setting">
- <div class="onchat-setting-info">
- <div class="onchat-setting-label">Intervalle</div>
- </div>
- <select class="onchat-select" data-setting="refreshInterval">
- <option value="1000" ${StateManager.get('refreshInterval') === 1000 ? 'selected' : ''}>1s (Ultra)</option>
- <option value="2000" ${StateManager.get('refreshInterval') === 2000 ? 'selected' : ''}>2s (Rapide)</option>
- <option value="3000" ${StateManager.get('refreshInterval') === 3000 ? 'selected' : ''}>3s (Normal)</option>
- <option value="5000" ${StateManager.get('refreshInterval') === 5000 ? 'selected' : ''}>5s (Éco)</option>
- <option value="10000" ${StateManager.get('refreshInterval') === 10000 ? 'selected' : ''}>10s (Lent)</option>
- </select>
- </div>
- </div>
- <div class="onchat-panel-section">
- <div class="onchat-panel-section-title">Notifications</div>
- <div class="onchat-setting">
- <div class="onchat-setting-info">
- <div class="onchat-setting-label">Sons</div>
- <div class="onchat-setting-desc">Bip pour nouveaux messages</div>
- </div>
- <div class="onchat-toggle ${SoundSystem.enabled ? CONFIG.CLASSES.ACTIVE : ''}" data-setting="sounds"></div>
- </div>
- <div class="onchat-setting">
- <div class="onchat-setting-info">
- <div class="onchat-setting-label">Notifications Desktop</div>
- <div class="onchat-setting-desc">Alertes même onglet inactif</div>
- </div>
- <div class="onchat-toggle ${NotificationSystem.enabled ? CONFIG.CLASSES.ACTIVE : ''}" data-setting="notifications"></div>
- </div>
- </div>
- <div class="onchat-panel-section">
- <div class="onchat-panel-section-title">Mentions</div>
- <div class="onchat-setting" style="flex-direction: column; align-items: stretch; gap: 8px;">
- <div class="onchat-setting-info">
- <div class="onchat-setting-label">Mots-clés de mention</div>
- <div class="onchat-setting-desc">Séparés par des virgules (votre pseudo est inclus automatiquement)</div>
- </div>
- <input type="text" class="onchat-input" data-setting="mentions"
- placeholder="mot1, mot2, mot3..."
- value="${SecurityUtils.escapeHTML(MentionSystem.keywords.slice(1).join(', '))}">
- </div>
- </div>
- <div class="onchat-panel-section">
- <div class="onchat-panel-section-title">Apparence</div>
- <div class="onchat-setting">
- <div class="onchat-setting-info">
- <div class="onchat-setting-label">Mode Compact</div>
- <div class="onchat-setting-desc">Messages plus petits</div>
- </div>
- <div class="onchat-toggle ${StateManager.get('compactMode') ? CONFIG.CLASSES.ACTIVE : ''}" data-setting="compactMode"></div>
- </div>
- <div class="onchat-setting">
- <div class="onchat-setting-info">
- <div class="onchat-setting-label">Thème</div>
- </div>
- <select class="onchat-select" data-setting="theme">
- <option value="dark" ${ThemeSystem.current === 'dark' ? 'selected' : ''}>🌙 Sombre</option>
- <option value="light" ${ThemeSystem.current === 'light' ? 'selected' : ''}>☀️ Clair</option>
- </select>
- </div>
- </div>
- <div class="onchat-panel-section" style="border-top: 1px solid var(--onchat-border); padding-top: 16px; margin-top: 8px;">
- <div style="display: flex; justify-content: space-between; align-items: center;">
- <span style="font-size: 11px; color: var(--onchat-text-muted);">
- Codename: ${CONFIG.CODENAME}
- </span>
- <span style="font-size: 11px; color: var(--onchat-text-muted);">
- Raccourcis: <kbd style="background: var(--onchat-bg-tertiary); padding: 2px 6px; border-radius: 4px; font-size: 10px;">?</kbd>
- </span>
- </div>
- </div>
- </div>
- `;
- document.body.appendChild(panel);
- this.elements.panel = panel;
- // Modal
- const modalOverlay = document.createElement('div');
- modalOverlay.id = 'onchat-modal-overlay';
- document.body.appendChild(modalOverlay);
- this.elements.modalOverlay = modalOverlay;
- const modal = document.createElement('div');
- modal.id = 'onchat-modal';
- modal.innerHTML = `
- <div class="onchat-modal-header">
- <span class="onchat-modal-title"></span>
- <button class="onchat-panel-close" data-action="close-modal">×</button>
- </div>
- <div class="onchat-modal-content"></div>
- `;
- document.body.appendChild(modal);
- this.elements.modal = modal;
- // FABs
- const scrollBtn = document.createElement('button');
- scrollBtn.id = 'onchat-scroll-btn';
- scrollBtn.className = 'onchat-fab';
- scrollBtn.innerHTML = '↓';
- scrollBtn.title = 'Aller en bas (G)';
- document.body.appendChild(scrollBtn);
- this.elements.scrollBtn = scrollBtn;
- const composeBtn = document.createElement('button');
- composeBtn.id = 'onchat-compose-btn';
- composeBtn.className = 'onchat-fab';
- composeBtn.innerHTML = '✎';
- composeBtn.title = 'Répondre (R)';
- document.body.appendChild(composeBtn);
- this.elements.composeBtn = composeBtn;
- // Apply minimized state
- if (SecurityUtils.safeGMGetValue(CONFIG.STORAGE_KEYS.PANEL_MINIMIZED, false)) {
- bar.classList.add(CONFIG.CLASSES.MINIMIZED);
- }
- },
- bindEvents() {
- // Bar buttons
- this.elements.bar.addEventListener('click', (e) => {
- const btn = e.target.closest('[data-action]');
- if (!btn) return;
- const action = btn.dataset.action;
- switch (action) {
- case 'toggle-live':
- const liveMode = StateManager.toggle('liveMode');
- btn.classList.toggle(CONFIG.CLASSES.ACTIVE, liveMode);
- if (liveMode) {
- Poller.start();
- ToastSystem.success('Mode Live activé');
- } else {
- Poller.stop();
- ToastSystem.info('Mode Live désactivé');
- }
- this.updateStatus();
- break;
- case 'toggle-sound':
- const sounds = SoundSystem.toggle();
- btn.classList.toggle(CONFIG.CLASSES.ACTIVE, sounds);
- ToastSystem.info(sounds ? 'Sons activés' : 'Sons désactivés');
- break;
- case 'toggle-notifications':
- NotificationSystem.toggle().then(enabled => {
- btn.classList.toggle(CONFIG.CLASSES.ACTIVE, enabled);
- ToastSystem.info(enabled ? 'Notifications activées' : 'Notifications désactivées');
- });
- break;
- case 'toggle-favorite':
- const topicId = FavoritesSystem.getTopicId();
- if (topicId) {
- const isFav = FavoritesSystem.toggle(topicId);
- btn.querySelector('span').textContent = isFav ? '★' : '☆';
- ToastSystem.info(isFav ? 'Ajouté aux favoris' : 'Retiré des favoris');
- }
- break;
- case 'toggle-settings':
- this.togglePanel();
- break;
- case 'toggle-minimize':
- const bar = this.elements.bar;
- bar.classList.toggle(CONFIG.CLASSES.MINIMIZED);
- const isMin = bar.classList.contains(CONFIG.CLASSES.MINIMIZED);
- btn.querySelector('span').textContent = isMin ? '▶' : '◀';
- SecurityUtils.safeGMSetValue(CONFIG.STORAGE_KEYS.PANEL_MINIMIZED, isMin);
- break;
- }
- });
- // Panel interactions
- this.elements.panel.addEventListener('click', (e) => {
- const toggle = e.target.closest('.onchat-toggle');
- if (toggle) {
- const setting = toggle.dataset.setting;
- switch (setting) {
- case 'liveMode':
- const liveMode = StateManager.toggle('liveMode');
- toggle.classList.toggle(CONFIG.CLASSES.ACTIVE, liveMode);
- if (liveMode) Poller.start();
- else Poller.stop();
- this.updateBarButton('toggle-live', liveMode);
- this.updateStatus();
- break;
- case 'sounds':
- const sounds = SoundSystem.toggle();
- toggle.classList.toggle(CONFIG.CLASSES.ACTIVE, sounds);
- this.updateBarButton('toggle-sound', sounds);
- break;
- case 'notifications':
- NotificationSystem.toggle().then(enabled => {
- toggle.classList.toggle(CONFIG.CLASSES.ACTIVE, enabled);
- this.updateBarButton('toggle-notifications', enabled);
- });
- break;
- case 'compactMode':
- const compact = StateManager.toggle('compactMode');
- toggle.classList.toggle(CONFIG.CLASSES.ACTIVE, compact);
- document.body.classList.toggle('onchat-compact', compact);
- break;
- }
- }
- if (e.target.dataset.action === 'close-panel') {
- this.closePanel();
- }
- });
- // Panel selects
- this.elements.panel.addEventListener('change', (e) => {
- const setting = e.target.dataset.setting;
- if (setting === 'refreshInterval') {
- const value = parseInt(e.target.value, 10);
- StateManager.set('refreshInterval', value);
- Poller.setInterval(value);
- ToastSystem.info(`Intervalle: ${value / 1000}s`);
- }
- if (setting === 'theme') {
- ThemeSystem.apply(e.target.value);
- ToastSystem.info(`Thème: ${e.target.value}`);
- }
- });
- // Mentions input
- const mentionsInput = this.elements.panel.querySelector('[data-setting="mentions"]');
- if (mentionsInput) {
- mentionsInput.addEventListener('change', (e) => {
- MentionSystem.setKeywords(e.target.value);
- ToastSystem.success('Mots-clés mis à jour');
- });
- }
- // Modal
- this.elements.modalOverlay.addEventListener('click', () => this.closeModal());
- this.elements.modal.querySelector('[data-action="close-modal"]').addEventListener('click', () => this.closeModal());
- // FABs
- this.elements.scrollBtn.addEventListener('click', () => {
- window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
- StateManager.set('unreadCount', 0);
- this.updateStatus();
- });
- this.elements.composeBtn.addEventListener('click', () => {
- KeyboardSystem.focusReply();
- });
- // Scroll handler
- let scrollTimeout;
- window.addEventListener('scroll', () => {
- clearTimeout(scrollTimeout);
- scrollTimeout = setTimeout(() => this.updateFABs(), 100);
- }, { passive: true });
- // Activity tracking
- ['mousedown', 'keydown', 'touchstart'].forEach(event => {
- document.addEventListener(event, () => StateManager.updateActivity(), { passive: true });
- });
- // Initial FAB state
- setTimeout(() => this.updateFABs(), 500);
- },
- togglePanel() {
- this.panelOpen = !this.panelOpen;
- this.elements.panel.classList.toggle(CONFIG.CLASSES.VISIBLE, this.panelOpen);
- },
- closePanel() {
- this.panelOpen = false;
- this.elements.panel.classList.remove(CONFIG.CLASSES.VISIBLE);
- },
- showModal(title, content) {
- this.elements.modal.querySelector('.onchat-modal-title').textContent = title;
- this.elements.modal.querySelector('.onchat-modal-content').innerHTML = content;
- this.elements.modalOverlay.classList.add(CONFIG.CLASSES.VISIBLE);
- this.elements.modal.classList.add(CONFIG.CLASSES.VISIBLE);
- this.modalOpen = true;
- },
- closeModal() {
- this.elements.modalOverlay.classList.remove(CONFIG.CLASSES.VISIBLE);
- this.elements.modal.classList.remove(CONFIG.CLASSES.VISIBLE);
- this.modalOpen = false;
- },
- updateStatus() {
- const status = this.elements.bar.querySelector('.onchat-bar-status');
- const unread = StateManager.get('unreadCount');
- if (StateManager.get('liveMode')) {
- status.className = 'onchat-bar-status onchat-bar-expandable live';
- status.textContent = unread > 0 ? `Live · ${unread} nouveau${unread > 1 ? 'x' : ''}` : 'Live';
- } else {
- status.className = 'onchat-bar-status onchat-bar-expandable paused';
- status.textContent = 'Pause';
- }
- // Update live button
- this.updateBarButton('toggle-live', StateManager.get('liveMode'));
- // Update unread badge
- const liveBtn = this.elements.bar.querySelector('[data-action="toggle-live"]');
- let badge = liveBtn.querySelector('.onchat-badge');
- if (unread > 0 && !document.hasFocus()) {
- if (!badge) {
- badge = document.createElement('span');
- badge.className = 'onchat-badge';
- liveBtn.appendChild(badge);
- }
- badge.textContent = unread > 99 ? '99+' : unread;
- } else if (badge) {
- badge.remove();
- }
- // Update title
- if (unread > 0) {
- document.title = `(${unread}) ${document.title.replace(/^\(\d+\)\s*/, '')}`;
- } else {
- document.title = document.title.replace(/^\(\d+\)\s*/, '');
- }
- },
- updateBarButton(action, active) {
- const btn = this.elements.bar.querySelector(`[data-action="${action}"]`);
- if (btn) {
- btn.classList.toggle(CONFIG.CLASSES.ACTIVE, active);
- }
- },
- updateFABs() {
- const scrolledFromBottom = document.documentElement.scrollHeight -
- (window.pageYOffset + document.documentElement.clientHeight);
- const showFABs = scrolledFromBottom > 200;
- this.elements.scrollBtn.classList.toggle(CONFIG.CLASSES.VISIBLE, showFABs);
- this.elements.composeBtn.classList.toggle(CONFIG.CLASSES.VISIBLE, showFABs);
- // Reset unread when at bottom
- if (!showFABs && StateManager.get('unreadCount') > 0) {
- StateManager.set('unreadCount', 0);
- this.updateStatus();
- }
- }
- };
- // ==================== MAIN APPLICATION ====================
- class OnChatUltimate {
- constructor() {
- this.initialized = false;
- }
- async init() {
- if (this.initialized) return;
- try {
- Logger.info('🚀 Initializing...');
- // Security first
- SecurityUtils.init();
- // Core systems
- StateManager.init();
- ThemeSystem.init();
- SoundSystem.init();
- await NotificationSystem.init();
- MentionSystem.init();
- BlockSystem.init();
- FavoritesSystem.init();
- // UI
- ToastSystem.init();
- UIManager.init();
- KeyboardSystem.init();
- // Message handling
- MessageManager.init();
- // Start polling if on topic page
- if (this.isTopicPage()) {
- if (StateManager.get('liveMode')) {
- Poller.start();
- }
- // Check favorite status
- const topicId = FavoritesSystem.getTopicId();
- if (topicId && FavoritesSystem.isFavorite(topicId)) {
- const favBtn = document.querySelector('[data-action="toggle-favorite"] span');
- if (favBtn) favBtn.textContent = '★';
- }
- }
- // Reset unread on focus
- window.addEventListener('focus', () => {
- StateManager.set('unreadCount', 0);
- UIManager.updateStatus();
- });
- // Intercept form submissions
- this.interceptForms();
- this.initialized = true;
- Logger.info('✅ Ready!');
- ToastSystem.success(`OnChat Pro ${CONFIG.VERSION} activé`);
- } catch (error) {
- Logger.error('Init failed:', error);
- ToastSystem.error('Erreur d\'initialisation');
- }
- }
- isTopicPage() {
- return window.location.pathname.includes('/topic/') &&
- document.querySelector(CONFIG.SELECTORS.MESSAGES_CONTAINER);
- }
- interceptForms() {
- const originalFetch = window.fetch;
- window.fetch = async function(...args) {
- const [url, options] = args;
- // Message POST
- if (options?.method === 'POST' && typeof url === 'string' && url.includes('/topic/')) {
- Logger.info('📤 Message POST');
- const response = await originalFetch.apply(this, args);
- if (response.ok) {
- // Trigger immediate poll
- setTimeout(() => {
- MessageManager.fetchNew().then(hasNew => {
- if (hasNew) {
- window.scrollTo({
- top: document.documentElement.scrollHeight,
- behavior: 'smooth'
- });
- }
- });
- }, 500);
- }
- return response;
- }
- return originalFetch.apply(this, args);
- };
- }
- }
- // ==================== BOOTSTRAP ====================
- const app = new OnChatUltimate();
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', () => app.init());
- } else {
- app.init();
- }
- })();
Add Comment
Please, Sign In to add comment