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