replying to messages

This commit is contained in:
trinkey 2024-12-24 10:22:37 -05:00
parent 2b34f47754
commit b053ea000b
8 changed files with 202 additions and 47 deletions

View file

@ -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 import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -18,6 +18,8 @@ class Migration(migrations.Migration):
('message_id', models.IntegerField(primary_key=True, serialize=False)), ('message_id', models.IntegerField(primary_key=True, serialize=False)),
('content', models.CharField(max_length=10000)), ('content', models.CharField(max_length=10000)),
('response', models.CharField(blank=True, max_length=10000, null=True)), ('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()), ('anonymous', models.BooleanField()),
], ],
), ),

View file

@ -20,6 +20,8 @@ class tMMessage(models.Model):
message_id = models.IntegerField(primary_key=True) message_id = models.IntegerField(primary_key=True)
content = models.CharField(max_length=10_000) content = models.CharField(max_length=10_000)
response = models.CharField(max_length=10_000, blank=True, null=True) 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() anonymous = models.BooleanField()
likes = models.ManyToManyField(tMUser, through="tMM2MLike", related_name="liked") likes = models.ManyToManyField(tMUser, through="tMM2MLike", related_name="liked")
u_to = models.ForeignKey(tMUser, on_delete=models.CASCADE, related_name="received") u_to = models.ForeignKey(tMUser, on_delete=models.CASCADE, related_name="received")

View file

@ -1,23 +1,95 @@
let offset; let offset;
let timelineElement = document.getElementById("messages"); let timelineElement = document.getElementById("messages");
let moreButton = document.getElementById("more-button");
function escapeHTML(content) { function escapeHTML(content) {
return content.replaceAll("&", "&amp;").replaceAll("<", "&gt;").replaceAll(">", "&lt;"); return content.replaceAll("&", "&amp;").replaceAll("<", "&gt;").replaceAll(">", "&lt;");
} }
function getMessageHTML(messageJSON, canRespond) { function getMessageHTML(messageJSON, canRespond) {
return `<div class="message-container" data-message-id="${messageJSON.id}"> let el = document.createElement("div");
<blockquote class="message"> el.classList.add("message-container");
<div><b>${messageJSON.from || "Anonymous"}</b> writes:</div> el.dataset.messageId = messageJSON.id;
<pre class="not-code">${escapeHTML(messageJSON.content)}</pre> el.innerHTML = `<blockquote class="message">
</blockquote> <div><b>${messageJSON.from || "Anonymous"}</b> writes: <small>${timeSince(messageJSON.timestamp)}</small></div>
${messageJSON.response ? `<pre class="not-code">${escapeHTML(messageJSON.response)}</pre>` : ( <pre class="not-code">${escapeHTML(messageJSON.content)}</pre>
canRespond ? ` </blockquote>
<div><textarea placeholder="Your response..." maxlength="10000"></textarea></div> ${messageJSON.response ? `<small>${timeSince(messageJSON.response_timestamp)}</small><pre class="not-code no-margin">\n${escapeHTML(messageJSON.response)}</pre>` : ""}
<div><button onclick="replyTo(${messageJSON.id})">Reply</button></div> ${
` : `<i>No response</i>` canRespond ? `
)} <div class="msg-error"></div>
</div>`; ${messageJSON.response ? "" : '<div><textarea placeholder="Your response..." maxlength="10000"></textarea></div>'}
<div class="center">
${messageJSON.response ? "" : `<button class="reply-button" onclick="replyTo(${messageJSON.id})">Reply</button>`}
<button onclick="deleteMessage(${messageJSON.id})">Delete</button>
</div>
` : `<i>No response</i>`
}`;
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 `<span data-timestamp="${date}" title="${dateString}">${Math.floor(amount)} ${unit}${Math.floor(amount) == 1 ? "" : "s"} ago</span>`;
}
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 = `<small>${timeSince(json.timestamp)}</small><pre class="not-code no-margin">${escapeHTML(json.content)}</pre>`;
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) { function _updateURL_replaceElement(element, to) {
@ -48,33 +120,64 @@ function updateURL(switchID) {
function fetchMessages(fetchFromStart=false) { function fetchMessages(fetchFromStart=false) {
document.getElementById("refresh").setAttribute("disabled", ""); document.getElementById("refresh").setAttribute("disabled", "");
timelineElement.innerHTML = "<i>Loading...</i>" let err, append;
if (fetchFromStart) {
timelineElement.innerHTML = "<i class='delete-if-more-messages'>Loading...</i>"
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}`)) fetch(url + (fetchFromStart === true ? "" : `${url.includes("?") ? "&" : "?"}offset=${offset}`))
.then((response) => (response.json())) .then((response) => (response.json()))
.then((json) => { .then((json) => {
if (json.success) { if (json.success) {
out = ""; let frag = document.createDocumentFragment();
if (json.messages.length == 0) { if (fetchFromStart && json.messages.length == 0) {
out = "<i>No messages</i>"; out = "<i>No messages</i>";
} else { } else {
for (const message of json.messages) { for (const message of json.messages) {
out += getMessageHTML(message, json.canRespond); frag.append(getMessageHTML(message, json.canRespond));
offset = message.id; 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 { } 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) => { .catch((error) => {
timelineElement.innerText = `Couldn't fetch timeline: ${err}`; 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"); document.getElementById("refresh").removeAttribute("disabled");
}); });
} }
fetchMessages(true); 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);

