mirror of
https://github.com/correl/planning-poker.git
synced 2024-11-21 19:18:37 +00:00
Join rooms via websockets
This commit is contained in:
parent
62b2bc2484
commit
a534184ff1
12 changed files with 255 additions and 81 deletions
|
@ -9,11 +9,11 @@
|
|||
"elm/browser": "1.0.2",
|
||||
"elm/core": "1.0.5",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/url": "1.0.0",
|
||||
"mdgriffith/elm-ui": "1.1.5"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/json": "1.1.3",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.2"
|
||||
}
|
||||
|
|
|
@ -14,8 +14,38 @@ import "../css/app.scss"
|
|||
//
|
||||
import "phoenix_html"
|
||||
|
||||
import { Elm } from "../src/Main.elm";
|
||||
import { Socket, Presence } from "phoenix"
|
||||
import { Elm } from "../src/Main.elm"
|
||||
import uuid4 from "uuid4"
|
||||
|
||||
var player_id = uuid4()
|
||||
|
||||
var socket = new Socket("/socket", {params: {player_id: player_id}})
|
||||
socket.connect()
|
||||
|
||||
var app = Elm.Main.init({
|
||||
node: document.getElementById("elm-main")
|
||||
});
|
||||
node: document.getElementById("elm-main"),
|
||||
flags: "player:" + player_id
|
||||
})
|
||||
|
||||
app.ports.joinRoom.subscribe(options => {
|
||||
let channel = socket.channel(
|
||||
"room:" + options.room,
|
||||
{playerName: options.playerName}
|
||||
)
|
||||
let presences = {}
|
||||
channel.join()
|
||||
.receive("ok", resp => {
|
||||
console.log("Joined successfully", resp);
|
||||
app.ports.joinedRoom.send(options.room);
|
||||
})
|
||||
.receive("error", resp => { console.log("Unable to join", resp) })
|
||||
channel.on("presence_state", state => {
|
||||
presences = Presence.syncState(presences, state)
|
||||
app.ports.gotPresence.send(presences)
|
||||
})
|
||||
channel.on("presence_diff", diff => {
|
||||
presences = Presence.syncDiff(presences, diff)
|
||||
app.ports.gotPresence.send(presences)
|
||||
})
|
||||
})
|
||||
|
|
5
assets/package-lock.json
generated
5
assets/package-lock.json
generated
|
@ -8132,6 +8132,11 @@
|
|||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"dev": true
|
||||
},
|
||||
"uuid4": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/uuid4/-/uuid4-1.1.4.tgz",
|
||||
"integrity": "sha512-Gr1q2k40LpF8CokcnQFjPDsdslzJbTCTBG5xQIEflUov431gFkY5KduiGIeKYAamkQnNn4IfdHJbLnl9Bib8TQ=="
|
||||
},
|
||||
"v8-compile-cache": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz",
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html"
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"uuid4": "^1.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
|
|
|
@ -13,6 +13,7 @@ import Url.Parser as Parser exposing ((</>), Parser, s, string)
|
|||
type alias Model =
|
||||
{ page : Page
|
||||
, key : Nav.Key
|
||||
, player : String
|
||||
}
|
||||
|
||||
|
||||
|
@ -34,9 +35,9 @@ type Msg
|
|||
| RoomMsg Room.Msg
|
||||
|
||||
|
||||
init : () -> Url -> Nav.Key -> ( Model, Cmd Msg )
|
||||
init _ url key =
|
||||
updateUrl url { page = NotFound, key = key }
|
||||
init : String -> Url -> Nav.Key -> ( Model, Cmd Msg )
|
||||
init player url key =
|
||||
updateUrl url { page = NotFound, key = key, player = player }
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
|
@ -83,7 +84,8 @@ updateUrl url model =
|
|||
EntryPage entryModel ->
|
||||
toRoom model
|
||||
(Room.init
|
||||
{ room = id
|
||||
{ id = id
|
||||
, player = model.player
|
||||
, roomName =
|
||||
case String.trim entryModel.roomName of
|
||||
"" ->
|
||||
|
@ -98,7 +100,8 @@ updateUrl url model =
|
|||
_ ->
|
||||
toRoom model
|
||||
(Room.init
|
||||
{ room = id
|
||||
{ id = id
|
||||
, player = model.player
|
||||
, roomName = "Planning Poker"
|
||||
, playerName = ""
|
||||
}
|
||||
|
@ -133,7 +136,7 @@ view model =
|
|||
NotFound.view
|
||||
|
||||
|
||||
main : Program () Model Msg
|
||||
main : Program String Model Msg
|
||||
main =
|
||||
Browser.application
|
||||
{ init = init
|
||||
|
@ -141,5 +144,13 @@ main =
|
|||
, update = update
|
||||
, onUrlChange = ChangedUrl
|
||||
, onUrlRequest = ClickedLink
|
||||
, subscriptions = \_ -> Sub.none
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions _ =
|
||||
Sub.batch
|
||||
[ Sub.map EntryMsg Entry.subscriptions
|
||||
, Sub.map RoomMsg Room.subscriptions
|
||||
]
|
||||
|
|
16
assets/src/PlanningPokerAPI.elm
Normal file
16
assets/src/PlanningPokerAPI.elm
Normal file
|
@ -0,0 +1,16 @@
|
|||
port module PlanningPokerAPI exposing
|
||||
( gotPresence
|
||||
, joinRoom
|
||||
, joinedRoom
|
||||
)
|
||||
|
||||
import Json.Decode exposing (Value)
|
||||
|
||||
|
||||
port joinRoom : { room : String, playerName : String } -> Cmd msg
|
||||
|
||||
|
||||
port joinedRoom : (String -> msg) -> Sub msg
|
||||
|
||||
|
||||
port gotPresence : (Value -> msg) -> Sub msg
|
|
@ -8,6 +8,7 @@ import Element.Border as Border
|
|||
import Element.Font as Font
|
||||
import Element.Input as Input
|
||||
import Html exposing (Html)
|
||||
import PlanningPokerAPI as API
|
||||
import PlanningPokerUI as UI
|
||||
|
||||
|
||||
|
@ -23,6 +24,7 @@ type Msg
|
|||
= PlayerNameChanged String
|
||||
| RoomNameChanged String
|
||||
| CreateRoom
|
||||
| JoinedRoom String
|
||||
|
||||
|
||||
init : () -> ( Model, Cmd Msg )
|
||||
|
@ -46,7 +48,16 @@ update key msg model =
|
|||
( { model | roomName = newName }, Cmd.none )
|
||||
|
||||
CreateRoom ->
|
||||
( model, Nav.pushUrl key "/room/a0fd1422-abd9-434e-9d7c-883294b2992c" )
|
||||
let
|
||||
room =
|
||||
"a0fd1422-abd9-434e-9d7c-883294b2992c"
|
||||
in
|
||||
( model
|
||||
, API.joinRoom { room = room, playerName = model.playerName }
|
||||
)
|
||||
|
||||
JoinedRoom room ->
|
||||
( model, Nav.pushUrl key ("/room/" ++ room) )
|
||||
|
||||
|
||||
view : Model -> Document Msg
|
||||
|
@ -92,3 +103,8 @@ layout model =
|
|||
<|
|
||||
text (Maybe.withDefault " " model.error)
|
||||
]
|
||||
|
||||
|
||||
subscriptions : Sub Msg
|
||||
subscriptions =
|
||||
API.joinedRoom JoinedRoom
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
module PlanningPokerRoom exposing (Model, Msg, init, update, view)
|
||||
module PlanningPokerRoom exposing
|
||||
( Model
|
||||
, Msg
|
||||
, init
|
||||
, subscriptions
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Browser exposing (Document)
|
||||
import Browser.Navigation as Nav
|
||||
|
@ -9,6 +16,8 @@ import Element.Border as Border
|
|||
import Element.Font as Font
|
||||
import Element.Input as Input
|
||||
import Html exposing (Html)
|
||||
import Json.Decode as Decode
|
||||
import PlanningPokerAPI as API
|
||||
import PlanningPokerUI as UI
|
||||
|
||||
|
||||
|
@ -16,14 +25,17 @@ type alias Model =
|
|||
{ room : Maybe Room
|
||||
, player : String
|
||||
, playerName : String
|
||||
, showVotes : Bool
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= Vote String
|
||||
| Reset
|
||||
| Reveal
|
||||
| PlayerNameChanged String
|
||||
| JoinRoom
|
||||
| GotPresence Decode.Value
|
||||
|
||||
|
||||
type alias Room =
|
||||
|
@ -33,11 +45,6 @@ type alias Room =
|
|||
}
|
||||
|
||||
|
||||
type UserLevel
|
||||
= Moderator
|
||||
| Participant
|
||||
|
||||
|
||||
type alias Player =
|
||||
{ level : UserLevel
|
||||
, name : String
|
||||
|
@ -45,52 +52,35 @@ type alias Player =
|
|||
}
|
||||
|
||||
|
||||
init : { room : String, roomName : String, playerName : String } -> ( Model, Cmd Msg )
|
||||
init { room, roomName, playerName } =
|
||||
type UserLevel
|
||||
= Moderator
|
||||
| Participant
|
||||
|
||||
|
||||
type Vote
|
||||
= Hidden (Maybe String)
|
||||
| Revealed (Maybe String)
|
||||
|
||||
|
||||
init :
|
||||
{ id : String
|
||||
, player : String
|
||||
, roomName : String
|
||||
, playerName : String
|
||||
}
|
||||
-> ( Model, Cmd Msg )
|
||||
init { id, player, roomName, playerName } =
|
||||
let
|
||||
preparedRooms =
|
||||
Dict.fromList
|
||||
[ -- Room created from mocked entry page
|
||||
( "a0fd1422-abd9-434e-9d7c-883294b2992c"
|
||||
, { id = "a0fd1422-abd9-434e-9d7c-883294b2992c"
|
||||
, name = roomName
|
||||
, players =
|
||||
Dict.fromList
|
||||
[ ( "00000000-0000-0000-0000-000000000000"
|
||||
, { level = Moderator, name = playerName, vote = Nothing }
|
||||
)
|
||||
, ( "44db0a59-28bb-4b9f-8e5d-a46f2c2a3266"
|
||||
, { level = Participant, name = "John", vote = Nothing }
|
||||
)
|
||||
, ( "69b8b450-bc2a-4eeb-b056-91c7aa4ba528"
|
||||
, { level = Participant, name = "Jane", vote = Nothing }
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
, -- Room created from direct url access (unjoined)
|
||||
( "joinable"
|
||||
, { id = "a0fd1422-abd9-434e-9d7c-883294b2992c"
|
||||
, name = "Today's Grooming Session"
|
||||
, players =
|
||||
Dict.fromList
|
||||
[ ( "ffffffff-ffff-ffff-ffff-ffffffffffff"
|
||||
, { level = Moderator, name = "Pat", vote = Nothing }
|
||||
)
|
||||
, ( "44db0a59-28bb-4b9f-8e5d-a46f2c2a3266"
|
||||
, { level = Participant, name = "John", vote = Nothing }
|
||||
)
|
||||
, ( "69b8b450-bc2a-4eeb-b056-91c7aa4ba528"
|
||||
, { level = Participant, name = "Jane", vote = Nothing }
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
]
|
||||
room =
|
||||
{ id = id
|
||||
, name = roomName
|
||||
, players = Dict.empty
|
||||
}
|
||||
in
|
||||
( { room = Dict.get room preparedRooms
|
||||
, player = "00000000-0000-0000-0000-000000000000"
|
||||
( { room = Just room
|
||||
, player = player
|
||||
, playerName = playerName
|
||||
, showVotes = False
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
@ -116,6 +106,11 @@ update key msg model =
|
|||
, Cmd.none
|
||||
)
|
||||
|
||||
Reveal ->
|
||||
( { model | showVotes = True }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Reset ->
|
||||
( { model
|
||||
| room =
|
||||
|
@ -126,6 +121,7 @@ update key msg model =
|
|||
(\k v -> { v | vote = Nothing })
|
||||
room.players
|
||||
}
|
||||
, showVotes = False
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
@ -146,7 +142,21 @@ update key msg model =
|
|||
room.players
|
||||
}
|
||||
in
|
||||
( { model | room = Just newRoom }, Cmd.none )
|
||||
( model
|
||||
, API.joinRoom { room = room.id, playerName = model.playerName }
|
||||
)
|
||||
|
||||
GotPresence value ->
|
||||
case Decode.decodeValue playersDecoder value of
|
||||
Ok players ->
|
||||
let
|
||||
newRoom =
|
||||
{ room | players = players }
|
||||
in
|
||||
( { model | room = Just newRoom }, Cmd.none )
|
||||
|
||||
Err _ ->
|
||||
( model, Cmd.none )
|
||||
|
||||
Nothing ->
|
||||
case msg of
|
||||
|
@ -168,7 +178,7 @@ view model =
|
|||
{ title = room.name
|
||||
, body =
|
||||
[ navBar { title = room.name, playerName = player.name }
|
||||
, viewRoom model.player room
|
||||
, viewRoom model.player room model.showVotes
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -190,8 +200,8 @@ view model =
|
|||
}
|
||||
|
||||
|
||||
viewRoom : String -> Room -> Element Msg
|
||||
viewRoom player room =
|
||||
viewRoom : String -> Room -> Bool -> Element Msg
|
||||
viewRoom player room showVotes =
|
||||
let
|
||||
myVote =
|
||||
Dict.get player room.players
|
||||
|
@ -203,7 +213,7 @@ viewRoom player room =
|
|||
[ el [ width (fillPortion 3), alignTop ] <|
|
||||
viewCards myVote
|
||||
, el [ width (fillPortion 1), alignTop ] <|
|
||||
viewPlayers (Dict.values room.players)
|
||||
viewPlayers (Dict.values room.players) showVotes
|
||||
]
|
||||
, moderatorTools
|
||||
]
|
||||
|
@ -259,8 +269,8 @@ viewCards selected =
|
|||
List.map card [ "1", "3", "5", "8", "13" ]
|
||||
|
||||
|
||||
viewPlayers : List Player -> Element Msg
|
||||
viewPlayers playerList =
|
||||
viewPlayers : List Player -> Bool -> Element Msg
|
||||
viewPlayers playerList showVotes =
|
||||
table [ width fill ]
|
||||
{ data = playerList
|
||||
, columns =
|
||||
|
@ -275,11 +285,19 @@ viewPlayers playerList =
|
|||
, width = px 50
|
||||
, view =
|
||||
\player ->
|
||||
let
|
||||
vote =
|
||||
if showVotes then
|
||||
player.vote
|
||||
|
||||
else
|
||||
Maybe.map (\_ -> "✓") player.vote
|
||||
in
|
||||
el
|
||||
[ padding 10
|
||||
, Font.alignRight
|
||||
]
|
||||
(text <| Maybe.withDefault " " player.vote)
|
||||
(text <| Maybe.withDefault " " vote)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -287,12 +305,20 @@ viewPlayers playerList =
|
|||
|
||||
moderatorTools : Element Msg
|
||||
moderatorTools =
|
||||
UI.actionButton
|
||||
[ centerX ]
|
||||
{ isActive = True
|
||||
, onPress = Reset
|
||||
, label = text "Reset"
|
||||
}
|
||||
row [ centerX, spacing 20 ]
|
||||
[ UI.actionButton
|
||||
[ centerX ]
|
||||
{ isActive = True
|
||||
, onPress = Reveal
|
||||
, label = text "Reveal"
|
||||
}
|
||||
, UI.actionButton
|
||||
[ centerX ]
|
||||
{ isActive = True
|
||||
, onPress = Reset
|
||||
, label = text "Reset"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
joinForm : Room -> String -> Element Msg
|
||||
|
@ -337,3 +363,38 @@ joinForm room playerName =
|
|||
++ " People are already here"
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
subscriptions : Sub Msg
|
||||
subscriptions =
|
||||
API.gotPresence GotPresence
|
||||
|
||||
|
||||
type alias Presence =
|
||||
{ metas : List PresenceMeta }
|
||||
|
||||
|
||||
type alias PresenceMeta =
|
||||
{ name : String
|
||||
, online_at : String
|
||||
, phx_ref : String
|
||||
}
|
||||
|
||||
|
||||
playersDecoder : Decode.Decoder (Dict String Player)
|
||||
playersDecoder =
|
||||
let
|
||||
meta =
|
||||
Decode.field "name" Decode.string
|
||||
|
||||
presence =
|
||||
Decode.field "metas" (Decode.index 0 meta)
|
||||
|
||||
toPlayer id name =
|
||||
{ level = Participant
|
||||
, name = name
|
||||
, vote = Nothing
|
||||
}
|
||||
in
|
||||
Decode.dict presence
|
||||
|> Decode.map (Dict.map toPlayer)
|
||||
|
|
|
@ -12,9 +12,10 @@ defmodule Planningpoker.Application do
|
|||
# Start the PubSub system
|
||||
{Phoenix.PubSub, name: Planningpoker.PubSub},
|
||||
# Start the Endpoint (http/https)
|
||||
PlanningpokerWeb.Endpoint
|
||||
PlanningpokerWeb.Endpoint,
|
||||
# Start a worker by calling: Planningpoker.Worker.start_link(arg)
|
||||
# {Planningpoker.Worker, arg}
|
||||
PlanningpokerWeb.Presence
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
|
|
10
lib/planningpoker_web/channels/presence.ex
Normal file
10
lib/planningpoker_web/channels/presence.ex
Normal file
|
@ -0,0 +1,10 @@
|
|||
defmodule PlanningpokerWeb.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.
|
||||
"""
|
||||
use Phoenix.Presence, otp_app: :planningpoker,
|
||||
pubsub_server: Planningpoker.PubSub
|
||||
end
|
23
lib/planningpoker_web/channels/room_channel.ex
Normal file
23
lib/planningpoker_web/channels/room_channel.ex
Normal file
|
@ -0,0 +1,23 @@
|
|||
defmodule PlanningpokerWeb.RoomChannel do
|
||||
use Phoenix.Channel
|
||||
alias PlanningpokerWeb.Presence
|
||||
|
||||
def join("room:" <> room_id, params, socket) do
|
||||
send(self(), :after_join)
|
||||
{:ok, %{channel: room_id, topic: "Planning Poker"},
|
||||
socket
|
||||
|> assign(:room_id, room_id)
|
||||
|> assign(:player_name, params["playerName"])}
|
||||
end
|
||||
def handle_info(:after_join, socket) do
|
||||
{:ok, _} = Presence.track(
|
||||
socket,
|
||||
"player:#{socket.assigns.player_id}",
|
||||
%{
|
||||
name: socket.assigns.player_name,
|
||||
online_at: inspect(System.system_time(:second))
|
||||
})
|
||||
push(socket, "presence_state", Presence.list(socket))
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
|
@ -2,7 +2,7 @@ defmodule PlanningpokerWeb.UserSocket do
|
|||
use Phoenix.Socket
|
||||
|
||||
## Channels
|
||||
# channel "room:*", PlanningpokerWeb.RoomChannel
|
||||
channel "room:*", PlanningpokerWeb.RoomChannel
|
||||
|
||||
# Socket params are passed from the client and can
|
||||
# be used to verify and authenticate a user. After
|
||||
|
@ -16,8 +16,8 @@ defmodule PlanningpokerWeb.UserSocket do
|
|||
# See `Phoenix.Token` documentation for examples in
|
||||
# performing token verification on connect.
|
||||
@impl true
|
||||
def connect(_params, socket, _connect_info) do
|
||||
{:ok, socket}
|
||||
def connect(params, socket, _connect_info) do
|
||||
{:ok, assign(socket, :player_id, params["player_id"])}
|
||||
end
|
||||
|
||||
# Socket id's are topics that allow you to identify all sockets for a given user:
|
||||
|
@ -31,5 +31,5 @@ defmodule PlanningpokerWeb.UserSocket do
|
|||
#
|
||||
# Returning `nil` makes this socket anonymous.
|
||||
@impl true
|
||||
def id(_socket), do: nil
|
||||
def id(_socket), do: "player:${socket.assigns.player_id}"
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue