Packages
phoenix_kit
1.7.156
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/live/notifications_bell.ex
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"""
<div></div>
"""
end
def render(assigns) do
~H"""
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-circle" title="Notifications">
<div class="indicator">
<.icon name="hero-bell" class="w-5 h-5" />
<%= if @unread_count > 0 do %>
<span class="badge badge-xs badge-primary indicator-item">
{display_count(@unread_count)}
</span>
<% end %>
</div>
</label>
<div
tabindex="0"
class="dropdown-content z-[100] mt-3 w-80 max-w-[95vw] card card-compact bg-base-100 shadow-xl border border-base-200"
>
<div class="card-body p-0">
<div class="flex items-center justify-between px-4 py-3 border-b border-base-200">
<span class="font-semibold">Notifications</span>
<%= if @unread_count > 0 do %>
<button
type="button"
phx-click="mark_all_seen"
class="btn btn-ghost btn-xs"
>
Mark all seen
</button>
<% end %>
</div>
<%= if @recent == [] do %>
<div class="px-4 py-8 text-center text-sm text-base-content/60">
You're all caught up.
</div>
<% else %>
<ul class="menu menu-sm max-h-96 overflow-y-auto p-0">
<%= for n <- @recent do %>
<% view = Render.render(n) %>
<li class={[
"border-b border-base-200 last:border-b-0",
is_nil(n.seen_at) && "bg-primary/5"
]}>
<button
type="button"
phx-click="open_notification"
phx-value-uuid={n.uuid}
class="flex items-start gap-3 w-full px-4 py-3 hover:bg-base-200 text-left"
>
<.icon name={view.icon} class="w-5 h-5 mt-0.5 shrink-0 text-base-content/70" />
<div class="flex-1 min-w-0">
<p class={[
"text-sm",
is_nil(n.seen_at) && "font-semibold"
]}>
{view.text}
</p>
<p class="text-xs text-base-content/50 mt-0.5">
{relative_time(n.inserted_at)}
</p>
</div>
</button>
</li>
<% end %>
</ul>
<% end %>
</div>
</div>
</div>
"""
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