webirc / js /main.js
AstraOS's picture
Upload 4 files
814ddd5 verified
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();
});