1/** 2 * Direct Messaging System 3 * Handles private messages between users 4 */ 5 6window.ChatroomDMs = (function() { 7 'use strict'; 8 9 const DM_CACHE_TTL = 5 * 60 * 1000; // 5 minutes - don't re-fetch if opened recently 10 const INITIAL_MESSAGE_LIMIT = 50; 11 const LOAD_OLDER_BATCH_SIZE = 50; 12 const SCROLL_LOAD_THRESHOLD = 150; 13 14 let dmsRef = null; 15 let currentUsername = null; 16 let currentDM = null; 17 let dmListeners = new Map(); 18 let displayedMessageIds = new Set(); 19 20 // DM cache: only fetch when conversation hasn't been opened recently 21 let dmCache = new Map(); // conversationId -> { messages, lastFetchTime, oldestTimestamp, newestTimestamp } 22 let loadingOlderDM = false; 23 let hasMoreOlderDM = true; 24 let dmScrollLoadHandler = null; 25 let suppressDMAnimations = false; 26 27 function callUnread(method) { 28 const mod = window.ChatroomUnreadCount; 29 if (!mod || typeof mod[method] !== 'function') return; 30 return mod[method].apply(mod, Array.prototype.slice.call(arguments, 1)); 31 } 32 33 function isMessageEmpty(message) { 34 const hasText = message.text != null && String(message.text).trim() !== ''; 35 const hasImages = message.images && Array.isArray(message.images) && message.images.length > 0; 36 return !hasText && !hasImages; 37 } 38 39 function applyDmInVoiceIndicators(inCall, partnerUsername) { 40 document.querySelectorAll('#channels-column-dm-list .dm-item').forEach(function (item) { 41 const other = item.dataset.dm; 42 let line = item.querySelector('.dm-in-voice-line'); 43 if (inCall && partnerUsername && other === partnerUsername) { 44 item.classList.add('dm-in-voice'); 45 if (!line) { 46 line = document.createElement('span'); 47 line.className = 'dm-in-voice-line'; 48 line.innerHTML = (window.ChatroomIcons ? window.ChatroomIcons.icon('phone') : 'ð') + ' In voice'; 49 item.appendChild(line); 50 } 51 } else { 52 item.classList.remove('dm-in-voice'); 53 if (line) line.remove(); 54 } 55 }); 56 } 57 58 /** 59 * Initialize DM system 60 */ 61 function init() { 62 window.addEventListener('chatroomDmCallChanged', function (e) { 63 const detail = e.detail || {}; 64 applyDmInVoiceIndicators(detail.inCall, detail.username); 65 }); 66 67 if (typeof firebase === 'undefined' || !firebase.database) { 68 console.error('Firebase not available for DMs'); 69 return false; 70 } 71 72 dmsRef = firebase.database().ref('chatroom/dms'); 73 74 // Wait for username 75 const checkUsername = setInterval(() => { 76 if (window.__sharedUsername) { 77 currentUsername = window.__sharedUsername.get(); 78 if (currentUsername) { 79 clearInterval(checkUsername); 80 setupUI(); 81 } 82 } 83 }, 100); 84 85 setTimeout(() => { 86 clearInterval(checkUsername); 87 if (!currentUsername) { 88 currentUsername = 'guest_' + Date.now(); 89 setupUI(); 90 } else { 91 // Update DM list after username is loaded 92 setTimeout(() => { 93 updateDMList(); 94 }, 500); 95 } 96 }, 5000); 97 98 // Periodically update DM list 99 setInterval(() => { 100 if (currentUsername) { 101 updateDMList(); 102 } 103 }, 30000); // Update every 30 seconds 104 105 // Restore last DM after UI is ready (longer delay to ensure everything is loaded) 106 setTimeout(() => { 107 restoreLastDM();
108 }, 2000); 109 110 return true; 111 } 112 113 /** 114 * Setup DM UI handlers 115 */ 116 function setupUI() { 117 const newDMBtn = document.getElementById('new-dm-btn'); 118 const dmModal = document.getElementById('dm-modal'); 119 const dmModalClose = document.getElementById('dm-modal-close'); 120 const dmUsernameInput = document.getElementById('dm-username-input'); 121 const userSuggestions = document.getElementById('user-suggestions'); 122 123 if (newDMBtn) { 124 newDMBtn.addEventListener('click', () => { 125 dmModal.style.display = 'flex'; 126 if (dmUsernameInput) dmUsernameInput.focus(); 127 }); 128 } 129 130 if (dmModalClose) { 131 dmModalClose.addEventListener('click', () => { 132 dmModal.style.display = 'none'; 133 }); 134 } 135 136 // Close modal on outside click 137 if (dmModal) { 138 dmModal.addEventListener('click', (e) => { 139 if (e.target === dmModal) { 140 dmModal.style.display = 'none'; 141 } 142 }); 143 } 144 145 // Username input with suggestions 146 if (dmUsernameInput) { 147 dmUsernameInput.addEventListener('input', (e) => { 148 const query = e.target.value.trim().toLowerCase(); 149 if (query.length > 0) { 150 showUserSuggestions(query); 151 } else { 152 userSuggestions.innerHTML = ''; 153 } 154 }); 155 156 dmUsernameInput.addEventListener('keydown', (e) => { 157 if (e.key === 'Enter') { 158 e.preventDefault(); 159 const username = e.target.value.trim(); 160 if (username) { 161 openDM(username); 162 dmModal.style.display = 'none'; 163 e.target.value = ''; 164 } 165 } 166 }); 167 } 168 } 169 170 /** 171 * Show user suggestions 172 */ 173 function showUserSuggestions(query) { 174 const userSuggestions = document.getElementById('user-suggestions'); 175 if (!userSuggestions) return; 176 177 // Get online users from presence system 178 const onlineUsers = window.ChatroomPresence ? window.ChatroomPresence.getOnlineUsers() : []; 179 180 userSuggestions.innerHTML = ''; 181 182 onlineUsers.forEach(username => { 183 if (username.toLowerCase().includes(query) && username !== currentUsername) { 184 const item = document.createElement('div'); 185 item.className = 'suggestion-item'; 186 item.textContent = username; 187 item.addEventListener('click', () => { 188 openDM(username); 189 document.getElementById('dm-modal').style.display = 'none'; 190 document.getElementById('dm-username-input').value = ''; 191 }); 192 userSuggestions.appendChild(item); 193 } 194 }); 195 } 196 197 /** 198 * Open or create a DM conversation 199 */ 200 function openDM(otherUsername) { 201 if (!otherUsername || otherUsername === currentUsername) return; 202 203 if (window.ChatroomBlocks && window.ChatroomBlocks.isBlocked(otherUsername)) { 204 alert('You have blocked this user. Unblock them from their profile to message again.'); 205 return; 206 } 207 208 // Create conversation ID (sorted usernames) 209 const conversationId = [currentUsername, otherUsername].sort().join('_'); 210 211 // If opening the same DM, don't re-setup listener (prevents duplicates) 212 if (currentDM === otherUsername && dmListeners.has(conversationId)) { 213 // Just switch to it 214 switchToDM(otherUsername); 215 return; 216 } 217 218 currentDM = otherUsername; 219 220 // Save to localStorage for persistence 221 try { 222 localStorage.setItem('chatroom_current_dm', otherUsername); 223 const dmHistory = JSON.parse(localStorage.getItem('chatroom_dm_history') || '[]'); 224 if (!dmHistory.includes(otherUsername)) { 225 dmHistory.unshift(otherUsername); 226 // Keep only last 20 DMs 227 if (dmHistory.length > 20) { 228 dmHistory.pop(); 229 } 230 localStorage.setItem('chatroom_dm_history', JSON.stringify(dmHistory)); 231 } 232 } catch (e) { 233 console.error('Error saving DM state:', e); 234 } 235 236 // Update UI 237 updateDMList(); 238 switchToDM(otherUsername); 239 240 // Setup message listener for this DM 241 setupDMListener(conversationId, otherUsername); 242 243 console.log('ð Opened DM with:', otherUsername); 244 } 245 246 /** 247 * Setup listener for a DM conversation (lazy: uses cache when valid) 248 */ 249 function setupDMListener(conversationId, otherUsername) { 250 // Remove old listener if exists 251 if (dmListeners.has(conversationId)) { 252 const oldRef = dmListeners.get(conversationId); 253 oldRef.off('child_added'); 254 dmListeners.delete(conversationId); 255 } 256 257 displayedMessageIds.clear(); 258 loadingOlderDM = false; 259 hasMoreOlderDM = true; 260 261 const oldIndicator = document.getElementById('load-older-indicator'); 262 if (oldIndicator) oldIndicator.remove(); 263 264 const messagesRef = dmsRef.child(conversationId).child('messages'); 265 266 // Check cache 267 const cached = dmCache.get(conversationId); 268 const now = Date.now(); 269 const cacheValid = cached && (now - cached.lastFetchTime) < DM_CACHE_TTL; 270
271 if (cacheValid && cached.messages.length > 0) { 272 const messagesObj = {}; 273 cached.messages.forEach(m => { messagesObj[m.id || m.timestamp] = m; }); 274 displayDMMessages(messagesObj, otherUsername); 275 setupDMRealtimeListener(messagesRef, conversationId, otherUsername, cached.newestTimestamp); 276 setupDMScrollToLoadOlder(conversationId, otherUsername, cached.oldestTimestamp); 277 } else { 278 fetchAndDisplayDMMessages(messagesRef, conversationId, otherUsername); 279 } 280 } 281 282 /** 283 * Fetch initial DM messages from Firebase 284 */ 285 function fetchAndDisplayDMMessages(messagesRef, conversationId, otherUsername) { 286 let lastLoadedTimestamp = 0; 287 let initialLoadComplete = false; 288 289 messagesRef.orderByChild('timestamp').limitToLast(INITIAL_MESSAGE_LIMIT).once('value', (snapshot) => { 290 const messages = snapshot.val(); 291 let oldestTimestamp = 0; 292 293 if (messages) { 294 const messagesArray = Object.entries(messages || {}) 295 .map(([id, m]) => ({ ...m, id })) // Use Firebase key as id (required for reaction paths) 296 .filter(m => !isMessageEmpty(m)) 297 .sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)); 298 oldestTimestamp = messagesArray.length > 0 ? Math.min(...messagesArray.map(m => m.timestamp || 0)) : 0; 299 lastLoadedTimestamp = messagesArray.length > 0 ? Math.max(...messagesArray.map(m => m.timestamp || 0)) : 0; 300 301 const messagesObj = {}; 302 messagesArray.forEach(m => { messagesObj[m.id] = m; }); 303 displayDMMessages(messagesObj, otherUsername); 304 305 dmCache.set(conversationId, { 306 messages: messagesArray, 307 lastFetchTime: Date.now(), 308 oldestTimestamp, 309 newestTimestamp: lastLoadedTimestamp 310 }); 311 } else { 312 dmCache.set(conversationId, { 313 messages: [], 314 lastFetchTime: Date.now(), 315 oldestTimestamp: 0, 316 newestTimestamp: 0 317 }); 318 } 319 320 initialLoadComplete = true;
321 setupDMRealtimeListener(messagesRef, conversationId, otherUsername, lastLoadedTimestamp); 322 setupDMScrollToLoadOlder(conversationId, otherUsername, oldestTimestamp); 323 scrollToBottom(); 324 }); 325 } 326 327 /** 328 * Real-time listener for new DM messages only 329 */ 330 function setupDMRealtimeListener(messagesRef, conversationId, otherUsername, afterTimestamp) { 331 let lastLoadedTimestamp = afterTimestamp || 0; 332 333 const listener = messagesRef.on('child_added', (snapshot) => { 334 if (!snapshot.exists()) return; 335 const message = snapshot.val(); 336 if (!message) return; 337 message.id = snapshot.key; // Firebase key required for reaction paths 338 if (isMessageEmpty(message)) return; 339 if (message.timestamp && message.timestamp <= lastLoadedTimestamp) return; 340 if (displayedMessageIds.has(message.id)) return; 341 342 const messagesList = document.getElementById('messages-list'); 343 if (messagesList) { 344 const existing = messagesList.querySelector(`[data-message-id="${message.id}"]`); 345 if (existing) { 346 displayedMessageIds.add(message.id); 347 return; 348 } 349 } 350 351 if (message.username === otherUsername || message.username === currentUsername) { 352 const fromOther = message.username === otherUsername; 353 if (fromOther) { 354 callUnread('onIncomingMessage', true); 355 } 356 if (!fromOther || callUnread('isNearBottom')) { 357 callUnread('markMessageAsRead', 'dm_' + otherUsername, message.id); 358 } 359 if (window.ChatroomMessageGrouping && messagesList) { 360 const headEl = window.ChatroomMessageGrouping.getGroupHeadElement(messagesList); 361 const headMeta = window.ChatroomMessageGrouping.getMetaFromElement(headEl); 362 message.grouped = window.ChatroomMessageGrouping.shouldGroupWithHead(headMeta, message); 363 } 364 addDMMessage(message, otherUsername); 365 } 366 lastLoadedTimestamp = Math.max(lastLoadedTimestamp, message.timestamp || 0); 367 368 const cached = dmCache.get(conversationId); 369 if (cached) { 370 cached.messages.push(message); 371 cached.newestTimestamp = lastLoadedTimestamp; 372 } 373 }); 374 375 dmListeners.set(conversationId, messagesRef); 376 } 377 378 /** 379 * Scroll-to-load-older for DMs 380 */ 381 function setupDMScrollToLoadOlder(conversationId, otherUsername, currentOldestTimestamp) { 382 const container = document.getElementById('messages-container'); 383 if (!container || currentDM !== otherUsername) return; 384 385 if (dmScrollLoadHandler) { 386 container.removeEventListener('scroll', dmScrollLoadHandler); 387 } 388 389 let oldestTimestamp = currentOldestTimestamp; 390 391 dmScrollLoadHandler = () => { 392 if (currentDM !== otherUsername) return; 393 if (loadingOlderDM || !hasMoreOlderDM) return; 394 if (container.scrollTop > SCROLL_LOAD_THRESHOLD) return; 395 396 loadingOlderDM = true; 397 showLoadOlderIndicator(true); 398 399 const messagesRef = dmsRef.child(conversationId).child('messages'); 400 messagesRef.orderByChild('timestamp').endAt(oldestTimestamp - 1).limitToLast(LOAD_OLDER_BATCH_SIZE) 401 .once('value', (snapshot) => { 402 const messages = snapshot.val(); 403 showLoadOlderIndicator(false); 404 loadingOlderDM = false; 405 406 if (!messages || Object.keys(messages).length === 0) { 407 hasMoreOlderDM = false; 408 return; 409 } 410 411 const messagesArray = Object.entries(messages || {}) 412 .map(([id, m]) => ({ ...m, id })) // Use Firebase key as id (required for reaction paths) 413 .filter(m => !isMessageEmpty(m)) 414 .sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)); 415 const prevScrollHeight = container.scrollHeight; 416 const prevScrollTop = container.scrollTop; 417 418 const messagesList = document.getElementById('messages-list'); 419 const groupedBatch = window.ChatroomMessageGrouping 420 ? window.ChatroomMessageGrouping.assignFlags(messagesArray) 421 : messagesArray;
422 groupedBatch.forEach((message) => { 423 prependDMMessage(message, otherUsername, messagesList); 424 }); 425 if (window.ChatroomMessageGrouping && messagesList) { 426 window.ChatroomMessageGrouping.reapplyDom(messagesList); 427 } 428 429 requestAnimationFrame(() => { 430 const newScrollHeight = container.scrollHeight; 431 container.scrollTop = prevScrollTop + (newScrollHeight - prevScrollHeight); 432 }); 433 434 oldestTimestamp = Math.min(...messagesArray.map(m => m.timestamp || 0)); 435 if (messagesArray.length < LOAD_OLDER_BATCH_SIZE) hasMoreOlderDM = false; 436 437 const cached = dmCache.get(conversationId); 438 if (cached) { 439 cached.messages = [...messagesArray, ...cached.messages]; 440 cached.oldestTimestamp = oldestTimestamp; 441 } 442 }).catch(err => { 443 console.error('Error loading older DM messages:', err); 444 showLoadOlderIndicator(false); 445 loadingOlderDM = false; 446 }); 447 }; 448 449 container.addEventListener('scroll', dmScrollLoadHandler); 450 } 451 452 function prependDMMessage(message, otherUsername, messagesList) { 453 if (!messagesList) return; 454 const existing = messagesList.querySelector(`[data-message-id="${message.id}"]`); 455 if (existing) return; 456 457 const isMod = window.ChatroomModerator && window.ChatroomModerator.isUserModerator(message.username); 458 const modBadge = isMod ? '<span class="mod-badge" title="Moderator">ð</span>' : ''; 459 const profilePicture = window.ChatroomProfilePicture && window.ChatroomProfilePicture.getProfilePicture(message.username); 460 const avatarDisplay = profilePicture 461 ? `<img src="${escapeHtml(profilePicture)}" alt="${escapeHtml(message.username)}" class="profile-picture-avatar">` 462 : getAvatar(message.username); 463 let imagesHTML = ''; 464 if (message.images && message.images.length > 0) { 465 imagesHTML = '<div class="message-images">'; 466 message.images.forEach((image, i) => { 467 const imageData = typeof image === 'string' ? image : (image.data || image); 468 imagesHTML += `<div class="message-image-container"><img src="${imageData}" alt="Image ${i + 1}" class="message-image" loading="lazy"><a href="${imageData}" target="_blank" rel="noopener noreferrer" class="message-image-link" title="Open">ð</a></div>`; 469 }); 470 imagesHTML += '</div>'; 471 } 472 473 const messageDiv = document.createElement('div'); 474 messageDiv.className = 'message' + (message.grouped ? ' grouped' : ''); 475 const prependDmText = message.text != null ? String(message.text) : ''; 476 if (prependDmText && !(message.images && message.images.length > 0) && window.ChatroomMessageFormatter?.shouldScaleEmojiMessage?.(prependDmText)) { 477 messageDiv.classList.add('message-emoji-only'); 478 } 479 messageDiv.dataset.messageId = message.id; 480 messageDiv.innerHTML = ` 481 <div class="message-avatar">${avatarDisplay}</div> 482 <div class="message-content"> 483 <div class="message-header"> 484 <span class="message-username">${modBadge}${escapeHtml(message.username)}</span> 485 <span class="message-time">${window.ChatroomMessageFormatter ? window.ChatroomMessageFormatter.formatTimestamp(message.timestamp) : formatTime(message.timestamp)}</span> 486 </div> 487 ${message.text ? `<div class="message-text">${window.ChatroomMessageFormatter ? window.ChatroomMessageFormatter.formatMessage(message.text) : escapeHtml(message.text)}</div>` : ''} 488 ${imagesHTML} 489 <div class="message-reactions"></div> 490 </div> 491 `; 492 if (window.ChatroomMessageGrouping) { 493 window.ChatroomMessageGrouping.applyMetaToElement(messageDiv, message); 494 window.ChatroomMessageGrouping.applyGroupedPresentation( 495 messageDiv, 496 !!message.grouped, 497 window.ChatroomSettings?.getSetting('showAvatars') !== false, 498 window.ChatroomSettings?.getSetting('showTimestamps') !== false 499 ); 500 } 501 messagesList.insertBefore(messageDiv, messagesList.firstChild); 502 503 displayedMessageIds.add(message.id); 504 if (window.ChatroomProfilePicture && window.ChatroomProfilePicture.updateAllAvatars) { 505 setTimeout(() => window.ChatroomProfilePicture.updateAllAvatars(), 50); 506 } 507 if (window.ChatroomReactions && window.ChatroomReactions.initMessageReactions) { 508 window.ChatroomReactions.initMessageReactions(messageDiv); 509 } 510 if (window.ChatroomLinkEmbeds?.initMessageEmbeds) { 511 window.ChatroomLinkEmbeds.initMessageEmbeds(messageDiv); 512 } 513 } 514 515 function showLoadOlderIndicator(show) { 516 let indicator = document.getElementById('load-older-indicator'); 517 if (show) { 518 if (!indicator) { 519 indicator = document.createElement('div'); 520 indicator.id = 'load-older-indicator'; 521 indicator.className = 'load-older-indicator'; 522 indicator.textContent = 'Loading older messages...'; 523 const messagesList = document.getElementById('messages-list'); 524 if (messagesList) messagesList.insertBefore(indicator, messagesList.firstChild); 525 } 526 indicator.style.display = 'block'; 527 } else if (indicator) { 528 indicator.style.display = 'none'; 529 } 530 } 531 532 /** 533 * Send a DM 534 */ 535 function sendDM(otherUsername, text, images = []) { 536 if ((!text || !text.trim()) && (!images || images.length === 0)) return; 537 if (!otherUsername) return; 538 539 const conversationId = [currentUsername, otherUsername].sort().join('_'); 540 const messagesRef = dmsRef.child(conversationId).child('messages'); 541 542 const message = { 543 username: currentUsername, 544 text: text ? text.trim() : '', 545 timestamp: Date.now(), 546 id: Date.now().toString() 547 }; 548 549 // Add images if any 550 if (images && images.length > 0) { 551 message.images = images; 552 } 553 554 if (window.ChatroomUserStats && window.ChatroomUserStats.incrementMessageCount) { 555 window.ChatroomUserStats.incrementMessageCount(currentUsername); 556 } 557
558 messagesRef.push(message).catch(error => { 559 console.error('Error sending DM:', error); 560 }); 561 562 // Update conversation metadata 563 dmsRef.child(conversationId).child('metadata').set({ 564 participants: [currentUsername, otherUsername].sort(), 565 lastMessage: message.timestamp, 566 lastMessageFrom: currentUsername 567 }).then(() => { 568 // Update DM list after sending 569 updateDMList(); 570 }); 571 572 } 573 574 /** 575 * Display DM messages 576 */ 577 function displayDMMessages(messages, otherUsername) { 578 const messagesList = document.getElementById('messages-list'); 579 if (!messagesList) return; 580 581 // Clear the list and reset tracking 582 messagesList.innerHTML = ''; 583 displayedMessageIds.clear(); 584 suppressDMAnimations = true; 585 586 const messagesArray = Object.entries(messages || {}) 587 .map(([id, m]) => ({ ...m, id })) // Use Firebase key as id (required for reaction paths) 588 .filter(m => !isMessageEmpty(m)) 589 .sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)); 590 591 // Remove duplicates by message ID before displaying 592 const uniqueMessages = []; 593 const seenIds = new Set(); 594 595 messagesArray.forEach(message => { 596 // Skip if we've already seen this message ID 597 if (!message.id || seenIds.has(message.id)) { 598 return; 599 } 600 seenIds.add(message.id); 601 uniqueMessages.push(message); 602 }); 603 604 const groupedMessages = window.ChatroomMessageGrouping 605 ? window.ChatroomMessageGrouping.assignFlags(uniqueMessages) 606 : uniqueMessages; 607 608 // Display unique messages 609 groupedMessages.forEach(message => { 610 addDMMessage(message, otherUsername); 611 // Mark each message as read immediately 612 if (window.ChatroomUnreadCount) { 613 const key = `dm_${otherUsername}`; 614 window.ChatroomUnreadCount.markMessageAsRead(key, message.id); 615 } 616 617 // Also mark notification as read in localStorage immediately 618 if (currentUsername && message.id) { 619 const conversationId = [currentUsername, otherUsername].sort().join('_'); 620 const notifiedKey = `chatroom_notified_${currentUsername}`; 621 try { 622 const notified = JSON.parse(localStorage.getItem(notifiedKey) || '{}'); 623 const notificationKey = `dm_${conversationId}_${message.id}`; 624 notified[notificationKey] = true; 625 localStorage.setItem(notifiedKey, JSON.stringify(notified)); 626 } catch (e) { 627 console.error('Error marking notification as read:', e); 628 } 629 } 630 }); 631 632 suppressDMAnimations = false; 633 634 // Mark all messages as read when viewing DM 635 if (window.ChatroomUnreadCount) { 636 const key = `dm_${otherUsername}`; 637 window.ChatroomUnreadCount.markAllAsRead(key); 638 } 639 640 // Track unread count for this DM 641 if (window.ChatroomUnreadCount) { 642 window.ChatroomUnreadCount.trackDM(otherUsername); 643 } 644 645 // Update all avatars after displaying all messages 646 setTimeout(() => { 647 if (window.ChatroomProfilePicture && window.ChatroomProfilePicture.updateAllAvatars) { 648 window.ChatroomProfilePicture.updateAllAvatars(); 649 } 650 }, 100); 651 652 scrollToBottom(); 653 // Delayed scrolls to account for async embeds loading 654 if (window.ChatroomSettings?.getSetting?.('autoScrollNewMessages') !== false) { 655 [300, 800, 1500, 2500].forEach(delay => { 656 setTimeout(() => { 657 if (window.ChatroomSettings?.getSetting?.('autoScrollNewMessages') !== false) { 658 scrollToBottom(); 659 } 660 }, delay); 661 }); 662 } 663 } 664 665 /** 666 * Add a single DM message to display 667 */ 668 function addDMMessage(message, otherUsername) { 669 const messagesList = document.getElementById('messages-list'); 670 if (!messagesList) return; 671 672 if (isMessageEmpty(message)) return; 673 674 if (window.ChatroomBlocks && window.ChatroomBlocks.shouldHideMe
674ssage(message.username)) { 675 return; 676 } 677 678 // Skip if message already displayed 679 if (displayedMessageIds.has(message.id)) { 680 return; 681 } 682 683 // Check if message already exists in DOM 684 const existing = messagesList.querySelector(`[data-message-id="${message.id}"]`); 685 if (existing) { 686 displayedMessageIds.add(message.id); 687 return; 688 } 689 690 // Additional check: look for messages with same content and timestamp (duplicate detection) 691 const allMessages = messagesList.querySelectorAll('.message'); 692 for (let msgEl of allMessages) { 693 const msgId = msgEl.dataset.messageId; 694 const msgTimeEl = msgEl.querySelector('.message-time'); 695 const msgTextEl = msgEl.querySelector('.message-text'); 696 const msgUsernameEl = msgEl.querySelector('.message-username'); 697 698 if (msgId === message.id || 699 (msgTimeEl && msgTextEl && msgUsernameEl && 700 msgTimeEl.textContent.trim() === (window.ChatroomMessageFormatter ? window.ChatroomMessageFormatter.formatTimestamp(message.timestamp) : formatTime(message.timestamp)) && 701 msgTextEl.textContent.trim() === (message.text || '').trim() && 702 msgUsernameEl.textContent.replace('ð', '').trim() === message.username)) { 703 // This is a duplicate 704 displayedMessageIds.add(message.id); 705 return; 706 } 707 } 708 709 // Mark as displayed 710 displayedMessageIds.add(message.id); 711 712 if (message.grouped == null && window.ChatroomMessageGrouping) { 713 const headEl = window.ChatroomMessageGrouping.getGroupHeadElement(messagesList); 714 const headMeta = window.ChatroomMessageGrouping.getMetaFromElement(headEl); 715 message.grouped = window.ChatroomMessageGrouping.shouldGroupWithHead(headMeta, message); 716 } 717 718 // Check if user is moderator 719 const isMod = window.ChatroomModerator && window.ChatroomModerator.isUserModerator(message.username); 720 const modBadge = isMod ? '<span class="mod-badge" title="Moderator">ð</span>' : ''; 721 722 const messageDiv = document.createElement('div'); 723 messageDiv.className = 'message'; 724 if (message.grouped) { 725 messageDiv.classList.add('grouped'); 726 } 727 const dmText = message.text != null ? String(message.text) : ''; 728 if (dmText && !(message.images && message.images.length > 0) && window.ChatroomMessageFormatter?.shouldScaleEmojiMessage?.(dmText)) { 729 messageDiv.classList.add('message-emoji-only'); 730 } 731 messageDiv.dataset.messageId = message.id; 732 733 // Get profile picture if available 734 const profilePicture = window.ChatroomProfilePicture && window.ChatroomProfilePicture.getProfilePicture(message.username); 735 const avatarDisplay = profilePicture 736 ? `<img src="${escapeHtml(profilePicture)}" alt="${escapeHtml(message.username)}" class="profile-picture-avatar">` 737 : getAvatar(message.username); 738 739 // Build images HTML if present 740 let imagesHTML = ''; 741 if (message.images && message.images.length > 0) { 742 imagesHTML = '<div class="message-images">'; 743 message.images.forEach((image, index) => { 744 const imageData = typeof image === 'string' ? image : (image.data || image); 745 // Base64 data URLs are safe for src attributes 746 imagesHTML += ` 747 <div class="message-image-container"> 748 <img src="${imageData}" alt="Image ${index + 1}" class="message-image" loading="lazy"> 749 <a href="${imageData}" target="_blank" rel="noopener noreferrer" class="message-image-link" title="Open image in new tab">ð</a> 750 </div> 751 `; 752 }); 753 imagesHTML += '</div>'; 754 } 755 756 messageDiv.innerHTML = ` 757 <div class="message-avatar">${avatarDisplay}</div> 758 <div class="message-content"> 759 <div class="message-header">
760 <span class="message-username">${modBadge}${escapeHtml(message.username)}</span> 761 ${window.ChatroomMessageMeta && window.ChatroomMessageMeta.editedLabelHtml ? window.ChatroomMessageMeta.editedLabelHtml(message) : ''} 762 <span class="message-time">${window.ChatroomMessageFormatter ? window.ChatroomMessageFormatter.formatTimestamp(message.timestamp) : formatTime(message.timestamp)}</span> 763 </div> 764 ${message.text ? `<div class="message-text">${window.ChatroomMessageFormatter ? window.ChatroomMessageFormatter.formatMessage(message.text) : escapeHtml(message.text)}</div>` : ''} 765 ${imagesHTML} 766 <div class="message-reactions"></div> 767 </div> 768 `; 769 770 if (window.ChatroomMessageMeta && window.ChatroomMessageMeta.applyEditedLabel) { 771 window.ChatroomMessageMeta.applyEditedLabel(messageDiv, message); 772 } 773 if (window.ChatroomMessageGrouping && window.ChatroomMessageGrouping.applyMetaToElement) { 774 window.ChatroomMessageGrouping.applyMetaToElement(messageDiv, message); 775 window.ChatroomMessageGrouping.applyGroupedPresentation( 776 messageDiv, 777 !!message.grouped, 778 window.ChatroomSettings?.getSetting('showAvatars') !== false, 779 window.ChatroomSettings?.getSetting('showTimestamps') !== false 780 ); 781 } 782 783 messagesList.appendChild(messageDiv); 784 785 if (!suppressDMAnimations && window.ChatroomMessageAnimations) { 786 window.ChatroomMessageAnimations.playAfterInsert(messageDiv); 787 } 788 789 // Mark message as read if viewing this DM (for new messages coming in) 790 if (currentDM === otherUsername) { 791 if (window.ChatroomUnreadCount) { 792 const key = `dm_${otherUsername}`; 793 window.ChatroomUnreadCount.markMessageAsRead(key, message.id); 794 } 795 796 // Also mark notification as read in localStorage immediately 797 if (currentUsername && message.id) { 798 const conversationId = [currentUsername, otherUsername].sort().join('_'); 799 const notifiedKey = `chatroom_notified_${currentUsername}`; 800 try { 801 const notified = JSON.parse(localStorage.getItem(notifiedKey) || '{}'); 802 const notificationKey = `dm_${conversationId}_${message.id}`; 803 notified[notificationKey] = true; 804 localStorage.setItem(notifiedKey, JSON.stringify(notified)); 805 } catch (e) { 806 console.error('Error marking notification as read:', e); 807 } 808 } 809 } 810 811 // Update avatar with profile picture if available (after DOM insertion) 812 setTimeout(() => { 813 if (window.ChatroomProfilePicture && window.ChatroomProfilePicture.updateAllAvatars) { 814 window.ChatroomProfilePicture.updateAllAvatars(); 815 } 816 }, 50); 817 818 // Initialize reactions 819 if (window.ChatroomReactions && window.ChatroomReactions.initMessageReactions) { 820 window.ChatroomReactions.initMessageReactions(messageDiv); 821 } 822 // Initialize link embeds 823 if (window.ChatroomLinkEmbeds?.initMessageEmbeds) { 824 window.ChatroomLinkEmbeds.initMessageEmbeds(messageDiv); 825 } 826 827 // Add moderator controls if current user is moderator 828 if (window.ChatroomMessageActions && window.ChatroomMessageActions.setupMessageHover) { 829 window.ChatroomMessageActions.setupMessageHover(messageDiv); 830 } 831 832 scrollToBottom(); 833 } 834 835 /** 836 * Switch UI to DM view 837 */ 838 function switchToDM(otherUsername) { 839 // Hide server channel list, show recent DMs in channels column 840 const serverChannelsCategory = document.getElementById('server-channels-category'); 841 const voiceChatCategory = document.getElementById('voice-chat-category'); 842 const dmCallCategory = document.getElementById('dm-call-category'); 843 const recentDmsCategory = document.getElementById('recent-dms-category'); 844 if (serverChannelsCategory) serverChannelsCategory.style.display = 'none'; 845 if (voiceChatCategory) voiceChatCategory.style.display = 'none'; 846 if (recentDmsCategory) recentDmsCategory.style.display = 'block'; 847 if (dmCallCategory) { 848 const inDmCall = window.ChatroomDMCalls && window.ChatroomDMCalls.getIsInCall(); 849 const call = window.ChatroomDMCalls && window.ChatroomDMCalls.getCurrentCall(); 850 dmCallCategory.style.display = (inDmCall && call && call.username === otherUsername) ? 'block' : 'none'; 851 } 852 853 // Update header 854 const channelIcon = document.getElementById('channel-icon-header'); 855 const channelName = document.getElementById('current-channel-name'); 856 const channelTopic = document.getElementById('channel-topic'); 857 858 if (channelIcon) channelIcon.textContent = 'ð'; 859 if (channelName) channelName.textContent = otherUsername; 860 if (channelTopic) channelTopic.textContent = 'Direct message'; 861 862 callUnread('markAllAsRead', 'dm_' + otherUsername); 863 callUnread('resetBanner'); 864 if (window.ChatroomMessageMeta) { 865 if (window.ChatroomMessageMeta.bindDM) { 866 window.ChatroomMessageMeta.bindDM(otherUsername); 867 } else if (window.ChatroomMessageMeta.teardownChannelMeta) { 868 window.ChatroomMessageMeta.teardownChannelMeta(); 869 } 870 } 871 872 // Add/update call button in header 873 setupCallButton(otherUsername); 874 875 // Update input placeholder 876 const messageInput = document.getElementById('message-input'); 877 if (messageInput) { 878 messageInput.placeholder = `Message @${otherUsername}`; 879 } 880 881 // Update active state 882 document.querySelectorAll('.channel-item').forEach(item => { 883 item.classList.remove('active'); 884 }); 885 document.querySelectorAll('.dm-item, .nav-dm-item').forEach(item => { 886 item.classList.remove('active'); 887 if (item.dataset.dm === otherUsername) { 888 item.classList.add('active'); 889 } 890 }); 891 892 // Populate channels column with recent DMs (sorted by activity) 893 updateChannelsColumnDMList(); 894 895 if (window.ChatroomDMCalls && window.ChatroomDMCalls.refreshUi) { 896 window.ChatroomDMCalls.refreshUi(); 897 } 898 899 // Clear messages 900 const messagesList = document.getElementById('messages-list'); 901 if (messagesList) messagesList.innerHTML = ''; 902 } 903 904 /** 905 * Setup call button in DM header 906 */ 907 function setupCallButton(otherUsername) { 908 const headerRight = document.querySelector('.header-right'); 909 if (!headerRight) return; 910 911 // Remove existing call button if any 912 const existingCallBtn = document.getElementById('dm-call-btn'); 913 if (existingCallBtn) { 914 existingCallBtn.remove(); 915 } 916 917 // Remove existing call status if any 918 const existingStatus = document.getElementById('dm-call-status'); 919 if (existingStatus) { 920 existingStatus.remove(); 921 } 922 923 // Only show call button if not in a call with someone else 924 const isInCall = window.ChatroomDMCalls && window.ChatroomDMCalls.getIsInCall(); 925 const currentCall = window.ChatroomDMCalls && window.ChatroomDMCalls.getCurrentCall(); 926 927 if (isInCall && currentCall && currentCall.username !== otherUsername) { 928 // In a call with someone else, don't show button 929 return; 930 } 931 932 // Create call button 933 const callBtn = document.createElement('button'); 934 callBtn.id = 'dm-call-btn'; 935 callBtn.className = 'header-btn cr-icon-btn'; 936 callBtn.title = isInCall ? 'End call' : 'Call'; 937 if (window.ChatroomIcons) window.ChatroomIcons.set(callBtn, 'phone'); 938 939 if (isInCall && currentCall && currentCall.username === otherUsername) { 940 callBtn.classList.add('active'); 941 } 942 943 // Add click handler 944 callBtn.addEventListener('click', () => { 945 if (isInCall && currentCall && currentCall.username === otherUsername) { 946 // End call 947 if (window.ChatroomDMCalls) { 948 window.ChatroomDMCalls.endCall(); 949 } 950 } else { 951 // Start call (with confirmation) 952 if (window.ChatroomDMCalls) { 953 window.ChatroomDMCalls.startCallWithConfirmation(otherUsername); 954 } 955 } 956 }); 957 958 // Insert before settings button 959 const settingsBtn = document.getElementById('settings-btn'); 960 if (settingsBtn) { 961 headerRight.insertBefore(callBtn, settingsBtn); 962 } else { 963 headerRight.appendChild(callBtn); 964 } 965 966 // Create call status element (hidden by default) 967 const callStatus = document.createElement('span'); 968 callStatus.id = 'dm-call-status'; 969 callStatus.style.display = 'none'; 970 callStatus.style.fontSize = '12px'; 971 callStatus.style.color = 'var(--text-4)'; 972 callStatus.style.marginRight = '8px'; 973 974 if (isInCall && currentCall && currentCall.username === otherUsername) { 975 callStatus.textContent = `Calling ${otherUsername}...`; 976 callStatus.style.display = 'block'; 977 } 978 979 // Insert call status before call button
980 headerRight.insertBefore(callStatus, callBtn); 981 } 982 983 /** 984 * Update the recent DMs list in the channels column (when in DM view) 985 */ 986 function updateChannelsColumnDMList() { 987 if (!currentUsername || !currentDM) return; 988 const dmList = document.getElementById('channels-column-dm-list'); 989 if (!dmList) return; 990 991 // Same sorting logic as updateDMList 992 let dmHistory = []; 993 try { 994 dmHistory = JSON.parse(localStorage.getItem('chatroom_dm_history') || '[]'); 995 } catch (e) { /* ignore */ } 996 997 dmsRef.once('value', (snapshot) => { 998 const conversations = snapshot.val(); 999 const dmMap = new Map(); 1000 if (conversations) { 1001 Object.keys(conversations).forEach(conversationId => { 1002 const conversation = conversations[conversationId]; 1003 if (conversation.metadata && conversation.metadata.participants) { 1004 const participants = conversation.metadata.participants; 1005 if (participants.includes(currentUsername)) { 1006 const otherUser = participants.find(u => u !== currentUsername); 1007 if (otherUser) { 1008 dmMap.set(otherUser, conversation.metadata.lastMessage || 0); 1009 } 1010 } 1011 } 1012 }); 1013 }
1014 dmHistory.forEach(username => { 1015 if (!dmMap.has(username)) dmMap.set(username, 0); 1016 }); 1017 const sortedDMs = Array.from(dmMap.entries()).sort((a, b) => { 1018 if (a[1] > 0 && b[1] > 0) return b[1] - a[1]; 1019 if (a[1] > 0 && b[1] === 0) return -1; 1020 if (a[1] === 0 && b[1] > 0) return 1; 1021 const aIndex = dmHistory.indexOf(a[0]); 1022 const bIndex = dmHistory.indexOf(b[0]); 1023 if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex; 1024 if (aIndex !== -1) return -1; 1025 if (bIndex !== -1) return 1; 1026 return 0; 1027 }); 1028 1029 dmList.innerHTML = ''; 1030 sortedDMs.forEach(([otherUser]) => { 1031 const dmItem = document.createElement('div'); 1032 dmItem.className = 'dm-item'; 1033 if (currentDM === otherUser) dmItem.classList.add('active'); 1034 dmItem.dataset.dm = otherUser; 1035 dmItem.innerHTML = ` 1036 <span class="channel-prefix">ð</span> 1037 <span class="channel-name">${escapeHtml(otherUser)}</span> 1038 `; 1039 dmItem.addEventListener('click', () => openDM(otherUser)); 1040 dmList.appendChild(dmItem); 1041 }); 1042 }); 1043 } 1044 1045 /** 1046 * Update DM list in sidebar 1047 */ 1048 function updateDMList() { 1049 if (!currentUsername) return; 1050 1051 // Try navigation column DM list first, fallback to channels column 1052 const dmList = document.getElementById('dm-list') || document.querySelector('.nav-dm-list'); 1053 if (!dmList) return; 1054 1055 // Get DM history from localStorage 1056 let dmHistory = []; 1057 try { 1058 dmHistory = JSON.parse(localStorage.getItem('chatroom_dm_history') || '[]'); 1059 } catch (e) { 1060 console.error('Error loading DM history:', e); 1061 } 1062 1063 // Get all conversations from Firebase 1064 dmsRef.once('value', (snapshot) => { 1065 const conversations = snapshot.val(); 1066 const dmMap = new Map(); 1067 1068 // Find all conversations this user is part of 1069 if (conversations) { 1070 Object.keys(conversations).forEach(conversationId => { 1071 const conversation = conversations[conversationId]; 1072 if (conversation.metadata && conversation.metadata.participants) { 1073 const participants = conversation.metadata.participants; 1074 if (participants.includes(currentUsername)) { 1075 const otherUser = participants.find(u => u !== currentUsername); 1076 if (otherUser) { 1077 dmMap.set(otherUser, conversation.metadata.lastMessage || 0); 1078 } 1079 } 1080 } 1081 }); 1082 } 1083 1084 // Merge with history (include DMs from history even if no recent messages)
1085 dmHistory.forEach(username => { 1086 if (!dmMap.has(username)) { 1087 dmMap.set(username, 0); 1088 } 1089 }); 1090 1091 // Sort by last message time, then by history order 1092 const sortedDMs = Array.from(dmMap.entries()).sort((a, b) => { 1093 // If both have messages, sort by timestamp 1094 if (a[1] > 0 && b[1] > 0) { 1095 return b[1] - a[1]; 1096 } 1097 // If one has messages and one doesn't, prioritize the one with messages 1098 if (a[1] > 0 && b[1] === 0) return -1; 1099 if (a[1] === 0 && b[1] > 0) return 1; 1100 // Both have no messages, sort by history order 1101 const aIndex = dmHistory.indexOf(a[0]); 1102 const bIndex = dmHistory.indexOf(b[0]); 1103 if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex; 1104 if (aIndex !== -1) return -1; 1105 if (bIndex !== -1) return 1; 1106 return 0; 1107 }); 1108 1109 dmList.innerHTML = ''; 1110 1111 sortedDMs.forEach(([otherUser, lastMessage]) => { 1112 const dmItem = document.createElement('div'); 1113 // Use nav-dm-item if in navigation column, otherwise dm-item 1114 const isNavColumn = dmList.classList.contains('nav-dm-list'); 1115 dmItem.className = isNavColumn ? 'nav-dm-item' : 'dm-item'; 1116 if (currentDM === otherUser) { 1117 dmItem.classList.add('active'); 1118 } 1119 dmItem.dataset.dm = otherUser; 1120 dmItem.title = otherUser; // Tooltip for navigation column 1121 1122 if (isNavColumn) { 1123 // Navigation column: show profile picture or avatar/emoji 1124 const profilePicture = window.ChatroomProfilePicture && window.ChatroomProfilePicture.getProfilePicture(otherUser); 1125 if (profilePicture) { 1126 dmItem.innerHTML = `<span><img src="${escapeHtml(profilePicture)}" alt="${escapeHtml(otherUser)}" class="profile-picture-avatar"></span>`; 1127 } else { 1128 dmItem.innerHTML = `<span>${getAvatar(otherUser)}</span>`; 1129 } 1130 } else { 1131 const inVoice = 1132 window.ChatroomDMCalls && 1133 window.ChatroomDMCalls.getIsInCall() && 1134 window.ChatroomDMCalls.getCurrentCall() && 1135 window.ChatroomDMCalls.getCurrentCall().username === otherUser; 1136 dmItem.innerHTML = 1137 '<span class="dm-icon">ð</span>' + 1138 '<span class="dm-name">' + escapeHtml(otherUser) + '</span>' + 1139 (inVoice 1140 ? '<span class="dm-in-voice-line">' + 1141 (window.ChatroomIcons ? window.ChatroomIcons.icon('phone') : 'ð') + 1142 ' In voice</span>' 1143 : ''); 1144 if (inVoice) { 1145 dmItem.classList.add('dm-in-voice'); 1146 } 1147 } 1148 1149 dmItem.addEventListener('click', () => { 1150 openDM(otherUser); 1151 }); 1152 dmList.appendChild(dmItem); 1153 1154 // Track unread count for this DM 1155 if (window.ChatroomUnreadCount) { 1156 window.ChatroomUnreadCount.trackDM(otherUser); 1157 } 1158 }); 1159 1160 // Update all avatars after DM list is populated 1161 setTimeout(() => { 1162 if (window.ChatroomProfilePicture && window.ChatroomProfilePicture.updateAllAvatars) { 1163 window.ChatroomProfilePicture.updateAllAvatars(); 1164 } 1165 }, 100); 1166 1167 // If in DM view, also update the channels column DM list 1168 if (currentDM) { 1169 updateChannelsColumnDMList(); 1170 } 1171 }); 1172 } 1173 1174 /** 1175 * Get avatar emoji for username 1176 */ 1177 function getAvatar(username) { 1178 const avatars = ['ð¾', 'ð¶', 'ð', 'ð¦´', 'ð¾', 'ð§¸', 'â', 'ð«', 'â¨', 'ð']; 1179 const hash = username.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); 1180 return avatars[hash % avatars.length]; 1181 } 1182 1183 /** 1184 * Format timestamp 1185 */ 1186 function formatTime(timestamp) { 1187 const date = new Date(timestamp); 1188 if (Number.isNaN(date.getTime())) return ''; 1189 const now = new Date(); 1190 const diff = now - date; 1191 1192 if (diff < 60000) return 'just now'; 1193 if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago'; 1194 if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago'; 1195 1196 return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 1197 } 1198 1199 /** 1200 * Escape HTML 1201 */ 1202 function escapeHtml(text) { 1203 const div = document.createElement('div'); 1204 div.textContent = text; 1205 return div.innerHTML; 1206 } 1207 1208 /** 1209 * Scroll to bottom 1210 */ 1211 function scrollToBottom() { 1212 const container = document.getElementById('messages-container'); 1213 if (container) { 1214 container.scrollTop = container.scrollHeight; 1215 } 1216 } 1217 1218 /** 1219 * Get current DM 1220 */ 1221 function getCurrentDM() { 1222 return currentDM;
1223 } 1224 1225 // Initialize on load 1226 if (document.readyState === 'loading') { 1227 document.addEventListener('DOMContentLoaded', init); 1228 } else { 1229 init(); 1230 } 1231 1232 /** 1233 * Clear current DM (switch back to channel) 1234 */ 1235 function clearCurrentDM() { 1236 currentDM = null; 1237 1238 // Remove from localStorage 1239 try { 1240 localStorage.removeItem('chatroom_current_dm'); 1241 } catch (e) { 1242 console.error('Error clearing DM state:', e); 1243 } 1244 1245 // Remove all DM listeners 1246 dmListeners.forEach((ref) => { 1247 ref.off('child_added'); 1248 }); 1249 dmListeners.clear(); 1250 1251 // Update UI - unhighlight all DM icons immediately (nav and channels column) 1252 document.querySelectorAll('.dm-item, .nav-dm-item').forEach(item => { 1253 item.classList.remove('active'); 1254 }); 1255 1256 // Remove DM call button and status immediately 1257 const callBtn = document.getElementById('dm-call-btn'); 1258 const callStatus = document.getElementById('dm-call-status'); 1259 if (callBtn) callBtn.remove(); 1260 if (callStatus) callStatus.remove(); 1261 1262 // Show server channels, hide recent DMs 1263 const serverChannelsCategory = document.getElementById('server-channels-category'); 1264 const voiceChatCategory = document.getElementById('voice-chat-category'); 1265 const dmCallCategory = document.getElementById('dm-call-category'); 1266 const recentDmsCategory = document.getElementById('recent-dms-category'); 1267 const dmCallStage = document.getElementById('dm-call-stage'); 1268 const chatArea = document.querySelector('.chat-area'); 1269 if (serverChannelsCategory) serverChannelsCategory.style.display = ''; 1270 if (dmCallCategory) dmCallCategory.style.display = 'none'; 1271 if (dmCallStage) dmCallStage.hidden = true; 1272 if (chatArea) chatArea.classList.remove('dm-call-active'); 1273 // Restore voice chat visibility based on connection state 1274 if (voiceChatCategory) { 1275 const isConnected = window.ChatroomVoiceChat && window.ChatroomVoiceChat.getStatus && window.ChatroomVoiceChat.getStatus().connected; 1276 voiceChatCategory.style.display = isConnected ? 'block' : 'none'; 1277 } 1278 if (recentDmsCategory) recentDmsCategory.style.display = 'none'; 1279 } 1280 1281 /** 1282 * Restore last open DM from localStorage 1283 */ 1284 function restoreLastDM() { 1285 if (!currentUsername) { 1286 setTimeout(restoreLastDM, 500); 1287 return; 1288 } 1289 1290 try { 1291 const lastDM = localStorage.getItem('chatroom_current_dm'); 1292 if (lastDM && lastDM !== currentUsername) { 1293 // Verify user still exists in DM history 1294 const dmHistory = JSON.parse(localStorage.getItem('chatroom_dm_history') || '[]'); 1295 if (dmHistory.includes(lastDM)) { 1296 // Small delay to ensure UI is ready 1297 setTimeout(() => { 1298 openDM(lastDM); 1299 console.log('ð Restored last DM:', lastDM); 1300 }, 800); 1301 } else { 1302 // Clear invalid DM state 1303 localStorage.removeItem('chatroom_current_dm'); 1304 } 1305 } 1306 } catch (e) { 1307 console.error('Error restoring last DM:', e); 1308 } 1309 } 1310 1311 function refreshDMMessagesLayout() { 1312 if (!currentDM || !currentUsername) return; 1313 const conversationId = [currentUsername, currentDM].sort().join('_'); 1314 const cached = dmCache.get(conversationId); 1315 if (!cached || !cached.messages || !cached.messages.length) return; 1316 1317 const scrollContainer = document.getElementById('messages-container'); 1318 const scrollTop = scrollContainer ? scrollContainer.scrollTop : 0; 1319 const atBottom = scrollContainer 1320 ? scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollC
1320ontainer.clientHeight < 80 1321 : false; 1322 1323 const messagesObj = {}; 1324 cached.messages.forEach(function (m) { 1325 messagesObj[m.id || m.timestamp] = m; 1326 }); 1327 displayDMMessages(messagesObj, currentDM); 1328 1329 if (scrollContainer) { 1330 if (atBottom && window.ChatroomSettings?.getSetting?.('autoScrollNewMessages') !== false) { 1331 scrollToBottom(); 1332 } else { 1333 scrollContainer.scrollTop = scrollTop; 1334 } 1335 } 1336 } 1337 1338 function removeMessageFromCache(messageId) { 1339 if (!messageId || !currentUsername || !currentDM) return; 1340 const conversationId = [currentUsername, currentDM].sort().join('_'); 1341 const cached = dmCache.get(conversationId); 1342 if (!cached || !cached.messages || !cached.messages.length) return; 1343 cached.messages = cached.messages.filter(function (m) { 1344 return m && m.id !== messageId; 1345 }); 1346 displayedMessageIds.delete(messageId); 1347 } 1348 1349 document.addEventListener('chatroom-messages-layout-refresh', refreshDMMessagesLayout); 1350 1351 return { 1352 openDM, 1353 sendDM, 1354 getCurrentDM, 1355 clearCurrentDM, 1356 restoreLastDM, 1357 updateDMList, 1358 setupCallButton, 1359 refreshDMMessagesLayout, 1360 removeMessageFromCache, 1361 init 1362 }; 1363})();
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.