Download js/main.js from AstraOS/webirc: direct link, hf CLI and curl.
- Browser
- Download file 22.5 kB
-
https://huggingface.co/spaces/AstraOS/webirc/resolve/main/js/main.js
- Command line
-
hf download hf://spaces/AstraOS/webirc/js/main.js
-
curl -L -o main.js https://huggingface.co/spaces/AstraOS/webirc/resolve/main/js/main.js
22.5 kB
| 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 ` | |
| <svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg"> | |
| <circle cx="60" cy="60" r="60" fill="${color}"/> | |
| <text | |
| x="50%" | |
| y="50%" | |
| text-anchor="middle" | |
| dominant-baseline="middle" | |
| font-size="48" | |
| font-family="sans-serif" | |
| fill="white" | |
| >${initials}</text> | |
| </svg> | |
| `; | |
| } | |
| 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 = ` | |
| <div class="section-header">Connected Channels</div> | |
| `; | |
| 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 = ` | |
| <div class="avatar">${generateAvatar(channel)}</div> | |
| <div class="chat-info"> | |
| <div class="chat-name">${channel}</div> | |
| <div class="chat-snippet">${ | |
| channelMessages[channel]?.length ? | |
| channelMessages[channel][channelMessages[channel].length - 1].text : | |
| irc.getChannelTopic(channel) || 'No topic set' | |
| }</div> | |
| </div> | |
| <div class="unread-badge">${irc.getChannelUsers(channel).length}</div> | |
| `; | |
| 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 = '<div class="section-header">Loading channels...</div>'; | |
| 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 = ` | |
| <div class="section-header">Global Channels</div> | |
| ${filteredChannels.map(channel => ` | |
| <div class="search-result-item"> | |
| <span>${channel}</span> | |
| ${!activeChannels.has(channel) ? | |
| `<button class="join-button" data-channel="${channel}">Join</button>` : | |
| '<span>Joined</span>' | |
| } | |
| </div> | |
| `).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 = '<div class="section-header">Error loading channels</div>'; | |
| } | |
| }, 300); | |
| } | |
| function updateUserList() { | |
| if (!selectedChannel) { | |
| userListEl.innerHTML = ''; | |
| return; | |
| } | |
| const users = irc.getChannelUsers(selectedChannel); | |
| userListEl.innerHTML = ` | |
| <div class="user-list-header"> | |
| <h3>Users (${users.length})</h3> | |
| </div> | |
| <div class="user-list-content"> | |
| ${users.map(user => ` | |
| <div class="user-item" data-user="${user}"> | |
| <div class="user-avatar">${generateAvatar(user)}</div> | |
| <div class="user-name">${user}</div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| 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 ? `<div class="channel-topic">${topic}</div>` : ''} | |
| `; | |
| 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 = ` | |
| <div class="user-list-header"> | |
| <h3>Users (${users.length})</h3> | |
| </div> | |
| <div class="user-list-content"> | |
| ${users.map(user => ` | |
| <div class="user-item" data-user="${user}"> | |
| <div class="user-avatar">${generateAvatar(user)}</div> | |
| <div class="user-name">${user}</div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| 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 => ` | |
| <div class="user-item" data-user="${user}"> | |
| <div class="user-avatar">${generateAvatar(user)}</div> | |
| <div class="user-name">${user}</div> | |
| </div> | |
| `).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 = ` | |
| <div class="section-header">Theme</div> | |
| ${themes.map(theme => ` | |
| <label class="theme-option"> | |
| <input type="radio" name="theme" value="${theme.class}"> | |
| ${theme.name} | |
| </label> | |
| `).join('')} | |
| `; | |
| document.querySelector('.settings-content').appendChild(themeSelector); | |
| } | |
| initializeUI(); | |
| initializeThemeSelector(); | |
| }); |