# User Notifications

<figure><img src="/files/4lfVon3j40lxE18B7SzR" alt=""><figcaption><p>Petal Pro with Notification Bell</p></figcaption></figure>

The Notification Bell Component is embedded in the [Side Bar layout](/petal-pro-documentation/fundamentals/layouts-and-menus.md#sidebar-layout) and [Stacked Layout](/petal-pro-documentation/fundamentals/layouts-and-menus.md#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="/files/o5Cjy2Y03mrPTdmpw9Ia" 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 %}

### Consuming via the Notification Bell

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 %}

### Consuming via a Live View

To consume the broadcast directly in a Live View, add a call to `PetalProWeb.Endpoint.subscribe` and a `handle_info` callback. The following example adjusts  `DashboadLive`:

```elixir
defmodule PetalProWeb.DashboardLive do
  @moduledoc false
  use PetalProWeb, :live_view

  alias PetalPro.Notifications

  @impl true
  def mount(_params, _session, socket) do
    current_user = socket.assigns.current_user

    if current_user do
      topic = Notifications.user_notifications_topic(current_user.id)
      PetalProWeb.Endpoint.subscribe(topic)
    end

    {:ok, assign(socket, page_title: gettext("Dashboard"))}
  end

  @impl true
  def handle_info(%{event: "notifications_updated"}, socket) do
    # Do something here...
    {:noreply, socket}
  end
end
```

### 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!


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.petal.build/petal-pro-documentation/fundamentals/user-notifications.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
