Packages
phoenix_kit
1.7.26
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/dashboard/presence.ex
defmodule PhoenixKit.Dashboard.Presence do
@moduledoc """
Presence tracking for dashboard tabs.
Tracks which users are viewing which dashboard tabs, enabling features like:
- "2 users viewing" indicators on tabs
- Real-time user count updates
- Activity heatmaps
- User awareness for collaborative features
## Integration with PhoenixKit Presence
This module integrates with PhoenixKit's existing presence system to provide
dashboard-specific tracking while sharing the underlying infrastructure.
## Usage in LiveViews
def mount(_params, _session, socket) do
if connected?(socket) do
# Track user on this tab
PhoenixKit.Dashboard.Presence.track_tab(socket, :orders)
# Subscribe to presence updates
PhoenixKit.Dashboard.Presence.subscribe()
end
{:ok, assign(socket, tab_viewers: Presence.get_tab_viewers(:orders))}
end
def handle_info({:presence_diff, _diff}, socket) do
{:noreply, assign(socket, tab_viewers: Presence.get_tab_viewers(:orders))}
end
## Configuration
config :phoenix_kit, :dashboard_presence,
enabled: true,
show_user_count: true,
show_user_names: false, # Privacy setting
track_anonymous: false
"""
alias PhoenixKit.Dashboard.{Registry, Tab}
alias PhoenixKit.Users.Auth.Scope
# Suppress warnings about optional PhoenixKit.Presence module
@compile {:no_warn_undefined, PhoenixKit.Presence}
@presence_topic "phoenix_kit:dashboard:presence"
@tab_prefix "tab:"
@doc """
Tracks a user's presence on a specific dashboard tab.
## Options
- `:meta` - Additional metadata to track with the presence (default: %{})
- `:tab_path` - The full path of the tab (used for analytics)
## Examples
Presence.track_tab(socket, :orders)
Presence.track_tab(socket, :printers, meta: %{printer_id: 123})
"""
@spec track_tab(Phoenix.LiveView.Socket.t(), atom(), keyword()) ::
{:ok, String.t()} | {:error, term()}
def track_tab(socket, tab_id, opts \\ []) when is_atom(tab_id) do
if enabled?() and connected?(socket) do
scope = socket.assigns[:phoenix_kit_current_scope]
user = Scope.user(scope)
if user || track_anonymous?() do
meta =
%{
tab_id: tab_id,
tab_path: opts[:tab_path],
user_id: user && user.id,
user_email: user && user.email,
joined_at: DateTime.utc_now(),
online_at: DateTime.utc_now()
}
|> Map.merge(opts[:meta] || %{})
topic = tab_topic(tab_id)
case get_presence_module() do
nil ->
# Fallback: Use PubSub directly for basic tracking
track_via_pubsub(socket, topic, meta)
presence_mod when is_atom(presence_mod) ->
call_presence_track(
presence_mod,
socket.assigns.live_socket_id || socket.id,
topic,
user_key(user),
meta
)
end
else
{:ok, "anonymous_tracking_disabled"}
end
else
{:ok, "presence_disabled"}
end
end
@doc """
Untracks a user from a dashboard tab.
"""
@spec untrack_tab(Phoenix.LiveView.Socket.t(), atom()) :: :ok
def untrack_tab(socket, tab_id) when is_atom(tab_id) do
if enabled?() and connected?(socket) do
topic = tab_topic(tab_id)
scope = socket.assigns[:phoenix_kit_current_scope]
user = Scope.user(scope)
case get_presence_module() do
nil ->
# Fallback: Broadcast leave
Phoenix.PubSub.broadcast(PhoenixKit.PubSub, topic, {:presence_leave, user_key(user)})
presence_mod when is_atom(presence_mod) ->
call_presence_untrack(
presence_mod,
socket.assigns.live_socket_id || socket.id,
topic,
user_key(user)
)
end
end
:ok
end
@doc """
Gets all users currently viewing a specific tab.
## Options
- `:format` - Output format: :full (default), :count, :emails, :ids
## Examples
Presence.get_tab_viewers(:orders)
# => [%{user_id: 1, user_email: "user@example.com", online_at: ~U[...]}]
Presence.get_tab_viewers(:orders, format: :count)
# => 3
Presence.get_tab_viewers(:orders, format: :emails)
# => ["user@example.com", "admin@example.com"]
"""
@spec get_tab_viewers(atom(), keyword()) :: list() | integer()
def get_tab_viewers(tab_id, opts \\ []) do
format = opts[:format] || :full
topic = tab_topic(tab_id)
presences =
case get_presence_module() do
nil ->
# Fallback: Return empty (no presence tracking without Phoenix.Presence)
[]
presence_mod when is_atom(presence_mod) ->
call_presence_list(presence_mod, topic)
|> Enum.flat_map(fn {_key, %{metas: metas}} -> metas end)
end
case format do
:count -> length(presences)
:emails -> Enum.map(presences, & &1[:user_email]) |> Enum.reject(&is_nil/1) |> Enum.uniq()
:ids -> Enum.map(presences, & &1[:user_id]) |> Enum.reject(&is_nil/1) |> Enum.uniq()
:full -> presences
end
end
@doc """
Gets viewer counts for all tabs at once.
Returns a map of tab_id => count.
## Examples
Presence.get_all_tab_counts()
# => %{orders: 3, printers: 1, settings: 0}
"""
@spec get_all_tab_counts() :: map()
def get_all_tab_counts do
Registry.get_tabs()
|> Enum.filter(&Tab.navigable?/1)
|> Enum.map(fn tab ->
{tab.id, get_tab_viewers(tab.id, format: :count)}
end)
|> Map.new()
rescue
_ -> %{}
end
@doc """
Subscribes to presence updates for all dashboard tabs.
The subscriber will receive messages in the format:
- `{:presence_diff, %{joins: %{}, leaves: %{}}}` - When users join/leave
- `{:tab_viewers_updated, tab_id, count}` - Simplified count update
"""
@spec subscribe() :: :ok | {:error, term()}
def subscribe do
Phoenix.PubSub.subscribe(PhoenixKit.PubSub, @presence_topic)
end
@doc """
Subscribes to presence updates for a specific tab.
"""
@spec subscribe_tab(atom()) :: :ok | {:error, term()}
def subscribe_tab(tab_id) do
Phoenix.PubSub.subscribe(PhoenixKit.PubSub, tab_topic(tab_id))
end
@doc """
Broadcasts a tab viewer count update.
This is called automatically when presence changes, but can also be
called manually to force a refresh.
"""
@spec broadcast_tab_count(atom()) :: :ok
def broadcast_tab_count(tab_id) do
count = get_tab_viewers(tab_id, format: :count)
Phoenix.PubSub.broadcast(
PhoenixKit.PubSub,
@presence_topic,
{:tab_viewers_updated, tab_id, count}
)
:ok
rescue
_ -> :ok
end
@doc """
Gets the presence topic for a specific tab.
"""
@spec tab_topic(atom()) :: String.t()
def tab_topic(tab_id) when is_atom(tab_id) do
"#{@presence_topic}:#{@tab_prefix}#{tab_id}"
end
@doc """
Gets the main presence topic for all dashboard presence updates.
"""
@spec presence_topic() :: String.t()
def presence_topic, do: @presence_topic
@doc """
Checks if presence tracking is enabled.
"""
@spec enabled?() :: boolean()
def enabled? do
config = Application.get_env(:phoenix_kit, :dashboard_presence, [])
Keyword.get(config, :enabled, true)
end
@doc """
Checks if user counts should be shown.
"""
@spec show_user_count?() :: boolean()
def show_user_count? do
config = Application.get_env(:phoenix_kit, :dashboard_presence, [])
Keyword.get(config, :show_user_count, true)
end
@doc """
Checks if user names/emails should be shown.
"""
@spec show_user_names?() :: boolean()
def show_user_names? do
config = Application.get_env(:phoenix_kit, :dashboard_presence, [])
Keyword.get(config, :show_user_names, false)
end
@doc """
Checks if anonymous users should be tracked.
"""
@spec track_anonymous?() :: boolean()
def track_anonymous? do
config = Application.get_env(:phoenix_kit, :dashboard_presence, [])
Keyword.get(config, :track_anonymous, false)
end
# Private helpers
defp connected?(%{transport_pid: pid}) when is_pid(pid), do: true
defp connected?(_), do: false
defp user_key(nil), do: "anonymous_#{System.unique_integer([:positive])}"
defp user_key(user), do: "user:#{user.id}"
defp get_presence_module do
# Check if PhoenixKit.Presence exists and is configured
cond do
Code.ensure_loaded?(PhoenixKit.Presence) ->
PhoenixKit.Presence
Code.ensure_loaded?(Phoenix.Presence) ->
# Check for app-specific presence module
nil
true ->
nil
end
end
defp track_via_pubsub(socket, topic, meta) do
# Simple tracking using PubSub for environments without Phoenix.Presence
socket_id = socket.assigns[:live_socket_id] || socket.id
# Store in process dictionary as fallback
Process.put({:dashboard_presence, topic}, meta)
# Broadcast join
Phoenix.PubSub.broadcast(PhoenixKit.PubSub, topic, {:presence_join, socket_id, meta})
{:ok, socket_id}
rescue
_ -> {:error, :pubsub_unavailable}
end
# Wrapper functions to avoid apply/3 with known argument counts
defp call_presence_track(mod, socket_id, topic, key, meta) do
mod.track(socket_id, topic, key, meta)
end
defp call_presence_untrack(mod, socket_id, topic, key) do
mod.untrack(socket_id, topic, key)
end
defp call_presence_list(mod, topic) do
mod.list(topic)
end
end