defmodule PhoenixKitWeb.Live.NotificationsBell do @moduledoc """ Nested LiveView that renders the notifications bell in the global layout. Embedded from `layout_wrapper.ex` via `live_render/3` with `sticky: true` so it keeps its socket across page navigations and its own PubSub subscription stays live. It owns its `handle_info` callbacks for `{:notification_created, _}` / `{:notification_seen, _}` / `{:notification_dismissed, _}` / `{:notifications_bulk_updated, _}`, refreshing the badge count and dropdown contents on each event. Not routable on its own — mount expects `user_uuid` in the nested session. """ use Phoenix.LiveView, layout: false use Gettext, backend: PhoenixKitWeb.Gettext import PhoenixKitWeb.Components.Core.Icon, only: [icon: 1] alias PhoenixKit.Notifications alias PhoenixKit.Notifications.Events alias PhoenixKit.Notifications.Render def mount(_params, %{"user_uuid" => user_uuid}, socket) when is_binary(user_uuid) do if connected?(socket), do: Events.subscribe(user_uuid) {:ok, socket |> assign(:user_uuid, user_uuid) |> refresh()} end # Graceful fallback for embeddings without a user session — renders an # empty element so the layout doesn't crash for anonymous visitors. def mount(_params, _session, socket) do {:ok, assign(socket, :user_uuid, nil)} end # ── Events ───────────────────────────────────────────────────────── def handle_event("open_notification", %{"uuid" => uuid}, socket) do user_uuid = socket.assigns.user_uuid case Notifications.mark_seen(user_uuid, uuid) do {:ok, notification} -> case Render.render(notification).link do nil -> {:noreply, refresh(socket)} target -> {:noreply, push_navigate(socket, to: target)} end _ -> {:noreply, refresh(socket)} end end def handle_event("dismiss", %{"uuid" => uuid}, socket) do Notifications.dismiss(socket.assigns.user_uuid, uuid) {:noreply, refresh(socket)} end def handle_event("mark_all_seen", _params, socket) do Notifications.mark_all_seen(socket.assigns.user_uuid) {:noreply, refresh(socket)} end # ── PubSub ───────────────────────────────────────────────────────── def handle_info({event, _payload}, socket) when event in [ :notification_created, :notification_seen, :notification_dismissed, :notifications_bulk_updated ] do {:noreply, refresh(socket)} end def handle_info(_msg, socket), do: {:noreply, socket} # ── Render ───────────────────────────────────────────────────────── def render(%{user_uuid: nil} = assigns) do ~H"""
""" end def render(assigns) do ~H""" """ end # ── Helpers ──────────────────────────────────────────────────────── defp refresh(socket) do user_uuid = socket.assigns.user_uuid socket |> assign(:unread_count, Notifications.count_unread(user_uuid)) |> assign(:recent, Notifications.recent_for_user(user_uuid, 10)) end defp display_count(n) when n > 99, do: "99+" defp display_count(n), do: Integer.to_string(n) defp relative_time(%DateTime{} = dt) do seconds = DateTime.diff(DateTime.utc_now(), dt) cond do seconds < 60 -> "just now" seconds < 3600 -> "#{div(seconds, 60)}m ago" seconds < 86_400 -> "#{div(seconds, 3600)}h ago" seconds < 604_800 -> "#{div(seconds, 86_400)}d ago" true -> Calendar.strftime(dt, "%b %d") end end defp relative_time(_), do: "" end