message list
This commit is contained in:
parent
103998c0e6
commit
2b34f47754
12 changed files with 222 additions and 9 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"TAUTH",
|
"TAUTH",
|
||||||
|
"tcommon",
|
||||||
"tmessage"
|
"tmessage"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ SECRET_KEY = config["services"]["message"]["token"]
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
STATIC_DIR = BASE_DIR / "tmessage/static"
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
@ -74,6 +75,4 @@ LANGUAGE_CODE = "en-us"
|
||||||
TIME_ZONE = "UTC"
|
TIME_ZONE = "UTC"
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
STATIC_URL = "/static/"
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
STATIC_ROOT = BASE_DIR / "collected-static"
|
|
||||||
|
|
17
tmessage/static/css/messages.css
Normal file
17
tmessage/static/css/messages.css
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.message-container {
|
||||||
|
width: calc(90vw - 40px);
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-radius: 6px;
|
||||||
|
outline: 1px solid rgb(var(--accent));
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-container textarea {
|
||||||
|
width: calc(90vw - 52px);
|
||||||
|
max-width: 388px;
|
||||||
|
resize: vertical;
|
||||||
|
height: 100px;
|
||||||
|
}
|
80
tmessage/static/js/messages.js
Normal file
80
tmessage/static/js/messages.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
let offset;
|
||||||
|
let timelineElement = document.getElementById("messages");
|
||||||
|
|
||||||
|
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>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateURL_replaceElement(element, to) {
|
||||||
|
let newElement = document.createElement(to);
|
||||||
|
newElement.dataset.url = element.dataset.url;
|
||||||
|
newElement.dataset.id = element.dataset.id;
|
||||||
|
newElement.innerHTML = element.innerHTML;
|
||||||
|
newElement.setAttribute("href", element.getAttribute("href"));
|
||||||
|
|
||||||
|
if (to == "a") {
|
||||||
|
newElement.classList.add("not-bold");
|
||||||
|
}
|
||||||
|
|
||||||
|
element.replaceWith(newElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateURL(switchID) {
|
||||||
|
let switchElement = document.getElementById("switch").querySelector(`[data-id="${switchID}"]`);
|
||||||
|
|
||||||
|
for (const el of document.getElementById("switch").querySelectorAll("b")) {
|
||||||
|
_updateURL_replaceElement(el, "a");
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateURL_replaceElement(switchElement, "b");
|
||||||
|
url = switchElement.dataset.url;
|
||||||
|
fetchMessages(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchMessages(fetchFromStart=false) {
|
||||||
|
document.getElementById("refresh").setAttribute("disabled", "");
|
||||||
|
timelineElement.innerHTML = "<i>Loading...</i>"
|
||||||
|
|
||||||
|
fetch(url + (fetchFromStart === true ? "" : `${url.includes("?") ? "&" : "?"}offset=${offset}`))
|
||||||
|
.then((response) => (response.json()))
|
||||||
|
.then((json) => {
|
||||||
|
if (json.success) {
|
||||||
|
out = "";
|
||||||
|
|
||||||
|
if (json.messages.length == 0) {
|
||||||
|
out = "<i>No messages</i>";
|
||||||
|
} else {
|
||||||
|
for (const message of json.messages) {
|
||||||
|
out += getMessageHTML(message, json.canRespond);
|
||||||
|
offset = message.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timelineElement.innerHTML = out;
|
||||||
|
} else {
|
||||||
|
timelineElement.innerText = "Something went wrong!";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
timelineElement.innerText = `Couldn't fetch timeline: ${err}`;
|
||||||
|
document.getElementById("refresh").removeAttribute("disabled");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchMessages(true);
|
||||||
|
document.getElementById("refresh").addEventListener("click", (() => { fetchMessages(true); }));
|
|
@ -6,7 +6,7 @@
|
||||||
<hr>
|
<hr>
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<p><textarea name="message" id="message" required maxlength="10000" placeholder="Your message"></textarea></p>
|
<p><textarea class="auto-size" name="message" id="message" required maxlength="10000" placeholder="Your message"></textarea></p>
|
||||||
{% if self_username %}
|
{% if self_username %}
|
||||||
Logged in as <b>{{ self_username }}</b>
|
Logged in as <b>{{ self_username }}</b>
|
||||||
<div>
|
<div>
|
||||||
|
|
19
tmessage/templates/messages.html
Normal file
19
tmessage/templates/messages.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="/static/css/messages.css">
|
||||||
|
<script>let url = "/api/messages/";</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>Your messages</h1>
|
||||||
|
Logged in as {{ username }}
|
||||||
|
<hr>
|
||||||
|
<button id="refresh">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>
|
||||||
|
<script src="/static/js/messages.js?v={{ config.version_str }}"></script>
|
||||||
|
{% endblock %}
|
|
@ -1,12 +1,15 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import include, path
|
||||||
|
|
||||||
from .views import auth, index, message, profile
|
from .views import auth, get_message_list, index, message, messages, profile
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", index),
|
path("", index),
|
||||||
path("auth/", auth),
|
path("auth/", auth),
|
||||||
|
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("static/", include("tmessage.views.static")),
|
||||||
path("django-admin/", admin.site.urls)
|
path("django-admin/", admin.site.urls)
|
||||||
]
|
]
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from .templates import auth, index, message, profile # noqa: F401
|
from .api import get_message_list # noqa: F401
|
||||||
|
from .templates import auth, index, message, messages, profile # noqa: F401
|
||||||
|
|
56
tmessage/views/api.py
Normal file
56
tmessage/views/api.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
from .helper import get_user_object, get_username
|
||||||
|
|
||||||
|
|
||||||
|
def get_message_list(request: WSGIRequest) -> HttpResponse:
|
||||||
|
username = get_username(request)
|
||||||
|
if username is None:
|
||||||
|
return HttpResponse(
|
||||||
|
json.dumps({
|
||||||
|
"success": False
|
||||||
|
}),
|
||||||
|
content_type="application/json",
|
||||||
|
status=401
|
||||||
|
)
|
||||||
|
|
||||||
|
user = get_user_object(username, i_promise_this_user_exists=True)
|
||||||
|
|
||||||
|
queryFilter = {}
|
||||||
|
|
||||||
|
if "offset" in request.GET and (request.GET.get("offset") or "").isdigit():
|
||||||
|
queryFilter["message_id__lt"] = int(request.GET.get("offset") or "")
|
||||||
|
|
||||||
|
if "unread" in request.GET:
|
||||||
|
queryFilter["response"] = None
|
||||||
|
|
||||||
|
if queryFilter:
|
||||||
|
msgObjects = user.received.filter(**queryFilter)
|
||||||
|
else:
|
||||||
|
msgObjects = user.received.all() # type: ignore
|
||||||
|
|
||||||
|
output = []
|
||||||
|
messages = msgObjects.order_by("-message_id")[:50].values_list(
|
||||||
|
"message_id", "content", "response", "anonymous", "u_from"
|
||||||
|
)
|
||||||
|
|
||||||
|
for message in messages:
|
||||||
|
output.append({
|
||||||
|
"id": message[0],
|
||||||
|
"content": message[1],
|
||||||
|
"response": message[2],
|
||||||
|
"from": message[4].username if not message[3] and message[4] else None
|
||||||
|
})
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
json.dumps({
|
||||||
|
"success": True,
|
||||||
|
"canRespond": True,
|
||||||
|
"messages": output,
|
||||||
|
"more": msgObjects.count() > 50
|
||||||
|
}),
|
||||||
|
content_type="application/json"
|
||||||
|
)
|
|
@ -23,7 +23,7 @@ def render_template(
|
||||||
c = {
|
c = {
|
||||||
"accent": random.choice(COLORS),
|
"accent": random.choice(COLORS),
|
||||||
"config": config,
|
"config": config,
|
||||||
"login_token": f"/auth/?sessionid={request.COOKIES.get('sessionid')}"
|
"login_token": f"/auth/?sessionid={request.COOKIES.get('session_id')}"
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, val in context.items():
|
for key, val in context.items():
|
||||||
|
|
26
tmessage/views/static.py
Normal file
26
tmessage/views/static.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.urls import path
|
||||||
|
from django.views.decorators.cache import cache_control
|
||||||
|
|
||||||
|
from tmessage.settings import STATIC_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def get_static_serve(path: str, content_type: str):
|
||||||
|
def x(request):
|
||||||
|
return HttpResponse(
|
||||||
|
open(STATIC_DIR / path, "rb").read(),
|
||||||
|
content_type=content_type
|
||||||
|
)
|
||||||
|
|
||||||
|
x.__name__ = path
|
||||||
|
return x
|
||||||
|
|
||||||
|
file_associations = {
|
||||||
|
"js": "text/javascript",
|
||||||
|
"css": "text/css"
|
||||||
|
}
|
||||||
|
|
||||||
|
urlpatterns = [path(i, cache_control(**{"max-age": 60 * 60 * 24 * 30})(get_static_serve(i, file_associations[i.split(".")[-1]]))) for i in [
|
||||||
|
"js/messages.js",
|
||||||
|
"css/messages.css"
|
||||||
|
]]
|
|
@ -12,9 +12,9 @@ from .helper import (get_user_object, get_username, render_template,
|
||||||
def auth(request: WSGIRequest) -> HttpResponseRedirect:
|
def auth(request: WSGIRequest) -> HttpResponseRedirect:
|
||||||
resp = HttpResponseRedirect("/")
|
resp = HttpResponseRedirect("/")
|
||||||
if "remove" in request.GET:
|
if "remove" in request.GET:
|
||||||
resp.set_cookie("sessionid", "", max_age=0, expires=datetime(0, 0, 0))
|
resp.set_cookie("session_id", "", max_age=0, expires=datetime(0, 0, 0))
|
||||||
else:
|
else:
|
||||||
resp.set_cookie("sessionid", request.GET.get("sessionid") or "")
|
resp.set_cookie("session_id", request.GET.get("sessionid") or "")
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@ -74,3 +74,14 @@ def message(request: WSGIRequest, username: str) -> HttpResponse:
|
||||||
error=error,
|
error=error,
|
||||||
self_username=get_username(request)
|
self_username=get_username(request)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def messages(request: WSGIRequest) -> HttpResponse:
|
||||||
|
username = get_username(request)
|
||||||
|
|
||||||
|
if username is None:
|
||||||
|
return HttpResponseRedirect("/")
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
request, "messages.html",
|
||||||
|
username=username
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue