document.addEventListener('DOMContentLoaded', () => {
const fileUpload = document.getElementById('file-upload');
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const messagesList = document.getElementById('messages-list');
const typingIndicator = document.getElementById('typing-indicator');
// Emoji list for reactions
const EMOJIS = ['👍', '❤️', '😂', '😮', '😢', '🔥', '✅', '🚀'];
// Scroll to bottom
messagesList.scrollTop = messagesList.scrollHeight;
const currentChannel = new URLSearchParams(window.location.search).get('channel_id') || 1;
let typingTimeout;
// Notification Permission
if ("Notification" in window && Notification.permission === "default") {
Notification.requestPermission();
}
// WebSocket for real-time
let ws;
let voiceHandler;
function connectWS() {
try {
ws = new WebSocket('ws://' + window.location.hostname + ':8080');
if (typeof VoiceChannel !== 'undefined') {
voiceHandler = new VoiceChannel(ws);
}
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
// Voice signaling
if (msg.type && msg.type.startsWith('voice_')) {
if (voiceHandler) voiceHandler.handleSignaling(msg);
return;
}
if (msg.type === 'message') {
const data = JSON.parse(msg.data);
if (data.channel_id == currentChannel) {
appendMessage(data);
messagesList.scrollTop = messagesList.scrollHeight;
// Desktop Notifications for mentions
if (data.content.includes(`@${window.currentUsername}`) && data.user_id != window.currentUserId) {
if (Notification.permission === "granted" && !window.isDndMode) {
new Notification(`Mention in #${window.currentChannelName}`, {
body: `${data.username}: ${data.content}`,
icon: data.avatar_url || ''
});
}
}
}
} else if (msg.type === 'typing') {
if (msg.channel_id == currentChannel && msg.user_id != window.currentUserId) {
showTyping(msg.username);
}
} else if (msg.type === 'reaction') {
updateReactionUI(msg.message_id, msg.reactions);
} else if (msg.type === 'message_edit') {
const el = document.querySelector(`.message-item[data-id="${msg.message_id}"] .message-text`);
if (el) el.innerHTML = msg.content.replace(/\n/g, '
');
} else if (msg.type === 'message_delete') {
document.querySelector(`.message-item[data-id="${msg.message_id}"]`)?.remove();
} else if (msg.type === 'presence') {
updatePresenceUI(msg.user_id, msg.status);
}
};
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'presence',
user_id: window.currentUserId,
status: 'online'
}));
};
ws.onclose = () => setTimeout(connectWS, 3000);
} catch (e) {
console.warn('WebSocket connection failed.');
}
}
connectWS();
function showTyping(username) {
typingIndicator.textContent = `${username} is typing...`;
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
typingIndicator.textContent = '';
}, 3000);
}
chatInput.addEventListener('input', () => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'typing',
channel_id: currentChannel,
user_id: window.currentUserId,
username: window.currentUsername
}));
}
});
chatForm.addEventListener('submit', (e) => {
e.preventDefault();
const content = chatInput.value.trim();
const file = fileUpload.files[0];
if (!content && !file) return;
chatInput.value = '';
const formData = new FormData();
formData.append('content', content);
formData.append('channel_id', currentChannel);
const progressContainer = document.getElementById('upload-progress-container');
const progressBar = document.getElementById('upload-progress-bar');
const progressPercent = document.getElementById('upload-percentage');
const progressFilename = document.getElementById('upload-filename');
if (file) {
formData.append('file', file);
fileUpload.value = ''; // Clear file input
// Show progress bar
progressContainer.style.display = 'block';
progressFilename.textContent = `Uploading: ${file.name}`;
progressBar.style.width = '0%';
progressPercent.textContent = '0%';
}
const xhr = new XMLHttpRequest();
xhr.open('POST', 'api_v1_messages.php', true);
xhr.upload.onprogress = (ev) => {
if (ev.lengthComputable && file) {
const percent = Math.round((ev.loaded / ev.total) * 100);
progressBar.style.width = percent + '%';
progressPercent.textContent = percent + '%';
}
};
xhr.onload = () => {
if (xhr.status === 200) {
const result = JSON.parse(xhr.responseText);
if (result.success) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'message',
data: JSON.stringify({
...result.message,
channel_id: currentChannel
})
}));
} else {
appendMessage(result.message);
messagesList.scrollTop = messagesList.scrollHeight;
}
} else {
alert(result.error || 'Failed to send message');
}
}
progressContainer.style.display = 'none';
};
xhr.onerror = () => {
console.error('XHR Error');
progressContainer.style.display = 'none';
alert('An error occurred during the upload.');
};
xhr.send(formData);
});
// Handle Reaction Clicks
document.addEventListener('click', (e) => {
const badge = e.target.closest('.reaction-badge');
if (badge) {
const msgId = badge.parentElement.dataset.messageId;
const emoji = badge.dataset.emoji;
toggleReaction(msgId, emoji);
return;
}
const addBtn = e.target.closest('.add-reaction-btn');
if (addBtn) {
const msgId = addBtn.parentElement.dataset.messageId;
showEmojiPicker(addBtn, msgId);
return;
}
// Close picker if click outside
if (!e.target.closest('.emoji-picker')) {
const picker = document.querySelector('.emoji-picker');
if (picker) picker.remove();
}
});
async function toggleReaction(messageId, emoji) {
try {
const resp = await fetch('api_v1_reactions.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message_id: messageId, emoji: emoji })
});
const result = await resp.json();
if (result.success) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'reaction',
message_id: messageId,
reactions: result.reactions
}));
}
updateReactionUI(messageId, result.reactions);
}
} catch (e) { console.error(e); }
}
function showEmojiPicker(anchor, messageId) {
document.querySelector('.emoji-picker')?.remove();
const picker = document.createElement('div');
picker.className = 'emoji-picker';
EMOJIS.forEach(emoji => {
const span = document.createElement('span');
span.textContent = emoji;
span.onclick = () => {
toggleReaction(messageId, emoji);
picker.remove();
};
picker.appendChild(span);
});
document.body.appendChild(picker);
const rect = anchor.getBoundingClientRect();
picker.style.top = `${rect.top - picker.offsetHeight - 5}px`;
picker.style.left = `${rect.left}px`;
}
function updateReactionUI(messageId, reactions) {
const container = document.querySelector(`.message-reactions[data-message-id="${messageId}"]`);
if (!container) return;
const addBtn = container.querySelector('.add-reaction-btn');
container.innerHTML = '';
reactions.forEach(r => {
const badge = document.createElement('span');
const userList = r.users.split(',');
const active = userList.includes(String(window.currentUserId));
badge.className = `reaction-badge ${active ? 'active' : ''}`;
badge.dataset.emoji = r.emoji;
badge.innerHTML = `${r.emoji} ${r.count}`;
container.appendChild(badge);
});
container.appendChild(addBtn);
}
function updatePresenceUI(userId, status) {
const memberItem = document.querySelector(`.start-dm-btn[data-user-id="${userId}"] .message-avatar`);
if (memberItem) {
let indicator = memberItem.querySelector('.presence-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.className = 'presence-indicator';
memberItem.appendChild(indicator);
}
indicator.style.position = 'absolute';
indicator.style.bottom = '0';
indicator.style.right = '0';
indicator.style.width = '10px';
indicator.style.height = '10px';
indicator.style.borderRadius = '50%';
indicator.style.border = '2px solid var(--bg-members)';
indicator.style.backgroundColor = status === 'online' ? '#23a559' : '#80848e';
}
}
// Voice
if (voiceHandler) {
document.querySelectorAll('.voice-item').forEach(item => {
item.addEventListener('click', () => {
const cid = item.dataset.channelId;
if (voiceHandler.currentChannelId == cid) {
voiceHandler.leave();
item.classList.remove('active');
} else {
voiceHandler.join(cid);
document.querySelectorAll('.voice-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
}
});
});
}
// Message Actions (Edit/Delete)
document.addEventListener('click', async (e) => {
const editBtn = e.target.closest('.action-btn.edit');
if (editBtn) {
const msgId = editBtn.dataset.id;
const msgItem = editBtn.closest('.message-item');
const textEl = msgItem.querySelector('.message-text');
const originalContent = textEl.innerText;
const input = document.createElement('input');
input.type = 'text';
input.className = 'form-control bg-dark text-white';
input.value = originalContent;
textEl.innerHTML = '';
textEl.appendChild(input);
input.focus();
input.onkeydown = async (ev) => {
if (ev.key === 'Enter') {
const newContent = input.value.trim();
if (newContent && newContent !== originalContent) {
const resp = await fetch('api_v1_messages.php', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: msgId, content: newContent })
});
if ((await resp.json()).success) {
textEl.innerHTML = newContent.replace(/\n/g, '
');
ws?.send(JSON.stringify({ type: 'message_edit', message_id: msgId, content: newContent }));
}
} else {
textEl.innerHTML = originalContent.replace(/\n/g, '
');
}
} else if (ev.key === 'Escape') {
textEl.innerHTML = originalContent.replace(/\n/g, '
');
}
};
return;
}
const deleteBtn = e.target.closest('.action-btn.delete');
if (deleteBtn) {
if (!confirm('Delete this message?')) return;
const msgId = deleteBtn.dataset.id;
const resp = await fetch('api_v1_messages.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: msgId })
});
if ((await resp.json()).success) {
deleteBtn.closest('.message-item').remove();
ws?.send(JSON.stringify({ type: 'message_delete', message_id: msgId }));
}
return;
}
const pinBtn = e.target.closest('.action-btn.pin');
if (pinBtn) {
const msgId = pinBtn.dataset.id;
const isPinned = pinBtn.dataset.pinned == '1';
const action = isPinned ? 'unpin' : 'pin';
const resp = await fetch('api_v1_messages.php', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: msgId, action: action })
});
const result = await resp.json();
if (result.success) {
location.reload(); // Simplest way to reflect changes across UI
}
return;
}
const pinnedMessagesBtn = document.getElementById('pinned-messages-btn');
if (e.target.closest('#pinned-messages-btn')) {
const container = document.getElementById('pinned-messages-container');
container.innerHTML = '