diff --git a/client/src/elm/MassiveDecks/Models/Decoders.elm b/client/src/elm/MassiveDecks/Models/Decoders.elm index 9f8394da..06c99b09 100644 --- a/client/src/elm/MassiveDecks/Models/Decoders.elm +++ b/client/src/elm/MassiveDecks/Models/Decoders.elm @@ -47,6 +47,7 @@ import MassiveDecks.Game.Time as Time import MassiveDecks.Model exposing (..) import MassiveDecks.Models.MdError as MdError exposing (MdError) import MassiveDecks.Notifications.Model as Notifications +import MassiveDecks.Pages.Lobby.Chat as Chat import MassiveDecks.Pages.Lobby.Configure.Decks.Model as DeckConfig import MassiveDecks.Pages.Lobby.Configure.Model as Configure import MassiveDecks.Pages.Lobby.Configure.Privacy.Model as PrivacyConfig @@ -196,6 +197,7 @@ settings = Json.succeed Settings |> Json.required "tokens" (Json.dict lobbyToken) |> Json.required "openUserList" Json.bool + |> Json.required "openChat" Json.bool |> Json.optional "lastUsedName" (Json.string |> Json.map Just) Nothing |> Json.required "recentDecks" (Json.list externalSource) |> Json.optional "chosenLanguage" (language |> Json.map Just) Nothing @@ -316,10 +318,18 @@ lobby ld calls = |> Json.required "users" users |> Json.required "owner" userId |> Json.required "config" config + |> Json.required "messages" (Json.list message) |> Json.optional "game" (game ld calls |> Json.map (Game.emptyModel >> Just)) Nothing |> Json.optional "errors" (Json.list gameStateError) [] +message : Json.Decoder Chat.Message +message = + Json.succeed Chat.Message + |> Json.required "content" Json.string + |> Json.required "author" Json.string + + game : Maybe LikeDetail -> Maybe (List Card.Call) -> Json.Decoder Game game ld calls = Json.succeed Game @@ -776,6 +786,9 @@ eventByName name = "GameEnded" -> gameEvent ended + "ReceiveChatMessage" -> + receiveChatMessage + "ErrorEncountered" -> errorEncountered @@ -809,6 +822,12 @@ ended = (Json.field "winner" (Json.list userId |> Json.map Set.fromList)) +receiveChatMessage : Json.Decoder Events.Event +receiveChatMessage = + Json.map (\m -> Events.ReceiveChatMessage { message = m }) + (Json.field "message" message) + + stageTimerDone : Json.Decoder Events.GameEvent stageTimerDone = Json.map2 (\r -> \s -> Events.StageTimerDone { round = r, stage = s }) diff --git a/client/src/elm/MassiveDecks/Models/Encoders.elm b/client/src/elm/MassiveDecks/Models/Encoders.elm index 966a196a..90f9c964 100644 --- a/client/src/elm/MassiveDecks/Models/Encoders.elm +++ b/client/src/elm/MassiveDecks/Models/Encoders.elm @@ -305,6 +305,7 @@ settings s = List.concat [ [ ( "tokens", s.tokens |> Dict.toList |> List.map (\( gc, t ) -> ( gc, Json.string t )) |> Json.object ) , ( "openUserList", Json.bool s.openUserList ) + , ( "openChat", Json.bool s.openChat ) , ( "recentDecks", Json.list source s.recentDecks ) , ( "compactCards", s.cardSize |> cardSize ) , ( "speech", s.speech |> speech ) diff --git a/client/src/elm/MassiveDecks/Pages/Lobby.elm b/client/src/elm/MassiveDecks/Pages/Lobby.elm index a2ebe319..ea96896d 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby.elm @@ -18,6 +18,7 @@ import Html exposing (Html) import Html.Attributes as HtmlA import Html.Events as HtmlE import Html.Keyed as HtmlK +import Json.Decode as Json import Json.Patch as Json import MassiveDecks.Animated as Animated exposing (Animated) import MassiveDecks.Card.Model as Card @@ -38,6 +39,7 @@ import MassiveDecks.Icon as Icon import MassiveDecks.Model exposing (..) import MassiveDecks.Models.MdError as MdError import MassiveDecks.Pages.Lobby.Actions as Actions +import MassiveDecks.Pages.Lobby.Chat as Chat import MassiveDecks.Pages.Lobby.Configure as Configure import MassiveDecks.Pages.Lobby.Configure.Model as Configure import MassiveDecks.Pages.Lobby.Events as Events @@ -60,7 +62,6 @@ import MassiveDecks.User as User exposing (User) import MassiveDecks.Util.Html as Html import MassiveDecks.Util.Html.Attributes as HtmlA import MassiveDecks.Util.Maybe as Maybe -import MassiveDecks.Util.NeList as NeList import Material.Button as Button import Material.Card as Card import Material.IconButton as IconButton @@ -102,6 +103,7 @@ initWithAuth _ r auth = , spectate = Spectate.init , gameMenu = Menu.Closed , userMenu = Nothing + , chatInput = "" } , ServerConnection.connect auth.claims.gc auth.token ) @@ -343,6 +345,13 @@ update wrap shared msg model = , Cmd.none ) + Events.ReceiveChatMessage message -> + let + m = + lobby.messages ++ [ message.message ] + in + ( Stay { model | lobbyAndConfigure = model.lobbyAndConfigure |> Maybe.map (\l -> { l | lobby = { lobby | messages = m } }) }, shared, Cmd.none ) + Nothing -> case event of Events.Sync { state, hand, play, partialTimeAnchor } -> @@ -421,6 +430,18 @@ update wrap shared msg model = Copy id -> ( Stay model, shared, Ports.copyText id ) + ChatMsg m -> + case m of + Chat.KeyDown key -> + if key == 13 then + ( Stay { model | chatInput = "" }, shared, Actions.sendChatMessage model.chatInput ) + + else + ( Stay model, shared, Cmd.none ) + + Chat.Input content -> + ( Stay { model | chatInput = content }, shared, Cmd.none ) + ChangeSection s -> let r = @@ -522,6 +543,9 @@ viewWithUsers wrap wrapSettings shared s viewContent model = usersShown = shared.settings.settings.openUserList + chatShown = + shared.settings.settings.openChat + castAttrs = case shared.castStatus of Cast.NoDevicesAvailable -> @@ -548,6 +572,13 @@ viewWithUsers wrap wrapSettings shared s viewContent model = else Icon.users + chatIcon = + if chatShown then + Icon.hide + + else + Icon.facebookMessenger + lobby = model.lobbyAndConfigure |> Maybe.map .lobby @@ -568,7 +599,7 @@ viewWithUsers wrap wrapSettings shared s viewContent model = in [ Html.div [ HtmlA.id "lobby" - , HtmlA.classList [ ( "collapsed-users", not usersShown ) ] + , HtmlA.classList [ ( "collapsed-users", not usersShown ), ( "collapsed-chat", not chatShown ) ] , shared.settings.settings.cardSize |> cardSizeToAttr ] (Html.div [ HtmlA.id "top-bar" ] @@ -578,6 +609,10 @@ viewWithUsers wrap wrapSettings shared s viewContent model = (usersIcon |> Icon.styled [ Icon.lg ] |> Icon.view) (Strings.ToggleUserList |> Lang.string shared) (usersShown |> not |> Settings.ChangeOpenUserList |> wrapSettings |> Just) + , IconButton.view + (chatIcon |> Icon.styled [ Icon.lg ] |> Icon.view) + (Strings.ToggleChat |> Lang.string shared) + (chatShown |> not |> Settings.ChangeOpenChat |> wrapSettings |> Just) , lobbyMenu wrap shared model.gameMenu model.route s audienceMode localUser localPlayer (maybeGame |> Maybe.map .game) ] , castButton @@ -587,7 +622,7 @@ viewWithUsers wrap wrapSettings shared s viewContent model = ] :: HtmlK.ol [ HtmlA.class "notifications" ] notifications :: (model.lobbyAndConfigure - |> Maybe.map2 (viewLobby wrap shared model.auth model.userMenu viewContent) model.timeAnchor + |> Maybe.map2 (viewLobby wrap shared model.auth model.userMenu viewContent model) model.timeAnchor |> Maybe.withDefault [ Html.div [ HtmlA.class "loading" ] [ Icon.loading |> Icon.styled [ Icon.fa3x ] |> Icon.view ] @@ -603,8 +638,8 @@ viewWithUsers wrap wrapSettings shared s viewContent model = ] -viewLobby : (Msg -> msg) -> Shared -> Auth -> Maybe User.Id -> ViewContent msg -> Time.Anchor -> LobbyAndConfigure -> List (Html msg) -viewLobby wrap shared auth openUserMenu viewContent timeAnchor lobbyAndConfigure = +viewLobby : (Msg -> msg) -> Shared -> Auth -> Maybe User.Id -> ViewContent msg -> Model -> Time.Anchor -> LobbyAndConfigure -> List (Html msg) +viewLobby wrap shared auth openUserMenu viewContent model timeAnchor lobbyAndConfigure = let lobby = lobbyAndConfigure.lobby @@ -632,6 +667,7 @@ viewLobby wrap shared auth openUserMenu viewContent timeAnchor lobbyAndConfigure in [ Html.div [ HtmlA.id "lobby-content" ] [ viewUsers wrap shared auth.claims.uid lobby openUserMenu game + , viewChat wrap lobby model , Html.div [ HtmlA.id "scroll-frame" ] [ viewContent configDisabledReason auth timeAnchor lobbyAndConfigure ] , lobby.errors |> viewErrors shared ] @@ -930,7 +966,27 @@ viewUsers wrap shared localUserId lobby openUserMenu game = groups = List.concat [ activeGroups, inactiveGroup ] in - Card.view [ HtmlA.id "users" ] [ Html.div [ HtmlA.class "collapsible" ] [ HtmlK.ol [] groups ] ] + Card.view [ HtmlA.id "users" ] + [ Html.div [ HtmlA.class "collapsible" ] + [ HtmlK.ol [] groups ] + ] + + +viewChat : (Msg -> msg) -> Lobby -> Model -> Html msg +viewChat wrap lobby model = + let + users = + lobby.users + + messages = + lobby.messages |> List.map (\message -> (users |> Dict.get message.author |> Maybe.map .name |> Maybe.withDefault "Unknown User") ++ ": " ++ message.content) |> List.map (Html.text >> (\t -> [ t ]) >> Html.p [ HtmlA.class "message" ]) + in + Card.view [ HtmlA.id "chat" ] + [ Html.div [ HtmlA.class "collapsible" ] + [ Html.ol [] messages + , Html.input [ HtmlA.placeholder "Message", HtmlE.on "keydown" (Json.map (Chat.KeyDown >> ChatMsg >> wrap) HtmlE.keyCode), HtmlE.onInput (Chat.Input >> ChatMsg >> wrap), HtmlA.value model.chatInput ] [] + ] + ] viewRoleGroup : (Msg -> msg) -> Shared -> User.Id -> User.Privilege -> Bool -> Maybe User.Id -> Maybe Game -> ( User.Role, List ( User.Id, User ) ) -> ( String, Html msg ) diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm index 50972072..4bce2624 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Actions.elm @@ -11,6 +11,7 @@ module MassiveDecks.Pages.Lobby.Actions exposing , pickCall , redraw , reveal + , sendChatMessage , setPlayerAway , setPresence , setPrivilege @@ -142,6 +143,11 @@ enforceTimeLimit round stage = ] +sendChatMessage : String -> Cmd msg +sendChatMessage message = + action "SendChatMessage" [ ( "message", message |> Json.string ) ] + + {- Private -} diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Chat.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Chat.elm new file mode 100644 index 00000000..3279b3a3 --- /dev/null +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Chat.elm @@ -0,0 +1,14 @@ +module MassiveDecks.Pages.Lobby.Chat exposing (..) + +import MassiveDecks.User as User + + +type alias Message = + { content : String + , author : User.Id + } + + +type Msg + = KeyDown Int + | Input String diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm index 130e26dc..2724355b 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm @@ -15,6 +15,7 @@ import MassiveDecks.Card.Play as Play import MassiveDecks.Game.Round as Round exposing (Round) import MassiveDecks.Game.Time as Time exposing (Time) import MassiveDecks.Models.MdError as MdError +import MassiveDecks.Pages.Lobby.Chat as Chat import MassiveDecks.Pages.Lobby.Model exposing (Lobby) import MassiveDecks.User as User import Set exposing (Set) @@ -38,6 +39,7 @@ type Event | PrivilegeChanged { user : User.Id, privilege : User.Privilege } | UserRoleChanged { user : User.Id, role : User.Role, hand : Maybe (List Card.Response) } | ErrorEncountered { error : MdError.GameStateError } + | ReceiveChatMessage { message : Chat.Message } {-| The user's intentional presence in the lobby. diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Messages.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Messages.elm index c9f881de..c14bfe7d 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Messages.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Messages.elm @@ -4,6 +4,7 @@ import MassiveDecks.Animated as Animated import MassiveDecks.Game.Messages as Game import MassiveDecks.Game.Time as Time import MassiveDecks.Models.MdError exposing (MdError) +import MassiveDecks.Pages.Lobby.Chat as Chat import MassiveDecks.Pages.Lobby.Configure.Messages as Configure import MassiveDecks.Pages.Lobby.Events exposing (Event) import MassiveDecks.Pages.Lobby.Model exposing (..) @@ -33,4 +34,5 @@ type Msg | SetGameMenuState Menu.State | SetUserMenuState User.Id Menu.State | EndGame + | ChatMsg Chat.Msg | NoOp diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Model.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Model.elm index 53349e01..dc54a020 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Model.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Model.elm @@ -18,6 +18,7 @@ import MassiveDecks.Error.Model exposing (Error) import MassiveDecks.Game.Model as Game import MassiveDecks.Game.Time as Time import MassiveDecks.Models.MdError exposing (GameStateError, MdError) +import MassiveDecks.Pages.Lobby.Chat as Chat import MassiveDecks.Pages.Lobby.Configure.Model as Configure import MassiveDecks.Pages.Lobby.GameCode exposing (GameCode) import MassiveDecks.Pages.Lobby.Route exposing (..) @@ -49,6 +50,7 @@ type alias Model = , spectate : Spectate.Model , gameMenu : Menu.State , userMenu : Maybe User.Id + , chatInput : String } @@ -64,6 +66,7 @@ type alias Lobby = { users : Dict User.Id User , owner : User.Id , config : Configure.Config + , messages : List Chat.Message , game : Maybe Game.Model , errors : List GameStateError } diff --git a/client/src/elm/MassiveDecks/Settings.elm b/client/src/elm/MassiveDecks/Settings.elm index dfafbaf3..e7aba990 100644 --- a/client/src/elm/MassiveDecks/Settings.elm +++ b/client/src/elm/MassiveDecks/Settings.elm @@ -75,6 +75,7 @@ defaults = { tokens = Dict.empty , lastUsedName = Nothing , openUserList = False + , openChat = False , recentDecks = [] , chosenLanguage = Nothing , cardSize = Full @@ -110,6 +111,9 @@ update shared msg = ChangeOpenUserList open -> changeSettings (\s -> { s | openUserList = open }) model + ChangeOpenChat open -> + changeSettings (\s -> { s | openChat = open }) model + RemoveInvalid tokenValidity -> let newTokens = diff --git a/client/src/elm/MassiveDecks/Settings/Messages.elm b/client/src/elm/MassiveDecks/Settings/Messages.elm index 80899e89..ec7d92eb 100644 --- a/client/src/elm/MassiveDecks/Settings/Messages.elm +++ b/client/src/elm/MassiveDecks/Settings/Messages.elm @@ -9,6 +9,7 @@ type Msg = ChangeLang (Maybe Language) | ChangeCardSize CardSize | ChangeOpenUserList Bool + | ChangeOpenChat Bool | ToggleOpen | RemoveInvalid (List Lobby.Token) | ToggleSpeech Bool diff --git a/client/src/elm/MassiveDecks/Settings/Model.elm b/client/src/elm/MassiveDecks/Settings/Model.elm index 0c50d713..dbdc42a9 100644 --- a/client/src/elm/MassiveDecks/Settings/Model.elm +++ b/client/src/elm/MassiveDecks/Settings/Model.elm @@ -28,6 +28,7 @@ This is really more than just user settings, it's any persistent data we store i type alias Settings = { tokens : Dict String Lobby.Token , openUserList : Bool + , openChat : Bool , lastUsedName : Maybe String , recentDecks : List Source.External , chosenLanguage : Maybe Language diff --git a/client/src/elm/MassiveDecks/Strings.elm b/client/src/elm/MassiveDecks/Strings.elm index 7d280b1d..e0bf29f8 100644 --- a/client/src/elm/MassiveDecks/Strings.elm +++ b/client/src/elm/MassiveDecks/Strings.elm @@ -215,6 +215,7 @@ type MdString | Likes { total : Int } -- A display of a number of likes. | LikesDescription -- A description of the number of likes a play received or a player has recieved. | ToggleUserList -- A description of the action of showing or hiding the user list. + | ToggleChat -- A description of the action of showing or hiding the chat. | GameMenu -- A description of the game menu. | UnknownUser -- A name for a user that doesn't have a known name. | InvitePlayers -- A short term for inviting players to the game. diff --git a/client/src/elm/MassiveDecks/Strings/Languages/De.elm b/client/src/elm/MassiveDecks/Strings/Languages/De.elm index 00e3237c..dde89f1b 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/De.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/De.elm @@ -694,6 +694,10 @@ translate _ mdString = ToggleUserList -> [ Text "Anzeigen oder Ausblenden der Anzeigetafel." ] + -- TODO: Translate + ToggleChat -> + [ Missing ] + GameMenu -> [ Text "Spiel-Menü." ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/DeXInformal.elm b/client/src/elm/MassiveDecks/Strings/Languages/DeXInformal.elm index e5e60abc..704a7d48 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/DeXInformal.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/DeXInformal.elm @@ -692,6 +692,10 @@ translate _ mdString = ToggleUserList -> [ Text "Anzeigen oder Ausblenden der Anzeigetafel." ] + -- TODO: Translate + ToggleChat -> + [ Missing ] + GameMenu -> [ Text "Spiel-Menü." ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/En/Internal.elm b/client/src/elm/MassiveDecks/Strings/Languages/En/Internal.elm index 62bf8f00..b6e9753e 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/En/Internal.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/En/Internal.elm @@ -642,6 +642,9 @@ translate _ mdString = ToggleUserList -> [ Text "Show or hide the scoreboard." ] + ToggleChat -> + [ Text "Show or hide the chat." ] + GameMenu -> [ Text "Game menu." ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/Es.elm b/client/src/elm/MassiveDecks/Strings/Languages/Es.elm index b404d00a..368f07a7 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/Es.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/Es.elm @@ -676,6 +676,10 @@ translate _ mdString = ToggleUserList -> [ Text "Mostrar o esconder el marcador." ] + -- TODO: Translate + ToggleChat -> + [ Missing ] + GameMenu -> [ Text "Menú de la partida." ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/Id.elm b/client/src/elm/MassiveDecks/Strings/Languages/Id.elm index 9233c73b..2c840f73 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/Id.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/Id.elm @@ -683,6 +683,10 @@ translate _ mdString = ToggleUserList -> [ Text "Menampilkan atau menyembunyikan papan skor." ] + -- TODO: Translate + ToggleChat -> + [ Missing ] + GameMenu -> [ Text "Menu permainan." ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/It.elm b/client/src/elm/MassiveDecks/Strings/Languages/It.elm index 836acf80..4fa271ce 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/It.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/It.elm @@ -703,6 +703,10 @@ translate _ mdString = ToggleUserList -> [ Text "Mostra o nascondi la classifica." ] + -- TODO: Translate + ToggleChat -> + [ Missing ] + GameMenu -> [ Text "Menu del gioco." ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/Ko.elm b/client/src/elm/MassiveDecks/Strings/Languages/Ko.elm index e7461e26..0d8467b7 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/Ko.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/Ko.elm @@ -659,6 +659,10 @@ translate _ mdString = ToggleUserList -> [ Text "스코어보드를 보이거나 숨깁니다." ] + -- TODO: Translate + ToggleChat -> + [ Missing ] + GameMenu -> [ Text "게임 메뉴." ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/Pl.elm b/client/src/elm/MassiveDecks/Strings/Languages/Pl.elm index db4c9dd9..e2890786 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/Pl.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/Pl.elm @@ -770,6 +770,10 @@ translate maybeDeclCase mdString = ToggleUserList -> [ Text "Ukryj lub pokaż listę graczy." ] + -- TODO: Translate + ToggleChat -> + [ Missing ] + GameMenu -> [ Text "Menu gry." ] diff --git a/client/src/elm/MassiveDecks/Strings/Languages/PtBR.elm b/client/src/elm/MassiveDecks/Strings/Languages/PtBR.elm index de79ffa3..cf34d651 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/PtBR.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/PtBR.elm @@ -690,6 +690,10 @@ translate _ mdString = ToggleUserList -> [ Text "Mostrar ou ocultar a lista de pontuação." ] + -- TODO: Translate + ToggleChat -> + [ Missing ] + GameMenu -> [ Text "Menu do jogo." ] diff --git a/client/src/scss/pages/_lobby.scss b/client/src/scss/pages/_lobby.scss index bfdb093e..dacefc0b 100644 --- a/client/src/scss/pages/_lobby.scss +++ b/client/src/scss/pages/_lobby.scss @@ -6,7 +6,7 @@ @use "../cards/_colors" as card-colors; $top-bar-height: 4rem; -$users-width: 18rem; +$side-bar-width: 18rem; @keyframes fadeIn { from { @@ -54,14 +54,14 @@ $users-width: 18rem; #scroll-frame { position: absolute; top: 0; - left: $users-width; + left: $side-bar-width; right: 0; bottom: 0; overflow: auto; transition: left 0.3s; } -.collapsed-users #scroll-frame { +.collapsed-users.collapsed-chat #scroll-frame { left: 0; } @@ -92,11 +92,11 @@ $users-width: 18rem; left: 0; top: 0; bottom: 0; - width: $users-width; + width: $side-bar-width; transition: width 0.3s; - --mdc-menu-max-width: #{calc($users-width - 2em)}; + --mdc-menu-max-width: #{calc($side-bar-width - 2em)}; mwc-menu { left: 0; @@ -173,12 +173,12 @@ $users-width: 18rem; } } -@media screen and (max-width: $users-width * 2) { +@media screen and (max-width: $side-bar-width * 2) { #scroll-frame { left: 100vw; } - #users { + #side-bar { width: 100vw; } } @@ -187,6 +187,57 @@ $users-width: 18rem; width: 0; } +.collapsed-chat #chat { + width: 0; +} + +#chat { + font-size: 1rem; + overflow-y: auto; + + z-index: 2; + + border-radius: 0; + + padding: 0; + + min-height: 100%; + + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: $side-bar-width; + + transition: width 0.3s; + + .collapsible { + padding: 1em; + } + + ol { + list-style: none; + margin: 0; + padding: 0; + } + + li { + margin-top: 0.5em; + } + + display: flex; + flex-direction: column-reverse; + + .message { + margin: 0.5em; + } + + input { + position: absolute; + bottom: 0; + } +} + .cast-button { &.connecting { animation: fadeIn 1s infinite alternate; diff --git a/server/src/ts/action.ts b/server/src/ts/action.ts index 8718efed..7082c3b3 100644 --- a/server/src/ts/action.ts +++ b/server/src/ts/action.ts @@ -4,6 +4,7 @@ import * as GameAction from "./action/game-action.js"; import type * as Handler from "./action/handler.js"; import * as Leave from "./action/leave.js"; import * as Privileged from "./action/privileged.js"; +import * as SendChatMessage from "./action/send-chat-message.js"; import * as SetUserRole from "./action/set-user-role.js"; import * as Validation from "./action/validation.validator.js"; import { AlreadyAuthenticatedError } from "./errors/authentication.js"; @@ -18,12 +19,14 @@ export type Action = | GameAction.GameAction | Privileged.Privileged | SetUserRole.SetUserRole + | SendChatMessage.SendChatMessage | Leave.Leave; const allActions = new Actions.PassThroughGroup( GameAction.actions, Privileged.actions, SetUserRole.actions, + SendChatMessage.actions, Leave.actions, ); diff --git a/server/src/ts/action/send-chat-message.ts b/server/src/ts/action/send-chat-message.ts new file mode 100644 index 00000000..801fd07c --- /dev/null +++ b/server/src/ts/action/send-chat-message.ts @@ -0,0 +1,45 @@ +import type { Action } from "../action.js"; +import type * as Lobby from "../lobby.js"; +import type * as Handler from "./handler.js"; +import * as Actions from "./actions.js"; +import * as Event from "../event.js"; +import * as ReceiveChatMessage from "../events/lobby-event/receive-chat-message.js"; + +/** + * A player sends a message in the chat. + */ +export interface SendChatMessage { + action: "SendChatMessage"; + message: string; +} + +class SendChatMessageActions extends Actions.Implementation< + Action, + SendChatMessage, + "SendChatMessage", + Lobby.Lobby +> { + protected readonly name = "SendChatMessage"; + + protected handle: Handler.Custom = ( + auth, + lobby, + action, + ) => { + lobby.messages.push({ + content: action.message, + author: auth.uid, + }); + const allEvents = [ + Event.targetAll( + ReceiveChatMessage.of({ content: action.message, author: auth.uid }), + ), + ]; + return { + lobby, + events: allEvents, + }; + }; +} + +export const actions = new SendChatMessageActions(); diff --git a/server/src/ts/action/validation.validator.ts b/server/src/ts/action/validation.validator.ts index 2704b965..61557bbc 100644 --- a/server/src/ts/action/validation.validator.ts +++ b/server/src/ts/action/validation.validator.ts @@ -80,6 +80,9 @@ export const Schema = { { $ref: "#/definitions/SetUserRole", }, + { + $ref: "#/definitions/SendChatMessage", + }, { $ref: "#/definitions/Leave", }, @@ -759,7 +762,6 @@ export const Schema = { description: "The name the user wishes to use.", maxLength: 100, minLength: 1, - type: "string", }, password: { description: @@ -821,6 +823,21 @@ export const Schema = { enum: ["Player", "Spectator"], type: "string", }, + SendChatMessage: { + additionalProperties: false, + description: "A player sends a message in the chat.", + properties: { + action: { + enum: ["SendChatMessage"], + type: "string", + }, + message: { + type: "string", + }, + }, + required: ["action", "message"], + type: "object", + }, SetPlayerAway: { additionalProperties: false, description: "A privileged user asks to set a given player as away.", diff --git a/server/src/ts/events/lobby-event.ts b/server/src/ts/events/lobby-event.ts index 244546f9..5a0da1c1 100644 --- a/server/src/ts/events/lobby-event.ts +++ b/server/src/ts/events/lobby-event.ts @@ -4,6 +4,7 @@ import type { ErrorEncountered } from "./lobby-event/error-encountered.js"; import type { PresenceChanged } from "./lobby-event/presence-changed.js"; import type { PrivilegeChanged } from "./lobby-event/privilege-changed.js"; import type { UserRoleChanged } from "./lobby-event/user-role-changed.js"; +import type { ReceiveChatMessage } from "./lobby-event/receive-chat-message.js"; export type LobbyEvent = | Configured @@ -11,4 +12,5 @@ export type LobbyEvent = | PresenceChanged | PrivilegeChanged | UserRoleChanged - | ErrorEncountered; + | ErrorEncountered + | ReceiveChatMessage; diff --git a/server/src/ts/events/lobby-event/receive-chat-message.ts b/server/src/ts/events/lobby-event/receive-chat-message.ts new file mode 100644 index 00000000..32f4d685 --- /dev/null +++ b/server/src/ts/events/lobby-event/receive-chat-message.ts @@ -0,0 +1,14 @@ +import type * as Chat from "../../lobby/chat.js"; + +/** + * Indicates a new message has been sent to the lobby. + */ +export interface ReceiveChatMessage { + event: "ReceiveChatMessage"; + message: Chat.Message; +} + +export const of = (message: Chat.Message): ReceiveChatMessage => ({ + event: "ReceiveChatMessage", + message, +}); diff --git a/server/src/ts/lobby.ts b/server/src/ts/lobby.ts index 33b22f77..e3dc8770 100644 --- a/server/src/ts/lobby.ts +++ b/server/src/ts/lobby.ts @@ -1,5 +1,6 @@ import type { CreateLobby } from "./action/initial/create-lobby.js"; import type { RegisterUser } from "./action/initial/register-user.js"; +import type * as Chat from "./lobby/chat.js"; import type * as Errors from "./errors.js"; import * as Event from "./event.js"; import * as PresenceChanged from "./events/lobby-event/presence-changed.js"; @@ -20,6 +21,7 @@ export interface Lobby { users: { [id: string]: User.User }; owner: User.Id; config: Config.Config; + messages: Chat.Message[]; game?: Game.Game; errors: Errors.Details[]; } @@ -50,6 +52,7 @@ export interface Public { users: { [id: string]: User.Public }; owner: User.Id; config: Config.Public; + messages: Chat.Message[]; game?: Game.Public; errors?: Errors.Details[]; } @@ -121,6 +124,7 @@ export function create( users: { [ownerId]: User.create(creation.owner, "Player", "Privileged") }, owner: ownerId, config: config, + messages: [], errors: [], }; config.rules.houseRules.rando = Rando.create( @@ -167,6 +171,7 @@ export const censor = (lobby: Lobby): Public => ({ users: usersObj(lobby), owner: lobby.owner, config: Config.censor(lobby.config), + messages: lobby.messages, ...(lobby.game === undefined ? {} : { game: lobby.game.public() }), ...(lobby.errors.length === 0 ? {} : { errors: lobby.errors }), }); diff --git a/server/src/ts/lobby/chat.ts b/server/src/ts/lobby/chat.ts new file mode 100644 index 00000000..18e25d5a --- /dev/null +++ b/server/src/ts/lobby/chat.ts @@ -0,0 +1,4 @@ +export interface Message { + content: string; + author: string; +}