interface User { id: number, username: string, deleted: boolean, desc: string, statustext: string, status: number, displayname: string, } interface Message { id: number, sender: number, message: string, reply_to: number | undefined, date: number, deleted: boolean, } let initial = true; const messages = new Map(); const users = new Map(); let sent = false; const host = location.protocol + "//" + location.host; let socket: WebSocket; function xhttp_request(location: string): Promise { let f = fetch(`${host}/api/${location}`); return f.then((response) => response.json()); } function xhttp_send(method: string, location: string, json: string) { let f = fetch(`${host}/api/${location}`, { method: method, headers: {"Content-Type": "application/json"}, body: json }) return f.then((response) => response.json()); } async function format_message(m: Message): Promise { var sender = await get_user_name(m.sender); var sclass = ""; if (m.deleted) { m.message = "*deleted*" sclass = "deleted" } let s = `

${format_date(m.date)}

${sender}${m.message}

` return s } async function format_reply(m: Message): Promise { var sender = await get_user_name(m.sender); var sclass = ""; if (m.deleted) { m.message = "*deleted*" sclass = "deleted" } let s = `

${sender}${m.message}

` return s } function format_time(t: number) { let as_str = `${t}`; if (as_str.length < 2) { return `0${t}` } else { return `${t}` } } function format_date(d: number) { let date = new Date(d * 1000); let hours = format_time(date.getHours()); let minutes = format_time(date.getMinutes()); return `${hours}:${minutes}`; } function delete_message(id: number) { xhttp_send("DELETE", "delete_message", JSON.stringify(id)).then((response) => { if (response.Ok == undefined) { alert("Could not delete message") } else { let old_msg = messages.get(id); if (old_msg != undefined) { old_msg.message = "*deleted*"; old_msg.deleted = true; messages.set(id, old_msg); chatwindow(); } else { throw new Error("Could not find message") } } }) } function message_info(id: number): void { let msg = messages.get(id); if (msg != undefined) { let final = `id: ${msg.id}\nsender: ${msg.sender}\nmessage: ${msg.message}\ndate: ${msg.date}\nreply_to: ${msg.reply_to}\ndeleted: ${msg.deleted}`; alert(final) } } function send(id: undefined|number) { var input = getelementbyid("input"); let button = getelementbyid("inputbox").lastElementChild get_user_from_name().then((user_id) => { if (user_id == undefined) { alert("User not found") return undefined } let reply_to: number|null = null; if (id != undefined) { reply_to = id } let message = JSON.stringify({"sender": user_id, "message": input.value, "reply_to": reply_to}) xhttp_send("POST", "send_message", message).then((id) => { if (id.Ok != undefined) { get_message(id.Ok).then(() => { chatwindow(id.Ok) }); input.value = ""; sent = true button.innerHTML = "Send" button.onclick = function() { send(undefined) } } else { alert(id.Err) sent = true } }) }); } function reply(id: number) { var input = getelementbyid("input"); let button = getelementbyid("inputbox").lastElementChild let element = getelementbyid(id.toString()); // Set button to reply input.focus() button.innerHTML = "Reply"; element.classList.add("reply_highlight"); button.onclick = function() { send(id) } } function get_newest_cache_id() { let last = -1; for (let k of messages.keys()) { if (k > last) { last = k } } if (last == -1) { return undefined } else { return last } } function get_newest_messages(): void { xhttp_request("get_message_id_newest").then(async (newest_id) => { let new_messages: HTMLSpanElement = getelementbyid("new_messages") let newest_cache_id = get_newest_cache_id(); if (newest_id.Err) { return } else { newest_id = newest_id.Ok } let final_array: Message[] = []; if (newest_id == newest_cache_id) { return } if (newest_cache_id == undefined) { let id = newest_id; if (id - 25 < 0) { get_message(0) chatwindow() } else { get_message(newest_id - 25) chatwindow() } return get_newest_messages() } if (newest_id > newest_cache_id) { let delta = newest_id - newest_cache_id ; if (delta > 25) { let oldest = newest_id - 25; let newest = delta; let list = await get_message_list(oldest, newest); for (let x of list.values()) { final_array.push(x) } newest_id = oldest; if (oldest - 25 < 0) { newest_cache_id = 0 } else { newest_cache_id = oldest - 25 } } } let list = await get_message_list(newest_cache_id, newest_id) for (let x of list.values()) { final_array.push(x) } // Insert all values into the message map for (let x of final_array.values()) { messages.set(x.id, x) } if (initial) { chatwindow(newest_id) initial = false } else { chatwindow() new_messages.style.display = "unset" } }); } async function get_message_list(oldest, newest): Promise { let data = await xhttp_request(`get_message_list/${oldest}/${newest}`).then(); let req = data.Ok; return req } async function get_message(id: number): Promise { let cached = messages.get(id) if (cached != undefined) { return cached } // Get message return await xhttp_request("get_message/" + id).then((data) => { if (data.Ok != undefined) { let msg = data.Ok; messages.set(msg.id, msg) return get_message(id) } }) } async function get_user_from_name() { let v = getelementbyid("username").value; for (let x of users.values()) { if (x.username == v) { return x.id } } let user = await xhttp_send("POST", "get_user_by_name", JSON.stringify(v)); if (user.Ok) { users.set(user.id, user) return user.Ok.id } else { return await create_user(v) } } async function create_user(username) { let response = await xhttp_send("POST", "create_user", JSON.stringify(username)); if (response.Ok != undefined) { await get_user_name(response.Ok); return response.Ok } else { alert("Could not create user") return undefined } } async function get_user_name(id) { let cached = users.get(id) if (cached != undefined) { return cached.displayname } let data = await xhttp_request("get_user/" + id,); let user = data.Ok; users.set(user.id, user) return user.displayname } // Pretty hacky if I do say so myself function to_dom(x: string): Node { let dom = new DOMParser().parseFromString(x, "text/html"); if (dom != null) { if (dom.firstChild) { if (dom.firstChild.lastChild) { if (dom.firstChild.lastChild.firstChild) { return dom.firstChild.lastChild.firstChild } } } } throw new Error("Could not convert to dom") } async function chatwindow(hash: any = undefined, hl = false) { let window: HTMLDivElement = getelementbyid("chatwindow"); let pre_scroll = window.scrollHeight - window.clientHeight; let scrolltop = window.scrollTop; window.innerHTML = ""; let array: Message[] = []; for (let v of messages.values()) { array.push(v); } array.sort( function(a, b) { return a.id - b.id } ); let old_date = new Date(0); let latest_v; for (let v of array) { latest_v = v; let date = new Date(v.date * 1000); if (date.getDate() > old_date.getDate() || date.getFullYear() > old_date.getFullYear() || date.getMonth() > old_date.getMonth()) { old_date = date; window.append(to_dom(`${date.getDate()}/${date.getMonth()}/${date.getFullYear()}`)) } if (v.reply_to != undefined) { let r = await get_message(v.reply_to); if (r != undefined) { let reply = await format_reply(r); window.append(to_dom(reply)) } } let data = await format_message(v); let dom: Node = to_dom(data); window.appendChild(dom); } if (scrolltop == pre_scroll) { if (latest_v != undefined && hash == undefined) { getelementbyid(latest_v.id).scrollIntoView(); } } if (hash != undefined) { getelementbyid(hash.toString()).scrollIntoView(); if (hl) { highlight(hash); } } } function highlight(id: number) { let element: HTMLSpanElement = getelementbyid(id.toString()); element.classList.add("highlight"); } function onscrollchat() { let window: HTMLDivElement = getelementbyid("chatwindow"); let new_messages: HTMLSpanElement = getelementbyid("new_messages") let pre_scroll = window.scrollHeight - window.clientHeight; if (window.scrollTop == pre_scroll) { new_messages.style.display = ""; } } function inputkey(event: KeyboardEvent) { let window: HTMLTextAreaElement = getelementbyid("input"); let button = getelementbyid("inputbox").lastElementChild // If the cursor pos is at the end of the textarea let comp = window.selectionEnd - window.value.length let key = event.key let shift = event.shiftKey; // @ts-ignore if (key == "Enter" && !shift && comp == 0 && key != "Convert") { button.onclick() } // @ts-ignore if (key == "Escape" && !shift && key != "Convert") { button.innerHTML = "Send" button.onclick = function() { send(undefined) } chatwindow() } } function clearnewlineinput(): void { let window: HTMLTextAreaElement = getelementbyid("input"); if (window.value == "\n" && sent == true) { window.value = "" sent = false } } function getelementbyid(s: string): any { let x = document.getElementById(s); if (x == null) { throw new Error("Could not find element") } else { return x } } function togglerightpane(): void { let window = getelementbyid("rightpane"); if (window.style.display != "flex") { window.style.display = "flex" } else { window.style.display = "" } } function toggleleftpane(): void { let window = getelementbyid("leftpane"); if (window.style.display != "none") { window.style.display = "none" } else { window.style.display = "" } } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function ws_connect(t: number): Promise { await sleep(t * 1000); return new WebSocket('wss://' + location.host + "/api/ws") } function ws_setup(t: number = 0) { ws_connect(t).then((sock) => { socket = sock; socket.addEventListener('message', (event) => { // let newest = JSON.parse(event.data) get_newest_messages(); }); socket.addEventListener('close', (event) => { console.log('Connection closed'); if (t + 5 > 15) { alert("Server timed out. Refresh page to try again"); } else { console.log('Retrying in: ', t + 5); ws_setup(t + 5); } }); socket.addEventListener('open', (event) => { // get_newest_messages(); }); }) } function load(): void { xhttp_request("").then((server_info) => { getelementbyid("info").innerHTML += server_info.version + ", " + server_info.name + ", users: " + server_info.users; // Version 0.3.x implements websockets if (server_info.version[2] < 3 || server_info.version[0] > 1) { const interval = setInterval(get_newest_messages, 2000); } else { ws_setup(); } }) }