Hi.
I’m learning about Phoenix by trying to build an app where logged-in users would be able to vote and see each other responses in real time. Like a Sprint Planning Estimation session.
I was able to:
- sign in users with Google
- see who is online using Presence
- all using LiveView, no changes in JavaScript. I would like to keep it this way if possible
Question: where to save the votes so all users see each other votes in real time?
I could save in the user object, but I would prefer to save it in a separate thing so I can use the Presence module to see who is online in other parts of the app.
I would like to avoid saving the votes in the database because they are ephemeral and I don’t need their history.
This is what I have so far:
# lib/app_web/live/online/index.ex
defmodule AppWeb.OnlineLive do
use AppWeb, :live_view
def mount(params, session, socket) do
session_random_id = params["session_random_id"] # random string generated on page load
current_user = session["current_user"] # who is authenticated with Google
socket = stream(socket, :online_users, [])
|> assign(:current_user, current_user)
|> assign(:session_random_id, session_random_id)
socket =
if current_user && connected?(socket) do
# adding the current user to the list of users that are online
AppWeb.Presence.track_user(session_random_id, %{id: current_user.id, name: current_user.name})
# listing to the session channel to know who is online
AppWeb.Presence.subscribe(session_random_id)
stream(socket, :online_users, AppWeb.Presence.list_online_users(session_random_id))
else
socket
end
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<h2>Online users:</h2>
<ul id="online_users" phx-update="stream">
<li :for={{dom_id, %{id: id, user: user, metas: _metas}} <- @streams.online_users} id={dom_id}>
id: <%= id %> name: <%= user.name %>
</li>
</ul>
<h2>Click to vote</h2>
<button class="bg-zinc-100 rounded px-2 py-1 hover:bg-zinc-200" phx-click="vote" phx-value-option="2">2</button>
<button class="bg-zinc-100 rounded px-2 py-1 hover:bg-zinc-200" phx-click="vote" phx-value-option="3">3</button>
<button class="bg-zinc-100 rounded px-2 py-1 hover:bg-zinc-200" phx-click="vote" phx-value-option="5">5</button>
<button class="bg-zinc-100 rounded px-2 py-1 hover:bg-zinc-200" phx-click="vote" phx-value-option="8">8</button>
<button class="bg-zinc-100 rounded px-2 py-1 hover:bg-zinc-200" phx-click="vote" phx-value-option="13">13</button>
"""
end
@impl true
def handle_event("vote", %{"option" => option}, socket) do
# QUESTION: where to save the votes so all users see each other votes in real time?
# I could save in the user object, but I would prefer to save it in a separate thing/module/etc
{:noreply, socket}
end
def handle_info({AppWeb.Presence, {:join, presence}}, socket) do
{:noreply, stream_insert(socket, :online_users, presence)}
end
def handle_info({AppWeb.Presence, {:leave, presence}}, socket) do
if presence.metas == [] do
{:noreply, stream_delete(socket, :online_users, presence)}
else
{:noreply, stream_insert(socket, :online_users, presence)}
end
end
end
# lib/app_web/channels/presence.ex
defmodule AppWeb.Presence do
@moduledoc """
Provides presence tracking to channels and processes.
See the [`Phoenix.Presence`](https://hexdocs.pm/phoenix/Phoenix.Presence.html)
docs for more details.
"""
use Phoenix.Presence,
otp_app: :app,
pubsub_server: App.PubSub
def init(_opts) do
{:ok, %{}}
end
def fetch(_topic, presences) do
for {key, %{metas: [meta | metas]}} <- presences, into: %{} do
{key, %{metas: [meta | metas], id: meta.id, user: meta}}
end
end
def handle_metas(topic, %{joins: joins, leaves: leaves}, presences, state) do
for {user_id, presence} <- joins do
user_data = %{id: user_id, user: presence.user, metas: Map.fetch!(presences, user_id)}
msg = {__MODULE__, {:join, user_data}}
Phoenix.PubSub.local_broadcast(App.PubSub, "proxy:#{topic}", msg)
end
for {user_id, presence} <- leaves do
metas =
case Map.fetch(presences, user_id) do
{:ok, presence_metas} -> presence_metas
:error -> []
end
user_data = %{id: user_id, user: presence.user, metas: metas}
msg = {__MODULE__, {:leave, user_data}}
Phoenix.PubSub.local_broadcast(App.PubSub, "proxy:#{topic}", msg)
end
{:ok, state}
end
def list_online_users(session_random_id),
do: list(session_random_id) |> Enum.map(fn {_id, presence} -> presence end)
def track_user(session_random_id, user),
do: track(self(), session_random_id, user.id, user)
def subscribe(session_random_id),
do: Phoenix.PubSub.subscribe(App.PubSub, "proxy:#{session_random_id}")
end
Does anyone know where and how to save the votes?
Thank you for any insight.