add notification for new post in thread

This commit is contained in:
Lera Elvoé 2025-05-28 04:01:51 +03:00
parent 8ea9afd39d
commit 1a96612544
Signed by: yagich
SSH Key Fingerprint: SHA256:6xjGb6uA7lAVcULa7byPEN//rQ0wPoG+UzYVMfZnbvc
10 changed files with 195 additions and 12 deletions

View File

@ -54,6 +54,7 @@ app:include("apps.topics", {path = "/topics"})
app:include("apps.threads", {path = "/threads"})
app:include("apps.mod", {path = "/mod"})
app:include("apps.post", {path = "/post"})
app:include("apps.api", {path = "/api"})
app:get("/", function(self)
return {redirect_to = self:url_for("all_topics")}

36
apps/api.lua Normal file
View File

@ -0,0 +1,36 @@
local app = require("lapis").Application()
local sse = require("lib.sse")
local db = require("lapis.db")
local util = require("util")
app:get("sse_thread_updates", "/thread-updates/:thread_id", function(self)
do
local thread = db.query("SELECT threads.id FROM threads WHERE threads.id = ?", self.params.thread_id)
if #thread == 0 then
return {status = 404, skip_render = true}
end
end
local now = os.time()
local stream = sse:new()
local thread_id = self.params.thread_id
local new_posts_query = "SELECT id FROM posts WHERE thread_id = ? AND posts.created_at > ? ORDER BY posts.created_at ASC LIMIT 1"
while stream.active do
stream:dispatch()
local new_post = db.query(new_posts_query, thread_id, now)
if #new_post > 0 then
local url = util.get_post_url(self, new_post[1].id)
stream:enqueue(url, "new_post_url")
end
ngx.sleep(5)
end
return {skip_render = true}
end)
return app

View File

@ -1,8 +1,3 @@
/* src: */
@font-face {
font-family: "body-text";
src: url("/static/fonts/DINish[slnt,wdth,wght].woff2") format("woff2");
}
@font-face {
font-family: "site-title";
src: url("/static/fonts/ChicagoFLF.woff2");
@ -520,3 +515,18 @@ ul, ol {
margin: 10px 0 10px 30px;
padding: 0;
}
.new-concept-notification.hidden {
display: none;
}
.new-concept-notification {
position: fixed;
bottom: 80px;
right: 80px;
border: 2px solid black;
background-color: #81a3e6;
padding: 20px 15px;
border-radius: 4px;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
}

View File

@ -35,4 +35,49 @@
form.action = `/post/${postId}/delete`
})
}
let newPostSubscription = null;
function hideNotification() {
const notification = document.getElementById('new-post-notification');
notification.classList.add('hidden');
}
function showNewPostNotification(url) {
const notification = document.getElementById("new-post-notification");
notification.classList.remove("hidden");
document.getElementById("dismiss-new-post-button").onclick = () => {
hideNotification();
reconnectSSE();
}
document.getElementById("go-to-new-post-button").href = url;
document.getElementById("unsub-new-post-button").onclick = () => {
hideNotification()
}
}
function reconnectSSE() {
if (newPostSubscription) newPostSubscription.close();
const threadEndpoint = document.getElementById("thread-subscribe-endpoint").value;
newPostSubscription = new EventSource(threadEndpoint);
newPostSubscription.onerror = (e) => {
console.error(e);
};
newPostSubscription.addEventListener("new_post_url", (e) => {
showNewPostNotification(e.data);
newPostSubscription.close();
})
}
window.addEventListener('beforeunload', () => {
if(newPostSubscription)
{
newPostSubscription.close();
}
});
reconnectSSE();
}

59
lib/sse.lua Normal file
View File

@ -0,0 +1,59 @@
---@class SSE
---@field active boolean if the stream is not active, you should stop the loop.
---@field private _queue table
local sse = {}
---Construct a new SSE object
---@return SSE
function sse:new()
ngx.header.content_type = "text/event-stream"
ngx.header.cache_control = "no-cache"
ngx.header.connection = "keep-alive"
ngx.status = ngx.HTTP_OK
ngx.flush(true)
local obj = {
active = true,
_queue = {},
}
ngx.on_abort(function()
obj.active = false
end)
return setmetatable(obj, {__index = sse})
end
---add data to the stream, writing on the next dispatch.
---if `event` is given, it will be the key.
---@param data string
---@param event? string
---@return boolean status
function sse:enqueue(data, event)
if not self.active then return false end
table.insert(self._queue, {
data = data,
event = event,
})
return true
end
---send all events since the last dispatch and flush the queue.
---call this every iteration of the loop.
function sse:dispatch()
while #self._queue > 0 do
local msg = table.remove(self._queue, 1)
if msg.event then
ngx.print("event: " .. msg.event .. "\n")
end
ngx.print("data: " .. msg.data .. "\n\n")
end
ngx.flush(true)
end
---close the stream.
function sse:close()
self.active = false
end
return sse

View File

@ -19,6 +19,7 @@ http {
lua_code_cache ${{CODE_CACHE}};
location / {
lua_check_client_abort on;
default_type text/html;
content_by_lua_block {
require("lapis").serve("app")

View File

@ -1,12 +1,5 @@
/* src: */
@use "sass:color";
@font-face {
font-family: "body-text";
src: url("/static/fonts/DINish[slnt,wdth,wght].woff2") format("woff2");
}
@font-face {
font-family: "site-title";
src: url("/static/fonts/ChicagoFLF.woff2");
@ -536,3 +529,18 @@ ul, ol {
margin: 10px 0 10px 30px;
padding: 0;
}
.new-concept-notification.hidden {
display: none;
}
.new-concept-notification {
position: fixed;
bottom: 80px;
right: 80px;
border: 2px solid black;
background-color: #81a3e6;
padding: 20px 15px;
border-radius: 4px;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.25);
}

View File

@ -9,6 +9,7 @@ local Avatars = require("models").Avatars
local Users = require("models").Users
local Posts = require("models").Posts
local PostHistory = require("models").PostHistory
local Threads = require("models").Threads
local babycode = require("lib.babycode")
@ -103,6 +104,16 @@ function util.split_sentences(sentences, max_sentences)
return util.s_split(sentences, ".", max_sentences or 2, true, false)
end
---@return string
function util.get_post_url(req, post_id)
local post = Posts:find({id = post_id})
if not post then return "" end
local thread = Threads:find({id = post.thread_id})
if not thread then return "" end
return req:url_for("thread", {slug = thread.slug}, {after = post_id}) .. "#post-" .. post_id
end
function util.infobox_message(msg)
local sentences = util.split_sentences(msg)
if #sentences == 1 then

View File

@ -0,0 +1,10 @@
<div id="new-post-notification" class="new-concept-notification hidden">
<div class="new-notification-content">
<p>New post in thread!</p>
<span class="notification-buttons">
<button id="dismiss-new-post-button">Dismiss</button>
<a class="linkbutton" id="go-to-new-post-button">View post</a>
<button id="unsub-new-post-button">Stop updates</button>
</span>
</div>
</div>

View File

@ -55,4 +55,6 @@
</span>
</div>
</dialog>
<input type="hidden" id="thread-subscribe-endpoint" value="<%= url_for("sse_thread_updates", {thread_id = thread.id}) %>">
<% render("views.threads.new-post-notification") %>
<script src="/static/js/thread.js"></script>