Packages

phoenix_kit

1.7.21
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 modules publishing presence_helpers.ex
Raw

lib/modules/publishing/presence_helpers.ex

defmodule PhoenixKit.Modules.Publishing.PresenceHelpers do
@moduledoc """
Helper functions for collaborative post editing with Phoenix.Presence.
Provides utilities for tracking editing sessions, determining owner/spectator roles,
and syncing state between users.
"""
alias PhoenixKit.Modules.Publishing.Presence
@doc """
Tracks the current LiveView process in a Presence topic.
## Parameters
- `form_key`: The unique key for the post being edited
- `socket`: The LiveView socket
- `user`: The current user struct
## Examples
track_editing_session("blog:my-post/en.phk", socket, user)
# => {:ok, ref}
"""
def track_editing_session(form_key, socket, user) do
topic = editing_topic(form_key)
Presence.track(self(), topic, socket.id, %{
user_id: user.id,
user_email: user.email,
user: user,
joined_at: System.system_time(:millisecond),
phx_ref: socket.id,
pid: self(),
transport_pid: socket.transport_pid
})
end
@doc """
Untracks the current LiveView process from a Presence topic.
Call this when switching languages or versions to release the lock
on the previous form before tracking the new one.
## Parameters
- `form_key`: The unique key for the post that was being edited
- `socket`: The LiveView socket
## Examples
untrack_editing_session("blog:my-post:en", socket)
# => :ok
"""
def untrack_editing_session(form_key, socket) do
topic = editing_topic(form_key)
Presence.untrack(self(), topic, socket.id)
end
@doc """
Unsubscribes from presence events and editor form events for a form.
Call this when switching languages or versions to clean up subscriptions.
"""
def unsubscribe_from_editing(form_key) do
topic = editing_topic(form_key)
Phoenix.PubSub.unsubscribe(:phoenix_kit_internal_pubsub, topic)
end
@doc """
Determines if the current socket is the owner (first in the presence list).
Returns `{:owner, presences}` if this socket is the owner (or same user in different tab), or
`{:spectator, owner_meta, presences}` if a different user is the owner.
## Examples
case get_editing_role("blog:my-post", socket.id, current_user.id) do
{:owner, all_presences} ->
# I can edit!
{:spectator, owner_metadata, all_presences} ->
# I'm read-only, sync with owner's state
end
"""
def get_editing_role(form_key, socket_id, current_user_id) do
presences = get_sorted_presences(form_key)
case presences do
[] ->
# No one here (shouldn't happen since caller is here)
# But treat as owner to avoid blocking
{:owner, []}
[{^socket_id, _meta} | _rest] ->
# I'm first! I'm the owner
{:owner, presences}
[{_other_socket_id, owner_meta} | _rest] ->
# Check if same user (different tab) or different user
if owner_meta.user_id == current_user_id do
# Same user, different tab - treat as owner so both tabs can edit
{:owner, presences}
else
# Different user - spectator mode (FIFO locking)
{:spectator, owner_meta, presences}
end
end
end
@doc """
Gets all presences for a form, sorted by join time (FIFO).
Returns a list of tuples: `[{socket_id, metadata}, ...]`
"""
def get_sorted_presences(form_key) do
topic = editing_topic(form_key)
raw_presences = Presence.list(topic)
raw_presences
|> Enum.flat_map(fn {socket_id, %{metas: metas}} ->
# Filter out metas with dead PIDs
valid_metas =
Enum.filter(metas, fn meta ->
case Map.get(meta, :pid) do
pid when is_pid(pid) -> Process.alive?(pid)
# Keep metas without PID for backward compatibility
_ -> true
end
end)
# Take the first valid meta (most recent)
case valid_metas do
[meta | _] -> [{socket_id, meta}]
[] -> []
end
end)
|> Enum.sort_by(fn {_socket_id, meta} -> meta.joined_at end)
end
@doc """
Gets the lock owner's metadata, or nil if no one is editing.
"""
def get_lock_owner(form_key) do
case get_sorted_presences(form_key) do
[{_socket_id, meta} | _] -> meta
[] -> nil
end
end
@doc """
Gets all spectators (everyone except the first person).
Returns a list of metadata for spectators only.
"""
def get_spectators(form_key) do
case get_sorted_presences(form_key) do
[] -> []
[_owner | spectators] -> Enum.map(spectators, fn {_id, meta} -> meta end)
end
end
@doc """
Counts total number of people editing (owner + spectators).
"""
def count_editors(form_key) do
get_sorted_presences(form_key) |> length()
end
@doc """
Subscribes the current process to presence events for a form.
After subscribing, the process will receive:
- `%Phoenix.Socket.Broadcast{event: "presence_diff", ...}` when users join/leave
"""
def subscribe_to_editing(form_key) do
topic = editing_topic(form_key)
Phoenix.PubSub.subscribe(:phoenix_kit_internal_pubsub, topic)
end
@doc """
Generates the Presence topic name for a form.
## Examples
editing_topic("docs:my-post/en.phk")
# => "publishing_edit:docs:my-post/en.phk"
"""
def editing_topic(form_key), do: "publishing_edit:#{form_key}"
end