1357 lines
40 KiB
JavaScript
1357 lines
40 KiB
JavaScript
alert("Chookchat is currently in early development, expect bugs! Please don't try breaking the public server, do that with your own test server (read more in the Git repo). Thanks for trying Chookchat!")
|
|
|
|
let ws;
|
|
let username;
|
|
let password;
|
|
let typingTimeout;
|
|
let typingPeople = [];
|
|
let currentRoom = "general";
|
|
let availableRooms = ["general"];
|
|
let isMobile = window.innerWidth < 768;
|
|
let avatarColors = {};
|
|
|
|
// Initialize DOM elements when document is ready
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Mobile menu toggle
|
|
if (isMobile) {
|
|
const menuToggle = document.createElement('button');
|
|
menuToggle.className = 'btn-icon menu-toggle';
|
|
menuToggle.innerHTML = '<i class="fa-solid fa-bars"></i>';
|
|
menuToggle.addEventListener('click', toggleSidebar);
|
|
|
|
const chatHeader = document.querySelector('.chat-header');
|
|
if (chatHeader) {
|
|
chatHeader.insertBefore(menuToggle, chatHeader.firstChild);
|
|
}
|
|
}
|
|
|
|
document.getElementById('messageInput').addEventListener('input', function () {
|
|
this.style.height = 'auto';
|
|
this.style.height = (this.scrollHeight) + 'px';
|
|
});
|
|
|
|
|
|
// Add event listeners
|
|
document.getElementById('messageInput').addEventListener('keydown', function(event) {
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
event.preventDefault();
|
|
sendMessage();
|
|
}
|
|
});
|
|
|
|
document.getElementById('password').addEventListener('keypress', (event) => {
|
|
if (event.key === 'Enter') {
|
|
connect();
|
|
}
|
|
});
|
|
|
|
document.getElementById('messageInput').addEventListener('input', startTypingIndicator);
|
|
|
|
// File upload preview
|
|
const fileInput = document.getElementById('fileupload');
|
|
if (fileInput) {
|
|
fileInput.addEventListener('change', previewFile);
|
|
}
|
|
|
|
// Handle window resize
|
|
window.addEventListener('resize', () => {
|
|
isMobile = window.innerWidth < 768;
|
|
if (!isMobile) {
|
|
const sidebar = document.querySelector('.sidebar');
|
|
if (sidebar) {
|
|
sidebar.classList.remove('visible');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Toggle sidebar on mobile
|
|
function toggleSidebar() {
|
|
const sidebar = document.querySelector('.sidebar');
|
|
if (sidebar) {
|
|
sidebar.classList.toggle('visible');
|
|
}
|
|
}
|
|
|
|
// Show server config
|
|
function showConfig() {
|
|
const serverconfig = document.getElementById('serverconfig');
|
|
if (serverconfig) {
|
|
serverconfig.style.display = serverconfig.style.display === 'block' ? 'none' : 'block';
|
|
}
|
|
}
|
|
|
|
// Hash password using MD5
|
|
function md5(string) {
|
|
return CryptoJS.MD5(string).toString();
|
|
}
|
|
|
|
// Get WebSocket URL
|
|
function getUrl() {
|
|
const serverUrl = document.getElementById('serverUrl').value.trim();
|
|
const serverPort = document.getElementById('serverPort').value;
|
|
const useWss = document.getElementById('securityStatus').checked;
|
|
const protocol = useWss ? 'wss' : 'ws';
|
|
|
|
const cleanUrl = serverUrl.replace(/^(https?:\/\/|wss?:\/\/)/, '');
|
|
|
|
return `${protocol}://${cleanUrl}:${serverPort}/api/websocket`;
|
|
}
|
|
|
|
// Get signup URL
|
|
function getSignupUrl() {
|
|
const serverUrl = document.getElementById('serverUrl').value.trim();
|
|
const serverPort = document.getElementById('serverPort').value;
|
|
const useWss = document.getElementById('securityStatus').checked;
|
|
const protocol = useWss ? 'https' : 'http';
|
|
|
|
const cleanUrl = serverUrl.replace(/^(https?:\/\/|wss?:\/\/)/, '');
|
|
|
|
return `${protocol}://${cleanUrl}:${serverPort}/api/createaccount/`;
|
|
}
|
|
|
|
// Get upload URL
|
|
function getUploadUrl() {
|
|
const serverUrl = document.getElementById('serverUrl').value.trim();
|
|
const serverPort = document.getElementById('serverPort').value;
|
|
const useWss = document.getElementById('securityStatus').checked;
|
|
const protocol = useWss ? 'https' : 'http';
|
|
|
|
const cleanUrl = serverUrl.replace(/^(https?:\/\/|wss?:\/\/)/, '');
|
|
|
|
return `${protocol}://${cleanUrl}:${serverPort}/api/upload`;
|
|
}
|
|
|
|
// Get available rooms
|
|
async function getRooms() {
|
|
try {
|
|
const serverUrl = document.getElementById('serverUrl').value.trim();
|
|
const serverPort = document.getElementById('serverPort').value;
|
|
const useWss = document.getElementById('securityStatus').checked;
|
|
const protocol = useWss ? 'https' : 'http';
|
|
const cleanUrl = serverUrl.replace(/^(https?:\/\/|wss?:\/\/)/, '');
|
|
const url = `${protocol}://${cleanUrl}:${serverPort}/api/rooms`;
|
|
const response = await fetch(url, {
|
|
mode: "no-cors"
|
|
});
|
|
const data = await response.json();
|
|
return JSON.parse(data.content);
|
|
} catch (error) {
|
|
console.error('Error fetching rooms:', error);
|
|
return ["general"];
|
|
}
|
|
}
|
|
|
|
// Get room message history
|
|
async function getRoomHistory(roomName) {
|
|
try {
|
|
const serverUrl = document.getElementById('serverUrl').value.trim();
|
|
const serverPort = document.getElementById('serverPort').value;
|
|
const useWss = document.getElementById('securityStatus').checked;
|
|
const protocol = useWss ? 'https' : 'http';
|
|
const cleanUrl = serverUrl.replace(/^(https?:\/\/|wss?:\/\/)/, '');
|
|
const url = `${protocol}://${cleanUrl}:${serverPort}/api/room/${roomName}/history`;
|
|
const response = await fetch(url, {
|
|
mode: "no-cors"
|
|
});
|
|
const history = await response.text();
|
|
return history;
|
|
} catch (error) {
|
|
console.error('Error fetching room history:', error);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// Image mime types and extensions
|
|
const imageMimeTypes = [
|
|
'image/webp',
|
|
'image/tiff',
|
|
'image/svg+xml',
|
|
'image/png',
|
|
'image/jpeg',
|
|
'image/vnd.microsoft.icon',
|
|
'image/gif',
|
|
'image/bmp',
|
|
];
|
|
|
|
const imageTypes = [
|
|
'png',
|
|
'jpg',
|
|
'jpeg',
|
|
'svg',
|
|
'tiff',
|
|
'gif',
|
|
'webp',
|
|
'bmp'
|
|
];
|
|
|
|
// Check if file is an image
|
|
function isImage(file) {
|
|
const fileSplit = file.split(".");
|
|
return imageTypes.includes(fileSplit[fileSplit.length - 1].toLowerCase());
|
|
}
|
|
|
|
// Connect to the server
|
|
async function connect() {
|
|
username = document.getElementById('username').value;
|
|
password = document.getElementById('password').value;
|
|
|
|
if (!username || !password) {
|
|
alert('Please enter a username and password');
|
|
return;
|
|
}
|
|
|
|
const wsUrl = getUrl();
|
|
if (ws) {
|
|
ws.close();
|
|
}
|
|
|
|
// Show connecting state
|
|
const loginButton = document.querySelector('.btn-primary');
|
|
if (loginButton) {
|
|
loginButton.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Connecting...';
|
|
loginButton.disabled = true;
|
|
}
|
|
|
|
ws = new WebSocket(wsUrl);
|
|
var incorrectDetail = 0;
|
|
|
|
ws.onopen = async () => {
|
|
if (typeof Notification !== "undefined") {
|
|
Notification.requestPermission();
|
|
}
|
|
console.log('Connected!');
|
|
|
|
document.getElementById('login-container').style.display = 'none';
|
|
document.getElementById('app').classList.remove('hidden');
|
|
|
|
// Set current username in UI
|
|
document.getElementById('current-username').textContent = username;
|
|
|
|
try {
|
|
availableRooms = await getRooms();
|
|
updateRoomList();
|
|
} catch (error) {
|
|
console.error('Failed to get room list:', error);
|
|
availableRooms = ["general"];
|
|
}
|
|
|
|
const connectMessage = {
|
|
"type": "connect",
|
|
"username": username,
|
|
"token": md5(password),
|
|
"content": `${username} joined the room!`
|
|
}
|
|
ws.send(JSON.stringify(connectMessage));
|
|
|
|
joinRoom("general");
|
|
|
|
ws.onmessage = handleMessage;
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
alert("Error connecting to the server. Please check your server settings.");
|
|
resetLoginButton();
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
if (!incorrectDetail) {
|
|
alert("Chookchat has disconnected. Refresh the page to reconnect.");
|
|
}
|
|
resetLoginButton();
|
|
};
|
|
}
|
|
|
|
// Reset login button state
|
|
function resetLoginButton() {
|
|
const loginButton = document.querySelector('.btn-primary');
|
|
if (loginButton) {
|
|
loginButton.innerHTML = 'Log in';
|
|
loginButton.disabled = false;
|
|
}
|
|
}
|
|
|
|
function markdownToHtmlDiv(markdown) {
|
|
// Emoji mapping
|
|
const emojiMap = {
|
|
// 😀 Faces
|
|
smile: '😄',
|
|
happy: '😊',
|
|
grin: '😁',
|
|
laugh: '😂',
|
|
joy: '🤣',
|
|
wink: '😉',
|
|
blush: '☺️',
|
|
cool: '😎',
|
|
smirk: '😏',
|
|
thinking: '🤔',
|
|
neutral: '😐',
|
|
expressionless: '😑',
|
|
sleepy: '😴',
|
|
dizzy: '😵',
|
|
surprised: '😲',
|
|
scream: '😱',
|
|
cry: '😢',
|
|
sob: '😭',
|
|
angry: '😠',
|
|
rage: '😡',
|
|
yum: '😋',
|
|
relieved: '😌',
|
|
confused: '😕',
|
|
nerd: '🤓',
|
|
zany: '🤪',
|
|
shush: '🤫',
|
|
hush: '🤐',
|
|
|
|
// ❤️ Emotions
|
|
heart: '❤️',
|
|
heartbroken: '💔',
|
|
love: '😍',
|
|
kiss: '😘',
|
|
hug: '🤗',
|
|
clap: '👏',
|
|
pray: '🙏',
|
|
ok: '👌',
|
|
fingerscrossed: '🤞',
|
|
thumbsup: '👍',
|
|
thumbsdown: '👎',
|
|
eyes: '👀',
|
|
|
|
// 🔥 Reactions
|
|
fire: '🔥',
|
|
100: '💯',
|
|
poop: '💩',
|
|
poo: '💩',
|
|
skull: '💀',
|
|
explosion: '💥',
|
|
mindblown: '🤯',
|
|
party: '🥳',
|
|
tada: '🎉',
|
|
balloon: '🎈',
|
|
|
|
// 🐶 Animals
|
|
chicken: '🐔',
|
|
dog: '🐶',
|
|
cat: '🐱',
|
|
fox: '🦊',
|
|
panda: '🐼',
|
|
pig: '🐷',
|
|
cow: '🐮',
|
|
rabbit: '🐰',
|
|
bear: '🐻',
|
|
unicorn: '🦄',
|
|
monkey: '🐒',
|
|
dragon: '🐉',
|
|
snake: '🐍',
|
|
|
|
// 🍔 Food & Drink
|
|
pizza: '🍕',
|
|
burger: '🍔',
|
|
fries: '🍟',
|
|
hotdog: '🌭',
|
|
taco: '🌮',
|
|
ramen: '🍜',
|
|
sushi: '🍣',
|
|
icecream: '🍨',
|
|
cake: '🎂',
|
|
coffee: '☕',
|
|
tea: '🍵',
|
|
beer: '🍺',
|
|
|
|
// 🌍 Nature & Weather
|
|
sun: '☀️',
|
|
moon: '🌙',
|
|
star: '⭐',
|
|
cloud: '☁️',
|
|
rain: '🌧️',
|
|
snow: '❄️',
|
|
lightning: '⚡',
|
|
rainbow: '🌈',
|
|
tree: '🌳',
|
|
flower: '🌸',
|
|
|
|
// ⚽ Activities
|
|
soccer: '⚽',
|
|
basketball: '🏀',
|
|
football: '🏈',
|
|
baseball: '⚾',
|
|
tennis: '🎾',
|
|
bowling: '🎳',
|
|
video_game: '🎮',
|
|
chess: '♟️',
|
|
music: '🎵',
|
|
guitar: '🎸',
|
|
mic: '🎤',
|
|
|
|
// 💡 Objects & Symbols
|
|
lightbulb: '💡',
|
|
phone: '📱',
|
|
laptop: '💻',
|
|
bomb: '💣',
|
|
money: '💰',
|
|
star2: '🌟',
|
|
warning: '⚠️',
|
|
check: '✅',
|
|
x: '❌',
|
|
question: '❓',
|
|
exclamation: '❗',
|
|
infinity: '♾️',
|
|
hourglass: '⏳',
|
|
clock: '🕒',
|
|
|
|
pepe: '🐸',
|
|
troll: '😈',
|
|
sus: '🧐',
|
|
amongus: '🟥',
|
|
monke: '🐵',
|
|
chad: '🦍',
|
|
gigachad: '💪',
|
|
wojak: '😔',
|
|
feelsbadman: '😞',
|
|
feelsgoodman: '😌',
|
|
yikes: '😬',
|
|
clown: '🤡',
|
|
clownworld: '🌎🤡',
|
|
triggered: '😡',
|
|
pog: '😲',
|
|
kek: '🤣',
|
|
based: '😎',
|
|
cringe: '😖',
|
|
dab: '🕺',
|
|
sigma: '🧠',
|
|
npc: '🤖',
|
|
doomer: '🌑',
|
|
zoomer: '⚡',
|
|
boomer: '👴',
|
|
sheesh: '😤',
|
|
rickroll: '🕺🎶',
|
|
trolled: '🧌',
|
|
rekt: '💀',
|
|
skillissue: '📉',
|
|
bro: '🙄',
|
|
cope: '😢',
|
|
ratio: '➗',
|
|
amogus: '👁️👄👁️',
|
|
|
|
};
|
|
|
|
// Escape HTML first
|
|
let html = markdown
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
|
|
// Code blocks (```...```)
|
|
html = html.replace(/```([^```]*)```/gs, (match, p1) =>
|
|
`<pre><code>${p1.trim()}</code></pre>`
|
|
);
|
|
|
|
// Inline code (`code`)
|
|
html = html.replace(/`([^`\n]+)`/g, (match, p1) => `<code>${p1}</code>`);
|
|
|
|
// Emojis :emoji_name:
|
|
html = html.replace(/:([a-z0-9_]+):/gi, (match, p1) => emojiMap[p1] || match);
|
|
|
|
// Process overlapping formatting in order: Bold+Underline+Italic
|
|
html = parseFormatting(html);
|
|
|
|
// Replace line breaks
|
|
html = html.replace(/\n/g, '<br>');
|
|
|
|
return html;
|
|
}
|
|
|
|
// Parse overlapping formatting using tokens
|
|
function parseFormatting(text) {
|
|
// Define replacement tokens and their HTML
|
|
const formattingRules = [
|
|
{ regex: /\*\*\*(.*?)\*\*\*/g, html: '<strong><em>$1</em></strong>' }, // ***bold italic***
|
|
{ regex: /___(.*?)___/g, html: '<u><em>$1</em></u>' }, // ___underline italic___
|
|
{ regex: /\*\*(.*?)\*\*/g, html: '<strong>$1</strong>' }, // **bold**
|
|
{ regex: /__(.*?)__/g, html: '<u>$1</u>' }, // __underline__
|
|
{ regex: /\*(.*?)\*/g, html: '<em>$1</em>' }, // *italic*
|
|
{ regex: /_(.*?)_/g, html: '<em>$1</em>' }, // _italic_
|
|
{ regex: /~~(.*?)~~/g, html: '<s>$1</s>' } // ~~strike~~
|
|
];
|
|
|
|
// Apply formatting in order
|
|
for (const rule of formattingRules) {
|
|
text = text.replace(rule.regex, rule.html);
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
|
|
// Handle incoming messages
|
|
function handleMessage(event) {
|
|
if (event.data === "ping") {
|
|
ws.send("pong");
|
|
return;
|
|
}
|
|
|
|
const message = JSON.parse(event.data);
|
|
|
|
// Handle error messages
|
|
if (message.type == "error") {
|
|
if (message.username == "system") {
|
|
if (message.content == "invalid-token") {
|
|
alert("Your password is incorrect! Please try again.");
|
|
location.reload();
|
|
return;
|
|
}
|
|
if (message.content == "unknown-account") {
|
|
alert("That username isn't on the server. Maybe try registering?");
|
|
location.reload();
|
|
return;
|
|
}
|
|
if (message.content == "banned") {
|
|
alert("You've been banned from the server.");
|
|
location.reload();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle typing indicators
|
|
else if (message.type == "typing") {
|
|
handleTypingIndicator(message);
|
|
return;
|
|
}
|
|
|
|
// Handle users list
|
|
else if (message.type == "users" && message.username == "system") {
|
|
updateOnlineUsers(message.content);
|
|
return;
|
|
}
|
|
|
|
// Handle room users
|
|
else if (message.type == "roomUsers" && message.username == "system") {
|
|
updateRoomUsers(message);
|
|
return;
|
|
}
|
|
|
|
// Handle rooms list
|
|
else if (message.type == "roomsList" && message.username == "system") {
|
|
try {
|
|
availableRooms = JSON.parse(message.content);
|
|
updateRoomList();
|
|
} catch (error) {
|
|
console.error('Error parsing rooms list:', error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Handle room created or joined
|
|
else if (message.type == "roomCreated" || message.type == "roomJoin") {
|
|
handleRoomChange(message);
|
|
return;
|
|
}
|
|
|
|
// Handle file messages
|
|
else if (message.type == "file") {
|
|
handleFileMessage(message);
|
|
return;
|
|
}
|
|
|
|
// Handle call messages
|
|
else if (message.type == "call") {
|
|
handleCallMessage(message);
|
|
return;
|
|
}
|
|
|
|
// Handle connect messages
|
|
else if (message.type == "connect") {
|
|
addSystemMessage(message.content);
|
|
|
|
if (document.hidden) {
|
|
new Notification("Chookchat", {body: message.content});
|
|
}
|
|
}
|
|
|
|
// Handle regular messages
|
|
else if (message.type == "message") {
|
|
if (message.room && message.room !== currentRoom) {
|
|
// Highlight room with unread message
|
|
highlightRoom(message.room);
|
|
return;
|
|
}
|
|
|
|
addChatMessage(message.username, message.content);
|
|
|
|
if (document.hidden) {
|
|
new Notification("Chookchat", {
|
|
body: `${message.username}: ${message.content}`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle typing indicator updates
|
|
function handleTypingIndicator(message) {
|
|
if (message.content == "1") {
|
|
if (username !== message.username && !typingPeople.includes(message.username)) {
|
|
typingPeople.push(message.username);
|
|
updatePeopleTyping();
|
|
}
|
|
} else if (message.content == "0") {
|
|
if (username !== message.username && typingPeople.includes(message.username)) {
|
|
const index = typingPeople.indexOf(message.username);
|
|
typingPeople.splice(index, 1);
|
|
updatePeopleTyping();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update online users display
|
|
function updateOnlineUsers(usersList) {
|
|
const usersPanel = document.querySelector('.users-list');
|
|
if (usersPanel) {
|
|
usersPanel.innerHTML = '';
|
|
|
|
const users = usersList.split(', ');
|
|
users.forEach(user => {
|
|
addUserToPanel(user);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Update room users display
|
|
function updateRoomUsers(message) {
|
|
const usersInRoom = message.content;
|
|
const roomName = message.room;
|
|
|
|
// Update current room if needed
|
|
if (roomName !== currentRoom) {
|
|
currentRoom = roomName;
|
|
updateCurrentRoomDisplay();
|
|
}
|
|
|
|
// Update users panel
|
|
const usersPanel = document.querySelector('.users-list');
|
|
if (usersPanel) {
|
|
usersPanel.innerHTML = '';
|
|
|
|
const users = usersInRoom.split(', ');
|
|
users.forEach(user => {
|
|
addUserToPanel(user);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add user to the users panel
|
|
function addUserToPanel(user) {
|
|
const usersPanel = document.querySelector('.users-list');
|
|
if (!usersPanel) return;
|
|
|
|
const userItem = document.createElement('div');
|
|
userItem.className = 'user-item';
|
|
|
|
// Create user avatar
|
|
const userAvatar = document.createElement('div');
|
|
userAvatar.className = 'user-avatar';
|
|
userAvatar.style.backgroundColor = getUserColor(user);
|
|
userAvatar.textContent = user.charAt(0).toUpperCase();
|
|
|
|
// Create user name
|
|
const userName = document.createElement('div');
|
|
userName.className = 'user-name';
|
|
userName.textContent = user;
|
|
|
|
userItem.appendChild(userAvatar);
|
|
userItem.appendChild(userName);
|
|
usersPanel.appendChild(userItem);
|
|
}
|
|
|
|
// Handle room change
|
|
function handleRoomChange(message) {
|
|
if (message.room) {
|
|
currentRoom = message.room;
|
|
updateCurrentRoomDisplay();
|
|
clearMessages();
|
|
loadRoomHistory(currentRoom);
|
|
updateRoomList();
|
|
}
|
|
|
|
// Display system message
|
|
addSystemMessage(message.content);
|
|
}
|
|
|
|
// Handle file messages
|
|
function handleFileMessage(message) {
|
|
// Only show file if it's for current room
|
|
if (message.room && message.room !== currentRoom) {
|
|
highlightRoom(message.room);
|
|
return;
|
|
}
|
|
|
|
const messagesDiv = document.getElementById('messagebox');
|
|
if (!messagesDiv) return;
|
|
|
|
const messageWrapper = document.createElement('div');
|
|
messageWrapper.className = 'message';
|
|
|
|
// Create avatar
|
|
const avatar = document.createElement('div');
|
|
avatar.className = 'message-avatar';
|
|
avatar.style.backgroundColor = getUserColor(message.username);
|
|
avatar.textContent = message.username.charAt(0).toUpperCase();
|
|
|
|
// Create message content
|
|
const contentDiv = document.createElement('div');
|
|
contentDiv.className = 'message-content';
|
|
|
|
// Add message header with username
|
|
const headerDiv = document.createElement('div');
|
|
headerDiv.className = 'message-header';
|
|
|
|
const authorSpan = document.createElement('span');
|
|
authorSpan.className = 'message-author';
|
|
authorSpan.textContent = message.username;
|
|
|
|
const timestampSpan = document.createElement('span');
|
|
timestampSpan.className = 'message-timestamp';
|
|
timestampSpan.textContent = new Date().toLocaleTimeString();
|
|
|
|
headerDiv.appendChild(authorSpan);
|
|
headerDiv.appendChild(timestampSpan);
|
|
contentDiv.appendChild(headerDiv);
|
|
|
|
let filename = message.content.replace("https://maxwellj.xyz/chookchat/uploads/", "");
|
|
|
|
if (isImage(filename)) {
|
|
// Image file
|
|
const imageMessage = document.createElement('div');
|
|
imageMessage.className = 'image-message';
|
|
|
|
const imagePreview = document.createElement('img');
|
|
imagePreview.src = message.content;
|
|
imagePreview.alt = filename;
|
|
imagePreview.addEventListener("click", function() {
|
|
window.open(message.content, "_blank");
|
|
});
|
|
|
|
imageMessage.appendChild(imagePreview);
|
|
contentDiv.appendChild(imageMessage);
|
|
} else {
|
|
// Other file type
|
|
const fileMessage = document.createElement('div');
|
|
fileMessage.className = 'file-message';
|
|
|
|
const fileIcon = document.createElement('i');
|
|
fileIcon.className = 'file-icon fa-solid fa-file';
|
|
|
|
const fileName = document.createElement('span');
|
|
fileName.className = 'file-name';
|
|
fileName.textContent = filename;
|
|
|
|
const fileButton = document.createElement('button');
|
|
fileButton.className = 'file-button';
|
|
fileButton.textContent = 'Open';
|
|
fileButton.addEventListener("click", function() {
|
|
window.open(message.content, "_blank");
|
|
});
|
|
|
|
fileMessage.appendChild(fileIcon);
|
|
fileMessage.appendChild(fileName);
|
|
fileMessage.appendChild(fileButton);
|
|
contentDiv.appendChild(fileMessage);
|
|
}
|
|
|
|
messageWrapper.appendChild(avatar);
|
|
messageWrapper.appendChild(contentDiv);
|
|
messagesDiv.appendChild(messageWrapper);
|
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
}
|
|
|
|
// Handle call messages
|
|
function handleCallMessage(message) {
|
|
if (message.room && message.room !== currentRoom) {
|
|
highlightRoom(message.room);
|
|
return;
|
|
}
|
|
|
|
const messagesDiv = document.getElementById('messagebox');
|
|
if (!messagesDiv) return;
|
|
|
|
const callNotification = document.createElement('div');
|
|
callNotification.className = 'call-notification';
|
|
callNotification.innerHTML = `<i class="fa-solid fa-phone"></i> ${message.username} started a call. Click to join!`;
|
|
callNotification.addEventListener('click', () => {
|
|
window.open(message.content, '_blank');
|
|
});
|
|
|
|
messagesDiv.appendChild(callNotification);
|
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
}
|
|
|
|
// Add a system message
|
|
function addSystemMessage(content, isHistory = false) {
|
|
const messagesDiv = document.getElementById('messagebox');
|
|
if (!messagesDiv) return;
|
|
|
|
const messageElement = document.createElement('div');
|
|
messageElement.className = 'system-message';
|
|
if (isHistory) messageElement.classList.add('history-message');
|
|
messageElement.textContent = content;
|
|
|
|
messagesDiv.appendChild(messageElement);
|
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
}
|
|
|
|
// Add a chat message
|
|
function addChatMessage(author, content, isHistory = false) {
|
|
const messagesDiv = document.getElementById('messagebox');
|
|
if (!messagesDiv) return;
|
|
|
|
const messageWrapper = document.createElement('div');
|
|
messageWrapper.className = 'message';
|
|
if (isHistory) messageWrapper.classList.add('history-message');
|
|
|
|
// Create avatar
|
|
const avatar = document.createElement('div');
|
|
avatar.className = 'message-avatar';
|
|
avatar.style.backgroundColor = getUserColor(author);
|
|
avatar.textContent = author.charAt(0).toUpperCase();
|
|
|
|
// Create message content
|
|
const contentDiv = document.createElement('div');
|
|
contentDiv.className = 'message-content';
|
|
|
|
// Add message header with username
|
|
const headerDiv = document.createElement('div');
|
|
headerDiv.className = 'message-header';
|
|
|
|
const authorSpan = document.createElement('span');
|
|
authorSpan.className = 'message-author';
|
|
authorSpan.textContent = author;
|
|
|
|
const timestampSpan = document.createElement('span');
|
|
timestampSpan.className = 'message-timestamp';
|
|
timestampSpan.textContent = isHistory ? 'History' : new Date().toLocaleTimeString();
|
|
|
|
headerDiv.appendChild(authorSpan);
|
|
headerDiv.appendChild(timestampSpan);
|
|
contentDiv.appendChild(headerDiv);
|
|
|
|
// Add message text
|
|
const textDiv = document.createElement('div');
|
|
textDiv.className = 'message-text';
|
|
textDiv.innerHTML = markdownToHtmlDiv(content);
|
|
contentDiv.appendChild(textDiv);
|
|
|
|
messageWrapper.appendChild(avatar);
|
|
messageWrapper.appendChild(contentDiv);
|
|
messagesDiv.appendChild(messageWrapper);
|
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
}
|
|
|
|
// Highlight a room with unread messages
|
|
function highlightRoom(roomName) {
|
|
const roomItems = document.querySelectorAll('.room-item');
|
|
roomItems.forEach(item => {
|
|
if (item.dataset.room === roomName && !item.classList.contains('active')) {
|
|
item.classList.add('unread');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Create a new room
|
|
async function createRoom(roomName) {
|
|
const message = {
|
|
type: 'createRoom',
|
|
username: username,
|
|
token: md5(password),
|
|
room: roomName,
|
|
content: ""
|
|
};
|
|
ws.send(JSON.stringify(message));
|
|
setTimeout(updateRoomList, 500);
|
|
}
|
|
|
|
// Join a room
|
|
async function joinRoom(roomName) {
|
|
if (roomName === currentRoom) {
|
|
const usersMessage = {
|
|
type: 'getUsersInRoom',
|
|
username: username,
|
|
token: md5(password),
|
|
room: roomName,
|
|
content: ""
|
|
};
|
|
ws.send(JSON.stringify(usersMessage));
|
|
return;
|
|
}
|
|
|
|
const message = {
|
|
type: 'joinRoom',
|
|
username: username,
|
|
token: md5(password),
|
|
room: roomName,
|
|
content: ""
|
|
};
|
|
ws.send(JSON.stringify(message));
|
|
}
|
|
|
|
// Send a message
|
|
function sendMessage() {
|
|
const messageInput = document.getElementById('messageInput');
|
|
const message = messageInput.value.trim();
|
|
|
|
if (!message) return;
|
|
|
|
const processedMessage = {
|
|
"type": "message",
|
|
"username": username,
|
|
"token": md5(password),
|
|
"room": currentRoom,
|
|
"content": message
|
|
}
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(processedMessage));
|
|
messageInput.value = '';
|
|
|
|
if (typingTimeout) {
|
|
clearTimeout(typingTimeout);
|
|
}
|
|
const stoppedTypingMessage = {
|
|
"type": "typing",
|
|
"username": username,
|
|
"token": md5(password),
|
|
"room": currentRoom,
|
|
"content": "0"
|
|
};
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(stoppedTypingMessage));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show file upload modal
|
|
function showFileUpload() {
|
|
const uploadModal = document.getElementById("upload-modal");
|
|
if (uploadModal) {
|
|
uploadModal.classList.remove("hidden");
|
|
}
|
|
}
|
|
|
|
// Hide file upload modal
|
|
function hideFileUpload() {
|
|
const uploadModal = document.getElementById("upload-modal");
|
|
if (uploadModal) {
|
|
uploadModal.classList.add("hidden");
|
|
}
|
|
|
|
// Clear file input
|
|
const fileInput = document.getElementById("fileupload");
|
|
if (fileInput) {
|
|
fileInput.value = "";
|
|
}
|
|
|
|
// Clear preview
|
|
const previewDiv = document.getElementById("upload-preview");
|
|
if (previewDiv) {
|
|
previewDiv.innerHTML = "";
|
|
}
|
|
}
|
|
|
|
// Preview file before upload
|
|
function previewFile() {
|
|
const fileInput = document.getElementById("fileupload");
|
|
const previewDiv = document.getElementById("upload-preview");
|
|
|
|
if (!fileInput || !previewDiv) return;
|
|
|
|
previewDiv.innerHTML = "";
|
|
|
|
if (!fileInput.files.length) return;
|
|
|
|
const file = fileInput.files[0];
|
|
const fileName = file.name;
|
|
|
|
if (file.type.startsWith('image/')) {
|
|
const img = document.createElement("img");
|
|
img.file = file;
|
|
previewDiv.appendChild(img);
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => { img.src = e.target.result; };
|
|
reader.readAsDataURL(file);
|
|
} else {
|
|
const fileInfo = document.createElement("div");
|
|
fileInfo.innerHTML = `<i class="fa-solid fa-file"></i> ${fileName} (${formatFileSize(file.size)})`;
|
|
previewDiv.appendChild(fileInfo);
|
|
}
|
|
}
|
|
|
|
// Format file size
|
|
function formatFileSize(bytes) {
|
|
if (bytes < 1024) return bytes + ' bytes';
|
|
else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
else return (bytes / 1048576).toFixed(1) + ' MB';
|
|
}
|
|
|
|
// Upload a file
|
|
async function uploadFile() {
|
|
const fileInput = document.getElementById("fileupload");
|
|
if (!fileInput.files.length) {
|
|
alert("Please select a file to upload");
|
|
return;
|
|
}
|
|
|
|
// Check file size
|
|
if (fileInput.files[0].size > 10485760) {
|
|
alert("File is too large. Maximum size is 10MB.");
|
|
return;
|
|
}
|
|
|
|
// Update button state
|
|
const uploadButton = document.querySelector('#upload-modal .btn-primary');
|
|
if (uploadButton) {
|
|
uploadButton.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Uploading...';
|
|
uploadButton.disabled = true;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append("file", fileInput.files[0]);
|
|
formData.append("room", currentRoom);
|
|
formData.append("username", username);
|
|
formData.append("token", md5(password));
|
|
|
|
try {
|
|
const response = await fetch(getUploadUrl(), {
|
|
method: 'POST',
|
|
mode: "no-cors",
|
|
body: formData
|
|
});
|
|
|
|
if (response.ok) {
|
|
hideFileUpload();
|
|
} else {
|
|
alert("Failed to upload file. Please try again.");
|
|
}
|
|
} catch (error) {
|
|
alert("Error uploading file: " + error.message);
|
|
}
|
|
|
|
// Reset button state
|
|
if (uploadButton) {
|
|
uploadButton.innerHTML = 'Upload';
|
|
uploadButton.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Start typing indicator
|
|
function startTypingIndicator() {
|
|
if (typingTimeout) {
|
|
clearTimeout(typingTimeout);
|
|
}
|
|
|
|
const typingMessage = {
|
|
"type": "typing",
|
|
"username": username,
|
|
"token": md5(password),
|
|
"room": currentRoom,
|
|
"content": "1"
|
|
};
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(typingMessage));
|
|
}
|
|
|
|
typingTimeout = setTimeout(() => {
|
|
const stoppedTypingMessage = {
|
|
"type": "typing",
|
|
"username": username,
|
|
"token": md5(password),
|
|
"room": currentRoom,
|
|
"content": "0"
|
|
};
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(stoppedTypingMessage));
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
// Update typing indicator display
|
|
function updatePeopleTyping() {
|
|
const typingDiv = document.getElementById('typing');
|
|
if (typingDiv) {
|
|
if (typingPeople.length === 0) {
|
|
typingDiv.textContent = '';
|
|
} else if (typingPeople.length === 1) {
|
|
typingDiv.innerHTML = `<i class="fa-solid fa-keyboard"></i> ${typingPeople[0]} is typing...`;
|
|
} else if (typingPeople.length === 2) {
|
|
typingDiv.innerHTML = `<i class="fa-solid fa-keyboard"></i> ${typingPeople[0]} and ${typingPeople[1]} are typing...`;
|
|
} else {
|
|
typingDiv.innerHTML = `<i class="fa-solid fa-keyboard"></i> ${typingPeople.length} people are typing...`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate random string for Jitsi meeting
|
|
function generateRandomString(length) {
|
|
const characters ='abcdefghijklmnopqrstuvwxyz';
|
|
let result = '';
|
|
const charactersLength = characters.length;
|
|
for (let i = 0; i < length; i++) {
|
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Start a Jitsi meeting
|
|
function startMeeting() {
|
|
const link = `https://meet.jit.si/chookchat-${generateRandomString(15)}`;
|
|
|
|
const processedMessage = {
|
|
"type": "call",
|
|
"username": username,
|
|
"token": md5(password),
|
|
"room": currentRoom,
|
|
"content": link
|
|
};
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(processedMessage));
|
|
window.open(link, '_blank');
|
|
} else {
|
|
alert("Something went wrong. Refresh the page and try again.");
|
|
}
|
|
}
|
|
|
|
// Update the room list UI
|
|
async function updateRoomList() {
|
|
const roomListDiv = document.getElementById('room-list');
|
|
if (!roomListDiv) return;
|
|
|
|
try {
|
|
availableRooms = await getRooms();
|
|
} catch (error) {
|
|
console.error('Error updating room list:', error);
|
|
}
|
|
|
|
// Clear existing rooms except category header
|
|
const roomCategory = roomListDiv.querySelector('.room-category');
|
|
if (roomCategory) {
|
|
const header = roomCategory.querySelector('.room-category-header');
|
|
roomCategory.innerHTML = '';
|
|
if (header) roomCategory.appendChild(header);
|
|
|
|
// Add room items
|
|
availableRooms.forEach(room => {
|
|
const roomElement = document.createElement('div');
|
|
roomElement.className = `room-item ${room === currentRoom ? 'active' : ''}`;
|
|
roomElement.dataset.room = room;
|
|
roomElement.innerHTML = `<i class="fa-solid fa-hashtag"></i> ${room}`;
|
|
roomElement.addEventListener('click', () => joinRoom(room));
|
|
roomCategory.appendChild(roomElement);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Update current room display
|
|
function updateCurrentRoomDisplay() {
|
|
const roomNameElement = document.getElementById('current-room-name');
|
|
if (roomNameElement) {
|
|
roomNameElement.textContent = currentRoom;
|
|
}
|
|
|
|
// Update active state in room list
|
|
const roomItems = document.querySelectorAll('.room-item');
|
|
roomItems.forEach(item => {
|
|
if (item.dataset.room === currentRoom) {
|
|
item.classList.add('active');
|
|
item.classList.remove('unread');
|
|
} else {
|
|
item.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Clear message display
|
|
function clearMessages() {
|
|
const messagebox = document.getElementById('messagebox');
|
|
if (messagebox) {
|
|
messagebox.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
// Load room message history
|
|
async function loadRoomHistory(roomName) {
|
|
try {
|
|
const history = await getRoomHistory(roomName);
|
|
if (history) {
|
|
const messagebox = document.getElementById('messagebox');
|
|
const lines = history.split('\n');
|
|
|
|
lines.forEach(line => {
|
|
if (line.trim()) {
|
|
// Parse history line to extract username and message
|
|
const historyMatch = line.match(/^(.*?):\s(.*)$/);
|
|
|
|
if (historyMatch && historyMatch.length >= 3) {
|
|
// If it's a regular message format (username: message)
|
|
const historyUsername = historyMatch[1].trim();
|
|
const historyContent = historyMatch[2].trim();
|
|
addChatMessage(historyUsername, historyContent, true);
|
|
} else if (line.includes("joined") || line.includes("left") || line.includes("created")) {
|
|
// System messages
|
|
addSystemMessage(line, true);
|
|
} else {
|
|
// Fallback for any other format
|
|
addSystemMessage(line, true);
|
|
}
|
|
}
|
|
});
|
|
|
|
messagebox.scrollTop = messagebox.scrollHeight;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading room history:', error);
|
|
}
|
|
}
|
|
|
|
// Prompt to create a new room
|
|
function promptCreateRoom() {
|
|
const roomName = prompt('Enter a name for the new room:');
|
|
if (roomName && roomName.trim()) {
|
|
createRoom(roomName.trim());
|
|
}
|
|
}
|
|
|
|
// Show eggs panel
|
|
function showEggs() {
|
|
const eggsPanel = document.getElementById('eggs-panel');
|
|
if (eggsPanel) {
|
|
eggsPanel.classList.remove('hidden');
|
|
eggsPanel.classList.add('visible');
|
|
}
|
|
}
|
|
|
|
// Close eggs panel
|
|
function closeEggs() {
|
|
const eggsPanel = document.getElementById('eggs-panel');
|
|
if (eggsPanel) {
|
|
eggsPanel.classList.remove('visible');
|
|
setTimeout(() => {
|
|
eggsPanel.classList.add('hidden');
|
|
}, 200);
|
|
}
|
|
}
|
|
|
|
// Get or generate consistent user color
|
|
function getUserColor(username) {
|
|
if (!avatarColors[username]) {
|
|
// Generate a color based on username
|
|
const hue = Math.abs(hashString(username) % 360);
|
|
avatarColors[username] = `hsl(${hue}, 70%, 60%)`;
|
|
}
|
|
return avatarColors[username];
|
|
}
|
|
|
|
// Simple string hash function
|
|
function hashString(str) {
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
hash |= 0; // Convert to 32bit integer
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
// Register a new account
|
|
async function doRegister(username, password) {
|
|
return fetch(`${getSignupUrl()}username:{${username}}token:{${md5(password)}}`)
|
|
.then(response => response.json())
|
|
.then(responseJson => responseJson);
|
|
}
|
|
|
|
// Register handler
|
|
async function register() {
|
|
username = document.getElementById('username').value;
|
|
password = document.getElementById('password').value;
|
|
|
|
if (!username || !password) {
|
|
alert('Please enter a username and password');
|
|
return;
|
|
}
|
|
|
|
// Update button state
|
|
const registerButton = document.querySelector('.btn-success');
|
|
if (registerButton) {
|
|
registerButton.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Registering...';
|
|
registerButton.disabled = true;
|
|
}
|
|
|
|
try {
|
|
const response = await doRegister(username, password);
|
|
if (response.type == "success") {
|
|
alert("Account created! Click 'Log in' to access Chookchat!");
|
|
} else {
|
|
alert(`We couldn't create your account: ${response.content}`);
|
|
}
|
|
} catch (error) {
|
|
alert("Error registering account: " + error.message);
|
|
}
|
|
|
|
// Reset button state
|
|
if (registerButton) {
|
|
registerButton.innerHTML = 'Register';
|
|
registerButton.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Notepad egg
|
|
function eggNotepad() {
|
|
const eggContent = document.getElementById('egg-content');
|
|
if (eggContent) {
|
|
eggContent.innerHTML = `
|
|
<div class="notepad-container">
|
|
<textarea class="notepad-textarea" placeholder="Type your notes here..."></textarea>
|
|
<div class="notepad-actions">
|
|
<button class="btn btn-primary" onclick="saveNotepad()">Save</button>
|
|
<button class="btn btn-danger" onclick="clearNotepad()">Clear</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Load saved content if available
|
|
const savedNote = localStorage.getItem('chookchat-notepad');
|
|
if (savedNote) {
|
|
eggContent.querySelector('.notepad-textarea').value = savedNote;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save notepad content
|
|
function saveNotepad() {
|
|
const textarea = document.querySelector('.notepad-textarea');
|
|
if (textarea) {
|
|
localStorage.setItem('chookchat-notepad', textarea.value);
|
|
alert('Notes saved!');
|
|
}
|
|
}
|
|
|
|
// Clear notepad content
|
|
function clearNotepad() {
|
|
const textarea = document.querySelector('.notepad-textarea');
|
|
if (textarea && confirm('Are you sure you want to clear your notes?')) {
|
|
textarea.value = '';
|
|
localStorage.removeItem('chookchat-notepad');
|
|
}
|
|
}
|
|
|
|
// File upload size validation
|
|
const uploadField = document.getElementById("fileupload");
|
|
if (uploadField) {
|
|
uploadField.onchange = function() {
|
|
if (this.files[0] && this.files[0].size > 10485760) {
|
|
alert("File is too large. Maximum size is 10MB.");
|
|
this.value = "";
|
|
}
|
|
};
|
|
}
|