Add desktop notifications to let players know when they need to do something. Also moved the overlays into Elm rather than javascript hackery.
This commit is contained in:
@@ -1,63 +1,63 @@
|
||||
module MassiveDecks.Components.About exposing (aboutOverlay)
|
||||
module MassiveDecks.Components.About exposing (show)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
|
||||
import MassiveDecks.Components.Icon exposing (..)
|
||||
import MassiveDecks.Components.Overlay as Overlay
|
||||
|
||||
|
||||
{-| The about overlay for telling players about the game.
|
||||
-}
|
||||
aboutOverlay : Html a
|
||||
aboutOverlay =
|
||||
div [ id "about" ]
|
||||
[ div [ class "mui-panel" ]
|
||||
[ h1 [] [ icon "info-circle", text " About" ]
|
||||
, p [] [ text "Massive Decks is a web game based on the excellent "
|
||||
, a [ href "https://cardsagainsthumanity.com/", target "_blank" ] [ text "Cards against Humanity" ]
|
||||
, text " - a party game where you play white cards to try and produce the most amusing outcome when "
|
||||
, text "combined with the given black card."
|
||||
]
|
||||
, p [] [ text "Massive Decks is also inspired by: "
|
||||
, ul [] [ li [] [ a [ href "https://www.cardcastgame.com/", target "_blank" ] [ text "Cardcast" ]
|
||||
, text " - an app that allows you to play on a ChromeCast."
|
||||
]
|
||||
, li [] [ a [ href "http://pretendyoure.xyz/zy/", target "_blank" ] [ text "Pretend You're Xyzzy" ]
|
||||
, text " - a web game where you can jump in with people you don't know."
|
||||
]
|
||||
]
|
||||
]
|
||||
, p [] [ text "This is an open source game developed in "
|
||||
, a [ href "http://elm-lang.org/", target "_blank" ] [ text "Elm" ]
|
||||
, text " for the client and "
|
||||
, a [ href "http://www.scala-lang.org/", target "_blank" ] [ text "Scala" ]
|
||||
, text " for the server."
|
||||
]
|
||||
, p [] [ text "We also use: "
|
||||
, ul [] [ li [] [ a [ href "https://www.cardcastgame.com/", target "_blank" ] [ text "Cardcast" ]
|
||||
, text "'s APIs for getting decks of cards (you can go there to make your own!)."
|
||||
]
|
||||
, li [] [ text "The "
|
||||
, a [ href "https://www.playframework.com/", target "_blank" ] [ text "Play framework" ]
|
||||
]
|
||||
, li [] [ a [ href "http://lesscss.org/", target "_blank" ] [ text "Less" ] ]
|
||||
, li [] [ a [ href "https://fortawesome.github.io/Font-Awesome/", target "_blank" ] [ text "Font Awesome" ] ]
|
||||
, li [] [ a [ href "https://www.muicss.com", target "_blank" ] [ text "MUI" ] ]
|
||||
]
|
||||
]
|
||||
, p [] [ text "Bug reports and contributions are welcome on the "
|
||||
, a [ href "https://github.com/Lattyware/massivedecks", target "_blank" ] [ text "GitHub repository" ]
|
||||
, text ", where you can find the complete source to the game, under the GPLv3 license. The game concept "
|
||||
, text "'Cards against Humanity' is used under a "
|
||||
, a [ href "https://creativecommons.org/licenses/by-nc-sa/2.0/", target "_blank" ] [ text "Creative Commons BY-NC-SA 2.0 license" ]
|
||||
, text " granted by "
|
||||
, a [ href "https://cardsagainsthumanity.com/", target "_blank" ] [ text "Cards against Humanity" ]
|
||||
]
|
||||
, p [ class "close-link" ]
|
||||
[ a [ class "link"
|
||||
, attribute "tabindex" "0"
|
||||
, attribute "role" "button"
|
||||
, attribute "onClick" "closeOverlay()"
|
||||
] [ icon "times", text " Close" ] ]
|
||||
]
|
||||
]
|
||||
show : Overlay.Message a
|
||||
show = Overlay.Show icon title contents
|
||||
|
||||
|
||||
icon : String
|
||||
icon = "info-circle"
|
||||
|
||||
|
||||
title : String
|
||||
title = "About"
|
||||
|
||||
|
||||
contents : List (Html a)
|
||||
contents =
|
||||
[ p [] [ text "Massive Decks is a web game based on the excellent "
|
||||
, a [ href "https://cardsagainsthumanity.com/", target "_blank" ] [ text "Cards against Humanity" ]
|
||||
, text " - a party game where you play white cards to try and produce the most amusing outcome when "
|
||||
, text "combined with the given black card."
|
||||
]
|
||||
, p [] [ text "Massive Decks is also inspired by: "
|
||||
, ul [] [ li [] [ a [ href "https://www.cardcastgame.com/", target "_blank" ] [ text "Cardcast" ]
|
||||
, text " - an app that allows you to play on a ChromeCast."
|
||||
]
|
||||
, li [] [ a [ href "http://pretendyoure.xyz/zy/", target "_blank" ] [ text "Pretend You're Xyzzy" ]
|
||||
, text " - a web game where you can jump in with people you don't know."
|
||||
]
|
||||
]
|
||||
]
|
||||
, p [] [ text "This is an open source game developed in "
|
||||
, a [ href "http://elm-lang.org/", target "_blank" ] [ text "Elm" ]
|
||||
, text " for the client and "
|
||||
, a [ href "http://www.scala-lang.org/", target "_blank" ] [ text "Scala" ]
|
||||
, text " for the server."
|
||||
]
|
||||
, p [] [ text "We also use: "
|
||||
, ul [] [ li [] [ a [ href "https://www.cardcastgame.com/", target "_blank" ] [ text "Cardcast" ]
|
||||
, text "'s APIs for getting decks of cards (you can go there to make your own!)."
|
||||
]
|
||||
, li [] [ text "The "
|
||||
, a [ href "https://www.playframework.com/", target "_blank" ] [ text "Play framework" ]
|
||||
]
|
||||
, li [] [ a [ href "http://lesscss.org/", target "_blank" ] [ text "Less" ] ]
|
||||
, li [] [ a [ href "https://fortawesome.github.io/Font-Awesome/", target "_blank" ] [ text "Font Awesome" ] ]
|
||||
, li [] [ a [ href "https://www.muicss.com", target "_blank" ] [ text "MUI" ] ]
|
||||
]
|
||||
]
|
||||
, p [] [ text "Bug reports and contributions are welcome on the "
|
||||
, a [ href "https://github.com/Lattyware/massivedecks", target "_blank" ] [ text "GitHub repository" ]
|
||||
, text ", where you can find the complete source to the game, under the GPLv3 license. The game concept "
|
||||
, text "'Cards against Humanity' is used under a "
|
||||
, a [ href "https://creativecommons.org/licenses/by-nc-sa/2.0/", target "_blank" ] [ text "Creative Commons BY-NC-SA 2.0 license" ]
|
||||
, text " granted by "
|
||||
, a [ href "https://cardsagainsthumanity.com/", target "_blank" ] [ text "Cards against Humanity" ]
|
||||
]
|
||||
]
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
port module MassiveDecks.Components.BrowserNotifications exposing (Model, Message, ConsumerMessage(..), Permission(..), init, update, subscriptions, notify, enable, disable)
|
||||
|
||||
import Maybe
|
||||
|
||||
import MassiveDecks.Util as Util
|
||||
|
||||
|
||||
init : Bool -> Bool -> Model
|
||||
init supported enabled =
|
||||
{ supported = supported
|
||||
, enabled = enabled
|
||||
, permission = Nothing
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ supported : Bool
|
||||
, enabled : Bool
|
||||
, permission : Maybe Permission
|
||||
}
|
||||
|
||||
|
||||
type alias Notification =
|
||||
{ title : String
|
||||
, icon : Maybe String
|
||||
}
|
||||
|
||||
|
||||
type Permission
|
||||
= Granted
|
||||
| Denied
|
||||
| Default
|
||||
|
||||
|
||||
update : Message -> Model -> (Model, Cmd Message, Cmd ConsumerMessage)
|
||||
update message model =
|
||||
case message of
|
||||
PermissionGiven permission ->
|
||||
({ model | permission = Just permission }, Cmd.none, Util.cmd (PermissionChanged permission))
|
||||
|
||||
SendNotification notification ->
|
||||
if model.supported && model.enabled then
|
||||
(model, notifications notification, Cmd.none)
|
||||
else
|
||||
(model, Cmd.none, Cmd.none)
|
||||
|
||||
EnableNotifications ->
|
||||
({ model | enabled = True }, requestPermission (), Cmd.none)
|
||||
|
||||
DisableNotifications ->
|
||||
({ model | enabled = False }, Cmd.none, Cmd.none)
|
||||
|
||||
|
||||
type ConsumerMessage
|
||||
= PermissionChanged Permission
|
||||
|
||||
|
||||
type Message
|
||||
= PermissionGiven Permission
|
||||
| SendNotification Notification
|
||||
| EnableNotifications
|
||||
| DisableNotifications
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Message
|
||||
subscriptions model =
|
||||
if model.supported && model.enabled && model.permission == Nothing then
|
||||
permissions permission
|
||||
else
|
||||
Sub.none
|
||||
|
||||
port permissions : (String -> msg) -> Sub msg
|
||||
|
||||
permission : String -> Message
|
||||
permission name =
|
||||
let
|
||||
permission = case name of
|
||||
"granted" -> Granted
|
||||
"denied" -> Denied
|
||||
"default" -> Default
|
||||
_ ->
|
||||
let
|
||||
_ = Debug.log "Unexpected permission for browser notifications, assuming denied" name
|
||||
in
|
||||
Denied
|
||||
in
|
||||
PermissionGiven permission
|
||||
|
||||
|
||||
notify : Notification -> Message
|
||||
notify notification = SendNotification notification
|
||||
|
||||
port notifications : Notification -> Cmd msg
|
||||
|
||||
|
||||
enable : Message
|
||||
enable = EnableNotifications
|
||||
|
||||
disable : Message
|
||||
disable = DisableNotifications
|
||||
|
||||
port requestPermission : () -> Cmd msg
|
||||
@@ -0,0 +1,95 @@
|
||||
port module MassiveDecks.Components.Overlay exposing (Model, Message(..), init, view, update, map)
|
||||
|
||||
import Json.Decode as Json
|
||||
|
||||
import Html.App as Html
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (..)
|
||||
|
||||
import MassiveDecks.Components.Icon as Icon
|
||||
|
||||
|
||||
type alias Model a =
|
||||
{ icon : String
|
||||
, title : String
|
||||
, contents : List (Html a)
|
||||
, wrap : (Message a) -> a
|
||||
, shown : Bool
|
||||
}
|
||||
|
||||
|
||||
type Message a
|
||||
= Show String String (List (Html a))
|
||||
| Hide
|
||||
| NoOp
|
||||
|
||||
|
||||
init : ((Message a) -> a) -> Model a
|
||||
init wrap =
|
||||
{ icon = ""
|
||||
, title = ""
|
||||
, contents = []
|
||||
, wrap = wrap
|
||||
, shown = False
|
||||
}
|
||||
|
||||
|
||||
map : (a -> b) -> Message a -> Message b
|
||||
map mapper message =
|
||||
case message of
|
||||
Show icon title contents ->
|
||||
Show icon title (List.map (Html.map mapper) contents)
|
||||
|
||||
Hide ->
|
||||
Hide
|
||||
|
||||
NoOp ->
|
||||
NoOp
|
||||
|
||||
|
||||
update : Message a -> Model a -> Model a
|
||||
update message model =
|
||||
case message of
|
||||
Show icon title contents ->
|
||||
{ model | icon = icon
|
||||
, title = title
|
||||
, contents = contents
|
||||
, shown = True
|
||||
}
|
||||
|
||||
Hide ->
|
||||
{ model | shown = False }
|
||||
|
||||
NoOp ->
|
||||
model
|
||||
|
||||
|
||||
view : Model a -> List (Html a)
|
||||
view model =
|
||||
if model.shown then
|
||||
[ div [ id "mui-overlay", onClickIfId "mui-overlay" (model.wrap Hide) (model.wrap NoOp) ]
|
||||
[ div [ class "overlay mui-panel" ]
|
||||
([ h1 [] [ Icon.icon model.icon, text " ", text model.title ] ] ++
|
||||
model.contents ++
|
||||
[ p [ class "close-link"]
|
||||
[ a [ class "link"
|
||||
, attribute "tabindex" "0"
|
||||
, attribute "role" "button"
|
||||
, onClick (model.wrap Hide)
|
||||
] [ Icon.icon "times", text " Close" ]
|
||||
]
|
||||
])
|
||||
]
|
||||
]
|
||||
else
|
||||
[]
|
||||
|
||||
|
||||
onClickIfId : String -> msg -> msg -> Attribute msg
|
||||
onClickIfId targetId message noOp =
|
||||
on "click" (ifIdDecoder targetId |> Json.map (\match -> if match then message else noOp))
|
||||
|
||||
|
||||
ifIdDecoder : String -> Json.Decoder Bool
|
||||
ifIdDecoder targetId = Json.at [ "target", "id" ] Json.string |> Json.map ((==) targetId)
|
||||
@@ -10,4 +10,5 @@ type alias Init =
|
||||
, gameCode : Maybe String
|
||||
, existingGame : Maybe GameCodeAndSecret
|
||||
, seed : String
|
||||
, browserNotificationsSupported : Bool
|
||||
}
|
||||
|
||||
@@ -8,11 +8,15 @@ import Time
|
||||
|
||||
import WebSocket
|
||||
|
||||
import AnimationFrame
|
||||
|
||||
import Html exposing (Html)
|
||||
|
||||
import MassiveDecks.API as API
|
||||
import MassiveDecks.API.Request as Request
|
||||
import MassiveDecks.Components.QR as QR
|
||||
import MassiveDecks.Components.BrowserNotifications as BrowserNotifications
|
||||
import MassiveDecks.Components.Overlay as Overlay
|
||||
import MassiveDecks.Models exposing (Init)
|
||||
import MassiveDecks.Models.JSON.Decode exposing (lobbyDecoder)
|
||||
import MassiveDecks.Models.JSON.Encode exposing (encodePlayerSecret)
|
||||
@@ -38,11 +42,12 @@ init init lobbyAndHand secret =
|
||||
, hand = lobbyAndHand.hand
|
||||
, config = Config.init
|
||||
, playing = Playing.init init
|
||||
, browserNotifications = BrowserNotifications.init init.browserNotificationsSupported False
|
||||
, secret = secret
|
||||
, init = init
|
||||
, notification = Nothing
|
||||
} ! [ QR.encodeAndRender "invite-qr-code" (Util.lobbyUrl init.url lobbyAndHand.lobby.gameCode)
|
||||
]
|
||||
, qrNeedsRendering = False
|
||||
} ! []
|
||||
|
||||
|
||||
{-| Subscriptions for the lobby.
|
||||
@@ -55,10 +60,15 @@ subscriptions model =
|
||||
Just round -> Playing.subscriptions model.playing |> Sub.map PlayingMessage
|
||||
|
||||
websocket = WebSocket.listen (webSocketUrl model.init.url model.lobby.gameCode) webSocketResponseDecoder
|
||||
|
||||
browserNotifications = BrowserNotifications.subscriptions model.browserNotifications |> Sub.map BrowserNotificationsMessage
|
||||
|
||||
render = if model.qrNeedsRendering then [ AnimationFrame.diffs (\_ -> LocalMessage RenderQr) ] else []
|
||||
in
|
||||
Sub.batch [ delegated |> Sub.map LocalMessage
|
||||
, websocket
|
||||
]
|
||||
Sub.batch ([ delegated |> Sub.map LocalMessage
|
||||
, websocket
|
||||
, browserNotifications |> Sub.map LocalMessage
|
||||
] ++ render)
|
||||
|
||||
|
||||
webSocketUrl : String -> String -> String
|
||||
@@ -138,6 +148,15 @@ update message model =
|
||||
in
|
||||
({ model | playing = playing }, Cmd.map (LocalMessage << PlayingMessage) cmd)
|
||||
|
||||
BrowserNotificationsMessage notificationMessage ->
|
||||
let
|
||||
(browserNotifications, localCmd, cmd) = BrowserNotifications.update notificationMessage model.browserNotifications
|
||||
in
|
||||
{ model | browserNotifications = browserNotifications } !
|
||||
[ Cmd.map (LocalMessage << BrowserNotificationsMessage) localCmd
|
||||
, Cmd.map overlayAlert cmd
|
||||
]
|
||||
|
||||
UpdateLobby lobby ->
|
||||
model |> updateLobbyAndHand { lobby = lobby, hand = model.hand }
|
||||
:> updateHandIfRoundStarted
|
||||
@@ -148,6 +167,12 @@ update message model =
|
||||
Identify ->
|
||||
(model, Cmd.map LocalMessage (WebSocket.send (webSocketUrl model.init.url model.lobby.gameCode) (encodePlayerSecret model.secret |> encode 0)))
|
||||
|
||||
DisplayInviteOverlay ->
|
||||
{ model | qrNeedsRendering = True } ! [ Util.cmd (UI.inviteOverlay model.init.url model.lobby.gameCode |> OverlayMessage) ]
|
||||
|
||||
RenderQr ->
|
||||
{ model | qrNeedsRendering = False } ! [ QR.encodeAndRender "invite-qr-code" (Util.lobbyUrl model.init.url model.lobby.gameCode) ]
|
||||
|
||||
DismissNotification notification ->
|
||||
let
|
||||
newModel =
|
||||
@@ -182,6 +207,35 @@ update message model =
|
||||
Event.PlayerLeft id ->
|
||||
notificationChange model (Notification.playerLeft id players)
|
||||
|
||||
Event.RoundJudging _ ->
|
||||
case model.lobby.round of
|
||||
Nothing ->
|
||||
(model, Cmd.none)
|
||||
|
||||
Just round ->
|
||||
let
|
||||
cmd = if (round.czar == model.secret.id) then
|
||||
Util.cmd (BrowserNotifications.notify { title = "You need to pick a winner for the round.", icon = icon model "gavel" } |> BrowserNotificationsMessage |> LocalMessage)
|
||||
else
|
||||
Cmd.none
|
||||
in
|
||||
(model, cmd)
|
||||
|
||||
Event.PlayerStatus id status ->
|
||||
let
|
||||
cmd = if (id == model.secret.id) then
|
||||
case status of
|
||||
Player.NotPlayed ->
|
||||
Util.cmd (BrowserNotifications.notify { title = "You need to play a card for the round.", icon = icon model "hourglass" } |> BrowserNotificationsMessage |> LocalMessage)
|
||||
Player.Skipping ->
|
||||
Util.cmd (BrowserNotifications.notify { title = "You are being skipped due to inactivity.", icon = icon model "fast-forward"} |> BrowserNotificationsMessage |> LocalMessage)
|
||||
_ ->
|
||||
Cmd.none
|
||||
else
|
||||
Cmd.none
|
||||
in
|
||||
(model, cmd)
|
||||
|
||||
_ ->
|
||||
(model, Cmd.none)
|
||||
|
||||
@@ -189,6 +243,24 @@ update message model =
|
||||
(model, Cmd.none)
|
||||
|
||||
|
||||
overlayAlert : BrowserNotifications.ConsumerMessage -> ConsumerMessage
|
||||
overlayAlert message =
|
||||
case message of
|
||||
BrowserNotifications.PermissionChanged permission ->
|
||||
case permission of
|
||||
BrowserNotifications.Denied ->
|
||||
(Overlay.Show "times-circle" "Can't enable desktop notifications."
|
||||
[ Html.text "You did not give Massive Decks permission to give you desktop notifications." ]
|
||||
) |> OverlayMessage
|
||||
|
||||
_ ->
|
||||
NoOp |> LocalMessage
|
||||
|
||||
|
||||
icon : Model -> String -> Maybe String
|
||||
icon model name = Just (model.init.url ++ "assets/images/icons/" ++ name ++ ".png")
|
||||
|
||||
|
||||
type alias Update = Model -> (Model, Cmd ConsumerMessage)
|
||||
|
||||
|
||||
|
||||
@@ -65,31 +65,80 @@ diffPlayer oldPlayers newPlayer =
|
||||
diffRound : Maybe Game.Round -> Maybe Game.Round -> List Event
|
||||
diffRound oldRound newRound =
|
||||
let
|
||||
differentRound = Maybe.map .call oldRound /= Maybe.map .call newRound
|
||||
differentOrChangedRoundEvents = if differentRound then
|
||||
List.filterMap identity
|
||||
[ Maybe.map roundStart newRound
|
||||
, newRound `Maybe.andThen` (\newRound -> case newRound.responses of
|
||||
Card.Hidden count -> Just (roundPlayed count)
|
||||
Card.Revealed _ -> Nothing)
|
||||
]
|
||||
else
|
||||
Maybe.map2 changedRound oldRound newRound |> Maybe.withDefault []
|
||||
roundEvent = newRound `Maybe.andThen` (\newRound -> case newRound.responses of
|
||||
Card.Hidden count -> Nothing
|
||||
Card.Revealed revealed -> roundEnd newRound.call newRound.czar revealed)
|
||||
in
|
||||
differentOrChangedRoundEvents `Util.andMaybe` roundEvent
|
||||
differentRounds = (Maybe.map .call oldRound) /= (Maybe.map .call newRound)
|
||||
|
||||
changedRound : Game.Round -> Game.Round -> List Event
|
||||
changedRound oldRound newRound =
|
||||
case oldRound.responses of
|
||||
Card.Hidden oldCount ->
|
||||
case newRound.responses of
|
||||
Card.Hidden newCount -> if (oldCount < newCount) then [ roundPlayed (newCount - oldCount) ] else []
|
||||
Card.Revealed _ -> [ roundJudging newRound ]
|
||||
Card.Revealed responses ->
|
||||
[]
|
||||
start = case newRound of
|
||||
Just round -> if differentRounds then [ roundStart round ] else []
|
||||
Nothing ->
|
||||
[]
|
||||
|
||||
played = if differentRounds then [] else case newRound of
|
||||
Just round ->
|
||||
let
|
||||
oldCount = playedInRound oldRound |> Maybe.withDefault 0
|
||||
newCount = playedInRound newRound |> Maybe.withDefault 0
|
||||
in
|
||||
if oldCount < newCount then [ RoundPlayed (newCount - oldCount) ] else []
|
||||
Nothing ->
|
||||
[]
|
||||
|
||||
judging = case newRound of
|
||||
Just round ->
|
||||
case round.responses of
|
||||
Card.Hidden _ ->
|
||||
[]
|
||||
Card.Revealed responses ->
|
||||
let
|
||||
oldJudging = Maybe.map (\or -> case or.responses of
|
||||
Card.Hidden _ -> False
|
||||
Card.Revealed oldResponses -> Util.isNothing oldResponses.playedByAndWinner) oldRound |> Maybe.withDefault False
|
||||
newJudging = Util.isNothing responses.playedByAndWinner
|
||||
in
|
||||
if newJudging && ((not oldJudging) || (oldJudging && differentRounds)) then
|
||||
[ RoundJudging responses.cards ]
|
||||
else
|
||||
[]
|
||||
Nothing ->
|
||||
[]
|
||||
|
||||
ended = case newRound of
|
||||
Just round ->
|
||||
case round.responses of
|
||||
Card.Hidden _ ->
|
||||
[]
|
||||
Card.Revealed responses ->
|
||||
case responses.playedByAndWinner of
|
||||
Nothing ->
|
||||
[]
|
||||
Just playedByAndWinner ->
|
||||
let
|
||||
oldEnded = Maybe.map (\or -> case or.responses of
|
||||
Card.Hidden _ -> False
|
||||
Card.Revealed oldResponses -> not (Util.isNothing oldResponses.playedByAndWinner)) oldRound |> Maybe.withDefault False
|
||||
in
|
||||
if (not oldEnded) || (oldEnded && differentRounds) then
|
||||
[ RoundEnd round.call round.czar responses.cards playedByAndWinner ]
|
||||
else
|
||||
[]
|
||||
Nothing ->
|
||||
[]
|
||||
in
|
||||
List.concat
|
||||
[ start
|
||||
, played
|
||||
, judging
|
||||
, ended
|
||||
]
|
||||
|
||||
|
||||
playedInRound : Maybe Game.Round -> Maybe Int
|
||||
playedInRound maybeRound =
|
||||
let
|
||||
calc = (\round -> case round.responses of
|
||||
Card.Hidden count -> Just count
|
||||
Card.Revealed _ -> Nothing)
|
||||
in
|
||||
maybeRound `Maybe.andThen` calc
|
||||
|
||||
{- Event Constructors -}
|
||||
|
||||
@@ -113,20 +162,3 @@ playerDisconnect player = PlayerDisconnect player.id
|
||||
|
||||
roundStart : Game.Round -> Event
|
||||
roundStart round = RoundStart round.call round.czar
|
||||
|
||||
roundPlayed : Int -> Event
|
||||
roundPlayed amount = RoundPlayed amount
|
||||
|
||||
roundJudging : Game.Round -> Event
|
||||
roundJudging round =
|
||||
let
|
||||
responses = case round.responses of
|
||||
Card.Hidden _ -> Nothing
|
||||
Card.Revealed responses -> Just responses
|
||||
played = Maybe.map .cards responses |> Maybe.withDefault []
|
||||
in
|
||||
RoundJudging played
|
||||
|
||||
roundEnd : Card.Call -> Player.Id -> Card.RevealedResponses -> Maybe Event
|
||||
roundEnd call czar responses =
|
||||
Maybe.map (RoundEnd call czar responses.cards) responses.playedByAndWinner
|
||||
|
||||
@@ -7,12 +7,15 @@ import MassiveDecks.Scenes.Lobby.Event as Event exposing (Event)
|
||||
import MassiveDecks.Scenes.Config.Messages as Config
|
||||
import MassiveDecks.Scenes.Playing.Messages as Playing
|
||||
import MassiveDecks.Components.Errors as Errors
|
||||
import MassiveDecks.Components.Overlay as Overlay
|
||||
import MassiveDecks.Components.BrowserNotifications as BrowserNotifications
|
||||
|
||||
|
||||
{-| This type is used for all sending of messages, allowing us to send messages handled outside this scene.
|
||||
-}
|
||||
type ConsumerMessage
|
||||
= ErrorMessage Errors.Message
|
||||
| OverlayMessage (Overlay.Message Message)
|
||||
| Leave
|
||||
| LocalMessage Message
|
||||
|
||||
@@ -24,7 +27,10 @@ type Message
|
||||
| UpdateLobby Game.Lobby
|
||||
| UpdateHand Card.Hand
|
||||
| Identify
|
||||
| DisplayInviteOverlay
|
||||
| RenderQr
|
||||
| NoOp
|
||||
| GameEvent Event
|
||||
| BrowserNotificationsMessage BrowserNotifications.Message
|
||||
| ConfigMessage Config.ConsumerMessage
|
||||
| PlayingMessage Playing.ConsumerMessage
|
||||
|
||||
@@ -7,6 +7,7 @@ import MassiveDecks.Models.Card as Card
|
||||
import MassiveDecks.Models.Notification as Notification exposing (Notification)
|
||||
import MassiveDecks.Scenes.Config.Models as Config
|
||||
import MassiveDecks.Scenes.Playing.Models as Playing
|
||||
import MassiveDecks.Components.BrowserNotifications as BrowserNotifications
|
||||
|
||||
|
||||
{-| The state of the lobby.
|
||||
@@ -16,7 +17,9 @@ type alias Model =
|
||||
, hand : Card.Hand
|
||||
, config : Config.Model
|
||||
, playing : Playing.Model
|
||||
, browserNotifications : BrowserNotifications.Model
|
||||
, secret : Player.Secret
|
||||
, init : Init
|
||||
, notification : Maybe Notification
|
||||
, qrNeedsRendering : Bool
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
module MassiveDecks.Scenes.Lobby.UI exposing (view)
|
||||
module MassiveDecks.Scenes.Lobby.UI exposing (view, inviteOverlay)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.App as Html
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (..)
|
||||
|
||||
import MassiveDecks.Components.About as About
|
||||
import MassiveDecks.Components.QR as QR
|
||||
import MassiveDecks.Components.Icon as Icon
|
||||
import MassiveDecks.Components.About as About
|
||||
import MassiveDecks.Components.Overlay as Overlay
|
||||
import MassiveDecks.Components.BrowserNotifications as BrowserNotifications
|
||||
import MassiveDecks.Scenes.Config as Config
|
||||
import MassiveDecks.Scenes.Playing as Playing
|
||||
import MassiveDecks.Scenes.Lobby.Models exposing (Model)
|
||||
@@ -24,7 +26,6 @@ view model =
|
||||
url = model.init.url
|
||||
gameCode = lobby.gameCode
|
||||
players = lobby.players
|
||||
notification = model.notification
|
||||
(header, contents) = case lobby.round of
|
||||
Nothing -> ([], [ Config.view model |> Html.map (ConfigMessage >> LocalMessage) ])
|
||||
Just round ->
|
||||
@@ -33,12 +34,10 @@ view model =
|
||||
in
|
||||
(h |> List.map (Html.map (PlayingMessage >> LocalMessage)), c |> List.map (Html.map (PlayingMessage >> LocalMessage)))
|
||||
in
|
||||
root [ appHeader header notification
|
||||
root [ appHeader header model
|
||||
, spacer
|
||||
, scores players
|
||||
, contentWrapper contents
|
||||
, inviteOverlay url gameCode
|
||||
, About.aboutOverlay
|
||||
]
|
||||
|
||||
|
||||
@@ -83,12 +82,12 @@ score player =
|
||||
]
|
||||
|
||||
|
||||
appHeader : List (Html ConsumerMessage) -> Maybe Notification -> Html ConsumerMessage
|
||||
appHeader contents notification = (header [] [ div [ class "mui-appbar mui--appbar-line-height" ]
|
||||
appHeader : List (Html ConsumerMessage) -> Model -> Html ConsumerMessage
|
||||
appHeader contents model = (header [] [ div [ class "mui-appbar mui--appbar-line-height" ]
|
||||
[ div [ class "mui--appbar-line-height" ]
|
||||
[ span [ class "score-buttons" ] (List.append [ scoresButton True, scoresButton False ] (notificationPopup notification))
|
||||
[ span [ class "score-buttons" ] (List.append [ scoresButton True, scoresButton False ] (notificationPopup model.notification))
|
||||
, span [ id "title", class "mui--text-title mui--visible-xs-inline-block" ] contents
|
||||
, gameMenu ] ] ])
|
||||
, gameMenu model ] ] ])
|
||||
|
||||
|
||||
scoresButton : Bool -> Html msg
|
||||
@@ -152,56 +151,79 @@ playerIcon player =
|
||||
|
||||
{-| The overlay for inviting players to a lobby.
|
||||
-}
|
||||
inviteOverlay : String -> String -> Html msg
|
||||
inviteOverlay appUrl lobbyId =
|
||||
inviteOverlay : String -> String -> Overlay.Message msg
|
||||
inviteOverlay appUrl gameCode =
|
||||
let
|
||||
url = Util.lobbyUrl appUrl lobbyId
|
||||
url = Util.lobbyUrl appUrl gameCode
|
||||
in
|
||||
div [ id "invite" ]
|
||||
[ div [ class "mui-panel" ]
|
||||
[ h1 [] [ Icon.icon "bullhorn", text " Invite Players" ]
|
||||
, p [] [ text "To invite other players, simply send them this link: " ]
|
||||
, p [] [ a [ href url ] [ text url ] ]
|
||||
, p [] [ text "Have them scan this QR code: " ]
|
||||
, QR.view "invite-qr-code"
|
||||
, p [] [ text "Or give them this game code to enter on the start page: " ]
|
||||
, p [] [ input [ readonly True, value lobbyId ] [] ]
|
||||
, p [ class "close-link" ]
|
||||
[ a [ class "link"
|
||||
, attribute "tabindex" "0"
|
||||
, attribute "role" "button"
|
||||
, attribute "onClick" "closeOverlay()"
|
||||
] [ Icon.icon "times", text " Close" ] ]
|
||||
]
|
||||
Overlay.Show "bullhorn" "Invite Players"
|
||||
[ p [] [ text "To invite other players, simply send them this link: " ]
|
||||
, p [] [ a [ href url ] [ text url ] ]
|
||||
, p [] [ text "Have them scan this QR code: " ]
|
||||
, QR.view "invite-qr-code"
|
||||
, p [] [ text "Or give them this game code to enter on the start page: " ]
|
||||
, p [] [ input [ readonly True, value gameCode ] [] ]
|
||||
]
|
||||
|
||||
|
||||
notificationsMenuItem : BrowserNotifications.Model -> List (Html ConsumerMessage)
|
||||
notificationsMenuItem model =
|
||||
let
|
||||
(notClickable, enabled) =
|
||||
if not model.supported then
|
||||
(Just "Your browser does not support desktop notifications.", False)
|
||||
else if model.permission == Just BrowserNotifications.Denied then
|
||||
(Just "You have denied Massive Decks permission to display desktop notifications.", False)
|
||||
else
|
||||
(Nothing, model.enabled)
|
||||
|
||||
classes = classList
|
||||
[ ("link", True)
|
||||
, ("disabled", not (Util.isNothing notClickable))
|
||||
]
|
||||
|
||||
extraAttrs =
|
||||
case notClickable of
|
||||
Nothing ->
|
||||
[ onClick (LocalMessage <| BrowserNotificationsMessage <| (if enabled then BrowserNotifications.disable else BrowserNotifications.enable)) ]
|
||||
Just msg ->
|
||||
[ title msg ]
|
||||
|
||||
attributes = [ classes, attribute "tabindex" "0", attribute "role" "button" ] ++ extraAttrs
|
||||
|
||||
description = " " ++ (if enabled then "Disable" else "Enable") ++ " Notifications"
|
||||
in
|
||||
[ li [] [ a attributes [ Icon.fwIcon (if enabled then "bell-slash" else "bell"), text description ] ]
|
||||
]
|
||||
|
||||
|
||||
{-| The menu for the game.
|
||||
-}
|
||||
gameMenu : Html ConsumerMessage
|
||||
gameMenu = div [ class "menu mui-dropdown" ]
|
||||
gameMenu : Model -> Html ConsumerMessage
|
||||
gameMenu model = div [ class "menu mui-dropdown" ]
|
||||
[ button [ class "mui-btn mui-btn--small mui-btn--primary"
|
||||
, attribute "data-mui-toggle" "dropdown"
|
||||
, title "Game menu."
|
||||
] [ Icon.fwIcon "ellipsis-h" ]
|
||||
, ul [ class "mui-dropdown__menu mui-dropdown__menu--right" ]
|
||||
[ li [] [ a [ class "link"
|
||||
, attribute "tabindex" "0"
|
||||
, attribute "role" "button"
|
||||
, attribute "onClick" "inviteOverlay()"
|
||||
] [ Icon.fwIcon "bullhorn", text " Invite Players" ] ]
|
||||
, li [] [ a [ class "link"
|
||||
, attribute "tabindex" "0"
|
||||
, attribute "role" "button"
|
||||
, onClick Leave
|
||||
] [ Icon.fwIcon "sign-out", text " Leave Game" ] ]
|
||||
, li [ class "mui-divider" ] []
|
||||
, li [] [ a [ class "link"
|
||||
, attribute "tabindex" "0"
|
||||
, attribute "role" "button"
|
||||
, attribute "onClick" "aboutOverlay()"
|
||||
] [ Icon.fwIcon "info-circle", text " About" ] ]
|
||||
, li [] [ a [ href "https://github.com/Lattyware/massivedecks/issues/new", target "_blank" ]
|
||||
[ Icon.fwIcon "bug", text " Report a bug" ] ]
|
||||
]
|
||||
([ li [] [ a [ class "link"
|
||||
, attribute "tabindex" "0"
|
||||
, attribute "role" "button"
|
||||
, onClick (DisplayInviteOverlay |> LocalMessage)
|
||||
] [ Icon.fwIcon "bullhorn", text " Invite Players" ] ]
|
||||
] ++ (notificationsMenuItem model.browserNotifications) ++
|
||||
[ li [] [ a [ class "link"
|
||||
, attribute "tabindex" "0"
|
||||
, attribute "role" "button"
|
||||
, onClick Leave
|
||||
] [ Icon.fwIcon "sign-out", text " Leave Game" ] ]
|
||||
, li [ class "mui-divider" ] []
|
||||
, li [] [ a [ class "link"
|
||||
, attribute "tabindex" "0"
|
||||
, attribute "role" "button"
|
||||
, onClick (About.show |> OverlayMessage)
|
||||
] [ Icon.fwIcon "info-circle", text " About" ] ]
|
||||
, li [] [ a [ href "https://github.com/Lattyware/massivedecks/issues/new", target "_blank" ]
|
||||
[ Icon.fwIcon "bug", text " Report a bug" ] ]
|
||||
])
|
||||
]
|
||||
|
||||
@@ -357,7 +357,12 @@ renderSkippingNotice : List (Html Message)
|
||||
renderSkippingNotice =
|
||||
[ div [ class "notice" ]
|
||||
[ h3 [] [ Icon.icon "fast-forward" ]
|
||||
, span [] [ text "You are currently being skipped because you took too long to play."
|
||||
, span [] [ text "You are currently being skipped because you took too long to play. "
|
||||
, a [ class "link"
|
||||
, attribute "tabindex" "0"
|
||||
, attribute "role" "button"
|
||||
]
|
||||
[ Icon.fwIcon "bell", text "Enable Notifications?" ]
|
||||
]
|
||||
, div [ class "actions" ]
|
||||
[ button [ class "mui-btn mui-btn--small"
|
||||
|
||||
@@ -8,6 +8,7 @@ import MassiveDecks.Models.Game as Game
|
||||
import MassiveDecks.Components.Storage as Storage
|
||||
import MassiveDecks.Components.Input as Input
|
||||
import MassiveDecks.Components.Errors as Errors
|
||||
import MassiveDecks.Components.Overlay as Overlay
|
||||
import MassiveDecks.Scenes.Start.Messages exposing (InputId(..), Message(..))
|
||||
import MassiveDecks.Scenes.Start.Models exposing (Model)
|
||||
import MassiveDecks.Scenes.Start.UI as UI
|
||||
@@ -36,6 +37,7 @@ init init =
|
||||
, gameCodeInput = Input.init GameCode "game-code-input" [ text "The code for the game to join." ] (init.gameCode |> Maybe.withDefault "") "" InputMessage
|
||||
, info = Nothing
|
||||
, errors = Errors.init
|
||||
, overlay = Overlay.init OverlayMessage
|
||||
}
|
||||
, command)
|
||||
|
||||
@@ -65,9 +67,9 @@ view model =
|
||||
Html.map LobbyMessage (Lobby.view lobby)
|
||||
in
|
||||
div []
|
||||
[ contents
|
||||
, Errors.view model.errors |> Html.map ErrorMessage
|
||||
]
|
||||
([ contents
|
||||
, Errors.view model.errors |> Html.map ErrorMessage
|
||||
] ++ Overlay.view model.overlay)
|
||||
|
||||
|
||||
{-| Handles messages and alters the model as appropriate.
|
||||
@@ -114,11 +116,17 @@ update message model =
|
||||
, gameCodeInput = gameCodeInput
|
||||
}, Cmd.batch [ nameCmd, gameCodeCmd ])
|
||||
|
||||
OverlayMessage overlayMessage ->
|
||||
({ model | overlay = Overlay.update overlayMessage model.overlay }, Cmd.none)
|
||||
|
||||
LobbyMessage message ->
|
||||
case message of
|
||||
Lobby.ErrorMessage message ->
|
||||
(model, Util.cmd (ErrorMessage message))
|
||||
|
||||
Lobby.OverlayMessage message ->
|
||||
(model, Util.cmd (OverlayMessage (Overlay.map (Lobby.LocalMessage >> LobbyMessage) message)))
|
||||
|
||||
Lobby.Leave ->
|
||||
let
|
||||
leave = case model.lobby of
|
||||
|
||||
@@ -2,6 +2,7 @@ module MassiveDecks.Scenes.Start.Messages exposing (..)
|
||||
|
||||
import MassiveDecks.Components.Input as Input
|
||||
import MassiveDecks.Components.Errors as Errors
|
||||
import MassiveDecks.Components.Overlay as Overlay
|
||||
import MassiveDecks.Scenes.Lobby.Messages as Lobby
|
||||
import MassiveDecks.Models.Game as Game
|
||||
import MassiveDecks.Models.Player as Player
|
||||
@@ -19,6 +20,7 @@ type Message
|
||||
| InputMessage (Input.Message InputId)
|
||||
| LobbyMessage Lobby.ConsumerMessage
|
||||
| ErrorMessage Errors.Message
|
||||
| OverlayMessage (Overlay.Message Message)
|
||||
| NoOp
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import MassiveDecks.Models exposing (Init)
|
||||
import MassiveDecks.Scenes.Lobby.Models as Lobby
|
||||
import MassiveDecks.Components.Input as Input
|
||||
import MassiveDecks.Components.Errors as Errors
|
||||
import MassiveDecks.Components.Overlay as Overlay
|
||||
import MassiveDecks.Scenes.Start.Messages exposing (Message, InputId(..))
|
||||
|
||||
|
||||
@@ -16,4 +17,5 @@ type alias Model =
|
||||
, gameCodeInput : Input.Model InputId Message
|
||||
, info : Maybe String
|
||||
, errors : Errors.Model
|
||||
, overlay : Overlay.Model Message
|
||||
}
|
||||
|
||||
@@ -42,10 +42,9 @@ view model =
|
||||
, a [ class "about-link mui--divider-top link"
|
||||
, attribute "tabindex" "0"
|
||||
, attribute "role" "button"
|
||||
, attribute "onClick" "aboutOverlay()"
|
||||
, onClick (About.show |> OverlayMessage)
|
||||
]
|
||||
[ Icon.icon "question-circle" , text " About" ]
|
||||
, About.aboutOverlay
|
||||
, div [ id "forkongithub" ] [ div [] [ a [ href "https://github.com/lattyware/massivedecks", target "_blank" ]
|
||||
[ Icon.icon "github", text " Fork me on GitHub" ] ] ]
|
||||
])
|
||||
|
||||
@@ -131,3 +131,11 @@ apply fs value = List.map (\f -> f value) fs
|
||||
after : Time -> Task x a -> Task x a
|
||||
after waitFor task =
|
||||
Process.sleep waitFor `Task.andThen` (\_ -> task)
|
||||
|
||||
{-| Check if a Maybe is Nothing.
|
||||
-}
|
||||
isNothing : Maybe a -> Bool
|
||||
isNothing m =
|
||||
case m of
|
||||
Just _ -> False
|
||||
Nothing -> True
|
||||
|
||||
Reference in New Issue
Block a user