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.network.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);
});
}