new frontend
This commit is contained in:
618
src/UI.js
618
src/UI.js
@@ -31,7 +31,6 @@ const uiState = {
|
||||
userColors: new Map(),
|
||||
currentUser: 'IRCDUser',
|
||||
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
|
||||
virtualScrollState: {
|
||||
startIndex: 0,
|
||||
@@ -280,30 +279,23 @@ function addChannel(name) {
|
||||
if (!uiState.channels.includes(normalizedName)) {
|
||||
uiState.channels.push(normalizedName);
|
||||
|
||||
// Add to channel list (async DOM update)
|
||||
requestAnimationFrame(() => {
|
||||
const channelItem = document.createElement('div');
|
||||
channelItem.className = 'channel-item';
|
||||
channelItem.innerHTML = `
|
||||
<div class="channel-icon">
|
||||
<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="M9 9H15M9 12H15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="channel-name">${escapeHtml(normalizedName)}</div>
|
||||
`;
|
||||
channelItem.addEventListener('click', () => setActiveTab(normalizedName));
|
||||
channelList.appendChild(channelItem);
|
||||
});
|
||||
// Add to channel list
|
||||
const channelItem = document.createElement('div');
|
||||
channelItem.className = 'channel-item';
|
||||
channelItem.innerHTML = `
|
||||
<div class="channel-icon">
|
||||
<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="M9 9H15M9 12H15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="channel-name">${escapeHtml(normalizedName)}</div>
|
||||
`;
|
||||
channelItem.addEventListener('click', () => setActiveTab(normalizedName));
|
||||
channelList.appendChild(channelItem);
|
||||
|
||||
// Add tab (async)
|
||||
requestAnimationFrame(() => addTab(normalizedName));
|
||||
|
||||
// Initialize message storage for this channel
|
||||
if (!uiState.channelMessages.has(normalizedName)) {
|
||||
uiState.channelMessages.set(normalizedName, []);
|
||||
}
|
||||
// Add tab
|
||||
addTab(normalizedName);
|
||||
|
||||
// Auto-join the channel when connected
|
||||
if (uiState.connected) {
|
||||
@@ -330,9 +322,6 @@ function removeChannel(name) {
|
||||
// Clear users for this channel
|
||||
clearChannelUsers(name);
|
||||
|
||||
// Clear messages for this channel (optional - you might want to keep them)
|
||||
// uiState.channelMessages.delete(name);
|
||||
|
||||
// Remove tab
|
||||
removeTab(name);
|
||||
|
||||
@@ -398,114 +387,51 @@ function setActiveTab(name) {
|
||||
|
||||
uiState.activeTab = normalizedName;
|
||||
|
||||
// Update tabs (async)
|
||||
requestAnimationFrame(() => {
|
||||
const tabs = tabBar.querySelectorAll('.tab');
|
||||
tabs.forEach(tab => {
|
||||
// Compare with both normalized and original name
|
||||
tab.classList.toggle('active', tab.dataset.tab === normalizedName || tab.dataset.tab === name);
|
||||
});
|
||||
// Update tabs
|
||||
const tabs = tabBar.querySelectorAll('.tab');
|
||||
tabs.forEach(tab => {
|
||||
// Compare with both normalized and original name
|
||||
tab.classList.toggle('active', tab.dataset.tab === normalizedName || tab.dataset.tab === name);
|
||||
});
|
||||
|
||||
// Update channels (async)
|
||||
requestAnimationFrame(() => {
|
||||
const channelItems = channelList.querySelectorAll('.channel-item');
|
||||
channelItems.forEach(item => {
|
||||
const channelName = item.querySelector('.channel-name').textContent;
|
||||
item.classList.toggle('active', channelName === normalizedName || channelName === name);
|
||||
});
|
||||
// Update channels
|
||||
const channelItems = channelList.querySelectorAll('.channel-item');
|
||||
channelItems.forEach(item => {
|
||||
const channelName = item.querySelector('.channel-name').textContent;
|
||||
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);
|
||||
|
||||
// Update messages (async)
|
||||
requestAnimationFrame(() => {
|
||||
updateMessagesForTab(normalizedName);
|
||||
});
|
||||
// Update messages
|
||||
updateMessagesForTab(normalizedName);
|
||||
}
|
||||
|
||||
function updateMessagesForTab(name) {
|
||||
// Clear messages
|
||||
messagesContainer.innerHTML = '';
|
||||
|
||||
// Ensure channel name format is correct
|
||||
let channelKey = name;
|
||||
if (name !== 'welcome' && !name.startsWith('#')) {
|
||||
channelKey = '#' + name;
|
||||
// Add welcome messages
|
||||
if (name === 'welcome') {
|
||||
const welcomeMsg = document.createElement('div');
|
||||
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
|
||||
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.');
|
||||
}
|
||||
}
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// Message handling
|
||||
@@ -545,289 +471,44 @@ function addMessage(sender, content, isUser = false) {
|
||||
const channel = uiState.activeTab;
|
||||
if (!channel || channel === 'welcome') return;
|
||||
|
||||
// Normalize channel name
|
||||
let channelKey = channel;
|
||||
if (!channelKey.startsWith('#')) {
|
||||
channelKey = '#' + channelKey;
|
||||
}
|
||||
const messageLine = document.createElement('div');
|
||||
messageLine.className = 'message-line';
|
||||
|
||||
// Store message in RAM - ALWAYS store
|
||||
if (!uiState.channelMessages.has(channelKey)) {
|
||||
uiState.channelMessages.set(channelKey, []);
|
||||
}
|
||||
const senderElement = document.createElement('div');
|
||||
senderElement.className = `message-sender ${isUser ? 'you' : ''}`;
|
||||
senderElement.textContent = isUser ? `${sender} (You):` : `${sender}:`;
|
||||
senderElement.style.color = isUser ? '' : getUserColor(sender);
|
||||
|
||||
// Use requestAnimationFrame for async DOM updates
|
||||
requestAnimationFrame(() => {
|
||||
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);
|
||||
|
||||
// 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();
|
||||
});
|
||||
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 channel = uiState.activeTab;
|
||||
if (!channel || channel === 'welcome') return;
|
||||
|
||||
// Normalize channel name
|
||||
let channelKey = channel;
|
||||
if (!channelKey.startsWith('#')) {
|
||||
channelKey = '#' + channelKey;
|
||||
}
|
||||
const systemMessage = document.createElement('div');
|
||||
systemMessage.className = 'system-message';
|
||||
systemMessage.textContent = content;
|
||||
|
||||
// Store message in RAM - ALWAYS store
|
||||
if (!uiState.channelMessages.has(channelKey)) {
|
||||
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();
|
||||
});
|
||||
messagesContainer.appendChild(systemMessage);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function addIRCMessage(content) {
|
||||
// Parse the message to determine which channel it belongs to
|
||||
let targetChannel = null;
|
||||
let joinMatch = null;
|
||||
let partMatch = null;
|
||||
const ircMessage = document.createElement('div');
|
||||
ircMessage.className = 'irc-message';
|
||||
ircMessage.innerHTML = content;
|
||||
|
||||
// Extract plain text for checking
|
||||
const plainText = content.replace(/<[^>]*>/g, '');
|
||||
|
||||
// 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();
|
||||
});
|
||||
messagesContainer.appendChild(ircMessage);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
@@ -959,42 +640,40 @@ function updatePeopleList(channel) {
|
||||
}
|
||||
|
||||
// Sort and filter users (all stored in RAM, not DOM)
|
||||
setTimeout(() => {
|
||||
let sortedUsers = Array.from(users).sort((a, b) => {
|
||||
const aIsOp = a.startsWith('@');
|
||||
const bIsOp = b.startsWith('@');
|
||||
const aIsVoiced = a.startsWith('+');
|
||||
const bIsVoiced = b.startsWith('+');
|
||||
|
||||
if (aIsOp && !bIsOp) return -1;
|
||||
if (!aIsOp && bIsOp) return 1;
|
||||
if (aIsVoiced && !bIsVoiced) return -1;
|
||||
if (!aIsVoiced && bIsVoiced) return 1;
|
||||
return a.localeCompare(b, undefined, { sensitivity: 'base' });
|
||||
let sortedUsers = Array.from(users).sort((a, b) => {
|
||||
const aIsOp = a.startsWith('@');
|
||||
const bIsOp = b.startsWith('@');
|
||||
const aIsVoiced = a.startsWith('+');
|
||||
const bIsVoiced = b.startsWith('+');
|
||||
|
||||
if (aIsOp && !bIsOp) return -1;
|
||||
if (!aIsOp && bIsOp) return 1;
|
||||
if (aIsVoiced && !bIsVoiced) return -1;
|
||||
if (!aIsVoiced && bIsVoiced) return 1;
|
||||
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();
|
||||
if (searchQuery) {
|
||||
sortedUsers = sortedUsers.filter(user => {
|
||||
const displayName = (user.startsWith('@') || user.startsWith('+'))
|
||||
? user.substring(1).toLowerCase()
|
||||
: user.toLowerCase();
|
||||
return displayName.includes(searchQuery);
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
function renderVisibleUsers() {
|
||||
@@ -1039,20 +718,18 @@ function renderVisibleUsers() {
|
||||
}
|
||||
|
||||
// Clear and render only visible items
|
||||
requestAnimationFrame(() => {
|
||||
if (!peopleList) return;
|
||||
|
||||
peopleList.innerHTML = '';
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const user = uiState.filteredUsers[i];
|
||||
const peopleItem = createPeopleItem(user);
|
||||
fragment.appendChild(peopleItem);
|
||||
}
|
||||
|
||||
peopleList.appendChild(fragment);
|
||||
});
|
||||
if (!peopleList) return;
|
||||
|
||||
peopleList.innerHTML = '';
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const user = uiState.filteredUsers[i];
|
||||
const peopleItem = createPeopleItem(user);
|
||||
fragment.appendChild(peopleItem);
|
||||
}
|
||||
|
||||
peopleList.appendChild(fragment);
|
||||
}
|
||||
|
||||
function createPeopleItem(user) {
|
||||
@@ -1098,49 +775,42 @@ function handlePeopleSearch(e) {
|
||||
}
|
||||
|
||||
function handlePeopleListScroll() {
|
||||
requestAnimationFrame(() => {
|
||||
renderVisibleUsers();
|
||||
});
|
||||
renderVisibleUsers();
|
||||
}
|
||||
|
||||
function updateVirtualScrollContainerHeight() {
|
||||
requestAnimationFrame(() => {
|
||||
if (peopleListContainer) {
|
||||
uiState.virtualScrollState.containerHeight = peopleListContainer.clientHeight;
|
||||
renderVisibleUsers();
|
||||
}
|
||||
});
|
||||
if (peopleListContainer) {
|
||||
uiState.virtualScrollState.containerHeight = peopleListContainer.clientHeight;
|
||||
renderVisibleUsers();
|
||||
}
|
||||
}
|
||||
|
||||
function parseNamesMessage(channel, usersStr) {
|
||||
if (!channel || !usersStr) return;
|
||||
|
||||
// Process parsing asynchronously
|
||||
setTimeout(() => {
|
||||
// Initialize users set for this channel if it doesn't exist
|
||||
// Note: We accumulate users from multiple NAMES messages until ENDOFNAMES
|
||||
// Users are stored in RAM, not DOM
|
||||
if (!uiState.channelUsers.has(channel)) {
|
||||
uiState.channelUsers.set(channel, new Set());
|
||||
// Initialize users set for this channel if it doesn't exist
|
||||
// Note: We accumulate users from multiple NAMES messages until ENDOFNAMES
|
||||
// Users are stored in RAM, not DOM
|
||||
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);
|
||||
|
||||
// Add all users from this NAMES message to RAM
|
||||
users.forEach(user => {
|
||||
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);
|
||||
});
|
||||
|
||||
// Update the people list if this is the active channel
|
||||
// This will use virtual scrolling to only render visible users
|
||||
if (uiState.activeTab === channel) {
|
||||
updatePeopleList(channel);
|
||||
}
|
||||
}
|
||||
|
||||
function clearChannelUsers(channel) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="./styles/styles.css">
|
||||
<link rel="stylesheet" href="./styles/mobile.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
@@ -14,7 +15,7 @@
|
||||
<aside class="sidebar" id="channels">
|
||||
<div class="sidebar-resize-handle" id="sidebarResizeHandle"></div>
|
||||
<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">
|
||||
<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"/>
|
||||
@@ -134,5 +135,6 @@
|
||||
|
||||
<script src="tauri.js"></script>
|
||||
<script src="UI.js"></script>
|
||||
<script src="mobile.js"></script>
|
||||
</body>
|
||||
</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 {
|
||||
font-family: "Inter", "Segoe UI", sans-serif;
|
||||
font-size: 15px;
|
||||
background: #1a1c21;
|
||||
color: #f1f1f1;
|
||||
background: #2D2D2D;
|
||||
color: #DDD;
|
||||
--sidebar-width: 280px;
|
||||
--sidebar-collapsed-width: 60px;
|
||||
--primary-color: #4b82ff;
|
||||
--primary-color: #4A4A4A;
|
||||
--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;
|
||||
--bg-dark: #2D2D2D;
|
||||
--bg-panel: #252525;
|
||||
--bg-content: #2D2D2D;
|
||||
--border-color: #555;
|
||||
--text-muted: #777;
|
||||
--message-bg: #1E1E1E;
|
||||
--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);
|
||||
font-family: 'Inter', sans-serif;
|
||||
overflow: hidden;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #2D2D2D;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
@@ -164,13 +173,14 @@ body {
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s ease;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
max-width: 500px;
|
||||
resize: horizontal;
|
||||
flex-shrink: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sidebar-right {
|
||||
@@ -212,6 +222,7 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
@@ -220,22 +231,24 @@ body {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
.toggle-sidebar {
|
||||
background: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
color: #DDD;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-sidebar:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: #3A3A3A;
|
||||
}
|
||||
|
||||
.connection-section,
|
||||
@@ -246,7 +259,7 @@ body {
|
||||
|
||||
.section-title {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
color: #DDD;
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
@@ -268,21 +281,22 @@ body {
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
color: #DDD;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #3a3f4d;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
background: #101218;
|
||||
color: #f1f1f1;
|
||||
background: #3A3A3A;
|
||||
color: #DDD;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
border-color: var(--primary-color);
|
||||
border-color: #555;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -290,35 +304,44 @@ body {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #3a3f4d;
|
||||
background: #242730;
|
||||
color: #f1f1f1;
|
||||
border: 1px solid #555;
|
||||
background: #3A3A3A;
|
||||
color: #DDD;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #2c303a;
|
||||
background: #4A4A4A;
|
||||
}
|
||||
|
||||
button:active:not(:disabled) {
|
||||
background: #2A2A2A;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.4;
|
||||
background: #2A2A2A;
|
||||
color: #777;
|
||||
cursor: not-allowed;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
background: #3A3A3A;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #5a92ff;
|
||||
background: #4A4A4A;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@@ -332,6 +355,7 @@ button:disabled {
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
font-size: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@@ -339,6 +363,7 @@ button:disabled {
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--danger-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
@@ -359,15 +384,16 @@ button:disabled {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.channel-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: #3A3A3A;
|
||||
}
|
||||
|
||||
.channel-item.active {
|
||||
background: rgba(75, 130, 255, 0.15);
|
||||
color: var(--primary-color);
|
||||
background: #3A3A3A;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
.channel-icon {
|
||||
@@ -389,6 +415,7 @@ button:disabled {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.people-section {
|
||||
@@ -402,17 +429,17 @@ button:disabled {
|
||||
.people-search-input {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #3a3f4d;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
background: #101218;
|
||||
color: #f1f1f1;
|
||||
background: #3A3A3A;
|
||||
color: #DDD;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.people-search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.people-list-container {
|
||||
@@ -444,10 +471,11 @@ button:disabled {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.85rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.people-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: #3A3A3A;
|
||||
}
|
||||
|
||||
.people-item.op {
|
||||
@@ -493,6 +521,7 @@ button:disabled {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-content);
|
||||
min-width: 0; /* Important for flexbox shrinking */
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@@ -500,40 +529,58 @@ button:disabled {
|
||||
gap: 2px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: #14151a;
|
||||
background: #252525;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
.tabs::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
border: 1px solid #2f323a;
|
||||
border: 1px solid #555;
|
||||
border-bottom: none;
|
||||
background: #1b1d23;
|
||||
background: #2D2D2D;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
color: #DDD;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: #23252d;
|
||||
color: #f5f5f5;
|
||||
background: #3A3A3A;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
margin-left: 4px;
|
||||
opacity: 0.6;
|
||||
font-size: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -555,14 +602,22 @@ button:disabled {
|
||||
.message-line {
|
||||
display: flex;
|
||||
padding: 2px 0;
|
||||
color: #DDD;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-sender {
|
||||
min-width: 120px;
|
||||
min-width: 80px;
|
||||
max-width: 120px;
|
||||
font-weight: 500;
|
||||
padding-right: 8px;
|
||||
text-align: right;
|
||||
color: var(--text-muted);
|
||||
color: #DDD;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-sender.you {
|
||||
@@ -571,13 +626,16 @@ button:disabled {
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
color: #e1e1e1;
|
||||
color: #DDD;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.system-message {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: 4px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.irc-message {
|
||||
@@ -598,24 +656,27 @@ button:disabled {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #2a2e35;
|
||||
border-radius: 24px;
|
||||
background: #3A3A3A;
|
||||
border-radius: 4px;
|
||||
padding: 0 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.composer-input input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #f1f1f1;
|
||||
color: #DDD;
|
||||
padding: 12px 0;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.composer-input input::placeholder {
|
||||
color: #6a7283;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
@@ -623,13 +684,15 @@ button:disabled {
|
||||
bottom: 110%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #101116;
|
||||
border: 1px solid #3a3f4d;
|
||||
background: #252525;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.suggestions button {
|
||||
@@ -637,11 +700,14 @@ button:disabled {
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
border-radius: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.suggestions button:hover,
|
||||
.suggestions button.active {
|
||||
background: #222430;
|
||||
background: #3A3A3A;
|
||||
}
|
||||
|
||||
.suggestions.hidden {
|
||||
@@ -674,23 +740,420 @@ button:disabled {
|
||||
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 */
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 1024px) {
|
||||
:root {
|
||||
--sidebar-width: 240px;
|
||||
}
|
||||
|
||||
.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;
|
||||
height: 100%;
|
||||
resize: none;
|
||||
min-width: unset;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.sidebar:not(.collapsed) {
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5);
|
||||
|
||||
.sidebar.active {
|
||||
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 {
|
||||
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 {
|
||||
-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