TriChat / static /main.js
parthmax24's picture
updated
241535f
class TriChat {
constructor() {
this.ws = null;
this.username = "";
this.room = "global";
this.isConnected = false;
this.users = [];
this.initElements();
this.bindEvents();
this.setupMessageInputResize();
this.updateFieldStates();
this.showWelcomeMessage();
}
initElements() {
this.elements = {
appShell: document.getElementById("appShell"),
connectScreen: document.getElementById("connectScreen"),
chatScreen: document.getElementById("chatScreen"),
connectionForm: document.getElementById("connectionForm"),
messageForm: document.getElementById("messageForm"),
usernameInput: document.getElementById("usernameInput"),
roomInput: document.getElementById("roomInput"),
connectBtn: document.getElementById("connectBtn"),
disconnectBtn: document.getElementById("disconnectBtn"),
mobileBackBtn: document.getElementById("mobileBackBtn"),
status: document.getElementById("status"),
roomTitle: document.getElementById("roomTitle"),
onlineCount: document.getElementById("onlineCount"),
messages: document.getElementById("messages"),
messageInput: document.getElementById("messageInput"),
sendBtn: document.getElementById("sendBtn"),
fileInput: document.getElementById("fileInput"),
userList: document.getElementById("userList"),
emptyUsers: document.getElementById("emptyUsers"),
usersToggle: document.getElementById("usersToggle"),
usersClose: document.getElementById("usersClose"),
drawerBackdrop: document.getElementById("drawerBackdrop")
};
}
bindEvents() {
this.elements.connectionForm.addEventListener("submit", (event) => {
event.preventDefault();
this.connect();
});
this.elements.messageForm.addEventListener("submit", (event) => {
event.preventDefault();
this.sendMessage();
});
this.elements.disconnectBtn.addEventListener("click", () => this.disconnect());
this.elements.mobileBackBtn.addEventListener("click", () => this.disconnect());
this.elements.fileInput.addEventListener("change", (event) => this.handleFileSelect(event));
this.elements.usersToggle.addEventListener("click", () => this.toggleUsersPanel());
this.elements.usersClose.addEventListener("click", () => this.closeUsersPanel());
this.elements.drawerBackdrop.addEventListener("click", () => this.closeUsersPanel());
this.elements.messageInput.addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.sendMessage();
}
});
this.elements.usernameInput.addEventListener("input", () => this.updateFieldStates());
this.elements.roomInput.addEventListener("input", () => this.updateFieldStates());
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") this.closeUsersPanel();
});
}
setupMessageInputResize() {
this.elements.messageInput.addEventListener("input", () => {
this.elements.messageInput.style.height = "auto";
this.elements.messageInput.style.height = `${Math.min(this.elements.messageInput.scrollHeight, 136)}px`;
});
}
updateFieldStates() {
const usernameShell = this.elements.usernameInput.closest(".field-shell");
const roomShell = this.elements.roomInput.closest(".field-shell");
usernameShell.classList.toggle("has-value", Boolean(this.elements.usernameInput.value.trim()));
roomShell.classList.toggle("has-value", Boolean(this.elements.roomInput.value.trim()));
}
async connect() {
const username = this.elements.usernameInput.value.trim();
const room = this.elements.roomInput.value.trim() || "global";
if (!username) {
this.updateStatus("Please enter your name", "disconnected");
this.elements.usernameInput.focus();
return;
}
this.username = username;
this.room = room;
this.elements.roomTitle.textContent = room;
try {
this.updateStatus("Connecting...", "neutral");
this.elements.connectBtn.disabled = true;
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/ws/${encodeURIComponent(room)}?username=${encodeURIComponent(username)}`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
this.isConnected = true;
this.elements.connectBtn.disabled = false;
this.updateStatus(`Connected to ${room}`, "connected");
this.updateUI();
this.elements.messageInput.focus();
};
this.ws.onmessage = (event) => {
let message;
try {
message = JSON.parse(event.data);
} catch (error) {
console.error("Invalid WebSocket message:", error);
return;
}
if (message.type === "user_list") {
this.updateUserList(message.users || []);
return;
}
this.displayMessage(message);
};
this.ws.onclose = () => {
this.isConnected = false;
this.elements.connectBtn.disabled = false;
this.updateStatus("Disconnected", "disconnected");
this.updateUI();
};
this.ws.onerror = (error) => {
console.error("WebSocket error:", error);
this.elements.connectBtn.disabled = false;
this.updateStatus("Connection error", "disconnected");
};
} catch (error) {
console.error("Connection failed:", error);
this.elements.connectBtn.disabled = false;
this.updateStatus("Failed to connect", "disconnected");
}
}
disconnect() {
if (this.ws) {
this.ws.close();
}
}
updateUI() {
document.body.classList.remove("users-open");
this.elements.usersToggle.setAttribute("aria-expanded", "false");
if (this.isConnected) {
this.elements.appShell.classList.add("is-chatting");
this.elements.connectScreen.classList.add("is-hidden");
this.elements.chatScreen.classList.remove("is-hidden");
this.elements.chatScreen.classList.add("is-connected-preview");
this.elements.usernameInput.disabled = true;
this.elements.roomInput.disabled = true;
} else {
this.elements.appShell.classList.remove("is-chatting");
this.elements.connectScreen.classList.remove("is-hidden");
this.elements.chatScreen.classList.add("is-hidden");
this.elements.chatScreen.classList.remove("is-connected-preview");
this.elements.usernameInput.disabled = false;
this.elements.roomInput.disabled = false;
this.elements.messages.innerHTML = "";
this.users = [];
this.updateUserList([]);
this.showWelcomeMessage();
}
}
updateStatus(text, type) {
const icon = type === "connected" ? "fa-check-circle" : type === "disconnected" ? "fa-times-circle" : "fa-info-circle";
this.elements.status.innerHTML = `<i class="fas ${icon}"></i><span>${this.escapeHtml(text)}</span>`;
this.elements.status.className = `status ${type}`;
}
updateUserList(users) {
this.users = users;
this.elements.userList.innerHTML = "";
users.forEach((user) => {
const li = document.createElement("li");
li.textContent = user;
this.elements.userList.appendChild(li);
});
const count = users.length;
this.elements.onlineCount.textContent = `${count} ${count === 1 ? "member" : "members"} online`;
this.elements.emptyUsers.classList.toggle("is-hidden", count > 0);
}
sendMessage() {
const text = this.elements.messageInput.value.trim();
if (!text || !this.isConnected) return;
this.ws.send(JSON.stringify({
type: "text",
username: this.username,
text,
timestamp: new Date().toISOString(),
room: this.room
}));
this.elements.messageInput.value = "";
this.elements.messageInput.style.height = "auto";
}
async handleFileSelect(event) {
const file = event.target.files[0];
if (!file || !this.isConnected) return;
if (file.size > 5 * 1024 * 1024) {
this.updateStatus("File size must be less than 5MB", "disconnected");
event.target.value = "";
return;
}
try {
const base64Data = await this.fileToBase64(file);
this.ws.send(JSON.stringify({
type: "file",
username: this.username,
fileName: file.name,
fileType: file.type,
fileSize: file.size,
fileData: base64Data,
timestamp: new Date().toISOString(),
room: this.room
}));
event.target.value = "";
} catch (error) {
console.error("File upload error:", error);
this.updateStatus("Failed to upload file", "disconnected");
}
}
fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result.split(",")[1]);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
displayMessage(message) {
if (message.type === "error") {
this.updateStatus(message.message || "Something went wrong", "disconnected");
return;
}
const messageElement = document.createElement("div");
messageElement.className = "message";
if (message.type === "system") {
messageElement.classList.add("system");
messageElement.innerHTML = `
<div class="message-content">
<i class="fas fa-info-circle"></i>
${this.escapeHtml(message.message)}
</div>
`;
} else {
if (message.username === this.username) {
messageElement.classList.add("own");
}
const timestamp = new Date(message.timestamp).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit"
});
if (message.type === "text") {
messageElement.innerHTML = `
<div class="message-header">
<span class="username">${this.escapeHtml(message.username)}</span>
<span class="timestamp">${timestamp}</span>
</div>
<div class="message-content">${this.escapeHtml(message.text)}</div>
`;
} else if (message.type === "file") {
const downloadUrl = message.fileUrl || "";
const safeDownloadUrl = this.escapeHtml(downloadUrl);
const safeFileName = this.escapeHtml(message.fileName);
const preview = downloadUrl && message.fileType.startsWith("image/")
? `<div class="file-preview"><img src="${safeDownloadUrl}" alt="${safeFileName}"></div>`
: "";
const fileIcon = this.getFileIcon(message.fileType);
messageElement.innerHTML = `
<div class="message-header">
<span class="username">${this.escapeHtml(message.username)}</span>
<span class="timestamp">${timestamp}</span>
</div>
<div class="message-content">
<div class="file-summary">
<i class="${fileIcon}"></i>
<strong>${safeFileName}</strong>
</div>
<div class="file-size">${this.formatFileSize(message.fileSize)}</div>
${preview}
<a href="${safeDownloadUrl}" download="${safeFileName}" class="file-download">
<i class="fas fa-download"></i>
Download
</a>
</div>
`;
}
}
this.elements.messages.appendChild(messageElement);
this.elements.messages.scrollTop = this.elements.messages.scrollHeight;
}
getFileIcon(fileType) {
if (fileType.startsWith("image/")) return "fas fa-image";
if (fileType.startsWith("video/")) return "fas fa-video";
if (fileType.startsWith("audio/")) return "fas fa-music";
if (fileType.includes("pdf")) return "fas fa-file-pdf";
if (fileType.includes("document") || fileType.includes("text")) return "fas fa-file-alt";
return "fas fa-file";
}
showWelcomeMessage() {
if (this.elements.messages.children.length > 0) return;
const welcomeMessage = document.createElement("div");
welcomeMessage.className = "message system";
welcomeMessage.innerHTML = `
<div class="message-content">
<i class="fas fa-rocket"></i>
Connect with your name to start chatting.
</div>
`;
this.elements.messages.appendChild(welcomeMessage);
}
toggleUsersPanel() {
const isOpen = document.body.classList.toggle("users-open");
this.elements.usersToggle.setAttribute("aria-expanded", String(isOpen));
}
closeUsersPanel() {
document.body.classList.remove("users-open");
this.elements.usersToggle.setAttribute("aria-expanded", "false");
}
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text || "";
return div.innerHTML;
}
formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
}
}
document.addEventListener("DOMContentLoaded", () => {
new TriChat();
});