1/** 2 * Direct Message Call System 3 * Handles 1-on-1 voice calls between users via DMs 4 */ 5 6window.ChatroomDMCalls = (function() { 7 'use strict'; 8 9 let currentCall = null; // { username, connection, localStream, isIncoming } 10 let currentUsername = null; 11 let peer = null; // PeerJS instance 12 let callListeners = new Map(); 13 let isInCall = false; 14 let callPhase = null; // 'ringing' | 'connected' 15 let callStartedAt = null; 16 let callTimerInterval = null; 17 let isDmCallMuted = false; 18 let dmCallControlsBound = false; 19 20 /** 21 * Initialize DM call system 22 */ 23 function init() { 24 if (typeof firebase === 'undefined' || !firebase.database) { 25 console.error('Firebase not available for DM calls'); 26 return false; 27 } 28 29 // Wait for username 30 const checkUsername = setInterval(() => { 31 if (window.__sharedUsername) { 32 currentUsername = window.__sharedUsername.get(); 33 if (currentUsername) { 34 clearInterval(checkUsername); 35 setupPeerJS(); 36 setupFirebaseListeners(); 37 } 38 } 39 }, 100); 40 41 setTimeout(() => { 42 clearInterval(checkUsername); 43 if (!currentUsername) { 44 currentUsername = 'guest_' + Date.now(); 45 setupPeerJS(); 46 setupFirebaseListeners(); 47 } 48 }, 5000); 49 50 return true; 51 } 52 53 /** 54 * Setup PeerJS instance for DM calls 55 */ 56 function setupPeerJS() { 57 if (typeof Peer === 'undefined') { 58 console.error('ð DM Calls: PeerJS not loaded'); 59 return; 60 } 61 62 // Always try to reuse voice chat's PeerJS instance (same peer ID = same username) 63 // This ensures both users can call each other using the same peer IDs 64 const tryGetPeer = setInterval(() => { 65 if (window.ChatroomVoiceChat && window.ChatroomVoiceChat.getPeer) { 66 const voiceChatPeer = window.ChatroomVoiceChat.getPeer(); 67 if (voiceChatPeer) { 68 peer = voiceChatPeer; 69 clearInterval(tryGetPeer); 70 console.log('ð DM Calls: Reusing voice chat PeerJS instance'); 71 72 // If peer is already open, set up handler immediately 73 if (peer.open) { 74 setupCallHandler(); 75 } else { 76 // Wait for peer to open 77 peer.on('open', () => { 78 console.log('ð DM Calls: Voice chat PeerJS opened, setting up call handler'); 79 setupCallHandler(); 80 }); 81 } 82 return; 83 } 84 } 85 }, 100); 86 87 // Don't create a fallback - we MUST use the voice chat peer to ensure peer IDs match 88 // If voice chat isn't initialized after 10 seconds, log a warning 89 setTimeout(() => { 90 if (!peer) { 91 clearInterval(tryGetPeer); 92 console.warn('ð DM Calls: Voice chat PeerJS not available yet. DM calls will not work until voice chat is initialized.'); 93 console.warn('ð DM Calls: To enable DM calls, please join voice chat first, or wait for voice chat to initialize.'); 94 } 95 }, 10000); 96 } 97 98 /** 99 * Setup call handler for incoming calls 100 */ 101 function setupCallHandler() { 102 if (peer && !peer._dmCallHandlerSetup) { 103 peer.on('call', (call) => { 104 handleIncomingCall(call); 105 }); 106 peer._dmCallHandlerSetup = true; 107 } 108 } 109 110 /** 111 * Setup Firebase listeners for call signaling 112 */ 113 function setupFirebaseListeners() { 114 if (!currentUsername || !firebase || !firebase.database) return; 115 116 const callsRef = firebase.database().ref('chatroom/dmCalls'); 117 118 // Listen for incoming call requests 119 callsRef.on('child_added', (snapshot) => { 120 const callData = snapshot.val(); 121 if (!callData) return; 122 123 // Only handle calls TO us 124 if (callData.to === currentUsername && callData.from !== currentUsername) { 125 handleIncomingCallRequest(callData); 126 } 127 }); 128 129 // Listen for call cancellations/endings 130 callsRef.on('child_removed', (snapshot) => { 131 const callData = snapshot.val(); 132 if (callData && callData.to === currentUsername && callData.from !== currentUsername) { 133 // Call was cancelled by the other person 134 if (isInCall && currentCall && currentCall.username === callData.from) { 135 endCall(); 136 } 137 } 138 }); 139 140 // Listen for call state changes 141 callsRef.on('child_changed', (snapshot) => { 142 const callData = snapshot.val(); 143 if (!callData) return; 144 145 // If call was accepted/rejected 146 if (callData.to === currentUsername && callData.from !== currentUsername) { 147 if (callData.status === 'rejected') {
148 // Other person rejected our call 149 if (currentCall && currentCall.username === callData.from) { 150 endCall(); 151 showNotification('Call rejected', `${callData.from} rejected your call`); 152 } 153 } 154 } 155 }); 156 } 157 158 /** 159 * Handle incoming call request from Firebase 160 */ 161 function handleIncomingCallRequest(callData) { 162 if (isInCall) { 163 // Already in a call, reject automatically 164 rejectCall(callData.from); 165 return; 166 } 167 168 // Show incoming call notification 169 const accept = confirm(`${callData.from} is calling you. Accept?`); 170 171 if (accept) { 172 acceptCall(callData.from); 173 } else { 174 rejectCall(callData.from); 175 } 176 } 177 178 /** 179 * Handle incoming PeerJS call 180 */ 181 async function handleIncomingCall(call) { 182 if (isInCall) { 183 console.log('ð Already in a call, rejecting incoming call'); 184 return; 185 } 186 187 // Get the caller's username from the call metadata or Firebase 188 const callerId = call.peer; 189 190 // Check Firebase for call data 191 const callsRef = firebase.database().ref('chatroom/dmCalls'); 192 callsRef.once('value', (snapshot) => { 193 const calls = snapshot.val(); 194 if (calls) { 195 for (const [key, callData] of Object.entries(calls)) { 196 if (callData.from === callerId && callData.to === currentUsername && callData.status === 'ringing') { 197 // This is the call we're accepting 198 answerCall(call, callerId); 199 return; 200 } 201 } 202 } 203 }); 204 } 205 206 /** 207 * Answer an incoming call 208 */ 209 async function answerCall(call, callerUsername) { 210 try { 211 // Get user's microphone 212 const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); 213 214 // Answer the call with audio stream 215 call.answer(stream); 216 217 // Set up call handlers 218 call.on('stream', (remoteStream) => { 219 handleRemoteStream(remoteStream, callerUsername); 220 }); 221 222 call.on('close', () => { 223 endCall(); 224 }); 225 226 call.on('error', (err) => { 227 console.error('ð Call error:', err); 228 endCall(); 229 }); 230 231 // Update call state 232 currentCall = { 233 username: callerUsername, 234 connection: call, 235 localStream: stream, 236 isIncoming: true 237 }; 238 isInCall = true; 239 240 // Update Firebase call status 241 const callsRef = firebase.database().ref('chatroom/dmCalls'); 242 callsRef.once('value', (snapshot) => { 243 const calls = snapshot.val(); 244 if (calls) { 245 for (const [key, callData] of Object.entries(calls)) { 246 if (callData.from === callerUsername && callData.to === currentUsername) { 247 callsRef.child(key).update({ status: 'accepted' }); 248 break; 249 } 250 } 251 } 252 }); 253 254 updateCallUI(callerUsername, true, 'connected'); 255 if (window.ChatroomDMs && window.ChatroomDMs.getCurrentDM() === callerUsername) { 256 appendCallSystemMessage(callerUsername + ' started a call.'); 257 } 258 showNotification('Call connected', 'Connected to ' + callerUsername); 259 } catch (error) { 260 console.error('ð Error answering call:', error); 261 rejectCall(callerUsername); 262 } 263 } 264 265 /** 266 * Accept a call request 267 */ 268 async function acceptCall(callerUsername) { 269 // Wait for PeerJS call event 270 // The actual connection will be handled in handleIncomingCall 271 const callsRef = firebase.database().ref('chatroom/dmCalls'); 272 callsRef.once('value', (snapshot) => { 273 const calls = snapshot.val(); 274 if (calls) { 275 for (const [key, callData] of Object.entries(calls)) { 276 if (callData.from === callerUsername && callData.to === currentUsername) { 277 callsRef.child(key).update({ status: 'accepted' }); 278 break; 279 } 280 } 281 } 282 }); 283 } 284 285 /**
286 * Reject a call 287 */ 288 function rejectCall(callerUsername) { 289 const callsRef = firebase.database().ref('chatroom/dmCalls'); 290 callsRef.once('value', (snapshot) => { 291 const calls = snapshot.val(); 292 if (calls) { 293 for (const [key, callData] of Object.entries(calls)) { 294 if (callData.from === callerUsername && callData.to === currentUsername) { 295 callsRef.child(key).update({ status: 'rejected' }); 296 // Remove after a delay 297 setTimeout(() => { 298 callsRef.child(key).remove(); 299 }, 5000); 300 break; 301 } 302 } 303 } 304 }); 305 } 306 307 /** 308 * Show confirmation dialog before starting a call 309 */ 310 function startCallWithConfirmation(otherUsername) { 311 try { 312 const skipConfirm = localStorage.getItem('chatroom_confirm_dm_call') === 'false'; 313 if (skipConfirm) { 314 startCall(otherUsername); 315 return; 316 } 317 } catch (e) { /* ignore */ } 318 319 if (!window.ChatroomConfirm) { 320 startCall(otherUsername); 321 return; 322 } 323 324 window.ChatroomConfirm.show({ 325 title: 'Call ' + otherUsername + '?', 326 message: 'Start a voice call with this user?', 327 confirmText: 'Call', 328 cancelText: 'Cancel', 329 confirmVariant: 'primary', 330 dontShowKey: 'chatroom_confirm_dm_call' 331 }).then(function (confirmed) { 332 if (confirmed) startCall(otherUsername); 333 }); 334 } 335 336 /** 337 * Start a call with another user 338 */ 339 async function startCall(otherUsername) { 340 if (isInCall) { 341 showNotification('Already in call', 'Please end the current call first'); 342 return; 343 } 344 345 // Wait for peer to be ready and open 346 const waitForPeer = new Promise((resolve, reject) => { 347 // First, try to get peer from voice chat if we don't have one 348 if (!peer) { 349 if (window.ChatroomVoiceChat && window.ChatroomVoiceChat.getPeer) { 350 peer = window.ChatroomVoiceChat.getPeer(); 351 } 352 } 353 354 // If still no peer, wait for it to be created 355 if (!peer) { 356 const checkPeer = setInterval(() => { 357 if (window.ChatroomVoiceChat && window.ChatroomVoiceChat.getPeer) { 358 peer = window.ChatroomVoiceChat.getPeer(); 359 } 360 if (peer && peer.open) { 361 clearInterval(checkPeer); 362 resolve(); 363 } 364 }, 100); 365 366 setTimeout(() => { 367 clearInterval(checkPeer); 368 if (!peer || !peer.open) { 369 reject(new Error('PeerJS not available. Please join voice chat first to enable DM calls.')); 370 } 371 }, 10000); 372 return; 373 } 374 375 // Peer exists, wait for it to be open 376 if (peer.open) { 377 resolve(); 378 } else { 379 const openHandler = () => { 380 if (peer) { 381 peer.off('open', openHandler); 382 } 383 resolve(); 384 }; 385 peer.on('open', openHandler); 386 setTimeout(() => { 387 if (peer) { 388 peer.off('open', openHandler); 389 } 390 reject(new Error('PeerJS timeout - peer did not open within 10 seconds')); 391 }, 10000); 392 } 393 }); 394 395 try { 396 await waitForPeer; 397 398 // Get user's microphone 399 const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
400 401 // Create call in Firebase 402 const callsRef = firebase.database().ref('chatroom/dmCalls'); 403 const callKey = callsRef.push({ 404 from: currentUsername, 405 to: otherUsername, 406 status: 'ringing', 407 timestamp: Date.now() 408 }).key; 409 410 // Call the other user using PeerJS 411 const call = peer.call(otherUsername, stream); 412 413 if (!call) { 414 throw new Error('Failed to create call'); 415 } 416 417 // Set up call handlers 418 call.on('stream', (remoteStream) => { 419 handleRemoteStream(remoteStream, otherUsername); 420 }); 421 422 call.on('close', () => { 423 endCall(); 424 }); 425 426 call.on('error', (err) => { 427 console.error('ð Call error:', err); 428 endCall(); 429 }); 430 431 // Update call state 432 currentCall = { 433 username: otherUsername, 434 connection: call, 435 localStream: stream, 436 isIncoming: false, 437 callKey: callKey 438 }; 439 isInCall = true; 440 441 updateCallUI(otherUsername, true, 'ringing'); 442 if (window.ChatroomDMs && window.ChatroomDMs.getCurrentDM() === otherUsername) { 443 appendCallSystemMessage((currentUsername || 'Someone') + ' started a call.'); 444 } 445 446 // Check if call was accepted (wait a bit for response) 447 setTimeout(() => { 448 callsRef.child(callKey).once('value', (snapshot) => { 449 const callData = snapshot.val(); 450 if (callData && callData.status === 'rejected') { 451 endCall(); 452 showNotification('Call rejected', `${otherUsername} rejected your call`); 453 } else if (callData && callData.status === 'accepted') { 454 setCallPhase('connected'); 455 updateCallUI(otherUsername, true, 'connected'); 456 showNotification('Call connected', 'Connected to ' + otherUsername); 457 } 458 }); 459 }, 1000); 460 461 } catch (error) { 462 console.error('ð Error starting call:', error); 463 showNotification('Call failed', 'Could not start call. Please check your microphone permissions.'); 464 endCall(); 465 } 466 } 467 468 /** 469 * End the current call 470 */ 471 function endCall() { 472 if (!isInCall) return; 473 474 // Close PeerJS connection 475 if (currentCall && currentCall.connection) { 476 currentCall.connection.close(); 477 } 478 479 // Stop local stream 480 if (currentCall && currentCall.localStream) { 481 currentCall.localStream.getTracks().forEach(track => track.stop()); 482 } 483 484 // Remove from Firebase 485 if (currentCall && currentCall.callKey) { 486 const callsRef = firebase.database().ref('chatroom/dmCalls'); 487 callsRef.child(currentCall.callKey).remove(); 488 } else if (currentCall) { 489 // Find and remove call from Firebase 490 const callsRef = firebase.database().ref('chatroom/dmCalls'); 491 callsRef.once('value', (snapshot) => { 492 const calls = snapshot.val(); 493 if (calls) { 494 for (const [key, callData] of Object.entries(calls)) { 495 if ((callData.from === currentCall.username && callData.to === currentUsername) || 496 (callData.to === currentCall.username && callData.from === currentUsername)) { 497 callsRef.child(key).remove(); 498 break; 499 } 500 } 501 } 502 }); 503 } 504 505 // Clear call state 506 const previousUsername = currentCall ? currentCall.username : null; 507 currentCall = null; 508 isInCall = false; 509 510 if (previousUsername && window.ChatroomDMs && window.ChatroomDMs.getCurrentDM() === previousUsername) {
511 appendCallSystemMessage('Call ended.'); 512 } 513 514 updateCallUI(previousUsername, false, null); 515 } 516 517 /** 518 * Handle remote audio stream 519 */ 520 function handleRemoteStream(remoteStream, username) { 521 const audio = new Audio(); 522 audio.srcObject = remoteStream; 523 audio.autoplay = true; 524 audio.play().catch(function (err) { 525 console.error('ð Error playing remote audio:', err); 526 }); 527 528 if (currentCall) { 529 currentCall.remoteAudio = audio; 530 } 531 532 if (callPhase !== 'connected') { 533 setCallPhase('connected'); 534 updateCallUI(username, true, 'connected'); 535 } 536 } 537 538 function escapeHtml(text) { 539 const div = document.createElement('div'); 540 div.textContent = text; 541 return div.innerHTML; 542 } 543 544 function getAvatarEmoji(username) { 545 const avatars = ['ð¾', 'ð¶', 'ð', 'ð¦´', 'ð¾', 'ð§¸', 'â', 'ð«', 'â¨', 'ð']; 546 const hash = (username || '').split('').reduce(function (acc, char) { 547 return acc + char.charCodeAt(0); 548 }, 0); 549 return avatars[hash % avatars.length]; 550 } 551 552 function fillAvatarElement(container, username) { 553 if (!container || !username) return; 554 const pic = window.ChatroomProfilePicture && window.ChatroomProfilePicture.getProfilePicture(username); 555 if (pic) { 556 container.innerHTML = '<img src="' + escapeHtml(pic) + '" alt="' + escapeHtml(username) + '" class="profile-picture-avatar">'; 557 } else { 558 container.textContent = getAvatarEmoji(username); 559 } 560 } 561 562 function formatCallDuration(ms) { 563 const totalSec = Math.max(0, Math.floor(ms / 1000)); 564 const h = Math.floor(totalSec / 3600); 565 const m = Math.floor((totalSec % 3600) / 60); 566 const s = totalSec % 60; 567 if (h > 0) { 568 return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); 569 } 570 return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); 571 } 572 573 function stopCallTimer() { 574 if (callTimerInterval) { 575 clearInterval(callTimerInterval); 576 callTimerInterval = null; 577 } 578 callStartedAt = null; 579 } 580 581 function startCallTimer() { 582 stopCallTimer(); 583 callStartedAt = Date.now(); 584 const tick = function () { 585 const elapsed = Date.now() - callStartedAt; 586 const text = formatCallDuration(elapsed); 587 const sidebarTimer = document.getElementById('dm-call-sidebar-timer'); 588 const stageStatus = document.getElementById('dm-call-stage-status'); 589 if (sidebarTimer) sidebarTimer.textContent = text; 590 if (stageStatus && callPhase === 'connected') { 591 stageStatus.textContent = text; 592 } 593 }; 594 tick(); 595 callTimerInterval = setInterval(tick, 1000); 596 } 597 598 function setCallPhase(phase) { 599 callPhase = phase; 600 const stageStatus = document.getElementById('dm-call-stage-status'); 601 if (stageStatus) { 602 if (phase === 'ringing') { 603 stageStatus.textContent = 'Ringingâ¦'; 604 } else if (phase === 'connected') { 605 startCallTimer(); 606 } 607 } 608 } 609 610 function bindDmCallControlsOnce() { 611 if (dmCallControlsBound) return; 612 dmCallControlsBound = true; 613 614 function wireEnd(btn) { 615 if (!btn) return; 616 btn.addEventListener('click', function (e) { 617 e.preventDefault(); 618 e.stopPropagation(); 619 endCall(); 620 }); 621 } 622 623 function wireMute(btn) { 624 if (!btn) return; 625 btn.addEventListener('click', function (e) { 626 e.preventDefault(); 627 e.stopPropagation(); 628 toggleDmCallMute(); 629 }); 630 } 631 632 wireEnd(document.getElementById('dm-call-stage-end-btn')); 633 wireEnd(document.getElementById('dm-call-disconnect-btn')); 634 wireMute(document.getElementById('dm-call-stage-mute-btn')); 635 wireMute(document.getElementById('dm-call-mute-btn')); 636 637 if (window.ChatroomIcons) { 638 window.ChatroomIcons.set(document.getElementById('dm-call-disconnect-btn'), 'phoneHangup'); 639 window.ChatroomIcons.set(document.getElementById('dm-call-stage-end-btn'), 'phoneHangup'); 640 window.ChatroomIcons.setVoiceMute(document.getElementById('dm-call-mute-btn'), false); 641 window.ChatroomIcons.setVoiceMute(document.getElementById('dm-call-stage-mute-btn'), false); 642 } 643 } 644 645 function toggleDmCallMute() { 646 isDmCallMuted = !isDmCallMuted; 647 if (currentCall && currentCall.localStream) {
648 currentCall.localStream.getAudioTracks().forEach(function (track) { 649 track.enabled = !isDmCallMuted; 650 }); 651 } 652 ['dm-call-mute-btn', 'dm-call-stage-mute-btn'].forEach(function (id) { 653 const btn = document.getElementById(id); 654 if (!btn) return; 655 btn.classList.toggle('muted', isDmCallMuted); 656 if (window.ChatroomIcons) { 657 window.ChatroomIcons.setVoiceMute(btn, isDmCallMuted); 658 } 659 btn.title = isDmCallMuted ? 'Unmute' : 'Mute'; 660 }); 661 } 662 663 function appendCallSystemMessage(text) { 664 const messagesList = document.getElementById('messages-list'); 665 if (!messagesList) return; 666 667 const msg = document.createElement('div'); 668 msg.className = 'message dm-call-system-message'; 669 const time = new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); 670 const icon = window.ChatroomIcons ? window.ChatroomIcons.icon('phone') : 'ð'; 671 msg.innerHTML = 672 '<div class="dm-call-system-inner">' + 673 icon + 674 '<span>' + escapeHtml(text) + '<span class="dm-call-system-time">' + escapeHtml(time) + '</span></span>' + 675 '</div>'; 676 messagesList.appendChild(msg); 677 messagesList.scrollTop = messagesList.scrollHeight; 678 } 679 680 function dispatchCallStateChange(username, inCall) { 681 try { 682 window.dispatchEvent(new CustomEvent('chatroomDmCallChanged', { 683 detail: { inCall: inCall, username: inCall ? username : null, phase: callPhase } 684 })); 685 } catch (e) { /* ignore */ } 686 } 687 688 function refreshDmCallUi() { 689 if (isInCall && currentCall && currentCall.username) { 690 updateCallUI(currentCall.username, true, callPhase); 691 } else { 692 updateCallUI(null, false, null); 693 } 694 } 695 696 /** 697 * Update call UI â header, stage, sidebar, DM list (Discord-style) 698 */ 699 function updateCallUI(username, inCall, phase) { 700 bindDmCallControlsOnce(); 701 702 if (inCall && phase) { 703 callPhase = phase; 704 } else if (!inCall) { 705 callPhase = null; 706 stopCallTimer(); 707 isDmCallMuted = false; 708 } 709 710 const currentDM = window.ChatroomDMs && window.ChatroomDMs.getCurrentDM 711 ? window.ChatroomDMs.getCurrentDM() 712 : null; 713 const viewingPartner = inCall && username && currentDM === username; 714 715 const callBtn = document.getElementById('dm-call-btn'); 716 const callStatus = document.getElementById('dm-call-status'); 717 const stage = document.getElementById('dm-call-stage'); 718 const dmCallCategory = document.getElementById('dm-call-category'); 719 const chatArea = document.querySelector('.chat-area'); 720 const channelIcon = document.getElementById('channel-icon-header'); 721 const channelTopic = document.getElementById('channel-topic'); 722 723 if (callBtn) { 724 if (inCall) { 725 if (window.ChatroomIcons) window.ChatroomIcons.set(callBtn, 'phoneSlash'); 726 callBtn.title = 'End call'; 727 callBtn.classList.add('active'); 728 } else { 729 if (window.ChatroomIcons) window.ChatroomIcons.set(callBtn, 'phone'); 730 callBtn.title = 'Call'; 731 callBtn.classList.remove('active'); 732 } 733 } 734 735 if (callStatus) { 736 if (inCall && username) { 737 callStatus.textContent = callPhase === 'connected' ? 'In voice' : 'Callingâ¦'; 738 callStatus.style.display = 'block'; 739 callStatus.style.color = 'var(--online)'; 740 } else { 741 callStatus.style.display = 'none'; 742 } 743 } 744 745 if (dmCallCategory) { 746 dmCallCategory.style.display = inCall ? 'block' : 'none'; 747 } 748 749 const sidebarName = document.getElementById('dm-call-sidebar-name'); 750 if (sidebarName && inCall && username) { 751 sidebarName.textContent = username; 752 } 753 754 if (stage) { 755 if (viewingPartner) { 756 stage.hidden = false; 757 fillAvatarElement(document.getElementById('dm-call-avatar-self'), currentUsername); 758 fillAvatarElement(document.getElementById('dm-call-avatar-other'), username); 759 const labelSelf = document.getElementById('dm-call-label-self'); 760 const labelOther = document.getElementById('dm-call-label-other'); 761 if (labelSelf) labelSelf.textContent = currentUsername ? currentUsername + ' (you)' : 'You'; 762 if (labelOther) labelOther.textContent = username; 763 const stageStatus = document.getElementById('dm-call-stage-status'); 764 if (stageStatus) { 765 if (callPhase === 'ringing') { 766 stageStatus.textContent = 'Ringingâ¦'; 767 } else if (callPhase === 'connected' && callStartedAt) { 768 stageStatus.textContent = formatCallDuration(Date.now() - callStartedAt); 769 } else if (callPhase === 'connected') { 770 stageStatus.textContent = 'Connected'; 771 } 772 } 773 } else { 774 stage.hidden = true; 775 } 776 } 777 778 if (chatArea) { 779 chatArea.classList.toggle('dm-call-active', !!viewingPartner); 780 } 781 782 if (currentDM === username || !inCall) { 783 if (channelIcon && inCall && currentDM === username) { 784 channelIcon.innerHTML = window.ChatroomIcons 785 ? window.ChatroomIcons.icon('phone') 786 : 'ð'; 787 } else if (channelIcon && currentDM) { 788 channelIcon.textContent = 'ð'; 789 } 790 if (channelTopic && currentDM === username) { 791 if (inCall) { 792 channelTopic.textContent = callPhase === 'connected' 793 ? 'In a call · ' + username 794 : 'Calling ' + username + 'â¦'; 795 channelTopic.style.color = 'var(--online)'; 796 } else { 797 channelTopic.textContent = 'Direct message'; 798 channelTopic.style.color = ''; 799 } 800 } 801 } 802 803 dispatchCallStateChange(username, inCall); 804 } 805 806 /** 807 * Show notification 808 */ 809 function showNotification(title, message) { 810 if (window.ChatroomNotifications && window.ChatroomNotifications.showNotification) { 811 window.ChatroomNotifications.showNotification(title, message); 812 } else { 813 console.log(`ð ${title}: ${message}`); 814 } 815 } 816 817 /** 818 * Check if currently in a call 819 */ 820 function getIsInCall() { 821 return isInCall; 822 } 823 824 /** 825 * Get current call info 826 */ 827 function getCurrentCall() { 828 return currentCall ? { ...currentCall } : null; 829 } 830 831 // Initialize on load 832 if (document.readyState === 'loading') { 833 document.addEventListener('DOMContentLoaded', () => { 834 console.log('ð DM Calls: DOM loaded, initializing...'); 835 init(); 836 }); 837 } else { 838 console.log('ð DM Calls: DOM already loaded, initializing...'); 839 init(); 840 } 841 842 // Public API 843 return { 844 init: init, 845 startCall: startCall, 846 startCallWithConfirmation: startCallWithConfirmation, 847 endCall: endCall, 848 getIsInCall: getIsInCall, 849 getCurrentCall: getCurrentCall, 850 refreshUi: refreshDmCallUi 851 }; 852})(); 853
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.