add notification for new post in thread
This commit is contained in:
		
							
								
								
									
										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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user