UI - Change
This commit is contained in:
500
src/UI.js
Normal file
500
src/UI.js
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
/**
|
||||||
|
// this is needed for Tauri to work properly!
|
||||||
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
//
|
||||||
|
|
||||||
|
let greetInputEl;
|
||||||
|
let greetMsgEl;
|
||||||
|
|
||||||
|
async function greet() {
|
||||||
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
|
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
greetInputEl = document.querySelector("#greet-input");
|
||||||
|
greetMsgEl = document.querySelector("#greet-msg");
|
||||||
|
document.querySelector("#greet-form").addEventListener("submit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
greet();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// UI State Management
|
||||||
|
const uiState = {
|
||||||
|
connected: false,
|
||||||
|
activeTab: null,
|
||||||
|
channels: [],
|
||||||
|
collapsedSidebar: false,
|
||||||
|
userColors: new Map(),
|
||||||
|
currentUser: 'IRCDUser'
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
const sidebar = document.getElementById('channels');
|
||||||
|
const sidebarResizeHandle = document.getElementById('sidebarResizeHandle');
|
||||||
|
const toggleSidebarBtn = document.getElementById('toggleSidebar');
|
||||||
|
const connectBtn = document.getElementById('connectBtn');
|
||||||
|
const disconnectBtn = document.getElementById('disconnectBtn');
|
||||||
|
const joinBtn = document.getElementById('joinBtn');
|
||||||
|
const partBtn = document.getElementById('partBtn');
|
||||||
|
const sendBtn = document.getElementById('sendBtn');
|
||||||
|
const statusDot = document.getElementById('statusDot');
|
||||||
|
const statusText = document.getElementById('status');
|
||||||
|
const tabBar = document.getElementById('tabBar');
|
||||||
|
const messagesContainer = document.getElementById('messages');
|
||||||
|
const channelList = document.getElementById('channelList');
|
||||||
|
const messageInput = document.getElementById('messageInput');
|
||||||
|
const channelInput = document.getElementById('newChannel');
|
||||||
|
const commandSuggestions = document.getElementById('commandSuggestions');
|
||||||
|
|
||||||
|
// Color palette for users
|
||||||
|
const userColors = [
|
||||||
|
'#4b82ff', '#ff6b6b', '#7edba5', '#ffa94d',
|
||||||
|
'#cc5de8', '#20c997', '#f06595', '#748ffc',
|
||||||
|
'#ffd43b', '#69db7c', '#a9e34b', '#ff8787'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Initialize UI
|
||||||
|
function initUI() {
|
||||||
|
// Set current user from nickname input
|
||||||
|
const nicknameInput = document.getElementById('nickname');
|
||||||
|
uiState.currentUser = nicknameInput.value;
|
||||||
|
nicknameInput.addEventListener('change', (e) => {
|
||||||
|
uiState.currentUser = e.target.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
toggleSidebarBtn.addEventListener('click', toggleSidebar);
|
||||||
|
connectBtn.addEventListener('click', handleConnect);
|
||||||
|
disconnectBtn.addEventListener('click', handleDisconnect);
|
||||||
|
joinBtn.addEventListener('click', handleJoin);
|
||||||
|
partBtn.addEventListener('click', handlePart);
|
||||||
|
sendBtn.addEventListener('click', handleSend);
|
||||||
|
messageInput.addEventListener('keydown', handleMessageInput);
|
||||||
|
channelInput.addEventListener('keydown', handleChannelInput);
|
||||||
|
|
||||||
|
// Set up sidebar resizing
|
||||||
|
setupSidebarResize();
|
||||||
|
|
||||||
|
// Set active tab
|
||||||
|
setActiveTab('welcome');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup sidebar resizing
|
||||||
|
function setupSidebarResize() {
|
||||||
|
let isResizing = false;
|
||||||
|
|
||||||
|
sidebarResizeHandle.addEventListener('mousedown', (e) => {
|
||||||
|
isResizing = true;
|
||||||
|
sidebarResizeHandle.classList.add('resizing');
|
||||||
|
document.body.style.cursor = 'col-resize';
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
|
||||||
|
const newWidth = e.clientX;
|
||||||
|
if (newWidth > 200 && newWidth < 500) {
|
||||||
|
sidebar.style.width = `${newWidth}px`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', () => {
|
||||||
|
if (isResizing) {
|
||||||
|
isResizing = false;
|
||||||
|
sidebarResizeHandle.classList.remove('resizing');
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle sidebar
|
||||||
|
function toggleSidebar() {
|
||||||
|
uiState.collapsedSidebar = !uiState.collapsedSidebar;
|
||||||
|
sidebar.classList.toggle('collapsed', uiState.collapsedSidebar);
|
||||||
|
|
||||||
|
// Update toggle button icon
|
||||||
|
const icon = toggleSidebarBtn.querySelector('svg');
|
||||||
|
if (uiState.collapsedSidebar) {
|
||||||
|
icon.innerHTML = '<path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
} else {
|
||||||
|
icon.innerHTML = '<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get color for a user
|
||||||
|
function getUserColor(username) {
|
||||||
|
if (!uiState.userColors.has(username)) {
|
||||||
|
// Assign a color from the palette based on username hash
|
||||||
|
const hash = username.split('').reduce((a, b) => {
|
||||||
|
a = ((a << 5) - a) + b.charCodeAt(0);
|
||||||
|
return a & a;
|
||||||
|
}, 0);
|
||||||
|
const colorIndex = Math.abs(hash) % userColors.length;
|
||||||
|
uiState.userColors.set(username, userColors[colorIndex]);
|
||||||
|
}
|
||||||
|
return uiState.userColors.get(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection handlers
|
||||||
|
function handleConnect() {
|
||||||
|
uiState.connected = true;
|
||||||
|
updateConnectionStatus();
|
||||||
|
addSystemMessage(`Connecting to ${document.getElementById('server').value}...`);
|
||||||
|
|
||||||
|
// Simulate connection
|
||||||
|
setTimeout(() => {
|
||||||
|
addSystemMessage('Connected to server!');
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDisconnect() {
|
||||||
|
uiState.connected = false;
|
||||||
|
updateConnectionStatus();
|
||||||
|
addSystemMessage('Disconnected from server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConnectionStatus() {
|
||||||
|
if (uiState.connected) {
|
||||||
|
statusDot.classList.add('online');
|
||||||
|
statusText.textContent = 'Connected';
|
||||||
|
connectBtn.disabled = true;
|
||||||
|
disconnectBtn.disabled = false;
|
||||||
|
} else {
|
||||||
|
statusDot.classList.remove('online');
|
||||||
|
statusText.textContent = 'Disconnected';
|
||||||
|
connectBtn.disabled = false;
|
||||||
|
disconnectBtn.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel handlers
|
||||||
|
function handleJoin() {
|
||||||
|
let channel = channelInput.value.trim();
|
||||||
|
if (channel && !channel.startsWith('#')) {
|
||||||
|
channel = '#' + channel;
|
||||||
|
channelInput.value = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel) {
|
||||||
|
addChannel(channel);
|
||||||
|
channelInput.value = '';
|
||||||
|
addSystemMessage(`Joined ${channel}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePart() {
|
||||||
|
if (uiState.activeTab && uiState.activeTab !== 'welcome') {
|
||||||
|
const channel = uiState.activeTab;
|
||||||
|
removeChannel(channel);
|
||||||
|
addSystemMessage(`Left ${channel}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addChannel(name) {
|
||||||
|
if (!uiState.channels.includes(name)) {
|
||||||
|
uiState.channels.push(name);
|
||||||
|
|
||||||
|
// Add to channel list
|
||||||
|
const channelItem = document.createElement('div');
|
||||||
|
channelItem.className = 'channel-item';
|
||||||
|
channelItem.innerHTML = `
|
||||||
|
<div class="channel-icon">#</div>
|
||||||
|
<div class="channel-name">${name}</div>
|
||||||
|
`;
|
||||||
|
channelItem.addEventListener('click', () => setActiveTab(name));
|
||||||
|
channelList.appendChild(channelItem);
|
||||||
|
|
||||||
|
// Add tab
|
||||||
|
addTab(name);
|
||||||
|
|
||||||
|
// Auto-join the channel when connected
|
||||||
|
if (uiState.connected) {
|
||||||
|
invoke('join_channel', { channel: name }).catch(e => {
|
||||||
|
addSystemMessage(`Failed to join ${name}: ${e}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeChannel(name) {
|
||||||
|
const index = uiState.channels.indexOf(name);
|
||||||
|
if (index > -1) {
|
||||||
|
uiState.channels.splice(index, 1);
|
||||||
|
|
||||||
|
// Remove from channel list
|
||||||
|
const channelItems = channelList.querySelectorAll('.channel-item');
|
||||||
|
channelItems.forEach(item => {
|
||||||
|
if (item.querySelector('.channel-name').textContent === name) {
|
||||||
|
item.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove tab
|
||||||
|
removeTab(name);
|
||||||
|
|
||||||
|
// Auto-leave the channel when connected
|
||||||
|
if (uiState.connected) {
|
||||||
|
invoke('part_channel', { channel: name }).catch(e => {
|
||||||
|
addSystemMessage(`Failed to leave ${name}: ${e}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab management
|
||||||
|
function addTab(name) {
|
||||||
|
const tab = document.createElement('div');
|
||||||
|
tab.className = 'tab';
|
||||||
|
tab.dataset.tab = name;
|
||||||
|
tab.innerHTML = `
|
||||||
|
<span>${name}</span>
|
||||||
|
<span class="tab-close">×</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tab.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('tab-close')) {
|
||||||
|
removeTab(name);
|
||||||
|
} else {
|
||||||
|
setActiveTab(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tabBar.appendChild(tab);
|
||||||
|
setActiveTab(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTab(name) {
|
||||||
|
const tab = tabBar.querySelector(`[data-tab="${name}"]`);
|
||||||
|
if (tab) {
|
||||||
|
tab.remove();
|
||||||
|
|
||||||
|
// If this was the active tab, switch to another
|
||||||
|
if (uiState.activeTab === name) {
|
||||||
|
if (uiState.channels.length > 0) {
|
||||||
|
setActiveTab(uiState.channels[0]);
|
||||||
|
} else {
|
||||||
|
setActiveTab('welcome');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveTab(name) {
|
||||||
|
uiState.activeTab = name;
|
||||||
|
|
||||||
|
// Update tabs
|
||||||
|
const tabs = tabBar.querySelectorAll('.tab');
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.classList.toggle('active', tab.dataset.tab === name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update channels
|
||||||
|
const channelItems = channelList.querySelectorAll('.channel-item');
|
||||||
|
channelItems.forEach(item => {
|
||||||
|
const channelName = item.querySelector('.channel-name').textContent;
|
||||||
|
item.classList.toggle('active', channelName === name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update messages
|
||||||
|
updateMessagesForTab(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMessagesForTab(name) {
|
||||||
|
// Clear messages
|
||||||
|
messagesContainer.innerHTML = '';
|
||||||
|
|
||||||
|
// Add appropriate messages for the tab
|
||||||
|
if (name === 'welcome') {
|
||||||
|
addSystemMessage('IRCD - Welcome! SystemTime: ' + new Date().toLocaleString());
|
||||||
|
} else {
|
||||||
|
addSystemMessage(`Welcome to #${name}!`);
|
||||||
|
addSystemMessage('This is the beginning of the channel.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message handling
|
||||||
|
function handleSend() {
|
||||||
|
const message = messageInput.value.trim();
|
||||||
|
if (message) {
|
||||||
|
if (message.startsWith('/')) {
|
||||||
|
handleCommand(message);
|
||||||
|
} else {
|
||||||
|
const target = uiState.activeTab;
|
||||||
|
if (target && target !== 'welcome') {
|
||||||
|
addMessage(uiState.currentUser, message, true);
|
||||||
|
// Send the message to the server
|
||||||
|
invoke('send_message', { target, message }).catch(e => {
|
||||||
|
addSystemMessage(`Failed to send message: ${e}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addSystemMessage('Join a channel to send messages.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messageInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCommand(command) {
|
||||||
|
addSystemMessage(`Command: ${command}`);
|
||||||
|
// Handle specific commands here
|
||||||
|
if (command.startsWith('/join ')) {
|
||||||
|
const channel = command.split(' ')[1];
|
||||||
|
if (channel) {
|
||||||
|
addChannel(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessage(sender, content, isUser = false) {
|
||||||
|
const messageLine = document.createElement('div');
|
||||||
|
messageLine.className = 'message-line';
|
||||||
|
|
||||||
|
const senderElement = document.createElement('div');
|
||||||
|
senderElement.className = `message-sender ${isUser ? 'you' : ''}`;
|
||||||
|
senderElement.textContent = isUser ? `${sender} (You):` : `${sender}:`;
|
||||||
|
senderElement.style.color = isUser ? '' : getUserColor(sender);
|
||||||
|
|
||||||
|
const contentElement = document.createElement('div');
|
||||||
|
contentElement.className = 'message-content';
|
||||||
|
contentElement.textContent = content;
|
||||||
|
|
||||||
|
messageLine.appendChild(senderElement);
|
||||||
|
messageLine.appendChild(contentElement);
|
||||||
|
|
||||||
|
messagesContainer.appendChild(messageLine);
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSystemMessage(content) {
|
||||||
|
const systemMessage = document.createElement('div');
|
||||||
|
systemMessage.className = 'system-message';
|
||||||
|
systemMessage.textContent = content;
|
||||||
|
|
||||||
|
messagesContainer.appendChild(systemMessage);
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addIRCMessage(content) {
|
||||||
|
const ircMessage = document.createElement('div');
|
||||||
|
ircMessage.className = 'irc-message';
|
||||||
|
ircMessage.textContent = content;
|
||||||
|
|
||||||
|
messagesContainer.appendChild(ircMessage);
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input handlers
|
||||||
|
function handleMessageInput(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChannelInput(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleJoin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format IRC messages
|
||||||
|
function formatIRCMessage(message) {
|
||||||
|
try {
|
||||||
|
// Parse the message object from Tauri
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different message types
|
||||||
|
if (message.command) {
|
||||||
|
const command = message.command;
|
||||||
|
|
||||||
|
// Handle PRIVMSG (regular messages)
|
||||||
|
if (command === 'PRIVMSG' && message.params && message.params.length >= 2) {
|
||||||
|
const sender = message.prefix ? extractNickname(message.prefix) : 'Unknown';
|
||||||
|
const target = message.params[0];
|
||||||
|
const content = message.params[1];
|
||||||
|
|
||||||
|
// Check if this message is for the active tab
|
||||||
|
if (target === uiState.activeTab || (target === uiState.currentUser && uiState.activeTab === sender)) {
|
||||||
|
addMessage(sender, content);
|
||||||
|
}
|
||||||
|
return `${sender} -> ${target}: ${content}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JOIN
|
||||||
|
if (command === 'JOIN' && message.params && message.params.length >= 1) {
|
||||||
|
const sender = message.prefix ? extractNickname(message.prefix) : 'Unknown';
|
||||||
|
const channel = message.params[0];
|
||||||
|
|
||||||
|
if (sender === uiState.currentUser) {
|
||||||
|
addSystemMessage(`You joined ${channel}`);
|
||||||
|
if (!uiState.channels.includes(channel)) {
|
||||||
|
addChannel(channel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addSystemMessage(`${sender} joined ${channel}`);
|
||||||
|
}
|
||||||
|
return `${sender} joined ${channel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle PART
|
||||||
|
if (command === 'PART' && message.params && message.params.length >= 1) {
|
||||||
|
const sender = message.prefix ? extractNickname(message.prefix) : 'Unknown';
|
||||||
|
const channel = message.params[0];
|
||||||
|
const reason = message.params[1] || '';
|
||||||
|
|
||||||
|
if (sender === uiState.currentUser) {
|
||||||
|
addSystemMessage(`You left ${channel}${reason ? `: ${reason}` : ''}`);
|
||||||
|
removeChannel(channel);
|
||||||
|
} else {
|
||||||
|
addSystemMessage(`${sender} left ${channel}${reason ? `: ${reason}` : ''}`);
|
||||||
|
}
|
||||||
|
return `${sender} left ${channel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle server responses
|
||||||
|
if (typeof command === 'object' && command.Response) {
|
||||||
|
const [code, ...params] = command.Response;
|
||||||
|
return `[${code}] ${params.join(' ')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle raw commands
|
||||||
|
if (typeof command === 'object' && command.Raw) {
|
||||||
|
const [code, ...params] = command.Raw;
|
||||||
|
return `[RAW ${code}] ${params.join(' ')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: stringify the message
|
||||||
|
return JSON.stringify(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(message);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error formatting IRC message:', e);
|
||||||
|
return String(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractNickname(prefix) {
|
||||||
|
if (typeof prefix === 'string') {
|
||||||
|
return prefix.split('!')[0];
|
||||||
|
}
|
||||||
|
if (prefix && prefix.Nickname) {
|
||||||
|
return prefix.Nickname[0];
|
||||||
|
}
|
||||||
|
if (prefix && prefix.ServerName) {
|
||||||
|
return prefix.ServerName;
|
||||||
|
}
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the UI when the page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', initUI);
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
function formatIRCMessage(msg) {
|
|
||||||
// Tauri gives us the message as a stringified Rust Debug object
|
|
||||||
// e.g. Message { tags: None, prefix: Some(Nickname("Nick", "", "")), command: PRIVMSG("#chan", "Hello") }
|
|
||||||
// We'll try to parse key info using regex
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Extract the prefix (who sent it)
|
|
||||||
const prefixMatch = msg.match(/prefix: Some\(([^)]+)\)/);
|
|
||||||
let sender = prefixMatch ? prefixMatch[1] : "Unknown";
|
|
||||||
|
|
||||||
// Extract the command and arguments
|
|
||||||
const cmdMatch = msg.match(/command: (\w+)\((.*)\)/);
|
|
||||||
if (!cmdMatch) return msg;
|
|
||||||
|
|
||||||
const command = cmdMatch[1];
|
|
||||||
const argsRaw = cmdMatch[2];
|
|
||||||
|
|
||||||
// Simple handler for common IRC commands
|
|
||||||
switch(command) {
|
|
||||||
case 'PRIVMSG': {
|
|
||||||
const args = argsRaw.split(/,(.+)/); // split first comma
|
|
||||||
const target = args[0].replace(/^"|"$/g, '');
|
|
||||||
const message = args[1].replace(/^"|"$/g, '');
|
|
||||||
return `[${target}] <${sender}> ${message}`;
|
|
||||||
}
|
|
||||||
case 'NOTICE': {
|
|
||||||
const args = argsRaw.split(/,(.+)/);
|
|
||||||
const target = args[0].replace(/^"|"$/g, '');
|
|
||||||
const message = args[1].replace(/^"|"$/g, '');
|
|
||||||
return `[NOTICE ${target}] ${message}`;
|
|
||||||
}
|
|
||||||
case 'Response':
|
|
||||||
case 'Raw':
|
|
||||||
case 'UserMODE':
|
|
||||||
return `[${command}] ${argsRaw}`;
|
|
||||||
default:
|
|
||||||
return msg; // fallback for unknown messages
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
return msg; // fallback if parsing fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
123
src/index.html
123
src/index.html
@@ -1,49 +1,92 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Tauri IRC Client</title>
|
<title>IRCd</title>
|
||||||
<style>
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
body { font-family: sans-serif; margin:0; display:flex; flex-direction:column; height:100vh; }
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
header { background:#222; color:#fff; padding:10px; display:flex; gap:10px; align-items:center; }
|
<link rel="stylesheet" href="./styles/styles.css">
|
||||||
main { flex:1; display:flex; overflow:hidden; }
|
|
||||||
#channels { width:200px; background:#f0f0f0; padding:10px; overflow-y:auto; }
|
|
||||||
#chat { flex:1; display:flex; flex-direction:column; padding:10px; }
|
|
||||||
#messages { flex:1; overflow-y:auto; border:1px solid #ccc; padding:5px; margin-bottom:5px; background:#fff; }
|
|
||||||
#input { display:flex; gap:5px; }
|
|
||||||
input, button { padding:5px; }
|
|
||||||
button { cursor:pointer; }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar" id="channels">
|
||||||
|
<div class="sidebar-resize-handle" id="sidebarResizeHandle"></div>
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h1>IRCd</h1>
|
||||||
|
<button class="toggle-sidebar" id="toggleSidebar">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<header>
|
<div class="connection-section">
|
||||||
<input id="server" placeholder="Server" value="irc.libera.chat">
|
<h3 class="section-title">Connection</h3>
|
||||||
<input id="port" type="number" placeholder="Port" value="6667">
|
<div class="form-group">
|
||||||
<input id="nickname" placeholder="Nickname" value="TauriUser">
|
<label for="server">Server</label>
|
||||||
<button id="connectBtn">Connect</button>
|
<input id="server" type="text" value="irc.libera.chat" placeholder="irc.example.net">
|
||||||
<button id="disconnectBtn" disabled>Disconnect</button>
|
</div>
|
||||||
<span id="status">Disconnected</span>
|
<div class="form-group">
|
||||||
</header>
|
<label for="port">Port</label>
|
||||||
|
<input id="port" type="number" value="6667" min="1" max="9999">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="nickname">Nickname</label>
|
||||||
|
<input id="nickname" type="text" value="IRCDUser">
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<button id="connectBtn" class="btn-primary">Connect</button>
|
||||||
|
<button id="disconnectBtn" disabled>Disconnect</button>
|
||||||
|
</div>
|
||||||
|
<div class="status-indicator">
|
||||||
|
<div class="status-dot" id="statusDot"></div>
|
||||||
|
<span id="status">Disconnected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main>
|
<div class="channel-section">
|
||||||
<div id="channels">
|
<h3 class="section-title">Channels</h3>
|
||||||
<h4>Channels</h4>
|
<div class="form-group">
|
||||||
<ul id="channelList"></ul>
|
<label for="newChannel">Join Channel</label>
|
||||||
<input id="newChannel" placeholder="#channel">
|
<input id="newChannel" type="text" placeholder="#channel">
|
||||||
<button id="joinBtn">Join</button>
|
</div>
|
||||||
<button id="partBtn">Part</button>
|
<div class="button-group">
|
||||||
|
<button id="joinBtn">Join</button>
|
||||||
|
<button id="partBtn">Leave</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="channel-list" id="channelList">
|
||||||
|
<!-- Channels will be added here dynamically -->
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="tabs" id="tabBar">
|
||||||
|
<!-- Tabs will be added here dynamically -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<section class="messages" id="messages">
|
||||||
|
<!-- Messages will be added here dynamically -->
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="composer">
|
||||||
|
<div class="composer-input">
|
||||||
|
<input id="messageInput" type="text" placeholder="Type message...">
|
||||||
|
<div id="commandSuggestions" class="suggestions hidden"></div>
|
||||||
|
</div>
|
||||||
|
<button id="sendBtn">Send</button>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<div id="chat">
|
|
||||||
<div id="messages"></div>
|
|
||||||
<div id="input">
|
<script src="tauri.js"></script>
|
||||||
<input id="messageInput" placeholder="Type message...">
|
<script src="UI.js"></script>
|
||||||
<button id="sendBtn">Send</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="main.js"></script>
|
|
||||||
</main>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
148
src/main.js
148
src/main.js
@@ -1,148 +0,0 @@
|
|||||||
/**
|
|
||||||
// this is needed for Tauri to work properly!
|
|
||||||
const { invoke } = window.__TAURI__.core;
|
|
||||||
//
|
|
||||||
|
|
||||||
let greetInputEl;
|
|
||||||
let greetMsgEl;
|
|
||||||
|
|
||||||
async function greet() {
|
|
||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
|
||||||
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
|
||||||
greetInputEl = document.querySelector("#greet-input");
|
|
||||||
greetMsgEl = document.querySelector("#greet-msg");
|
|
||||||
document.querySelector("#greet-form").addEventListener("submit", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
greet();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
const { invoke } = window.__TAURI__.core;
|
|
||||||
const { listen } = window.__TAURI__.event;
|
|
||||||
|
|
||||||
const serverEl = document.getElementById('server');
|
|
||||||
const portEl = document.getElementById('port');
|
|
||||||
const nicknameEl = document.getElementById('nickname');
|
|
||||||
const connectBtn = document.getElementById('connectBtn');
|
|
||||||
const disconnectBtn = document.getElementById('disconnectBtn');
|
|
||||||
const statusEl = document.getElementById('status');
|
|
||||||
|
|
||||||
const channelListEl = document.getElementById('channelList');
|
|
||||||
const newChannelEl = document.getElementById('newChannel');
|
|
||||||
const joinBtn = document.getElementById('joinBtn');
|
|
||||||
const partBtn = document.getElementById('partBtn');
|
|
||||||
|
|
||||||
const messagesEl = document.getElementById('messages');
|
|
||||||
const messageInputEl = document.getElementById('messageInput');
|
|
||||||
const sendBtn = document.getElementById('sendBtn');
|
|
||||||
|
|
||||||
function addMessage(msg) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = msg;
|
|
||||||
messagesEl.appendChild(div);
|
|
||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshChannels() {
|
|
||||||
try {
|
|
||||||
const channels = await invoke("list_channels");
|
|
||||||
channelListEl.innerHTML = '';
|
|
||||||
channels.forEach(ch => {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.textContent = ch;
|
|
||||||
li.dataset.channel = ch;
|
|
||||||
li.onclick = () => { newChannelEl.value = ch; };
|
|
||||||
channelListEl.appendChild(li);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
addMessage("Failed to refresh channels: " + e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectBtn.onclick = async () => {
|
|
||||||
const server = serverEl.value.trim();
|
|
||||||
const port = Number(portEl.value);
|
|
||||||
const nickname = nicknameEl.value.trim();
|
|
||||||
|
|
||||||
if (!server || !port || !nickname) {
|
|
||||||
addMessage('Please fill in server, port, and nickname.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await invoke('connect', {
|
|
||||||
server: serverEl.value.trim(),
|
|
||||||
port: Number(portEl.value),
|
|
||||||
nickname: nicknameEl.value.trim(),
|
|
||||||
useTls: false // must match Rust parameter exactly
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
statusEl.textContent = 'Connected';
|
|
||||||
connectBtn.disabled = true;
|
|
||||||
disconnectBtn.disabled = false;
|
|
||||||
addMessage('Connected to ' + server);
|
|
||||||
refreshChannels();
|
|
||||||
} catch(e) {
|
|
||||||
addMessage('Connection failed: ' + e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
disconnectBtn.onclick = async () => {
|
|
||||||
try {
|
|
||||||
await invoke('disconnect');
|
|
||||||
statusEl.textContent = 'Disconnected';
|
|
||||||
connectBtn.disabled = false;
|
|
||||||
disconnectBtn.disabled = true;
|
|
||||||
channelListEl.innerHTML = '';
|
|
||||||
addMessage('Disconnected');
|
|
||||||
} catch(e) {
|
|
||||||
addMessage('Disconnect failed: ' + e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
joinBtn.onclick = async () => {
|
|
||||||
const channel = newChannelEl.value.trim();
|
|
||||||
if (!channel) return;
|
|
||||||
try {
|
|
||||||
await invoke('join_channel', { channel });
|
|
||||||
addMessage(`Joined ${channel}`);
|
|
||||||
refreshChannels();
|
|
||||||
} catch(e) {
|
|
||||||
addMessage(`Failed to join ${channel}: ${e}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
partBtn.onclick = async () => {
|
|
||||||
const channel = newChannelEl.value.trim();
|
|
||||||
if (!channel) return;
|
|
||||||
try {
|
|
||||||
await invoke('part_channel', { channel });
|
|
||||||
addMessage(`Left ${channel}`);
|
|
||||||
refreshChannels();
|
|
||||||
} catch(e) {
|
|
||||||
addMessage(`Failed to leave ${channel}: ${e}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sendBtn.onclick = async () => {
|
|
||||||
const target = newChannelEl.value.trim();
|
|
||||||
const message = messageInputEl.value.trim();
|
|
||||||
if (!target || !message) return;
|
|
||||||
try {
|
|
||||||
await invoke('send_message', { target, message });
|
|
||||||
addMessage(`<You> ${message}`);
|
|
||||||
messageInputEl.value = '';
|
|
||||||
} catch(e) {
|
|
||||||
addMessage(`Failed to send message: ${e}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Listen to backend IRC messages
|
|
||||||
listen('irc-message', event => {
|
|
||||||
const formatted = formatIRCMessage(event.payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
112
src/styles.css
112
src/styles.css
@@ -1,112 +0,0 @@
|
|||||||
.logo.vanilla:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #ffe21c);
|
|
||||||
}
|
|
||||||
:root {
|
|
||||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color: #0f0f0f;
|
|
||||||
background-color: #f6f6f6;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
margin: 0;
|
|
||||||
padding-top: 10vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: 0.75s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.tauri:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #24c8db);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
color: #0f0f0f;
|
|
||||||
background-color: #ffffff;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
border-color: #396cd8;
|
|
||||||
}
|
|
||||||
button:active {
|
|
||||||
border-color: #396cd8;
|
|
||||||
background-color: #e8e8e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
button {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#greet-input {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
color: #f6f6f6;
|
|
||||||
background-color: #2f2f2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #24c8db;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
button {
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #0f0f0f98;
|
|
||||||
}
|
|
||||||
button:active {
|
|
||||||
background-color: #0f0f0f69;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
src/styles/font/JetBrainsMono-Bold.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-Bold.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-BoldItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-ExtraBold.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-ExtraBoldItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-ExtraBoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-ExtraLight.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-ExtraLightItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-ExtraLightItalic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-Italic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-Italic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-Light.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-Light.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-LightItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-LightItalic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-Medium.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-Medium.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-MediumItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-Regular.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-Regular.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-SemiBold.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-SemiBold.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-SemiBoldItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-SemiBoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-Thin.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-Thin.woff2
Normal file
Binary file not shown.
BIN
src/styles/font/JetBrainsMono-ThinItalic.woff2
Normal file
BIN
src/styles/font/JetBrainsMono-ThinItalic.woff2
Normal file
Binary file not shown.
568
src/styles/styles.css
Normal file
568
src/styles/styles.css
Normal file
@@ -0,0 +1,568 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-Thin.woff2") format("woff2");
|
||||||
|
font-weight: 100;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-ThinItalic.woff2") format("woff2");
|
||||||
|
font-weight: 100;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-ExtraLight.woff2") format("woff2");
|
||||||
|
font-weight: 200;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-ExtraLightItalic.woff2") format("woff2");
|
||||||
|
font-weight: 200;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-Light.woff2") format("woff2");
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-LightItalic.woff2") format("woff2");
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-Regular.woff2") format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-Italic.woff2") format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-Medium.woff2") format("woff2");
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-MediumItalic.woff2") format("woff2");
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-SemiBold.woff2") format("woff2");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-SemiBoldItalic.woff2") format("woff2");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-Bold.woff2") format("woff2");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-BoldItalic.woff2") format("woff2");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-ExtraBold.woff2") format("woff2");
|
||||||
|
font-weight: 800;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "JetBrains Mono";
|
||||||
|
src: url("./styles/fonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2");
|
||||||
|
font-weight: 800;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: "Inter", "Segoe UI", sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
background: #1a1c21;
|
||||||
|
color: #f1f1f1;
|
||||||
|
--sidebar-width: 280px;
|
||||||
|
--sidebar-collapsed-width: 60px;
|
||||||
|
--primary-color: #4b82ff;
|
||||||
|
--success-color: #7edba5;
|
||||||
|
--danger-color: #d36d6d;
|
||||||
|
--bg-dark: #111217;
|
||||||
|
--bg-panel: #181a1f;
|
||||||
|
--bg-content: #1a1c21;
|
||||||
|
--border-color: #2a2d35;
|
||||||
|
--text-muted: #9ea4b9;
|
||||||
|
--message-bg: #1e2028;
|
||||||
|
--message-bg-user: #2d4fcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Styles */
|
||||||
|
.sidebar {
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 500px;
|
||||||
|
resize: horizontal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 4px;
|
||||||
|
cursor: col-resize;
|
||||||
|
background: transparent;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-resize-handle:hover,
|
||||||
|
.sidebar-resize-handle.resizing {
|
||||||
|
background: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed {
|
||||||
|
width: var(--sidebar-collapsed-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-sidebar {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-sidebar:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-section,
|
||||||
|
.channel-section {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #3a3f4d;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #101218;
|
||||||
|
color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #3a3f4d;
|
||||||
|
background: #242730;
|
||||||
|
color: #f1f1f1;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
background: #2c303a;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: #5a92ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--danger-color);
|
||||||
|
border-color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.online {
|
||||||
|
background: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-item.active {
|
||||||
|
background: rgba(75, 130, 255, 0.15);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-name {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Styles */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: #14151a;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
border: 1px solid #2f323a;
|
||||||
|
border-bottom: none;
|
||||||
|
background: #1b1d23;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #23252d;
|
||||||
|
color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close {
|
||||||
|
margin-left: 4px;
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-line {
|
||||||
|
display: flex;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-sender {
|
||||||
|
min-width: 120px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding-right: 8px;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-sender.you {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
flex: 1;
|
||||||
|
color: #e1e1e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-message {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.irc-message {
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-input {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #2a2e35;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-input input {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #f1f1f1;
|
||||||
|
padding: 12px 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
outline: none;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-input input::placeholder {
|
||||||
|
color: #6a7283;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 110%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #101116;
|
||||||
|
border: 1px solid #3a3f4d;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions button {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions button:hover,
|
||||||
|
.suggestions button.active {
|
||||||
|
background: #222430;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsed sidebar styles */
|
||||||
|
.sidebar.collapsed .section-title,
|
||||||
|
.sidebar.collapsed .form-group,
|
||||||
|
.sidebar.collapsed .button-group,
|
||||||
|
.sidebar.collapsed .status-indicator,
|
||||||
|
.sidebar.collapsed .channel-name {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .sidebar-header h1 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .connection-section,
|
||||||
|
.sidebar.collapsed .channel-section {
|
||||||
|
padding: 16px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .channel-item {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar:not(.collapsed) {
|
||||||
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=number]::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
147
src/tauri.js
Normal file
147
src/tauri.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
const { listen } = window.__TAURI__.event;
|
||||||
|
|
||||||
|
// Original Tauri integration code
|
||||||
|
async function refreshChannels() {
|
||||||
|
try {
|
||||||
|
const channels = await invoke("list_channels");
|
||||||
|
// Clear existing channels that aren't in the new list
|
||||||
|
const currentChannels = [...uiState.channels];
|
||||||
|
currentChannels.forEach(ch => {
|
||||||
|
if (!channels.includes(ch)) {
|
||||||
|
removeChannel(ch);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add new channels
|
||||||
|
channels.forEach(ch => {
|
||||||
|
if (!uiState.channels.includes(ch)) {
|
||||||
|
addChannel(ch);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
addSystemMessage("Failed to refresh channels: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override the connect button handler to use Tauri
|
||||||
|
connectBtn.onclick = async () => {
|
||||||
|
const server = document.getElementById('server').value.trim();
|
||||||
|
const port = Number(document.getElementById('port').value);
|
||||||
|
const nickname = document.getElementById('nickname').value.trim();
|
||||||
|
|
||||||
|
if (!server || !port || !nickname) {
|
||||||
|
addSystemMessage('Please fill in server, port, and nickname.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('connect', {
|
||||||
|
server: server,
|
||||||
|
port: port,
|
||||||
|
nickname: nickname,
|
||||||
|
useTls: false
|
||||||
|
});
|
||||||
|
|
||||||
|
uiState.connected = true;
|
||||||
|
updateConnectionStatus();
|
||||||
|
addSystemMessage('Connected to ' + server);
|
||||||
|
|
||||||
|
// Refresh channels after connection
|
||||||
|
setTimeout(refreshChannels, 1000);
|
||||||
|
} catch(e) {
|
||||||
|
addSystemMessage('Connection failed: ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Override the disconnect button handler to use Tauri
|
||||||
|
disconnectBtn.onclick = async () => {
|
||||||
|
try {
|
||||||
|
await invoke('disconnect');
|
||||||
|
uiState.connected = false;
|
||||||
|
updateConnectionStatus();
|
||||||
|
|
||||||
|
// Clear channels and tabs
|
||||||
|
uiState.channels.forEach(channel => {
|
||||||
|
removeChannel(channel);
|
||||||
|
});
|
||||||
|
setActiveTab('welcome');
|
||||||
|
addSystemMessage('Disconnected');
|
||||||
|
} catch(e) {
|
||||||
|
addSystemMessage('Disconnect failed: ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Override the join button handler to use Tauri
|
||||||
|
joinBtn.onclick = async () => {
|
||||||
|
let channel = channelInput.value.trim();
|
||||||
|
if (!channel) return;
|
||||||
|
|
||||||
|
if (!channel.startsWith('#')) {
|
||||||
|
channel = '#' + channel;
|
||||||
|
channelInput.value = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('join_channel', { channel });
|
||||||
|
addChannel(channel);
|
||||||
|
channelInput.value = '';
|
||||||
|
} catch(e) {
|
||||||
|
addSystemMessage(`Failed to join ${channel}: ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Override the part button handler to use Tauri
|
||||||
|
partBtn.onclick = async () => {
|
||||||
|
const channel = uiState.activeTab;
|
||||||
|
if (!channel || channel === 'welcome') {
|
||||||
|
addSystemMessage('Select a channel to leave first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('part_channel', { channel });
|
||||||
|
removeChannel(channel);
|
||||||
|
} catch(e) {
|
||||||
|
addSystemMessage(`Failed to leave ${channel}: ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Override the send button handler to use Tauri
|
||||||
|
sendBtn.onclick = async () => {
|
||||||
|
const target = uiState.activeTab;
|
||||||
|
const message = messageInput.value.trim();
|
||||||
|
|
||||||
|
if (!target || target === 'welcome') {
|
||||||
|
addSystemMessage('Join a channel to send messages.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('send_message', { target, message });
|
||||||
|
addMessage(uiState.currentUser, message, true);
|
||||||
|
messageInput.value = '';
|
||||||
|
} catch(e) {
|
||||||
|
addSystemMessage(`Failed to send message: ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen to backend IRC messages
|
||||||
|
listen('irc-message', event => {
|
||||||
|
if (event.payload) {
|
||||||
|
/*const formattedMessage = formatIRCMessage(event.payload);
|
||||||
|
if (formattedMessage) {
|
||||||
|
addIRCMessage(formattedMessage);
|
||||||
|
}*/
|
||||||
|
addIRCMessage(event.payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for channel updates
|
||||||
|
listen('channel-update', event => {
|
||||||
|
if (event.payload) {
|
||||||
|
refreshChannels();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user