/** * Менеджер уведомлений - подключение к WebSocket и управление */ import { getNotifications, invalidateNotifications } from '../api/notificationsApi.js'; class NotificationManager { constructor() { this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 1000; this.isConnected = false; this.notificationPermission = 'default'; this.soundEnabled = this.loadSoundPreference(); this.notificationSound = null; this.init(); } /** * Загрузить настройку звука из localStorage */ loadSoundPreference() { const saved = localStorage.getItem('notificationSoundEnabled'); return saved === null ? true : saved === 'true'; } /** * Сохранить настройку звука */ saveSoundPreference(enabled) { this.soundEnabled = enabled; localStorage.setItem('notificationSoundEnabled', enabled.toString()); } /** * Воспроизвести звук уведомления */ playNotificationSound() { if (!this.soundEnabled) return; if (!this.notificationSound) { // Создаем короткий звук через Web Audio API const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.value = 800; // Частота звука oscillator.type = 'sine'; gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.3); } } init() { this.requestNotificationPermission(); this.connectWebSocket(); this.loadInitialNotifications(); this.setupDropdown(); } /** * Запросить разрешение на показ браузерных уведомлений */ async requestNotificationPermission() { if (!('Notification' in window)) { console.warn('[Notifications] Browser does not support notifications'); return; } if (Notification.permission === 'granted') { this.notificationPermission = 'granted'; console.log('[Notifications] Permission already granted'); return; } if (Notification.permission !== 'denied') { try { const permission = await Notification.requestPermission(); this.notificationPermission = permission; console.log('[Notifications] Permission:', permission); } catch (error) { console.error('[Notifications] Error requesting permission:', error); } } } connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws/notifications/`; console.log('[Notifications] Connecting to:', wsUrl); try { this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('[Notifications] WebSocket connected'); this.isConnected = true; this.reconnectAttempts = 0; this.updateConnectionStatus(true); }; this.ws.onmessage = (event) => { this.handleMessage(event); }; this.ws.onclose = () => { console.log('[Notifications] WebSocket disconnected'); this.isConnected = false; this.updateConnectionStatus(false); this.attemptReconnect(); }; this.ws.onerror = (error) => { console.error('[Notifications] WebSocket error:', error); }; } catch (error) { console.error('[Notifications] Failed to create WebSocket:', error); this.attemptReconnect(); } } handleMessage(event) { try { const data = JSON.parse(event.data); // Игнорируем ping сообщения для keepalive (не логируем) if (data.type === 'ping') { return; } console.log('[Notifications] Received:', data); switch (data.type) { case 'unread_count': this.updateBadge(data.count); break; case 'new_notification': this.showNewNotification(data.notification); this.updateBadge(data.notification.unread_count); // Инвалидируем кеш при получении нового уведомления invalidateNotifications(); break; case 'count_update': this.updateBadge(data.count); // Инвалидируем кеш при изменении счетчика invalidateNotifications(); break; default: console.warn('[Notifications] Unknown message type:', data.type); } } catch (error) { console.error('[Notifications] Error parsing message:', error); } } attemptReconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = this.reconnectDelay * this.reconnectAttempts; console.log(`[Notifications] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); setTimeout(() => { this.connectWebSocket(); }, delay); } else { console.error('[Notifications] Max reconnect attempts reached'); } } updateConnectionStatus(connected) { // Визуальная индикация статуса подключения (опционально) const badge = document.getElementById('notificationBadge'); if (badge) { if (!connected) { badge.style.backgroundColor = '#dc3545'; // красный при отключении } else { badge.style.backgroundColor = ''; // вернуть стандартный цвет } } } updateBadge(count) { const badge = document.getElementById('notificationBadge'); if (badge) { if (count > 0) { badge.textContent = count > 99 ? '99+' : count; badge.classList.remove('d-none'); } else { badge.classList.add('d-none'); } } } async loadInitialNotifications() { try { // Используем кешированный API вместо прямого fetch const data = await getNotifications({ page: 1, page_size: 5, unread_only: true }); console.log('[Notifications] Loaded data:', data); this.renderNotifications(data.notifications || []); this.updateBadge(data.total || 0); } catch (error) { console.error('[Notifications] Error loading notifications:', error); // Показываем заглушку при ошибке const list = document.getElementById('notificationList'); if (list) { list.innerHTML = `