From d03b82ecfa45ed07630c0a7434ca0454446cffcb Mon Sep 17 00:00:00 2001 From: Dan Barber Date: Wed, 8 Nov 2023 17:17:38 -0600 Subject: [PATCH] Implement presence and fix email test --- assets/js/app.js | 2 +- lib/chess/board.ex | 2 +- lib/chess/emails.ex | 4 +- lib/chess_web/channels/presence.ex | 69 ------------------- .../templates/game/game_info.html.leex | 6 +- lib/chess_web/views/game_view.ex | 9 +++ lib/chess_web/views/live/board_live.ex | 43 +++++++++--- lib/chess_web/views/live/game_info_live.ex | 35 +++++++++- test/chess/board_test.exs | 6 +- 9 files changed, 89 insertions(+), 87 deletions(-) diff --git a/assets/js/app.js b/assets/js/app.js index 2f2961d..ab4e73d 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -17,6 +17,6 @@ const liveSocket = new LiveSocket( liveSocket.connect(); -liveSocket.enableDebug(); +// liveSocket.enableDebug(); window.liveSocket = liveSocket; diff --git a/lib/chess/board.ex b/lib/chess/board.ex index 63f18fd..eb82753 100644 --- a/lib/chess/board.ex +++ b/lib/chess/board.ex @@ -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} diff --git a/lib/chess/emails.ex b/lib/chess/emails.ex index eb46189..ec1df82 100644 --- a/lib/chess/emails.ex +++ b/lib/chess/emails.ex @@ -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) diff --git a/lib/chess_web/channels/presence.ex b/lib/chess_web/channels/presence.ex index 3dbab22..7af26a2 100644 --- a/lib/chess_web/channels/presence.ex +++ b/lib/chess_web/channels/presence.ex @@ -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 diff --git a/lib/chess_web/templates/game/game_info.html.leex b/lib/chess_web/templates/game/game_info.html.leex index 317cab7..2ed6916 100644 --- a/lib/chess_web/templates/game/game_info.html.leex +++ b/lib/chess_web/templates/game/game_info.html.leex @@ -1,7 +1,11 @@

Playing <%= opponent(@game, @user.id).name %> - offline + <%= if Map.has_key?(@presence, opponent(@game, @user.id).id) do %> + online + <% else %> + offline + <% end %>

diff --git a/lib/chess_web/views/game_view.ex b/lib/chess_web/views/game_view.ex index a76ecc7..31da762 100644 --- a/lib/chess_web/views/game_view.ex +++ b/lib/chess_web/views/game_view.ex @@ -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 diff --git a/lib/chess_web/views/live/board_live.ex b/lib/chess_web/views/live/board_live.ex index ac062fb..f6943ef 100644 --- a/lib/chess_web/views/live/board_live.ex +++ b/lib/chess_web/views/live/board_live.ex @@ -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(), diff --git a/lib/chess_web/views/live/game_info_live.ex b/lib/chess_web/views/live/game_info_live.ex index 63e873e..1d5eb04 100644 --- a/lib/chess_web/views/live/game_info_live.ex +++ b/lib/chess_web/views/live/game_info_live.ex @@ -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 diff --git a/test/chess/board_test.exs b/test/chess/board_test.exs index d626d82..c2ec727 100644 --- a/test/chess/board_test.exs +++ b/test/chess/board_test.exs @@ -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"},