User Notifications
A realtime notification/broadcast system for authenticated users.
Was this helpful?
A realtime notification/broadcast system for authenticated users.
Was this helpful?
The Notification Bell Component is embedded in the Side Bar layout and Stacked Layout via core_components.ex
. If you want to omit the Notification Bell Component, you can use the :show_notification_bell
attribute:
<.layout show_notification_bell={false} current_page={:dashboard} current_user={@current_user} type="sidebar">
<.container max_width="xl">
<div>content</div>
</.container>
</.layout>
You can add the Notification Bell Component anywhere you like with the following code:
<.live_component
module={PetalProWeb.NotificationBellComponent}
id={PetalProWeb.NotificationBellComponent.lc_id()}
current_user={@current_user}
/>
Hereβs an example of how you could send an Org invitation notification for a User:
alias PetalPro.Notifications
alias PetalPro.Notifications.UserNotificationAttrs
attrs = UserNotificationAttrs.invite_to_org_notification(org, sender_id, recipient_id)
case Notifications.create_user_notification(attrs) do
{:ok, notification} ->
Notifications.broadcast_user_notification(notification)
{:error, changeset} ->
raise "Failed to create notification: #{inspect(changeset)}"
end
create_user_notification/1
will create and store the notification.
broadcast_user_notification/1
will send a message to the PetalProWeb.UserNotificationChannel
.
Devices consuming the UserNotificationChannel
will use the information to update the user. For example, any browser tab that includes the Notification Bell Component will automatically update itself (see Consuming Broadcast Messages).
Notifications for the currently authenticated user are visible via the Notification Bell Component:
The Notification Bell Component comes with its own event handlers. This is why itβs implemented as a Live Component:
<.live_component
module={PetalProWeb.NotificationBellComponent}
id={PetalProWeb.NotificationBellComponent.lc_id()}
current_user={@current_user}
/>
Clicking on a notification will navigate the user to a nominated route. The :require_authenticated_user
hook has been updated to mark notifications read if the read path matches.
The UserNotificationChannel
is a Channel that is configured in user_socket.ex
:
defmodule PetalProWeb.UserSocket do
use Phoenix.Socket
channel "user_notifications:*", PetalProWeb.UserNotificationsChannel
...
end
If you send a broadcast using broadcast_user_notification/1
, it will create a Topic with the users' id
:
defmodule PetalPro.Notifications do
def user_notifications_topic(user_id) when not is_nil(user_id),
do: "user_notifications:#{user_id}"
def broadcast_user_notification(%UserNotification{
id: notification_id,
type: notification_type,
recipient_id: recipient_id
}) do
PetalProWeb.Endpoint.broadcast(
user_notifications_topic(recipient_id),
"notifications_updated",
%{id: notification_id, type: notification_type}
)
end
end
You can then consume this broadcast using any device (e.g. browser or mobile) using a client library that supports Phoenix Channels.
In the case of Petal Pro, the Notification Bell Component includes code that intercepts broadcast messages. This is achieved via a JavaScript Hook:
The NotificationBellHook
consumes broadcast messages client-side
If a broadcast message is raised it pushes an event to the Notification Bell Component
The event causes the Notification Bell Component to update itself
If you look at UserNotificationAttrs.invite_to_org_notification/3
:
def invite_to_org_notification(%Org{} = org, sender_id, recipient_id) do
%{
read_path: ~p"/app/users/org-invitations",
type: :invited_to_org,
recipient_id: recipient_id,
sender_id: sender_id,
org_id: org.id,
message: gettext("You have been invited to join the %{org_name} organisation!", org_name: org.name)
}
end
Youβll see that it has a read_path
property. This must be a verified route - if the user navigates to this page, then the notification is marked as read (handled in the :require_authenticated_user
on_mount
hook).
If you want to create your own user notification, you'll need to implement the following:
Create a helper function in user_notification_attrs.ex
Add a notification_type
to user_notification.ex
Add code to create/broadcast the new message
Add a notification_item
to notification_components.ex
Let's create a notification for a user when they have been promoted to org admin!
In user_notification_attrs.ex
, add the following function:
defmodule PetalPro.Notifications.UserNotificationAttrs do
...
def promote_to_org_admin(%Org{} = org, sender_id, recipient_id) do
%{
read_path: ~p"/app/org/#{org.slug}/team",
type: :promoted_to_org_admin,
recipient_id: recipient_id,
sender_id: sender_id,
org_id: org.id,
message: gettext("You have been promoted to org admin for %{org_name}!", org_name: org.name)
}
end
...
end
Note that the read_path
points to the org/team route - a path that would be inaccessible if the user was not an org admin.
In user_notification.ex
, add a new Notification Type:
defmodule PetalPro.Notifications.UserNotification do
...
@notification_types [:invited_to_org, :promoted_to_org_admin]
...
end
This adds :promoted_to_org_admin
as an allowed type.
In orgs.ex
modify update_membership/2
:
defmodule PetalPro.Orgs do
...
def update_membership(org_admin, %Membership{} = membership, attrs) do
changeset = Membership.update_changeset(membership, attrs)
org = get_org_by_id!(membership.org_id)
Ecto.Multi.new()
|> Ecto.Multi.update(:membership, changeset)
|> maybe_create_admin_notification(org, org_admin, changeset)
|> Repo.transaction()
|> case do
{:ok, %{membership: membership} = result} ->
result[:user_notification] && Notifications.broadcast_user_notification(result[:user_notification])
{:ok, membership}
{:error, :membership, error, _} ->
{:error, error}
{:error, :user_notification, error, _} ->
{:error, error}
{:error, error} ->
{:error, error}
end
end
defp maybe_create_admin_notification(multi, org, org_admin, changeset) do
promoted = Ecto.Changeset.changed?(changeset, :role, to: :admin)
if promoted do
recipient_id = Ecto.Changeset.get_field(changeset, :user_id)
user_notification_attrs = UserNotificationAttrs.promote_to_org_admin(org, org_admin.id, recipient_id)
maybe_create_user_notification_multi(multi, user_notification_attrs, recipient_id)
else
multi
end
end
...
end
This should allow the notification to be created/broadcast whenever a user's membership role has been changed to :admin
. However, the Notification Bell Component needs to be updated to allow for the new Notification Type.
Finally, the last thing we need to do is update the item template for the Notification Bell Component. You can do this by updating notification_components.ex
:
defmodule PetalProWeb.Notifications.Components do
...
def notification_item(%{notification: %UserNotification{type: :promoted_to_org_admin}} = assigns) do
~H"""
<.link
href={"#{@notification.read_path}"}
class="flex p-2 transition-colors rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
>
<.avatar
random_color
name={user_name(@notification.sender)}
src={user_avatar_url(@notification.sender)}
class="flex-shrink-0"
/>
<div class="flex flex-col my-auto ml-4 space-y-1 text-sm text-gray-700 dark:text-gray-100">
<p>
<%= gettext("%{name} has promoted you to admin for the organisation %{org_name}.",
name: "<span class='font-medium'>#{get_sender_name(@notification.sender)}</span>",
org_name: "<span class='font-medium'>#{@notification.org.name}</span>"
)
|> raw() %>
</p>
<p class="text-xs text-gray-500"><%= humanized_time_since(@notification.inserted_at) %></p>
</div>
<span class="flex-grow" />
<span
:if={is_nil(@notification.read_at)}
class="flex-shrink-0 w-2 h-2 my-auto mr-2 bg-red-500 rounded-full"
/>
</.link>
"""
end
...
end
This pattern matches on the new Notification Type :promoted_to_org_admin
. If we didn't add the new call to notification_item
, then any page with the Notification Bell will cause an error.
Just like the :invited_to_org
notification, if the user clicks on the notification to navigate to the page, then the notification will be marked as read!