// ====================
// Timezone utilities
// ====================
function getUserTimezone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}
function parseUTCToLocal(utcTimestamp) {
if (!utcTimestamp) return new Date();
let isoString = utcTimestamp.replace(' ', 'T');
if (!isoString.endsWith('Z') && !isoString.includes('+')) {
isoString += 'Z';
}
return new Date(isoString);
}
function formatLocalTime(utcTimestamp) {
const localDate = parseUTCToLocal(utcTimestamp);
const dateStr = localDate.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric'
});
const timeStr = localDate.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit'
});
return { dateStr, timeStr };
}
// ====================
// Messaging and chat related logic
// ====================
let socket = null;
let isSending = false;
function connectSocketIO() {
if (!token) return;
if (socket) {
socket.disconnect();
}
console.log('Connecting to SocketIO...');
try {
// Connect to the same host/port as the HTTP requests
const socketUrl = window.location.origin;
socket = io(socketUrl, {
transports: ['websocket', 'polling']
});
socket.on('connect', () => {
console.log('SocketIO connected');
});
socket.on('new_message', (message) => {
console.log('SocketIO: New message received:', message);
handleSocketMessage(message);
});
socket.on('user_ban_status', (data) => {
console.log('SocketIO: User ban status:', data);
if (data.user_id == userId && data.is_banned) {
// Current user was banned, disconnect
alert('You have been banned from the chat.');
logout();
}
});
socket.on('delete_message', (data) => {
console.log('SocketIO: Delete message:', data);
// Remove the message from the UI
const messageElement = document.querySelector(`[data-msg-id="${data.id}"]`);
if (messageElement) {
messageElement.remove();
}
});
socket.on('system_notification', (data) => {
console.log('SocketIO: System notification:', data);
// Show system notification
showBrowserNotification('System Notification', data.message, null, null);
// Could also display in chat if needed
});
socket.on('disconnect', () => {
console.log('SocketIO disconnected');
});
socket.on('connect_error', (error) => {
console.error('SocketIO connection error:', error);
// Try to reconnect after a delay
setTimeout(() => {
if (!socket.connected) {
console.log('Retrying SocketIO connection...');
connectSocketIO();
}
}, 5000);
});
} catch (e) {
console.error('SocketIO: Error creating connection:', e);
}
}
function handleSocketMessage(message) {
if (message.sender_id == userId) return;
let key = 'global';
if (message.channel_id) {
key = conversationKey('c', message.channel_id);
} else if (message.is_global != 1) {
const otherUserId = message.sender_id == userId ? message.receiver_id : message.sender_id;
key = conversationKey('d', otherUserId);
}
if (shouldNotifyMessage(message, key)) {
let title = '';
let body = '';
if (message.channel_id) {
let channelName = 'Channel';
let serverName = 'Server';
servers.forEach(s => {
const ch = s.channels.find(c => c.id == message.channel_id);
if (ch) {
channelName = ch.name;
serverName = s.name;
}
});
title = `${serverName} - #${channelName}`;
} else if (message.is_global == 1) {
title = 'Global Chat';
} else {
title = `DM from ${message.username}`;
}
let contentPreview = '';
try {
const parsed = JSON.parse(message.content);
contentPreview = parsed.text || parsed.file?.filename || '[File]';
} catch (e) {
contentPreview = message.content;
}
body = contentPreview.substring(0, 100);
if (contentPreview.length > 100) body += '...';
showBrowserNotification(title, body, message.avatar_url || null, null);
markMessageNotified(message.id);
}
const ts = new Date(message.timestamp).getTime();
if (!lastSeen[key] || ts > lastSeen[key]) {
unreadCounts[key] = (unreadCounts[key] || 0) + 1;
lastSeen[key] = ts;
renderUnreadIndicators();
}
// Add the message to the UI if it's for the current view
//console.log('Attempting to display message:', message, 'currentChannelId:', currentChannelId, 'currentDMUserId:', currentDMUserId);
displayMessages([message]);
}
function disconnectSocketIO() {
if (socket) {
socket.disconnect();
socket = null;
}
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
async function requestNotificationPermission() {
if (!('Notification' in window)) {
console.log('This browser does not support notifications');
return false;
}
if (Notification.permission === 'granted') {
return true;
}
if (Notification.permission !== 'denied') {
const permission = await Notification.requestPermission();
notificationPermission = permission;
return permission === 'granted';
}
return false;
}
function showBrowserNotification(title, body, icon = null, onClick = null) {
if (notificationPermission !== 'granted') {
return;
}
const options = {
body: body,
icon: icon || '/static/avatars/avatar_1.png',
badge: '/static/avatars/avatar_1.png',
tag: title,
requireInteraction: false
};
const notification = new Notification(title, options);
if (onClick) {
notification.onclick = function() {
window.focus();
onClick();
notification.close();
};
}
setTimeout(() => {
notification.close();
}, 5000);
}
function shouldNotifyMessage(msg, conversationKey) {
if (msg.sender_id == userId) return false;
const isCurrentConversation = (
(currentDMUserId && conversationKey === `d${currentDMUserId}`) ||
(currentChannelId && conversationKey === `c${currentChannelId}`) ||
(!currentDMUserId && !currentChannelId && conversationKey === 'global')
);
if (isCurrentConversation && document.visibilityState === 'visible') {
return false;
}
const msgKey = `msg_${msg.id}`;
if (lastNotifiedMessages[msgKey]) {
return false;
}
return true;
}
function markMessageNotified(msgId) {
lastNotifiedMessages[`msg_${msgId}`] = Date.now();
const keys = Object.keys(lastNotifiedMessages);
if (keys.length > 100) {
const cutoff = Date.now() - (24 * 60 * 60 * 1000);
keys.forEach(key => {
if (lastNotifiedMessages[key] < cutoff) {
delete lastNotifiedMessages[key];
}
});
}
}
function renderMessageContent(content) {
let parsed;
try {
parsed = JSON.parse(content);
} catch (e) {
parsed = null;
}
if (parsed && parsed.file) {
const file = parsed.file;
let html = '';
if (parsed.text) {
html += escapeHtml(parsed.text) + '
';
}
const url = file.url;
const mime = file.mimetype || '';
if (mime.startsWith('image/')) {
html += ``;
} else if (mime.startsWith('video/')) {
html += ``;
} else if (mime.startsWith('audio/')) {
html += ``;
} else {
html += `${escapeHtml(file.filename)} (${formatBytes(file.size)})`;
}
return html;
}
return escapeHtml(content);
}
async function sendMessage() {
if (isSending) return;
const text = messageInput.value.trim();
const file = fileInput ? fileInput.files[0] : null;
if (!text && !file) return;
isSending = true;
sendBtn.disabled = true;
sendBtn.classList.add('btn-disabled');
messageInput.readOnly = true;
messageInput.classList.add('input-disabled');
try {
let res;
if (file) {
const form = new FormData();
if (text) form.append('content', text);
form.append('file', file);
if (currentDMUserId) form.append('receiver_id', currentDMUserId);
if (currentChannelId) form.append('channel_id', currentChannelId);
if (replyToMessage) form.append('reply_to', replyToMessage.id);
res = await fetch(`${API_URL}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: form
});
} else {
const body = { content: text };
if (currentDMUserId) body.receiver_id = currentDMUserId;
if (currentChannelId) body.channel_id = currentChannelId;
if (replyToMessage) body.reply_to = replyToMessage.id;
res = await fetch(`${API_URL}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(body)
});
}
if (res && res.ok) {
messageInput.value = '';
fileInput.value = '';
filePreview.style.display = 'none';
cancelReply();
if (currentDMUserId) {
loadDM(currentDMUserId);
} else {
loadChannel(currentChannelId);
}
} else if (res) {
const err = await res.json();
console.error('sendMessage error response', err);
}
} catch (error) {
console.error('Error sending message:', error);
} finally {
isSending = false;
sendBtn.disabled = false;
sendBtn.classList.remove('btn-disabled');
messageInput.readOnly = false;
messageInput.classList.remove('input-disabled');
}
}
async function loadChannel(channelId) {
try {
let url = `${API_URL}/messages?channel_id=${channelId}`;
if (lastFetchedTs) url += `&since=${encodeURIComponent(lastFetchedTs)}`;
const res = await fetch(url, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.ok) {
const messages = await res.json();
displayMessages(messages, !!lastFetchedTs);
}
} catch (error) {
console.error('Error loading channel messages:', error);
}
}
async function loadGlobalChat() {
if (currentChannelId) return loadChannel(currentChannelId);
try {
const res = await fetch(`${API_URL}/messages`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.ok) {
const messages = await res.json();
displayMessages(messages);
}
} catch (error) {
console.error('Error loading messages:', error);
}
}
async function loadUsers() {
try {
const res = await fetch(`${API_URL}/users`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.ok) {
allUsers = await res.json();
}
} catch (error) {
console.error('Error loading users:', error);
}
}
async function loadDM(userId) {
try {
const res = await fetch(`${API_URL}/dm/${userId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.ok) {
const messages = await res.json();
displayMessages(messages);
}
} catch (error) {
console.error('Error loading DM:', error);
}
}
let lastFetchedTs = null;
let isAtBottom = true;
function displayMessages(messages, append = false) {
const existing = new Set();
[...messagesContainer.querySelectorAll('.message')].forEach(el => {
const id = el.getAttribute('data-msg-id');
if (id) existing.add(String(id));
});
messages.forEach(msg => {
//console.log('Processing message:', msg.id, 'channel_id:', msg.channel_id, 'currentChannelId:', currentChannelId, 'currentDMUserId:', currentDMUserId, 'is_global:', msg.is_global);
if (typeof currentChannelId !== 'undefined' && currentChannelId) {
if (String(msg.channel_id || '') !== String(currentChannelId)) {
console.log('Message filtered out: channel mismatch');
return;
}
} else if (typeof currentDMUserId !== 'undefined' && currentDMUserId) {
const rid = msg.receiver_id || msg.receiver || null;
if (!(String(msg.sender_id) === String(currentDMUserId) || String(rid) === String(currentDMUserId))) return;
} else {
if (msg.is_global === 0 || msg.is_global === '0') return;
}
const msgId = String(msg.id || msg._id || msg.timestamp);
if (existing.has(msgId)) {
return;
}
const msgEl = document.createElement('div');
msgEl.className = `message ${msg.sender_id == userId ? 'own' : ''}`;
msgEl.setAttribute('data-msg-id', msgId);
msgEl.setAttribute('data-sender-id', msg.sender_id);
let avatarHtml = '';
if (msg.avatar_url) {
avatarHtml = `
`;
}
let badgeHtml = '';
if (msg.badge) {
const badgeClass = msg.badge.toLowerCase().replace(/\s+/g, '-');
badgeHtml = `${msg.badge}`;
}
let pron = msg.pronouns ? ` (${msg.pronouns})` : '';
const { dateStr, timeStr } = formatLocalTime(msg.timestamp);
const renderedContent = renderMessageContent(msg.content);
let replyIndicatorHtml = '';
if (msg.reply_to) {
replyIndicatorHtml = `