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.