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.enableDebug();
|
||||
// liveSocket.enableDebug();
|
||||
|
||||
window.liveSocket = liveSocket;
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user