1
0
mirror of https://github.com/danbee/chess synced 2025-03-04 08:39:06 +00:00

Implement presence and fix email test

This commit is contained in:
Daniel Barber 2023-11-08 17:17:38 -06:00
parent e30627b3ac
commit d03b82ecfa
9 changed files with 89 additions and 87 deletions

View File

@ -17,6 +17,6 @@ const liveSocket = new LiveSocket(
liveSocket.connect();
liveSocket.enableDebug();
// liveSocket.enableDebug();
window.liveSocket = liveSocket;

View File

@ -65,7 +65,7 @@ defmodule Chess.Board do
})
end
def castling_move(board, %{"from" => [4, rank], "to" => [6, _rank]}) do
def castling_move(board, %{from: {4, rank}, to: {6, _rank}}) do
move_piece(board, %{
from: {7, rank},
to: {5, rank}

View File

@ -19,8 +19,8 @@ defmodule Chess.Emails do
end
def opponent_moved_email(socket, game) do
user = Repo.get(User, socket.assigns.user_id)
opponent = opponent(game, socket.assigns.user_id)
user = Repo.get(User, socket.assigns.user.id)
opponent = opponent(game, socket.assigns.user.id)
new_email()
|> to(opponent)

View File

@ -1,73 +1,4 @@
defmodule ChessWeb.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.
## Usage
Presences can be tracked in your channel after joining:
defmodule Chess.MyChannel do
use ChessWeb, :channel
alias Chess.Presence
def join("some:topic", _params, socket) do
send(self, :after_join)
{:ok, assign(socket, :user_id, ...)}
end
def handle_info(:after_join, socket) do
push socket, "presence_state", Presence.list(socket)
{:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
online_at: inspect(System.system_time(:second))
})
{:noreply, socket}
end
end
In the example above, `Presence.track` is used to register this
channel's process as a presence for the socket's user ID, with
a map of metadata. Next, the current presence list for
the socket's topic is pushed to the client as a `"presence_state"` event.
Finally, a diff of presence join and leave events will be sent to the
client as they happen in real-time with the "presence_diff" event.
See `Phoenix.Presence.list/2` for details on the presence datastructure.
## Fetching Presence Information
The `fetch/2` callback is triggered when using `list/1`
and serves as a mechanism to fetch presence information a single time,
before broadcasting the information to all channel subscribers.
This prevents N query problems and gives you a single place to group
isolated data fetching to extend presence metadata.
The function receives a topic and map of presences and must return a
map of data matching the Presence datastructure:
%{"123" => %{metas: [%{status: "away", phx_ref: ...}],
"456" => %{metas: [%{status: "online", phx_ref: ...}]}
The `:metas` key must be kept, but you can extend the map of information
to include any additional information. For example:
def fetch(_topic, entries) do
users = entries |> Map.keys() |> Accounts.get_users_map(entries)
# => %{"123" => %{name: "User 123"}, "456" => %{name: nil}}
for {key, %{metas: metas}} <- entries, into: %{} do
{key, %{metas: metas, user: users[key]}}
end
end
The function above fetches all users from the database who
have registered presences for the given topic. The fetched
information is then extended with a `:user` key of the user's
information, while maintaining the required `:metas` field from the
original presence data.
"""
use Phoenix.Presence,
otp_app: :chess,
pubsub_server: Chess.PubSub

View File

@ -1,7 +1,11 @@
<div class="game-info">
<p>
Playing <%= opponent(@game, @user.id).name %>
<img class="game-info__opponent-status" src="/images/eye-closed.svg" alt="offline">
<%= if Map.has_key?(@presence, opponent(@game, @user.id).id) do %>
<img class="game-info__opponent-status" src="/images/eye-open.svg" alt="online">
<% else %>
<img class="game-info__opponent-status" src="/images/eye-closed.svg" alt="offline">
<% end %>
</p>
</div>

View File

@ -2,6 +2,7 @@ defmodule ChessWeb.GameView do
use ChessWeb, :view
alias Chess.GameState
alias Chess.Repo
import Phoenix.Component
import Chess.Auth, only: [current_user: 1]
@ -88,6 +89,14 @@ defmodule ChessWeb.GameView do
end
end
def opponent_id(game, user_id) do
if game.user_id == user_id do
game.opponent_id
else
game.user_id
end
end
def opponent(game, user_id) do
if game.user_id == user_id do
game.opponent

View File

@ -1,6 +1,8 @@
defmodule ChessWeb.BoardLive do
use Phoenix.LiveView, container: {:div, class: "board__container"}
alias Chess.Emails
alias Chess.Mailer
alias Chess.Store.User
alias Chess.Store.Game
alias Chess.Store.Move
@ -8,6 +10,7 @@ defmodule ChessWeb.BoardLive do
alias Chess.Moves
alias ChessWeb.GameView
alias ChessWeb.Presence
import Ecto.Query
@ -25,6 +28,7 @@ defmodule ChessWeb.BoardLive do
game =
Game.for_user(user)
|> Repo.get!(game_id)
|> Repo.preload([:user, :opponent])
{:ok, assign(socket, default_assigns(game, user))}
end
@ -51,10 +55,14 @@ defmodule ChessWeb.BoardLive do
{:noreply, assign(socket, state)}
end
def handle_info(%{event: "presence_diff", payload: _params}, socket) do
{:noreply, socket}
end
defp handle_click(socket, file, rank) do
game = socket.assigns[:game]
game = socket.assigns.game
board = game.board
user = socket.assigns[:user]
user = socket.assigns.user
colour = GameView.player_colour(user, game)
@ -65,7 +73,7 @@ defmodule ChessWeb.BoardLive do
handle_selection(board, colour, file, rank)
_ ->
handle_move(socket.assigns, file, rank)
handle_move(socket, file, rank)
end
end
@ -85,17 +93,16 @@ defmodule ChessWeb.BoardLive do
end
end
defp handle_move(
%{game: game, available: available, selected: selected},
file,
rank
) do
defp handle_move(socket, file, rank) do
%{game: game, available: available, selected: selected} = socket.assigns
if {file, rank} in available do
game
|> Moves.make_move(%{from: selected, to: {file, rank}})
|> case do
{:ok, %{game: game}} ->
game
|> Repo.reload()
|> Repo.preload([:user, :opponent])
|> Repo.preload(
moves:
@ -106,6 +113,8 @@ defmodule ChessWeb.BoardLive do
)
|> broadcast_move(game.board)
email_move(socket, game)
[
{:selected, nil},
{:available, []},
@ -118,6 +127,24 @@ defmodule ChessWeb.BoardLive do
end
end
defp email_move(socket, game) do
opponent_id =
GameView.opponent_id(game, socket.assigns.user.id)
|> Integer.to_string()
"game:#{game.id}"
|> Presence.list()
|> case do
%{^opponent_id => _} ->
nil
_ ->
socket
|> Emails.opponent_moved_email(game)
|> Mailer.deliver_later()
end
end
defp broadcast_move(game, board) do
ChessWeb.Endpoint.broadcast_from(
self(),

View File

@ -5,6 +5,7 @@ defmodule ChessWeb.GameInfoLive do
alias Chess.Store.Game
alias Chess.Store.Move
alias Chess.Repo
alias ChessWeb.Presence
import Ecto.Query
@ -15,7 +16,9 @@ defmodule ChessWeb.GameInfoLive do
end
def mount(_params, %{"game_id" => game_id, "user_id" => user_id}, socket) do
ChessWeb.Endpoint.subscribe("game:#{game_id}")
topic = "game:#{game_id}"
ChessWeb.Endpoint.subscribe(topic)
user = Repo.get!(User, user_id)
@ -32,10 +35,38 @@ defmodule ChessWeb.GameInfoLive do
)
|> Repo.get!(game_id)
{:ok, assign(socket, game: game, user: user)}
Presence.track(self(), topic, :user, %{id: user_id})
{:ok, assign(socket, game: game, user: user, presence: presence_list(topic))}
end
def handle_info(%{event: "move", payload: state}, socket) do
{:noreply, assign(socket, state)}
end
def handle_info(
%{event: "presence_diff", payload: %{joins: joins, leaves: leaves}},
socket
) do
{:noreply, socket |> handle_joins(joins) |> handle_leaves(leaves)}
end
defp presence_list(topic) do
Presence.list(topic)
|> Map.get("user")
|> Map.get(:metas)
|> Map.new(fn meta -> {meta.id, meta} end)
end
defp handle_joins(socket, joins) do
Enum.reduce(joins, socket, fn {"user", %{metas: [meta | _]}}, socket ->
assign(socket, :presence, Map.put(socket.assigns.presence, meta.id, meta))
end)
end
defp handle_leaves(socket, leaves) do
Enum.reduce(leaves, socket, fn {"user", %{metas: [meta | _]}}, socket ->
assign(socket, :presence, Map.delete(socket.assigns.presence, meta.id))
end)
end
end

View File

@ -72,7 +72,7 @@ defmodule Chess.BoardTest do
"3,0" => %{"type" => "queen", "colour" => "white"}
}
%{board: new_board} = Board.move_piece(board, %{"from" => [3, 0], "to" => [5, 2]})
%{board: new_board} = Board.move_piece(board, %{from: {3, 0}, to: {5, 2}})
assert new_board == %{
"5,2" => %{"type" => "queen", "colour" => "white"}
@ -85,7 +85,7 @@ defmodule Chess.BoardTest do
"7,0" => %{"type" => "rook", "colour" => "white"}
}
%{board: new_board} = Board.move_piece(board, %{"from" => [4, 0], "to" => [6, 0]})
%{board: new_board} = Board.move_piece(board, %{from: {4, 0}, to: {6, 0}})
assert new_board == %{
"6,0" => %{"type" => "king", "colour" => "white"},
@ -99,7 +99,7 @@ defmodule Chess.BoardTest do
"0,0" => %{"type" => "rook", "colour" => "white"}
}
%{board: new_board} = Board.move_piece(board, %{"from" => [4, 0], "to" => [2, 0]})
%{board: new_board} = Board.move_piece(board, %{from: {4, 0}, to: {2, 0}})
assert new_board == %{
"2,0" => %{"type" => "king", "colour" => "white"},