1
0
mirror of https://github.com/danbee/chess synced 2025-03-04 08:39:06 +00:00

Show user status

This commit is contained in:
Daniel Barber 2018-08-26 15:04:08 -04:00
parent eb28da621e
commit 7280c98cc9
Signed by: danbarber
GPG Key ID: 931D8112E0103DD8
15 changed files with 215 additions and 30 deletions

View File

@ -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;
}
}
}

View File

@ -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;

View File

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

View File

@ -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;
}

View File

@ -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;
});
}

View File

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

View File

@ -1,11 +1,16 @@
const defaultState = {
selectedSquare: null,
playerId: null,
opponentId: null,
player: null,
opponent: null,
turn: null,
state: null,
opponentStatus: "offline",
availableMoves: [],
moves: [],

View File

@ -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

View File

@ -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

View File

@ -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 = %{

View 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

View File

@ -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

View File

@ -21,7 +21,7 @@ defmodule Chess.Mixfile do
def application do
[
mod: {Chess, []},
extra_applications: [:logger]
extra_applications: [:logger],
]
end

View File

@ -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