From 0532d71133f15bf38b8a809694968ba99e249b31 Mon Sep 17 00:00:00 2001 From: curly Date: Thu, 16 Feb 2023 11:48:55 -0700 Subject: typescript ftw, styling --- .gitignore | 1 + Makefile | 12 ++ TODO | 1 + index.html | 21 --- main.css | 93 -------------- main.js | 345 ------------------------------------------------- src/index.html | 21 +++ src/main.css | 116 +++++++++++++++++ src/main.ts | 399 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 17 +++ 10 files changed, 567 insertions(+), 459 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 TODO delete mode 100644 index.html delete mode 100644 main.css delete mode 100644 main.js create mode 100644 src/index.html create mode 100644 src/main.css create mode 100644 src/main.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c795b05 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a93bc93 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: build + +build: copy + mkdir -p build + tsc --outDir build/ + +copy: + cp src/index.html build/ + cp src/main.css build/ + +clean: + rm -r build \ No newline at end of file diff --git a/TODO b/TODO new file mode 100644 index 0000000..5551f7d --- /dev/null +++ b/TODO @@ -0,0 +1 @@ +Reply to messages diff --git a/index.html b/index.html deleted file mode 100644 index a3fb0ab..0000000 --- a/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - Localhost - Server Information: - username: - - New Messages -
- - - - - - diff --git a/main.css b/main.css deleted file mode 100644 index 96a2d60..0000000 --- a/main.css +++ /dev/null @@ -1,93 +0,0 @@ -.UI { - background-color: blue; -} -#chatwindow { - display: flex; - flex-flow: column; - overflow-y: scroll; - word-break: break-word; - scroll-snap-align: end; - flex-basis: 100vh; - flex-grow: 1; -} -.message:hover { - background-color: #DDD; -} -.message { - display: flex; - height: 1.6em; - padding-left: 2px; - padding-right: 2px; - align-items: center; -} -.message:hover > :last-child { - display: flex; -} -.messagetext { - white-space: pre-wrap; - flex-grow: 1; -} -.messagedate { - font-size: 12px; -} -.message:not(.deleted) > .messagedate { - color: #666; -} - -.message_buttons { - display: none; -} -.message_buttons > button { - height: 1.6em; - width: 1.6em; - padding: 2px; - margin: 2px; -} - -#navbar { - border-bottom: 2px solid black; - width: 100%; - display: inline-block; - flex-grow: 0; -} -#inputbox { - border-top: 2px solid black; - display: flex; -} -#inputbox > button { - padding: 0 5px 0 5px; -} -#input { - resize: none; - flex-grow: 1; - font-size: 16px; -} - -* { - margin: 0; - padding: 0; - border: 0; -} - -body { - display: flex; - flex-direction: column; - max-height: 100vh; -} - -.date { - text-align: center; - font-weight: bold; - border-bottom: 1px solid black; -} - -.deleted { - color: #999; -} - -.new_messages { - display: none; - text-align: center; - background-color: red; - color: white; -} \ No newline at end of file diff --git a/main.js b/main.js deleted file mode 100644 index c51bd0c..0000000 --- a/main.js +++ /dev/null @@ -1,345 +0,0 @@ -const messages = new Map(); -const users = new Map(); -let sent = false; -const host = location.protocol + "//" + location.host; - - -function xhttp_request(location) { - let f = fetch(`${host}/api/${location}`); - return f.then((response) => response.json()); - -} - -function xhttp_send(method, location, json) { - 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) { - var s; - var sender = await get_user_name(m.sender); - var sclass = ""; - - if (m.deleted) { - m.message = "*deleted*" - sclass = "deleted" - } - s = `

${format_date(m.date)}

${sender}: ${m.message}

