From b053ea000bfd2799f18eb6f9a8e00ee9c971a9b4 Mon Sep 17 00:00:00 2001 From: trinkey Date: Tue, 24 Dec 2024 10:22:37 -0500 Subject: [PATCH] replying to messages --- tmessage/migrations/0001_initial.py | 4 +- tmessage/models.py | 2 + tmessage/static/js/messages.js | 145 ++++++++++++++++++++++++---- tmessage/templates/messages.html | 7 +- tmessage/urls.py | 4 +- tmessage/views/__init__.py | 2 +- tmessage/views/api.py | 82 ++++++++++++---- tmessage/views/templates.py | 3 + 8 files changed, 202 insertions(+), 47 deletions(-) diff --git a/tmessage/migrations/0001_initial.py b/tmessage/migrations/0001_initial.py index 539b51a..a42c16d 100644 --- a/tmessage/migrations/0001_initial.py +++ b/tmessage/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.7 on 2024-12-23 15:14 +# Generated by Django 5.0.7 on 2024-12-24 14:31 import django.db.models.deletion from django.db import migrations, models @@ -18,6 +18,8 @@ class Migration(migrations.Migration): ('message_id', models.IntegerField(primary_key=True, serialize=False)), ('content', models.CharField(max_length=10000)), ('response', models.CharField(blank=True, max_length=10000, null=True)), + ('sent_timestamp', models.IntegerField()), + ('response_timestamp', models.IntegerField(blank=True, null=True)), ('anonymous', models.BooleanField()), ], ), diff --git a/tmessage/models.py b/tmessage/models.py index 682be24..36def70 100644 --- a/tmessage/models.py +++ b/tmessage/models.py @@ -20,6 +20,8 @@ class tMMessage(models.Model): message_id = models.IntegerField(primary_key=True) content = models.CharField(max_length=10_000) response = models.CharField(max_length=10_000, blank=True, null=True) + sent_timestamp = models.IntegerField() + response_timestamp = models.IntegerField(blank=True, null=True) anonymous = models.BooleanField() likes = models.ManyToManyField(tMUser, through="tMM2MLike", related_name="liked") u_to = models.ForeignKey(tMUser, on_delete=models.CASCADE, related_name="received") diff --git a/tmessage/static/js/messages.js b/tmessage/static/js/messages.js index b08a081..1f2da2d 100644 --- a/tmessage/static/js/messages.js +++ b/tmessage/static/js/messages.js @@ -1,23 +1,95 @@ let offset; let timelineElement = document.getElementById("messages"); +let moreButton = document.getElementById("more-button"); function escapeHTML(content) { return content.replaceAll("&", "&").replaceAll("<", ">").replaceAll(">", "<"); } function getMessageHTML(messageJSON, canRespond) { - return `
-
-
${messageJSON.from || "Anonymous"} writes:
-
${escapeHTML(messageJSON.content)}
-
- ${messageJSON.response ? `
${escapeHTML(messageJSON.response)}
` : ( - canRespond ? ` -
-
- ` : `No response` - )} -
`; + let el = document.createElement("div"); + el.classList.add("message-container"); + el.dataset.messageId = messageJSON.id; + el.innerHTML = `
+
${messageJSON.from || "Anonymous"} writes: ${timeSince(messageJSON.timestamp)}
+
${escapeHTML(messageJSON.content)}
+
+ ${messageJSON.response ? `${timeSince(messageJSON.response_timestamp)}
\n${escapeHTML(messageJSON.response)}
` : ""} + ${ + canRespond ? ` +
+ ${messageJSON.response ? "" : '
'} +
+ ${messageJSON.response ? "" : ``} + +
+ ` : `No response` + }`; + + return el +} + +function timeSince(date) { + let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + let dateObject = new Date(date * 1000); + let dateString = `${months[dateObject.getMonth()]} ${dateObject.getDate()}, ${dateObject.getFullYear()}, ${String(dateObject.getHours()).padStart(2, "0")}:${String(dateObject.getMinutes()).padStart(2, "0")}:${String(dateObject.getSeconds()).padStart(2, "0")}`; + let seconds = Math.floor((+(new Date()) / 1000 - date + 1)); + let unit = "second"; + let amount = seconds > 0 ? seconds : 0; + const timeAmounts = [ + { name: "minute", amount: 60 }, + { name: "hour", amount: 3600 }, + { name: "day", amount: 86400 }, + { name: "month", amount: 2592000 }, + { name: "year", amount: 31536000 } + ]; + + for (const info of timeAmounts) { + let interval = seconds / info.amount; + if (interval >= 1) { + unit = info.name; + amount = Math.floor(interval); + } + } + return `${Math.floor(amount)} ${unit}${Math.floor(amount) == 1 ? "" : "s"} ago`; +} + +function replyTo(messageID) { + let msgContainer = document.querySelector(`.message-container[data-message-id="${messageID}"]`); + let self = msgContainer.querySelector(".reply-button") + let err = msgContainer.querySelector(`.msg-error`); + + self.setAttribute("disabled", ""); + + fetch("/api/messages/", { + method: "POST", + body: JSON.stringify({ + id: messageID, + content: msgContainer.querySelector("textarea").value + }) + }) + .then((response) => (response.json())) + .then((json) => { + if (json.success) { + let response = document.createElement("div"); + response.innerHTML = `${timeSince(json.timestamp)}
${escapeHTML(json.content)}
`; + err.innerHTML = ""; + + msgContainer.querySelector("textarea").replaceWith(response); + self.remove(); + } else { + err.innerHTML = "Something went wrong!"; + self.removeAttribute("disabled"); + } + }) + .catch((err) => { + err.innerHTML = `Something went wrong: ${err}`; + self.removeAttribute("disabled"); + }); +} + +function deleteMessage(messageID) { + // } function _updateURL_replaceElement(element, to) { @@ -48,33 +120,64 @@ function updateURL(switchID) { function fetchMessages(fetchFromStart=false) { document.getElementById("refresh").setAttribute("disabled", ""); - timelineElement.innerHTML = "Loading..." + let err, append; + + if (fetchFromStart) { + timelineElement.innerHTML = "Loading..." + moreButton.hidden = true; + } else { + err = document.querySelector(".delete-if-more-messages"); + append = !!err; + err = err || document.createElement("i"); + err.classList.add("delete-if-more-messages") + } fetch(url + (fetchFromStart === true ? "" : `${url.includes("?") ? "&" : "?"}offset=${offset}`)) .then((response) => (response.json())) .then((json) => { if (json.success) { - out = ""; + let frag = document.createDocumentFragment(); - if (json.messages.length == 0) { + if (fetchFromStart && json.messages.length == 0) { out = "No messages"; } else { for (const message of json.messages) { - out += getMessageHTML(message, json.canRespond); + frag.append(getMessageHTML(message, json.canRespond)); offset = message.id; } } - timelineElement.innerHTML = out; + moreButton.hidden = !json.more; + timelineElement.append(frag); + + for (el of document.getElementsByClassName("delete-if-more-messages")) { + el.remove(); + } } else { - timelineElement.innerText = "Something went wrong!"; + err = document.querySelector(".delete-if-more-messages"); + append = !!err; + err = err || document.createElement("i"); + err.classList.add("delete-if-more-messages") + err.innerText = "Something went wrong!"; + append && timelineElement.append(err); } + document.getElementById("refresh").removeAttribute("disabled"); }) - .catch((err) => { - timelineElement.innerText = `Couldn't fetch timeline: ${err}`; + .catch((error) => { + err = document.querySelector(".delete-if-more-messages"); + append = !!err; + err = err || document.createElement("i"); + err.classList.add("delete-if-more-messages") + err.innerText = `Couldn't fetch timeline: ${error}`; + append && timelineElement.append(err); document.getElementById("refresh").removeAttribute("disabled"); }); } fetchMessages(true); -document.getElementById("refresh").addEventListener("click", (() => { fetchMessages(true); })); + +setInterval(function () { + for (const timestamp of document.querySelectorAll("[data-timestamp]")) { + timestamp.innerHTML = timeSince(Number(timestamp.dataset.timestamp)); + } +}, 5000); diff --git a/tmessage/templates/messages.html b/tmessage/templates/messages.html index 7131295..02b2c95 100644 --- a/tmessage/templates/messages.html +++ b/tmessage/templates/messages.html @@ -7,13 +7,14 @@ {% block body %}

Your messages

- Logged in as {{ username }} + Logged in as {{ username }}
- +

All messages - Not responded

-
Loading...
+
Loading...
+ {% endblock %} diff --git a/tmessage/urls.py b/tmessage/urls.py index 23f60bd..998aa44 100644 --- a/tmessage/urls.py +++ b/tmessage/urls.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.urls import include, path -from .views import auth, get_message_list, index, message, messages, profile +from .views import api_messages, auth, index, message, messages, profile urlpatterns = [ path("", index), @@ -9,7 +9,7 @@ urlpatterns = [ path("messages/", messages), path("u//", profile), path("m//", message), - path("api/messages/", get_message_list), + path("api/messages/", api_messages), path("static/", include("tmessage.views.static")), path("django-admin/", admin.site.urls) ] diff --git a/tmessage/views/__init__.py b/tmessage/views/__init__.py index 7da553a..ada94fb 100644 --- a/tmessage/views/__init__.py +++ b/tmessage/views/__init__.py @@ -1,2 +1,2 @@ -from .api import get_message_list # noqa: F401 +from .api import api_messages # noqa: F401 from .templates import auth, index, message, messages, profile # noqa: F401 diff --git a/tmessage/views/api.py b/tmessage/views/api.py index 9dd95e5..8135e16 100644 --- a/tmessage/views/api.py +++ b/tmessage/views/api.py @@ -1,21 +1,66 @@ import json +import math +import time from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponse +from django.views.decorators.csrf import csrf_exempt + +from tmessage.models import tMMessage from .helper import get_user_object, get_username -def get_message_list(request: WSGIRequest) -> HttpResponse: +def _json_response(data: dict | list, /, *, status: int=200, content_type: str="application/json") -> HttpResponse: + return HttpResponse( + json.dumps(data), + status=status, + content_type=content_type + ) + +RESPONSE_400 = _json_response({ "success": False }, status=400) +RESPONSE_401 = _json_response({ "success": False }, status=401) + +@csrf_exempt +def api_messages(request: WSGIRequest) -> HttpResponse: username = get_username(request) + if username is None: - return HttpResponse( - json.dumps({ - "success": False - }), - content_type="application/json", - status=401 - ) + return RESPONSE_401 + + if request.method == "POST": + body = json.loads(request.body) + reply = body["content"].strip() + message_id = body["id"] + + if not (isinstance(message_id, int) and isinstance(reply, str)): + return RESPONSE_400 + + user = get_user_object(username, i_promise_this_user_exists=True) + + try: + message = tMMessage.objects.get(message_id=message_id) + except tMMessage.DoesNotExist: + return RESPONSE_400 + + if username != message.u_to.username: + return RESPONSE_401 + + if message.response is not None: + return RESPONSE_400 + + message.response = reply + message.response_timestamp = math.floor(time.time()) + message.save() + + return _json_response({ + "success": True, + "content": reply, + "timestamp": message.response_timestamp + }) + + elif request.method == "DELETE": + ... user = get_user_object(username, i_promise_this_user_exists=True) @@ -34,7 +79,7 @@ def get_message_list(request: WSGIRequest) -> HttpResponse: output = [] messages = msgObjects.order_by("-message_id")[:50].values_list( - "message_id", "content", "response", "anonymous", "u_from" + "message_id", "content", "response", "anonymous", "u_from", "sent_timestamp", "response_timestamp" ) for message in messages: @@ -42,15 +87,14 @@ def get_message_list(request: WSGIRequest) -> HttpResponse: "id": message[0], "content": message[1], "response": message[2], - "from": message[4].username if not message[3] and message[4] else None + "from": message[4] if not message[3] and message[4] else None, + "timestamp": message[5], + "response_timestamp": message[6] }) - return HttpResponse( - json.dumps({ - "success": True, - "canRespond": True, - "messages": output, - "more": msgObjects.count() > 50 - }), - content_type="application/json" - ) + return _json_response({ + "success": True, + "canRespond": True, + "messages": output, + "more": msgObjects.count() > 50 + }) diff --git a/tmessage/views/templates.py b/tmessage/views/templates.py index ea7a57b..48f22ac 100644 --- a/tmessage/views/templates.py +++ b/tmessage/views/templates.py @@ -1,3 +1,5 @@ +import math +import time from datetime import datetime from django.core.handlers.wsgi import WSGIRequest @@ -61,6 +63,7 @@ def message(request: WSGIRequest, username: str) -> HttpResponse: tMMessage.objects.create( content=content, response=None, + sent_timestamp=math.floor(time.time()), anonymous=anonymous, u_to=get_user_object(username, i_promise_this_user_exists=True), u_from=u_from