This commit is contained in:
Radon 2025-09-02 08:59:05 -05:00
parent ea36b0d535
commit b92de5c0fd

View File

@ -1,4 +1,6 @@
// ==================== CONFIGURATION ====================
// noinspection HtmlUnknownTarget
const CONFIG = {
OUTPUT: {
ECHO_CANCELLATION: true,
@ -374,7 +376,6 @@ class Utils {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username })
});
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
return { available: data.available, error: data.error };
} catch (error) {
@ -431,16 +432,16 @@ class UIManager {
document.body.appendChild(notice);
}
static removeReconnectionNotice() {
const notice = document.getElementById('reconnection-notice');
const notice = $('#reconnection-notice');
if (notice) notice.remove();
}
static updateMicrophoneStatus(granted) {
const micStatus = document.getElementById('mic-status');
const micStatus = $('#mic-status');
if (!micStatus) return;
if (granted) {
micStatus.textContent = '✅ Microphone access granted';
micStatus.className = 'mic-status granted';
document.getElementById('mic-section').style.display = 'none';
$('#mic-section').style.display = 'none';
} else {
micStatus.textContent = '❌ Microphone access denied. Please refresh and allow microphone access.';
micStatus.innerHTML = micStatus.textContent.replace('. ', '.<br>');
@ -448,47 +449,44 @@ class UIManager {
}
}
static updateJoinButton(text, disabled = false) {
const joinBtn = document.getElementById('join-chat-btn');
const joinBtn = $('#join-chat-btn');
joinBtn.textContent = text;
joinBtn.disabled = disabled;
}
static showVoiceChatUI() {
document.getElementById('join-section').style.display = 'none';
document.getElementById('username-section').style.display = 'none';
document.getElementById('voice-controls-sidebar').style.display = 'block';
document.getElementById('chat-section').style.display = 'flex';
$('#join-section').style.display = 'none';
$('#username-section').style.display = 'none';
$('#voice-controls-sidebar').style.display = 'block';
$('#chat-section').style.display = 'flex';
}
static showUsernameUI() {
document.getElementById('join-section').style.display = 'flex';
document.getElementById('username-section').style.display = 'block';
document.getElementById('voice-controls-sidebar').style.display = 'none';
document.getElementById('chat-section').style.display = 'none';
document.getElementById('users-list').innerHTML = '<div class="no-users"></div>';
$('#join-section').style.display = 'flex';
$('#username-section').style.display = 'block';
$('#voice-controls-sidebar').style.display = 'none';
$('#chat-section').style.display = 'none';
$('#users-list').innerHTML = '<div class="no-users"></div>';
}
static handleClickOutsideEmojiPicker(event) {
const emojiPicker = document.getElementById('emoji-picker');
const emojiPicker = $('#emoji-picker');
if (!emojiPicker.contains(event.target)) {
UIManager.closeEmojiPicker();
}
}
static closeEmojiPicker() {
const emojiPicker = document.getElementById('emoji-picker');
const emojiPicker = $('#emoji-picker');
if (emojiPicker) {
emojiPicker.remove();
document.removeEventListener('click', UIManager.handleClickOutsideEmojiPicker);
return;
}
}
static openEmojiPicker() {
if (document.getElementById('emoji-picker')) {
if ($('#emoji-picker')) {
return;
}
const chatSection = document.getElementById('chat-messages');
const chatInputContainer = $('.chat-input-container');
const emojiBtn = document.getElementById('emoji-btn');
// Create emoji picker container
const emojiPicker = document.createElement('div');
@ -516,7 +514,7 @@ class UIManager {
// Add emojis to the picker
EMOJIS.forEach(emoji => {
const emojiSpan = document.createElement('span');
const sendBtn = document.getElementById('send-btn');
const sendBtn = $('#send-btn');
emojiSpan.className = 'emoji';
emojiSpan.textContent = emoji;
emojiSpan.style.cursor = 'pointer';
@ -526,7 +524,7 @@ class UIManager {
emojiSpan.style.fontSize = '1.5rem';
emojiSpan.style.borderRadius = '0.5rem';
emojiSpan.onclick = () => {
const messageInput = document.getElementById('chat-input');
const messageInput = $('#chat-input');
messageInput.value += emoji;
messageInput.focus();
sendBtn.disabled = messageInput.value.trim() === '';
@ -554,7 +552,7 @@ class UIManager {
}
static toggleEmojiPicker() {
if (document.getElementById('emoji-picker')) {
if ($('#emoji-picker')) {
UIManager.closeEmojiPicker();
} else {
UIManager.openEmojiPicker();
@ -562,19 +560,19 @@ class UIManager {
}
static async loadSettingsButton() {
const settingsBtn = document.getElementById('voice-settings-btn');
const settingsBtn = $('#voice-settings-btn');
settingsBtn.innerHTML = await RenderSvg(SVGS.SETTINGS);
settingsBtn.title = 'Settings';
}
static async loadDisconnectButton() {
const disconnectBtn = document.getElementById('leave-voice-btn');
const disconnectBtn = $('#leave-voice-btn');
disconnectBtn.innerHTML = await RenderSvg(SVGS.CONNECT);
disconnectBtn.title = 'Disconnect';
}
static async updateMuteButton() {
const muteBtn = document.getElementById('toggle-mute-btn');
const muteBtn = $('#toggle-mute-btn');
if (state.isMuted) {
muteBtn.innerHTML = await RenderSvg(SVGS.MUTED);
muteBtn.title = 'Unmute (enable outgoing audio)';
@ -589,7 +587,7 @@ class UIManager {
}
}
static async updateDeafenButton() {
const deafenBtn = document.getElementById('toggle-deafen-btn');
const deafenBtn = $('#toggle-deafen-btn');
if (state.isDeafened) {
deafenBtn.innerHTML = await RenderSvg(SVGS.DEAFENED);
deafenBtn.title = 'Undeafen (enable incoming audio)';
@ -720,8 +718,7 @@ class AudioManager {
// New: Update microphone gain
static updateMicrophoneGain() {
if (state.micGainNode) {
const gainValue = state.micVolume / CONFIG.MEDIA.MAX_VOLUME;
state.micGainNode.gain.value = gainValue;
state.micGainNode.gain.value = state.micVolume / CONFIG.MEDIA.MAX_VOLUME;
} else {
console.warn('🎤 Cannot update microphone gain - no gain node available');
}
@ -811,7 +808,7 @@ class AudioManager {
state.localStream = null;
await Utils.delay(100);
const newStream = await navigator.mediaDevices.getUserMedia({
state.localStream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: CONFIG.OUTPUT.ECHO_CANCELLATION,
noiseSuppression: CONFIG.OUTPUT.NOISE_SUPPRESSION,
@ -820,7 +817,6 @@ class AudioManager {
deviceId: CONFIG.OUTPUT.DEVICE_ID,
}
});
state.localStream = newStream;
// Set up audio processing again
await AudioManager.setupAudioProcessing();
@ -863,7 +859,7 @@ class AudioManager {
audioElement.volume = (state.headphoneVolume / CONFIG.MEDIA.MAX_VOLUME) * (userVolume / CONFIG.MEDIA.MAX_VOLUME);
audioElement.muted = state.isDeafened || state.mutedUsers.has(userId);
AudioManager.setupAudioEventListeners(audioElement, userId);
document.getElementById('audio-container').appendChild(audioElement);
$('#audio-container').appendChild(audioElement);
audioElement.play().then(() => {
console.log('🔊 Successfully started playing audio for user:', userId);
}).catch(error => {
@ -933,10 +929,12 @@ class WebSocketManager {
switch (message.type) {
case 'system_message':
ChatManager.addChatAlert(message.data, message.dataType, message.dataTime);
break;
case 'client_id':
console.log('🔑 Client ID received:', message.clientId);
state.currentId = message.clientId;
state.save();
break;
case 'users_list':
console.log('🔵 This is a users_list message with data:', message.data);
UserManager.updateUsersList(message.data);
@ -974,8 +972,8 @@ class WebSocketManager {
if (state.isInVoiceChat) {
console.log('🔄 Server connection lost, resetting client state...');
PeerConnectionManager.cleanupAllConnections();
document.getElementById('audio-container').innerHTML = '';
document.getElementById('users-list').innerHTML = '<div class="no-users"></div>';
$('#audio-container').innerHTML = '';
$('#users-list').innerHTML = '<div class="no-users"></div>';
const savedUsername = state.currentUsername;
state.isInVoiceChat = false;
if (!state.intentionalDisconnect) {
@ -1328,7 +1326,7 @@ class UserManager {
users = [];
}
const usersList = document.getElementById('users-list');
const usersList = $('#users-list');
const currentUserIds = users.map(u => u.id);
const previousUserIds = Object.keys(state.previousUsers);
@ -1340,12 +1338,12 @@ class UserManager {
}
static setUsername() {
const modalUsernameInput = document.getElementById('modal-username-input');
const usernameInput = document.getElementById('username-input');
const modalUsernameInput = $('#modal-username-input');
const usernameInput = $('#username-input');
const username = modalUsernameInput.value.trim();
if (username == state.currentUsername) {
if (username === state.currentUsername) {
return;
}
@ -1418,7 +1416,7 @@ class UserManager {
static detectUserChanges(users, currentUserIds, previousUserIds) {
if (state.previousUsers && state.isInVoiceChat) {
const newUsers = users ? users.filter(u => !previousUserIds.includes(u.id) && u.username !== state.currentUsername) : [];
newUsers.forEach(user => {
newUsers.forEach(_ => {
Utils.playSound('/sounds/join.wav', state.apparentOutputVolume());
});
const leftUserIds = previousUserIds.filter(id => !currentUserIds.includes(id));
@ -1466,7 +1464,7 @@ class UserManager {
const clickHandler = isCurrentUser ? `onclick="ModalManager.openSelfModal()"` : `onclick="ModalManager.openUserModal('${user.id}', '${user.username}')"`;
const muteButtonHtml = !isCurrentUser ? `
<button class="mute-user-btn" onclick="event.stopPropagation(); VoiceControls.muteUser('${user.id}'); const muteBtn = document.getElementById('modal-mute-btn'); const isMuted = state.mutedUsers.has('${user.id}'); muteBtn.textContent = isMuted ? '🔇 User Muted' : '🔊 Mute User'; muteBtn.className = isMuted ? 'control-btn muted' : 'control-btn';" title="${isMuted ? 'Unmute user' : 'Mute user'}">
<button class="mute-user-btn" onclick="event.stopPropagation(); VoiceControls.muteUser('${user.id}'); const muteBtn = $('#modal-mute-btn'); const isMuted = state.mutedUsers.has('${user.id}'); muteBtn.textContent = isMuted ? '🔇 User Muted' : '🔊 Mute User'; muteBtn.className = isMuted ? 'control-btn muted' : 'control-btn';" title="${isMuted ? 'Unmute user' : 'Mute user'}">
${isMuted ? '🔇' : '🔊'}
</button>
` : '';
@ -1535,7 +1533,7 @@ class UserManager {
// ==================== CHAT MANAGEMENT ====================
class ChatManager {
static handleChatMessage(data) {
const chatMessages = document.getElementById('chat-messages');
const chatMessages = $('#chat-messages');
const isOwnMessage = data.username === state.currentUsername;
let message = '';
let timestamp = Date.now();
@ -1553,8 +1551,7 @@ class ChatManager {
}
const messageEl = document.createElement('div');
messageEl.className = `chat-message ${isOwnMessage ? 'own' : 'other'}`;
const messageId = `msg_${Date.now()}_${Math.random()}`;
messageEl.id = messageId;
messageEl.id = `msg_${Date.now()}_${Math.random()}`;
messageEl.innerHTML = `
<div class="chat-message-header">
<div class="username">${isOwnMessage ? 'You' : data.username}</div>
@ -1567,7 +1564,7 @@ class ChatManager {
console.log('Added chat message from:', data.username, 'message:', message);
}
static addChatAlert(message, type, timeout) {
const chatMessages = document.getElementById('chat-messages');
const chatMessages = $('#chat-messages');
if (!chatMessages) return;
const alertEl = document.createElement('div');
alertEl.className = `chat-alert chat-alert-${type}`;
@ -1594,7 +1591,7 @@ class ChatManager {
}
static deleteMessage(messageId) {
const messageEl = document.getElementById(messageId);
const messageEl = $(messageId);
if (messageEl) {
messageEl.classList.add('fading');
setTimeout(() => {
@ -1602,7 +1599,7 @@ class ChatManager {
messageEl.remove();
}
state.messageTimeouts.delete(messageId);
}, 1000);
}, 500);
}
}
@ -1657,8 +1654,8 @@ class VoiceControls {
state.save();
}
static updateMicVolume() {
const slider = document.getElementById('mic-volume-slider');
const percentage = document.getElementById('mic-volume-percentage');
const slider = $('#mic-volume-slider');
const percentage = $('#mic-volume-percentage');
state.micVolume = parseInt(slider.value);
percentage.textContent = `${state.micVolume}%`;
@ -1666,8 +1663,8 @@ class VoiceControls {
AudioManager.updateMicrophoneGain();
}
static updateHeadphoneVolume() {
const slider = document.getElementById('headphone-volume-slider');
const percentage = document.getElementById('headphone-volume-percentage');
const slider = $('#headphone-volume-slider');
const percentage = $('#headphone-volume-percentage');
state.headphoneVolume = parseInt(slider.value);
percentage.textContent = `${state.headphoneVolume}%`;
const audioElements = $$('#audio-container audio');
@ -1680,10 +1677,10 @@ class VoiceControls {
static resetAllVolumes() {
state.micVolume = 100;
state.headphoneVolume = 100;
const micSlider = document.getElementById('mic-volume-slider');
const headphoneSlider = document.getElementById('headphone-volume-slider');
const micPercentage = document.getElementById('mic-volume-percentage');
const headphonePercentage = document.getElementById('headphone-volume-percentage');
const micSlider = $('#mic-volume-slider');
const headphoneSlider = $('#headphone-volume-slider');
const micPercentage = $('#mic-volume-percentage');
const headphonePercentage = $('#headphone-volume-percentage');
if (micSlider) {
micSlider.value = 100;
micPercentage.textContent = '100%';
@ -1701,10 +1698,10 @@ class VoiceControls {
// ==================== MODAL MANAGEMENT ====================
class ModalManager {
static openSelfModal() {
const modal = document.getElementById('user-control-modal');
const modalUsername = document.getElementById('modal-username');
const modal = $('#user-control-modal');
const modalUsername = $('#modal-username');
modalUsername.textContent = 'Settings';
const percentage = document.getElementById('headphone-volume-percentage');
const percentage = $('#headphone-volume-percentage');
if (percentage) {
percentage.textContent = `${state.headphoneVolume}%`;
}
@ -1713,8 +1710,8 @@ class ModalManager {
<div class="control-section">
<h1>Username</h1>
<div class="modal-username">
<input type="text" id="modal-username-input" placeholder="Enter your username" value="${state.currentUsername}"></input>
<input type="button" value="Apply" class="control-btn secondary" onclick="UserManager.setUsername()"></input>
<input type="text" id="modal-username-input" placeholder="Enter your username" value="${state.currentUsername}">
<input type="button" value="Apply" class="control-btn secondary" onclick="UserManager.setUsername()">
</div >
</div>
<div class="control-section">
@ -1744,7 +1741,7 @@ class ModalManager {
</div>
</div>
`;
const modalUsernameInput = document.getElementById('modal-username-input');
const modalUsernameInput = $('#modal-username-input');
modalUsernameInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
@ -1755,15 +1752,15 @@ class ModalManager {
console.log('Opened self audio settings modal');
}
// static closeSelfModal() {
// const modal = document.getElementById('user-control-modal');
// const modal = $('#user-control-modal');
// modal.style.display = 'none';
// state.currentModalUserId = null;
// console.log('Closed self audio settings modal');
// }
static openUserModal(userId, username) {
state.currentModalUserId = userId;
const modal = document.getElementById('user-control-modal');
const modalUsername = document.getElementById('modal-username');
const modal = $('#user-control-modal');
const modalUsername = $('#modal-username');
modalUsername.textContent = `${username} `;
const modalBody = modal.querySelector('.modal-body');
modalBody.innerHTML = `
@ -1787,11 +1784,11 @@ class ModalManager {
</div>
`;
const currentVolume = state.userVolumes.get(userId) || 100;
const volumeSlider = document.getElementById('user-volume-slider');
const volumePercentage = document.getElementById('volume-percentage');
const volumeSlider = $('#user-volume-slider');
const volumePercentage = $('#volume-percentage');
volumeSlider.value = currentVolume;
volumePercentage.textContent = `${currentVolume}% `;
const muteBtn = document.getElementById('modal-mute-btn');
const muteBtn = $('#modal-mute-btn');
const isMuted = state.mutedUsers.has(userId);
if (muteBtn) {
muteBtn.textContent = isMuted ? '🔇 User Muted' : '🔊 Mute User';
@ -1801,15 +1798,15 @@ class ModalManager {
console.log(`Opened modal for user ${username}(${userId})`);
}
static closeUserModal() {
const modal = document.getElementById('user-control-modal');
const modal = $('#user-control-modal');
modal.style.display = 'none';
state.currentModalUserId = null;
console.log('Closed user modal');
}
static updateUserVolume() {
if (!state.currentModalUserId) return;
const slider = document.getElementById('user-volume-slider');
const volumePercentage = document.getElementById('volume-percentage');
const slider = $('#user-volume-slider');
const volumePercentage = $('#volume-percentage');
const volume = parseInt(slider.value);
volumePercentage.textContent = `${volume}% `;
state.userVolumes.set(state.currentModalUserId, volume);
@ -1829,15 +1826,15 @@ class ModalManager {
static toggleUserMuteFromModal() {
if (!state.currentModalUserId) return;
VoiceControls.muteUser(state.currentModalUserId);
const muteBtn = document.getElementById('modal-mute-btn');
const muteBtn = $('#modal-mute-btn');
const isMuted = state.mutedUsers.has(state.currentModalUserId);
muteBtn.textContent = isMuted ? '🔇 User Muted' : '🔊 Mute User';
muteBtn.className = isMuted ? 'control-btn muted' : 'control-btn';
}
static resetUserVolume() {
if (!state.currentModalUserId) return;
const volumeSlider = document.getElementById('user-volume-slider');
const volumePercentage = document.getElementById('volume-percentage');
const volumeSlider = $('#user-volume-slider');
const volumePercentage = $('#volume-percentage');
volumeSlider.value = 100;
volumePercentage.textContent = '100%';
ModalManager.updateUserVolume();
@ -1847,7 +1844,7 @@ class ModalManager {
// ==================== VOICE CHAT MANAGER ====================
class VoiceChatManager {
static async joinVoiceChat() {
const usernameInput = document.getElementById('username-input');
const usernameInput = $('#username-input');
const username = usernameInput.value.trim();
state.intentionalDisconnect = false;
if (!username) {
@ -1892,7 +1889,7 @@ class VoiceChatManager {
PeerConnectionManager.stopPeerHealthCheck();
PeerConnectionManager.cleanupAllConnections();
UserManager.onVoiceChatLeave();
document.getElementById('audio-container').innerHTML = '';
$('#audio-container').innerHTML = '';
state.reset();
ModalManager.closeUserModal();
state.intentionalDisconnect = true;
@ -1920,7 +1917,7 @@ class VoiceChatManager {
state.isInVoiceChat = false;
UIManager.updateJoinButton('Join Voice Chat', false);
UIManager.showUsernameUI();
const usernameInput = document.getElementById('username-input');
const usernameInput = $('#username-input');
usernameInput.focus();
usernameInput.select();
console.log('Username rejected, reset to initial state');
@ -1929,11 +1926,23 @@ class VoiceChatManager {
// ==================== EVENT HANDLERS ====================
class EventHandlers {
static setupEventListeners() {
const usernameInput = document.getElementById('username-input');
const joinChatBtn = document.getElementById('join-chat-btn');
const chatInput = document.getElementById('chat-input');
const sendBtn = document.getElementById('send-btn');
const fileBtn = document.getElementById('file-btn');
const usernameInput = $('#username-input');
const joinChatBtn = $('#join-chat-btn');
const chatInput = $('#chat-input');
const sendBtn = $('#send-btn');
const chatMessages = $('.chat-messages');
// const fileBtn = $('#file-btn');
chatMessages.addEventListener('scroll', function() {
const atBottom = UIManager.isChatScrolledToBottom();
if (!atBottom) {
$('#scroll-btn').style.display = 'block';
}
if (atBottom) {
$('#scroll-btn').style.display = 'none';
}
});
joinChatBtn.disabled = usernameInput.value.trim().length === 0;
@ -1946,12 +1955,13 @@ class EventHandlers {
}
});
chatInput.addEventListener('input', function(e) {
sendBtn.disabled = this.value.trim().length === 0;
const textarea = e.target;
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, window.innerHeight * 0.5) + 'px';
document.getElementById('send-btn').disabled = textarea.value.trim().length === 0;
$('#send-btn').disabled = textarea.value.trim().length === 0;
});
chatInput.addEventListener('keydown', function(e) {
@ -1965,7 +1975,7 @@ class EventHandlers {
}
static setupGlobalEventListeners() {
document.addEventListener('click', function(event) {
const modal = document.getElementById('user-control-modal');
const modal = $('#user-control-modal');
if (event.target === modal) {
ModalManager.closeUserModal();
}
@ -1998,7 +2008,7 @@ class EventHandlers {
}
static sendChatMessage() {
const chatInput = document.getElementById('chat-input');
const chatInput = $('#chat-input');
const message = chatInput.value.trim();
if (!message || !state.ws || state.ws.readyState !== WebSocket.OPEN) {
return;
@ -2006,7 +2016,7 @@ class EventHandlers {
WebSocketManager.sendChatMessage(message);
chatInput.value = '';
chatInput.style.height = 'auto';
document.getElementById('send-btn').disabled = true;
$('#send-btn').disabled = true;
}
}
// ==================== DEBUG UTILITIES ====================
@ -2115,31 +2125,20 @@ class DebugUtils {
// }
// }, 5000);
$('.chat-messages').addEventListener('scroll', function() {
const atBottom = UIManager.isChatScrolledToBottom();
if (!atBottom) {
$('#scroll-btn').style.display = 'block';
}
if (atBottom) {
$('#scroll-btn').style.display = 'none';
}
});
// ==================== INITIALIZATION ====================
for (let { event, target } of [
{ event: 'DOMContentLoaded', target: document },
{ event: 'online', target: window }
]) {
// Auto-join on load
target.addEventListener(event, async function() {
state.load();
if (state.currentUsername) {
document.getElementById('username-input').value = state.currentUsername;
$('#username-input').value = state.currentUsername;
}
EventHandlers.setupEventListeners();
await AudioManager.requestMicrophonePermission();
VoiceChatManager.joinVoiceChat();
await VoiceChatManager.joinVoiceChat();
});
}
// ================== DE-INITIALIZATION ===================