Packages

phoenix_kit

1.7.2
1.7.216 1.7.215 1.7.214 1.7.213 1.7.212 1.7.211 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
phoenix_kit lib phoenix_kit_web live modules blogging presence_helpers.ex
Raw

lib/phoenix_kit_web/live/modules/blogging/presence_helpers.ex

defmodule PhoenixKitWeb.Live.Modules.Blogging.PresenceHelpers do
@moduledoc """
Helper functions for collaborative blog post editing with Phoenix.Presence.
Provides utilities for tracking editing sessions, determining owner/spectator roles,
and syncing state between users.
"""
alias PhoenixKitWeb.Live.Modules.Blogging.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 """
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("blog:my-post/en.phk")
# => "blog_edit:blog:my-post/en.phk"
"""
def editing_topic(form_key), do: "blog_edit:#{form_key}"
end