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/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"
}

View file

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

View file

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

View file

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

View file

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

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

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.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)

View file

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

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