From b7e4d0348c917bbd12511918ab9e490919007cf1 Mon Sep 17 00:00:00 2001 From: Andrew Northall Date: Mon, 30 Oct 2023 10:49:35 +0000 Subject: [PATCH] Improve notifications system (#202) (#205) * Hide notification pane before page has fully loaded (#202) * Abstract notifications dropdown into own template * Add Alpine and HTMX Morph plugins * Add Notification middleware to automatically mark notifications as read --- app/static/css/core.css | 2 + app/static/js/alpine-morph-plugin.min.js | 1 + app/static/js/htmx-alpine-morph.js | 16 ++++++++ app/templates/_minimal.html | 4 +- app/templates/base.html | 41 +------------------ .../users/htmx_notifications_dropdown.html | 41 +++++++++++++++++++ app/users/middleware.py | 23 ++++++++++- app/users/urls.py | 5 +++ app/users/views.py | 4 ++ config/django/settings/base.py | 1 + 10 files changed, 97 insertions(+), 41 deletions(-) create mode 100644 app/static/js/alpine-morph-plugin.min.js create mode 100644 app/static/js/htmx-alpine-morph.js create mode 100644 app/templates/users/htmx_notifications_dropdown.html diff --git a/app/static/css/core.css b/app/static/css/core.css index 80487893..8e2c7542 100644 --- a/app/static/css/core.css +++ b/app/static/css/core.css @@ -234,6 +234,8 @@ body { cursor: pointer; } +[x-cloak] { display: none !important; } + .htmx-request { opacity: 0.5; transition: opacity 300ms linear; diff --git a/app/static/js/alpine-morph-plugin.min.js b/app/static/js/alpine-morph-plugin.min.js new file mode 100644 index 00000000..ddd9ffbf --- /dev/null +++ b/app/static/js/alpine-morph-plugin.min.js @@ -0,0 +1 @@ +(()=>{function E(d,r,u){Y();let m,h,b,B,O,_,v,T,A,C;function W(e={}){let n=a=>a.getAttribute("key"),o=()=>{};O=e.updating||o,_=e.updated||o,v=e.removing||o,T=e.removed||o,A=e.adding||o,C=e.added||o,b=e.key||n,B=e.lookahead||!1}function D(e,n){if(q(e,n))return I(e,n);let o=!1;if(!y(O,e,n,()=>o=!0)){if(e.nodeType===1&&window.Alpine&&window.Alpine.cloneNode(e,n),X(n)){$(e,n),_(e,n);return}o||G(e,n),_(e,n),L(e,n)}}function q(e,n){return e.nodeType!=n.nodeType||e.nodeName!=n.nodeName||g(e)!=g(n)}function I(e,n){if(y(v,e))return;let o=n.cloneNode(!0);y(A,o)||(e.replaceWith(o),T(e),C(o))}function $(e,n){let o=n.nodeValue;e.nodeValue!==o&&(e.nodeValue=o)}function G(e,n){if(e._x_transitioning||e._x_isShown&&!n._x_isShown||!e._x_isShown&&n._x_isShown)return;let o=Array.from(e.attributes),a=Array.from(n.attributes);for(let i=o.length-1;i>=0;i--){let t=o[i].name;n.hasAttribute(t)||e.removeAttribute(t)}for(let i=a.length-1;i>=0;i--){let t=a[i].name,x=a[i].value;e.getAttribute(t)!==x&&e.setAttribute(t,x)}}function L(e,n){let o=H(e.children),a={},i=V(n),t=V(e);for(;i;){let s=g(i),p=g(t);if(!t)if(s&&a[s]){let l=a[s];e.appendChild(l),t=l}else{if(!y(A,i)){let l=i.cloneNode(!0);e.appendChild(l),C(l)}i=c(n,i);continue}let S=l=>l&&l.nodeType===8&&l.textContent==="[if BLOCK]>l&&l.nodeType===8&&l.textContent==="[if ENDBLOCK]>0)l--;else if(N(f)&&l===0){t=f;break}t=f}let R=t;l=0;let j=i;for(;i;){let f=c(n,i);if(S(f))l++;else if(N(f)&&l>0)l--;else if(N(f)&&l===0){i=f;break}i=f}let z=i,J=new w(k,R),Q=new w(j,z);L(J,Q);continue}if(t.nodeType===1&&B&&!t.isEqualNode(i)){let l=c(n,i),k=!1;for(;!k&&l;)l.nodeType===1&&t.isEqualNode(l)&&(k=!0,t=K(e,i,t),p=g(t)),l=c(n,l)}if(s!==p){if(!s&&p){a[p]=t,t=K(e,i,t),a[p].remove(),t=c(e,t),i=c(n,i);continue}if(s&&!p&&o[s]&&(t.replaceWith(o[s]),t=o[s]),s&&p){let l=o[s];if(l)a[p]=t,t.replaceWith(l),t=l;else{a[p]=t,t=K(e,i,t),a[p].remove(),t=c(e,t),i=c(n,i);continue}}}let P=t&&c(e,t);D(t,i),i=i&&c(n,i),t=P}let x=[];for(;t;)y(v,t)||x.push(t),t=c(e,t);for(;x.length;){let s=x.shift();s.remove(),T(s)}}function g(e){return e&&e.nodeType===1&&b(e)}function H(e){let n={};for(let o of e){let a=g(o);a&&(n[a]=o)}return n}function K(e,n,o){if(!y(A,n)){let a=n.cloneNode(!0);return e.insertBefore(a,o),C(a),a}return n}return W(u),m=d,h=typeof r=="string"?U(r):r,window.Alpine&&window.Alpine.closestDataStack&&!d._x_dataStack&&(h._x_dataStack=window.Alpine.closestDataStack(d),h._x_dataStack&&window.Alpine.cloneNode(d,h)),D(d,h),m=void 0,h=void 0,d}E.step=()=>{};E.log=()=>{};function y(d,...r){let u=!1;return d(...r,()=>u=!0),u}var F=!1;function U(d){let r=document.createElement("template");return r.innerHTML=d,r.content.firstElementChild}function X(d){return d.nodeType===3||d.nodeType===8}var w=class{constructor(r,u){this.startComment=r,this.endComment=u}get children(){let r=[],u=this.startComment.nextSibling;for(;u&&u!==this.endComment;)r.push(u),u=u.nextSibling;return r}appendChild(r){this.endComment.before(r)}get firstChild(){let r=this.startComment.nextSibling;if(r!==this.endComment)return r}nextNode(r){let u=r.nextSibling;if(u!==this.endComment)return u}insertBefore(r,u){return u.before(r),r}};function V(d){return d.firstChild}function c(d,r){if(r._x_teleport)return r._x_teleport;let u;return d instanceof w?u=d.nextNode(r):u=r.nextSibling,u}function Y(){if(F)return;F=!0;let d=Element.prototype.setAttribute,r=document.createElement("div");Element.prototype.setAttribute=function(m,h){if(!m.includes("@"))return d.call(this,m,h);r.innerHTML=``;let b=r.firstElementChild.getAttributeNode(m);r.firstElementChild.removeAttributeNode(b),this.setAttributeNode(b)}}function M(d){d.morph=E}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(M)});})(); diff --git a/app/static/js/htmx-alpine-morph.js b/app/static/js/htmx-alpine-morph.js new file mode 100644 index 00000000..31d2e148 --- /dev/null +++ b/app/static/js/htmx-alpine-morph.js @@ -0,0 +1,16 @@ +htmx.defineExtension('alpine-morph', { + isInlineSwap: function (swapStyle) { + return swapStyle === 'morph'; + }, + handleSwap: function (swapStyle, target, fragment) { + if (swapStyle === 'morph') { + if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + Alpine.morph(target, fragment.firstElementChild); + return [target]; + } else { + Alpine.morph(target, fragment.outerHTML); + return [target]; + } + } + } +}); diff --git a/app/templates/_minimal.html b/app/templates/_minimal.html index c541357e..2a8cc685 100644 --- a/app/templates/_minimal.html +++ b/app/templates/_minimal.html @@ -30,13 +30,15 @@ + {% django_htmx_script %} - + + diff --git a/app/templates/base.html b/app/templates/base.html index c84a38b9..3d2b95dd 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -32,49 +32,12 @@ -
- - - {% if notifications.unread %} - - {{ notifications.unread }} - {% else %} - - {% endif %} - - -
-
- Notifications -
- - {% if notifications.list %} - {% for notification in notifications.list %} -
- {{ notification.get_message }} -
- - {{ notification.updated|timesince }} ago - -
- {% endfor %} + {% include "users/htmx_notifications_dropdown.html" %} - - {% else %} -
- You do not have any notifications. Oh well! -
- {% endif %} -
-
{% endif %} +