# User Notifications

<figure><img src="https://2011413909-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fng2vid5a0N74u3tEdCZc%2Fuploads%2FOXtLF9ooz6b548uzGJBN%2FXnapper-2024-07-31-22.04.22.png?alt=media&#x26;token=2d3afe95-8d77-43c7-8713-922b0e87cd94" alt=""><figcaption><p>Petal Pro with Notification Bell</p></figcaption></figure>

The Notification Bell Component is embedded in the [Side Bar layout](https://docs.petal.build/petal-pro-documentation/v2.0.0/layouts-and-menus#sidebar-layout) and [Stacked Layout](https://docs.petal.build/petal-pro-documentation/v2.0.0/layouts-and-menus#stacked-layout) via `core_components.ex`. If you want to omit the Notification Bell Component, you can use the `:show_notification_bell` attribute:

```markup
<.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:

```markup
<.live_component
  module={PetalProWeb.NotificationBellComponent}
  id={PetalProWeb.NotificationBellComponent.lc_id()}
  current_user={@current_user}
/>
```

### Sending Notifications <a href="#sending_notifications" id="sending_notifications"></a>

Here’s an example of how you could send an Org invitation notification for a User:

```elixir
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](#consuming_broadcast_messages)).

### Viewing Notifications <a href="#viewing_notifications" id="viewing_notifications"></a>

Notifications for the currently authenticated user are visible via the Notification Bell Component:

<figure><img src="https://2011413909-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fng2vid5a0N74u3tEdCZc%2Fuploads%2FAIcZlBWn1Q5b4j65P9rf%2FXnapper-2024-07-31-22.29.40.png?alt=media&#x26;token=939bc86d-1dd3-4c0d-8121-59ad43ce1a90" alt=""><figcaption><p>Unread notification</p></figcaption></figure>

The Notification Bell Component comes with its own event handlers. This is why it’s implemented as a Live Component:

```markup
<.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](#the_read_path) matches.

### Consuming Broadcast Messages <a href="#consuming_broadcast_messages" id="consuming_broadcast_messages"></a>

The `UserNotificationChannel` is a [Channel](https://hexdocs.pm/phoenix/channels.html) that is configured in `user_socket.ex`:

```elixir
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`:

```elixir
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](https://hexdocs.pm/phoenix/channels.html#client-libraries) that supports Phoenix Channels.

{% hint style="info" %}
Broadcast messages can only be created and consumed by authenticated users. See `user_socket.ex` if you want to see how this works.
{% endhint %}

In the case of Petal Pro, the Notification Bell Component includes code that intercepts broadcast messages. This is achieved via a JavaScript Hook:&#x20;

1. The `NotificationBellHook` consumes broadcast messages client-side&#x20;
2. If a broadcast message is raised it pushes an event to the Notification Bell Component
3. The event causes the Notification Bell Component to update itself

{% hint style="info" %}
Though data is sent via the Channel, ultimately the data that is displayed by the Notification Bell Component is loaded from the database (not the Channel).
{% endhint %}

### The Read Path <a href="#the_read_path" id="the_read_path"></a>

If you look at `UserNotificationAttrs.invite_to_org_notification/3`:

```elixir
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).

### Creating your own notification <a href="#creating_your_own_notification" id="creating_your_own_notification"></a>

If you want to create your own user notification, you'll need to implement the following:

1. Create a helper function in `user_notification_attrs.ex`
2. Add a `notification_type` to `user_notification.ex`
3. Add code to create/broadcast the new message&#x20;
4. Add a `notification_item` to `notification_components.ex`

Let's create a notification for a user when they have been promoted to org admin!

#### Create the helper function

In `user_notification_attrs.ex`, add  the following function:

```elixir
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.

#### Add the Notification Type

In `user_notification.ex`, add a new Notification Type:

```elixir
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.

#### Add code to broadcast the message

In `orgs.ex` modify `update_membership/2`:

```elixir
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
```

{% hint style="info" %}
You'll need to update any call to `update_membership`. E.g:

```elixir
@impl true
def handle_event("save", %{"membership" => params}, socket) do
  current_user = socket.assigns.current_user
  membership = socket.assigns.membership
  
  case Orgs.update_membership(current_user, membership, params) do
    ...
  end
end
```

{% endhint %}

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.

#### Add a Notification Item

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`:

```elixir
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!
