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.threads", {path = "/threads"})
|
||||||
app:include("apps.mod", {path = "/mod"})
|
app:include("apps.mod", {path = "/mod"})
|
||||||
app:include("apps.post", {path = "/post"})
|
app:include("apps.post", {path = "/post"})
|
||||||
|
app:include("apps.api", {path = "/api"})
|
||||||
|
|
||||||
app:get("/", function(self)
|
app:get("/", function(self)
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
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-face {
|
||||||
font-family: "site-title";
|
font-family: "site-title";
|
||||||
src: url("/static/fonts/ChicagoFLF.woff2");
|
src: url("/static/fonts/ChicagoFLF.woff2");
|
||||||
@ -520,3 +515,18 @@ ul, ol {
|
|||||||
margin: 10px 0 10px 30px;
|
margin: 10px 0 10px 30px;
|
||||||
padding: 0;
|
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`
|
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}};
|
lua_code_cache ${{CODE_CACHE}};
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
lua_check_client_abort on;
|
||||||
default_type text/html;
|
default_type text/html;
|
||||||
content_by_lua_block {
|
content_by_lua_block {
|
||||||
require("lapis").serve("app")
|
require("lapis").serve("app")
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
/* src: */
|
|
||||||
|
|
||||||
@use "sass:color";
|
@use "sass:color";
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "body-text";
|
|
||||||
src: url("/static/fonts/DINish[slnt,wdth,wght].woff2") format("woff2");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "site-title";
|
font-family: "site-title";
|
||||||
src: url("/static/fonts/ChicagoFLF.woff2");
|
src: url("/static/fonts/ChicagoFLF.woff2");
|
||||||
@ -536,3 +529,18 @@ ul, ol {
|
|||||||
margin: 10px 0 10px 30px;
|
margin: 10px 0 10px 30px;
|
||||||
padding: 0;
|
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 Users = require("models").Users
|
||||||
local Posts = require("models").Posts
|
local Posts = require("models").Posts
|
||||||
local PostHistory = require("models").PostHistory
|
local PostHistory = require("models").PostHistory
|
||||||
|
local Threads = require("models").Threads
|
||||||
|
|
||||||
local babycode = require("lib.babycode")
|
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)
|
return util.s_split(sentences, ".", max_sentences or 2, true, false)
|
||||||
end
|
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)
|
function util.infobox_message(msg)
|
||||||
local sentences = util.split_sentences(msg)
|
local sentences = util.split_sentences(msg)
|
||||||
if #sentences == 1 then
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</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>
|
<script src="/static/js/thread.js"></script>
|
||||||
|
Loading…
Reference in New Issue
Block a user