PageSourceSearch

https://pupwife.com/projects/chatroom/scripts/notifications.js

js pupwife.com collected 2026-09-30 15:06:57 UTC 39,956 bytes, 906 lines download raw bytes

1/**
2 * Chatroom Notification System
3 * Shows popup notifications for new messages (works across all pages)
4 */
5
6window.ChatroomNotifications = (function() {
7    'use strict';
8
9    function emitDevLog() {
10        if (typeof window !== 'undefined' && typeof window.devLog === 'function') {
11            window.devLog.apply(window, arguments);
12        }
13    }
14    function emitDevWarn() {
15        if (typeof window !== 'undefined' && typeof window.devWarn === 'function') {
16            window.devWarn.apply(window, arguments);
17        }
18    }
19
20    let notificationsEnabled = false;
21    let desktopNotificationsEnabled = false;
22    let currentUsername = null;
23    let messageListeners = [];
24    let notificationQueue = [];
25    let isShowingNotification = false;
26    let lastMessageTimes = new Map();
27
28    /**
29     * Initialize notification system
30     */
31    function init() {
32        emitDevLog('📢 ChatroomNotifications: init() called');
33        
34        // Load notification settings
35        loadNotificationSettings();
36        emitDevLog('📢 ChatroomNotifications: Notifications enabled?', notificationsEnabled);
37
38        // Create notification container if it doesn't exist
39        createNotificationContainer();
40
41        // Wait for username
42        const checkUsername = setInterval(() => {
43            if (window.__sharedUsername) {
44                currentUsername = window.__sharedUsername.get();
45                if (currentUsername) {
46                    clearInterval(checkUsername);
47                    emitDevLog('📢 ChatroomNotifications: Username loaded:', currentUsername);
48                    setupNotifications();
49                }
50            }
51        }, 100);
52
53        setTimeout(() => {
54            clearInterval(checkUsername);
55            if (!currentUsername) {
56                currentUsername = 'guest_' + Date.now();
57                emitDevLog('📢 ChatroomNotifications: Using guest username:', currentUsername);
58            }
59            setupNotifications();
60        }, 5000);
61
62        return true;
63    }
64
65    /**
66     * Load notification settings
67     */
68    function loadNotificationSettings() {
69        try {
70            const saved = localStorage.getItem('chatroom_settings');
71            if (saved) {
72                const parsed = JSON.parse(saved);
73                notificationsEnabled = parsed.notifications === true;
74                desktopNotificationsEnabled = parsed.desktopNotifications === true;
75            } else {
76                notificationsEnabled = false;
77                desktopNotificationsEnabled = false;
78            }
79        } catch (e) {
80            console.error('Error loading notification settings:', e);
81            notificationsEnabled = false;
82            desktopNotificationsEnabled = false;
83        }
84    }
85
86    /**
87     * Setup notification listeners
88     */
89    function setupNotifications() {
90        if (!notificationsEnabled && !desktopNotificationsEnabled) {
91            emitDevLog('📢 ChatroomNotifications: Notifications are disabled in settings');
92            return;
93        }
94        
95        if (typeof firebase === 'undefined' || !firebase.database) {
96            emitDevWarn('📢 ChatroomNotifications: Firebase not available, cannot setup notifications');
97            return;
98        }
99        
100        emitDevLog('📢 ChatroomNotifications: Setting up notification listeners for', currentUsername);
101
102        // Always setup admin notifications first (they work without username)
103        setupAdminNotifications();
104
105        // Setup chatroom notifications if we have a username
106        // (We'll check if user is viewing the channel/DM before showing notifications)
107        if (currentUsername) {
108            setupChatroomNotifications();
109        }
110    }
111
112    /**
113     * Setup chatroom message notifications
114     */
115    function setupChatroomNotifications() {
116        if (!currentUsername) return;
117
118        const db = firebase.database();
119        
120        // Clean up any existing chatroom listeners (but keep admin notifications)
121        messageListeners = messageListeners.filter(({ type }) => type === 'admin-notification');
122        
123        // Listen for channel messages (only setup once)
124        const channelsRef = db.ref('chatroom/channels');
125        const channelsListener = channelsRef.on('value', (snapshot) => {
126            const channels = snapshot.val();
127            if (!channels) return;
128
129            Object.keys(channels).forEach(channelId => {
130                // Skip if listener already exists for this channel
131                const existingListener = messageListeners.find(l => l.type === 'channel' && l.id === channelId);
132                if (existingListener) return;
133                
134                // Listen for new messages in this channel
135                const ref = db.ref(`chatroom/channels/${channelId}/messages`);
136                
137                const listener = ref.limitToLast(1).on('child_added', (messageSnapshot) => {
138                    if (!messageSnapshot.exists()) return;
139                    const message = messageSnapshot.val();
140                    
141                    // Check if we've already notified for this message (persist in localStorage)
142                    const key = `channel_${channelId}_${message.id}`;
143                    const notifiedKey = `chatroom_notified_${currentUsername}`;
144                    
145                    try {
146                        const notified = JSON.parse(localStorage.getItem(notifiedKey) || '{}');
147                        if (notified[key]) {
148                            return; // Already notified, skip
149                        }
150                        
151                        // Also check if we viewed this channel after the message was sent
152                        const lastViewed = notified[`channel_${channelId}_last_viewed`];
153                        if (lastViewed && message.timestamp && message.timestamp <= lastViewed) {
154                            // We viewed the channel after this message was sent, so mark as notified
155                            notified[key] = true;
156                            localStorage.setItem(notifiedKey, JSON.stringify(notified));
157                            return;
158                        }
159                    } catch (e) {
160                        // Fallback to in-memory tracking
161                        if (lastMessageTimes.has(key)) {
162                            return;
163                        }
164                    }
165                    
166                    // Only notify if message is from someone else
167                    if (message.username && message.username !== currentUsername) {
168                        // Check if user is currently viewing this channel
169                        const isViewingChannel = window.Chatroom && 
170                            window.Chatroom.getCurrentChannel && 
171                            window.Chatroom.getCurrentChannel() === channelId;
172                        
173                        // Check if message is already read
174                        const isRead = window.ChatroomUnreadCount && 
175                            window.ChatroomUnreadCount.isMessageRead(channelId, message.id);
176                        
177                        // Don't show notification if user is viewing this channel or message is already read
178                        if (!isViewingChannel && !isRead) {
179                            // Mark as notified BEFORE showing notification
180                            try {
181                                const notified = JSON.parse(localStorage.getItem(notifiedKey) || '{}');
182                                notified[key] = true;
183                                localStorage.setItem(notifiedKey, JSON.stringify(notified));
184                            } catch (e) {
185                                lastMessageTimes.set(key, message.timestamp);
186                            }
187                            
188                            showNotification({
189                                type: 'channel',
190                                channel: channelId,
191                                username: message.username,
192                                text: message.text,
193                                timestamp: message.timestamp,
194                                id: message.id
195                            });
196                            
197                            // Update unread count
198                            if (window.ChatroomUnreadCount) {
199                                window.ChatroomUnreadCount.updateUnreadCount(channelId);
200                            }
201                        } else {
202                            // Mark as notified even if we don't show notification (already viewed/read)
203                            try {
204                                const notified = JSON.parse(localStorage.getItem(notifiedKey) || '{}');
205                                notified[key] = true;
206                                localStorage.setItem(notifiedKey, JSON.stringify(notified));
207                            } catch (e) {
208                                lastMessageTimes.set(key, message.timestamp);
209                            }
210                        }
211                    }
212                });
213                messageListeners.push({ ref, listener, type: 'channel', id: channelId });
214            });
215        });
216        
217        // Store the channels listener for cleanup
218        messageListeners.push({ ref: channelsRef, listener: channelsListener, type: 'channels-listener' });
219
220        // Listen for DM messages (only setup once)
221        const dmsRef = db.ref('chatroom/dms');
222        emitDevLog('📢 ChatroomNotifications: Setting up DM listener for', currentUsername);
223        
224        const dmsListener = dmsRef.on('value', (snapshot) => {
225            const conversations = snapshot.val();
226            if (!conversations) {
227                emitDevLog('📢 ChatroomNotifications: No DM conversations found');
228                return;
229            }
230
231            emitDevLog('📢 ChatroomNotifications: Found', Object.keys(conversations).length, 'DM conversations');
232            
233            Object.keys(conversations).forEach(conversationId => {
234                const participants = conversations[conversationId]?.metadata?.participants;
235                if (!participants || !participants.includes(currentUsername)) {
236                    return;
237                }
238
239                const otherUser = participants.find(u => u !== currentUsername);
240                if (!otherUser) return;
241
242                // Skip if listener already exists for this DM
243                const existingListener = messageListeners.find(l => l.type === 'dm' && l.id === conversationId);
244                if (existingListener) {
245                    emitDevLog('📢 ChatroomNotifications: Listener already exists for DM with', otherUser);
246                    return;
247                }
248
249                emitDevLog('📢 ChatroomNotifications: Setting up listener for DM with', otherUser);
250                const messagesRef = db.ref(`chatroom/dms/${conversationId}/messages`);
251                
252                // Get the last message timestamp to ignore old messages
253                let lastKnownTimestamp = 0;
254                let setupComplete = false;
255                
256                messagesRef.limitToLast(1).once('value', (snapshot) => {
257                    if (snapshot.exists()) {
258                        const lastMessage = snapshot.val();
259                        const lastKey = Object.keys(lastMessage)[0];
260                        if (lastMessage[lastKey] && lastMessage[lastKey].timestamp) {
261                            lastKnownTimestamp = lastMessage[lastKey].timestamp;
262                            emitDevLog('📢 ChatroomNotifications: Last known message timestamp for', otherUser, ':', lastKnownTimestamp, '(current time:', Date.now(), ')');
263                        }
264                    }
265                    setupComplete = true;
266                });
267                
268                // Helper function to process DM messages
269                function processDMMessage(message, otherUser, conversationId) {
270                    
271                    // Check if we've already notified for this message (persist in localStorage)
272                    const key = `dm_${conversationId}_${message.id}`;
273                    const notifiedKey = `chatroom_notified_${currentUsername}`;
274                    
275                    try {
276                        const notified = JSON.parse(localStorage.getItem(notifiedKey) || '{}');
277                        if (notified[key]) {
278                            emitDevLog('📢 ChatroomNotifications: Already notified for message', message.id);
279                            return; // Already notified, skip
280                        }
281                    } catch (e) {
282                        // Fallback to in-memory tracking
283                        if (lastMessageTimes.has(key)) {
284                            emitDevLog('📢 ChatroomNotifications: Already notified for message', message.id, '(in-memory)');
285                            return;
286                        }
287                    }
288                    
289                    // Only notify if message is from the other user
290                    if (message.username === otherUser) {
291                        emitDevLog('📢 ChatroomNotifications: Processing DM message from', otherUser, 'ID:', message.id);
292                        
293                        // Check if user is currently viewing this DM
294                        const isViewingDM = window.ChatroomDMs && 
295                            window.ChatroomDMs.getCurrentDM && 
296                            window.ChatroomDMs.getCurrentDM() === otherUser;
297                        
298                        emitDevLog('📢 ChatroomNotifications: Is viewing DM?', isViewingDM);
299                        
300                        // Check if message is already read (with a 
300small delay to ensure read status is updated)
301                        let isRead = false;
302                        if (window.ChatroomUnreadCount) {
303                            // Check immediately
304                            isRead = window.ChatroomUnreadCount.isMessageRead(`dm_${otherUser}`, message.id);
305                            
306                            // Also check localStorage for read status
307                            if (!isRead && currentUsername) {
308                                try {
309                                    const readKey = `chatroom_read_messages_${currentUsername}`;
310                                    const readData = JSON.parse(localStorage.getItem(readKey) || '{}');
311                                    const dmKey = `dm_${otherUser}`;
312                                    if (readData[dmKey] && Array.isArray(readData[dmKey])) {
313                                        isRead = readData[dmKey].includes(message.id);
314                                    }
315                                } catch (e) {
316                                    // Ignore errors
317                                }
318                            }
319                        }
320                        
321                        emitDevLog('📢 ChatroomNotifications: Is message read?', isRead);
322                        
323                        // Don't show notification if user is viewing this DM or message is already read
324                        if (!isViewingDM && !isRead) {
325                            emitDevLog('📢 ChatroomNotifications: Showing notification for DM from', otherUser);
326                            // Mark as notified BEFORE showing notification
327                            try {
328                                const notified = JSON.parse(localStorage.getItem(notifiedKey) || '{}');
329                                notified[key] = true;
330                                localStorage.setItem(notifiedKey, JSON.stringify(notified));
331                            } catch (e) {
332                                lastMessageTimes.set(key, message.timestamp);
333                            }
334                            
335                            // Auto-open DM if this is the first message from this user
336                            const dmHistory = JSON.parse(localStorage.getItem('chatroom_dm_history') || '[]');
337                            const isFirstMessage = !dmHistory.includes(otherUser);
338                            
339                            if (isFirstMessage && window.ChatroomDMs && window.ChatroomDMs.openDM) {
340                                // Add to DM history
341                                dmHistory.unshift(otherUser);
342                                if (dmHistory.length > 20) {
343                                    dmHistory.pop();
344                                }
345                                localStorage.setItem('chatroom_dm_history', JSON.stringify(dmHistory));
346                                
347                                // Auto-open the DM
348                                setTimeout(() => {
349                                    window.ChatroomDMs.openDM(otherUser);
350                                }, 100);
351                            }
352                            
353                            showNotification({
354                                type: 'dm',
355                                username: otherUser,
356                                text: message.text,
357                                timestamp: message.timestamp,
358                                id: message.id
359                            });
360                            
361                            // Update unread count and DM list
362                            if (window.ChatroomUnreadCount) {
363                                const dmKey = `dm_${otherUser}`;
364                                window.ChatroomUnreadCount.updateUnreadCount(dmKey);
365                            }
366                            
367                            // Update DM list to show new DM in sidebar
368                            if (window.ChatroomDMs && window.ChatroomDMs.updateDMList) {
369                                setTimeout(() => {
370                                    window.ChatroomDMs.updateDMList();
371                                }, 200);
372                            }
373                        } else {
374                            // Mark as notified even if we don't show notification (already viewed/read)
375                            try {
376                                const notified = JSON.parse(localStorage.getItem(notifiedKey) || '{}');
377                                notified[key] = true;
378                                localStorage.setItem(notifiedKey, JSON.stringify(notified));
379                            } catch (e) {
380                                lastMessageTimes.set(key, message.timestamp);
381                            }
382                        }
383                    }
384                }
385                
386                const listener = messagesRef.on('child_added', (messageSnapshot) => {
387                    if (!messageSnapshot.exists()) return;
388                    const message = messageSnapshot.val();
389                    
390                    // Wait for setup to complete before processing
391                    if (!setupComplete) {
392                        setTimeout(() => {
393                            if (message.timestamp && message.timestamp <= lastKnownTimestamp) {
394                                emitDevLog('📢 ChatroomNotifications: Skipping old message from', otherUser, '(timestamp:', message.timestamp, '<=', lastKnownTimestamp, ')');
395                                return;
396                            }
397                            processDMMessage(message, otherUser, conversationId);
398                        }, 200);
399                        return;
400                    }
401                    
402                    // Skip if this is an old message (before we set up the listener)
403                    if (message.timestamp && message.timestamp <= lastKnownTimestamp) {
404                        emitDevLog('📢 ChatroomNotifications: Skipping old message from', otherUser, '(timestamp:', message.timestamp, '<=', lastKnownTimestamp, ')');
405                        return;
406                    }
407                    
408                    emitDevLog('📢 ChatroomNotifications: New DM message received from', otherUser, 'ID:', message.id, 'timestamp:', message.timestamp, '(last known:', lastKnownTimestamp, ')');
409                    processDMMessage(message, otherUser, conversationId);
410                });
411                
412                messageListeners.push({ ref: messagesRef, listener, type: 'dm', id: conversationId });
413            });
414        });
415        
416        // Store the DMs listener for cleanup
417        messageListeners.push({ ref: dmsRef, listener: dmsListener, type: 'dms-listener' });
418    }
419
420    /**
421     * Show a system/desktop notification (works when tabbed out or on other pages)
422     */
423    function showDesktopNotification(data) {
424        if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return;
425        
426        let title = data.username || 'Chatroom';
427        let body = '';
428        let tag = 'chatroom-notification';
429        
430        if (data.type === 'admin') {
431            title = data.title || 'Admin Notification';
432            body = data.message || '';
433            tag = 'chatroom-admin-' + (data.id || Date.now());
434        } else if (data.type === 'channel') {
435            title = `${data.username} in #${data.channel}`;
436            const text = data.text || '';
437            body = text.length > 100 ? text.substring(0, 100) + '...' : text;
438            tag = `channel-${data.channel}-${data.id || Date.now()}`;
439        } else if (data.type === 'dm') {
440            title = `${data.username} (Direct Message)`;
441            const text = data.text || '';
442            body = text.length > 100 ? text.substring(0, 100) + '...' : text;
443            tag = `dm-${data.username}-${data.id || Date.now()}`;
444        }
445        
446        try {
447            const iconUrl = (typeof window !== 'undefined' && window.location && window.location.origin)
448                ? new URL('/icons/puppybutton.png', window.location.origin).href
449                : '/icons/puppybutton.png';
450            const n = new Notification(title, {
451                body: body || 'New message',
452                tag: tag,
453                icon: iconUrl,
454                requireInteraction: false
455            });
456            n.onclick = () => {
457                window.focus();
458                const url = new URL('/chatroom.html', window.location.origin);
459                if (data.type === 'channel') {
460                    url.searchParams.set('channel', data.channel);
461                } else if (data.type === 'dm') {
462                    url.searchParams.set('dm', data.username);
463                }
464                if (data.id) url.searchParams.set('message', data.id);
465                window.location.href = url.toString();
466                n.close();
467            };
468        } catch (e) {
469            emitDevWarn('Desktop notification failed:', e);
470        }
471    }
472
473    /**
474     * Create notification container
475     */
476    function createNotificationContainer() {
477        let container = document.getElementById('chatroom-notification-container');
478        if (!container) {
479            container = document.createElement('div');
480            container.id = 'chatroom-notification-container';
481            document.body.appendChild(container);
482        }
483    }
484
485    /**
486     * Show a notification
487     */
488    function showNotification(data) {
489        emitDevLog('📢 ChatroomNotifications: showNotification called', data);
490        
491        if (!notificationsEnabled && !desktopNotificationsEnabled) {
492            emitDevLog('📢 ChatroomNotifications: Notifications disabled');
493            return;
494        }
495
496        // Show system/desktop notification (works when tabbed out or on other pages)
497        if (desktopNotificationsEnabled && typeof Notification !== 'undefined' && Notification.permission === 'granted') {
498            showDesktopNotification(data);
499        }
500        
501        if (!notificationsEnabled) {
502            isShowingNotification = false;
503            if (notificationQueue.length > 0) {
504                const next = notificationQueue.shift();
505                setTimeout(() => showNotification(next), 100);
506            }
507            return;
508        }
509        
510        if (isShowingNotification) {
511            emitDevLog('📢 ChatroomNotifications: Already showing notification, queuing');
512            // Queue notification if one is already showing
513            notificationQueue.push(data);
514            return;
515        }
516
517        isShowingNotification = true;
518        let container = document.getElementById('chatroom-notification-container');
519        if (!container) {
520            emitDevWarn('📢 ChatroomNotifications: Container not found, creating it');
521            createNotificationContainer();
522            container = document.getElementById('chatroom-notification-container');
523            if (!container) {
524                console.error('📢 ChatroomNotifications: Failed to create container');
525                isShowingNotification = false;
526                return;
527            }
528        }
529        
530        emitDevLog('📢 ChatroomNotifications: Container found, creating notification element');
531
532        // Create notification element
533        const notification = document.createElement('div');
534        notification.className = 'chatroom-notification';
535        notification.dataset.type = data.type;
536        notification.dataset.id = data.id;
537
538        // Get avatar
539        const avatar = data.type === 'admin' ? '👑' : getAvatar(data.username);
540
541        // Format notification content
542        let title = data.username;
543        let subtitle = '';
544        if (data.type === 'admin') {
545            title = data.title || 'Admin Notification';
546            subtitle = 'Admin';
547        } else if (data.type === 'channel') {
548            subtitle = `#${data.channel}`;
549        } else if (data.type === 'dm') {
550            subtitle = 'Direct Message';
551        }
552
553        // Truncate message text
554        const maxLength = 100;
555        // For admin notifications, use 'message' property; for others use 'text'
556        let messageText = data.type === 'admin' ? (data.message || '') : (data.text || '');
557        if (messageText.length > maxLength) {
558            messageText = messageText.substring(0, maxLength) + '...';
559        }
560
561        notification.innerHTML = `
562            <div class="notification-avatar">${avatar}</div>
563            <div class="notification-content">
564                <div class="notification-header">
565                    <span class="notification-username">${escapeHtml(title)}</span>
566                    <span class="notification-subtitle">${escapeHtml(subtitle)}</span>
567                </div>
568                <div class="notification-message">${escapeHtml(messageText)}</div>
569            </div>
570            <button class="notification-close" title="Close">×</button>
571        `;
572
573        // Add click handler to navigate to chatroom (skip for admin notifications)
574        if (data.type !== 'admin') {
575            notification.addEventListener('click', (e) => {
576                // Don't navigate if clicking the close button
577                if (e.target.classList.contains('notification-close')) {
578                    return;
579                }
580                
581                // Navigate to chatroom
582                const url = new URL('/chatroom.html', window.location.origin);
583                if (data.type === 'channel') {
584                    url.searchParams.set('channel', data.channel);
585                } else if (data.type === 'dm') {
586                    url.searchParams.set('dm', data.username);
587                }
588                url.searchParams.set('message', data.id);
589                window.location.href = url.toString();
590            });
591        }
592
593        // Close button handler
594        const closeBtn = notification.querySelector('.notification-close');
595        if (closeBtn) {
596            closeBtn.addEventListener('click', (e) => {
597                e.stopPropagation();
598                hideNotification(notification);
599            });
600        }
601
602        container.appendChild(notification);
603        emitDevLog('📢 ChatroomNotifications: Notification element added to DOM, container:', container.id);
604
605        // Animate in
606        setTimeout(() => {
607            notification.classList.add('show');
608            emitDevLog('📢 ChatroomNotifications: Notification animation started (added "show" class), element:', notification);
609            
610            // Verify the element is visible
611            const computedStyle = window.getComputedStyle(notification);
612            emitDevLog('📢 ChatroomNotifications: Notification styles - opacity:', computedStyle.opacity, 'transform:', computedStyle.transform, 'display:', computedStyle.display);
613        }, 10);
614
615        // Auto-hide after 5 seconds
616        const autoHideTimeout = setTimeout(() => {
617            hideNotification(notification);
618        }, 5000);
619
620        // Store timeout for cleanup
621        notification.dataset.autoHideTimeout = autoHideTimeout;
622    }
623
624    /**
625     * Hide a notification
626     */
627    function hideNotification(notification) {
628        if (!notification) return;
629
630        notification.classList.remove('show');
631        
632        // Clear auto-hide timeout
633        if (notification.dataset.autoHideTimeout) {
634            clearTimeout(parseInt(notification.dataset.autoHideTimeout));
635        }
636
637        setTimeout(() => {
638            notification.remove();
639            isShowingNotification = false;
640
641            // Show next queued notification
642            if (notificationQueue.length > 0) {
643                const next = notificationQueue.shift();
644                setTimeout(() => showNotification(next), 300);
645            }
646        }, 300);
647    }
648
649    /**
650     * Get avatar for username
651     */
652    function getAvatar(username) {
653        if (!username) return '👤';
654        const avatars = ['🐾', '🐱', '🐶', '🦊', '🐰', '🐻', '🐼', '🐨', '🐯', '🦁'];
655        let hash = 0;
656        for (let i = 0; i < username.length; i++) {
657            hash = username.charCodeAt(i) + ((hash << 5) - hash);
658        }
659        return avatars[Math.abs(hash) % avatars.length];
660    }
661
662    /**
663     * Escape HTML
664     */
665    function escapeHtml(text) {
666        const div = document.createElement('div');
667        div.textContent = text;
668        return div.innerHTML;
669    }
670
671    /**
672     * Update desktop notification setting
673     */
674    function updateDesktopSetting(enabled) {
675        desktopNotificationsEnabled = enabled;
676        if (notificationsEnabled || desktopNotificationsEnabled) {
677            setupNotifications();
678        } else {
679            if (window.ChatroomNotifications && window.ChatroomNotifications.cleanup) {
680                window.ChatroomNotifications.cleanup();
681            }
682        }
683    }
684
685    /**
686     * Update notification settings
687     */
688    function updateSettings(enabled) {
689        notificationsEnabled = enabled;
690        
691        if (enabled || desktopNotificationsEnabled) {
692            setupNotifications();
693        } else {
694            // Remove all listeners
695            messageListeners.forEach(({ ref, listener }) => {
696                if (ref && listener) {
697                    ref.off('child_added', listener);
698                }
699            });
700            messageListeners = [];
701            
702            // Clear any visible notifications
703            const container = document.getElementById('chatroom-notification-container');
704            if (container) {
705                container.innerHTML = '';
706            }
707            isShowingNotification = false;
708            notificationQueue = [];
709        }
710    }
711
712    /**
713     * Cleanup listeners
714     */
715    function cleanup() {
716        messageListeners.forEach(({ ref, listener, type }) => {
717            if (ref && listener) {
718                if (type === 'channels-listener' || type === 'dms-listener') {
719                    ref.off('value', listener);
720                } else if (type === 'admin-notification') {
721                    ref.off('child_added', listener);
722                } else {
723                    ref.off('child_added', listener);
724                }
725            }
726        });
727        messageListeners = [];
728        lastMessageTimes.clear();
729    }
730
731    // Initialize on load (with delay to ensure Firebase is loaded)
732    let initAttempts = 0;
733    const maxInitAttempts = 100; // Try for up to 10 seconds (100 * 100ms)
734    
735    function delayedInit() {
736        initAttempts++;
737        
738        // Check if Firebase is available
739        if (typeof firebase === 'undefined' || !firebase.database) {
740            if (initAttempts < maxInitAttempts) {
741                // Wait for Firebase to load
742                setTimeout(delayedInit, 100);
743                return;
744            } else {
745                emitDevWarn('📢 ChatroomNotifications: Firebase not available after', maxInitAttempts * 100, 'ms');
746                return;
747            }
748        }
749        
750        // Double-check that firebase.database() is callable
751        try {
752            const testRef = firebase.database().ref('test');
753            if (!testRef) {
754                if (initAttempts < maxInitAttempts) {
755                    setTimeout(delayedInit, 100);
756                    return;
757                }
758            }
759        } catch (e) {
760            if (initAttempts < maxInitAttempts) {
761                setTimeout(delayedInit, 100);
762                return;
763            } else {
764                emitDevWarn('📢 ChatroomNotifications: Firebase database not ready:', e);
765                return;
766            }
767        }
768        
769        emitDevLog('📢 ChatroomNotifications: Initializing notification system...');
770        init();
771    }
772
773    if (document.readyState === 'loading') {
774        document.addEventListener('DOMContentLoaded', () => {
775            setTimeout(delayedInit, 1000); // Wait 1 second for scripts to load
776        });
777    } else {
778        setTimeout(delayedInit, 1000); // Wait 1 second for scripts to load
779    }
780
781    // Cleanup on page unload
782    window.addEventListener('beforeunload', cleanup);
783
784    /**
785     * Show admin notification
786     */
787    function showAdminNotification(title, message) {
788        if (!notificationsEnabled && !desktopNotificationsEnabled) return;
789        
790        showNotification({
791            type: 'admin',
792            title: title || 'Admin Notification',
793            message: message || '',
794            timestamp: Date.now(),
795            id: 'admin_' + Date.now()
796        });
797    }
798
799    /**
800     * Listen for admin notifications from Firebase
801     */
802    function setupAdminNotifications() {
803        if (!notificationsEnabled && !desktopNotificationsEnabled) return;
804        if (typeof firebase === 'undefined' || !firebase.database) return;
805        // Admin notifications work even on chatroom page
806
807        // Check if admin notification listener already exists
808        const existingListener = messageListeners.find(l => l.type === 'admin-notification');
809        if (existingListener) return;
810
811        const db = firebase.database();
812        const adminNotifRef = db.ref('chatroom/adminNotifications');
813        
814        // Cleanup old notifications (older than 1 minute) periodically
815        function cleanupOldNotifications() {
816            adminNotifRef.once('value', (snapshot) => {
817                if (!snapshot.exists()) return;
818                
819                const now = Date.now();
820                const oneMinuteAgo = now - 60000; // 1 minute in milliseconds
821                const updates = {};
822                let hasUpdates = false;
823                
824                snapshot.forEach((childSnapshot) => {
825                    const notification = childSnapshot.val();
826                    const notificationTime = notification.timestamp || 0;
827                    
828                    // Delete notifications older than 1 minute
829                    if (notificationTime < oneMinuteAgo) {
830                        updates[childSnapshot.key] = null; // null means delete in Firebase
831                        hasUpdates = true;
832                    }
833                });
834                
835                if (hasUpdates) {
836                    adminNotifRef.update(updates).catch(err => {
837                        console.error('Error cleaning up old admin notifications:', err);
838                    });
839                }
840            });
841        }
842        
843        // Run cleanup every 30 seconds
844        setInterval(cleanupOldNotifications, 30000);
845        // Also run cleanup immediately
846        cleanupOldNotifications();
847        
848        // Listen for new admin notifications
849        const listener = adminNotifRef.limitToLast(1).on('child_added', (snapshot) => {
850            if (!snapshot.exists()) return;
851            const notification = snapshot.val();
852            
853            // Check if we've already shown this notification using localStorage (persists across page loads)
854            const notificationId = notification.id || snapshot.key;
855            const shownNotificationsKey = 'chatroom_admin_notifications_shown';
856            let shownNotifications = [];
857            try {
858                const saved = localStorage.getItem(shownNotificationsKey);
859                if (saved) {
860                    shownNotifications = JSON.parse(saved);
861                }
862            } catch (e) {
863                console.error('Error loading shown admin notifications:', e);
864            }
865            
866            // Check if this notification was already shown
867            if (shownNotifications.includes(notificationId)) {
868                return;
869            }
870            
871            // Mark as shown in localStorage
872            shownNotifications.push(notificationId);
873            // Keep only last 100 notification IDs to prevent localStorage from growing too large
874            if (shownNotifications.length > 100) {
875                shownNotifications = shownNotifications.slice(-100);
876            }
877            try {
878                localStorage.setItem(shownNotificationsKey, JSON.stringify(shownNotifications));
879            } catch (e) {
880                console.error('Error saving shown admin notifications:', e);
881            }
882            
883            // Also update in-memory tracking
884            const key = `admin_${notificationId}`;
885            lastMessageTimes.set(key, notification.timestamp || Date.now());
886            
887            showNotification({
888                type: 'admin',
889                title: notification.title || 'Admin Notification',
890                message: notification.message || '',
891                timestamp: notification.timestamp || Date.now(),
892                id: notificationId
893            });
894        });
895        
896        messageListeners.push({ ref: adminNotifRef, listener, type: 'admin-notification' });
897    }
898
899    return {
900        updateSettings,
901        updateDesktopSetting,
902        init,
903        cleanup,
904        showAdminNotification
905    };
906})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.