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:
parent
e30627b3ac
commit
d03b82ecfa
@ -17,6 +17,6 @@ const liveSocket = new LiveSocket(
|
|||||||
|
|
||||||
liveSocket.connect();
|
liveSocket.connect();
|
||||||
|
|
||||||
liveSocket.enableDebug();
|
// liveSocket.enableDebug();
|
||||||
|
|
||||||
window.liveSocket = liveSocket;
|
window.liveSocket = liveSocket;
|
||||||
|
|||||||
@ -65,7 +65,7 @@ defmodule Chess.Board do
|
|||||||
})
|
})
|
||||||
end
|
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, %{
|
move_piece(board, %{
|
||||||
from: {7, rank},
|
from: {7, rank},
|
||||||
to: {5, rank}
|
to: {5, rank}
|
||||||
|
|||||||
@ -19,8 +19,8 @@ defmodule Chess.Emails do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def opponent_moved_email(socket, game) do
|
def opponent_moved_email(socket, game) do
|
||||||
user = Repo.get(User, socket.assigns.user_id)
|
user = Repo.get(User, socket.assigns.user.id)
|
||||||
opponent = opponent(game, socket.assigns.user_id)
|
opponent = opponent(game, socket.assigns.user.id)
|
||||||
|
|
||||||
new_email()
|
new_email()
|
||||||
|> to(opponent)
|
|> to(opponent)
|
||||||
|
|||||||
@ -1,73 +1,4 @@
|
|||||||
defmodule ChessWeb.Presence do
|
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,
|
use Phoenix.Presence,
|
||||||
otp_app: :chess,
|
otp_app: :chess,
|
||||||
pubsub_server: Chess.PubSub
|
pubsub_server: Chess.PubSub
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
<div class="game-info">
|
<div class="game-info">
|
||||||
<p>
|
<p>
|
||||||
Playing <%= opponent(@game, @user.id).name %>
|
Playing <%= opponent(@game, @user.id).name %>
|
||||||
|
<%= 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">
|
<img class="game-info__opponent-status" src="/images/eye-closed.svg" alt="offline">
|
||||||
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ defmodule ChessWeb.GameView do
|
|||||||
use ChessWeb, :view
|
use ChessWeb, :view
|
||||||
|
|
||||||
alias Chess.GameState
|
alias Chess.GameState
|
||||||
|
alias Chess.Repo
|
||||||
|
|
||||||
import Phoenix.Component
|
import Phoenix.Component
|
||||||
import Chess.Auth, only: [current_user: 1]
|
import Chess.Auth, only: [current_user: 1]
|
||||||
@ -88,6 +89,14 @@ defmodule ChessWeb.GameView do
|
|||||||
end
|
end
|
||||||
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
|
def opponent(game, user_id) do
|
||||||
if game.user_id == user_id do
|
if game.user_id == user_id do
|
||||||
game.opponent
|
game.opponent
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
defmodule ChessWeb.BoardLive do
|
defmodule ChessWeb.BoardLive do
|
||||||
use Phoenix.LiveView, container: {:div, class: "board__container"}
|
use Phoenix.LiveView, container: {:div, class: "board__container"}
|
||||||
|
|
||||||
|
alias Chess.Emails
|
||||||
|
alias Chess.Mailer
|
||||||
alias Chess.Store.User
|
alias Chess.Store.User
|
||||||
alias Chess.Store.Game
|
alias Chess.Store.Game
|
||||||
alias Chess.Store.Move
|
alias Chess.Store.Move
|
||||||
@ -8,6 +10,7 @@ defmodule ChessWeb.BoardLive do
|
|||||||
alias Chess.Moves
|
alias Chess.Moves
|
||||||
|
|
||||||
alias ChessWeb.GameView
|
alias ChessWeb.GameView
|
||||||
|
alias ChessWeb.Presence
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
@ -25,6 +28,7 @@ defmodule ChessWeb.BoardLive do
|
|||||||
game =
|
game =
|
||||||
Game.for_user(user)
|
Game.for_user(user)
|
||||||
|> Repo.get!(game_id)
|
|> Repo.get!(game_id)
|
||||||
|
|> Repo.preload([:user, :opponent])
|
||||||
|
|
||||||
{:ok, assign(socket, default_assigns(game, user))}
|
{:ok, assign(socket, default_assigns(game, user))}
|
||||||
end
|
end
|
||||||
@ -51,10 +55,14 @@ defmodule ChessWeb.BoardLive do
|
|||||||
{:noreply, assign(socket, state)}
|
{:noreply, assign(socket, state)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info(%{event: "presence_diff", payload: _params}, socket) do
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
defp handle_click(socket, file, rank) do
|
defp handle_click(socket, file, rank) do
|
||||||
game = socket.assigns[:game]
|
game = socket.assigns.game
|
||||||
board = game.board
|
board = game.board
|
||||||
user = socket.assigns[:user]
|
user = socket.assigns.user
|
||||||
|
|
||||||
colour = GameView.player_colour(user, game)
|
colour = GameView.player_colour(user, game)
|
||||||
|
|
||||||
@ -65,7 +73,7 @@ defmodule ChessWeb.BoardLive do
|
|||||||
handle_selection(board, colour, file, rank)
|
handle_selection(board, colour, file, rank)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
handle_move(socket.assigns, file, rank)
|
handle_move(socket, file, rank)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -85,17 +93,16 @@ defmodule ChessWeb.BoardLive do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_move(
|
defp handle_move(socket, file, rank) do
|
||||||
%{game: game, available: available, selected: selected},
|
%{game: game, available: available, selected: selected} = socket.assigns
|
||||||
file,
|
|
||||||
rank
|
|
||||||
) do
|
|
||||||
if {file, rank} in available do
|
if {file, rank} in available do
|
||||||
game
|
game
|
||||||
|> Moves.make_move(%{from: selected, to: {file, rank}})
|
|> Moves.make_move(%{from: selected, to: {file, rank}})
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, %{game: game}} ->
|
{:ok, %{game: game}} ->
|
||||||
game
|
game
|
||||||
|
|> Repo.reload()
|
||||||
|> Repo.preload([:user, :opponent])
|
|> Repo.preload([:user, :opponent])
|
||||||
|> Repo.preload(
|
|> Repo.preload(
|
||||||
moves:
|
moves:
|
||||||
@ -106,6 +113,8 @@ defmodule ChessWeb.BoardLive do
|
|||||||
)
|
)
|
||||||
|> broadcast_move(game.board)
|
|> broadcast_move(game.board)
|
||||||
|
|
||||||
|
email_move(socket, game)
|
||||||
|
|
||||||
[
|
[
|
||||||
{:selected, nil},
|
{:selected, nil},
|
||||||
{:available, []},
|
{:available, []},
|
||||||
@ -118,6 +127,24 @@ defmodule ChessWeb.BoardLive do
|
|||||||
end
|
end
|
||||||
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
|
defp broadcast_move(game, board) do
|
||||||
ChessWeb.Endpoint.broadcast_from(
|
ChessWeb.Endpoint.broadcast_from(
|
||||||
self(),
|
self(),
|
||||||
|
|||||||
@ -5,6 +5,7 @@ defmodule ChessWeb.GameInfoLive do
|
|||||||
alias Chess.Store.Game
|
alias Chess.Store.Game
|
||||||
alias Chess.Store.Move
|
alias Chess.Store.Move
|
||||||
alias Chess.Repo
|
alias Chess.Repo
|
||||||
|
alias ChessWeb.Presence
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
@ -15,7 +16,9 @@ defmodule ChessWeb.GameInfoLive do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def mount(_params, %{"game_id" => game_id, "user_id" => user_id}, socket) do
|
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)
|
user = Repo.get!(User, user_id)
|
||||||
|
|
||||||
@ -32,10 +35,38 @@ defmodule ChessWeb.GameInfoLive do
|
|||||||
)
|
)
|
||||||
|> Repo.get!(game_id)
|
|> 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
|
end
|
||||||
|
|
||||||
def handle_info(%{event: "move", payload: state}, socket) do
|
def handle_info(%{event: "move", payload: state}, socket) do
|
||||||
{:noreply, assign(socket, state)}
|
{:noreply, assign(socket, state)}
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@ -72,7 +72,7 @@ defmodule Chess.BoardTest do
|
|||||||
"3,0" => %{"type" => "queen", "colour" => "white"}
|
"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 == %{
|
assert new_board == %{
|
||||||
"5,2" => %{"type" => "queen", "colour" => "white"}
|
"5,2" => %{"type" => "queen", "colour" => "white"}
|
||||||
@ -85,7 +85,7 @@ defmodule Chess.BoardTest do
|
|||||||
"7,0" => %{"type" => "rook", "colour" => "white"}
|
"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 == %{
|
assert new_board == %{
|
||||||
"6,0" => %{"type" => "king", "colour" => "white"},
|
"6,0" => %{"type" => "king", "colour" => "white"},
|
||||||
@ -99,7 +99,7 @@ defmodule Chess.BoardTest do
|
|||||||
"0,0" => %{"type" => "rook", "colour" => "white"}
|
"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 == %{
|
assert new_board == %{
|
||||||
"2,0" => %{"type" => "king", "colour" => "white"},
|
"2,0" => %{"type" => "king", "colour" => "white"},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user