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 @@
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"},