]> git.mar77i.info Git - chat/commitdiff
this is mvp, I suppose
authormar77i <mar77i@protonmail.ch>
Mon, 30 Sep 2024 18:36:50 +0000 (20:36 +0200)
committermar77i <mar77i@protonmail.ch>
Mon, 30 Sep 2024 18:40:47 +0000 (20:40 +0200)
chat/migrations/0001_initial.py
chat/static/chat/chat.css
chat/static/chat/chat.js
chat/static/chat/style.css
todo.txt

index c4293266cb6b6e385870295667f37868a3e45325..49267585bc7508d20824924333fae7ecfb7a6d91 100644 (file)
@@ -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
index 09c96d20dae41e49bc76c295238f667f4f4996c0..33d11219a79dc551cbbbaabae620ec44257257c2 100644 (file)
@@ -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
index 26d4c97f96218bc9adf6d3d4337460a8c7f2b388..a35742be918130787f1d6758f4eedc0328911a15 100644 (file)
 (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) {
 
     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);
         }
     }
 
             "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(
         }
     }
 
+    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);
         }
     }
 
             "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
         );
     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));
                 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();
+        }
+    );
 }());
index 2341668065ae0b741f5fe7487b15b46a822f0f91..750cc65e331565787e451992ef664b0720382324 100644 (file)
@@ -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;
-}
index 0a81c6b2917769a4c174e958bfb1e0a383af60f2..9090dd5185899e324e74932026dfecf357386655 100644 (file)
--- 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