add file upload/download

This commit is contained in:
Radon 2025-09-03 20:41:34 -05:00
parent 22c3996527
commit d7236780ce
2 changed files with 113 additions and 83 deletions

View File

@ -50,7 +50,7 @@ const CONFIG = {
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
const $id = (sel) => document.getElementById(sel);
const EMOJIS = [
// Smileys & Emotion
@ -277,7 +277,7 @@ class Utils {
}
static processMediaEmbeds(text) {
// Images (including GIFs)
text = text.replace(/(https?:\/\/\S+\.(?:jpg|jpeg|png|gif|webp|svg|bmp|tiff|ico|avif|jfif)(?:\?\S*)?)/gi,
text = text.replace(/(https?:\/\/\S+\.(?:jpg|svg|jpeg|png|gif|webp|svg|bmp|tiff|ico|avif|jfif)(?:\?\S*)?)/gi,
'<img src="$1" alt="Image" class="embedded-image" loading="lazy">');
// Videos
text = text.replace(/(https?:\/\/\S+\.(?:mp4|webm|ogg|avi|mov|wmv|flv|mkv|m4v|3gp)(?:\?\S*)?)/gi,
@ -388,12 +388,42 @@ class Utils {
class UIManager {
static openFilePicker() {
const fileInput = $('#file-input');
const fileInput = $id('file-input');
fileInput.click();
fileInput.addEventListener('change', (e) => {
fileInput.addEventListener('change', async (e) => {
const filesToUpload = e.target.files;
console.log(filesToUpload);
})
if (!filesToUpload || filesToUpload.length === 0) {
return;
}
const formData = new FormData();
const files = Array.from(filesToUpload);
files.forEach((file, index) => {
formData.append(`file_${index}`, file);
});
console.log(`Uploading ${files.length} files`);
formData.append('username', state.currentUsername)
formData.append('client_id', state.currentId)
try {
const response = await fetch('/files', {
method: 'POST',
body: formData
});
if (response.ok) {
const data = await response.json();
console.log('Upload successful:', data);
} else {
console.error('Upload failed:', response.statusText);
}
} catch (error) {
console.error('Upload error:', error);
}
}, { once: true });
}
static updateConnectionStatus(connected) {
@ -442,16 +472,16 @@ class UIManager {
document.body.appendChild(notice);
}
static removeReconnectionNotice() {
const notice = $('#reconnection-notice');
const notice = $id('reconnection-notice');
if (notice) notice.remove();
}
static updateMicrophoneStatus(granted) {
const micStatus = $('#mic-status');
const micStatus = $id('mic-status');
if (!micStatus) return;
if (granted) {
micStatus.textContent = '✅ Microphone access granted';
micStatus.className = 'mic-status granted';
$('#mic-section').style.display = 'none';
$id('mic-section').style.display = 'none';
} else {
micStatus.textContent = '❌ Microphone access denied. Please refresh and allow microphone access.';
micStatus.innerHTML = micStatus.textContent.replace('. ', '.<br>');
@ -459,32 +489,32 @@ class UIManager {
}
}
static updateJoinButton(text, disabled = false) {
const joinBtn = $('#join-chat-btn');
const joinBtn = $id('join-chat-btn');
joinBtn.textContent = text;
joinBtn.disabled = disabled;
}
static showVoiceChatUI() {
$('#join-section').style.display = 'none';
$('#username-section').style.display = 'none';
$('#voice-controls-sidebar').style.display = 'block';
$('#chat-section').style.display = 'flex';
$id('join-section').style.display = 'none';
$id('username-section').style.display = 'none';
$id('voice-controls-sidebar').style.display = 'block';
$id('chat-section').style.display = 'flex';
}
static showUsernameUI() {
$('#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>';
$id('join-section').style.display = 'flex';
$id('username-section').style.display = 'block';
$id('voice-controls-sidebar').style.display = 'none';
$id('chat-section').style.display = 'none';
$id('users-list').innerHTML = '<div class="no-users"></div>';
}
static handleClickOutsideEmojiPicker(event) {
const emojiPicker = $('#emoji-picker');
const emojiPicker = $id('emoji-picker');
if (!emojiPicker.contains(event.target)) {
UIManager.closeEmojiPicker();
}
}
static closeEmojiPicker() {
const emojiPicker = $('#emoji-picker');
const emojiPicker = $id('emoji-picker');
if (emojiPicker) {
emojiPicker.remove();
document.removeEventListener('click', UIManager.handleClickOutsideEmojiPicker);
@ -492,7 +522,7 @@ class UIManager {
}
static openEmojiPicker() {
if ($('#emoji-picker')) {
if ($id('emoji-picker')) {
return;
}
@ -524,7 +554,7 @@ class UIManager {
// Add emojis to the picker
EMOJIS.forEach(emoji => {
const emojiSpan = document.createElement('span');
const sendBtn = $('#send-btn');
const sendBtn = $id('send-btn');
emojiSpan.className = 'emoji';
emojiSpan.textContent = emoji;
emojiSpan.style.cursor = 'pointer';
@ -534,7 +564,7 @@ class UIManager {
emojiSpan.style.fontSize = '1.5rem';
emojiSpan.style.borderRadius = '0.5rem';
emojiSpan.onclick = () => {
const messageInput = $('#chat-input');
const messageInput = $id('chat-input');
messageInput.value += emoji;
messageInput.focus();
sendBtn.disabled = messageInput.value.trim() === '';
@ -562,7 +592,7 @@ class UIManager {
}
static toggleEmojiPicker() {
if ($('#emoji-picker')) {
if ($id('emoji-picker')) {
UIManager.closeEmojiPicker();
} else {
UIManager.openEmojiPicker();
@ -570,19 +600,19 @@ class UIManager {
}
static async loadSettingsButton() {
const settingsBtn = $('#voice-settings-btn');
const settingsBtn = $id('voice-settings-btn');
settingsBtn.innerHTML = await RenderSvg(SVGS.SETTINGS);
settingsBtn.title = 'Settings';
}
static async loadDisconnectButton() {
const disconnectBtn = $('#leave-voice-btn');
const disconnectBtn = $id('leave-voice-btn');
disconnectBtn.innerHTML = await RenderSvg(SVGS.CONNECT);
disconnectBtn.title = 'Disconnect';
}
static async updateMuteButton() {
const muteBtn = $('#toggle-mute-btn');
const muteBtn = $id('toggle-mute-btn');
if (state.isMuted) {
muteBtn.innerHTML = await RenderSvg(SVGS.MUTED);
muteBtn.title = 'Unmute (enable outgoing audio)';
@ -597,7 +627,7 @@ class UIManager {
}
}
static async updateDeafenButton() {
const deafenBtn = $('#toggle-deafen-btn');
const deafenBtn = $id('toggle-deafen-btn');
if (state.isDeafened) {
deafenBtn.innerHTML = await RenderSvg(SVGS.DEAFENED);
deafenBtn.title = 'Undeafen (enable incoming audio)';
@ -849,7 +879,7 @@ class AudioManager {
}
static addRemoteAudio(userId, stream) {
console.log('🔊 Adding remote audio for user:', userId);
const existingAudio = document.getElementById(`audio-${userId}`);
const existingAudio = $id(`audio-${userId}`);
if (existingAudio) {
console.log('🔊 Removing existing audio element for:', userId);
existingAudio.remove();
@ -869,7 +899,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);
$('#audio-container').appendChild(audioElement);
$id('audio-container').appendChild(audioElement);
audioElement.play().then(() => {
console.log('🔊 Successfully started playing audio for user:', userId);
}).catch(error => {
@ -982,8 +1012,8 @@ class WebSocketManager {
if (state.isInVoiceChat) {
console.log('🔄 Server connection lost, resetting client state...');
PeerConnectionManager.cleanupAllConnections();
$('#audio-container').innerHTML = '';
$('#users-list').innerHTML = '<div class="no-users"></div>';
$id('audio-container').innerHTML = '';
$id('users-list').innerHTML = '<div class="no-users"></div>';
const savedUsername = state.currentUsername;
state.isInVoiceChat = false;
if (!state.intentionalDisconnect) {
@ -1240,7 +1270,7 @@ class PeerConnectionManager {
state.peerConnections[userId].close();
delete state.peerConnections[userId];
}
const audioElement = document.getElementById(`audio-${userId}`);
const audioElement = $id(`audio-${userId}`);
if (audioElement) {
audioElement.remove();
}
@ -1336,7 +1366,7 @@ class UserManager {
users = [];
}
const usersList = $('#users-list');
const usersList = $id('users-list');
const currentUserIds = users.map(u => u.id);
const previousUserIds = Object.keys(state.previousUsers);
@ -1348,8 +1378,8 @@ class UserManager {
}
static setUsername() {
const modalUsernameInput = $('#modal-username-input');
const usernameInput = $('#username-input');
const modalUsernameInput = $id('modal-username-input');
const usernameInput = $id('username-input');
const username = modalUsernameInput.value.trim();
@ -1474,7 +1504,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 = $('#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 = $id('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>
` : '';
@ -1543,7 +1573,7 @@ class UserManager {
// ==================== CHAT MANAGEMENT ====================
class ChatManager {
static handleChatMessage(data) {
const chatMessages = $('#chat-messages');
const chatMessages = $id('chat-messages');
const isOwnMessage = data.username === state.currentUsername;
let message = '';
let timestamp = Date.now();
@ -1574,7 +1604,7 @@ class ChatManager {
console.log('Added chat message from:', data.username, 'message:', message);
}
static addChatAlert(message, type, timeout) {
const chatMessages = $('#chat-messages');
const chatMessages = $id('chat-messages');
if (!chatMessages) return;
const alertEl = document.createElement('div');
alertEl.className = `chat-alert chat-alert-${type}`;
@ -1601,7 +1631,7 @@ class ChatManager {
}
static deleteMessage(messageId) {
const messageEl = $(messageId);
const messageEl = $id(messageId);
if (messageEl) {
messageEl.classList.add('fading');
setTimeout(() => {
@ -1655,7 +1685,7 @@ class VoiceControls {
} else {
state.mutedUsers.add(userId);
}
const audioElement = document.getElementById(`audio-${userId}`);
const audioElement = $id(`audio-${userId}`);
if (audioElement) {
audioElement.muted = state.mutedUsers.has(userId);
}
@ -1664,8 +1694,8 @@ class VoiceControls {
state.save();
}
static updateMicVolume() {
const slider = $('#mic-volume-slider');
const percentage = $('#mic-volume-percentage');
const slider = $id('mic-volume-slider');
const percentage = $id('mic-volume-percentage');
state.micVolume = parseInt(slider.value);
percentage.textContent = `${state.micVolume}%`;
@ -1673,8 +1703,8 @@ class VoiceControls {
AudioManager.updateMicrophoneGain();
}
static updateHeadphoneVolume() {
const slider = $('#headphone-volume-slider');
const percentage = $('#headphone-volume-percentage');
const slider = $id('headphone-volume-slider');
const percentage = $id('headphone-volume-percentage');
state.headphoneVolume = parseInt(slider.value);
percentage.textContent = `${state.headphoneVolume}%`;
const audioElements = $$('#audio-container audio');
@ -1687,10 +1717,10 @@ class VoiceControls {
static resetAllVolumes() {
state.micVolume = 100;
state.headphoneVolume = 100;
const micSlider = $('#mic-volume-slider');
const headphoneSlider = $('#headphone-volume-slider');
const micPercentage = $('#mic-volume-percentage');
const headphonePercentage = $('#headphone-volume-percentage');
const micSlider = $id('mic-volume-slider');
const headphoneSlider = $id('headphone-volume-slider');
const micPercentage = $id('mic-volume-percentage');
const headphonePercentage = $id('headphone-volume-percentage');
if (micSlider) {
micSlider.value = 100;
micPercentage.textContent = '100%';
@ -1708,10 +1738,10 @@ class VoiceControls {
// ==================== MODAL MANAGEMENT ====================
class ModalManager {
static openSelfModal() {
const modal = $('#user-control-modal');
const modalUsername = $('#modal-username');
const modal = $id('user-control-modal');
const modalUsername = $id('modal-username');
modalUsername.textContent = 'Settings';
const percentage = $('#headphone-volume-percentage');
const percentage = $id('headphone-volume-percentage');
if (percentage) {
percentage.textContent = `${state.headphoneVolume}%`;
}
@ -1751,7 +1781,7 @@ class ModalManager {
</div>
</div>
`;
const modalUsernameInput = $('#modal-username-input');
const modalUsernameInput = $id('modal-username-input');
modalUsernameInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
@ -1762,15 +1792,15 @@ class ModalManager {
console.log('Opened self audio settings modal');
}
// static closeSelfModal() {
// const modal = $('#user-control-modal');
// const modal = $id('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 = $('#user-control-modal');
const modalUsername = $('#modal-username');
const modal = $id('user-control-modal');
const modalUsername = $id('modal-username');
modalUsername.textContent = `${username} `;
const modalBody = modal.querySelector('.modal-body');
modalBody.innerHTML = `
@ -1794,11 +1824,11 @@ class ModalManager {
</div>
`;
const currentVolume = state.userVolumes.get(userId) || 100;
const volumeSlider = $('#user-volume-slider');
const volumePercentage = $('#volume-percentage');
const volumeSlider = $id('user-volume-slider');
const volumePercentage = $id('volume-percentage');
volumeSlider.value = currentVolume;
volumePercentage.textContent = `${currentVolume}% `;
const muteBtn = $('#modal-mute-btn');
const muteBtn = $id('modal-mute-btn');
const isMuted = state.mutedUsers.has(userId);
if (muteBtn) {
muteBtn.textContent = isMuted ? '🔇 User Muted' : '🔊 Mute User';
@ -1808,20 +1838,20 @@ class ModalManager {
console.log(`Opened modal for user ${username}(${userId})`);
}
static closeUserModal() {
const modal = $('#user-control-modal');
const modal = $id('user-control-modal');
modal.style.display = 'none';
state.currentModalUserId = null;
console.log('Closed user modal');
}
static updateUserVolume() {
if (!state.currentModalUserId) return;
const slider = $('#user-volume-slider');
const volumePercentage = $('#volume-percentage');
const slider = $id('user-volume-slider');
const volumePercentage = $id('volume-percentage');
const volume = parseInt(slider.value);
volumePercentage.textContent = `${volume}% `;
state.userVolumes.set(state.currentModalUserId, volume);
const audioElement = document.getElementById(`audio - ${state.currentModalUserId} `);
const audioElement = $id(`audio - ${state.currentModalUserId} `);
if (audioElement) {
audioElement.volume = volume / 100;
}
@ -1836,15 +1866,15 @@ class ModalManager {
static toggleUserMuteFromModal() {
if (!state.currentModalUserId) return;
VoiceControls.muteUser(state.currentModalUserId);
const muteBtn = $('#modal-mute-btn');
const muteBtn = $id('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 = $('#user-volume-slider');
const volumePercentage = $('#volume-percentage');
const volumeSlider = $id('user-volume-slider');
const volumePercentage = $id('volume-percentage');
volumeSlider.value = 100;
volumePercentage.textContent = '100%';
ModalManager.updateUserVolume();
@ -1854,7 +1884,7 @@ class ModalManager {
// ==================== VOICE CHAT MANAGER ====================
class VoiceChatManager {
static async joinVoiceChat() {
const usernameInput = $('#username-input');
const usernameInput = $id('username-input');
const username = usernameInput.value.trim();
state.intentionalDisconnect = false;
if (!username) {
@ -1899,7 +1929,7 @@ class VoiceChatManager {
PeerConnectionManager.stopPeerHealthCheck();
PeerConnectionManager.cleanupAllConnections();
UserManager.onVoiceChatLeave();
$('#audio-container').innerHTML = '';
$id('audio-container').innerHTML = '';
state.reset();
ModalManager.closeUserModal();
state.intentionalDisconnect = true;
@ -1927,7 +1957,7 @@ class VoiceChatManager {
state.isInVoiceChat = false;
UIManager.updateJoinButton('Join Voice Chat', false);
UIManager.showUsernameUI();
const usernameInput = $('#username-input');
const usernameInput = $id('username-input');
usernameInput.focus();
usernameInput.select();
console.log('Username rejected, reset to initial state');
@ -1936,21 +1966,21 @@ class VoiceChatManager {
// ==================== EVENT HANDLERS ====================
class EventHandlers {
static setupEventListeners() {
const usernameInput = $('#username-input');
const joinChatBtn = $('#join-chat-btn');
const chatInput = $('#chat-input');
const sendBtn = $('#send-btn');
const usernameInput = $id('username-input');
const joinChatBtn = $id('join-chat-btn');
const chatInput = $id('chat-input');
const sendBtn = $id('send-btn');
const chatMessages = $('.chat-messages');
// const fileBtn = $('#file-btn');
// const fileBtn = $id('file-btn');
chatMessages.addEventListener('scroll', function() {
const atBottom = UIManager.isChatScrolledToBottom();
if (!atBottom) {
$('#scroll-btn').disabled = false;
$id('scroll-btn').disabled = false;
}
if (atBottom) {
$('#scroll-btn').disabled = true;
$id('scroll-btn').disabled = true;
}
});
@ -1971,7 +2001,7 @@ class EventHandlers {
const textarea = e.target;
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, window.innerHeight * 0.5) + 'px';
$('#send-btn').disabled = textarea.value.trim().length === 0;
$id('send-btn').disabled = textarea.value.trim().length === 0;
});
chatInput.addEventListener('keydown', function(e) {
@ -1985,7 +2015,7 @@ class EventHandlers {
}
static setupGlobalEventListeners() {
document.addEventListener('click', function(event) {
const modal = $('#user-control-modal');
const modal = $id('user-control-modal');
if (event.target === modal) {
ModalManager.closeUserModal();
}
@ -2018,7 +2048,7 @@ class EventHandlers {
}
static sendChatMessage() {
const chatInput = $('#chat-input');
const chatInput = $id('chat-input');
const message = chatInput.value.trim();
if (!message || !state.ws || state.ws.readyState !== WebSocket.OPEN) {
return;
@ -2026,7 +2056,7 @@ class EventHandlers {
WebSocketManager.sendChatMessage(message);
chatInput.value = '';
chatInput.style.height = 'auto';
$('#send-btn').disabled = true;
$id('send-btn').disabled = true;
}
}
// ==================== DEBUG UTILITIES ====================
@ -2144,7 +2174,7 @@ for (let { event, target } of [
target.addEventListener(event, async function() {
state.load();
if (state.currentUsername) {
$('#username-input').value = state.currentUsername;
$id('username-input').value = state.currentUsername;
}
EventHandlers.setupEventListeners();
await AudioManager.requestMicrophonePermission();

View File

@ -106,7 +106,7 @@
<label for="chat-input"></label>
<div class="chat-input-buttons">
<button id="scroll-btn" onclick="scrollChatToBottom()" disabled></button>
<input type="file" id="file-input" style="display: none;"/>
<input type="file" id="file-input" multiple style="display: none;"/>
<button id="file-btn" onclick='openFilePicker()'>📤</button>
<button id="emoji-btn" onclick="toggleEmojiPicker()">😀</button>
<button id="send-btn" onclick="sendChatMessage()" disabled>Send</button>