Get game passwords and public/private games working, errors better represented client-side, HttpData rework.

This commit is contained in:
Gareth Latty
2019-06-27 21:22:13 +01:00
parent 8f9e1d91a9
commit 12ef605479
50 changed files with 1160 additions and 520 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ import MassiveDecks.Card.Source.Cardcast as Cardcast
import MassiveDecks.Card.Source.Fake as Fake
import MassiveDecks.Card.Source.Methods exposing (..)
import MassiveDecks.Card.Source.Model exposing (..)
import MassiveDecks.Components as Components
import MassiveDecks.Components.Form.Message exposing (Message)
import MassiveDecks.Model exposing (..)
import Weightless as Wl
import Weightless.Attributes as WlA
@@ -52,7 +52,7 @@ name source =
() |> (methods source |> .name)
validate : Source -> Maybe (Components.Message msg)
validate : Source -> Maybe (Message msg)
validate source =
() |> (methods source |> .problem)
@@ -6,7 +6,7 @@ import Html.Events as HtmlE
import MassiveDecks.Card.Source.Cardcast.Model exposing (..)
import MassiveDecks.Card.Source.Methods as Source
import MassiveDecks.Card.Source.Model as Source exposing (Source)
import MassiveDecks.Components as Components
import MassiveDecks.Components.Form.Message as Message exposing (Message)
import MassiveDecks.Model exposing (..)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
@@ -46,10 +46,10 @@ equals (PlayCode pc) source =
False
problem : PlayCode -> Maybe (Components.Message msg)
problem : PlayCode -> Maybe (Message msg)
problem (PlayCode pc) =
if String.isEmpty pc then
Strings.CardcastEmptyPlayCode |> Components.info |> Just
Strings.CardcastEmptyPlayCode |> Message.info |> Just
else
Nothing
@@ -2,14 +2,14 @@ module MassiveDecks.Card.Source.Methods exposing (Methods)
import Html exposing (Html)
import MassiveDecks.Card.Source.Model exposing (..)
import MassiveDecks.Components as Components
import MassiveDecks.Components.Form.Message exposing (Message)
import MassiveDecks.Model exposing (..)
{-| A collection of methods applied to the source data.
-}
type alias Methods msg =
{ problem : () -> Maybe (Components.Message msg)
{ problem : () -> Maybe (Message msg)
, details : () -> Details
, tooltip : () -> Maybe ( String, Html msg )
, logo : () -> Maybe (Html msg)
+2 -122
View File
@@ -1,109 +1,28 @@
module MassiveDecks.Components exposing
( Fix
, Message
, Severity(..)
, error
, errorWithFix
, floatingActionButton
, formSection
( floatingActionButton
, iconButton
, iconButtonStyled
, info
, linkButton
, message
, warning
)
{-| Reusable interface elements.
-}
import FontAwesome.Attributes as Icon
import FontAwesome.Icon as Icon exposing (Icon)
import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Attributes.Aria as Aria
import Html.Events as HtmlE
import MassiveDecks.Model exposing (..)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import Weightless as Wl
import Weightless.Attributes as WlA
{-| A section containing inputs and messages..
{-| Something that looks like a link but is actually a button suitable for handling events on click.
-}
formSection : Shared -> String -> Html msg -> List (Message msg) -> Html msg
formSection shared id component messages =
Html.div [ HtmlA.id id, HtmlA.class "form-section" ] (component :: (messages |> List.filterMap (message shared)))
type alias Fix msg =
{ text : MdString
, msg : msg
}
type Severity
= Info
| Warning
| Error
type alias Message msg =
Maybe (InternalMessage msg)
message : Shared -> Message msg -> Maybe (Html msg)
message shared msg =
msg |> Maybe.map (internalMessage shared)
info : MdString -> Message msg
info mdString =
Just
{ severity = Info
, description = mdString
, fix = Nothing
}
warning : MdString -> Message msg
warning mdString =
Just
{ severity = Warning
, description = mdString
, fix = Nothing
}
error : MdString -> Message msg
error mdString =
Just
{ severity = Error
, description = mdString
, fix = Nothing
}
errorWithFix : MdString -> MdString -> msg -> Message msg
errorWithFix errorString fixString fix =
Just
{ severity = Error
, description = errorString
, fix = Just { text = fixString, msg = fix }
}
linkButton : List (Html.Attribute msg) -> List (Html msg) -> Html msg
linkButton attrs contents =
Html.span (HtmlA.class "link-button" :: Aria.role "button" :: HtmlA.tabindex 0 :: attrs) contents
--Html.button (HtmlA.class "link-button" :: attrs) contents
{-| A button that is just an icon.
-}
iconButton : List (Html.Attribute msg) -> Icon -> Html msg
@@ -124,42 +43,3 @@ Only one of these should exist on screen at any time.
floatingActionButton : List (Html.Attribute msg) -> Icon -> Html msg
floatingActionButton attrs icon =
Wl.button (List.concat [ [ WlA.fab ], attrs ]) [ Icon.view icon ]
{- Private -}
type alias InternalMessage msg =
{ severity : Severity
, description : MdString
, fix : Maybe (Fix msg)
}
internalMessage : Shared -> InternalMessage msg -> Html msg
internalMessage shared { severity, description, fix } =
let
( class, icon ) =
case severity of
Info ->
( "info", Icon.infoCircle )
Warning ->
( "warning", Icon.exclamationTriangle )
Error ->
( "inline-error", Icon.exclamationCircle )
fixLink =
case fix of
Just { text, msg } ->
[ Html.text " ", linkButton [ msg |> HtmlE.onClick ] [ text |> Lang.html shared ] ]
Nothing ->
[]
in
Html.span [ HtmlA.class class ]
[ Icon.viewStyled [ Icon.fw ] icon
, Html.span [] ((description |> Lang.html shared) :: fixLink)
]
@@ -0,0 +1,14 @@
module MassiveDecks.Components.Form exposing (section)
import Html exposing (Html)
import Html.Attributes as HtmlA
import MassiveDecks.Components.Form.Message as Message exposing (Message)
import MassiveDecks.Model exposing (Shared)
{-| A section containing inputs and messages..
-}
section : Shared -> String -> Html msg -> List (Message msg) -> Html msg
section shared id component messages =
Html.div [ HtmlA.id id, HtmlA.class "form-section" ]
(component :: (messages |> List.filterMap (Message.view shared)))
@@ -0,0 +1,123 @@
module MassiveDecks.Components.Form.Message exposing
( Fix
, Message
, Severity(..)
, error
, errorWithFix
, info
, none
, view
, warning
)
import FontAwesome.Attributes as Icon
import FontAwesome.Icon as Icon
import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import MassiveDecks.Components as Components
import MassiveDecks.Model exposing (..)
import MassiveDecks.Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
type alias Fix msg =
{ text : MdString
, msg : msg
}
type Severity
= Info
| Warning
| Error
type alias Message msg =
Maybe (InternalMessage msg)
view : Shared -> Message msg -> Maybe (Html msg)
view shared msg =
msg |> Maybe.map (internalMessage shared)
info : MdString -> Message msg
info mdString =
Just
{ severity = Info
, description = mdString
, fix = Nothing
}
warning : MdString -> Message msg
warning mdString =
Just
{ severity = Warning
, description = mdString
, fix = Nothing
}
error : MdString -> Message msg
error mdString =
Just
{ severity = Error
, description = mdString
, fix = Nothing
}
errorWithFix : MdString -> MdString -> msg -> Message msg
errorWithFix errorString fixString fix =
Just
{ severity = Error
, description = errorString
, fix = Just { text = fixString, msg = fix }
}
none : Message msg
none =
Nothing
{- Private -}
type alias InternalMessage msg =
{ severity : Severity
, description : MdString
, fix : Maybe (Fix msg)
}
internalMessage : Shared -> InternalMessage msg -> Html msg
internalMessage shared { severity, description, fix } =
let
( class, icon ) =
case severity of
Info ->
( "info", Icon.infoCircle )
Warning ->
( "warning", Icon.exclamationTriangle )
Error ->
( "inline-error", Icon.exclamationCircle )
fixLink =
case fix of
Just { text, msg } ->
[ Html.text " ", Components.linkButton [ msg |> HtmlE.onClick ] [ text |> Lang.html shared ] ]
Nothing ->
[]
in
Html.span [ HtmlA.class class ]
[ Icon.viewStyled [ Icon.fw ] icon
, Html.span [] ((description |> Lang.html shared) :: fixLink)
]
+4 -11
View File
@@ -2,7 +2,6 @@ module MassiveDecks.Error exposing (view)
import Html exposing (Html)
import Html.Attributes as HtmlA
import Http
import Json.Decode as Json
import MassiveDecks.Error.Model exposing (..)
import MassiveDecks.Model exposing (Shared)
@@ -104,13 +103,10 @@ render error =
case error of
Http httpError ->
case httpError of
Http.NetworkError ->
NetworkError ->
Model Strings.NetworkError Nothing
Http.BadBody message ->
Model Strings.BadPayloadError ("Decoding error: " ++ message |> Just)
Http.BadStatus code ->
BadStatus code ->
case code of
504 ->
Model Strings.ServerDownError Nothing
@@ -118,10 +114,10 @@ render error =
_ ->
Model Strings.BadStatusError (code |> String.fromInt |> Just)
Http.Timeout ->
Timeout ->
Model Strings.TimeoutError Nothing
Http.BadUrl url ->
BadUrl url ->
Model Strings.BadUrlError (Just ("Url: " ++ url))
Token tokenError ->
@@ -141,6 +137,3 @@ render error =
Json jsonError ->
Model Strings.BadPayloadError (jsonError |> Json.errorToString |> Just)
Generic string ->
Model string Nothing
+11 -4
View File
@@ -1,21 +1,28 @@
module MassiveDecks.Error.Model exposing
( Error(..)
, HttpError(..)
, Overlay
)
import Http
import Json.Decode as Json
import MassiveDecks.Pages.Lobby.Model as Lobby
import MassiveDecks.Strings exposing (MdString)
{-| A generic error for the application as a whole.
-}
type Error
= Http Http.Error
= Http HttpError
| Json Json.Error
| Token Lobby.TokenDecodingError
| Generic MdString
{-| An error from an HTTP request.
-}
type HttpError
= BadUrl String
| Timeout
| NetworkError
| BadStatus Int
{-| An overlay displaying a number of errors.
@@ -7,9 +7,11 @@ module MassiveDecks.Game.Player exposing
, isCzar
, playState
, role
, roleDescription
)
import MassiveDecks.Game.Round as Round exposing (Round)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.User as User
import Set
@@ -42,6 +44,16 @@ type Role
| RPlayer
roleDescription : Role -> MdString
roleDescription toDescribe =
case toDescribe of
RCzar ->
Strings.Czar
RPlayer ->
Strings.Player
{-| The state of a player in regards to playing into a round.
-}
type PlayState
@@ -8,6 +8,7 @@ module MassiveDecks.Game.Round exposing
, Playing
, Revealing
, Round(..)
, Stage(..)
, complete
, data
, idDecoder
@@ -15,12 +16,15 @@ module MassiveDecks.Game.Round exposing
, noPick
, playing
, revealing
, stage
, stageDescription
)
import Dict exposing (Dict)
import Json.Decode as Json
import MassiveDecks.Card.Model as Card
import MassiveDecks.Card.Play as Play exposing (Play)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.User as User
import Set exposing (Set)
@@ -36,6 +40,51 @@ idDecoder =
Json.string |> Json.map Id
{-| The stage of the round.
-}
type Stage
= SPlaying
| SRevealing
| SJudging
| SComplete
{-| Get the stage of the given round.
-}
stage : Round -> Stage
stage round =
case round of
P _ ->
SPlaying
R _ ->
SRevealing
J _ ->
SJudging
C _ ->
SComplete
{-| A description of the given stage.
-}
stageDescription : Stage -> MdString
stageDescription toDescribe =
case toDescribe of
SPlaying ->
Strings.Playing
SRevealing ->
Strings.Revealing
SJudging ->
Strings.Judging
SComplete ->
Strings.Complete
{-| A round during a game.
-}
type Round
+155 -109
View File
@@ -10,6 +10,7 @@ module MassiveDecks.Models.Decoders exposing
, lobbyState
, lobbySummary
, lobbyToken
, mdError
, privilege
, revealingRound
, settings
@@ -44,28 +45,34 @@ import MassiveDecks.User as User exposing (User)
import Set exposing (Set)
unknownValue : String -> String -> Json.Decoder a
unknownValue name value =
("Unknown " ++ name ++ ": \"" ++ value ++ "\".") |> Json.fail
castStatus : Json.Decoder Cast.Status
castStatus =
Json.field "status" Json.string
|> Json.andThen
(\status ->
case status of
"NoDevicesAvailable" ->
Json.succeed Cast.NoDevicesAvailable
Json.field "status" Json.string |> Json.andThen castStatusByName
"NotConnected" ->
Json.succeed Cast.NotConnected
"Connecting" ->
Json.succeed Cast.Connecting
castStatusByName : String -> Json.Decoder Cast.Status
castStatusByName name =
case name of
"NoDevicesAvailable" ->
Json.succeed Cast.NoDevicesAvailable
"Connected" ->
Json.map Cast.Connected
(Json.field "name" Json.string)
"NotConnected" ->
Json.succeed Cast.NotConnected
_ ->
Json.fail ("Unknown cast status: " ++ status)
)
"Connecting" ->
Json.succeed Cast.Connecting
"Connected" ->
Json.map Cast.Connected
(Json.field "name" Json.string)
_ ->
unknownValue "cast status" name
castFlags : Json.Decoder Cast.Flags
@@ -115,7 +122,7 @@ sourceByName name =
Json.field "playCode" Json.string |> Json.map (Cardcast.playCode >> Source.Cardcast)
_ ->
"Unknown source \"" ++ name ++ "\"" |> Json.fail
unknownValue "source" name
tokenValidity : Json.Decoder (Dict Lobby.Token Bool)
@@ -130,16 +137,15 @@ lobbyToken =
language : Json.Decoder Language
language =
Json.string
|> Json.andThen
(\code ->
case Lang.fromCode code of
Just lang ->
Json.succeed lang
Json.string |> Json.andThen languageFromCode
Nothing ->
Json.fail ("Unknown language code: '" ++ code ++ "'.")
)
languageFromCode : String -> Json.Decoder Language
languageFromCode code =
code
|> Lang.fromCode
|> Maybe.map Json.succeed
|> Maybe.withDefault (unknownValue "language code" code)
lobby : Json.Decoder Lobby
@@ -150,7 +156,6 @@ lobby =
(Json.field "users" users)
(Json.field "owner" userId)
(Json.field "config" config)
--(Json.field "game" game |> Json.map (Game.emptyModel >> Just))
(Json.maybe (Json.field "game" game |> Json.map Game.emptyModel))
@@ -167,11 +172,12 @@ game =
config : Json.Decoder Configure.Config
config =
Json.map4 Configure.Config
Json.map5 Configure.Config
(Json.field "rules" rules)
(Json.field "decks" (Json.list deck))
(Json.maybe (Json.field "password" Json.string))
(Json.field "version" Json.string)
(Json.maybe (Json.field "public" Json.bool) |> Json.map (Maybe.withDefault False))
deck : Json.Decoder Configure.Deck
@@ -238,19 +244,20 @@ player =
control : Json.Decoder Player.Control
control =
Json.string
|> Json.andThen
(\name ->
case name of
"Human" ->
Json.succeed Player.Human
Json.string |> Json.andThen controlByName
"Computer" ->
Json.succeed Player.Computer
_ ->
Json.fail ("Unknown controller: " ++ name)
)
controlByName : String -> Json.Decoder Player.Control
controlByName name =
case name of
"Human" ->
Json.succeed Player.Human
"Computer" ->
Json.succeed Player.Computer
_ ->
unknownValue "user controller" name
score : Json.Decoder Player.Score
@@ -275,79 +282,84 @@ user =
userConnection : Json.Decoder User.Connection
userConnection =
Json.string
|> Json.andThen
(\name ->
case name of
"Connected" ->
Json.succeed User.Connected
Json.string |> Json.andThen userConnectionByName
"Disconnected" ->
Json.succeed User.Disconnected
_ ->
Json.fail ("Unknown connection state: " ++ name)
)
userConnectionByName : String -> Json.Decoder User.Connection
userConnectionByName name =
case name of
"Connected" ->
Json.succeed User.Connected
"Disconnected" ->
Json.succeed User.Disconnected
_ ->
unknownValue "connection state" name
userPresence : Json.Decoder User.Presence
userPresence =
Json.string
|> Json.andThen
(\name ->
case name of
"Joined" ->
Json.succeed User.Joined
Json.string |> Json.andThen userPresenceByName
"Left" ->
Json.succeed User.Left
_ ->
Json.fail ("Unknown presence state: " ++ name)
)
userPresenceByName : String -> Json.Decoder User.Presence
userPresenceByName name =
case name of
"Joined" ->
Json.succeed User.Joined
"Left" ->
Json.succeed User.Left
_ ->
unknownValue "presence state" name
role : Json.Decoder User.Role
role =
Json.string
|> Json.andThen
(\name ->
case name of
"Spectator" ->
Json.succeed User.Spectator
Json.string |> Json.andThen roleByName
"Player" ->
Json.succeed User.Player
_ ->
Json.fail ("Unknown role: " ++ name)
)
roleByName : String -> Json.Decoder User.Role
roleByName name =
case name of
"Spectator" ->
Json.succeed User.Spectator
"Player" ->
Json.succeed User.Player
_ ->
unknownValue "user role" name
lobbyState : Json.Decoder Lobby.State
lobbyState =
Json.string
|> Json.andThen
(\name ->
case name of
"Playing" ->
Json.succeed Lobby.Playing
Json.string |> Json.andThen lobbyStateByName
"SettingUp" ->
Json.succeed Lobby.SettingUp
_ ->
Json.fail ("Unknown lobby state: " ++ name)
)
lobbyStateByName : String -> Json.Decoder Lobby.State
lobbyStateByName name =
case name of
"Playing" ->
Json.succeed Lobby.Playing
"SettingUp" ->
Json.succeed Lobby.SettingUp
_ ->
unknownValue "lobby state" name
lobbySummary : Json.Decoder LobbyBrowser.Summary
lobbySummary =
Json.map4 LobbyBrowser.Summary
Json.map5 LobbyBrowser.Summary
(Json.field "name" Json.string)
(Json.field "gameCode" gameCode)
(Json.field "state" lobbyState)
(Json.field "users" userSummary)
(Json.maybe (Json.field "password" Json.bool) |> Json.map (Maybe.withDefault False))
userId : Json.Decoder User.Id
@@ -357,19 +369,20 @@ userId =
privilege : Json.Decoder User.Privilege
privilege =
Json.string
|> Json.andThen
(\name ->
case name of
"Privileged" ->
Json.succeed User.Privileged
Json.string |> Json.andThen privilegeByName
"Unprivileged" ->
Json.succeed User.Unprivileged
_ ->
Json.fail ("Unknown privilege level '" ++ name ++ "'.")
)
privilegeByName : String -> Json.Decoder User.Privilege
privilegeByName name =
case name of
"Privileged" ->
Json.succeed User.Privileged
"Unprivileged" ->
Json.succeed User.Unprivileged
_ ->
unknownValue "privilege level" name
userSummary : Json.Decoder LobbyBrowser.UserSummary
@@ -426,6 +439,9 @@ eventByName name =
"HouseRuleChanged" ->
configured houseRuleChanged
"PublicSet" ->
configured publicSet
"GameStarted" ->
gameStarted
@@ -448,7 +464,7 @@ eventByName name =
gameEvent roundFinished
_ ->
Json.fail ("Unknown event '" ++ name ++ "'.")
unknownValue "event" name
roundFinished : Json.Decoder Events.GameEvent
@@ -514,7 +530,7 @@ houseRuleChangeFromName name =
maybeHouseRuleChange (Json.field "cost" Json.int) Rules.Reboot Rules.RebootChange
_ ->
Json.fail ("Unknown house rule (for change) \"" ++ name ++ "\"")
unknownValue "house rule (for change)" name
maybeHouseRuleChange :
@@ -564,6 +580,12 @@ presence state =
(Json.field "user" userId)
publicSet : Json.Decoder Events.ConfigChanged
publicSet =
Json.map (\public -> Events.PublicSet { public = public })
(Json.field "public" Json.bool)
passwordSet : Json.Decoder Events.ConfigChanged
passwordSet =
Json.map (\password -> Events.PasswordSet { password = password })
@@ -620,7 +642,7 @@ deckChangeByName name =
Json.field "reason" failReason |> Json.map (\r -> Events.Fail { reason = r })
_ ->
"Unknown deck change \"" ++ name ++ "\"" |> Json.fail
unknownValue "deck change" name
failReason : Json.Decoder Source.LoadFailureReason
@@ -638,7 +660,7 @@ failReasonByName name =
Json.succeed Source.NotFound
_ ->
"Unknown failure reason \"" ++ name ++ "\"" |> Json.fail
unknownValue "failure reason" name
play : Json.Decoder Play
@@ -721,7 +743,7 @@ transformByName maybeName =
Json.succeed Parts.Capitalize
_ ->
Json.fail ("Unknown transform '" ++ name ++ "'.")
unknownValue "transform" name
round : Json.Decoder Round
@@ -745,7 +767,7 @@ roundByName name =
completeRound |> Json.map Round.C
_ ->
Json.fail ("Unknown round stage '" ++ name ++ "'.")
unknownValue "round stage" name
playerSet : Json.Decoder (Set User.Id)
@@ -809,7 +831,7 @@ playerRoleByName name =
Json.succeed Player.RPlayer
_ ->
Json.fail ("Unknown player role: '" ++ name ++ "'.")
unknownValue "player role" name
userRole : Json.Decoder User.Role
@@ -827,7 +849,7 @@ userRoleByName name =
Json.succeed User.Player
_ ->
Json.fail ("Unknown user role: '" ++ name ++ "'.")
unknownValue "user role" name
mdError : Json.Decoder MdError
@@ -868,14 +890,38 @@ mdErrorByName name =
Json.succeed MdError.OutOfCardsError |> Json.map MdError.Game
_ ->
Json.fail ("Unknown error: '" ++ name ++ "'.")
unknownValue "error" name
stage : Json.Decoder Round.Stage
stage =
Json.string |> Json.andThen stageByName
stageByName : String -> Json.Decoder Round.Stage
stageByName name =
case name of
"Playing" ->
Json.succeed Round.SPlaying
"Revealing" ->
Json.succeed Round.SRevealing
"Judging" ->
Json.succeed Round.SJudging
"Complete" ->
Json.succeed Round.SComplete
_ ->
unknownValue "round stage" name
incorrectRoundStageError : Json.Decoder MdError.ActionExecutionError
incorrectRoundStageError =
Json.map2 (\s -> \e -> MdError.IncorrectRoundStageError { stage = s, expected = e })
(Json.field "stage" Json.string)
(Json.field "expected" Json.string)
(Json.field "stage" stage)
(Json.field "expected" stage)
configEditConflictError : Json.Decoder MdError.ActionExecutionError
@@ -917,12 +963,12 @@ authenticationErrorByName name =
Json.succeed MdError.InvalidLobbyPassword
_ ->
Json.fail ("Unknown authentication failure reason: '" ++ name ++ "'.")
unknownValue "authentication failure reason" name
lobbyError : Json.Decoder MdError.LobbyNotFoundError
lobbyError =
Json.field "reason" Json.string |> Json.andThen lobbyErrorByName
Json.string |> Json.andThen lobbyErrorByName
lobbyErrorByName : String -> Json.Decoder MdError.LobbyNotFoundError
@@ -935,4 +981,4 @@ lobbyErrorByName name =
Json.succeed MdError.DoesNotExist
_ ->
Json.fail ("Unknown lobby error reason: '" ++ name ++ "'.")
unknownValue "lobby not found error" name
@@ -86,7 +86,9 @@ language l =
userRegistration : User.Registration -> Json.Value
userRegistration r =
Json.object
[ ( "name", r.name |> Json.string ) ]
(( "name", r.name |> Json.string )
:: (r.password |> Maybe.map (\p -> [ ( "password", p |> Json.string ) ]) |> Maybe.withDefault [])
)
houseRuleChange : Rules.HouseRuleChange -> Json.Value
+52 -1
View File
@@ -4,10 +4,13 @@ module MassiveDecks.Models.MdError exposing
, GameStateError(..)
, LobbyNotFoundError(..)
, MdError(..)
, describe
)
import MassiveDecks.Game.Player as Player
import MassiveDecks.Game.Round as Round
import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.User as User
@@ -21,7 +24,7 @@ type MdError
type ActionExecutionError
= IncorrectPlayerRole { role : Player.Role, expected : Player.Role }
| IncorrectUserRole { role : User.Role, expected : User.Role }
| IncorrectRoundStageError { stage : String, expected : String }
| IncorrectRoundStageError { stage : Round.Stage, expected : Round.Stage }
| ConfigEditConflictError { version : String, expected : String }
| Unprivileged
| GameNotStarted
@@ -40,3 +43,51 @@ type LobbyNotFoundError
type GameStateError
= OutOfCardsError
describe : MdError -> MdString
describe error =
case error of
ActionExecution aee ->
case aee of
IncorrectPlayerRole { role, expected } ->
Strings.IncorrectPlayerRoleError { role = Player.roleDescription role, expected = Player.roleDescription expected }
IncorrectUserRole { role, expected } ->
Strings.IncorrectUserRoleError { role = User.roleDescription role, expected = User.roleDescription expected }
IncorrectRoundStageError { stage, expected } ->
Strings.IncorrectRoundStageError { stage = Round.stageDescription stage, expected = Round.stageDescription expected }
ConfigEditConflictError _ ->
Strings.ConfigEditConflictError
Unprivileged ->
Strings.UnprivilegedError
GameNotStarted ->
Strings.GameNotStartedError
Authentication ae ->
case ae of
IncorrectIssuer ->
Strings.IncorrectIssuerError
InvalidAuthentication ->
Strings.InvalidAuthenticationError
InvalidLobbyPassword ->
Strings.InvalidLobbyPasswordError
LobbyNotFound { reason, gameCode } ->
case reason of
Closed ->
Strings.LobbyClosedError { gameCode = GameCode.toString gameCode }
DoesNotExist ->
Strings.LobbyDoesNotExistError { gameCode = GameCode.toString gameCode }
Game gse ->
case gse of
OutOfCardsError ->
Strings.OutOfCardsError
+1 -1
View File
@@ -566,7 +566,7 @@ playStateDetail round userId =
Just (Round.P p) ->
case Player.playState p userId of
Player.Playing ->
Just Strings.Playing
Just Strings.StillPlaying
Player.Played ->
Just Strings.Played
@@ -6,6 +6,7 @@ module MassiveDecks.Pages.Lobby.Actions exposing
, reveal
, setHandSize
, setPassword
, setPublic
, setScoreLimit
, startGame
, submit
@@ -56,6 +57,11 @@ changeHouseRule value =
configAction "ChangeHouseRule" [ ( "change", value |> Encoders.houseRuleChange ) ]
setPublic : Bool -> String -> Cmd msg
setPublic value =
configAction "SetPublic" [ ( "public", value |> Json.bool ) ]
submit : List Card.Id -> Cmd msg
submit play =
action "Submit" [ ( "play", play |> Json.list Json.string ) ]
@@ -17,6 +17,8 @@ import MassiveDecks.Card.Source as Source
import MassiveDecks.Card.Source.Cardcast.Model as Cardcast
import MassiveDecks.Card.Source.Model as Source exposing (Source)
import MassiveDecks.Components as Components
import MassiveDecks.Components.Form as Form
import MassiveDecks.Components.Form.Message as Message exposing (Message)
import MassiveDecks.Game.Rules as Rules
import MassiveDecks.Messages as Global
import MassiveDecks.Model exposing (..)
@@ -24,7 +26,7 @@ import MassiveDecks.Pages.Lobby.Actions as Actions
import MassiveDecks.Pages.Lobby.Configure.Messages exposing (..)
import MassiveDecks.Pages.Lobby.Configure.Model exposing (..)
import MassiveDecks.Pages.Lobby.Events as Events
import MassiveDecks.Pages.Lobby.GameCode exposing (GameCode)
import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode)
import MassiveDecks.Pages.Lobby.Invite as Invite
import MassiveDecks.Pages.Lobby.Messages as Lobby
import MassiveDecks.Pages.Lobby.Model as Lobby exposing (Lobby)
@@ -47,11 +49,13 @@ init =
, scoreLimit = Just 25
, tab = Decks
, password = Nothing
, passwordVisible = False
, houseRules =
{ rando = Nothing
, packingHeat = Nothing
, reboot = Nothing
}
, public = False
}
@@ -63,7 +67,9 @@ updateFromConfig config model =
, handSize = config.rules.handSize
, scoreLimit = config.rules.scoreLimit
, password = config.password
, passwordVisible = model.passwordVisible
, houseRules = config.rules.houseRules
, public = config.public
}
@@ -106,6 +112,9 @@ update msg model config =
in
( { model | password = value }, cmd )
TogglePasswordVisibility ->
( { model | passwordVisible = not model.passwordVisible }, Cmd.none )
HouseRuleChange target value ->
let
send =
@@ -113,6 +122,13 @@ update msg model config =
in
( { model | houseRules = model.houseRules |> Rules.apply value }, send )
PublicChange target value ->
let
send =
ifRemote (Actions.setPublic value config.version) target
in
( { model | public = value }, send )
view : Shared -> Bool -> Model -> GameCode -> Lobby -> Config -> Html Global.Msg
view shared canEdit model gameCode lobby config =
@@ -122,7 +138,7 @@ view shared canEdit model gameCode lobby config =
[ Html.h2 [] [ lobby.name |> Html.text ]
, Html.div []
[ Invite.button shared
, Strings.GameCode { code = gameCode } |> Lang.html shared
, Strings.GameCode { code = GameCode.toString gameCode } |> Lang.html shared
]
]
, Wl.tabGroup [ WlA.align WlA.Center ] (tabs |> List.map (tab shared model.tab))
@@ -160,6 +176,11 @@ applyChange configChange oldConfig oldConfigure =
, { oldConfigure | houseRules = oldConfigure.houseRules |> Rules.apply change }
)
Events.PublicSet { public } ->
( { oldConfig | public = public }
, { oldConfigure | public = public }
)
{- Private -}
@@ -178,7 +199,7 @@ startGameSegment shared canEdit lobby config =
else
[ WlA.disabled ]
in
Components.formSection shared
Form.section shared
"start-game"
(Wl.button startGameAttrs [ Strings.StartGame |> Lang.html shared ])
(startErrors |> Maybe.justIf canEdit |> Maybe.withDefault [])
@@ -227,7 +248,7 @@ applyDeckChange event config configure =
( newConfig, newConfigure )
startGameProblems : Dict User.Id User -> Config -> List (Components.Message Global.Msg)
startGameProblems : Dict User.Id User -> Config -> List (Message Global.Msg)
startGameProblems users config =
let
-- We assume decks will have calls/responses.
@@ -242,7 +263,7 @@ startGameProblems users config =
deckIssues =
if noDecks then
[ Components.errorWithFix
[ Message.errorWithFix
Strings.NeedAtLeastOneDeck
Strings.NoDecksHint
("CAHBS" |> Cardcast.playCode |> Source.Cardcast |> AddDeck |> lift)
@@ -250,14 +271,14 @@ startGameProblems users config =
]
else if loadingDecks then
[ Strings.WaitForDecks |> Components.info |> Just ]
[ Strings.WaitForDecks |> Message.info |> Just ]
else
[ Strings.MissingCardType { cardType = Strings.Call }
|> Components.error
|> Message.error
|> Maybe.justIf ((summaries .calls |> List.sum) < 1)
, Strings.MissingCardType { cardType = Strings.Response }
|> Components.error
|> Message.error
|> Maybe.justIf ((summaries .responses |> List.sum) < 1)
]
@@ -265,7 +286,7 @@ startGameProblems users config =
users |> Dict.values |> List.filter (\user -> user.role == User.Player) |> List.length
playerIssues =
[ Components.errorWithFix
[ Message.errorWithFix
Strings.NeedAtLeastThreePlayers
Strings.Invite
(Lobby.ToggleInviteDialog |> Global.LobbyMsg)
@@ -288,7 +309,7 @@ ifRemote cmd target =
tabs : List Tab
tabs =
[ Decks, Rules, Game ]
[ Decks, Rules, Privacy ]
tab : Shared -> Tab -> Tab -> Html Global.Msg
@@ -309,8 +330,8 @@ tabName target =
Rules ->
Strings.ConfigureRules
Game ->
Strings.ConfigureGame
Privacy ->
Strings.ConfigurePrivacy
tabContent : Shared -> Bool -> Model -> Lobby -> Config -> Html Global.Msg
@@ -322,7 +343,7 @@ tabContent shared canEdit model lobby config =
Rules ->
configureRules shared canEdit model lobby config
Game ->
Privacy ->
configureGameSettings shared canEdit model lobby config
@@ -347,7 +368,7 @@ handSize shared canEdit model config =
value =
model.handSize
in
Components.formSection shared
Form.section shared
"hand-size"
(Html.div
[ HtmlA.class "multipart" ]
@@ -372,7 +393,7 @@ handSize shared canEdit model config =
(Icon.save |> Maybe.justIf (config.rules.handSize /= value) |> Maybe.withDefault Icon.check)
]
)
[ Components.info Strings.HandSizeDescription ]
[ Message.info Strings.HandSizeDescription ]
scoreLimit : Shared -> Bool -> Model -> Config -> Html Global.Msg
@@ -384,7 +405,7 @@ scoreLimit shared canEdit model config =
value =
model.scoreLimit
in
Components.formSection shared
Form.section shared
"score-limit"
(Html.div
[ HtmlA.class "multipart" ]
@@ -414,7 +435,7 @@ scoreLimit shared canEdit model config =
(Icon.save |> Maybe.justIf (config.rules.scoreLimit /= value) |> Maybe.withDefault Icon.check)
]
)
[ Components.info Strings.ScoreLimitDescription ]
[ Message.info Strings.ScoreLimitDescription ]
houseRules : Shared -> Bool -> Model -> Lobby -> Config -> Html Global.Msg
@@ -460,7 +481,7 @@ houseRule shared id { default, change, title, description, extract, insert } can
|> Maybe.withDefault []
in
Html.div [ HtmlA.classList [ ( "house-rule", True ), ( "enabled", enabled ) ] ]
[ Components.formSection
[ Form.section
shared
id
(Html.div [ HtmlA.class "multipart" ]
@@ -473,7 +494,7 @@ houseRule shared id { default, change, title, description, extract, insert } can
(Icon.check |> Maybe.justIf saved |> Maybe.withDefault Icon.save)
]
)
[ Components.info (localValue |> description) ]
[ Message.info (localValue |> description) ]
, Html.div [ HtmlA.class "house-rule-settings" ] settings
]
@@ -485,7 +506,7 @@ rando shared canEdit model config =
randoSettings : Shared -> Bool -> Rules.Rando -> (Rules.Rando -> Global.Msg) -> List (Html Global.Msg)
randoSettings shared canEdit value localChange =
[ Components.formSection
[ Form.section
shared
"rando-number"
(Wl.textField
@@ -503,7 +524,7 @@ randoSettings shared canEdit value localChange =
]
[]
)
[ Strings.HouseRuleRandoCardrissianNumberDescription |> Components.info ]
[ Strings.HouseRuleRandoCardrissianNumberDescription |> Message.info ]
]
@@ -524,7 +545,7 @@ reboot shared canEdit model config =
rebootSettings : Shared -> Bool -> Rules.Reboot -> (Rules.Reboot -> Global.Msg) -> List (Html Global.Msg)
rebootSettings shared canEdit value localChange =
[ Components.formSection
[ Form.section
shared
"reboot-cost"
(Wl.textField
@@ -542,7 +563,7 @@ rebootSettings shared canEdit value localChange =
]
[]
)
[ Strings.HouseRuleRebootCostDescription |> Components.info ]
[ Strings.HouseRuleRebootCostDescription |> Message.info ]
]
@@ -618,7 +639,7 @@ configureGameSettings shared canEdit model lobby config =
Nothing ->
[ "" |> WlA.value, WlA.disabled ]
, [ Strings.GamePassword |> Lang.string shared |> WlA.label
, [ Strings.LobbyPassword |> Lang.string shared |> WlA.label
, WlA.minLength 1
, WlA.outlined
, HtmlA.class "primary"
@@ -629,6 +650,7 @@ configureGameSettings shared canEdit model lobby config =
[ HtmlE.onInput (Just >> PasswordChange Local >> lift)
, HtmlE.onBlur (model.password |> PasswordChange Remote |> lift)
]
, [ WlA.Password |> WlA.type_ ] |> Maybe.justIf (not model.passwordVisible) |> Maybe.withDefault []
]
passwordSwitchAttrs =
@@ -645,12 +667,17 @@ configureGameSettings shared canEdit model lobby config =
]
password =
Components.formSection
Form.section
shared
"add-deck"
"password"
(Html.div [ HtmlA.class "multipart" ]
[ Wl.switch passwordSwitchAttrs
, Wl.textField passwordAttrs []
, Components.iconButton
[ TogglePasswordVisibility |> lift |> HtmlE.onClick
, WlA.disabled |> Maybe.justIf (Maybe.isNothing model.password) |> Maybe.withDefault HtmlA.nothing
]
(Icon.eyeSlash |> Maybe.justIf model.passwordVisible |> Maybe.withDefault Icon.eye)
, Components.iconButton
[ WlA.disabled
|> Maybe.justIf (not canEdit || config.password == model.password)
@@ -659,12 +686,27 @@ configureGameSettings shared canEdit model lobby config =
(Icon.save |> Maybe.justIf (config.password /= model.password) |> Maybe.withDefault Icon.check)
]
)
[ Components.info Strings.GamePasswordDescription
, Components.warning Strings.PasswordNotSecured
[ Message.info Strings.LobbyPasswordDescription
, Message.warning Strings.PasswordShared
, Message.warning Strings.PasswordNotSecured
]
public =
Form.section shared
"public"
(Html.div [ HtmlA.class "multipart" ]
[ Wl.switch
[ WlA.disabled |> Maybe.justIf (not canEdit) |> Maybe.withDefault (PublicChange Remote >> lift |> HtmlE.onCheck)
, WlA.checked |> Maybe.justIf model.public |> Maybe.withDefault HtmlA.nothing
]
, Html.span [ HtmlA.class "primary" ] [ Strings.Public |> Lang.html shared ]
]
)
[ Message.info Strings.PublicDescription ]
in
Html.div [ HtmlA.class "game-settings" ]
[ password
[ public
, password
]
@@ -681,7 +723,7 @@ addDeckWidget shared existing deckToAdd =
in
Html.form
[ submit |> Result.map (lift >> HtmlE.onSubmit) |> Result.withDefault HtmlA.nothing ]
[ Components.formSection
[ Form.section
shared
"add-deck"
(Html.div [ HtmlA.class "multipart" ]
@@ -707,12 +749,12 @@ addDeckWidget shared existing deckToAdd =
]
submitDeckAction : List Deck -> Source.External -> Result (Components.Message Global.Msg) Msg
submitDeckAction : List Deck -> Source.External -> Result (Message Global.Msg) Msg
submitDeckAction existing deckToAdd =
let
potentialProblem =
if List.any (.source >> Source.Ex >> Source.equals (Source.Ex deckToAdd)) existing then
Strings.DeckAlreadyAdded |> Components.error |> Just
Strings.DeckAlreadyAdded |> Message.error |> Just
else
Source.validate (Source.Ex deckToAdd)
@@ -15,6 +15,8 @@ type Msg
| ScoreLimitChange Target (Maybe Int)
| PasswordChange Target (Maybe String)
| HouseRuleChange Target Rules.HouseRuleChange
| PublicChange Target Bool
| TogglePasswordVisibility
{-| We don't want to push every tiny change to the server. Instead we only push some changes.
@@ -13,7 +13,7 @@ import MassiveDecks.Game.Rules as Rules exposing (Rules)
type Tab
= Decks
| Rules
| Game
| Privacy
type alias DeckError =
@@ -28,8 +28,10 @@ type alias Model =
, handSize : Int
, scoreLimit : Maybe Int
, password : Maybe String
, passwordVisible : Bool
, tab : Tab
, houseRules : Rules.HouseRules
, public : Bool
}
@@ -46,4 +48,5 @@ type alias Config =
, decks : List Deck
, password : Maybe String
, version : String
, public : Bool
}
@@ -43,6 +43,7 @@ type ConfigChanged
| ScoreLimitSet { limit : Maybe Int }
| PasswordSet { password : Maybe String }
| HouseRuleChanged { change : Rules.HouseRuleChange }
| PublicSet { public : Bool }
type DeckChange
@@ -6,6 +6,8 @@ import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Json.Decode
import MassiveDecks.Components as Components
import MassiveDecks.Components.Form as Form
import MassiveDecks.Components.Form.Message as Message
import MassiveDecks.Messages as Global
import MassiveDecks.Model exposing (..)
import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode)
@@ -18,7 +20,6 @@ import MassiveDecks.Util.Html as Html
import QRCode
import Url exposing (Url)
import Weightless as Wl
import Weightless.Attributes as WlA
{-| A button to show the dialog.
@@ -54,8 +55,8 @@ dialog shared gameCode password open =
]
Icon.times
, Wl.card [ onClickNoPropegation Global.NoOp ]
[ Strings.InviteExplanation { gameCode = gameCode, password = password } |> Lang.html shared
, Components.formSection shared
[ Strings.InviteExplanation { gameCode = GameCode.toString gameCode, password = password } |> Lang.html shared
, Form.section shared
"invite-link"
(Html.div [ HtmlA.class "multipart" ]
[ Html.input
@@ -68,7 +69,7 @@ dialog shared gameCode password open =
, Components.iconButton [ "invite-link-field" |> Global.Copy |> HtmlE.onClick ] Icon.copy
]
)
[ Components.info Strings.InviteLinkHelp ]
[ Message.info Strings.InviteLinkHelp ]
, lobbyUrl |> qr
]
]
@@ -81,7 +82,7 @@ overlay shared gameCode =
Html.div [ HtmlA.class "invite" ]
[ Html.div [ HtmlA.class "join-info" ]
[ Html.p [] [ Strings.JoinTheGame |> Lang.html shared ]
, Html.p [] [ Strings.GameCode { code = gameCode } |> Lang.html shared ]
, Html.p [] [ Strings.GameCode { code = GameCode.toString gameCode } |> Lang.html shared ]
, Html.p [] [ Html.text (stripProtocol shared.origin) ]
]
, Html.div [ HtmlA.class "qr-code" ] [ url shared gameCode |> qr ]
@@ -15,13 +15,7 @@ decode token =
body
|> Base64.decode
|> Result.mapError TokenBase64Error
|> Result.andThen
(\json ->
json
|> Json.decodeString decodeClaims
|> Result.mapError TokenJsonError
|> Result.map (\claims -> Auth token claims)
)
|> Result.andThen (decodeJson token)
_ ->
Err (InvalidTokenStructure token)
@@ -31,6 +25,14 @@ decode token =
{- Private -}
decodeJson : Token -> String -> Result TokenDecodingError Auth
decodeJson token json =
json
|> Json.decodeString decodeClaims
|> Result.mapError TokenJsonError
|> Result.map (\claims -> Auth token claims)
decodeClaims : Json.Decoder Claims
decodeClaims =
Json.map3 Claims
@@ -18,6 +18,7 @@ import MassiveDecks.Card.Source.Cardcast.Model as Cardcast
import MassiveDecks.Card.Source.Model as Source
import MassiveDecks.Messages exposing (..)
import MassiveDecks.Model exposing (..)
import MassiveDecks.Pages.Lobby.GameCode as GameCode
import MassiveDecks.Pages.Route as Route
import MassiveDecks.Pages.Spectate.Messages as Spectate
import MassiveDecks.Pages.Spectate.Model exposing (..)
@@ -119,7 +120,7 @@ view shared model =
]
, Html.div [ HtmlA.class "join-info" ]
[ Html.p [] [ Strings.JoinTheGame |> Lang.html shared ]
, Html.p [] [ Strings.GameCode { code = model.route.lobby.gameCode } |> Lang.html shared ]
, Html.p [] [ Strings.GameCode { code = GameCode.toString model.route.lobby.gameCode } |> Lang.html shared ]
, Html.p [] [ Html.text (stripProtocol shared.origin) ]
]
, Html.div [ HtmlA.class "qr-code" ] qr
+98 -28
View File
@@ -13,15 +13,17 @@ import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Http
import MassiveDecks.Card as Card
import MassiveDecks.Card.Model as Card exposing (Card)
import MassiveDecks.Card.Parts as Parts
import MassiveDecks.Card.Source.Model as Source
import MassiveDecks.Components as Components
import MassiveDecks.Components.Form as Form
import MassiveDecks.Components.Form.Message as Message
import MassiveDecks.Error as Error
import MassiveDecks.Error.Model as Error exposing (Error)
import MassiveDecks.Messages as Global
import MassiveDecks.Model exposing (..)
import MassiveDecks.Models.MdError as MdError exposing (MdError)
import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode)
import MassiveDecks.Pages.Lobby.Model as Lobby
import MassiveDecks.Pages.Lobby.Token as Token
@@ -34,6 +36,7 @@ import MassiveDecks.Requests.Api as Api
import MassiveDecks.Requests.HttpData as HttpData
import MassiveDecks.Requests.HttpData.Messages as HttpData
import MassiveDecks.Requests.HttpData.Model as HttpData exposing (HttpData)
import MassiveDecks.Requests.Request as Request
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util as Util
@@ -78,6 +81,7 @@ init shared r =
, lobbies = lobbies
, newLobbyRequest = HttpData.initLazy
, joinLobbyRequest = HttpData.initLazy
, password = Nothing
}
, lobbiesCmd
)
@@ -106,7 +110,7 @@ update msg model =
Just gc ->
Util.modelLift (\jlr -> { model | joinLobbyRequest = jlr })
(HttpData.update
(joinGameRequest gc model.name)
(joinGameRequest gc model.name model.password)
httpDataMsg
model.joinLobbyRequest
)
@@ -117,6 +121,26 @@ update msg model =
LobbyBrowserMsg lbm ->
Util.modelLift (\lobbies -> { model | lobbies = lobbies }) (LobbyBrowser.update lbm model.lobbies)
PasswordChanged newPassword ->
( { model | password = Just newPassword }, Cmd.none )
PasswordWrong ->
if Maybe.isJust model.password then
let
jlr =
model.joinLobbyRequest
newJlr =
{ jlr
| error = MdError.InvalidLobbyPassword |> MdError.Authentication |> Just
, loading = False
}
in
( { model | joinLobbyRequest = newJlr }, Cmd.none )
else
( { model | password = Just "", joinLobbyRequest = HttpData.initLazy }, Cmd.none )
view : Shared -> Model -> List (Html Global.Msg)
view shared model =
@@ -170,20 +194,38 @@ view shared model =
startGameRequest : String -> HttpData.Pull Global.Msg
startGameRequest name =
HttpData.interceptedRequest
(Api.newLobby { owner = { name = name } })
(Token.decode >> Result.mapError Error.Token)
(StartGame >> Global.StartMsg)
(Global.JoinLobby name)
Api.newLobby
((HttpData.Response >> StartGame >> Global.StartMsg)
|> Request.intercept Request.passthrough Request.passthrough (Request.replace (Global.JoinLobby name))
)
{ owner = { name = name, password = Nothing } }
|> Http.request
joinGameRequest : GameCode -> String -> HttpData.Pull Global.Msg
joinGameRequest gameCode name =
HttpData.interceptedRequest
(Api.joinLobby gameCode { name = name })
(Token.decode >> Result.mapError Error.Token)
(StartGame >> Global.StartMsg)
(Global.JoinLobby name)
joinGameRequest : GameCode -> String -> Maybe String -> HttpData.Pull Global.Msg
joinGameRequest gameCode name password =
Api.joinLobby
((HttpData.Response >> JoinGame >> Global.StartMsg)
|> Request.intercept Request.passthrough (Request.maybeReplace onJoinError) (Request.replace (Global.JoinLobby name))
)
gameCode
{ name = name, password = password }
|> Http.request
onJoinError : MdError -> Maybe Global.Msg
onJoinError error =
case error of
MdError.Authentication ae ->
case ae of
MdError.InvalidLobbyPassword ->
PasswordWrong |> Global.StartMsg |> Just
_ ->
Nothing
_ ->
Nothing
loadingOrLoaded : Model -> Bool
@@ -276,16 +318,13 @@ newContent shared model =
Icon.view Icon.play
error =
model.newLobbyRequest.error
|> Maybe.map
(\e ->
[ Error.view shared (Route.Start model.route) e ]
)
|> Maybe.withDefault []
model.newLobbyRequest.generalError
|> Maybe.map (Error.view shared (Route.Start model.route))
|> Maybe.withDefault Html.nothing
in
Html.div [ HtmlA.class "new-game start-tab" ]
(List.concat
[ error
[ [ error ]
, nameField shared model
, [ Wl.button
[ buttonAttr
@@ -315,12 +354,23 @@ joinContent shared model =
else
Icon.view Icon.play
error =
model.joinLobbyRequest.generalError
|> Maybe.map (Error.view shared (Route.Start model.route))
|> Maybe.withDefault Html.nothing
maybePasswordField =
model.password
|> Maybe.map (passwordField shared model.joinLobbyRequest.error)
|> Maybe.withDefault []
in
Html.div [ HtmlA.class "join-game start-tab" ]
(List.concat
[ rejoinSection shared model
[ [ error ]
, rejoinSection shared model
, nameField shared model
, [ Components.formSection shared
, [ Form.section shared
"game-code-input"
(Wl.textField
[ HtmlA.class "game-code-input"
@@ -331,8 +381,10 @@ joinContent shared model =
]
[]
)
[ Components.info Strings.GameCodeHowToAcquire ]
, Wl.button
[ Message.info Strings.GameCodeHowToAcquire ]
]
, maybePasswordField
, [ Wl.button
[ buttonAttr
]
[ buttonIcon, Strings.PlayGame |> Lang.html shared ]
@@ -341,6 +393,24 @@ joinContent shared model =
)
passwordField : Shared -> Maybe MdError -> String -> List (Html Global.Msg)
passwordField shared error password =
[ Form.section shared
"password-input"
(Wl.textField
[ PasswordChanged >> Global.StartMsg |> HtmlE.onInput
, WlA.value password
, WlA.outlined
, Strings.LobbyPassword |> Lang.label shared
]
[]
)
[ Message.info Strings.LobbyRequiresPassword
, error |> Maybe.map (MdError.describe >> Message.error) |> Maybe.withDefault Message.none
]
]
rejoinSection : Shared -> Model -> List (Html Global.Msg)
rejoinSection shared model =
let
@@ -364,7 +434,7 @@ rejoinLobby shared result =
Ok auth ->
Html.li []
[ Html.a [ Route.Lobby { gameCode = auth.claims.gc } |> Route.href ]
[ Strings.RejoinGame { code = auth.claims.gc } |> Lang.html shared
[ Strings.RejoinGame { code = GameCode.toString auth.claims.gc } |> Lang.html shared
]
]
|> Just
@@ -386,7 +456,7 @@ nameField shared model =
_ =
""
in
[ Components.formSection shared
[ Form.section shared
"name-input"
(Wl.textField
[ NameChanged
@@ -11,6 +11,7 @@ import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Html.Keyed as HtmlK
import Http
import MassiveDecks.Messages as Global
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Pages.Lobby.GameCode as GameCode
@@ -26,7 +27,9 @@ import MassiveDecks.Requests.HttpData.Messages as HttpData
import MassiveDecks.Requests.HttpData.Model as HttpData exposing (HttpData)
import MassiveDecks.Strings as Strings exposing (MdString(..))
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Html as Html
import MassiveDecks.Util.List as List
import MassiveDecks.Util.Maybe as Maybe
import Weightless as Wl
import Weightless.Attributes as WlA
@@ -73,7 +76,7 @@ refresh model =
requestLobbySummaries : HttpData.Pull Global.Msg
requestLobbySummaries =
HttpData.request Api.lobbySummaries |> Cmd.map (SummaryUpdate >> lift)
Api.lobbySummaries (HttpData.Response >> SummaryUpdate >> lift) |> Http.request
lift : Msg -> Global.Msg
@@ -145,7 +148,7 @@ stateGroup shared ( state, lobbies ) =
( state |> stateId
, Html.li []
[ Html.div []
[ Html.h2 [] [ state |> stateDescription |> Lang.html shared ]
[ Html.h3 [] [ state |> stateDescription |> Lang.html shared ]
, HtmlK.ul [] (lobbies |> List.map (lobby shared))
]
]
@@ -159,8 +162,16 @@ lobby shared data =
[ HtmlE.onClick (Route.Start { section = Start.Join (Just data.gameCode) } |> Global.ChangePage)
, WlA.clickable
]
[ Html.span [] [ Html.text data.name ]
, Html.span [] [ Strings.GameCode { code = data.gameCode } |> Lang.html shared ]
[ Html.span [ HtmlA.class "lobby-name", Strings.LobbyRequiresPassword |> Lang.title shared ]
[ Html.text data.name
, Html.text " "
, Icon.lock
|> Icon.viewStyled []
|> Maybe.justIf data.password
|> Maybe.withDefault Html.nothing
]
, Html.span [ HtmlA.class "lobby-game-code" ]
[ Strings.GameCode { code = GameCode.toString data.gameCode } |> Lang.html shared ]
, Icon.viewStyled
[ HtmlA.title "Join Game"
, WlA.listItemSlot WlA.AfterItem
@@ -5,4 +5,4 @@ import MassiveDecks.Requests.HttpData.Messages as HttpData
type Msg
= SummaryUpdate (HttpData.Msg (List Summary))
= SummaryUpdate (HttpData.Msg () (List Summary))
@@ -4,6 +4,7 @@ module MassiveDecks.Pages.Start.LobbyBrowser.Model exposing
, UserSummary
)
import MassiveDecks.Error.Model exposing (Error)
import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode)
import MassiveDecks.Pages.Lobby.Model as Lobby
import MassiveDecks.Requests.HttpData.Model exposing (HttpData)
@@ -12,7 +13,7 @@ import MassiveDecks.Requests.HttpData.Model exposing (HttpData)
{-| The model for the lobby browser.
-}
type alias Model =
HttpData (List Summary)
HttpData () (List Summary)
{-| An external summary of a lobby.
@@ -22,6 +23,7 @@ type alias Summary =
, gameCode : GameCode
, state : Lobby.State
, users : UserSummary
, password : Bool
}
@@ -1,5 +1,6 @@
module MassiveDecks.Pages.Start.Messages exposing (Msg(..))
import MassiveDecks.Models.MdError exposing (MdError)
import MassiveDecks.Pages.Lobby.Model as Lobby
import MassiveDecks.Pages.Start.LobbyBrowser.Messages as LobbyBrowser
import MassiveDecks.Requests.HttpData.Messages as HttpData
@@ -8,6 +9,8 @@ import MassiveDecks.Requests.HttpData.Messages as HttpData
type Msg
= GameCodeChanged String
| NameChanged String
| StartGame (HttpData.Msg Lobby.Auth)
| JoinGame (HttpData.Msg Lobby.Auth)
| StartGame (HttpData.Msg () Lobby.Auth)
| JoinGame (HttpData.Msg MdError Lobby.Auth)
| LobbyBrowserMsg LobbyBrowser.Msg
| PasswordChanged String
| PasswordWrong
@@ -3,6 +3,7 @@ module MassiveDecks.Pages.Start.Model exposing
, Model
)
import MassiveDecks.Models.MdError exposing (LobbyNotFoundError, MdError)
import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode)
import MassiveDecks.Pages.Lobby.Model as Lobby
import MassiveDecks.Pages.Start.LobbyBrowser.Model as LobbyBrowser
@@ -18,8 +19,9 @@ type alias Model =
, lobbies : LobbyBrowser.Model
, name : String
, gameCode : Maybe GameCode
, newLobbyRequest : HttpData Lobby.Auth
, joinLobbyRequest : HttpData Lobby.Auth
, newLobbyRequest : HttpData () Lobby.Auth
, joinLobbyRequest : HttpData MdError Lobby.Auth
, password : Maybe String
}
+31 -14
View File
@@ -7,27 +7,31 @@ module MassiveDecks.Requests.Api exposing
import Dict exposing (Dict)
import Http
import Json.Decode
import Json.Decode as Json
import MassiveDecks.Error.Model as Error
import MassiveDecks.Models.Decoders as Decoders
import MassiveDecks.Models.Encoders as Encoders
import MassiveDecks.Models.MdError exposing (MdError)
import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode)
import MassiveDecks.Pages.Lobby.Model as Lobby
import MassiveDecks.Pages.Lobby.Token as Token
import MassiveDecks.Pages.Start.LobbyBrowser.Model as LobbyBrowser
import MassiveDecks.Pages.Start.Model as Start
import MassiveDecks.Requests.Request exposing (Request)
import MassiveDecks.Requests.Request as Request exposing (Request)
import MassiveDecks.User as User
import MassiveDecks.Util.Result as Result
import Url.Builder
{-| List the public lobbies.
-}
lobbySummaries : Request (List LobbyBrowser.Summary)
lobbySummaries =
lobbySummaries : (Request.Response () (List LobbyBrowser.Summary) -> msg) -> Request msg
lobbySummaries msg =
{ method = "GET"
, headers = []
, url = url [ "games" ]
, body = Http.emptyBody
, expect = Http.expectJson identity (Json.Decode.list Decoders.lobbySummary)
, expect = Request.expectResponse msg noError (Json.list Decoders.lobbySummary)
, timeout = Nothing
, tracker = Nothing
}
@@ -35,37 +39,37 @@ lobbySummaries =
{-| Create a new lobby.
-}
newLobby : Start.LobbyCreation -> Request Lobby.Token
newLobby creation =
newLobby : (Request.Response () Lobby.Auth -> msg) -> Start.LobbyCreation -> Request msg
newLobby msg creation =
{ method = "POST"
, headers = []
, url = url [ "games" ]
, body = creation |> Encoders.lobbyCreation |> Http.jsonBody
, expect = Http.expectJson identity Decoders.lobbyToken
, expect = Request.expectResponse (decodeToken >> msg) noError Decoders.lobbyToken
, timeout = Nothing
, tracker = Nothing
}
joinLobby : GameCode -> User.Registration -> Request Lobby.Token
joinLobby gameCode registration =
joinLobby : (Request.Response MdError Lobby.Auth -> msg) -> GameCode -> User.Registration -> Request msg
joinLobby msg gameCode registration =
{ method = "POST"
, headers = []
, url = url [ "games", gameCode |> GameCode.toString ]
, body = registration |> Encoders.userRegistration |> Http.jsonBody
, expect = Http.expectJson identity Decoders.lobbyToken
, expect = Request.expectResponse (decodeToken >> msg) Decoders.mdError Decoders.lobbyToken
, timeout = Nothing
, tracker = Nothing
}
checkAlive : List Lobby.Token -> Request (Dict Lobby.Token Bool)
checkAlive tokens =
checkAlive : (Request.Response () (Dict Lobby.Token Bool) -> msg) -> List Lobby.Token -> Request msg
checkAlive msg tokens =
{ method = "POST"
, headers = []
, url = url [ "games", "alive" ]
, body = tokens |> Encoders.checkAlive |> Http.jsonBody
, expect = Http.expectJson identity Decoders.tokenValidity
, expect = Request.expectResponse msg noError Decoders.tokenValidity
, timeout = Nothing
, tracker = Nothing
}
@@ -75,6 +79,19 @@ checkAlive tokens =
{- Private -}
decodeToken : Request.Response error Lobby.Token -> Request.Response error Lobby.Auth
decodeToken =
Request.map
Request.GeneralError
Request.SpecificError
(Token.decode >> Result.unifiedMap (Error.Token >> Request.GeneralError) Request.Value)
url : List String -> String
url path =
Url.Builder.absolute ([ "api" ] ++ path) []
noError : Json.Decoder ()
noError =
Json.succeed ()
@@ -2,11 +2,8 @@ module MassiveDecks.Requests.HttpData exposing
( autoRefresh
, init
, initLazy
, interceptedRequest
, loadingOrLoaded
, mappedRequest
, refreshButton
, request
, update
, view
)
@@ -17,17 +14,14 @@ import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Http
import MassiveDecks.Error as Error
import MassiveDecks.Error.Model as Error exposing (Error)
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Pages.Route exposing (Route)
import MassiveDecks.Requests.HttpData.Messages exposing (..)
import MassiveDecks.Requests.HttpData.Model exposing (..)
import MassiveDecks.Requests.Request exposing (Request)
import MassiveDecks.Requests.Request as Request exposing (Request)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Result as Result
import Time
import Weightless as Wl
import Weightless.Attributes as WlA
@@ -35,35 +29,35 @@ import Weightless.Attributes as WlA
{-| Set up the empty HttpData and send a request to load the data.
-}
init : Pull msg -> ( HttpData result, Cmd msg )
init : Pull msg -> ( HttpData error result, Cmd msg )
init req =
( initLazy, req )
{-| Set up the empty HttpData with no initial request, the request can be made later.
-}
initLazy : HttpData result
initLazy : HttpData error result
initLazy =
{ loading = False, data = Nothing, error = Nothing }
{ loading = False, data = Nothing, error = Nothing, generalError = Nothing }
{-| Tries to refresh the data every X milliseconds.
-}
autoRefresh : Float -> Sub (Msg result)
autoRefresh : Float -> Sub (Msg error result)
autoRefresh every =
Time.every every (\_ -> Pull)
{-| If the data has been loaded, or is currently loading.
-}
loadingOrLoaded : HttpData result -> Bool
loadingOrLoaded : HttpData error result -> Bool
loadingOrLoaded model =
model.loading || model.data /= Nothing
{-| Update the data with the response from the request.
-}
update : Pull msg -> Msg result -> HttpData result -> ( HttpData result, Cmd msg )
update : Pull msg -> Msg error result -> HttpData error result -> ( HttpData error result, Cmd msg )
update req msg model =
case msg of
Pull ->
@@ -79,32 +73,30 @@ update req msg model =
{ model | loading = False }
in
case result of
Ok response ->
( { loadingStoppedModel
| data = Just response
, error = Nothing
}
, Cmd.none
)
Request.Value response ->
( { loadingStoppedModel | data = Just response, generalError = Nothing }, Cmd.none )
Err error ->
Request.SpecificError error ->
( { loadingStoppedModel | error = Just error }, Cmd.none )
Request.GeneralError generalError ->
( { loadingStoppedModel | generalError = Just generalError }, Cmd.none )
{-| A view over the data with any error received trying to load (or refresh) if it isn't there (or prefixed if during a
refresh).
-}
view : Shared -> Route -> (Msg result -> msg) -> (result -> Html msg) -> HttpData result -> Html msg
view : Shared -> Route -> (Msg error result -> msg) -> (result -> Html msg) -> HttpData error result -> Html msg
view shared route wrap viewResult model =
let
error =
model.error |> Maybe.map (Error.view shared route)
generalError =
model.generalError |> Maybe.map (Error.view shared route)
result =
model.data |> Maybe.map viewResult
content =
List.filterMap identity [ error, result ]
List.filterMap identity [ generalError, result ]
contentOrSpinner =
if List.isEmpty content then
@@ -118,7 +110,7 @@ view shared route wrap viewResult model =
{-| Show a refresh button for the data.
-}
refreshButton : Shared -> HttpData result -> Html (Msg result)
refreshButton : Shared -> HttpData error result -> Html (Msg error result)
refreshButton shared model =
let
title =
@@ -147,49 +139,3 @@ refreshButton shared model =
Wl.button
(List.concat [ style, title, onClick ])
[ Icon.viewStyled spin Icon.sync ]
{-| A request that just stores the result in the `HttpData`.
-}
request : Request result -> Cmd (Msg result)
request req =
mappedRequest req Ok
{-| A request that just maps the result before storage in the `HttpData`.
-}
mappedRequest : Request response -> (response -> Result Error result) -> Pull (Msg result)
mappedRequest req f =
interceptedRequest req f identity (Ok >> Response)
{-| A request that just maps the result before storage in the `HttpData`, as well as intercepting successes to perform
an action (note that this means the value will never get filled!
-}
interceptedRequest :
Request response
-> (response -> Result Error result)
-> (Msg result -> msg)
-> (result -> msg)
-> Pull msg
interceptedRequest req f wrap intercept =
req |> Http.request |> Cmd.map (mapResponse f wrap intercept)
{- Private -}
mapResponse :
(response -> Result Error result)
-> (Msg result -> msg)
-> (result -> msg)
-> Result Http.Error response
-> msg
mapResponse f wrap intercept result =
result
|> Result.mapError Error.Http
|> Result.andThen f
|> Result.map intercept
|> Result.mapError (Err >> Response >> wrap)
|> Result.unify
@@ -1,10 +1,10 @@
module MassiveDecks.Requests.HttpData.Messages exposing (Msg(..))
import MassiveDecks.Error.Model as Error exposing (Error)
import MassiveDecks.Requests.Request as Request
{-| A message for HttpData.
-}
type Msg result
type Msg error result
= Pull
| Response (Result Error result)
| Response (Request.Response error result)
@@ -8,10 +8,11 @@ import MassiveDecks.Error.Model as Error exposing (Error)
{-| Some data that is requested and received via an HTTP request.
-}
type alias HttpData result =
type alias HttpData error result =
{ loading : Bool
, data : Maybe result
, error : Maybe Error
, error : Maybe error
, generalError : Maybe Error
}
@@ -1,6 +1,19 @@
module MassiveDecks.Requests.Request exposing (Request)
module MassiveDecks.Requests.Request exposing
( Interception
, Request
, Response(..)
, expectResponse
, intercept
, map
, maybeReplace
, passthrough
, replace
)
import Http
import Json.Decode as Json
import MassiveDecks.Error.Model as Error exposing (Error)
import MassiveDecks.Util.Result as Result
type alias Request msg =
@@ -8,7 +21,136 @@ type alias Request msg =
, headers : List Http.Header
, url : String
, body : Http.Body
, expect : Http.Expect (Result Http.Error msg)
, expect : Http.Expect msg
, timeout : Maybe Float
, tracker : Maybe String
}
type Response error value
= GeneralError Error
| SpecificError error
| Value value
type Interception intercepted msg
= Intercept (intercepted -> Maybe msg)
| Continue
replace : (intercepted -> msg) -> Interception intercepted msg
replace originally =
Intercept (originally >> Just)
maybeReplace : (intercepted -> Maybe msg) -> Interception intercepted msg
maybeReplace =
Intercept
passthrough : Interception intercepted msg
passthrough =
Continue
expectResponse : (Response error value -> msg) -> Json.Decoder error -> Json.Decoder value -> Http.Expect msg
expectResponse mapMsg errorDecoder resultDecoder =
expectJsonOrError mapMsg errorDecoder resultDecoder
intercept :
Interception Error msg
-> Interception error msg
-> Interception value msg
-> (Response error value -> msg)
-> Response error value
-> msg
intercept generalErrorInterception errorInterception resultInterception otherwise response =
case response of
GeneralError error ->
case generalErrorInterception of
Intercept f ->
f error |> Maybe.withDefault (otherwise response)
Continue ->
otherwise response
SpecificError error ->
case errorInterception of
Intercept f ->
f error |> Maybe.withDefault (otherwise response)
Continue ->
otherwise response
Value result ->
case resultInterception of
Intercept f ->
f result |> Maybe.withDefault (otherwise response)
Continue ->
otherwise response
map : (Error -> msg) -> (error -> msg) -> (value -> msg) -> Response error value -> msg
map mapNetworkError mapError mapResult response =
case response of
GeneralError error ->
mapNetworkError error
SpecificError error ->
mapError error
Value result ->
mapResult result
{- Private -}
responseMap : (Response error value -> msg) -> Result Never (Response error value) -> msg
responseMap mapMsg value =
value |> Result.byDefinition |> mapMsg
expectJsonOrError : (Response error value -> msg) -> Json.Decoder error -> Json.Decoder value -> Http.Expect msg
expectJsonOrError toMsg errorDecoder valueDecoder =
Http.expectStringResponse (responseMap toMsg) (manageResponse errorDecoder valueDecoder)
manageResponse : Json.Decoder error -> Json.Decoder value -> Http.Response String -> Result Never (Response error value)
manageResponse errorDecoder valueDecoder response =
let
r =
case response of
Http.BadUrl_ url ->
url |> Error.BadUrl |> Error.Http |> GeneralError
Http.Timeout_ ->
Error.Timeout |> Error.Http |> GeneralError
Http.NetworkError_ ->
Error.NetworkError |> Error.Http |> GeneralError
Http.BadStatus_ metadata body ->
if metadata.statusCode >= 400 && metadata.statusCode < 500 then
case Json.decodeString errorDecoder body of
Ok error ->
error |> SpecificError
Err err ->
err |> Error.Json |> GeneralError
else
metadata.statusCode |> Error.BadStatus |> Error.Http |> GeneralError
Http.GoodStatus_ metadata body ->
case Json.decodeString valueDecoder body of
Ok value ->
value |> Value
Err err ->
err |> Error.Json |> GeneralError
in
Ok r
+18 -16
View File
@@ -17,6 +17,8 @@ import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Http
import MassiveDecks.Components as Components
import MassiveDecks.Components.Form as Form
import MassiveDecks.Components.Form.Message as Message
import MassiveDecks.LocalStorage as LocalStorage
import MassiveDecks.Messages as Global
import MassiveDecks.Model exposing (..)
@@ -24,12 +26,12 @@ import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode)
import MassiveDecks.Pages.Lobby.Model as Lobby
import MassiveDecks.Pages.Lobby.Token as Token
import MassiveDecks.Requests.Api as Api
import MassiveDecks.Requests.Request as Request
import MassiveDecks.Settings.Messages exposing (..)
import MassiveDecks.Settings.Model exposing (..)
import MassiveDecks.Strings as Strings
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Strings.Languages.Model as Lang exposing (Language)
import MassiveDecks.Util.Result as Result
import Weightless as Wl
import Weightless.Attributes as WlA
@@ -44,13 +46,8 @@ init settings =
else
settings.tokens
|> Dict.values
|> Api.checkAlive
|> Api.checkAlive (Request.map ignore ignore (RemoveInvalid >> Global.SettingsMsg))
|> Http.request
|> Cmd.map
(Result.map (RemoveInvalid >> Global.SettingsMsg)
>> Result.mapError (always Global.NoOp)
>> Result.unify
)
in
( { settings = settings
, open = False
@@ -160,6 +157,11 @@ auths settings =
{- Private -}
ignore : anything -> Global.Msg
ignore =
always Global.NoOp
changeSettings : (Settings -> Settings) -> Model -> ( Model, Cmd msg )
changeSettings f model =
let
@@ -178,7 +180,7 @@ compactSwitch shared =
settings =
model.settings
in
Components.formSection shared
Form.section shared
"compact-cards"
(Html.div
[ HtmlA.class "multipart" ]
@@ -193,7 +195,7 @@ compactSwitch shared =
]
]
)
[ Components.info Strings.CompactCardsExplanation ]
[ Message.info Strings.CompactCardsExplanation ]
@@ -202,7 +204,7 @@ compactSwitch shared =
speechSwitch : Shared -> Html Global.Msg
speechSwitch shared =
Components.formSection shared
Form.section shared
"speech"
(Html.div
[ HtmlA.class "multipart" ]
@@ -214,7 +216,7 @@ speechSwitch shared =
]
]
)
[ Components.info Strings.SpeechExplanation ]
[ Message.info Strings.SpeechExplanation ]
@@ -223,7 +225,7 @@ speechSwitch shared =
notificationsSwitch : Shared -> Html Global.Msg
notificationsSwitch shared =
Components.formSection shared
Form.section shared
"notifications"
(Html.div
[ HtmlA.class "multipart" ]
@@ -235,8 +237,8 @@ notificationsSwitch shared =
]
]
)
[ Components.info Strings.NotificationsExplanation
, Components.info Strings.NotificationsBrowserPermissions
[ Message.info Strings.NotificationsExplanation
, Message.info Strings.NotificationsBrowserPermissions
]
@@ -246,7 +248,7 @@ languageSelector shared =
selected =
Lang.currentLanguage shared
in
Components.formSection
Form.section
shared
"language"
(Wl.select
@@ -256,7 +258,7 @@ languageSelector shared =
]
(Lang.languages |> List.map (languageOption selected))
)
[ Components.info Strings.MissingLanguage ]
[ Message.info Strings.MissingLanguage ]
onChangeLang : String -> Global.Msg
+29 -9
View File
@@ -3,8 +3,6 @@ module MassiveDecks.Strings exposing (MdString(..))
{-| This module deals with text that is shown to the user in the application - strings.
-}
import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode)
{-| Each type represents a message that may be shown to the user. Some have arguments that are variable but should be
included in some form in the message.
@@ -36,7 +34,8 @@ type MdString
| NameLabel -- A label for a user name text field.
| NameInUse -- An error indicating the name the user asked for is already in use and they should try another.
| RejoinTitle -- A title for a list of games the user was previously in and might be able to rejoin.
| RejoinGame { code : GameCode } -- A description of the action of attempting to rejoin a game the user was previously in.
| RejoinGame { code : String } -- A description of the action of attempting to rejoin a game the user was previously in.
| LobbyRequiresPassword -- An explanation that the given lobby requires a password to join.
-- Rules
| CardsAgainstHumanity -- The name of "Cards Against Humanity" (https://cardsagainsthumanity.com/).
| Rules -- The title for a DESCRIPTION of the rules.
@@ -79,6 +78,8 @@ type MdString
-- Terms
| Czar -- The name for the "Card Czar" (the player that judges the round).
| CzarDescription -- A short description of what the czar does.
| Player -- A term for a player in the game with no special role.
| Spectator -- A term for a user who watches the game, but doesn't play in it.
| Call -- The name for a call card (a black card).
| CallDescription -- A short description of what a call is.
| Response -- The name for a response card (a white card).
@@ -87,11 +88,11 @@ type MdString
| PointDescription -- A short description of what a point is.
| GameCodeTerm -- The term for a unique code for a game that allows a user to find the game easily.
| GameCodeDescription -- A short description of what a game code is.
| GameCode { code : GameCode } -- Render a game code.
| GameCode { code : String } -- Render a game code.
| GameCodeSpecificDescription -- A short description of what a specific game code and how to use it.
| GameCodeHowToAcquire -- A short description of how to get a game code.
| Deck -- The name for a deck of cards.
| Playing -- A term for a person who is in a round, but has not yet submitted a play.
| StillPlaying -- A term for a person who is in a round, but has not yet submitted a play.
| PlayingDescription -- A description of a person who is in a round, but has not yet submitted a play.
| Played -- A term for a person who is in a round, and has submitted a play.
| PlayedDescription -- A description of a person who is in a round, and has submitted a play.
@@ -111,7 +112,7 @@ type MdString
| NumberOfCards { numberOfCards : Int } -- A number of cards as a single-digit number. This will be enhanced to render specially as a circle with the number in.
-- Lobby
| Invite -- A description of the action of inviting players to the game.
| InviteExplanation { gameCode : GameCode, password : Maybe String } -- An explanation of how players can join the game using the given game code and, potentially, password.
| InviteExplanation { gameCode : String, password : Maybe String } -- An explanation of how players can join the game using the given game code and, potentially, password.
| InviteLinkHelp -- An explanation that the users can send the link to people to invite them to the game.
| Cast -- A description of the action of casting a view of the game to another device (e.g: a TV).
| CastConnecting -- A description of trying to connect to the casting device.
@@ -153,22 +154,29 @@ type MdString
| DeckAlreadyAdded -- A description of the problem of the deck already being added to the game configuration.
| ConfigureDecks -- A name for the section of the configuration screen for changing the decks for the game.
| ConfigureRules -- A name for the section of the configuration screen for changing the rules for the game.
| ConfigureGame -- A name for the section of the configuration screen for changing the settings for the game.
| ConfigurePrivacy -- A name for the section of the configuration screen for changing the settings for the game.
| HandSize -- The name of the rule defining how many cards a player can hold in their hand.
| HandSizeDescription -- The description of the above rule.
| ScoreLimit -- The name of the rule defining how many points a player has to accumulate to win the game.
| ScoreLimitDescription -- The description of the above rule.
| NeedAtLeastOneDeck -- A description of the problem that the game needs at least one deck to start.
| NeedAtLeastThreePlayers -- A description of the problem that the game needs at least three players to start.
| PasswordShared -- A warning that game passwords are visible to anyone else in the game.
| PasswordNotSecured -- A warning that game passwords are not stored securely and should not be used elsewhere.
| GamePassword -- A short label for the game password.
| GamePasswordDescription -- A description of a password to stop random people entering your game.
| LobbyPassword -- A short label for the lobby password.
| LobbyPasswordDescription -- A description of a password to stop random people entering your lobby.
| StartGame -- A short description of the action of starting the game.
| Public -- The name of the setting for making the lobby public.
| PublicDescription -- A description of what the public setting does (makes the game visible in the lobby browser).
-- Game
| SubmitPlay -- A description of the action of submitting the play for the czar to judge.
| TakeBackPlay -- A description of the action of taking back a previously submitted play.
| JudgePlay -- A description of the action of choosing a play to win the round.
| LikePlay -- A description of the action of liking a play.
| Playing -- A description of the stage of the round where players are playing responses into the round.
| Revealing -- A description of the stage of the round where the czar is revealing the plays.
| Judging -- A description of the stage of the round where the czar is picking a winner.
| Complete -- A description of the stage of the round where it is finished.
-- Instructions
| PlayInstruction { numberOfCards : Int } -- Instruction to the player on how to play cards.
| SubmitInstruction -- Instruction to the player on how to submit their play.
@@ -194,6 +202,18 @@ type MdString
| BadStatusError -- An error where the server gave a response we didn't expect.
| BadPayloadError -- An error where the server gave a response we didn't understand.
| CastError -- An error where we the cast device couldn't connect to the game.
| IncorrectPlayerRoleError { role : MdString, expected : MdString } -- An error where the player tries to do something when they don't have the right role (czar/player).
| IncorrectUserRoleError { role : MdString, expected : MdString } -- An error where the user tries to do something when they don't have the right role (player/spectator).
| IncorrectRoundStageError { stage : MdString, expected : MdString } -- An error where the user tries to do something when it doesn't make sense given the stage of the game.
| ConfigEditConflictError -- An error where the user tries to make a change to the configuration, but someone else changed it first.
| UnprivilegedError -- An error where the user doesn't have the privileges to perform the action they are trying to do.
| GameNotStartedError -- An error where the game hasn't started and the user tries to do something that needs to be done in a game.
| IncorrectIssuerError -- An error where the user tries to authenticate using credentials that are out of date.
| InvalidAuthenticationError -- An error where the user tries to authenticate using credentials that are corrupted.
| InvalidLobbyPasswordError -- An error where the user tries to join a game with the wrong lobby password.
| LobbyClosedError { gameCode : String } -- An error where the user tries to join a game that has finished.
| LobbyDoesNotExistError { gameCode : String } -- An error where the user tries to join a game that never existed.
| OutOfCardsError -- An error where there weren't enough cards in the deck to deal cards that were needed, even after shuffling discards.
-- Language Names
| English -- The name of the English language (no specific dialect).
| BritishEnglish -- The name of the British dialect of the English language.
@@ -4,7 +4,6 @@ module MassiveDecks.Strings.Languages.En exposing (pack)
This is the primary language, strings here are the canonical representation, and are suitable to translate from.
-}
import MassiveDecks.Pages.Lobby.GameCode as GameCode
import MassiveDecks.Strings exposing (MdString(..))
import MassiveDecks.Strings.Translation as Translation exposing (Result(..))
@@ -109,6 +108,9 @@ translate mdString =
RejoinGame { code } ->
[ Text "Rejoin ", GameCode { code = code } |> Ref, Text "." ]
LobbyRequiresPassword ->
[ Text "You need a password to join this game. Try asking the person that invited you." ]
-- Rules
CardsAgainstHumanity ->
[ Text "Cards Against Humanity" ]
@@ -281,6 +283,12 @@ translate mdString =
CzarDescription ->
[ Text "The player judging the round." ]
Player ->
[ Text "Player" ]
Spectator ->
[ Text "Spectator" ]
Call ->
[ Text "Black Card" ]
@@ -306,7 +314,7 @@ translate mdString =
[ Text "A code that lets other people find and join your game." ]
GameCode { code } ->
[ Text (GameCode.toString code) ]
[ Text code ]
GameCodeSpecificDescription ->
[ Text "Give this game code to people and they can join the game." ]
@@ -317,7 +325,7 @@ translate mdString =
Deck ->
[ Text "Deck" ]
Playing ->
StillPlaying ->
[ Text "Playing" ]
PlayingDescription ->
@@ -417,13 +425,13 @@ translate mdString =
[ Text "Casting to ", Text deviceName, Text "." ]
Players ->
[ Text "Players" ]
[ Ref (Plural { singular = Player, amount = Nothing }) ]
PlayersDescription ->
[ Text "Users playing the game." ]
Spectators ->
[ Text "Audience" ]
[ Ref (Plural { singular = Spectator, amount = Nothing }) ]
SpectatorsDescription ->
[ Text "Users watching the game without playing." ]
@@ -532,8 +540,8 @@ translate mdString =
ConfigureRules ->
[ Text "Rules" ]
ConfigureGame ->
[ Text "Game" ]
ConfigurePrivacy ->
[ Text "Privacy" ]
HandSize ->
[ Text "Hand Size" ]
@@ -560,24 +568,34 @@ translate mdString =
NeedAtLeastThreePlayers ->
[ Text "You need at least three players to start the game." ]
PasswordNotSecured ->
[ Text "Please note that game passwords are "
, Em [ Text "not" ]
, Text " stored securely and are shared with everyone in the lobby"
, Text "given this, please "
, Em [ Text "do not" ]
, Text " use passwords you use elsewhere."
PasswordShared ->
[ Text "Anyone in the game can see the password! "
, Text "Hiding it above only affects you (useful if streaming, etc)."
]
GamePassword ->
PasswordNotSecured ->
[ Text "Game passwords are "
, Em [ Text "not" ]
, Text " stored securelygiven this, please "
, Em [ Text "do not" ]
, Text " use serious passwords you use elsewhere!"
]
LobbyPassword ->
[ Text "Game Password" ]
GamePasswordDescription ->
LobbyPasswordDescription ->
[ Text "A password to users must enter before they can join the game." ]
StartGame ->
[ Text "Start Game" ]
Public ->
[ Text "Public Game" ]
PublicDescription ->
[ Text "If enabled, the game will show up in the public game list for anyone to find." ]
-- Game
SubmitPlay ->
[ Text "Give these cards to the ", Ref Czar, Text " as your play for the round." ]
@@ -591,6 +609,18 @@ translate mdString =
LikePlay ->
[ Text "Add a like to this play." ]
Playing ->
[ Text "Playing" ]
Revealing ->
[ Text "Revealing" ]
Judging ->
[ Text "Judging" ]
Complete ->
[ Text "Finished" ]
-- Instructions
PlayInstruction { numberOfCards } ->
[ Text "You need to choose "
@@ -670,6 +700,46 @@ translate mdString =
CastError ->
[ Text "Sorry, something went wrong trying to connect to the game." ]
IncorrectPlayerRoleError { role, expected } ->
[ Text "You need to be a ", Ref expected, Text " to do that, but you are a ", Ref role, Text "." ]
IncorrectUserRoleError { role, expected } ->
[ Text "You need to be a ", Ref expected, Text " to do that, but you are a ", Ref role, Text "." ]
IncorrectRoundStageError { stage, expected } ->
[ Text "The round needs to be at the ", Ref expected, Text " stage to do that, but it is at the ", Ref stage, Text " stage." ]
ConfigEditConflictError ->
[ Text "Someone else changed the configuration before you, so their changes took priority." ]
UnprivilegedError ->
[ Text "You don't have the privileges to do that." ]
GameNotStartedError ->
[ Text "The game needs to started to do that." ]
IncorrectIssuerError ->
[ Text "Your credentials to join this game are out of date, the game no longer exists." ]
InvalidAuthenticationError ->
[ Text "Your credentials to join this game are corrupt." ]
InvalidLobbyPasswordError ->
[ Text "The game password you gave was wrong. Try typing it again and if it still doesn't work, ask the person who invited you again." ]
LobbyClosedError { gameCode } ->
[ Text "The game you wish to join (", Ref (GameCode { code = gameCode }), Text ") has ended." ]
LobbyDoesNotExistError { gameCode } ->
[ Text "The game code you entered ("
, Ref (GameCode { code = gameCode })
, Text ") doesn't exist. "
, Text "Try typing it again and if it still doesn't work, ask the person who invited you again."
]
OutOfCardsError ->
[ Text "There were not enough cards in the deck to deal everyone a hand! Try adding more decks in the game configuration." ]
-- Language Names
English ->
[ Text "English" ]
@@ -186,7 +186,7 @@ enhanceHtml context mdString unenhanced =
CardcastPlayCode ->
[ Html.blankA [ HtmlA.href "https://www.cardcastgame.com/browse" ] unenhanced ]
Playing ->
StillPlaying ->
term context PlayingDescription Icon.clock unenhanced
Played ->
+14
View File
@@ -6,11 +6,14 @@ module MassiveDecks.User exposing
, Registration
, Role(..)
, User
, roleDescription
)
{-| Operations and models for a user in the game.
-}
import MassiveDecks.Strings as Strings exposing (MdString)
{-| A unique Id for a user.
-}
@@ -32,6 +35,16 @@ type Role
| Spectator
roleDescription : Role -> MdString
roleDescription toDescribe =
case toDescribe of
Player ->
Strings.Player
Spectator ->
Strings.Spectator
{-| If the user is actively a part of the lobby.
-}
type Presence
@@ -50,6 +63,7 @@ type Connection
-}
type alias Registration =
{ name : String
, password : Maybe String
}
+22 -1
View File
@@ -1,7 +1,9 @@
module MassiveDecks.Util.Result exposing
( error
( byDefinition
, error
, isError
, isOk
, unifiedMap
, unify
)
@@ -35,6 +37,18 @@ isOk result =
Result.toMaybe result /= Nothing
{-| A result with a never error can be simplified to just a value.
-}
byDefinition : Result Never a -> a
byDefinition result =
case result of
Ok value ->
value
Err n ->
never n
{-| Turn a result that gives the same type in both cases into that result.
-}
unify : Result a a -> a
@@ -45,3 +59,10 @@ unify result =
Err value ->
value
{-| Map both sides of a result at the same time to the same type, and give the unified result.
-}
unifiedMap : (err -> a) -> (ok -> a) -> Result err ok -> a
unifiedMap errMap okMap =
Result.mapError errMap >> Result.map okMap >> unify
+10 -5
View File
@@ -5,13 +5,18 @@
align-items: center;
}
a {
.svg-inline--fa {
margin-right: 0.3em;
}
h2,
h3 {
margin: 0;
}
.subheader {
ul {
list-style: none;
padding: 0;
margin: 0;
}
.lobby-game-code {
font-size: 0.8em;
}
}
+8 -2
View File
@@ -8,10 +8,12 @@ import * as setHandSize from "./configure/set-hand-size";
import { SetHandSize } from "./configure/set-hand-size";
import * as setPassword from "./configure/set-password";
import { SetPassword } from "./configure/set-password";
import { SetPublic } from "./configure/set-public";
import * as setScoreLimit from "./configure/set-score-limit";
import { SetScoreLimit } from "./configure/set-score-limit";
import * as changeHouseRule from "./configure/change-house-rule";
import { ChangeHouseRule } from "./configure/change-house-rule";
import * as setPublic from "./configure/set-public";
/**
* An action to change the configuration of the lobby.
@@ -21,14 +23,16 @@ export type Configure =
| SetHandSize
| SetScoreLimit
| ChangeDecks
| ChangeHouseRule;
| ChangeHouseRule
| SetPublic;
const possible = new Set([
setPassword.is,
setHandSize.is,
setScoreLimit.is,
changeDecks.is,
changeHouseRule.is
changeHouseRule.is,
setPublic.is
]);
/**
@@ -65,5 +69,7 @@ export const handle: Handler<Configure> = (auth, lobby, action, config) => {
return changeDecks.handle(auth, lobby, action, config);
case "ChangeHouseRule":
return changeHouseRule.handle(auth, lobby, action, config);
case "SetPublic":
return setPublic.handle(auth, lobby, action, config);
}
};
@@ -0,0 +1,36 @@
import { Action } from "../../../action";
import * as event from "../../../event";
import * as publicSet from "../../../events/lobby-event/configured/public-set";
import { Handler } from "../../handler";
import * as configure from "../configure";
/**
* Set (or unset) the password for the lobby.
*/
export interface SetPublic extends configure.Base {
action: NameType;
public: boolean;
}
type NameType = "SetPublic";
const name: NameType = "SetPublic";
/**
* Check if an action is an change decks action.
* @param action The action to check.
*/
export const is = (action: Action): action is SetPublic =>
action.action === name;
export const handle: Handler<SetPublic> = (auth, lobby, action) => {
const config = lobby.config;
if (action.public !== config.public) {
const version = config.version + 1;
const resultEvent = publicSet.of(version.toString(), action.public);
config.public = action.public;
config.version = version;
return { lobby, events: [event.targetAll(resultEvent)] };
} else {
return {};
}
};
+32 -6
View File
@@ -4,7 +4,7 @@
import Ajv = require("ajv");
import { RegisterUser, CreateLobby, Action, CheckAlive } from "./validation";
export const ajv = new Ajv({
allErrors: true,
allErrors: false,
coerceTypes: false,
format: "fast",
nullable: true,
@@ -48,6 +48,9 @@ export const Schema = {
{
$ref: "#/definitions/SetPassword"
},
{
$ref: "#/definitions/SetPublic"
},
{
$ref: "#/definitions/SetScoreLimit"
},
@@ -188,7 +191,7 @@ export const Schema = {
description: "Set the hand size for the lobby.",
properties: {
action: {
$ref: "#/definitions/NameType_7"
$ref: "#/definitions/NameType_8"
},
change: {
$ref: "#/definitions/Change"
@@ -287,14 +290,18 @@ export const Schema = {
type: "string"
},
NameType_6: {
enum: ["SetScoreLimit"],
enum: ["SetPublic"],
type: "string"
},
NameType_7: {
enum: ["ChangeHouseRule"],
enum: ["SetScoreLimit"],
type: "string"
},
NameType_8: {
enum: ["ChangeHouseRule"],
type: "string"
},
NameType_9: {
enum: ["StartGame"],
type: "string"
},
@@ -424,12 +431,31 @@ export const Schema = {
required: ["action", "if"],
type: "object"
},
SetPublic: {
defaultProperties: [],
description: "Set (or unset) the password for the lobby.",
properties: {
action: {
$ref: "#/definitions/NameType_6"
},
if: {
description:
"If the config version doesn't match this, the operation will be rejected.\nThis avoids users accidentally overwriting each other's changes.",
type: "string"
},
public: {
type: "boolean"
}
},
required: ["action", "if", "public"],
type: "object"
},
SetScoreLimit: {
defaultProperties: [],
description: "(Un)Set the score limit for the lobby.",
properties: {
action: {
$ref: "#/definitions/NameType_6"
$ref: "#/definitions/NameType_7"
},
if: {
description:
@@ -451,7 +477,7 @@ export const Schema = {
description: "Start a game in the lobby if possible.",
properties: {
action: {
$ref: "#/definitions/NameType_8"
$ref: "#/definitions/NameType_9"
}
},
required: ["action"],
@@ -2,6 +2,7 @@ import { DecksChanged } from "./configured/decks-changed";
import { HandSizeSet } from "./configured/hand-size-set";
import { HouseRuleChanged } from "./configured/house-rule-changed";
import { PasswordSet } from "./configured/password-set";
import { PublicSet } from "./configured/public-set";
import { ScoreLimitSet } from "./configured/score-limit-set";
/**
@@ -12,7 +13,8 @@ export type Configured =
| HandSizeSet
| ScoreLimitSet
| DecksChanged
| HouseRuleChanged;
| HouseRuleChanged
| PublicSet;
export interface Base {
event: string;
@@ -0,0 +1,15 @@
import * as configured from "../configured";
/**
* The lobby password is (un)set.
*/
export interface PublicSet extends configured.Base {
event: "PublicSet";
public: boolean;
}
export const of = (version: string, isPublic: boolean): PublicSet => ({
event: "PublicSet",
version,
public: isPublic
});
+1 -1
View File
@@ -94,7 +94,7 @@ async function main(): Promise<void> {
res.status(HttpStatus.CREATED).json(token);
});
app.post("/api/games/alive", async (req, res) => {
app.post("/api/alive", async (req, res) => {
const result: { [key: string]: boolean } = {};
for (const current of checkAlive.validate(req.body).tokens) {
try {
+7 -4
View File
@@ -59,6 +59,7 @@ export interface Summary {
gameCode: GameCode;
state: State;
users: { players: number; spectators: number };
password?: boolean;
}
/**
@@ -67,7 +68,8 @@ export interface Summary {
export const defaultConfig = (): Config => ({
version: 0,
rules: rules.create(),
decks: []
decks: [],
public: false
});
/**
@@ -104,7 +106,8 @@ export const summary = (gameCode: GameCode, lobby: Lobby): Summary => ({
users: util.counts(Object.values(lobby.users), {
players: user.isPlaying,
spectators: user.isSpectating
})
}),
...(lobby.config.password !== undefined ? { password: true } : {})
});
/**
@@ -121,11 +124,11 @@ function usersObj(lobby: Lobby): { [id: string]: user.Public } {
return obj;
}
export const censor = (lobby: Lobby, auth: token.Claims): Public => ({
export const censor = (lobby: Lobby): Public => ({
name: lobby.name,
public: lobby.public,
users: usersObj(lobby),
owner: lobby.owner,
config: config.censor(lobby.config, auth),
config: config.censor(lobby.config),
...(lobby.game === undefined ? {} : { game: game.censor(lobby.game) })
});
+10 -20
View File
@@ -1,7 +1,6 @@
import * as source from "../games/cards/source";
import { Rules } from "../games/rules";
import * as rules from "../games/rules";
import * as token from "../user/token";
/**
* Configuration for a lobby.
@@ -9,6 +8,7 @@ import * as token from "../user/token";
export interface Config {
version: Version;
rules: Rules;
public: boolean;
password?: string;
decks: SummarisedSource[];
}
@@ -18,11 +18,8 @@ export type Version = number;
export interface Public {
version: string;
rules: rules.Public;
/**
* @maxLength 100
* @minLength 1
*/
password?: boolean | string;
public?: boolean;
password?: string;
decks: SummarisedSource[];
}
@@ -31,20 +28,13 @@ export interface SummarisedSource {
summary?: source.Summary;
}
export function censor(config: Config, auth: token.Claims): Public {
const privilegedPart: {} =
auth !== undefined && auth.pvg === "Privileged"
? config.password === undefined
? {}
: { password: config.password }
: { password: config.password !== undefined };
return {
version: config.version.toString(),
rules: rules.censor(config.rules),
decks: config.decks,
...privilegedPart
};
}
export const censor = (config: Config): Public => ({
version: config.version.toString(),
rules: rules.censor(config.rules),
decks: config.decks,
...(config.public ? { public: true } : {}),
...(config.password !== undefined ? { password: config.password } : {})
});
/**
* The way in which the decks configuration is changed.
+2 -2
View File
@@ -103,7 +103,7 @@ export class SocketManager {
lobby,
events: [
event.targetOnly(
sync.of(gameLobby.censor(lobby, knownAuth), hand, play),
sync.of(gameLobby.censor(lobby), hand, play),
uid
)
]
@@ -166,7 +166,7 @@ export class Sockets {
const users = this.users(gameCode);
const didDelete = users.delete(id);
if (users.size < 1) {
this.sockets.delete(gameCode);
this.sockets.delete(gameCode);
}
return didDelete;
}
+4 -3
View File
@@ -53,9 +53,10 @@ export class InMemoryStore extends Store {
}
public async *lobbySummaries(): AsyncIterableIterator<gameLobby.Summary> {
for (const summary of wu(this.lobbies.entries()).spreadMap(
gameLobby.summary
)) {
const publicSummaries = wu(this.lobbies.entries())
.filter(([_, l]) => l.config.public)
.spreadMap(gameLobby.summary);
for (const summary of publicSummaries) {
yield summary;
}
}