defmodule LiveBeatsWeb.Presence do @moduledoc """ Provides presence tracking to channels and processes. See the [`Phoenix.Presence`](http://hexdocs.pm/phoenix/Phoenix.Presence.html) docs for more details. """ use Phoenix.Presence, otp_app: :live_beats, pubsub_server: LiveBeats.PubSub, presence: __MODULE__ @pubsub LiveBeats.PubSub use LiveBeatsWeb, :html alias LiveBeats.{Accounts, MediaLibrary} alias LiveBeatsWeb.Presence.BadgeComponent def track_profile_user(%MediaLibrary.Profile{} = profile, current_user_id) do track( self(), "proxy:" <> topic(profile), current_user_id, %{} ) end def untrack_profile_user(%MediaLibrary.Profile{} = profile, current_user_id) do untrack( self(), "proxy:" <> topic(profile), current_user_id ) end def init(_opts) do {:ok, %{}} end def handle_metas(topic, %{joins: joins, leaves: leaves}, presences, state) do for {user_id, presence} <- joins do user_data = %{user: presence.user, metas: Map.fetch!(presences, user_id)} local_broadcast(topic, {__MODULE__, %{user_joined: user_data}}) end for {user_id, presence} <- leaves do metas = case Map.fetch(presences, user_id) do {:ok, presence_metas} -> presence_metas :error -> [] end user_data = %{user: presence.user, metas: metas} local_broadcast(topic, {__MODULE__, %{user_left: user_data}}) end {:ok, state} end def list_profile_users(%MediaLibrary.Profile{} = profile) do list("proxy:" <> topic(profile)) end def subscribe(%MediaLibrary.Profile{} = profile) do Phoenix.PubSub.subscribe(@pubsub, topic(profile)) end def fetch(_topic, presences) do users = presences |> Map.keys() |> Accounts.get_users_map() |> Enum.into(%{}) for {key, %{metas: metas}} <- presences, into: %{} do {key, %{metas: metas, user: users[String.to_integer(key)]}} end end def listening_now(assigns) do count = Enum.count(assigns.presence_ids) assigns = assigns |> assign(:count, count) |> assign_new(:total_count, fn -> count end) ~H"""

Listening now (<%= @count %>)

<%= if @total_count > @count do %>

+ <%= @total_count - @count %> more

<% end %>
""" end defp local_broadcast("proxy:" <> topic, payload) do Phoenix.PubSub.local_broadcast(@pubsub, topic, payload) end defp topic(%MediaLibrary.Profile{} = profile) do "active_profiles:#{profile.user_id}" end end defmodule LiveBeatsWeb.Presence.BadgeComponent do use LiveBeatsWeb, :live_component #  https://fly.io/docs/reference/regions/ @region_names %{ "ams" => "Amsterdam, Netherlands", "atl" => "Atlanta, Georgia (US)", "cdg" => "Paris, France", "dfw" => "Dallas, Texas (US)", "ewr" => "Parsippany, NJ (US)", "fra" => "Frankfurt, Germany", "gru" => "Sao Paulo, Brazil", "hkg" => "Hong Kong", "iad" => "Ashburn, Virginia (US)", "lax" => "Los Angeles, California (US)", "lhr" => "London, United Kingdom", "maa" => "Chennai (Madras), India", "nrt" => "Tokyo, Japan", "ord" => "Chicago, Illinois (US)", "scl" => "Santiago, Chile", "sea" => "Seattle, Washington (US)", "sin" => "Singapore", "sjc" => "Sunnyvale, California (US)", "syd" => "Sydney, Australia", "yyz" => "Toronto, Canada" } def render(assigns) do ~H"""
  • <.link navigate={profile_path(@presence)} class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md truncate" >
    <%= @presence.username %> <%= if @ping do %>

    ping: <%= @ping %>ms

    <%= if @region do %> <% end %> <% end %>
  • """ end def mount(socket) do {:ok, socket, temporary_assigns: [presence: nil, ping: nil, region: nil]} end def update(%{action: {:ping, action}}, socket) do %{user: user, ping: ping, region: region} = action now = now_ms() # debounce other tabs sending valid ping frequency if now - socket.assigns.last_ping_at > 1000 do {:ok, assign(socket, presence: user, ping: ping, region: region, last_ping_at: now)} else {:ok, socket} end end def update(%{presence: nil}, socket), do: {:ok, socket} def update(assigns, socket) do {:ok, socket |> assign(id: assigns.id, presence: assigns.presence) |> assign_new(:pings, fn -> %{} end) |> assign_new(:regions, fn -> %{} end) |> assign_new(:last_ping_at, fn -> now_ms() end)} end defp now_ms, do: System.system_time(:millisecond) defp region_name(region), do: Map.get(@region_names, region) end