add notification for new post in thread
This commit is contained in:
parent
8ea9afd39d
commit
1a96612544
1
app.lua
1
app.lua
@ -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
36
apps/api.lua
Normal 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
|
@ -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);
|
||||
}
|
||||
|
45
js/thread.js
45
js/thread.js
@ -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
59
lib/sse.lua
Normal 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
|
@ -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")
|
||||
|
@ -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);
|
||||
}
|
||||
|
11
util.lua
11
util.lua
@ -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
|
||||
|
10
views/threads/new-post-notification.etlua
Normal file
10
views/threads/new-post-notification.etlua
Normal 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>
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user