replying to messages
This commit is contained in:
parent
2b34f47754
commit
b053ea000b
8 changed files with 202 additions and 47 deletions
|
@ -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()),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 `<div class="message-container" data-message-id="${messageJSON.id}">
|
||||
<blockquote class="message">
|
||||
<div><b>${messageJSON.from || "Anonymous"}</b> writes:</div>
|
||||
<pre class="not-code">${escapeHTML(messageJSON.content)}</pre>
|
||||
</blockquote>
|
||||
${messageJSON.response ? `<pre class="not-code">${escapeHTML(messageJSON.response)}</pre>` : (
|
||||
canRespond ? `
|
||||
<div><textarea placeholder="Your response..." maxlength="10000"></textarea></div>
|
||||
<div><button onclick="replyTo(${messageJSON.id})">Reply</button></div>
|
||||
` : `<i>No response</i>`
|
||||
)}
|
||||
</div>`;
|
||||
let el = document.createElement("div");
|
||||
el.classList.add("message-container");
|
||||
el.dataset.messageId = messageJSON.id;
|
||||
el.innerHTML = `<blockquote class="message">
|
||||
<div><b>${messageJSON.from || "Anonymous"}</b> writes: <small>${timeSince(messageJSON.timestamp)}</small></div>
|
||||
<pre class="not-code">${escapeHTML(messageJSON.content)}</pre>
|
||||
</blockquote>
|
||||
${messageJSON.response ? `<small>${timeSince(messageJSON.response_timestamp)}</small><pre class="not-code no-margin">\n${escapeHTML(messageJSON.response)}</pre>` : ""}
|
||||
${
|
||||
canRespond ? `
|
||||
<div class="msg-error"></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) {
|
||||
|
@ -48,33 +120,64 @@ function updateURL(switchID) {
|
|||
|
||||
function fetchMessages(fetchFromStart=false) {
|
||||
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}`))
|
||||
.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 = "<i>No messages</i>";
|
||||
} 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);
|
||||
|
|
|
@ -7,13 +7,14 @@
|
|||
|
||||
{% block body %}
|
||||
<h1>Your messages</h1>
|
||||
Logged in as {{ username }}
|
||||
Logged in as <b>{{ username }}</b>
|
||||
<hr>
|
||||
<button id="refresh">Refresh</button>
|
||||
<button id="refresh" onclick="fetchMessages(true);">Refresh</button>
|
||||
<p id="switch">
|
||||
<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>
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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/<str:username>/", profile),
|
||||
path("m/<str:username>/", message),
|
||||
path("api/messages/", get_message_list),
|
||||
path("api/messages/", api_messages),
|
||||
path("static/", include("tmessage.views.static")),
|
||||
path("django-admin/", admin.site.urls)
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue