Join rooms via websockets

This commit is contained in:
Correl Roush 2020-05-06 01:30:43 -04:00
parent 62b2bc2484
commit a534184ff1
12 changed files with 255 additions and 81 deletions

View file

@ -9,11 +9,11 @@
"elm/browser": "1.0.2", "elm/browser": "1.0.2",
"elm/core": "1.0.5", "elm/core": "1.0.5",
"elm/html": "1.0.0", "elm/html": "1.0.0",
"elm/json": "1.1.3",
"elm/url": "1.0.0", "elm/url": "1.0.0",
"mdgriffith/elm-ui": "1.1.5" "mdgriffith/elm-ui": "1.1.5"
}, },
"indirect": { "indirect": {
"elm/json": "1.1.3",
"elm/time": "1.0.0", "elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2" "elm/virtual-dom": "1.0.2"
} }

View file

@ -14,8 +14,38 @@ import "../css/app.scss"
// //
import "phoenix_html" 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({ 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)
})
})

View file

@ -8132,6 +8132,11 @@
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true "dev": true
}, },
"uuid4": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/uuid4/-/uuid4-1.1.4.tgz",
"integrity": "sha512-Gr1q2k40LpF8CokcnQFjPDsdslzJbTCTBG5xQIEflUov431gFkY5KduiGIeKYAamkQnNn4IfdHJbLnl9Bib8TQ=="
},
"v8-compile-cache": { "v8-compile-cache": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz",

View file

@ -9,7 +9,8 @@
}, },
"dependencies": { "dependencies": {
"phoenix": "file:../deps/phoenix", "phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html" "phoenix_html": "file:../deps/phoenix_html",
"uuid4": "^1.1.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.0.0", "@babel/core": "^7.0.0",

View file

@ -13,6 +13,7 @@ import Url.Parser as Parser exposing ((</>), Parser, s, string)
type alias Model = type alias Model =
{ page : Page { page : Page
, key : Nav.Key , key : Nav.Key
, player : String
} }
@ -34,9 +35,9 @@ type Msg
| RoomMsg Room.Msg | RoomMsg Room.Msg
init : () -> Url -> Nav.Key -> ( Model, Cmd Msg ) init : String -> Url -> Nav.Key -> ( Model, Cmd Msg )
init _ url key = init player url key =
updateUrl url { page = NotFound, key = key } updateUrl url { page = NotFound, key = key, player = player }
update : Msg -> Model -> ( Model, Cmd Msg ) update : Msg -> Model -> ( Model, Cmd Msg )
@ -83,7 +84,8 @@ updateUrl url model =
EntryPage entryModel -> EntryPage entryModel ->
toRoom model toRoom model
(Room.init (Room.init
{ room = id { id = id
, player = model.player
, roomName = , roomName =
case String.trim entryModel.roomName of case String.trim entryModel.roomName of
"" -> "" ->
@ -98,7 +100,8 @@ updateUrl url model =
_ -> _ ->
toRoom model toRoom model
(Room.init (Room.init
{ room = id { id = id
, player = model.player
, roomName = "Planning Poker" , roomName = "Planning Poker"
, playerName = "" , playerName = ""
} }
@ -133,7 +136,7 @@ view model =
NotFound.view NotFound.view
main : Program () Model Msg main : Program String Model Msg
main = main =
Browser.application Browser.application
{ init = init { init = init
@ -141,5 +144,13 @@ main =
, update = update , update = update
, onUrlChange = ChangedUrl , onUrlChange = ChangedUrl
, onUrlRequest = ClickedLink , onUrlRequest = ClickedLink
, subscriptions = \_ -> Sub.none , subscriptions = subscriptions
} }
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.batch
[ Sub.map EntryMsg Entry.subscriptions
, Sub.map RoomMsg Room.subscriptions
]

View 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

View file

@ -8,6 +8,7 @@ import Element.Border as Border
import Element.Font as Font import Element.Font as Font
import Element.Input as Input import Element.Input as Input
import Html exposing (Html) import Html exposing (Html)
import PlanningPokerAPI as API
import PlanningPokerUI as UI import PlanningPokerUI as UI
@ -23,6 +24,7 @@ type Msg
= PlayerNameChanged String = PlayerNameChanged String
| RoomNameChanged String | RoomNameChanged String
| CreateRoom | CreateRoom
| JoinedRoom String
init : () -> ( Model, Cmd Msg ) init : () -> ( Model, Cmd Msg )
@ -46,7 +48,16 @@ update key msg model =
( { model | roomName = newName }, Cmd.none ) ( { model | roomName = newName }, Cmd.none )
CreateRoom -> 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 view : Model -> Document Msg
@ -92,3 +103,8 @@ layout model =
<| <|
text (Maybe.withDefault " " model.error) text (Maybe.withDefault " " model.error)
] ]
subscriptions : Sub Msg
subscriptions =
API.joinedRoom JoinedRoom

View file

@ -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 exposing (Document)
import Browser.Navigation as Nav import Browser.Navigation as Nav
@ -9,6 +16,8 @@ import Element.Border as Border
import Element.Font as Font import Element.Font as Font
import Element.Input as Input import Element.Input as Input
import Html exposing (Html) import Html exposing (Html)
import Json.Decode as Decode
import PlanningPokerAPI as API
import PlanningPokerUI as UI import PlanningPokerUI as UI
@ -16,14 +25,17 @@ type alias Model =
{ room : Maybe Room { room : Maybe Room
, player : String , player : String
, playerName : String , playerName : String
, showVotes : Bool
} }
type Msg type Msg
= Vote String = Vote String
| Reset | Reset
| Reveal
| PlayerNameChanged String | PlayerNameChanged String
| JoinRoom | JoinRoom
| GotPresence Decode.Value
type alias Room = type alias Room =
@ -33,11 +45,6 @@ type alias Room =
} }
type UserLevel
= Moderator
| Participant
type alias Player = type alias Player =
{ level : UserLevel { level : UserLevel
, name : String , name : String
@ -45,52 +52,35 @@ type alias Player =
} }
init : { room : String, roomName : String, playerName : String } -> ( Model, Cmd Msg ) type UserLevel
init { room, roomName, playerName } = = 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 let
preparedRooms = room =
Dict.fromList { id = id
[ -- Room created from mocked entry page
( "a0fd1422-abd9-434e-9d7c-883294b2992c"
, { id = "a0fd1422-abd9-434e-9d7c-883294b2992c"
, name = roomName , name = roomName
, players = , players = Dict.empty
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 }
)
]
}
)
]
in in
( { room = Dict.get room preparedRooms ( { room = Just room
, player = "00000000-0000-0000-0000-000000000000" , player = player
, playerName = playerName , playerName = playerName
, showVotes = False
} }
, Cmd.none , Cmd.none
) )
@ -116,6 +106,11 @@ update key msg model =
, Cmd.none , Cmd.none
) )
Reveal ->
( { model | showVotes = True }
, Cmd.none
)
Reset -> Reset ->
( { model ( { model
| room = | room =
@ -126,6 +121,7 @@ update key msg model =
(\k v -> { v | vote = Nothing }) (\k v -> { v | vote = Nothing })
room.players room.players
} }
, showVotes = False
} }
, Cmd.none , Cmd.none
) )
@ -146,8 +142,22 @@ update key msg model =
room.players room.players
} }
in in
( 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 ) ( { model | room = Just newRoom }, Cmd.none )
Err _ ->
( model, Cmd.none )
Nothing -> Nothing ->
case msg of case msg of
_ -> _ ->
@ -168,7 +178,7 @@ view model =
{ title = room.name { title = room.name
, body = , body =
[ navBar { title = room.name, playerName = player.name } [ 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 : String -> Room -> Bool -> Element Msg
viewRoom player room = viewRoom player room showVotes =
let let
myVote = myVote =
Dict.get player room.players Dict.get player room.players
@ -203,7 +213,7 @@ viewRoom player room =
[ el [ width (fillPortion 3), alignTop ] <| [ el [ width (fillPortion 3), alignTop ] <|
viewCards myVote viewCards myVote
, el [ width (fillPortion 1), alignTop ] <| , el [ width (fillPortion 1), alignTop ] <|
viewPlayers (Dict.values room.players) viewPlayers (Dict.values room.players) showVotes
] ]
, moderatorTools , moderatorTools
] ]
@ -259,8 +269,8 @@ viewCards selected =
List.map card [ "1", "3", "5", "8", "13" ] List.map card [ "1", "3", "5", "8", "13" ]
viewPlayers : List Player -> Element Msg viewPlayers : List Player -> Bool -> Element Msg
viewPlayers playerList = viewPlayers playerList showVotes =
table [ width fill ] table [ width fill ]
{ data = playerList { data = playerList
, columns = , columns =
@ -275,11 +285,19 @@ viewPlayers playerList =
, width = px 50 , width = px 50
, view = , view =
\player -> \player ->
let
vote =
if showVotes then
player.vote
else
Maybe.map (\_ -> "") player.vote
in
el el
[ padding 10 [ padding 10
, Font.alignRight , Font.alignRight
] ]
(text <| Maybe.withDefault " " player.vote) (text <| Maybe.withDefault " " vote)
} }
] ]
} }
@ -287,12 +305,20 @@ viewPlayers playerList =
moderatorTools : Element Msg moderatorTools : Element Msg
moderatorTools = moderatorTools =
UI.actionButton row [ centerX, spacing 20 ]
[ UI.actionButton
[ centerX ]
{ isActive = True
, onPress = Reveal
, label = text "Reveal"
}
, UI.actionButton
[ centerX ] [ centerX ]
{ isActive = True { isActive = True
, onPress = Reset , onPress = Reset
, label = text "Reset" , label = text "Reset"
} }
]
joinForm : Room -> String -> Element Msg joinForm : Room -> String -> Element Msg
@ -337,3 +363,38 @@ joinForm room playerName =
++ " People are already here" ++ " 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)

View file

@ -12,9 +12,10 @@ defmodule Planningpoker.Application do
# Start the PubSub system # Start the PubSub system
{Phoenix.PubSub, name: Planningpoker.PubSub}, {Phoenix.PubSub, name: Planningpoker.PubSub},
# Start the Endpoint (http/https) # Start the Endpoint (http/https)
PlanningpokerWeb.Endpoint PlanningpokerWeb.Endpoint,
# Start a worker by calling: Planningpoker.Worker.start_link(arg) # Start a worker by calling: Planningpoker.Worker.start_link(arg)
# {Planningpoker.Worker, arg} # {Planningpoker.Worker, arg}
PlanningpokerWeb.Presence
] ]
# See https://hexdocs.pm/elixir/Supervisor.html # See https://hexdocs.pm/elixir/Supervisor.html

View 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

View 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

View file

@ -2,7 +2,7 @@ defmodule PlanningpokerWeb.UserSocket do
use Phoenix.Socket use Phoenix.Socket
## Channels ## Channels
# channel "room:*", PlanningpokerWeb.RoomChannel channel "room:*", PlanningpokerWeb.RoomChannel
# Socket params are passed from the client and can # Socket params are passed from the client and can
# be used to verify and authenticate a user. After # be used to verify and authenticate a user. After
@ -16,8 +16,8 @@ defmodule PlanningpokerWeb.UserSocket do
# See `Phoenix.Token` documentation for examples in # See `Phoenix.Token` documentation for examples in
# performing token verification on connect. # performing token verification on connect.
@impl true @impl true
def connect(_params, socket, _connect_info) do def connect(params, socket, _connect_info) do
{:ok, socket} {:ok, assign(socket, :player_id, params["player_id"])}
end end
# Socket id's are topics that allow you to identify all sockets for a given user: # 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. # Returning `nil` makes this socket anonymous.
@impl true @impl true
def id(_socket), do: nil def id(_socket), do: "player:${socket.assigns.player_id}"
end end