Skip to content

Commit

Permalink
Improve notifications system (#202) (#205)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
anorthall committed Oct 30, 2023
1 parent 7fb94e3 commit b7e4d03
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 41 deletions.
2 changes: 2 additions & 0 deletions app/static/css/core.css
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ body {
cursor: pointer;
}

[x-cloak] { display: none !important; }

.htmx-request {
opacity: 0.5;
transition: opacity 300ms linear;
Expand Down
1 change: 1 addition & 0 deletions app/static/js/alpine-morph-plugin.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions app/static/js/htmx-alpine-morph.js
Original file line number Diff line number Diff line change
@@ -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];
}
}
}
});
4 changes: 3 additions & 1 deletion app/templates/_minimal.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@

<!-- HTMX -->
<script src="{% static "js/htmx.min.js" %}"></script>
<script src="{% static "js/htmx-alpine-morph.js" %}"></script>
{% django_htmx_script %}

<!-- Bootstrap v5.3.0 JS -->
<script src="{% static "js/bootstrap.bundle.min.js" %}"></script>

<!-- Alpine 3.12.2 JS -->
<script src="{% static "js/alpine.min.js" %}" defer></script>
<script defer src="{% static "js/alpine-morph-plugin.min.js" %}"></script>
<script defer src="{% static "js/alpine.min.js" %}"></script>

<!-- jQuery v3.7.0 JS -->
<script src="{% static "js/jquery-3.7.0.min.js" %}"></script>
Expand Down
41 changes: 2 additions & 39 deletions app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,49 +32,12 @@
</a>
</div>

<div x-data="{ open: false }">
<a class="nav-link" href="#">
<span class="icon-badge-container text-white" x-on:click="open = !open">
{% if notifications.unread %}
<i class="bi bi-bell-fill icon-badge-icon"></i>
<span class="icon-badge">{{ notifications.unread }}</span>
{% else %}
<i class="bi bi-bell"></i>
{% endif %}
</span>
</a>
<div class="notification-dropdown shadow-lg border border-2 bg-light" x-show="open" @click.outside="open = false" x-transition>
<div class="item title my-0">
Notifications
</div>

{% if notifications.list %}
{% for notification in notifications.list %}
<div data-href="{% url 'users:notification' pk=notification.pk %}" class="item text-primary">
<span{% if not notification.read %} class="fw-bold"{% endif %}>{{ notification.get_message }}</span>
<br />
<small class="text-muted">
{{ notification.updated|timesince }} ago
</small>
</div>
{% endfor %}
{% include "users/htmx_notifications_dropdown.html" %}

<div data-href="{% url 'users:notifications' %}" class="item text-primary">
<a href="{% url 'users:notifications' %}"
class="btn btn-primary">
See all notifications
</a>
</div>
{% else %}
<div class="item title fw-normal text-primary">
<span>You do not have any notifications. Oh well!</span>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>

<div class="container d-flex flex-row justify-content-center d-lg-none" id="navbarMobileMenu">
<ul class="d-flex flex-row justify-content-between w-100 px-0">
{% include "_navbar_links.html" %}
Expand Down
41 changes: 41 additions & 0 deletions app/templates/users/htmx_notifications_dropdown.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{# <div hx-target="this" hx-ext="alpine-morph" hx-swap="morph" hx-get="{% url 'users:htmx_notifications_dropdown' %}" hx-trigger="every 15s" x-data="{ open: false }"> #}
<div x-data="{ open: false }">
<a class="nav-link" href="#">
<span class="icon-badge-container text-white" x-on:click="open = !open">
{% if notifications.unread %}
<i class="bi bi-bell-fill icon-badge-icon"></i>
<span class="icon-badge">{{ notifications.unread }}</span>
{% else %}
<i class="bi bi-bell"></i>
{% endif %}
</span>
</a>
<div class="notification-dropdown shadow-lg border border-2 bg-light" x-show="open" @click.outside="open = false" x-cloak x-transition>
<div class="item title my-0">
Notifications
</div>

{% if notifications.list %}
{% for notification in notifications.list %}
<div data-href="{% url 'users:notification' pk=notification.pk %}" class="item text-primary">
<span{% if not notification.read %} class="fw-bold"{% endif %}>{{ notification.get_message }}</span>
<br />
<small class="text-muted">
{{ notification.updated|timesince }} ago
</small>
</div>
{% endfor %}

<div data-href="{% url 'users:notifications' %}" class="item text-primary">
<a href="{% url 'users:notifications' %}"
class="btn btn-primary">
See all notifications
</a>
</div>
{% else %}
<div class="item title fw-normal text-primary">
<span>You do not have any notifications. Oh well!</span>
</div>
{% endif %}
</div>
</div>
23 changes: 22 additions & 1 deletion app/users/middleware.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from zoneinfo import ZoneInfoNotFoundError

from django.utils import timezone
from users.models import CavingUser
from users.models import CavingUser, Notification


class DistanceUnitsMiddleware:
Expand Down Expand Up @@ -42,3 +42,24 @@ def __call__(self, request):
request.user.save(update_fields=["last_seen"])

return self.get_response(request)


class NotificationsMiddleware:
"""Mark a notification as read if a user goes on the page it links to."""

def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
if not request.user.is_authenticated:
return self.get_response(request)

unread_notifications = Notification.objects.filter(
user=request.user, read=False
)
for notification in unread_notifications:
if notification.get_url() == request.path:
notification.read = True
notification.save(update_fields=["read"])

return self.get_response(request)
5 changes: 5 additions & 0 deletions app/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
views.FriendRequestAcceptView.as_view(),
name="friend_request_accept",
),
path(
"notifications/dropdown/",
views.HTMXNotificationsDropdown.as_view(),
name="htmx_notifications_dropdown",
),
path(
"notifications/<int:pk>/",
views.NotificationRedirect.as_view(),
Expand Down
4 changes: 4 additions & 0 deletions app/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,10 @@ def form_valid(self, form):
return super().form_valid(form)


class HTMXNotificationsDropdown(LoginRequiredMixin, TemplateView):
template_name = "users/htmx_notifications_dropdown.html"


class NotificationsList(LoginRequiredMixin, ListView):
model = Notification
template_name = "users/notifications.html"
Expand Down
1 change: 1 addition & 0 deletions config/django/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ def env(name, default=None, force_type: Any = str):
"users.middleware.TimezoneMiddleware",
"users.middleware.LastSeenMiddleware",
"users.middleware.DistanceUnitsMiddleware",
"users.middleware.NotificationsMiddleware",
]


Expand Down

0 comments on commit b7e4d03

Please sign in to comment.