- - - - -
` - - return s -} - -function format_time(t) { - let as_str = `${t}`; - if (as_str.length < 2) { - return `0${t}` - } else { - return `${t}` - } -} - -function format_date(d) { - let date = new Date(d * 1000); - let hours = format_time(date.getHours()); - let minutes = format_time(date.getMinutes()); - let seconds = format_time(date.getSeconds()); - return `${hours}:${minutes}:${seconds}`; -} - -function delete_message(id) { - 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); - old_msg.message = "*deleted*"; - old_msg.deleted = true; - messages.set(id, old_msg); - - chatwindow(null).then(); - } - }) -} - -function send() { - var input = document.getElementById("input").value; - let user_id = get_user_from_name().then((user_id) => { - if (user_id == undefined) { - alert("User not found") - return undefined - } - - let message = JSON.stringify({"sender": user_id, "message": input}) - - var id = xhttp_send("POST", "send_message", message).then((id) => { - if (id.Ok != undefined) { - get_message(id.Ok); - - document.getElementById("input").value = "" - sent = true - } else { - alert(id.Err) - sent = true - } - }) - }); -} - -function get_newest_cache_id() { - let last = -1; - for (k of messages.keys()) { - if (k > last) { - last = k - } - } - - if (last == -1) { - return undefined - } else { - return last - } -} - -function get_newest_messages() { - xhttp_request("get_message_id_newest").then(async (newest_id) => { - let newest_cache_id = get_newest_cache_id(); - - if (newest_id.Err) { - return - } else { - newest_id = newest_id.Ok - } - - - let final_array = []; - - if (newest_id == newest_cache_id) { - return - } - - if (newest_cache_id == undefined) { - let id = newest_id; - if (id - 25 < 0) { - get_message(0) - } else { - get_message(newest_id - 25) - } - 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 = get_message_list(oldest, newest); - for (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 (x of list.values()) { - final_array.push(x) - } - - // Insert all values into the message map - for (x of final_array.values()) { - messages.set(x.id, x) - } - - chatwindow().then() - }); -} - -async function get_message_list(oldest, newest) { - let data = await xhttp_request(`get_message_list/${oldest}/${newest}`).then(); - let req = data.Ok; - return req -} - -function get_message(id) { - let cached = messages.get(id) - if (cached != undefined) { - return to_dom(format_message(cached)) - } - - // Get message - xhttp_request("get_message/" + id).then((data) => { - if (data.Ok != undefined) { - let msg = data.Ok; - messages.set(msg.id, msg) - - chatwindow(msg.id) - } - }) -} - -async function get_user_from_name() { - let v = document.getElementById("username").value; - - for (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.username - } - - - let data = await xhttp_request("get_user/" + id,); - let user = data.Ok; - - users.set(user.id, user) - - return user.username -} - -function to_dom(x) { - // Pretty hacky if I do say so myself - let dom = new DOMParser().parseFromString(x, "text/html").firstChild.lastChild.firstChild; - return dom -} - -async function chatwindow(hash) { - let window = document.getElementById("chatwindow"); - let pre_scroll = window.scrollHeight - window.clientHeight; - window.innerHTML = ""; - - let array = []; - - for (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 (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() > date.getMonth()) { - old_date = date; - window.append(to_dom(`${date.getDate()}/${date.getMonth()}/${date.getFullYear()}`)) - } - - let data = await format_message(v).then() - let dom = to_dom(data); - - window.appendChild(dom); - - } - - if (hash == null) { - return - } - - if (window.scrollTop == pre_scroll) { - if (latest_v != undefined && hash == undefined) { - document.getElementById(v.id).scrollIntoView(); - } - } else { - document.getElementById("new_messages").style.display = "unset" - } - - if (hash != undefined) { - document.getElementById(hash).scrollIntoView(); - } -} - -function onscrollchat() { - let window = document.getElementById("chatwindow"); - let pre_scroll = window.scrollHeight - window.clientHeight; - - if (window.scrollTop == pre_scroll) { - document.getElementById("new_messages").style.display = ""; - } -} - -function inputkey(event) { - let window = document.getElementById("input"); - - // 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; - if (key == "Enter" && !shift && comp == 0) { - send() - } -} - -function clearnewlineinput() { - let window = document.getElementById("input"); - - if (window.value == "\n" && sent == true) { - window.value = "" - sent = false - } -} - -function onload() { - xhttp_request("").then((server_info) => { - - document.getElementById("info").innerHTML += server_info.version + ", " + server_info.name + ", users: " + server_info.users; - - // Get ready for websockets implementation in rocket_test 0.3 - if (server_info.version[2] < 3 ) { - const interval = setInterval(get_newest_messages, 1000); - } else { - // Initiate websocket "polling" - } - }) -} diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..38bbbc8 --- /dev/null +++ b/src/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + Localhost + Server Information: + username: + + New Messages +
+ + + + + + diff --git a/src/main.css b/src/main.css new file mode 100644 index 0000000..6c6218d --- /dev/null +++ b/src/main.css @@ -0,0 +1,116 @@ +.UI { + background-color: blue; +} +#chatwindow { + display: flex; + flex-flow: column; + overflow-y: scroll; + overflow-x: clip; + word-break: break-word; + scroll-snap-align: end; + flex-basis: 100vh; + flex-grow: 1; +} +.message:hover { + background-color: #DDD; +} +.message { + display: flex; + flex-basis: 1.6em; + padding-left: 2px; + padding-right: 2px; + flex-shrink: 0; +} +.message:hover > :last-child > :first-child { + display: flex; +} +.messagetext { + white-space: pre-wrap; + flex-grow: 1; +} +.messagedate { + font-size: 12px; + align-self: start; + margin-top: 4px; + word-break: keep-all; + +} +.message:not(.deleted) > .messagedate { + color: #666; +} + +.message_buttons_anchor { + position: relative; +} +.message_buttons { + display: none; + position: absolute; + /* calc (((button height + anchor padding) / 2) * -1) */ + top: calc(((1.6em + 4px) / 2) * -1); + /* calc ((buttons amount(button size + button padding) + anchor padding) * -1) */ + left: calc((3(1.6em + 4px) + 4px) * -1); + right: 0; + bottom: 0; + + margin-right: 8px; + padding: 2px; + height: min-content; + background-color: white; + border: 1px solid black; + border-radius: 2px; +} +.message_buttons > button { + height: 1.6em; + width: 1.6em; + padding: 2px; + margin: 2px; +} + +#navbar { + border-bottom: 2px solid black; + width: 100%; + display: inline-block; + flex-grow: 0; +} +#inputbox { + border-top: 2px solid black; + display: flex; +} +#inputbox > button { + /* padding: 0 10px 0 10px; */ + width: 4em; +} +#input { + resize: none; + flex-grow: 1; + font-size: 16px; +} + +* { + margin: 0; + padding: 0; + border: 0; +} + +body { + display: flex; + flex-direction: column; + max-height: 100vh; +} + +.date { + text-align: center; + font-weight: bold; + border-bottom: 1px solid black; +} + +.deleted { + color: #999; +} + +.new_messages { + display: none; + text-align: center; + background-color: red; + color: white; +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..cec62d8 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,399 @@ +interface User { + id: number, + username: string, + deleted: boolean, +} +interface Message { + id: number, + sender: number, + message: string, + reply_to: number, + date: number, + deleted: boolean, +} + + +const messages = new Map(); +const users = new Map(); +let sent = false; +const host = location.protocol + "//" + location.host; + + +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 +} + +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()); + let seconds = format_time(date.getSeconds()); + return `${hours}:${minutes}:${seconds}`; +} + +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(null).then(); + } 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}\ndelted: ${msg.deleted}`; + alert(final) + } +} + +function send() { + var input = getelementbyid("input"); + + get_user_from_name().then((user_id) => { + if (user_id == undefined) { + alert("User not found") + return undefined + } + + let message = JSON.stringify({"sender": user_id, "message": input.value}) + + xhttp_send("POST", "send_message", message).then((id) => { + if (id.Ok != undefined) { + get_message(id.Ok); + + input.value = ""; + sent = true + } else { + alert(id.Err) + sent = true + } + }) + }); +} + +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() { + xhttp_request("get_message_id_newest").then(async (newest_id) => { + 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) + } else { + get_message(newest_id - 25) + } + 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) + } + + chatwindow().then() + }); +} + +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 +} + +function get_message(id: number): void { + let cached = messages.get(id) + if (cached != undefined) { + chatwindow(id) + } + + // Get message + xhttp_request("get_message/" + id).then((data) => { + if (data.Ok != undefined) { + let msg = data.Ok; + messages.set(msg.id, msg) + + chatwindow(msg.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.username + } + + + let data = await xhttp_request("get_user/" + id,); + let user = data.Ok; + + users.set(user.id, user) + + return user.username +} + +// 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) { + let window: HTMLDivElement = getelementbyid("chatwindow"); + let new_messages: HTMLSpanElement = getelementbyid("new_messages") + let pre_scroll = window.scrollHeight - window.clientHeight; + 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() > date.getMonth()) { + old_date = date; + window.append(to_dom(`${date.getDate()}/${date.getMonth()}/${date.getFullYear()}`)) + } + + let data = await format_message(v).then() + let dom: Node = to_dom(data); + + window.appendChild(dom); + } + + if (hash == null) { + return + } + + if (window.scrollTop == pre_scroll) { + if (latest_v != undefined && hash == undefined) { + getelementbyid(latest_v.id).scrollIntoView(); + } + } else { + new_messages.style.display = "unset" + } + + if (hash != undefined) { + getelementbyid(hash.toString()).scrollIntoView(); + } +} + +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"); + + // 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; + if (key == "Enter" && !shift && comp == 0) { + send() + } +} + +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 load(): void { + xhttp_request("").then((server_info) => { + + getelementbyid("info").innerHTML += server_info.version + ", " + server_info.name + ", users: " + server_info.users; + + // Get ready for websockets implementation in rocket_test 0.3 + if (server_info.version[2] < 3 ) { + const interval = setInterval(get_newest_messages, 1000); + } else { + // Initiate websocket "polling" + } + }) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9443036 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "node", + "target": "ES2020", + "jsx": "preserve", + "strictNullChecks": true, + "strictFunctionTypes": true + }, + "include": [ + "src/*" + ], + "exclude": [ + "node_modules", + "**/node_modules/*" + ] +} \ No newline at end of file -- cgit v1.2.3