|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
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;
|
|
|
|
|
|
const channelList = document.querySelector('.chat-items');
|
|
|
|
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 = '';
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
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 {
|
|
|
|
irc.send('LIST');
|
|
|
|
|
|
const filteredChannels = Array.from(globalChannels)
|
|
.filter(channel => channel.toLowerCase().includes(query.toLowerCase()))
|
|
.slice(0, 20);
|
|
|
|
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('')}
|
|
`;
|
|
|
|
|
|
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 = '';
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
|
|
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;
|
|
|
|
pmMessages[user].slice().reverse().forEach(msg => {
|
|
const msgEl = createMessageElement(msg);
|
|
msgEl.classList.add('pm-message');
|
|
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();
|
|
}
|
|
|
|
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);
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
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 = '';
|
|
});
|
|
|
|
|
|
settingsBtn.addEventListener('click', () => {
|
|
settingsPanel.classList.toggle('open');
|
|
});
|
|
closeSettingsBtn.addEventListener('click', () => {
|
|
settingsPanel.classList.remove('open');
|
|
});
|
|
|
|
|
|
chatSearchInput.addEventListener('input', () => {
|
|
updateChannelList();
|
|
});
|
|
|
|
|
|
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);
|
|
|
|
|
|
backBtn.addEventListener('click', () => {
|
|
channelInfoEl.classList.add('hidden');
|
|
});
|
|
|
|
|
|
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) {
|
|
|
|
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 });
|
|
} else {
|
|
addPMMessage(data.from, { type, from: data.from, text, timestamp: data.timestamp });
|
|
}
|
|
} 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 });
|
|
} else {
|
|
addChannelMessage(channel, { type, from: data.from, text, timestamp: data.timestamp, channel });
|
|
}
|
|
}
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
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 = '';
|
|
messagesContainer.innerHTML = '';
|
|
updateUserList();
|
|
updateHeader();
|
|
channelInfoEl.classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
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;
|
|
}
|
|
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();
|
|
}); |