From 0b3cb954382c60efa2990b8768cf6e31e176ebf1 Mon Sep 17 00:00:00 2001 From: mar77i Date: Mon, 30 Sep 2024 20:36:50 +0200 Subject: [PATCH] this is mvp, I suppose --- chat/migrations/0001_initial.py | 2 +- chat/static/chat/chat.css | 41 +++++- chat/static/chat/chat.js | 231 +++++++++++++++++++++++++++----- chat/static/chat/style.css | 37 ----- todo.txt | 9 +- 5 files changed, 241 insertions(+), 79 deletions(-) diff --git a/chat/migrations/0001_initial.py b/chat/migrations/0001_initial.py index c429326..4926758 100644 --- a/chat/migrations/0001_initial.py +++ b/chat/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.1 on 2024-09-30 09:06 +# Generated by Django 5.1.1 on 2024-09-30 15:56 import django.contrib.auth.models import django.contrib.auth.validators diff --git a/chat/static/chat/chat.css b/chat/static/chat/chat.css index 09c96d2..33d1121 100644 --- a/chat/static/chat/chat.css +++ b/chat/static/chat/chat.css @@ -1,3 +1,42 @@ -main { +html, body, nav, main, .messages, .channels, .users { height: 100%; } + +body, nav, main { + display: flex; +} + +nav { + width: 10em; + overflow: auto; +} + +main, .messages, .input > textarea { + width: 100%; +} + +nav, main { + flex-direction: column; +} + +nav, .input { + display:flex; +} + +.user, .users, .channels { + overflow-x: hidden; +} + +.user { + flex-shrink: 0; +} + +nav > div { + width: calc(100% - 1px); + border-bottom: 1px solid; + border-right: 1px solid; +} + +.input > textarea { + resize: none; +} \ No newline at end of file diff --git a/chat/static/chat/chat.js b/chat/static/chat/chat.js index 26d4c97..a35742b 100644 --- a/chat/static/chat/chat.js +++ b/chat/static/chat/chat.js @@ -1,22 +1,132 @@ (function () { var current_channel = {}; - var users_per_id = {}, current_user_id = 0; + var users_per_id = null; + var channels_per_id = null; - function add_msg(sender, msg) { - var p = document.createElement("P"); + function clear_children(element) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + } + + function get_data_id(element) { + var attr = element.getAttribute("data-id"); + if (attr === null) + return null; + return Number(attr); + } + + function get_current_user_id() { + return get_data_id(document.getElementsByClassName("user")[0].children[0]); + } + + function set_msg(p, sender, msg) { + clear_children(p); + if (typeof sender === 'number') { + sender = users_per_id[sender].username; + } p.appendChild(document.createTextNode(sender)); p.appendChild(document.createTextNode(": ")); p.appendChild(document.createTextNode(msg)); + } + + function add_msg(msg_id, sender, msg) { + var p = document.createElement("P"); + p.setAttribute("data-id", msg_id); + set_msg(p, sender, msg); document.getElementsByClassName("messages")[0].appendChild(p); } + function find_existing_message(msg_id, callback) { + var i, messages = document.getElementsByClassName("messages")[0]; + for (i = 0; i < messages.children.length; i++) { + if (get_data_id(messages.children[i]) === msg_id) { + fetch_data(current_channel.url + msg_id + "/", callback); + return; + } + } + } + + function delete_existing_message(msg_id) { + var i, messages = document.getElementsByClassName("messages")[0]; + for (i = 0; i < messages.children.length; i++) { + if (get_data_id(messages.children[i]) === msg_id) { + messages.removeChild(messages.children[i]); + return; + } + } + } + function ws_receive(msg) { - add_msg("ws", msg.data); + var data = JSON.parse(msg.data); + add_msg(null, "ws", msg.data); + if ( + current_channel.url === "/api/privatemessage/" + && data.table === "chat_privatemessage" + ) { + if ( + ( + data.new.sender_id === get_current_user_id() + && data.new.recipient_id === current_channel.data.recipient + ) || ( + data.new.sender_id === current_channel.data.recipient + && data.new.recipient_id === get_current_user_id() + ) + ) { + if (data.op === "INSERT") { + fetch_data( + current_channel.url + data.new.id + "/", add_privatemessage + ); + } else if (data.op === "UPDATE") { + find_existing_message(data.new.id, update_privatemessage); + } else if (data.op === "DELETE") { + delete_existing_message(data.old.id); + } else if (data.op === "TRUNCATE") { + clear_children(document.getElementsByClassName("messages")[0]); + } + } + } else if ( + current_channel.url === "/api/channelmessage/" + && data.table === "chat_channelmessage" + ) { + if (data.new.channel_id === current_channel.data.channel) { + if (data.op === "INSERT") { + fetch_data( + current_channel.url + data.new.id + "/", add_channelmessage + ); + } else if (data.op === "UPDATE") { + find_existing_message(data.new.id, update_channelmessage); + } else if (data.op === "DELETE") { + delete_existing_message(data.old.id); + } else if (data.op === "TRUNCATE") { + clear_children(document.getElementsByClassName("messages")[0]); + } + } + } else if (data.table === "chat_user") { + reload_users(); + } else if (data.table === "chat_channel") { + reload_channels(); + } + } + + function reload_users() { + clear_children(document.getElementsByClassName("users")[0].children[0]); + fetch_data("/api/user/", setup_user_callback); + } + + function reload_channels() { + clear_children(document.getElementsByClassName("channels")[0].children[0]); + fetch_data("/api/channel/", setup_channel_callback); } function xhr_error(msg) { console.log(msg); - add_msg("xhr_error", msg); + add_msg(null, "xhr_error", msg); + } + + function xhr_abort(msg) { + console.log(msg); + add_msg(null, "xhr_abort", msg); } function fetch_data(url, callback) { @@ -29,16 +139,35 @@ function post_data(url, data, callback) { var xhr = new XMLHttpRequest(); - xhr.addEventListener("load", callback); + if (callback !== null) { + xhr.addEventListener("load", callback); + } xhr.addEventListener("error", xhr_error); + xhr.addEventListener("abort", xhr_abort); xhr.open("post", url); xhr.send(data); } + function add_privatemessage() { + var msg = JSON.parse(this.responseText); + add_msg(msg.id, msg.sender, msg.text); + } + + function update_privatemessage() { + var msg = JSON.parse(this.responseText); + var i, messages = document.getElementsByClassName("messages")[0]; + for (i = 0; i < messages.children.length; i++) { + if (get_data_id(messages.children[i]) === msg.id) { + set_msg(messages.children[i], msg.sender, msg.text); + return; + } + } + } + function add_privatemessages() { var msgs = JSON.parse(this.responseText), i; for (i = 0; i < msgs.result.length; i += 1) { - add_msg(users_per_id[msgs.result[i].sender].username, msgs.result[i].text); + add_msg(msgs.result[i].id, msgs.result[i].sender, msgs.result[i].text); } } @@ -48,23 +177,26 @@ "url": "/api/privatemessage/", "data": {"recipient": user_id}, } - while(messages.firstChild) { - messages.removeChild(messages.firstChild); - } + clear_children(messages); fetch_data( "/api/privatemessage/?other=" + user_id.toString(), add_privatemessages ); } function setup_user_callback() { - var data = JSON.parse(this.responseText), ul, li, a; + var data = JSON.parse(this.responseText), ul, li, a, p; ul = document.getElementsByClassName("users")[0].children[0]; + users_per_id = {}; for (i = 0; i < data.result.length; i += 1) { users_per_id[data.result[i].id] = data.result[i]; if (data.result[i].is_current) { - current_user_id = data.result[i].id; + p = document.createElement("P"); + p.setAttribute("data-id", data.result[i].id); + p.appendChild(document.createTextNode(data.result[i].username)); + document.getElementsByClassName("user")[0].appendChild(p); } li = document.createElement("li"); + li.setAttribute("data-id", data.result[i].id); a = document.createElement("a"); a.setAttribute("href", "#"); a.appendChild( @@ -83,10 +215,26 @@ } } + function add_channelmessage() { + var msg = JSON.parse(this.responseText); + add_msg(msg.id, msg.user, msg.text); + } + + function update_channelmessage() { + var msg = JSON.parse(this.responseText); + var i, messages = document.getElementsByClassName("messages")[0]; + for (i = 0; i < messages.children.length; i++) { + if (get_data_id(messages.children[i]) === msg.id) { + set_msg(messages.children[i], msg.user, msg.text); + return; + } + } + } + function add_channelmessages() { var msgs = JSON.parse(this.responseText), i; for (i = 0; i < msgs.result.length; i += 1) { - add_msg(users_per_id[msgs.result[i].user].username, msgs.result[i].text); + add_msg(msgs.result[i].id, msgs.result[i].user, msgs.result[i].text); } } @@ -96,9 +244,7 @@ "url": "/api/channelmessage/", "data": {"channel": channel_id}, } - while(messages.firstChild) { - messages.removeChild(messages.firstChild); - } + clear_children(messages); fetch_data( "/api/channelmessage/?channel=" + channel_id.toString(), add_channelmessages ); @@ -107,8 +253,11 @@ function setup_channel_callback() { var data = JSON.parse(this.responseText), ul, li, a; ul = document.getElementsByClassName("channels")[0].children[0]; + channels_per_id = {}; for (i = 0; i < data.result.length; i += 1) { + channels_per_id[data.result[i].id] = data.result[i]; li = document.createElement("li"); + li.setAttribute("data-id", data.result[i].id); a = document.createElement("a"); a.setAttribute("href", "#"); a.appendChild(document.createTextNode(data.result[i].name)); @@ -131,27 +280,39 @@ data[name] = current_channel.data[name]; } } - post_data( - current_channel.url, - JSON.stringify(data), function () { add_msg("send", "success"); } - ); + post_data(current_channel.url, JSON.stringify(data), null); ta.value = ""; } - window.addEventListener("load", function(e) { - var ws, schema; - e = e || window.event; - if (e.target.readyState !== "complete") { - return; + function input_onkeydown(event) { + var keycode; + event = event || window.event; + keycode = event.code || event.key; + if (keycode === "Enter" || keycode === "NumpadEnter") { + // todo: make it possible to add newlines using ctrl+enter + send(); + event.preventDefault(); + event.stopPropagation(); } - // setup ws - schema = {"http:": "ws:", "https:": "wss:"}[window.location.protocol]; - ws = new WebSocket(schema + "//" + window.location.host + "/"); - ws.addEventListener("message", ws_receive); - fetch_data("/api/user/", setup_user_callback); - fetch_data("/api/channel/", setup_channel_callback); - document.getElementsByClassName("input")[0].children[1].addEventListener( - "click", send - ); - }); + } + + window.addEventListener( + "load", + function (event) { + var ws, schema, input; + event = event || window.event; + if (event.target.readyState !== "complete") { + return; + } + schema = {"http:": "ws:", "https:": "wss:"}[window.location.protocol]; + ws = new WebSocket(schema + "//" + window.location.host + "/"); + ws.addEventListener("message", ws_receive); + input = document.getElementsByClassName("input")[0]; + input.children[0].addEventListener("keydown", input_onkeydown); + input.children[0].value = ""; + input.children[1].addEventListener("click", send); + reload_users(); + reload_channels(); + } + ); }()); diff --git a/chat/static/chat/style.css b/chat/static/chat/style.css index 2341668..750cc65 100644 --- a/chat/static/chat/style.css +++ b/chat/static/chat/style.css @@ -4,40 +4,3 @@ background-color: #222; color: white; } - -html, body, nav, .messages, .channels, .users { - height: 100%; -} - -body, nav, main { - display: flex; -} - -nav { - width: 10em; - overflow: auto; -} - -main, .messages, .input > textarea, .user, .users, .channels { - width: 100%; -} - -nav, main { - flex-direction: column; -} - -nav, .input { - display:flex; -} - -.user, .users, .channels { - overflow-x: hidden; -} - -.user { - flex-shrink: 0; -} - -nav > div { - border-bottom: 1px solid; -} diff --git a/todo.txt b/todo.txt index 0a81c6b..9090dd5 100644 --- a/todo.txt +++ b/todo.txt @@ -1,14 +1,13 @@ +[ ] JS + - ws client infrastructure: auto-reconnect, notify-throttle +[ ] server-side throttling [ ] finish tests for api endpoints [ ] tests for pg-trigger→notify websocket - somehow test each trigger statement individually, that is 4 models * 5 endpoints -[ ] simple postgres→json/json→postgres rest backend -[ ] JS - - ws client infrastructure: auto-reconnect, notify-throttle - - xhr to fetch new data -[ ] css for chat UI [ ] ws and chat tests? [ ] channel management: channel admins? [ ] file uploads [ ] media uploads: images, gifs, even video files? [ ] moderation functions: report messages to MANAGERS, including private ones +[ ] write email to user -- 2.47.0