mirror of
https://github.com/danbee/chess
synced 2025-03-04 08:39:06 +00:00
Show user status
This commit is contained in:
parent
eb28da621e
commit
7280c98cc9
@ -1,3 +1,29 @@
|
||||
.game-info {
|
||||
grid-area: game-info;
|
||||
|
||||
.offline,
|
||||
.viewing {
|
||||
&:before {
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
display: inline-block;
|
||||
height: 0.75rem;
|
||||
margin-right: 0.25rem;
|
||||
width: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.offline {
|
||||
opacity: 0.4;
|
||||
|
||||
&:before {
|
||||
background-color: #cc3333;
|
||||
}
|
||||
}
|
||||
|
||||
.viewing {
|
||||
&:before {
|
||||
background-color: #66cc33;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,10 @@ class App extends React.Component {
|
||||
this.channel = new Channel(store, gameId);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.channel.leave();
|
||||
}
|
||||
|
||||
get moves() {
|
||||
const { store } = this.props;
|
||||
return store.getState().moves;
|
||||
|
||||
@ -4,7 +4,11 @@ import { connect } from "react-redux";
|
||||
const GameInfo = (props) => {
|
||||
return (
|
||||
<div className="game-info">
|
||||
<p>Playing {props.opponent}</p>
|
||||
<p>
|
||||
Playing {props.opponent} - <span className={props.opponentStatus}>
|
||||
{props.opponentStatus}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -12,6 +16,7 @@ const GameInfo = (props) => {
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
opponent: state.opponent,
|
||||
opponentStatus: state.opponentStatus,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -4,14 +4,17 @@ import defaultState from "../store/default-state";
|
||||
|
||||
const chessBoardReducer = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case "SET_PLAYER":
|
||||
case "SET_USER_ID":
|
||||
return Immutable.fromJS(state)
|
||||
.set("player", action.player)
|
||||
.set("userId", action.user_id)
|
||||
.toJS();
|
||||
|
||||
case "SET_OPPONENT":
|
||||
case "SET_PLAYERS":
|
||||
return Immutable.fromJS(state)
|
||||
.set("player", action.player)
|
||||
.set("playerId", action.player_id)
|
||||
.set("opponent", action.opponent)
|
||||
.set("opponentId", action.opponent_id)
|
||||
.toJS();
|
||||
|
||||
case "SET_GAME":
|
||||
@ -40,6 +43,11 @@ const chessBoardReducer = (state = defaultState, action) => {
|
||||
.set("availableMoves", [])
|
||||
.toJS();
|
||||
|
||||
case "SET_OPPONENT_STATUS":
|
||||
return Immutable.fromJS(state)
|
||||
.set("opponentStatus", action.opponentStatus)
|
||||
.toJS();
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@ -1,10 +1,19 @@
|
||||
import socket from "../socket";
|
||||
import { setPlayer, setOpponent, setGame, setAvailableMoves } from "../store/actions";
|
||||
import _ from "lodash";
|
||||
import socket from "./socket";
|
||||
import { Presence } from "phoenix";
|
||||
import {
|
||||
setUserId,
|
||||
setPlayers,
|
||||
setGame,
|
||||
setAvailableMoves,
|
||||
setOpponentStatus,
|
||||
} from "../store/actions";
|
||||
|
||||
class Channel {
|
||||
constructor(store, gameId) {
|
||||
this.store = store;
|
||||
this.channel = socket.channel(`game:${gameId}`, {});
|
||||
this.presences = {};
|
||||
|
||||
this.join();
|
||||
this.subscribe();
|
||||
@ -17,13 +26,41 @@ class Channel {
|
||||
});
|
||||
}
|
||||
|
||||
leave() {
|
||||
this.channel.leave();
|
||||
}
|
||||
|
||||
subscribe() {
|
||||
this.channel.on("game:update", data => {
|
||||
if (data.player != undefined) {
|
||||
this.store.dispatch(setPlayer(data.player));
|
||||
this.store.dispatch(setOpponent(data.opponent));
|
||||
}
|
||||
this.store.dispatch(setGame(data));
|
||||
this.channel.on("game:update", this.updateGame.bind(this));
|
||||
|
||||
this.channel.on("presence_state", data => {
|
||||
this.presences = Presence.syncState(this.presences, data);
|
||||
this.setOpponentStatus();
|
||||
});
|
||||
|
||||
this.channel.on("presence_diff", data => {
|
||||
this.presences = Presence.syncDiff(this.presences, data);
|
||||
this.setOpponentStatus();
|
||||
});
|
||||
}
|
||||
|
||||
updateGame(data) {
|
||||
if (data.player != undefined) {
|
||||
this.store.dispatch(setUserId(data.user_id));
|
||||
this.store.dispatch(setPlayers(data));
|
||||
}
|
||||
this.store.dispatch(setGame(data));
|
||||
}
|
||||
|
||||
setOpponentStatus() {
|
||||
this.store.dispatch(setOpponentStatus(
|
||||
this.opponentOnline() ? "viewing" : "offline"
|
||||
));
|
||||
}
|
||||
|
||||
opponentOnline() {
|
||||
return _.find(this.presences, (value, id) => {
|
||||
return parseInt(id) == this.store.getState().opponentId;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,21 +1,25 @@
|
||||
const SET_PLAYER = "SET_PLAYER";
|
||||
const SET_OPPONENT = "SET_OPPONENT";
|
||||
const SET_USER_ID = "SET_USER_ID";
|
||||
const SET_PLAYERS = "SET_PLAYERS";
|
||||
const SET_GAME = "SET_GAME";
|
||||
const SET_AVAILABLE_MOVES = "SET_AVAILABLE_MOVES";
|
||||
const SET_GAME_ID = "SET_GAME_ID";
|
||||
const SELECT_PIECE = "SELECT_PIECE";
|
||||
const SET_OPPONENT_STATUS = "SET_OPPONENT_STATUS";
|
||||
|
||||
export const setPlayer = (player) => {
|
||||
export const setUserId = (user_id) => {
|
||||
return {
|
||||
type: SET_PLAYER,
|
||||
player,
|
||||
type: SET_USER_ID,
|
||||
user_id,
|
||||
};
|
||||
};
|
||||
|
||||
export const setOpponent = (opponent) => {
|
||||
export const setPlayers = (data) => {
|
||||
return {
|
||||
type: SET_OPPONENT,
|
||||
opponent,
|
||||
type: SET_PLAYERS,
|
||||
player: data.player,
|
||||
player_id: data.player_id,
|
||||
opponent: data.opponent,
|
||||
opponent_id: data.opponent_id,
|
||||
};
|
||||
};
|
||||
|
||||
@ -49,3 +53,10 @@ export const selectPiece = (coords) => {
|
||||
coords,
|
||||
};
|
||||
};
|
||||
|
||||
export const setOpponentStatus = (opponentStatus) => {
|
||||
return {
|
||||
type: SET_OPPONENT_STATUS,
|
||||
opponentStatus,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
const defaultState = {
|
||||
selectedSquare: null,
|
||||
|
||||
playerId: null,
|
||||
opponentId: null,
|
||||
|
||||
player: null,
|
||||
opponent: null,
|
||||
turn: null,
|
||||
state: null,
|
||||
|
||||
opponentStatus: "offline",
|
||||
|
||||
availableMoves: [],
|
||||
|
||||
moves: [],
|
||||
|
||||
@ -14,7 +14,7 @@ config :chess, ChessWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
secret_key_base: "iiTDTKorCWTFoeBgAkr35XZp22cNIM2RsmnHiHdzKAuSHXUGXx42z7lawAwiu1B1",
|
||||
render_errors: [view: ChessWeb.ErrorView, accepts: ~w(html json)],
|
||||
pubsub: [name: ChessWeb.PubSub,
|
||||
pubsub: [name: Chess.PubSub,
|
||||
adapter: Phoenix.PubSub.PG2]
|
||||
|
||||
# Configures Elixir's Logger
|
||||
|
||||
@ -16,6 +16,7 @@ defmodule Chess do
|
||||
supervisor(ChessWeb.Endpoint, []),
|
||||
# Start your own worker by calling: Chess.Worker.start_link(arg1, arg2, arg3)
|
||||
# worker(Chess.Worker, [arg1, arg2, arg3]),
|
||||
supervisor(ChessWeb.Presence, []),
|
||||
]
|
||||
|
||||
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
|
||||
|
||||
@ -9,6 +9,7 @@ defmodule ChessWeb.GameChannel do
|
||||
alias Chess.MoveList
|
||||
alias Chess.Moves
|
||||
alias Chess.Repo.Queries
|
||||
alias ChessWeb.Presence
|
||||
|
||||
def join("game:" <> game_id, _params, socket) do
|
||||
send(self(), {:after_join, game_id})
|
||||
@ -18,12 +19,14 @@ defmodule ChessWeb.GameChannel do
|
||||
|
||||
def handle_info({:after_join, game_id}, socket) do
|
||||
game =
|
||||
socket.assigns.current_user_id
|
||||
socket.assigns.user_id
|
||||
|> Queries.game_for_info(game_id)
|
||||
|
||||
payload = %{
|
||||
player: player(game, socket.assigns.current_user_id),
|
||||
opponent: opponent(game, socket.assigns.current_user_id).name,
|
||||
player_id: socket.assigns.user_id,
|
||||
opponent_id: opponent(game, socket.assigns.user_id).id,
|
||||
player: player(game, socket.assigns.user_id),
|
||||
opponent: opponent(game, socket.assigns.user_id).name,
|
||||
board: Board.transform(game.board),
|
||||
turn: game.turn,
|
||||
state: game.state,
|
||||
@ -33,13 +36,15 @@ defmodule ChessWeb.GameChannel do
|
||||
socket
|
||||
|> push("game:update", payload)
|
||||
|
||||
track_presence(socket)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_in("game:move", params, socket) do
|
||||
move_params = convert_params(params)
|
||||
|
||||
socket.assigns.current_user_id
|
||||
socket.assigns.user_id
|
||||
|> Queries.game_with_moves(socket.assigns.game_id)
|
||||
|> Moves.make_move(move_params)
|
||||
|> case do
|
||||
@ -60,7 +65,7 @@ defmodule ChessWeb.GameChannel do
|
||||
socket
|
||||
) do
|
||||
game =
|
||||
socket.assigns.current_user_id
|
||||
socket.assigns.user_id
|
||||
|> Queries.game_with_moves(socket.assigns.game_id)
|
||||
|
||||
moves = Moves.available(game.board, {
|
||||
@ -75,6 +80,16 @@ defmodule ChessWeb.GameChannel do
|
||||
{:reply, {:ok, reply}, socket}
|
||||
end
|
||||
|
||||
def track_presence(socket) do
|
||||
{:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
|
||||
user_id: socket.assigns.user_id,
|
||||
online_at: inspect(System.system_time(:seconds))
|
||||
})
|
||||
|
||||
socket
|
||||
|> push("presence_state", Presence.list(socket))
|
||||
end
|
||||
|
||||
def convert_params(%{"from" => from, "to" => to}) do
|
||||
%{
|
||||
"from" => Enum.map(from, &(String.to_integer(&1))),
|
||||
@ -84,7 +99,7 @@ defmodule ChessWeb.GameChannel do
|
||||
|
||||
def send_update(socket) do
|
||||
game =
|
||||
socket.assigns.current_user_id
|
||||
socket.assigns.user_id
|
||||
|> Queries.game_with_moves(socket.assigns.game_id)
|
||||
|
||||
payload = %{
|
||||
|
||||
73
lib/chess_web/channels/presence.ex
Normal file
73
lib/chess_web/channels/presence.ex
Normal file
@ -0,0 +1,73 @@
|
||||
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(:seconds))
|
||||
})
|
||||
{: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
|
||||
end
|
||||
@ -24,7 +24,7 @@ defmodule ChessWeb.UserSocket do
|
||||
def connect(%{"token" => token}, socket) do
|
||||
case Token.verify(socket, "game socket", token, max_age: 1_209_600) do
|
||||
{:ok, user_id} ->
|
||||
{:ok, assign(socket, :current_user_id, user_id)}
|
||||
{:ok, assign(socket, :user_id, user_id)}
|
||||
{:error, _reason} ->
|
||||
:error
|
||||
end
|
||||
|
||||
2
mix.exs
2
mix.exs
@ -21,7 +21,7 @@ defmodule Chess.Mixfile do
|
||||
def application do
|
||||
[
|
||||
mod: {Chess, []},
|
||||
extra_applications: [:logger]
|
||||
extra_applications: [:logger],
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ defmodule ChessWeb.UserSocketTest do
|
||||
token = Phoenix.Token.sign(@endpoint, "game socket", 42)
|
||||
|
||||
assert {:ok, socket} = connect(UserSocket, %{"token" => token})
|
||||
assert socket.assigns.current_user_id == 42
|
||||
assert socket.assigns.user_id == 42
|
||||
end
|
||||
|
||||
test "cannot authenticate with invalid token" do
|
||||
|
||||
Loading…
Reference in New Issue
Block a user