let sendMessageClicked, userId, eventSource, gotFirstMsg; let messagesUsed = []; let curRoomName = window.location.search.split("?room=")[1] || "home"; let curRoom, editUsernameTimeout; let canSend = true; let username, nameColor, id, seenUsernameInfo, notifyKeywords; let db = localforage.createInstance({ name: "openchat" }); db.getItem('username').then(v => { username = v || ""; document.querySelector("#username-text").value = username; }).catch(console.err); db.getItem('id').then(v => { id = v || Date.now(); db.setItem('id', id).catch(console.err); sendMessageClicked(`/room ${curRoomName}`, false); }).catch(console.err); db.getItem('nameColor').then(v => nameColor = v || "green").catch(console.err); db.getItem('seenUsernameInfo').then(v => seenUsernameInfo = v || false).catch(console.err); db.getItem('notifyKeywords').then(v => notifyKeywords = v || { home: [] }).catch(console.err); let endpoint = 'https://nocache.nocache.saucyotteep.com/openchat/api'; //let endpoint = '/openchat/api'; sendMessageClicked = async (text, deleteMsgInput = true) => { let messageText = document.querySelector("#message-text").value.replace(/\s+$/, "") + " "; username = document.querySelector("#username-text").value.replace(/\s+$/, ""); db.setItem('username', username).catch(console.err); if (text) messageText = text; if (messageText.startsWith("/help ") || messageText.startsWith("/h ")) { addSystemMessage(` Commands:\n /help | /h - Shows this message\n /clear | /c - Clears the chat (locally)\n /room {room name} | /r {room name} - Connects to a different room (Enter 'home' to connect to the home room)\n /roomlist | /rl - Shows a list of all rooms\n /color {hex code or color name} - Sets your name color\n /colorpicker | /cp - Opens a color picker\n /sendfile | /sf - Upload a file to the chat (Limit is 32MB)\n /notify (comma-seperated list of keywords) - Subscribe to notifications for a list of keywords in the current room (leave blank to notify for every message) `); if (deleteMsgInput) document.querySelector("#message-text").value = ""; return; } if (messageText.startsWith("/clear ") || messageText.startsWith("/c ")) { var elem = document.getElementById("messages"); let last10Messages = Array.from(elem.childNodes).splice(-10, 10); elem.innerHTML = ''; last10Messages.forEach(e => elem.appendChild(e)); elem.childNodes.forEach(c => c.innerHTML = ''); if (deleteMsgInput) document.querySelector("#message-text").value = ""; return; } if (messageText.startsWith("/room ") || messageText.startsWith("/r ")) { addSystemMessage(`Connecting to room...`); let res = await fetch(`${endpoint}/v1/getRoom?` + new URLSearchParams({ id: messageText.split(" ")[1], userId: id }), { method: "GET", headers: { "Content-Type": "application/json", } }); console.log(res); let resJ = await res.json(); if (!resJ.success) { console.log(resJ); addSystemMessage(`Error: ${typeof (resJ?.err) == 'object' ? JSON.stringify(resJ.err) : resJ?.err?.message || resJ?.err}`); } if (!resJ.roomExists) { addSystemMessage(`Room does not exist, creating room...`); try { let res2 = await fetch(`${endpoint}/v1/createRoom?` + new URLSearchParams({ id: messageText.split(" ")[1] }), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ id, author: username }) }); let resJ2 = await res2.json(); console.log(resJ2); if (!resJ2.success) { console.log(resJ2); addSystemMessage(`Error: ${typeof (resJ2?.err) == 'object' ? JSON.stringify(resJ2.err) : resJ2?.err?.message || resJ2?.err}`); } if (resJ2.roomId) { if (eventSource) eventSource.close(); curRoom = resJ2.roomId; curRoomName = messageText.split(" ")[1]; listen(); addSystemMessage(`Connected to #${messageText.split(" ")[1]}`); window.history.pushState(null, document.title, `/openchat?room=${messageText.split(" ")[1]}`); if (deleteMsgInput) document.querySelector("#message-text").value = ""; } } catch (err) { addSystemMessage(`Error: ${err?.message || err}`); } return; } if (resJ.roomId) { if (eventSource) eventSource.close(); curRoom = resJ.roomId; curRoomName = messageText.split(" ")[1]; listen(); addSystemMessage(`Connected to #${messageText.split(" ")[1]}`); window.history.pushState(null, document.title, `/openchat?room=${messageText.split(" ")[1]}`); if (deleteMsgInput) document.querySelector("#message-text").value = ""; } return; } if (messageText.startsWith("/roomlist ") || messageText.startsWith("/rl ")) { let res = await fetch(`${endpoint}/v1/rooms.json`, { method: "GET", headers: { "Content-Type": "application/json", }, }); console.log(res); let resJ = await res.json(); let roomsString = "avaliable rooms:\n"; for (let i in resJ) { roomsString += `${i}, `; } addSystemMessage(roomsString); if (deleteMsgInput) document.querySelector("#message-text").value = ""; return; } if (messageText.startsWith("/color ")) { nameColor = messageText.split(" ")[1]; await db.setItem("nameColor", nameColor); addSystemMessage(`Name color set to ${nameColor}`); if (deleteMsgInput) document.querySelector("#message-text").value = ""; return; } if (messageText.startsWith("/colorpicker ") || messageText.startsWith("/cp ")) { let clrEl = await document.createElement("input"); let elem = document.querySelector("#messages"); clrEl.type = "color"; clrEl.value = nameColor; clrEl.style.display = "none"; clrEl.onchange = async () => { nameColor = clrEl.value; await db.setItem("nameColor", nameColor); addSystemMessage(`Name color set to ${nameColor}`); clrEl.remove(); }; clrEl.click(); elem.appendChild(clrEl); if (deleteMsgInput) document.querySelector("#message-text").value = ""; return; } if (messageText.startsWith("/sendfile ") || messageText.startsWith("/sf ")) { let flEl = await document.createElement("input"); let elem = document.querySelector("#messages"); let progressEl = await document.createElement("span"); progressEl.style.color = "gray"; progressEl.innerHTML = "Waiting for file..."; elem.appendChild(progressEl); flEl.type = "file"; flEl.style.display = "none"; flEl.multiple = true; flEl.onchange = async () => { for (index in flEl.files) { let file = flEl.files[index]; if (typeof (file) == "object") { let promise = new Promise(async (resolve, reject) => { function a() { if (file.size > 3.2e+7) { flEl.remove(); progressEl.remove(); addSystemMessage(`File ${file.name} is too big (${Math.round(file.size / 10000) / 100} MB > 32 MB)`); resolve(); return; } const xhr = new XMLHttpRequest(); xhr.upload.addEventListener("progress", async (e) => { progressEl.innerHTML = `uploading, ${Math.round((e.loaded / e.total) * 100)}% done`; if (e.loaded / e.total == 1) { await pause(1000); progressEl.innerHTML = `done uploading file`; await pause(3000); progressEl.innerHTML = `waiting for server...`; } }); xhr.addEventListener("load", async () => { console.log("file finished"); console.log(xhr.responseText); if (xhr.status == 429) { await pause(xhr.getResponseHeader('Retry-After') * 1000); a(); return; } progressEl.innerHTML = `file done uploading!`; flEl.remove(); await pause(2000); progressEl.remove(); resolve(); }); xhr.addEventListener("error", async () => { console.error("File upload failed"); await pause(2000); a(); }); xhr.addEventListener("abort", () => console.error("File upload aborted")); xhr.open("POST", `${endpoint}/v1/sendFile/${curRoom}/`, true); xhr.setRequestHeader("fileName", file.name.replaceAll(/[^a-zA-Z0-9_\- .]/g, '')); xhr.setRequestHeader("fileSize", file.size); xhr.setRequestHeader("fileType", file.type); xhr.setRequestHeader("author", username); xhr.setRequestHeader("color", nameColor); xhr.send(file); } a(); }); await Promise.all([promise]); } } }; elem.appendChild(flEl); flEl.click(); if (deleteMsgInput) document.querySelector("#message-text").value = ""; return; } if (messageText.startsWith("/notify ")) { let keywords = messageText.split(' ').splice(1, messageText.split(" ").length - 2).join(' ') || ''; keywordsArr = keywords.split(', '); if (keywordsArr.length == 1) keywordsArr = keywords.split(','); if (keywordsArr.length == 1 && keywordsArr[0] == '') keywordsArr = []; notifyKeywords[curRoomName] = keywordsArr; await db.setItem("notifyKeywords", notifyKeywords); function start() { if ('serviceWorker' in navigator) { console.log('Registering service worker'); Notification.requestPermission(function (result) { if (result === 'granted') { runNotifSw().catch(error => { console.error(error); addSystemMessage(error); }); addSystemMessage(`Subscribed to notifications in #${curRoomName}`); } else if (result == 'default') { setTimeout(start, 5000); } else { addSystemMessage('Notification permission denied :('); } }); } else { addSystemMessage('Service workers not supported'); } } start(); return; } if (messageText.startsWith("/")) { addSystemMessage(`Command "${messageText.split(" ")[0]}" does not exist`); //document.querySelector("#message-text").value = ""; return; } if (username == "" || messageText == "") { return; } const message = { author: username, content: messageText, color: nameColor, id, }; if (!canSend) return console.log(message); if (deleteMsgInput) document.querySelector("#message-text").value = ""; try { let res = await fetch(`${endpoint}/v1/sendMessage/${curRoom}`, { method: "POST", body: JSON.stringify(message), headers: { "Content-Type": "application/json", }, }); if (!seenUsernameInfo) { addSystemMessage('Note: You can only change your username every 10 minutes'); seenUsernameInfo = true; await db.setItem("seenUsernameInfo", seenUsernameInfo); } //document.querySelector("#username-text").setAttribute('readonly', true); //document.querySelector("#username-text").setAttribute('disabled', true); //document.querySelector("#username-text").ariaReadOnly = true; if (!editUsernameTimeout) { editUsernameTimeout = setTimeout(() => { //document.querySelector("#username-text").setAttribute('readonly', false); //document.querySelector("#username-text").setAttribute('disabled', false); //document.querySelector("#username-text").ariaReadOnly = false; //addSystemMessage('You can edit your username now'); editUsernameTimeout = undefined; }, 600000); } console.log(res); let body; try { body = await res.json(); } catch (err) { console.log(err); let body2; try { let bodyText = await res.text(); body = { err: bodyText }; } catch (err2) { console.log(err2); body = { err: err2?.message || err2 || err?.message || err }; } } if (res.status != 200) { addSystemMessage(`Error: ${body.err}`); } } catch (err) { addSystemMessage(`Error: ${err.message || err}`); } }; //moved to after db initalized //sendMessageClicked(`/room ${curRoomName}`); document.querySelector("#message-text").addEventListener("keydown", function (event) { if (event.key == "Enter" && !event.shiftKey && !event.ctrlKey) { event.preventDefault(); sendMessageClicked(); auto_height(document.querySelector("#message-text")); } }); document.querySelector("#username-text").addEventListener("keydown", function (event) { if (event.key == "Enter" && !event.shiftKey && !event.ctrlKey) { event.preventDefault(); sendMessageClicked(); auto_height(document.querySelector("#username-text")); } }); async function listen() { eventSource = new EventSource(`${endpoint}/v1/listen/${curRoom}`); eventSource.onmessage = function (event) { /*if (!gotFirstMsg) { setTimeout(() => { addSystemMessage("New features!\nTry using /help"); }, 1000); }*/ gotFirstMsg = true; resJson = JSON.parse(event.data); console.log(resJson); handleMsgs(resJson); }; eventSource.onerror = async function (err) { console.log(err); addSystemMessage('Connection error, reconnecting in 5 sec...'); await pause(5000); sendMessageClicked(`/room ${curRoomName}`, false); } } function addSystemMessage(message) { let ms = []; ms.push({ id: "empty" }); ms.push({ id: "internal", content: message, }); ms.push({ id: "empty" }); handleMsgs({ messages: ms }); } async function handleMsgs(resJson) { if (!resJson.messages) return; if (resJson.messages.length > 50) { let msgEl = await document.createElement("span"); msgEl.innerHTML = `Loading ${resJson.messages.length} messages... (May take a while)`; document.getElementById("messages").appendChild(msgEl); await pause(2000); setTimeout(() => document.getElementById("messages").removeChild(msgEl), 30000); } let prevMsg; let prevDate = ''; for (i in resJson.messages) { //await pause(10); let msg = resJson.messages[i]; var elem = await document.getElementById("messages"); var chatbox = await document.getElementById("chatBox"); let idUsed = false; elem.childNodes.forEach((m) => { if (m.msgId == msg.id) { idUsed = true; } }); let dateStr = new Date(msg.time).toLocaleString(); if (dateStr.split(', ')[0] == prevDate) dateStr = dateStr.split(', ')[1] || dateStr; else prevDate = dateStr.split(', ')[0]; if (!idUsed || msg.id == "internal" || msg.id == "empty") { let scrollAfterDone = false; if (elem.scrollHeight - elem.scrollTop <= elem.clientHeight + 1000) { scrollAfterDone = true; } let msgEl = await document.createElement("span"); let msgElTime = await document.createElement("span"); let msgElAuthor = await document.createElement("span"); let msgElMsg = await document.createElement("span"); let replaceId; if (msg.relation && msg.relation.rel_type == "m.replace") { replaceId = msg.relation.event_id; function finder(nodes, condition) { let match; nodes.forEach(child => { if (condition(child)) match = child; }); return match; }; msgEl = finder(elem.childNodes, m => m.msgId == replaceId); msgElTime = finder(msgEl.childNodes, c => c.type == "time"); msgElAuthor = finder(msgEl.childNodes, c => c.type == "author"); msgElMsg = finder(msgEl.childNodes, c => c.type == "message"); } msgElTime.innerHTML = `[${dateStr.replace(' ', ' ')}] `; msgElTime.style.color = "blue"; msgElTime.type = "time"; msgElAuthor.innerHTML = msg.authorId; msgElAuthor.style.color = msg.authorColor || "green"; msgElAuthor.style["word-break"] = "break-word"; msgElAuthor.type = "author"; msgElMsg.innerHTML = `: ${msg.content}`; msgElMsg.style.color = "black"; msgElMsg.style["word-break"] = "break-word"; msgElMsg.type = "message"; //msgEl.style["text-align"] = "center"; msgEl.msgId = replaceId || msg.id; msgEl.ip = msg.ip; for (let m of document.querySelector("#messages").children) { let mAuthor = m.children[1]; if (m.ip == msgEl.ip && m.ip && mAuthor) { mAuthor.innerHTML = msg.authorId; mAuthor.style.color = msg.authorColor || "green"; } } if (prevMsg && msg.time - prevMsg.time >= 3600000 * 1.5) { document.querySelector("#messages").appendChild(document.createElement('hr')); dateStr = new Date(msg.time).toLocaleString(); msgElTime.innerHTML = `[${dateStr.replace(' ', ' ')}] `; } prevMsg = msg; if (msg.id == "internal") { msgElMsg.innerHTML = msg.content; msgElMsg.style.color = "green"; msgEl.appendChild(msgElMsg); document.querySelector("#messages").appendChild(msgEl); msgEl.appendChild(document.createElement('br')); } else if (msg.id == "empty") { msgElMsg.innerHTML = "---"; msgElMsg.style.color = "white"; msgEl.appendChild(msgElMsg); document.querySelector("#messages").appendChild(msgEl); msgEl.appendChild(document.createElement('br')); } else if (msg.announcement) { msgElMsg.innerHTML = `Announcement: ${msg.content}`; msgElMsg.style.color = "orange"; msgEl.appendChild(msgElTime); msgEl.appendChild(msgElMsg); document.querySelector("#messages").appendChild(msgEl); msgEl.appendChild(document.createElement('br')); } else if (replaceId) { } else { msgEl.appendChild(msgElTime); msgEl.appendChild(msgElAuthor); msgEl.appendChild(msgElMsg); document.querySelector("#messages").appendChild(msgEl); msgEl.appendChild(document.createElement('br')); } msgElMsg.innerHTML = msgElMsg.innerHTML.replaceAll('\n', '
'); if (scrollAfterDone) { elem.scrollTop = elem.scrollHeight; } } } return; } function pause(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); }