document.addEventListener('DOMContentLoaded', () => { // ...existing code... const irc = new IRCClient(); const messageForm = document.getElementById('messageForm'); const messageInput = document.getElementById('messageText'); const messagesContainer = document.getElementById('messages'); const connectBtn = document.getElementById('connectBtn'); const connectionDialog = document.getElementById('connectionDialog'); const connectionForm = document.getElementById('connectionForm'); const chatSearchInput = document.getElementById('chatSearch'); const settingsBtn = document.getElementById('settingsBtn'); const settingsPanel = document.getElementById('settingsPanel'); const closeSettingsBtn = document.getElementById('closeSettingsBtn'); const connectionStatusEl = document.getElementById('connectionStatus'); const serverInfoText = document.getElementById('serverInfoText'); const reconnectStatusEl = document.getElementById('reconnectStatus'); const pmDialog = document.getElementById('pmDialog'); const pmCloseBtn = document.getElementById('pmCloseBtn'); const pmMessagesContainer = document.getElementById('pmMessages'); const pmInput = document.getElementById('pmInput'); const backBtn = document.getElementById('backBtn'); let activeChannels = new Set(); let selectedChannel = ''; let currentChannel = ''; let currentPMUser = ''; let channelMessages = {}; let pmMessages = {}; let globalChannels = new Set(); let searchTimeout = null; // UI Components const channelList = document.querySelector('.chat-items'); // Create user list if not already present let userListEl = document.querySelector('.user-list'); if (!userListEl) { userListEl = document.createElement('div'); userListEl.classList.add('user-list'); document.querySelector('.chat-window').appendChild(userListEl); } const channelInfoEl = document.getElementById('channelInfo'); const channelUserListEl = document.getElementById('channelUserList'); const channelNameEl = document.getElementById('channelName'); const channelTopicEl = document.getElementById('topicText'); const userSearchInput = document.getElementById('userSearch'); function generateAvatar(name) { const colors = ["#1abc9c", "#2ecc71", "#3498db", "#9b59b6", "#34495e", "#16a085", "#27ae60", "#2980b9", "#8e44ad", "#2c3e50", "#f1c40f", "#e67e22", "#e74c3c", "#95a5a6", "#f39c12", "#d35400", "#c0392b", "#bdc3c7", "#77b1a9", "#94d82d", "#a327ff", "#f7e01d", "#e64a19", "#ecf0f1"]; const colorIndex = name.charCodeAt(0) % colors.length; const color = colors[colorIndex]; const initials = name.substring(0, 2).toUpperCase(); return ` ${initials} `; } function updateChannelList() { const searchQuery = chatSearchInput.value.toLowerCase(); channelList.innerHTML = ''; // Connected channels section if (activeChannels.size > 0) { const connectedSection = document.createElement('div'); connectedSection.className = 'channel-category'; connectedSection.innerHTML = `
Connected Channels
`; Array.from(activeChannels) .filter(channel => channel.toLowerCase().includes(searchQuery)) .forEach(channel => { const li = createChannelListItem(channel); connectedSection.appendChild(li); }); channelList.appendChild(connectedSection); } // Update search results if query exists if (searchQuery.length >= 2) { searchGlobalChannels(searchQuery); } } function createChannelListItem(channel) { const li = document.createElement('li'); li.classList.add('chat-item'); if (channel === selectedChannel) li.classList.add('active'); li.innerHTML = `
${generateAvatar(channel)}
${channel}
${ channelMessages[channel]?.length ? channelMessages[channel][channelMessages[channel].length - 1].text : irc.getChannelTopic(channel) || 'No topic set' }
${irc.getChannelUsers(channel).length}
`; li.addEventListener('click', () => { selectedChannel = channel; currentChannel = channel; updateChannelList(); updateUserList(); updateHeader(); displayMessagesForCurrentChannel(); channelInfoEl.classList.remove('hidden'); showChannelInfo(channel); }); return li; } async function searchGlobalChannels(query) { if (searchTimeout) clearTimeout(searchTimeout); searchTimeout = setTimeout(async () => { const searchResults = document.createElement('div'); searchResults.className = 'search-results'; if (query.length < 2) { const existing = document.querySelector('.search-results'); if (existing) existing.remove(); return; } searchResults.innerHTML = '
Loading channels...
'; chatSearchInput.parentElement.appendChild(searchResults); try { // Request channel list from server irc.send('LIST'); // Filter channels based on query const filteredChannels = Array.from(globalChannels) .filter(channel => channel.toLowerCase().includes(query.toLowerCase())) .slice(0, 20); // Limit results searchResults.innerHTML = `
Global Channels
${filteredChannels.map(channel => `
${channel} ${!activeChannels.has(channel) ? `` : 'Joined' }
`).join('')} `; // Add click handlers for join buttons searchResults.querySelectorAll('.join-button').forEach(btn => { btn.addEventListener('click', () => { const channel = btn.dataset.channel; irc.join(channel); activeChannels.add(channel); selectedChannel = channel; updateChannelList(); searchResults.remove(); }); }); } catch (error) { searchResults.innerHTML = '
Error loading channels
'; } }, 300); } function updateUserList() { if (!selectedChannel) { userListEl.innerHTML = ''; return; } const users = irc.getChannelUsers(selectedChannel); userListEl.innerHTML = `

Users (${users.length})

${users.map(user => `
${generateAvatar(user)}
${user}
`).join('')}
`; userListEl.querySelectorAll('.user-item').forEach(item => { item.addEventListener('click', () => { const user = item.getAttribute('data-user'); showPMDialog(user); }); }); } function updateHeader() { const header = document.querySelector('.chat-header h2'); const avatar = document.querySelector('.chat-header .avatar'); if (selectedChannel) { const topic = irc.getChannelTopic(selectedChannel); header.innerHTML = ` ${selectedChannel} ${topic ? `
${topic}
` : ''} `; avatar.innerHTML = generateAvatar(selectedChannel); } else { header.textContent = 'Welcome to Libera Chat'; avatar.innerHTML = ''; // Clear avatar when no channel selected } } function addChannelMessage(channel, message) { if (!channel) return; if (!channelMessages[channel]) { channelMessages[channel] = []; } channelMessages[channel].push(message); if (selectedChannel === channel) { displayMessagesForCurrentChannel(); } } function displayMessagesForCurrentChannel() { messagesContainer.innerHTML = ''; if (!selectedChannel || !channelMessages[selectedChannel]) return; channelMessages[selectedChannel].forEach(msg => { const el = createMessageElement(msg); messagesContainer.appendChild(el); }); messagesContainer.scrollTop = messagesContainer.scrollHeight; } function createMessageElement(msg) { const messageWrapper = document.createElement('div'); messageWrapper.classList.add('message', msg.type); const bubble = document.createElement('div'); bubble.classList.add('message-bubble'); if (msg.from && msg.type !== 'system' && msg.type !== 'error') { const nameSpan = document.createElement('span'); nameSpan.classList.add('message-sender'); nameSpan.textContent = msg.from; nameSpan.style.cursor = 'pointer'; nameSpan.addEventListener('click', () => { showPMDialog(msg.from); }); bubble.appendChild(nameSpan); } const textSpan = document.createElement('span'); textSpan.classList.add('message-text'); textSpan.textContent = msg.text; bubble.appendChild(textSpan); // Timestamp element const timeSpan = document.createElement('span'); timeSpan.classList.add('message-time'); const date = new Date(msg.timestamp); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); timeSpan.textContent = `${hours}:${minutes}`; bubble.appendChild(timeSpan); messageWrapper.appendChild(bubble); return messageWrapper; } function addPMMessage(user, msg) { if (!user) return; if (!pmMessages[user]) { pmMessages[user] = []; } pmMessages[user].push(msg); if (currentPMUser === user) { displayPMMessages(user); } } function displayPMMessages(user) { pmMessagesContainer.innerHTML = ''; if (!pmMessages[user]) return; // Display messages in reverse order to show newest at the bottom pmMessages[user].slice().reverse().forEach(msg => { const msgEl = createMessageElement(msg); // Reuse createMessageElement msgEl.classList.add('pm-message'); // Add class for PM styling if needed pmMessagesContainer.appendChild(msgEl); }); pmMessagesContainer.scrollTop = pmMessagesContainer.scrollHeight; } function showPMDialog(user) { currentPMUser = user; document.getElementById('pmUserName').textContent = `Chat with ${user}`; pmDialog.classList.remove('hidden'); displayPMMessages(user); pmInput.focus(); // Focus on input when PM dialog opens } function hidePMDialog() { pmDialog.classList.add('hidden'); currentPMUser = ''; } function showConnectionDialog() { connectionDialog.style.display = 'flex'; } function hideConnectionDialog() { connectionDialog.style.display = 'none'; } function updateConnectionStatus(status) { connectionStatusEl.textContent = status; if (status.toLowerCase().includes('connected')) { connectionStatusEl.classList.add('hidden'); } else { connectionStatusEl.classList.remove('hidden'); } } function initializeUI() { messageInput.disabled = true; messageForm.querySelector('button').disabled = true; } function showChannelInfo(channel) { channelNameEl.textContent = channel; channelTopicEl.textContent = irc.getChannelTopic(channel) || 'No topic set'; const users = irc.getChannelUsers(channel); channelUserListEl.innerHTML = `

Users (${users.length})

${users.map(user => `
${generateAvatar(user)}
${user}
`).join('')}
`; channelUserListEl.querySelectorAll('.user-item').forEach(item => { item.addEventListener('click', () => { const user = item.getAttribute('data-user'); showPMDialog(user); }); }); } // Event Listeners connectBtn.addEventListener('click', () => { showConnectionDialog(); }); connectionDialog.querySelector('.cancel').addEventListener('click', () => { hideConnectionDialog(); }); connectionForm.addEventListener('submit', (e) => { e.preventDefault(); const nickname = document.getElementById('nickname').value; currentChannel = document.getElementById('channel').value; if (!nickname || !currentChannel) { addChannelMessage(currentChannel, { type: 'error', text: 'Please enter both nickname and channel', timestamp: Date.now(), channel: currentChannel }); return; } hideConnectionDialog(); updateConnectionStatus('Connecting to Libera Chat...'); addChannelMessage(currentChannel, { type: 'system', text: 'Connecting to Libera Chat...', timestamp: Date.now(), channel: currentChannel }); try { irc.connect(nickname); activeChannels.add(currentChannel); selectedChannel = currentChannel; updateChannelList(); } catch (error) { addChannelMessage(currentChannel, { type: 'error', text: `Failed to connect: ${error.message}`, timestamp: Date.now(), channel: currentChannel }); } }); messageForm.addEventListener('submit', (e) => { e.preventDefault(); const text = messageInput.value.trim(); if (!text) return; if (text.startsWith('/')) { const [command, ...args] = text.slice(1).split(' '); switch (command.toLowerCase()) { case 'join': if (args[0]) { let channel = args[0]; if (!channel.startsWith('#')) channel = '#' + channel; irc.join(channel); activeChannels.add(channel); selectedChannel = channel; currentChannel = channel; updateChannelList(); } break; case 'part': if (selectedChannel) { irc.part(selectedChannel); activeChannels.delete(selectedChannel); selectedChannel = ''; updateChannelList(); messagesContainer.innerHTML = ''; updateHeader(); // Clear header when leaving channel } break; case 'me': if (args.length > 0 && currentChannel) { const action = args.join(' '); irc.send(`PRIVMSG ${currentChannel} :\u0001ACTION ${action}\u0001`); addChannelMessage(currentChannel, { type: 'action', from: irc.nickname, text: action, timestamp: Date.now(), channel: currentChannel }); } break; case 'msg': if (args.length >= 2) { const [target, ...msgParts] = args; const message = msgParts.join(' '); irc.sendPrivateMessage(target, message); addPMMessage(target, { type: 'sent', from: irc.nickname, text: message, timestamp: Date.now() }); } break; default: addChannelMessage(currentChannel, { type: 'system', text: 'Unknown command', timestamp: Date.now(), channel: currentChannel }); } } else if (currentChannel) { irc.sendMessage(currentChannel, text); addChannelMessage(currentChannel, { type: 'sent', from: irc.nickname, text, timestamp: Date.now(), channel: currentChannel }); } messageInput.value = ''; }); // Settings panel toggle settingsBtn.addEventListener('click', () => { settingsPanel.classList.toggle('open'); }); closeSettingsBtn.addEventListener('click', () => { settingsPanel.classList.remove('open'); }); // Channel search listener chatSearchInput.addEventListener('input', () => { updateChannelList(); }); // PM dialog input listener pmInput.addEventListener('keyup', (e) => { if (e.key === 'Enter' && currentPMUser) { const text = pmInput.value.trim(); if (text) { irc.sendPrivateMessage(currentPMUser, text); addPMMessage(currentPMUser, { type: 'sent', from: irc.nickname, text, timestamp: Date.now() }); pmInput.value = ''; } } }); pmCloseBtn.addEventListener('click', hidePMDialog); // Back button listener backBtn.addEventListener('click', () => { channelInfoEl.classList.add('hidden'); }); // IRC event handlers irc.on('connected', () => { addChannelMessage(currentChannel, { type: 'system', text: 'Connected to Libera Chat', timestamp: Date.now(), channel: currentChannel }); updateConnectionStatus('Connected to Libera Chat'); messageInput.disabled = false; messageForm.querySelector('button').disabled = false; if (currentChannel) { irc.join(currentChannel); } }); irc.on('message', (data) => { if (data.target === irc.nickname) { // Private message const type = data.from === irc.nickname ? 'sent' : 'received'; let text = data.message; if (data.isAction) { text = text.replace(/^\u0001ACTION /, '').replace(/\u0001$/, ''); addPMMessage(data.from, { type: 'action', from: data.from, text, timestamp: data.timestamp }); // Use timestamp from IRC event } else { addPMMessage(data.from, { type, from: data.from, text, timestamp: data.timestamp }); // Use timestamp from IRC event } } else { const channel = data.target; const type = data.from === irc.nickname ? 'sent' : 'received'; let text = data.message; if (data.isAction) { text = text.replace(/^\u0001ACTION /, '').replace(/\u0001$/, ''); addChannelMessage(channel, { type: 'action', from: data.from, text, timestamp: data.timestamp, channel }); // Use timestamp from IRC event } else { addChannelMessage(channel, { type, from: data.from, text, timestamp: data.timestamp, channel }); // Use timestamp from IRC event } } }); irc.on('join', (data) => { addChannelMessage(data.channel, { type: 'system', text: `${data.nick} joined ${data.channel}`, timestamp: Date.now(), channel: data.channel }); irc.requestChannelInfo(data.channel); // Request channel info on join }); irc.on('part', (data) => { addChannelMessage(data.channel, { type: 'system', text: `${data.nick} left ${data.channel}`, timestamp: Date.now(), channel: data.channel }); if (data.nick === irc.nickname && selectedChannel === data.channel) { selectedChannel = ''; // Clear selected channel if user parts messagesContainer.innerHTML = ''; // Clear messages updateUserList(); // Clear user list updateHeader(); // Update header channelInfoEl.classList.add('hidden'); // Hide channel info } }); irc.on('error', (error) => { let errorMessage = error; if (error instanceof Event) { errorMessage = 'Connection error occurred'; } else if (typeof error === 'object') { errorMessage = error.message || 'Unknown error occurred'; } addChannelMessage(currentChannel, { type: 'error', text: `Error: ${errorMessage}`, timestamp: Date.now(), channel: currentChannel }); messageInput.disabled = true; messageForm.querySelector('button').disabled = true; }); irc.onChannelUsers(({ channel, users }) => { if (channel === selectedChannel) { updateUserList(); } updateChannelList(); }); irc.onTopic(({ channel, topic }) => { if (channel === selectedChannel) { updateHeader(); channelTopicEl.textContent = topic; // Update topic in channel info as well } updateChannelList(); }); irc.onServerInfo((info) => { if (serverInfoText) { serverInfoText.textContent = `Users: ${info.users} | Channels: ${info.channels}`; } }); irc.onChannelList((channels) => { globalChannels = new Set(channels); }); userSearchInput.addEventListener('input', () => { const searchQuery = userSearchInput.value.toLowerCase(); const users = irc.getChannelUsers(selectedChannel); const filteredUsers = users.filter(user => user.toLowerCase().includes(searchQuery)); channelUserListEl.querySelector('.user-list-content').innerHTML = ` ${filteredUsers.map(user => `
${generateAvatar(user)}
${user}
`).join('')} `; channelUserListEl.querySelectorAll('.user-item').forEach(item => { item.addEventListener('click', () => { const user = item.getAttribute('data-user'); showPMDialog(user); }); }); }); function initializeThemeSelector() { const themes = [ { name: 'Light', class: 'theme-light' }, { name: 'Dark', class: 'theme-dark' }, { name: 'System', class: 'theme-system' } ]; const themeSelector = document.createElement('div'); themeSelector.className = 'theme-switcher'; themeSelector.innerHTML = `
Theme
${themes.map(theme => ` `).join('')} `; document.querySelector('.settings-content').appendChild(themeSelector); } initializeUI(); initializeThemeSelector(); });