View file

@ -7,13 +7,14 @@
{% block body %} {% block body %}
<h1>Your messages</h1> <h1>Your messages</h1>
Logged in as {{ username }} Logged in as <b>{{ username }}</b>
<hr> <hr>
<button id="refresh">Refresh</button> <button id="refresh" onclick="fetchMessages(true);">Refresh</button>
<p id="switch"> <p id="switch">
<b data-url="/api/messages/" data-id="all" href="javascript:(() => { updateURL('all'); })()">All messages</b> - <b data-url="/api/messages/" data-id="all" href="javascript:(() => { updateURL('all'); })()">All messages</b> -
<a data-url="/api/messages/?unread" data-id="unread" class="not-bold" href="javascript:(() => { updateURL('unread'); })()">Not responded</a> <a data-url="/api/messages/?unread" data-id="unread" class="not-bold" href="javascript:(() => { updateURL('unread'); })()">Not responded</a>
</p> </p>
<div id="messages"><i>Loading...</i></div> <div id="messages"><i class="delete-if-more-messages">Loading...</i></div>
<button hidden id="more-button" onclick="fetchMessages(false);">Load more</button>
<script src="/static/js/messages.js?v={{ config.version_str }}"></script> <script src="/static/js/messages.js?v={{ config.version_str }}"></script>
{% endblock %} {% endblock %}

View file

@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.urls import include, path 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 = [ urlpatterns = [
path("", index), path("", index),
@ -9,7 +9,7 @@ urlpatterns = [
path("messages/", messages), path("messages/", messages),
path("u/<str:username>/", profile), path("u/<str:username>/", profile),
path("m/<str:username>/", message), path("m/<str:username>/", message),
path("api/messages/", get_message_list), path("api/messages/", api_messages),
path("static/", include("tmessage.views.static")), path("static/", include("tmessage.views.static")),
path("django-admin/", admin.site.urls) path("django-admin/", admin.site.urls)
] ]

View file

@ -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 from .templates import auth, index, message, messages, profile # noqa: F401

View file

@ -1,21 +1,66 @@
import json import json
import math
import time
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse 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 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) username = get_username(request)
if username is None: if username is None:
return HttpResponse( return RESPONSE_401
json.dumps({
"success": False if request.method == "POST":
}), body = json.loads(request.body)
content_type="application/json", reply = body["content"].strip()
status=401 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) user = get_user_object(username, i_promise_this_user_exists=True)
@ -34,7 +79,7 @@ def get_message_list(request: WSGIRequest) -> HttpResponse:
output = [] output = []
messages = msgObjects.order_by("-message_id")[:50].values_list( 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: for message in messages:
@ -42,15 +87,14 @@ def get_message_list(request: WSGIRequest) -> HttpResponse:
"id": message[0], "id": message[0],
"content": message[1], "content": message[1],
"response": message[2], "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( return _json_response({
json.dumps({ "success": True,
"success": True, "canRespond": True,
"canRespond": True, "messages": output,
"messages": output, "more": msgObjects.count() > 50
"more": msgObjects.count() > 50 })
}),
content_type="application/json"
)

View file

@ -1,3 +1,5 @@
import math
import time
from datetime import datetime from datetime import datetime
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
@ -61,6 +63,7 @@ def message(request: WSGIRequest, username: str) -> HttpResponse:
tMMessage.objects.create( tMMessage.objects.create(
content=content, content=content,
response=None, response=None,
sent_timestamp=math.floor(time.time()),
anonymous=anonymous, anonymous=anonymous,
u_to=get_user_object(username, i_promise_this_user_exists=True), u_to=get_user_object(username, i_promise_this_user_exists=True),
u_from=u_from u_from=u_from