changeset 40:7ea33179592a

email notification
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 27 Feb 2025 16:44:20 -0700
parents 471b13e6ce2c
children 818697418dbe
files src/account.html.luan src/active.js.luan src/add_post.js.luan src/chat.js src/images/notify.mp3 src/lib/Notify.luan src/lib/Shared.luan src/lib/User.luan src/lib/Utils.luan src/login.html.luan src/save_notify.js.luan src/site.css
diffstat 12 files changed, 234 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/src/account.html.luan	Sun Nov 17 12:06:08 2024 -0700
+++ b/src/account.html.luan	Thu Feb 27 16:44:20 2025 -0700
@@ -1,5 +1,7 @@
 local Luan = require "luan:Luan.luan"
 local error = Luan.error
+local Parsers = require "luan:Parsers.luan"
+local json_string = Parsers.json_string or error()
 local Io = require "luan:Io.luan"
 local Http = require "luan:http/Http.luan"
 local Shared = require "site:/lib/Shared.luan"
@@ -14,6 +16,7 @@
 return function()
 	local user = current_user()
 	if user == nil then return end
+	local notify_email = user.notify_email
 	Io.stdout = Http.response.text_writer()
 %>
 <!doctype html>
@@ -23,6 +26,34 @@
 		<script>
 			'use strict';
 
+			let notifyEmail = <%= json_string(notify_email or "") %>;
+			let multiNotify = <%= user.multi_notify %>;
+
+			function showNotify() {
+				let span = document.querySelector('span[notify]');
+				span.textContent = notifyEmail ? `Send notifications to ${notifyEmail}` : 'No notifications'
+			}
+
+			function editNotify() {
+				let dialog = document.querySelector('dialog[edit_notify]');
+				let input = dialog.querySelector('input[name=notify_email]');
+				input.value = notifyEmail;
+				let radio = dialog.querySelector(`input[type=radio][value=${multiNotify}]`);
+				radio.checked = true;
+				openModal(dialog);
+			}
+
+			function saveNotify() {
+				let dialog = document.querySelector('dialog[edit_notify]');
+				let input = dialog.querySelector('input[name=notify_email]');
+				notifyEmail = input.value;
+				let radio = dialog.querySelector('input[type=radio]:checked');
+				multiNotify = radio.value === 'true';
+				closeModal(input);
+				showNotify();
+				ajax(`save_notify.js?email=${encodeURIComponent(notifyEmail)}&multi=${multiNotify}`);
+			}
+
 			function deleteUser() {
 				let dialog = document.querySelector('dialog[delete_user]');
 				openModal(dialog);
@@ -32,6 +63,10 @@
 				closeModal(el);
 				ajax('delete_user.js');
 			}
+
+			function init() {
+				showNotify();
+			}
 		</script>
 		<style>
 			div[content] {
@@ -42,6 +77,12 @@
 			h1 {
 				text-align: center;
 			}
+			input[name=notify_email] {
+				width: 300px;
+			}
+			span[note] {
+				font-size: small;
+			}
 		</style>
 	</head>
 	<body>
@@ -50,9 +91,34 @@
 			<h1>Your Account</h1>
 			<p><a href="about.html">About Web Chat</a></p>
 			<p>Your URL: <%= base_url() %>/?with=<%=user.email%></p>
+			<p><span notify></span> <a href="javascript:editNotify()">Edit</a></p>
 			<p><a href="javascript:logout()">Logout</a></p>
 			<p><a href="javascript:deleteUser()">Delete account</a></p>
 		</div>
+		<dialog edit_notify>
+			<h2>Edit Notification</h2>
+			<form action="javascript:saveNotify()">
+				<p>
+					<label>Send notifications to</label><br> 
+					<input type=email name=notify_email><br>
+					<span note>Leave blank for no notifications</span>
+				</p>
+				<p>
+					<label clickable>
+						<input type=radio name=multi_notify value=false>
+						Notify only for first message
+					</label><br>
+					<label clickable>
+						<input type=radio name=multi_notify value=true>
+						Notify for all messages
+					</label>
+				</p>
+				<div buttons>
+					<button type=button cancel onclick="closeModal(this)">Cancel</button>
+					<button type=submit go>Save</button>
+				</div>
+			</form>
+		</dialog>
 		<dialog delete_user>
 			<h2>Delete Account</h2>
 			<p>Are you sure that you want to delete your account?</p>
@@ -61,6 +127,7 @@
 				<button go onclick="doDeleteUser(this)">Delete</button>
 			</div>
 		</dialog>
+		<script> init(); </script>
 	</body>
 </html>
 <%
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/active.js.luan	Thu Feb 27 16:44:20 2025 -0700
@@ -0,0 +1,11 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local User = require "site:/lib/User.luan"
+local current_user = User.current or error()
+local Notify = require "site:/lib/Notify.luan"
+
+
+return function()
+	local user = current_user() or error()
+	Notify.remove(user)
+end
--- a/src/add_post.js.luan	Sun Nov 17 12:06:08 2024 -0700
+++ b/src/add_post.js.luan	Thu Feb 27 16:44:20 2025 -0700
@@ -18,6 +18,7 @@
 local Shared = require "site:/lib/Shared.luan"
 local post_html = Shared.post_html or error()
 local http_push_to_users = Shared.http_push_to_users or error()
+local Notify = require "site:/lib/Notify.luan"
 
 
 return function()
@@ -38,6 +39,7 @@
 		chat.updated = now
 		chat.save()
 	end )
