aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/index.html21
-rw-r--r--src/main.css116
-rw-r--r--src/main.ts399
3 files changed, 536 insertions, 0 deletions
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 @@
+<!DOCTYPE html>
+ <head>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <script src="main.js"></script>
+ <link rel="stylesheet" href="main.css">
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/css/fork-awesome.min.css" integrity="sha256-XoaMnoYC5TH6/+ihMEnospgm0J1PM/nioxbOUdnM8HY=" crossorigin="anonymous">
+ </head>
+ <body onload="load()">
+ <span id="navbar">
+ <span>Localhost</span>
+ <span id = "info" class="UI">Server Information: </span>
+ <span>username: <input id="username"></span>
+ </span>
+ <span id="new_messages" class="new_messages">New Messages</span>
+ <div id="chatwindow" onscroll="onscrollchat()"></div>
+ <span id="inputbox">
+ <textarea id="input" rows="2.5" onkeypress="inputkey(event)" onkeyup="clearnewlineinput()"></textarea>
+ <button onclick="send()">Send</button>
+ </span>
+ </body>
+</html>
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<number, Message>();
+const users = new Map<number, User>();
+let sent = false;
+const host = location.protocol + "//" + location.host;
+
+
+function xhttp_request(location: string): Promise<any> {
+ 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<string> {
+ var sender = await get_user_name(m.sender);
+ var sclass = "";
+
+ if (m.deleted) {
+ m.message = "*deleted*"
+ sclass = "deleted"
+ }
+
+ let s = `<span id="${m.id}" class="message ${sclass}">
+ <p class="messagedate">${format_date(m.date)}</p>
+ <p class="messagetext"> ${sender}: ${m.message}</p>
+ <span class="message_buttons_anchor">
+ <span class="message_buttons">
+ <button title="information" onclick="message_info(${m.id})"><i class="fa fa-info" aria-hidden="true"></i></button>
+ <button title="reply"><i class="fa fa-reply" aria-hidden="true"></i></button>
+ <button title="delete" onclick="delete_message(${m.id})"><i class="fa fa-trash-o" aria-hidden="true"></i></button>
+ </span>
+ </span></span>`
+
+ 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<Message[]> {
+ 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(`<span class="date">${date.getDate()}/${date.getMonth()}/${date.getFullYear()}</span>`))
+ }
+
+ 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"
+ }
+ })
+}