new frontend
This commit is contained in:
618
src/UI.js
618
src/UI.js
@@ -31,7 +31,6 @@ const uiState = {
|
|||||||
userColors: new Map(),
|
userColors: new Map(),
|
||||||
currentUser: 'IRCDUser',
|
currentUser: 'IRCDUser',
|
||||||
channelUsers: new Map(), // Map<channel, Set<user>>
|
channelUsers: new Map(), // Map<channel, Set<user>>
|
||||||
channelMessages: new Map(), // Map<channel, Array<{type, sender, content, timestamp, html}>>
|
|
||||||
peopleSearchQuery: '', // Current search query for people list
|
peopleSearchQuery: '', // Current search query for people list
|
||||||
virtualScrollState: {
|
virtualScrollState: {
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
@@ -280,30 +279,23 @@ function addChannel(name) {
|
|||||||
if (!uiState.channels.includes(normalizedName)) {
|
if (!uiState.channels.includes(normalizedName)) {
|
||||||
uiState.channels.push(normalizedName);
|
uiState.channels.push(normalizedName);
|
||||||
|
|
||||||
// Add to channel list (async DOM update)
|
// Add to channel list
|
||||||
requestAnimationFrame(() => {
|
const channelItem = document.createElement('div');
|
||||||
const channelItem = document.createElement('div');
|
channelItem.className = 'channel-item';
|
||||||
channelItem.className = 'channel-item';
|
channelItem.innerHTML = `
|
||||||
channelItem.innerHTML = `
|
<div class="channel-icon">
|
||||||
<div class="channel-icon">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<path d="M5 9C5 5.13401 8.13401 2 12 2C15.866 2 19 5.13401 19 9C19 10.933 18.2165 12.683 16.8569 14.0002C16.8569 14.0002 16.5 14.5 15.5 15.5C14.5 16.5 13 18 13 18L12 19L11 18C11 18 9.5 16.5 8.5 15.5C7.5 14.5 7.14314 14.0002 7.14314 14.0002C5.78354 12.683 5 10.933 5 9Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M5 9C5 5.13401 8.13401 2 12 2C15.866 2 19 5.13401 19 9C19 10.933 18.2165 12.683 16.8569 14.0002C16.8569 14.0002 16.5 14.5 15.5 15.5C14.5 16.5 13 18 13 18L12 19L11 18C11 18 9.5 16.5 8.5 15.5C7.5 14.5 7.14314 14.0002 7.14314 14.0002C5.78354 12.683 5 10.933 5 9Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M9 9H15M9 12H15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
<path d="M9 9H15M9 12H15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
</svg>
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
<div class="channel-name">${escapeHtml(normalizedName)}</div>
|
||||||
<div class="channel-name">${escapeHtml(normalizedName)}</div>
|
`;
|
||||||
`;
|
channelItem.addEventListener('click', () => setActiveTab(normalizedName));
|
||||||
channelItem.addEventListener('click', () => setActiveTab(normalizedName));
|
channelList.appendChild(channelItem);
|
||||||
channelList.appendChild(channelItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add tab (async)
|
// Add tab
|
||||||
requestAnimationFrame(() => addTab(normalizedName));
|
addTab(normalizedName);
|
||||||
|
|
||||||
// Initialize message storage for this channel
|
|
||||||
if (!uiState.channelMessages.has(normalizedName)) {
|
|
||||||
uiState.channelMessages.set(normalizedName, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-join the channel when connected
|
// Auto-join the channel when connected
|
||||||
if (uiState.connected) {
|
if (uiState.connected) {
|
||||||
@@ -330,9 +322,6 @@ function removeChannel(name) {
|
|||||||
// Clear users for this channel
|
// Clear users for this channel
|
||||||
clearChannelUsers(name);
|
clearChannelUsers(name);
|
||||||
|
|
||||||
// Clear messages for this channel (optional - you might want to keep them)
|
|
||||||
// uiState.channelMessages.delete(name);
|
|
||||||
|
|
||||||
// Remove tab
|
// Remove tab
|
||||||
removeTab(name);
|
removeTab(name);
|
||||||
|
|
||||||
@@ -398,114 +387,51 @@ function setActiveTab(name) {
|
|||||||
|
|
||||||
uiState.activeTab = normalizedName;
|
uiState.activeTab = normalizedName;
|
||||||
|
|
||||||
// Update tabs (async)
|
// Update tabs
|
||||||
requestAnimationFrame(() => {
|
const tabs = tabBar.querySelectorAll('.tab');
|
||||||
const tabs = tabBar.querySelectorAll('.tab');
|
tabs.forEach(tab => {
|
||||||
tabs.forEach(tab => {
|
// Compare with both normalized and original name
|
||||||
// Compare with both normalized and original name
|
tab.classList.toggle('active', tab.dataset.tab === normalizedName || tab.dataset.tab === name);
|
||||||
tab.classList.toggle('active', tab.dataset.tab === normalizedName || tab.dataset.tab === name);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update channels (async)
|
// Update channels
|
||||||
requestAnimationFrame(() => {
|
const channelItems = channelList.querySelectorAll('.channel-item');
|
||||||
const channelItems = channelList.querySelectorAll('.channel-item');
|
channelItems.forEach(item => {
|
||||||
channelItems.forEach(item => {
|
const channelName = item.querySelector('.channel-name').textContent;
|
||||||
const channelName = item.querySelector('.channel-name').textContent;
|
item.classList.toggle('active', channelName === normalizedName || channelName === name);
|
||||||
item.classList.toggle('active', channelName === normalizedName || channelName === name);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update people list for the active channel (async)
|
// Update people list for the active channel
|
||||||
updatePeopleList(normalizedName);
|
updatePeopleList(normalizedName);
|
||||||
|
|
||||||
// Update messages (async)
|
// Update messages
|
||||||
requestAnimationFrame(() => {
|
updateMessagesForTab(normalizedName);
|
||||||
updateMessagesForTab(normalizedName);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMessagesForTab(name) {
|
function updateMessagesForTab(name) {
|
||||||
// Clear messages
|
// Clear messages
|
||||||
messagesContainer.innerHTML = '';
|
messagesContainer.innerHTML = '';
|
||||||
|
|
||||||
// Ensure channel name format is correct
|
// Add welcome messages
|
||||||
let channelKey = name;
|
if (name === 'welcome') {
|
||||||
if (name !== 'welcome' && !name.startsWith('#')) {
|
const welcomeMsg = document.createElement('div');
|
||||||
channelKey = '#' + name;
|
welcomeMsg.className = 'system-message';
|
||||||
|
welcomeMsg.textContent = 'IRCD - Welcome! SystemTime: ' + new Date().toLocaleString();
|
||||||
|
messagesContainer.appendChild(welcomeMsg);
|
||||||
|
} else {
|
||||||
|
// For channels, just show a welcome message
|
||||||
|
const welcomeMsg = document.createElement('div');
|
||||||
|
welcomeMsg.className = 'system-message';
|
||||||
|
welcomeMsg.textContent = `Welcome to ${name}!`;
|
||||||
|
messagesContainer.appendChild(welcomeMsg);
|
||||||
|
|
||||||
|
const infoMsg = document.createElement('div');
|
||||||
|
infoMsg.className = 'system-message';
|
||||||
|
infoMsg.textContent = 'This is the beginning of the channel.';
|
||||||
|
messagesContainer.appendChild(infoMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load stored messages from RAM
|
scrollToBottom();
|
||||||
const storedMessages = uiState.channelMessages.get(channelKey);
|
|
||||||
if (storedMessages && storedMessages.length > 0) {
|
|
||||||
// Restore all stored messages (only once, not duplicated)
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
// Use a Set to track displayed messages and prevent duplicates
|
|
||||||
const displayedHashes = new Set();
|
|
||||||
|
|
||||||
storedMessages.forEach(msg => {
|
|
||||||
// Create a hash to detect duplicates
|
|
||||||
const msgHash = `${msg.type}-${msg.timestamp}-${msg.content.substring(0, 50)}`;
|
|
||||||
|
|
||||||
if (displayedHashes.has(msgHash)) {
|
|
||||||
return; // Skip duplicate
|
|
||||||
}
|
|
||||||
displayedHashes.add(msgHash);
|
|
||||||
|
|
||||||
if (msg.html) {
|
|
||||||
// For IRC messages, the HTML is the full formatted content
|
|
||||||
// The HTML from backend is a string of spans, so we wrap it in a div
|
|
||||||
const ircMessage = document.createElement('div');
|
|
||||||
ircMessage.className = 'irc-message';
|
|
||||||
|
|
||||||
// Use the stored HTML (which is the full formatted content from backend)
|
|
||||||
// If raw exists, prefer it, otherwise use html
|
|
||||||
const htmlContent = msg.raw || msg.html;
|
|
||||||
ircMessage.innerHTML = htmlContent;
|
|
||||||
|
|
||||||
messagesContainer.appendChild(ircMessage);
|
|
||||||
} else if (msg.type === 'message') {
|
|
||||||
// Recreate message element if HTML not stored
|
|
||||||
const messageLine = document.createElement('div');
|
|
||||||
messageLine.className = 'message-line';
|
|
||||||
|
|
||||||
const senderElement = document.createElement('div');
|
|
||||||
senderElement.className = `message-sender ${msg.isUser ? 'you' : ''}`;
|
|
||||||
senderElement.textContent = msg.isUser ? `${msg.sender} (You):` : `${msg.sender}:`;
|
|
||||||
senderElement.style.color = msg.isUser ? '' : getUserColor(msg.sender);
|
|
||||||
|
|
||||||
const contentElement = document.createElement('div');
|
|
||||||
contentElement.className = 'message-content';
|
|
||||||
contentElement.textContent = msg.content;
|
|
||||||
|
|
||||||
messageLine.appendChild(senderElement);
|
|
||||||
messageLine.appendChild(contentElement);
|
|
||||||
messagesContainer.appendChild(messageLine);
|
|
||||||
} else if (msg.type === 'system') {
|
|
||||||
const systemMessage = document.createElement('div');
|
|
||||||
systemMessage.className = 'system-message';
|
|
||||||
systemMessage.textContent = msg.content;
|
|
||||||
messagesContainer.appendChild(systemMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
scrollToBottom();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Add welcome messages for new channels (only if no stored messages)
|
|
||||||
if (name === 'welcome') {
|
|
||||||
// Don't store welcome messages, just display
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const welcomeMsg = document.createElement('div');
|
|
||||||
welcomeMsg.className = 'system-message';
|
|
||||||
welcomeMsg.textContent = 'IRCD - Welcome! SystemTime: ' + new Date().toLocaleString();
|
|
||||||
messagesContainer.appendChild(welcomeMsg);
|
|
||||||
});
|
|
||||||
} else if (channelKey !== 'welcome') {
|
|
||||||
// Only add welcome message if channel has no messages
|
|
||||||
addSystemMessage(`Welcome to ${channelKey}!`);
|
|
||||||
addSystemMessage('This is the beginning of the channel.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message handling
|
// Message handling
|
||||||
@@ -545,289 +471,44 @@ function addMessage(sender, content, isUser = false) {
|
|||||||
const channel = uiState.activeTab;
|
const channel = uiState.activeTab;
|
||||||
if (!channel || channel === 'welcome') return;
|
if (!channel || channel === 'welcome') return;
|
||||||
|
|
||||||
// Normalize channel name
|
const messageLine = document.createElement('div');
|
||||||
let channelKey = channel;
|
messageLine.className = 'message-line';
|
||||||
if (!channelKey.startsWith('#')) {
|
|
||||||
channelKey = '#' + channelKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store message in RAM - ALWAYS store
|
const senderElement = document.createElement('div');
|
||||||
if (!uiState.channelMessages.has(channelKey)) {
|
senderElement.className = `message-sender ${isUser ? 'you' : ''}`;
|
||||||
uiState.channelMessages.set(channelKey, []);
|
senderElement.textContent = isUser ? `${sender} (You):` : `${sender}:`;
|
||||||
}
|
senderElement.style.color = isUser ? '' : getUserColor(sender);
|
||||||
|
|
||||||
// Use requestAnimationFrame for async DOM updates
|
const contentElement = document.createElement('div');
|
||||||
requestAnimationFrame(() => {
|
contentElement.className = 'message-content';
|
||||||
const messageLine = document.createElement('div');
|
contentElement.textContent = content;
|
||||||
messageLine.className = 'message-line';
|
|
||||||
|
messageLine.appendChild(senderElement);
|
||||||
const senderElement = document.createElement('div');
|
messageLine.appendChild(contentElement);
|
||||||
senderElement.className = `message-sender ${isUser ? 'you' : ''}`;
|
|
||||||
senderElement.textContent = isUser ? `${sender} (You):` : `${sender}:`;
|
messagesContainer.appendChild(messageLine);
|
||||||
senderElement.style.color = isUser ? '' : getUserColor(sender);
|
scrollToBottom();
|
||||||
|
|
||||||
const contentElement = document.createElement('div');
|
|
||||||
contentElement.className = 'message-content';
|
|
||||||
contentElement.textContent = content;
|
|
||||||
|
|
||||||
messageLine.appendChild(senderElement);
|
|
||||||
messageLine.appendChild(contentElement);
|
|
||||||
|
|
||||||
// Store FULL message data with HTML
|
|
||||||
const messageData = {
|
|
||||||
type: 'message',
|
|
||||||
sender,
|
|
||||||
content,
|
|
||||||
isUser,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
html: messageLine.outerHTML, // Full HTML content
|
|
||||||
raw: messageLine.outerHTML, // Keep raw HTML
|
|
||||||
formatted: true // Mark as formatted
|
|
||||||
};
|
|
||||||
|
|
||||||
uiState.channelMessages.get(channelKey).push(messageData);
|
|
||||||
console.log(`[STORE] Stored FULL user message in ${channelKey} (${content.substring(0, 50)}...)`);
|
|
||||||
|
|
||||||
// Update activeTab to normalized name for consistency
|
|
||||||
if (uiState.activeTab !== channelKey) {
|
|
||||||
uiState.activeTab = channelKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesContainer.appendChild(messageLine);
|
|
||||||
scrollToBottom();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSystemMessage(content) {
|
function addSystemMessage(content) {
|
||||||
const channel = uiState.activeTab;
|
const channel = uiState.activeTab;
|
||||||
if (!channel || channel === 'welcome') return;
|
if (!channel || channel === 'welcome') return;
|
||||||
|
|
||||||
// Normalize channel name
|
const systemMessage = document.createElement('div');
|
||||||
let channelKey = channel;
|
systemMessage.className = 'system-message';
|
||||||
if (!channelKey.startsWith('#')) {
|
systemMessage.textContent = content;
|
||||||
channelKey = '#' + channelKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store message in RAM - ALWAYS store
|
messagesContainer.appendChild(systemMessage);
|
||||||
if (!uiState.channelMessages.has(channelKey)) {
|
scrollToBottom();
|
||||||
uiState.channelMessages.set(channelKey, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use requestAnimationFrame for async DOM updates
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const systemMessage = document.createElement('div');
|
|
||||||
systemMessage.className = 'system-message';
|
|
||||||
systemMessage.textContent = content;
|
|
||||||
|
|
||||||
// Store FULL message data with HTML
|
|
||||||
const messageData = {
|
|
||||||
type: 'system',
|
|
||||||
sender: null,
|
|
||||||
content,
|
|
||||||
isUser: false,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
html: systemMessage.outerHTML, // Full HTML content
|
|
||||||
raw: systemMessage.outerHTML, // Keep raw HTML
|
|
||||||
formatted: true // Mark as formatted
|
|
||||||
};
|
|
||||||
|
|
||||||
uiState.channelMessages.get(channelKey).push(messageData);
|
|
||||||
console.log(`[STORE] Stored FULL system message in ${channelKey} (${content.substring(0, 50)}...)`);
|
|
||||||
|
|
||||||
// Update activeTab to normalized name for consistency
|
|
||||||
if (uiState.activeTab !== channelKey) {
|
|
||||||
uiState.activeTab = channelKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesContainer.appendChild(systemMessage);
|
|
||||||
scrollToBottom();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addIRCMessage(content) {
|
function addIRCMessage(content) {
|
||||||
// Parse the message to determine which channel it belongs to
|
const ircMessage = document.createElement('div');
|
||||||
let targetChannel = null;
|
ircMessage.className = 'irc-message';
|
||||||
let joinMatch = null;
|
ircMessage.innerHTML = content;
|
||||||
let partMatch = null;
|
|
||||||
|
|
||||||
// Extract plain text for checking
|
messagesContainer.appendChild(ircMessage);
|
||||||
const plainText = content.replace(/<[^>]*>/g, '');
|
scrollToBottom();
|
||||||
|
|
||||||
// Check if this is a server notice (starts with server name, no channel)
|
|
||||||
const isServerNotice = /^[a-zA-Z0-9.-]+\s+\[NOTICE\]/.test(plainText) ||
|
|
||||||
/^[a-zA-Z0-9.-]+\s+NOTICE/.test(plainText);
|
|
||||||
|
|
||||||
// Try to extract channel from PRIVMSG
|
|
||||||
// PRIVMSG format: <sender> → <channel>: <message>
|
|
||||||
const privmsgMatch = content.match(/→\s*<span[^>]*>([^<]+)<\/span><span[^>]*>:\s*<\/span>/);
|
|
||||||
if (privmsgMatch) {
|
|
||||||
targetChannel = privmsgMatch[1].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTICE format: [NOTICE] <sender> → <target>: <message>
|
|
||||||
// Only if target is a channel (starts with #)
|
|
||||||
const noticeMatch = content.match(/\[NOTICE\].*?→\s*<span[^>]*>([^<]+)<\/span><span[^>]*>:\s*<\/span>/);
|
|
||||||
if (noticeMatch) {
|
|
||||||
const target = noticeMatch[1].trim();
|
|
||||||
// Only treat as channel message if target starts with #
|
|
||||||
if (target.startsWith('#')) {
|
|
||||||
targetChannel = target;
|
|
||||||
} else if (!isServerNotice) {
|
|
||||||
// If it's a user notice, check if we're in a PM (future feature)
|
|
||||||
// For now, skip storing user notices
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// JOIN format: <sender> joined <channel>
|
|
||||||
joinMatch = content.match(/joined\s*<span[^>]*>([^<]+)<\/span>/);
|
|
||||||
if (joinMatch) {
|
|
||||||
targetChannel = joinMatch[1].trim();
|
|
||||||
// Extract sender name from the beginning of the message
|
|
||||||
const senderMatch = plainText.match(/^([^\s]+)\s+joined/);
|
|
||||||
if (senderMatch) {
|
|
||||||
const sender = senderMatch[1];
|
|
||||||
console.log(`[JOIN] ${sender} joined ${targetChannel}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PART format: <sender> left <channel>
|
|
||||||
partMatch = content.match(/left\s*<span[^>]*>([^<]+)<\/span>/);
|
|
||||||
if (partMatch) {
|
|
||||||
targetChannel = partMatch[1].trim();
|
|
||||||
// Extract sender name from the beginning of the message
|
|
||||||
const senderMatch = plainText.match(/^([^\s]+)\s+left/);
|
|
||||||
if (senderMatch) {
|
|
||||||
const sender = senderMatch[1];
|
|
||||||
console.log(`[PART] ${sender} left ${targetChannel}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// QUIT format: <sender> quit (reason)
|
|
||||||
const quitMatch = plainText.match(/^([^\s]+)\s+quit/);
|
|
||||||
if (quitMatch) {
|
|
||||||
// QUIT messages don't have a channel, but we should still display them
|
|
||||||
const sender = quitMatch[1];
|
|
||||||
console.log(`[QUIT] ${sender} quit`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NICK format: <sender> is now known as <newnick>
|
|
||||||
const nickMatch = plainText.match(/^([^\s]+)\s+is now known as\s+([^\s]+)/);
|
|
||||||
if (nickMatch) {
|
|
||||||
const sender = nickMatch[1];
|
|
||||||
const newNick = nickMatch[2];
|
|
||||||
console.log(`[NICK] ${sender} is now known as ${newNick}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NAMES format: [NAMES] <channel>:
|
|
||||||
const namesMatch = content.match(/\[NAMES\]\s*<\/span>\s*<span[^>]*>([^<]+)<\/span>/);
|
|
||||||
if (namesMatch) {
|
|
||||||
targetChannel = namesMatch[1].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize channel name
|
|
||||||
if (targetChannel && !targetChannel.startsWith('#')) {
|
|
||||||
targetChannel = '#' + targetChannel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize active channel for comparison
|
|
||||||
let activeChannel = uiState.activeTab;
|
|
||||||
if (activeChannel && activeChannel !== 'welcome' && !activeChannel.startsWith('#')) {
|
|
||||||
activeChannel = '#' + activeChannel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine where to store the message - SIMPLE AND RELIABLE
|
|
||||||
const isJoinOrPart = joinMatch || partMatch;
|
|
||||||
|
|
||||||
// Always store in active channel (where user is viewing)
|
|
||||||
// This ensures all messages seen by the user are stored
|
|
||||||
let storeChannel = activeChannel || 'welcome';
|
|
||||||
|
|
||||||
// If message has a specific target channel (PRIVMSG, JOIN, PART, etc.), also store there
|
|
||||||
// But primary storage is always the active channel
|
|
||||||
if (targetChannel && targetChannel.startsWith('#')) {
|
|
||||||
// Store in both the target channel AND the active channel
|
|
||||||
// This way messages are available when switching channels
|
|
||||||
const channelsToStore = [storeChannel, targetChannel];
|
|
||||||
|
|
||||||
channelsToStore.forEach(ch => {
|
|
||||||
if (ch && ch !== 'welcome') {
|
|
||||||
if (!uiState.channelMessages.has(ch)) {
|
|
||||||
uiState.channelMessages.set(ch, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingMessages = uiState.channelMessages.get(ch);
|
|
||||||
const now = Date.now();
|
|
||||||
const duplicateWindow = isJoinOrPart ? 5000 : 2000;
|
|
||||||
|
|
||||||
// Check for duplicates
|
|
||||||
const isDuplicate = existingMessages.some(msg =>
|
|
||||||
msg.type === 'irc' &&
|
|
||||||
msg.content === plainText &&
|
|
||||||
Math.abs(msg.timestamp - now) < duplicateWindow
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isDuplicate) {
|
|
||||||
const messageData = {
|
|
||||||
type: 'irc',
|
|
||||||
sender: null,
|
|
||||||
content: plainText,
|
|
||||||
isUser: false,
|
|
||||||
timestamp: now,
|
|
||||||
html: content, // Full HTML content
|
|
||||||
raw: content, // Keep raw HTML
|
|
||||||
formatted: true // Mark as formatted
|
|
||||||
};
|
|
||||||
|
|
||||||
existingMessages.push(messageData);
|
|
||||||
console.log(`[STORE] Stored FULL message in ${ch} (${plainText.substring(0, 50)}...)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// No target channel - store only in active channel
|
|
||||||
if (storeChannel && storeChannel !== 'welcome') {
|
|
||||||
if (!uiState.channelMessages.has(storeChannel)) {
|
|
||||||
uiState.channelMessages.set(storeChannel, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingMessages = uiState.channelMessages.get(storeChannel);
|
|
||||||
const now = Date.now();
|
|
||||||
const duplicateWindow = isJoinOrPart ? 5000 : 2000;
|
|
||||||
|
|
||||||
const isDuplicate = existingMessages.some(msg =>
|
|
||||||
msg.type === 'irc' &&
|
|
||||||
msg.content === plainText &&
|
|
||||||
Math.abs(msg.timestamp - now) < duplicateWindow
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isDuplicate) {
|
|
||||||
const messageData = {
|
|
||||||
type: 'irc',
|
|
||||||
sender: null,
|
|
||||||
content: plainText,
|
|
||||||
isUser: false,
|
|
||||||
timestamp: now,
|
|
||||||
html: content, // Full HTML content
|
|
||||||
raw: content, // Keep raw HTML
|
|
||||||
formatted: true // Mark as formatted
|
|
||||||
};
|
|
||||||
|
|
||||||
existingMessages.push(messageData);
|
|
||||||
console.log(`[STORE] Stored FULL message in ${storeChannel} (no target channel) (${plainText.substring(0, 50)}...)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display ALL messages in the active channel - no filtering
|
|
||||||
// Always display, regardless of target channel
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const ircMessage = document.createElement('div');
|
|
||||||
ircMessage.className = 'irc-message';
|
|
||||||
ircMessage.innerHTML = content; // Use innerHTML to render HTML formatting
|
|
||||||
|
|
||||||
messagesContainer.appendChild(ircMessage);
|
|
||||||
scrollToBottom();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
@@ -959,42 +640,40 @@ function updatePeopleList(channel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort and filter users (all stored in RAM, not DOM)
|
// Sort and filter users (all stored in RAM, not DOM)
|
||||||
setTimeout(() => {
|
let sortedUsers = Array.from(users).sort((a, b) => {
|
||||||
let sortedUsers = Array.from(users).sort((a, b) => {
|
const aIsOp = a.startsWith('@');
|
||||||
const aIsOp = a.startsWith('@');
|
const bIsOp = b.startsWith('@');
|
||||||
const bIsOp = b.startsWith('@');
|
const aIsVoiced = a.startsWith('+');
|
||||||
const aIsVoiced = a.startsWith('+');
|
const bIsVoiced = b.startsWith('+');
|
||||||
const bIsVoiced = b.startsWith('+');
|
|
||||||
|
if (aIsOp && !bIsOp) return -1;
|
||||||
if (aIsOp && !bIsOp) return -1;
|
if (!aIsOp && bIsOp) return 1;
|
||||||
if (!aIsOp && bIsOp) return 1;
|
if (aIsVoiced && !bIsVoiced) return -1;
|
||||||
if (aIsVoiced && !bIsVoiced) return -1;
|
if (!aIsVoiced && bIsVoiced) return 1;
|
||||||
if (!aIsVoiced && bIsVoiced) return 1;
|
return a.localeCompare(b, undefined, { sensitivity: 'base' });
|
||||||
return a.localeCompare(b, undefined, { sensitivity: 'base' });
|
});
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
const searchQuery = uiState.peopleSearchQuery.toLowerCase();
|
||||||
|
if (searchQuery) {
|
||||||
|
sortedUsers = sortedUsers.filter(user => {
|
||||||
|
const displayName = (user.startsWith('@') || user.startsWith('+'))
|
||||||
|
? user.substring(1).toLowerCase()
|
||||||
|
: user.toLowerCase();
|
||||||
|
return displayName.includes(searchQuery);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
// Apply search filter
|
|
||||||
const searchQuery = uiState.peopleSearchQuery.toLowerCase();
|
// Store filtered list for virtual scrolling (in RAM)
|
||||||
if (searchQuery) {
|
uiState.filteredUsers = sortedUsers;
|
||||||
sortedUsers = sortedUsers.filter(user => {
|
|
||||||
const displayName = (user.startsWith('@') || user.startsWith('+'))
|
// Reset scroll position when filtering
|
||||||
? user.substring(1).toLowerCase()
|
if (searchQuery && peopleListContainer) {
|
||||||
: user.toLowerCase();
|
peopleListContainer.scrollTop = 0;
|
||||||
return displayName.includes(searchQuery);
|
}
|
||||||
});
|
|
||||||
}
|
// Update virtual scroll - only renders visible items
|
||||||
|
renderVisibleUsers();
|
||||||
// Store filtered list for virtual scrolling (in RAM)
|
|
||||||
uiState.filteredUsers = sortedUsers;
|
|
||||||
|
|
||||||
// Reset scroll position when filtering
|
|
||||||
if (searchQuery && peopleListContainer) {
|
|
||||||
peopleListContainer.scrollTop = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update virtual scroll - only renders visible items
|
|
||||||
renderVisibleUsers();
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderVisibleUsers() {
|
function renderVisibleUsers() {
|
||||||
@@ -1039,20 +718,18 @@ function renderVisibleUsers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear and render only visible items
|
// Clear and render only visible items
|
||||||
requestAnimationFrame(() => {
|
if (!peopleList) return;
|
||||||
if (!peopleList) return;
|
|
||||||
|
peopleList.innerHTML = '';
|
||||||
peopleList.innerHTML = '';
|
const fragment = document.createDocumentFragment();
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
|
for (let i = startIndex; i < endIndex; i++) {
|
||||||
for (let i = startIndex; i < endIndex; i++) {
|
const user = uiState.filteredUsers[i];
|
||||||
const user = uiState.filteredUsers[i];
|
const peopleItem = createPeopleItem(user);
|
||||||
const peopleItem = createPeopleItem(user);
|
fragment.appendChild(peopleItem);
|
||||||
fragment.appendChild(peopleItem);
|
}
|
||||||
}
|
|
||||||
|
peopleList.appendChild(fragment);
|
||||||
peopleList.appendChild(fragment);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPeopleItem(user) {
|
function createPeopleItem(user) {
|
||||||
@@ -1098,49 +775,42 @@ function handlePeopleSearch(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePeopleListScroll() {
|
function handlePeopleListScroll() {
|
||||||
requestAnimationFrame(() => {
|
renderVisibleUsers();
|
||||||
renderVisibleUsers();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateVirtualScrollContainerHeight() {
|
function updateVirtualScrollContainerHeight() {
|
||||||
requestAnimationFrame(() => {
|
if (peopleListContainer) {
|
||||||
if (peopleListContainer) {
|
uiState.virtualScrollState.containerHeight = peopleListContainer.clientHeight;
|
||||||
uiState.virtualScrollState.containerHeight = peopleListContainer.clientHeight;
|
renderVisibleUsers();
|
||||||
renderVisibleUsers();
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseNamesMessage(channel, usersStr) {
|
function parseNamesMessage(channel, usersStr) {
|
||||||
if (!channel || !usersStr) return;
|
if (!channel || !usersStr) return;
|
||||||
|
|
||||||
// Process parsing asynchronously
|
// Initialize users set for this channel if it doesn't exist
|
||||||
setTimeout(() => {
|
// Note: We accumulate users from multiple NAMES messages until ENDOFNAMES
|
||||||
// Initialize users set for this channel if it doesn't exist
|
// Users are stored in RAM, not DOM
|
||||||
// Note: We accumulate users from multiple NAMES messages until ENDOFNAMES
|
if (!uiState.channelUsers.has(channel)) {
|
||||||
// Users are stored in RAM, not DOM
|
uiState.channelUsers.set(channel, new Set());
|
||||||
if (!uiState.channelUsers.has(channel)) {
|
}
|
||||||
uiState.channelUsers.set(channel, new Set());
|
|
||||||
|
const users = usersStr.trim().split(/\s+/).filter(u => u && u.trim().length > 0);
|
||||||
|
const channelUsers = uiState.channelUsers.get(channel);
|
||||||
|
|
||||||
|
// Add all users from this NAMES message to RAM
|
||||||
|
users.forEach(user => {
|
||||||
|
const trimmed = user.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
channelUsers.add(trimmed);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
const users = usersStr.trim().split(/\s+/).filter(u => u && u.trim().length > 0);
|
|
||||||
const channelUsers = uiState.channelUsers.get(channel);
|
// Update the people list if this is the active channel
|
||||||
|
// This will use virtual scrolling to only render visible users
|
||||||
// Add all users from this NAMES message to RAM
|
if (uiState.activeTab === channel) {
|
||||||
users.forEach(user => {
|
updatePeopleList(channel);
|
||||||
const trimmed = user.trim();
|
}
|
||||||
if (trimmed) {
|
|
||||||
channelUsers.add(trimmed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the people list if this is the active channel (async)
|
|
||||||
// This will use virtual scrolling to only render visible users
|
|
||||||
if (uiState.activeTab === channel) {
|
|
||||||
updatePeopleList(channel);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearChannelUsers(channel) {
|
function clearChannelUsers(channel) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link rel="stylesheet" href="./styles/styles.css">
|
<link rel="stylesheet" href="./styles/styles.css">
|
||||||
|
<link rel="stylesheet" href="./styles/mobile.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
<aside class="sidebar" id="channels">
|
<aside class="sidebar" id="channels">
|
||||||
<div class="sidebar-resize-handle" id="sidebarResizeHandle"></div>
|
<div class="sidebar-resize-handle" id="sidebarResizeHandle"></div>
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h1>IRCd</h1>
|
<h1>IRCd<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M10 3H20M4 21H14M15 3L9 21" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg></h1>
|
||||||
<button class="toggle-sidebar" id="toggleSidebar">
|
<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">
|
<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"/>
|
<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
@@ -134,5 +135,6 @@
|
|||||||
|
|
||||||
<script src="tauri.js"></script>
|
<script src="tauri.js"></script>
|
||||||
<script src="UI.js"></script>
|
<script src="UI.js"></script>
|
||||||
|
<script src="mobile.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
613
src/mobile.js
Normal file
613
src/mobile.js
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
// mobile.js - Fixed fullscreen sidebar behavior with textbox preservation
|
||||||
|
|
||||||
|
class MobileManager {
|
||||||
|
constructor() {
|
||||||
|
this.breakpoints = {
|
||||||
|
desktop: 1024,
|
||||||
|
tablet: 768,
|
||||||
|
mobile: 480,
|
||||||
|
narrow: 320,
|
||||||
|
superNarrow: 280
|
||||||
|
};
|
||||||
|
this.currentBreakpoint = this.getCurrentBreakpoint();
|
||||||
|
this.sidebarOpen = false;
|
||||||
|
this.peopleSidebarOpen = false;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentBreakpoint() {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
if (width >= this.breakpoints.desktop) return 'desktop';
|
||||||
|
if (width >= this.breakpoints.tablet) return 'tablet';
|
||||||
|
if (width >= this.breakpoints.mobile) return 'mobile';
|
||||||
|
if (width >= this.breakpoints.narrow) return 'narrow';
|
||||||
|
return 'super-narrow';
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
console.log('Initializing mobile interface for:', this.currentBreakpoint);
|
||||||
|
this.createMobileOverlay();
|
||||||
|
this.createMobileHeader();
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.overrideCollapseButtons();
|
||||||
|
this.applyResponsiveStyles();
|
||||||
|
this.handleResize();
|
||||||
|
this.enhanceTouchInteractions();
|
||||||
|
}
|
||||||
|
|
||||||
|
createMobileOverlay() {
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'mobile-overlay';
|
||||||
|
overlay.id = 'mobileOverlay';
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
overlay.addEventListener('click', () => {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createMobileHeader() {
|
||||||
|
const mainContent = document.querySelector('.main-content');
|
||||||
|
if (!mainContent) return;
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'mobile-header';
|
||||||
|
header.innerHTML = `
|
||||||
|
<button class="mobile-menu-button" id="mobileMenuButton" aria-label="Toggle channels">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 12H21M3 6H21M3 18H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<h1 class="mobile-title" id="mobileTitle">IRCD</h1>
|
||||||
|
<button class="mobile-menu-button" id="mobilePeopleButton" aria-label="Toggle people list">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17 21V19C17 17.9391 16.5786 16.9217 15.8284 16.1716C15.0783 15.4214 14.0609 15 13 15H5C3.93913 15 2.92172 15.4214 2.17157 16.1716C1.42143 16.9217 1 17.9391 1 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
mainContent.insertBefore(header, mainContent.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
overrideCollapseButtons() {
|
||||||
|
// Override the channels sidebar toggle button (left sidebar)
|
||||||
|
const sidebarToggle = document.getElementById('toggleSidebar');
|
||||||
|
if (sidebarToggle) {
|
||||||
|
const newToggle = sidebarToggle.cloneNode(true);
|
||||||
|
sidebarToggle.parentNode.replaceChild(newToggle, sidebarToggle);
|
||||||
|
|
||||||
|
newToggle.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.isMobile()) {
|
||||||
|
// On mobile: close the channels sidebar instead of collapsing
|
||||||
|
this.closeSidebar();
|
||||||
|
} else {
|
||||||
|
// On desktop: use original collapse behavior for channels sidebar
|
||||||
|
if (typeof window.toggleSidebar === 'function') {
|
||||||
|
window.toggleSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override the people sidebar toggle button (right sidebar)
|
||||||
|
const peopleToggle = document.getElementById('togglePeopleSidebar');
|
||||||
|
if (peopleToggle) {
|
||||||
|
const newPeopleToggle = peopleToggle.cloneNode(true);
|
||||||
|
peopleToggle.parentNode.replaceChild(newPeopleToggle, peopleToggle);
|
||||||
|
|
||||||
|
newPeopleToggle.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.isMobile()) {
|
||||||
|
// On mobile: close the people sidebar instead of collapsing
|
||||||
|
this.closePeopleSidebar();
|
||||||
|
} else {
|
||||||
|
// On desktop: use original collapse behavior for people sidebar
|
||||||
|
if (typeof window.togglePeopleSidebar === 'function') {
|
||||||
|
window.togglePeopleSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateToggleButtonIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateToggleButtonIcons() {
|
||||||
|
const sidebarToggle = document.getElementById('toggleSidebar');
|
||||||
|
const peopleToggle = document.getElementById('togglePeopleSidebar');
|
||||||
|
|
||||||
|
if (sidebarToggle) {
|
||||||
|
const icon = sidebarToggle.querySelector('svg');
|
||||||
|
if (icon) {
|
||||||
|
if (this.isMobile() && this.sidebarOpen) {
|
||||||
|
// Show close icon (X) when channels sidebar is open on mobile
|
||||||
|
icon.innerHTML = '<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
} else {
|
||||||
|
// Show collapse icon (chevron)
|
||||||
|
icon.innerHTML = '<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peopleToggle) {
|
||||||
|
const icon = peopleToggle.querySelector('svg');
|
||||||
|
if (icon) {
|
||||||
|
if (this.isMobile() && this.peopleSidebarOpen) {
|
||||||
|
// Show close icon (X) when people sidebar is open on mobile
|
||||||
|
icon.innerHTML = '<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
} else {
|
||||||
|
// Show collapse icon (chevron)
|
||||||
|
icon.innerHTML = '<path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Mobile menu buttons - only control channels and people sidebars
|
||||||
|
document.getElementById('mobileMenuButton')?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.toggleSidebar();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('mobilePeopleButton')?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.togglePeopleSidebar();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Touch events
|
||||||
|
this.setupTouchEvents();
|
||||||
|
|
||||||
|
// Orientation and resize
|
||||||
|
window.addEventListener('orientationchange', () => {
|
||||||
|
setTimeout(() => this.handleResize(), 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => this.throttle(this.handleResize.bind(this), 250));
|
||||||
|
|
||||||
|
// Fixed: Don't close sidebars when clicking on form elements
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (this.isMobile() &&
|
||||||
|
!e.target.closest('#channels') &&
|
||||||
|
!e.target.closest('#peopleSidebar') &&
|
||||||
|
!e.target.closest('.mobile-menu-button') &&
|
||||||
|
// Don't close sidebars when clicking on form inputs
|
||||||
|
!e.target.matches('input, textarea, select')) {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard handling
|
||||||
|
this.setupKeyboardHandling();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTouchEvents() {
|
||||||
|
let touchStartX = 0;
|
||||||
|
let touchStartY = 0;
|
||||||
|
|
||||||
|
document.addEventListener('touchstart', (e) => {
|
||||||
|
touchStartX = e.touches[0].clientX;
|
||||||
|
touchStartY = e.touches[0].clientY;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('touchend', (e) => {
|
||||||
|
if (!this.isMobile()) return;
|
||||||
|
|
||||||
|
const touchEndX = e.changedTouches[0].clientX;
|
||||||
|
const touchEndY = e.changedTouches[0].clientY;
|
||||||
|
const diffX = touchEndX - touchStartX;
|
||||||
|
const diffY = touchEndY - touchStartY;
|
||||||
|
|
||||||
|
// Only handle horizontal swipes with minimal vertical movement
|
||||||
|
if (Math.abs(diffX) > 50 && Math.abs(diffY) < 30) {
|
||||||
|
if (diffX > 0) {
|
||||||
|
// Swipe right - open channels sidebar
|
||||||
|
this.toggleSidebar();
|
||||||
|
} else {
|
||||||
|
// Swipe left - open people sidebar or close open ones
|
||||||
|
if (this.sidebarOpen) {
|
||||||
|
this.closeSidebar();
|
||||||
|
} else {
|
||||||
|
this.togglePeopleSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Better touch feedback
|
||||||
|
document.querySelectorAll('button, .channel-item, .people-item, .tab').forEach(element => {
|
||||||
|
element.addEventListener('touchstart', function() {
|
||||||
|
this.classList.add('touch-active');
|
||||||
|
});
|
||||||
|
|
||||||
|
element.addEventListener('touchend', function() {
|
||||||
|
this.classList.remove('touch-active');
|
||||||
|
});
|
||||||
|
|
||||||
|
element.addEventListener('touchcancel', function() {
|
||||||
|
this.classList.remove('touch-active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupKeyboardHandling() {
|
||||||
|
if ('visualViewport' in window) {
|
||||||
|
const visualViewport = window.visualViewport;
|
||||||
|
|
||||||
|
visualViewport.addEventListener('resize', () => {
|
||||||
|
this.handleKeyboardResize(visualViewport);
|
||||||
|
});
|
||||||
|
|
||||||
|
visualViewport.addEventListener('scroll', () => {
|
||||||
|
this.handleKeyboardScroll(visualViewport);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed: Only auto-close sidebars when focusing on main message input
|
||||||
|
document.addEventListener('focusin', (e) => {
|
||||||
|
if (this.isMobile() && e.target.id === 'messageInput') {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
}
|
||||||
|
// Don't close sidebars for other input types (connection form, etc.)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyboardResize(viewport) {
|
||||||
|
// Handle virtual keyboard appearance
|
||||||
|
if (this.isMobile()) {
|
||||||
|
const composer = document.querySelector('.composer');
|
||||||
|
if (composer) {
|
||||||
|
composer.style.marginBottom = '0px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyboardScroll(viewport) {
|
||||||
|
// Adjust scroll when keyboard appears
|
||||||
|
if (this.isMobile()) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const messages = document.querySelector('.messages');
|
||||||
|
if (messages) {
|
||||||
|
messages.scrollTop = messages.scrollHeight;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSidebar() {
|
||||||
|
const sidebar = document.getElementById('channels');
|
||||||
|
const overlay = document.getElementById('mobileOverlay');
|
||||||
|
|
||||||
|
if (!sidebar || !overlay) return;
|
||||||
|
|
||||||
|
if (this.sidebarOpen) {
|
||||||
|
this.closeSidebar();
|
||||||
|
} else {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
sidebar.classList.add('active');
|
||||||
|
overlay.classList.add('active');
|
||||||
|
this.sidebarOpen = true;
|
||||||
|
this.updateMobileTitle('Channels');
|
||||||
|
this.updateToggleButtonIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePeopleSidebar() {
|
||||||
|
const peopleSidebar = document.getElementById('peopleSidebar');
|
||||||
|
const overlay = document.getElementById('mobileOverlay');
|
||||||
|
|
||||||
|
if (!peopleSidebar || !overlay) return;
|
||||||
|
|
||||||
|
if (this.peopleSidebarOpen) {
|
||||||
|
this.closePeopleSidebar();
|
||||||
|
} else {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
peopleSidebar.classList.add('active');
|
||||||
|
overlay.classList.add('active');
|
||||||
|
this.peopleSidebarOpen = true;
|
||||||
|
this.updateMobileTitle('People');
|
||||||
|
this.updateToggleButtonIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSidebar() {
|
||||||
|
const sidebar = document.getElementById('channels');
|
||||||
|
const overlay = document.getElementById('mobileOverlay');
|
||||||
|
|
||||||
|
if (sidebar) sidebar.classList.remove('active');
|
||||||
|
if (overlay) overlay.classList.remove('active');
|
||||||
|
this.sidebarOpen = false;
|
||||||
|
this.updateMobileTitle();
|
||||||
|
this.updateToggleButtonIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
closePeopleSidebar() {
|
||||||
|
const peopleSidebar = document.getElementById('peopleSidebar');
|
||||||
|
const overlay = document.getElementById('mobileOverlay');
|
||||||
|
|
||||||
|
if (peopleSidebar) peopleSidebar.classList.remove('active');
|
||||||
|
if (overlay) overlay.classList.remove('active');
|
||||||
|
this.peopleSidebarOpen = false;
|
||||||
|
this.updateMobileTitle();
|
||||||
|
this.updateToggleButtonIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAllSidebars() {
|
||||||
|
this.closeSidebar();
|
||||||
|
this.closePeopleSidebar();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMobileTitle(title = '') {
|
||||||
|
const mobileTitle = document.getElementById('mobileTitle');
|
||||||
|
if (!mobileTitle) return;
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
mobileTitle.textContent = this.truncateText(title, this.getMaxTitleLength());
|
||||||
|
} else {
|
||||||
|
const activeTab = document.querySelector('.tab.active');
|
||||||
|
if (activeTab) {
|
||||||
|
const tabText = activeTab.textContent.replace('×', '').trim();
|
||||||
|
mobileTitle.textContent = this.truncateText(tabText, this.getMaxTitleLength());
|
||||||
|
} else {
|
||||||
|
mobileTitle.textContent = 'IRCD';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
truncateText(text, maxLength) {
|
||||||
|
if (text.length <= maxLength) return text;
|
||||||
|
return text.substring(0, maxLength - 3) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxTitleLength() {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
if (width < this.breakpoints.superNarrow) return 12;
|
||||||
|
if (width < this.breakpoints.narrow) return 15;
|
||||||
|
if (width < this.breakpoints.mobile) return 20;
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyResponsiveStyles() {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = this.generateResponsiveCSS();
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
this.updateResponsiveClasses();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResponsiveClasses() {
|
||||||
|
const breakpoint = this.currentBreakpoint;
|
||||||
|
document.body.className = document.body.className.replace(/\b(desktop|tablet|mobile|narrow-screen|super-narrow)\b/g, '');
|
||||||
|
document.body.classList.add(breakpoint === 'narrow' ? 'narrow-screen' : breakpoint);
|
||||||
|
document.body.classList.add('mobile-device');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize() {
|
||||||
|
const previousBreakpoint = this.currentBreakpoint;
|
||||||
|
this.currentBreakpoint = this.getCurrentBreakpoint();
|
||||||
|
|
||||||
|
if (previousBreakpoint !== this.currentBreakpoint) {
|
||||||
|
console.log('Breakpoint changed:', previousBreakpoint, '→', this.currentBreakpoint);
|
||||||
|
this.updateResponsiveClasses();
|
||||||
|
this.updateMobileTitle();
|
||||||
|
this.adjustLayoutForBreakpoint();
|
||||||
|
this.updateToggleButtonIcons();
|
||||||
|
|
||||||
|
// Close sidebars when switching to mobile layout
|
||||||
|
if (this.isMobile() && !previousBreakpoint.includes('mobile') && !previousBreakpoint.includes('narrow')) {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.adjustComposerForMobile();
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustLayoutForBreakpoint() {
|
||||||
|
const breakpoint = this.currentBreakpoint;
|
||||||
|
|
||||||
|
// Adjust tab bar for narrow screens
|
||||||
|
const tabs = document.querySelector('.tabs');
|
||||||
|
if (tabs) {
|
||||||
|
if (breakpoint === 'super-narrow') {
|
||||||
|
tabs.style.flexWrap = 'nowrap';
|
||||||
|
tabs.style.overflowX = 'auto';
|
||||||
|
} else {
|
||||||
|
tabs.style.flexWrap = '';
|
||||||
|
tabs.style.overflowX = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust message layout
|
||||||
|
const messages = document.querySelector('.messages');
|
||||||
|
if (messages) {
|
||||||
|
if (breakpoint === 'super-narrow') {
|
||||||
|
messages.classList.add('narrow-layout');
|
||||||
|
} else {
|
||||||
|
messages.classList.remove('narrow-layout');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only hide collapse buttons for channels/people sidebars, not server section
|
||||||
|
const sidebarToggle = document.querySelector('#channels .toggle-sidebar');
|
||||||
|
const peopleToggle = document.querySelector('#peopleSidebar .toggle-people-sidebar');
|
||||||
|
|
||||||
|
if (this.isMobile()) {
|
||||||
|
if (sidebarToggle) sidebarToggle.style.display = 'none';
|
||||||
|
if (peopleToggle) peopleToggle.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
if (sidebarToggle) sidebarToggle.style.display = '';
|
||||||
|
if (peopleToggle) peopleToggle.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustComposerForMobile() {
|
||||||
|
const composer = document.querySelector('.composer');
|
||||||
|
const messages = document.querySelector('.messages');
|
||||||
|
|
||||||
|
if (!composer || !messages) return;
|
||||||
|
|
||||||
|
if (this.isMobile()) {
|
||||||
|
messages.style.paddingBottom = '90px';
|
||||||
|
} else {
|
||||||
|
messages.style.paddingBottom = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enhanceTouchInteractions() {
|
||||||
|
let lastTap = 0;
|
||||||
|
document.addEventListener('touchend', (e) => {
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const tapLength = currentTime - lastTap;
|
||||||
|
|
||||||
|
if (tapLength < 500 && tapLength > 0) {
|
||||||
|
const messageLine = e.target.closest('.message-line');
|
||||||
|
if (messageLine && !e.target.closest('.message-sender')) {
|
||||||
|
this.handleDoubleTapMessage(messageLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastTap = currentTime;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDoubleTapMessage(messageLine) {
|
||||||
|
messageLine.style.backgroundColor = '#3A3A3A';
|
||||||
|
setTimeout(() => {
|
||||||
|
messageLine.style.backgroundColor = '';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
isMobile() {
|
||||||
|
return this.currentBreakpoint !== 'desktop';
|
||||||
|
}
|
||||||
|
|
||||||
|
throttle(func, limit) {
|
||||||
|
let inThrottle;
|
||||||
|
return function() {
|
||||||
|
const args = arguments;
|
||||||
|
const context = this;
|
||||||
|
if (!inThrottle) {
|
||||||
|
func.apply(context, args);
|
||||||
|
inThrottle = true;
|
||||||
|
setTimeout(() => inThrottle = false, limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public methods for integration
|
||||||
|
handleNewMessage() {
|
||||||
|
if (this.isMobile() && (this.sidebarOpen || this.peopleSidebarOpen)) {
|
||||||
|
setTimeout(() => this.closeAllSidebars(), 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChannelSwitch(channelName) {
|
||||||
|
if (this.isMobile()) {
|
||||||
|
this.closeAllSidebars();
|
||||||
|
this.updateMobileTitle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMessageInput() {
|
||||||
|
const messageInput = document.getElementById('messageInput');
|
||||||
|
if (!messageInput) return;
|
||||||
|
|
||||||
|
messageInput.addEventListener('focus', () => {
|
||||||
|
if (this.isMobile()) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const messages = document.querySelector('.messages');
|
||||||
|
if (messages) {
|
||||||
|
messages.scrollTop = messages.scrollHeight;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
messageInput.addEventListener('keypress', (e) => {
|
||||||
|
if (this.isMobile() && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
setTimeout(() => {
|
||||||
|
const sendBtn = document.getElementById('sendBtn');
|
||||||
|
if (sendBtn) sendBtn.click();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize mobile manager
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.mobileManager = new MobileManager();
|
||||||
|
integrateWithExistingApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Integration with existing app
|
||||||
|
function integrateWithExistingApp() {
|
||||||
|
// Override setActiveTab
|
||||||
|
const originalSetActiveTab = window.setActiveTab;
|
||||||
|
if (originalSetActiveTab) {
|
||||||
|
window.setActiveTab = function(name) {
|
||||||
|
originalSetActiveTab(name);
|
||||||
|
if (window.mobileManager) {
|
||||||
|
window.mobileManager.handleChannelSwitch(name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override addMessage
|
||||||
|
const originalAddMessage = window.addMessage;
|
||||||
|
if (originalAddMessage) {
|
||||||
|
window.addMessage = function(sender, content, isUser = false) {
|
||||||
|
originalAddMessage(sender, content, isUser);
|
||||||
|
if (window.mobileManager) {
|
||||||
|
window.mobileManager.handleNewMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override addSystemMessage
|
||||||
|
const originalAddSystemMessage = window.addSystemMessage;
|
||||||
|
if (originalAddSystemMessage) {
|
||||||
|
window.addSystemMessage = function(content) {
|
||||||
|
originalAddSystemMessage(content);
|
||||||
|
if (window.mobileManager) {
|
||||||
|
window.mobileManager.handleNewMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override the original toggle functions
|
||||||
|
if (window.toggleSidebar) {
|
||||||
|
const originalToggleSidebar = window.toggleSidebar;
|
||||||
|
window.toggleSidebar = function() {
|
||||||
|
if (window.mobileManager && window.mobileManager.isMobile()) {
|
||||||
|
window.mobileManager.toggleSidebar();
|
||||||
|
} else {
|
||||||
|
originalToggleSidebar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.togglePeopleSidebar) {
|
||||||
|
const originalTogglePeopleSidebar = window.togglePeopleSidebar;
|
||||||
|
window.togglePeopleSidebar = function() {
|
||||||
|
if (window.mobileManager && window.mobileManager.isMobile()) {
|
||||||
|
window.mobileManager.togglePeopleSidebar();
|
||||||
|
} else {
|
||||||
|
originalTogglePeopleSidebar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup mobile message input
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.mobileManager) {
|
||||||
|
window.mobileManager.setupMessageInput();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = MobileManager;
|
||||||
|
}
|
||||||
223
src/styles/mobile.css
Normal file
223
src/styles/mobile.css
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
#channels .connection-section {
|
||||||
|
position: relative !important;
|
||||||
|
width: auto !important;
|
||||||
|
max-width: none !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only apply fullscreen to the sidebar containers, not their internal sections */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-device #channels.active {
|
||||||
|
width: 85vw !important;
|
||||||
|
max-width: 400px !important;
|
||||||
|
min-width: 280px !important;
|
||||||
|
transform: translateX(0) !important;
|
||||||
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-device #peopleSidebar.active {
|
||||||
|
width: 85vw !important;
|
||||||
|
max-width: 400px !important;
|
||||||
|
min-width: 280px !important;
|
||||||
|
transform: translateX(0) !important;
|
||||||
|
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure server connection form stays properly sized */
|
||||||
|
.mobile-device #channels.active .connection-section {
|
||||||
|
width: auto !important;
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-device #channels.active .form-group input {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Super narrow screens (280px and below) */
|
||||||
|
@media (max-width: 280px) {
|
||||||
|
.super-narrow .mobile-header {
|
||||||
|
padding: 4px 8px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .mobile-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .mobile-menu-button {
|
||||||
|
padding: 6px;
|
||||||
|
min-width: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .tab {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
max-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .message-sender {
|
||||||
|
min-width: 50px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .message-content {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .composer {
|
||||||
|
padding: 8px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .composer-input input {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow button {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .form-group input {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust sidebar width for super narrow */
|
||||||
|
.super-narrow #channels.active,
|
||||||
|
.super-narrow #peopleSidebar.active {
|
||||||
|
width: 95vw !important;
|
||||||
|
min-width: 250px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Narrow screens (320px and below) */
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
.narrow-screen .mobile-header {
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .tab {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .messages {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .message-sender {
|
||||||
|
min-width: 60px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .composer {
|
||||||
|
padding: 10px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .composer-input input {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen #channels.active,
|
||||||
|
.narrow-screen #peopleSidebar.active {
|
||||||
|
width: 90vw !important;
|
||||||
|
min-width: 260px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .channel-item,
|
||||||
|
.narrow-screen .people-item {
|
||||||
|
padding: 10px 8px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .form-group {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .button-group {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile optimizations */
|
||||||
|
.mobile-device {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-device .messages,
|
||||||
|
.mobile-device .channel-list,
|
||||||
|
.mobile-device .people-list-container {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.touch-active {
|
||||||
|
background-color: #4A4A4A !important;
|
||||||
|
transition: background-color 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improved tap targets */
|
||||||
|
.mobile-device button,
|
||||||
|
.mobile-device .channel-item,
|
||||||
|
.mobile-device .people-item,
|
||||||
|
.mobile-device .tab {
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide resize handles on mobile */
|
||||||
|
.mobile-device .sidebar-resize-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact forms for narrow screens */
|
||||||
|
.narrow-screen .connection-section,
|
||||||
|
.narrow-screen .channel-section,
|
||||||
|
.narrow-screen .people-section {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-screen .section-title {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stack buttons vertically on very narrow screens */
|
||||||
|
.super-narrow .button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .button-group button {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust message layout for narrow screens */
|
||||||
|
.super-narrow .message-line {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .message-sender {
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 0;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact composer for narrow screens */
|
||||||
|
.super-narrow .composer {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-narrow .composer-input {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
@@ -121,20 +121,26 @@
|
|||||||
:root {
|
:root {
|
||||||
font-family: "Inter", "Segoe UI", sans-serif;
|
font-family: "Inter", "Segoe UI", sans-serif;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
background: #1a1c21;
|
background: #2D2D2D;
|
||||||
color: #f1f1f1;
|
color: #DDD;
|
||||||
--sidebar-width: 280px;
|
--sidebar-width: 280px;
|
||||||
--sidebar-collapsed-width: 60px;
|
--sidebar-collapsed-width: 60px;
|
||||||
--primary-color: #4b82ff;
|
--primary-color: #4A4A4A;
|
||||||
--success-color: #7edba5;
|
--success-color: #7edba5;
|
||||||
--danger-color: #d36d6d;
|
--danger-color: #d36d6d;
|
||||||
--bg-dark: #111217;
|
--bg-dark: #2D2D2D;
|
||||||
--bg-panel: #181a1f;
|
--bg-panel: #252525;
|
||||||
--bg-content: #1a1c21;
|
--bg-content: #2D2D2D;
|
||||||
--border-color: #2a2d35;
|
--border-color: #555;
|
||||||
--text-muted: #9ea4b9;
|
--text-muted: #777;
|
||||||
--message-bg: #1e2028;
|
--message-bg: #1E1E1E;
|
||||||
--message-bg-user: #2d4fcc;
|
--message-bg-user: #3A3A3A;
|
||||||
|
|
||||||
|
/* Responsive variables */
|
||||||
|
--mobile-breakpoint: 768px;
|
||||||
|
--tablet-breakpoint: 1024px;
|
||||||
|
--mobile-sidebar-width: 280px;
|
||||||
|
--mobile-header-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
@@ -149,12 +155,15 @@ body {
|
|||||||
background: var(--bg-dark);
|
background: var(--bg-dark);
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
color: #DDD;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-container {
|
.app-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background-color: #2D2D2D;
|
||||||
|
color: #DDD;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar Styles */
|
/* Sidebar Styles */
|
||||||
@@ -164,13 +173,14 @@ body {
|
|||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
transition: width 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
resize: horizontal;
|
resize: horizontal;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-right {
|
.sidebar-right {
|
||||||
@@ -212,6 +222,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
min-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header h1 {
|
.sidebar-header h1 {
|
||||||
@@ -220,22 +231,24 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
color: #DDD;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-sidebar {
|
.toggle-sidebar {
|
||||||
background: none;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-muted);
|
color: #DDD;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-sidebar:hover {
|
.toggle-sidebar:hover {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: #3A3A3A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-section,
|
.connection-section,
|
||||||
@@ -246,7 +259,7 @@ body {
|
|||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-muted);
|
color: #DDD;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
@@ -268,21 +281,22 @@ body {
|
|||||||
.form-group label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-muted);
|
color: #DDD;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input {
|
.form-group input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border: 1px solid #3a3f4d;
|
border: 1px solid #555;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #101218;
|
background: #3A3A3A;
|
||||||
color: #f1f1f1;
|
color: #DDD;
|
||||||
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input:focus {
|
.form-group input:focus {
|
||||||
border-color: var(--primary-color);
|
border-color: #555;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,35 +304,44 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #3a3f4d;
|
border: 1px solid #555;
|
||||||
background: #242730;
|
background: #3A3A3A;
|
||||||
color: #f1f1f1;
|
color: #DDD;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover:not(:disabled) {
|
button:hover:not(:disabled) {
|
||||||
background: #2c303a;
|
background: #4A4A4A;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active:not(:disabled) {
|
||||||
|
background: #2A2A2A;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
opacity: 0.4;
|
background: #2A2A2A;
|
||||||
|
color: #777;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--primary-color);
|
background: #3A3A3A;
|
||||||
border-color: var(--primary-color);
|
border-color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
.btn-primary:hover:not(:disabled) {
|
||||||
background: #5a92ff;
|
background: #4A4A4A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
@@ -332,6 +355,7 @@ button:disabled {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
@@ -339,6 +363,7 @@ button:disabled {
|
|||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--danger-color);
|
background: var(--danger-color);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.online {
|
.status-dot.online {
|
||||||
@@ -359,15 +384,16 @@ button:disabled {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-item:hover {
|
.channel-item:hover {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: #3A3A3A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-item.active {
|
.channel-item.active {
|
||||||
background: rgba(75, 130, 255, 0.15);
|
background: #3A3A3A;
|
||||||
color: var(--primary-color);
|
color: #DDD;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-icon {
|
.channel-icon {
|
||||||
@@ -389,6 +415,7 @@ button:disabled {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.people-section {
|
.people-section {
|
||||||
@@ -402,17 +429,17 @@ button:disabled {
|
|||||||
.people-search-input {
|
.people-search-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border: 1px solid #3a3f4d;
|
border: 1px solid #555;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #101218;
|
background: #3A3A3A;
|
||||||
color: #f1f1f1;
|
color: #DDD;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.people-search-input:focus {
|
.people-search-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--primary-color);
|
border-color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.people-list-container {
|
.people-list-container {
|
||||||
@@ -444,10 +471,11 @@ button:disabled {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.people-item:hover {
|
.people-item:hover {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: #3A3A3A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.people-item.op {
|
.people-item.op {
|
||||||
@@ -493,6 +521,7 @@ button:disabled {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--bg-content);
|
background: var(--bg-content);
|
||||||
|
min-width: 0; /* Important for flexbox shrinking */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
@@ -500,40 +529,58 @@ button:disabled {
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
background: #14151a;
|
background: #252525;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari and Opera */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
border: 1px solid #2f323a;
|
border: 1px solid #555;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
background: #1b1d23;
|
background: #2D2D2D;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-muted);
|
color: #DDD;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
background: #23252d;
|
background: #3A3A3A;
|
||||||
color: #f5f5f5;
|
color: #DDD;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-close {
|
.tab-close {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-close:hover {
|
.tab-close:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -555,14 +602,22 @@ button:disabled {
|
|||||||
.message-line {
|
.message-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
|
color: #DDD;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-sender {
|
.message-sender {
|
||||||
min-width: 120px;
|
min-width: 80px;
|
||||||
|
max-width: 120px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: var(--text-muted);
|
color: #DDD;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-sender.you {
|
.message-sender.you {
|
||||||
@@ -571,13 +626,16 @@ button:disabled {
|
|||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
color: #e1e1e1;
|
color: #DDD;
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.system-message {
|
.system-message {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.irc-message {
|
.irc-message {
|
||||||
@@ -598,24 +656,27 @@ button:disabled {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #2a2e35;
|
background: #3A3A3A;
|
||||||
border-radius: 24px;
|
border-radius: 4px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-input input {
|
.composer-input input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #f1f1f1;
|
color: #DDD;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-input input::placeholder {
|
.composer-input input::placeholder {
|
||||||
color: #6a7283;
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions {
|
.suggestions {
|
||||||
@@ -623,13 +684,15 @@ button:disabled {
|
|||||||
bottom: 110%;
|
bottom: 110%;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: #101116;
|
background: #252525;
|
||||||
border: 1px solid #3a3f4d;
|
border: 1px solid #555;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions button {
|
.suggestions button {
|
||||||
@@ -637,11 +700,14 @@ button:disabled {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions button:hover,
|
.suggestions button:hover,
|
||||||
.suggestions button.active {
|
.suggestions button.active {
|
||||||
background: #222430;
|
background: #3A3A3A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions.hidden {
|
.suggestions.hidden {
|
||||||
@@ -674,23 +740,420 @@ button:disabled {
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Overlay */
|
||||||
|
.mobile-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
z-index: 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-overlay.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Header */
|
||||||
|
.mobile-header {
|
||||||
|
display: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: var(--mobile-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #DDD;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-button:hover {
|
||||||
|
background: #3A3A3A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #DDD;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbars - Vertical */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #1E1E1E;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #1E1E1E;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #3A3A3A;
|
||||||
|
border-radius: 5px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:active {
|
||||||
|
background: #6A6A6A;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox scrollbar */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #3A3A3A #1E1E1E;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive styles */
|
/* Responsive styles */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 1024px) {
|
||||||
|
:root {
|
||||||
|
--sidebar-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: absolute;
|
min-width: 180px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-sender {
|
||||||
|
min-width: 70px;
|
||||||
|
max-width: 100px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer {
|
||||||
|
padding: 12px;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-input input {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-container {
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: var(--mobile-sidebar-width);
|
||||||
|
max-width: 100vw;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
height: 100%;
|
resize: none;
|
||||||
|
min-width: unset;
|
||||||
|
max-width: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar:not(.collapsed) {
|
.sidebar.active {
|
||||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-right {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-right.active {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-line {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 4px 0;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-sender {
|
||||||
|
min-width: auto;
|
||||||
|
max-width: none;
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.composer {
|
.composer {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-resize-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide sidebar toggle in header when sidebar is open on mobile */
|
||||||
|
.sidebar.active ~ .main-content .mobile-header .mobile-menu-button {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
:root {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 12px;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-section,
|
||||||
|
.channel-section,
|
||||||
|
.people-section {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-list {
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-input {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-input input {
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header {
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
:root {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
max-width: 100px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch device improvements */
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
button,
|
||||||
|
.channel-item,
|
||||||
|
.people-item,
|
||||||
|
.tab {
|
||||||
|
min-height: 44px; /* Better touch targets */
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-sidebar,
|
||||||
|
.mobile-menu-button {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-resize-handle {
|
||||||
|
width: 8px; /* Larger touch target for resize */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce hover effects on touch devices */
|
||||||
|
.channel-item:hover,
|
||||||
|
.people-item:hover,
|
||||||
|
.tab:hover {
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-item:active,
|
||||||
|
.people-item:active,
|
||||||
|
.tab:active {
|
||||||
|
background: #3A3A3A;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High DPI screens */
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||||
|
:root {
|
||||||
|
font-size: 16px; /* Slightly larger text for better readability */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print styles */
|
||||||
|
@media print {
|
||||||
|
.sidebar,
|
||||||
|
.composer,
|
||||||
|
.mobile-header,
|
||||||
|
.tabs {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
overflow: visible !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=number]::-webkit-inner-spin-button {
|
input[type=number]::-webkit-inner-spin-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"]::-webkit-outer-spin-button,
|
||||||
|
input[type="number"]::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility classes for responsive behavior */
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-on-mobile {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-on-mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hide-on-mobile {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-on-mobile {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-on-mobile-flex {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user