+	Notify.add(chat)
 	local html = `post_html(post)`
 	local js = "added("..json_string(html)..")"
 	chat.http_push(js)
--- a/src/chat.js	Sun Nov 17 12:06:08 2024 -0700
+++ b/src/chat.js	Thu Feb 27 16:44:20 2025 -0700
@@ -4,7 +4,6 @@
 let currentChatId = null;
 let eventSource;
 let lastUpdate;
-let hasUnseen = false;
 let userId;
 
 function evalEvent(event) {
@@ -153,6 +152,8 @@
 	input.insertAdjacentHTML('beforebegin',html);
 	fixPosts();
 	input.scrollIntoView({block: 'end'});
+	if( document.hasFocus() )
+		ajax('active.js');
 }
 
 function getChats(chatId,updated) {
@@ -163,10 +164,8 @@
 	}
 	if( updated )
 		lastUpdate = updated;
-	if( !document.hasFocus() && !hasUnseen ) {
+	if( !document.hasFocus() ) {
 		document.title = title + ' *';
-		notify();
-		hasUnseen = true;
 	}
 }
 
@@ -187,13 +186,13 @@
 window.onfocus = function() {
 	// console.log('onfocus');
 	document.title = title;
-	hasUnseen = false;
+	ajax('active.js');
 };
 
 let urlRegex = /(^|\s)(https?:\/\/\S+)/g;
 
 function urlsToLinks(text) {
-	return text.replace( urlRegex, '$1<a href="$2">$2</a>' );
+	return text.replace( urlRegex, '$1<a target="_blank" href="$2">$2</a>' );
 }
 
 let currentPulldown = null;
@@ -224,11 +223,6 @@
 	ajax(`heartbeat.js?last_update=${lastUpdate}`);
 }, 10000 );
 
