Packages

phoenix_kit

1.7.86
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
phoenix_kit lib phoenix_kit dashboard presence.ex
Raw

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.PubSubHelper
alias PhoenixKit.Users.Auth.Scope
alias PhoenixKit.Utils.Date, as: UtilsDate
# 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_uuid: user && user.uuid,
user_email: user && user.email,
joined_at: UtilsDate.utc_now(),
online_at: UtilsDate.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(
PubSubHelper.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_uuid: "019...", 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_uuid]) |> 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(PubSubHelper.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(PubSubHelper.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(
PubSubHelper.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 """
Clears the cached presence module detection.
Call this after hot code reloading if the presence module configuration
has changed. The next call to any presence function will re-detect.
"""
@spec clear_presence_module_cache() :: :ok
def clear_presence_module_cache do
:persistent_term.erase({__MODULE__, :presence_module})
:ok
rescue
# Ignore if key doesn't exist
ArgumentError -> :ok
end
@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.uuid}"
# Cache key for persistent_term storage
@presence_module_cache_key {__MODULE__, :presence_module}
defp get_presence_module do
# Use persistent_term to cache the result of Code.ensure_loaded?
# This avoids ~180+ code_server calls per second on busy dashboards
case :persistent_term.get(@presence_module_cache_key, :not_cached) do
:not_cached ->
module = determine_presence_module()
:persistent_term.put(@presence_module_cache_key, module)
module
cached_module ->
cached_module
end
end
defp determine_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(PubSubHelper.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