-let sound = new Audio('/images/notify.mp3');
-function notify() {
-	sound.play();
-}
-
 let online = {};
 
 function setOnline(userId) {
Binary file src/images/notify.mp3 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/Notify.luan	Thu Feb 27 16:44:20 2025 -0700
@@ -0,0 +1,102 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local pairs = Luan.pairs or error()
+local stringify = Luan.stringify or error()
+local Time = require "luan:Time.luan"
+local time_now = Time.now or error()
+local Thread = require "luan:Thread.luan"
+local Http = require "luan:http/Http.luan"
+local Chat = require "site:/lib/Chat.luan"
+local User = require "site:/lib/User.luan"
+local get_user_by_id = User.get_by_id or error()
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+local Shared = require "site:/lib/Shared.luan"
+local send_mail = Shared.send_mail or error()
+local Utils = require "site:/lib/Utils.luan"
+local shallow_copy = Utils.shallow_copy or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "Notify"
+
+
+local Notify = {}
+
+local url = Http.domain and "https://"..Http.domain.."/" or "http://localhost:8080/"
+
+local wait = Time.period{seconds=10}
+
+local function set_notified(user,was_notified)
+	run_in_transaction( function()
+		user = user.reload()
+		user.was_notified = was_notified
+		user.save()
+	end )
+end
+
+local function init()
+	local users = {}
+	local fns = {}
+
+	function fns.add(user_ids)
+		local now = time_now()
+		for _, user_id in ipairs(user_ids) do
+			local user = get_user_by_id(user_id)
+			if users[user_id] == nil and user.notify_email ~= nil and (user.multi_notify or not user.was_notified) then
+				users[user_id] = now
+logger.info("add "..user_id)
+			end
+		end
+	end
+
+	function fns.remove(user_id)
+		users[user_id] = nil
+logger.info("remove "..user_id)
+	end
+
+	function fns.notify()
+logger.info("notify")
+		local now = time_now()
+		for user_id, when in pairs(shallow_copy(users)) do
+			if now - when > wait then
+				local user = get_user_by_id(user_id)
+logger.info("notify "..user.notify_email.." "..user_id)
+				send_mail {
+					From = "Web Chat <chat@luan.software>"
+					To = user.notify_email
+					Subject = "New Messages"
+					body = `%>
+You have received new messages.
+
+<%= url %>
+<%					`
+				}
+				users[user_id] = nil
+				set_notified(user,true)
+			end
+		end
+	end
+
+	return fns
+end
+
+local glob = Thread.global_callable("notify",init)
+
+function Notify.add(chat)
+	Thread.run(function()
+		glob.add(chat.user_ids)
+	end)
+end
+
+function Notify.remove(user)
+	Thread.run(function()
+		glob.remove(user.id)
+		if user.was_notified then
+			set_notified(user,false)
+		end
+	end)
+end
+
+Thread.schedule( glob.notify, { repeating_delay=Time.period{seconds=10} } )
+
+return Notify
--- a/src/lib/Shared.luan	Sun Nov 17 12:06:08 2024 -0700
+++ b/src/lib/Shared.luan	Thu Feb 27 16:44:20 2025 -0700
@@ -84,6 +84,7 @@
 end
 
 local send_mail = Mail.sender(Shared.config.mail_server).send
+Shared.send_mail = send_mail
 
 function Shared.send_mail_async(mail)
 	thread_run( function()
--- a/src/lib/User.luan	Sun Nov 17 12:06:08 2024 -0700
+++ b/src/lib/User.luan	Thu Feb 27 16:44:20 2025 -0700
@@ -33,6 +33,9 @@
 		id = doc.id
 		email = doc.user_email
 		password = doc.password
+		was_notified = doc.was_notified=="true"
+		notify_email = doc.notify_email
+		multi_notify = doc.multi_notify=="true"
 	}
 end
 
@@ -42,6 +45,9 @@
 		id = user.id
 		user_email = user.email
 		password = user.password
+		was_notified = user.was_notified and "true" or nil
+		notify_email = user.notify_email
+		multi_notify = user.multi_notify and "true" or nil
 	}
 end
 
@@ -53,6 +59,10 @@
 		user.id = doc.id
 	end
 
+	function user.reload()
+		return User.get_by_id(user.id) or error(user.id)
+	end
+
 	function user.delete()
 		run_in_transaction( function()
 			local id = user.id
@@ -135,6 +145,7 @@
 			user = User.new{
 				email = email
 				password = new_password()
+				notify_email = email
 			}
 			user.save()
 		end
--- a/src/lib/Utils.luan	Sun Nov 17 12:06:08 2024 -0700
+++ b/src/lib/Utils.luan	Thu Feb 27 16:44:20 2025 -0700
@@ -68,4 +68,12 @@
 	return false
 end
 
+function Utils.shallow_copy(t)
+	local rtn = {}
+	for key, val in pairs(t) do
+		rtn[key] = val
+	end
+	return rtn
+end
+
 return Utils
--- a/src/login.html.luan	Sun Nov 17 12:06:08 2024 -0700
+++ b/src/login.html.luan	Thu Feb 27 16:44:20 2025 -0700
@@ -21,6 +21,12 @@
 <html>
 	<head>
 <%		head() %>
+		<style>
+			input[name=email] {
+				width: 300px;
+				max-width: 100%;
+			}
+		</style>
 	</head>
 	<body>
 <%		header() %>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/save_notify.js.luan	Thu Feb 27 16:44:20 2025 -0700
@@ -0,0 +1,19 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Http = require "luan:http/Http.luan"
+local Db = require "site:/lib/Db.luan"
+local run_in_transaction = Db.run_in_transaction or error()
+local User = require "site:/lib/User.luan"
+local current_user = User.current or error()
+
+
+return function()
+	local email = Http.request.parameters.email or error()
+	local multi = Http.request.parameters.multi or error()
+	run_in_transaction( function()
+		local user = current_user() or error()
+		user.notify_email = email ~= "" and email or nil
+		user.multi_notify = multi == "true"
+		user.save()
+	end )
+end
--- a/src/site.css	Sun Nov 17 12:06:08 2024 -0700
+++ b/src/site.css	Thu Feb 27 16:44:20 2025 -0700
@@ -20,6 +20,8 @@
 
 button,
 img[onclick],
+label[clickable],
+input[type="radio"],
 input[type="submit"] {
 	cursor: pointer;
 }