diff --git a/backend/src/main/scala/industries/sunshine/planningpoker/MyHttpService.scala b/backend/src/main/scala/industries/sunshine/planningpoker/MyHttpService.scala index 3173de8..82d04be 100644 --- a/backend/src/main/scala/industries/sunshine/planningpoker/MyHttpService.scala +++ b/backend/src/main/scala/industries/sunshine/planningpoker/MyHttpService.scala @@ -28,7 +28,7 @@ object MyHttpService { Stream .emits(TestModels.testChangesList) .covary[IO] - .metered(1.second) ++ Stream.never[IO] + .metered(1.second) ) .map(state => WebSocketFrame.Text(state.asJson.noSpaces)) diff --git a/frontend/src/main/scala/industries/sunshine/planningpoker/FrontendMain.scala b/frontend/src/main/scala/industries/sunshine/planningpoker/FrontendMain.scala index 5ebfea0..d775147 100644 --- a/frontend/src/main/scala/industries/sunshine/planningpoker/FrontendMain.scala +++ b/frontend/src/main/scala/industries/sunshine/planningpoker/FrontendMain.scala @@ -20,36 +20,8 @@ def FrontendMain(): Unit = ) object Main { - final case class AppState( - myId: Option[PlayerID] - ) - // TODO is this ok for state creation? link with auth component and use in another? - val appStateSignal = Var(AppState(Some(TestModels.me.id))).signal - import io.laminext.websocket.circe.WebSocket._ - import io.laminext.websocket.circe.webSocketReceiveBuilderSyntax - - val roomStateWSStream = io.laminext.websocket.WebSocket - .path("/api/subscribe") - .json[RoomStateView, Unit] - .build( - managed = true, - autoReconnect = false, - reconnectDelay = 1.second, - reconnectDelayOffline = 20.seconds, - reconnectRetries = 1 - ) - - val stateStream = roomStateWSStream.received - // and what's the difference between EventStream and Signal??? - val stateSignal = - stateStream.startWith(RoomStateView.empty) - - // NOTE let's try with fetch \ rest - // import io.laminext.fetch.Fetch - // import com.raquo.laminar.api.L._ - // import scala.concurrent.ExecutionContext.Implicits.global - // val testRest = Fetch.get("http://0.0.0.0:5173/api/subscribe").text + val loggedIn = Var(true) import scala.scalajs.js.Dynamic.{global => g} def appElement(): Element = { @@ -59,9 +31,11 @@ object Main { className := "h-24 w-full flex flex-for justify-center items-center bg-green-200", p(className := "text-2xl", "Here be header") ), - child <-- roomStateWSStream.isConnected.map( if (_) emptyNode else JoinRoomComponent.render()), - child <-- roomStateWSStream.isConnected.map( if (_) RoomView.renderRoom(stateSignal) else emptyNode), - roomStateWSStream.connect + child <-- loggedIn.signal.map( mybool => s"the 'logged in' signal is $mybool" ), + child <-- loggedIn.signal.map(if (_) emptyNode else JoinRoomComponent.render(loggedIn.writer)), + child <-- loggedIn.signal.map( + if (_) RoomView.renderRoom(loggedIn.writer) else emptyNode + ), ) } } diff --git a/frontend/src/main/scala/industries/sunshine/planningpoker/JoinRoomComponent.scala b/frontend/src/main/scala/industries/sunshine/planningpoker/JoinRoomComponent.scala index ec349de..95e518f 100644 --- a/frontend/src/main/scala/industries/sunshine/planningpoker/JoinRoomComponent.scala +++ b/frontend/src/main/scala/industries/sunshine/planningpoker/JoinRoomComponent.scala @@ -48,28 +48,35 @@ object JoinRoomComponent { val (responsesStream, responseReceived) = EventStream.withCallback[FetchResponse[String]] - val submitButton = button( - "Join room", - onClick - .mapTo { - (roomNameVar.now(), roomPassVar.now(), nicknameVar.now(), nicknamePass.now()) - } - .flatMap { case (roomName, roomPass, nickname, nicknamePass) => - Fetch - .post( - "/api/login", - body = Requests.LogIn( - roomName, - nickname, - roomPass, - nicknamePass - ) - ) - .text - } --> responseReceived - ) - def render(): Element = { + def render(loggedIn: Observer[Boolean]): Element = { + + val submitButton = button( + "Join room", + onClick + .mapTo { + (roomNameVar.now(), roomPassVar.now(), nicknameVar.now(), nicknamePass.now()) + } + .flatMap { case (roomName, roomPass, nickname, nicknamePass) => + Fetch + .post( + "/api/login", + body = Requests.LogIn( + roomName, + nickname, + roomPass, + nicknamePass + ) + ) + .text.map { response => + if (response.ok) { + loggedIn.onNext(true) + response + } else response + } + } --> responseReceived + ) + div( className := "flex flex-col h-full justify-center", "Logging in:", @@ -86,19 +93,21 @@ object JoinRoomComponent { cls := "flex flex-col space-y-4 p-4 max-h-96 overflow-auto bg-gray-900", children.command <-- responsesStream.recoverToTry.map { case Success(response) => - CollectionCommand.Append( - div( + { + CollectionCommand.Append( div( - cls := "flex space-x-2 items-center", - code(cls := "text-green-500", "Status: "), - code(cls := "text-green-300", s"${response.status} ${response.statusText}") - ), - div( - cls := "text-green-400 text-xs", - code(response.data) + div( + cls := "flex space-x-2 items-center", + code(cls := "text-green-500", "Status: "), + code(cls := "text-green-300", s"${response.status} ${response.statusText}") + ), + div( + cls := "text-green-400 text-xs", + code(response.data) + ) ) ) - ) + } case Failure(exception) => CollectionCommand.Append( div( diff --git a/frontend/src/main/scala/industries/sunshine/planningpoker/RoomView.scala b/frontend/src/main/scala/industries/sunshine/planningpoker/RoomView.scala index d2c85f1..57de0ca 100644 --- a/frontend/src/main/scala/industries/sunshine/planningpoker/RoomView.scala +++ b/frontend/src/main/scala/industries/sunshine/planningpoker/RoomView.scala @@ -3,18 +3,41 @@ package industries.sunshine.planningpoker import scala.scalajs.js import com.raquo.laminar.api.L.{*, given} import industries.sunshine.planningpoker.common.Models.* +import io.laminext.websocket.circe.WebSocket._ +import io.laminext.websocket.circe.webSocketReceiveBuilderSyntax + +import scala.concurrent.duration._ -/** Rendering of the Room state - */ object RoomView { - // TODO this will take in signal of the room observable - // NOTE i guess "other players" would have to be in circle with 'me' as empty space in the bottom - def renderRoom(roomStateSignal: Signal[RoomStateView]): Element = { - // i want to number other players, for the star arrangement + + /** Rendering of the Room state central table with cards, player control UI and other players + * description + * + * @param loggedIn + * \- channel for signaling to the parent about dead websocket, i.e logged out state + */ + def renderRoom(loggedIn: Observer[Boolean]): Element = { + + val wsStream = io.laminext.websocket.WebSocket + .path("/api/subscribe") + .json[RoomStateView, Unit] + .build( + managed = true, + autoReconnect = true, + reconnectDelay = 500.millis, + reconnectDelayOffline = 20.seconds, + reconnectRetries = 3 + ) + + val roomStateSignal: Signal[RoomStateView] = + wsStream.received.startWith(RoomStateView.empty) + val otherPlayers = roomStateSignal.map { state => state.players.filterNot(_.id == state.me).zipWithIndex } + val wsFinalDeathSignal = wsStream.closed.collect { case (_, false) => () } + div( className := "w-full h-full border-4 border-amber-900 flex flex-col", div( @@ -23,7 +46,9 @@ object RoomView { renderPlayer(playerSignal, roomStateSignal.map(_.allowedCards.size)) } ), - TableView.renderTable(roomStateSignal) + TableView.renderTable(roomStateSignal), + wsStream.connect, + wsFinalDeathSignal.map(_ => false) --> loggedIn ) } diff --git a/frontend/src/main/scala/industries/sunshine/planningpoker/TableView.scala b/frontend/src/main/scala/industries/sunshine/planningpoker/TableView.scala index 279293a..74c4549 100644 --- a/frontend/src/main/scala/industries/sunshine/planningpoker/TableView.scala +++ b/frontend/src/main/scala/industries/sunshine/planningpoker/TableView.scala @@ -14,9 +14,8 @@ object TableView { def renderTable(roundSignal: Signal[RoomStateView]): Element = { val playerIdToCardTypeSignal = roundSignal - .combineWith(Main.appStateSignal.map(_.myId)) - .map((state, myIdOpt) => - state.players.map(p => p.id -> getPlayerCardType(p.id, state.round, p.name, myIdOpt)) + .map(state => + state.players.map(p => p.id -> getPlayerCardType(p.id, state.round, p.name, state.me)) ) div( @@ -36,11 +35,11 @@ object TableView { id: PlayerID, state: RoundState, name: String, - myId: Option[PlayerID] + myId: PlayerID ): CardState = { state match { case isOpen: RoundState.Voting => - if (myId.forall(_ == id)) { + if (myId == id) { isOpen.myCard.fold(NoCard(name))(vote => Open(vote)) } else isOpen.alreadyVoted.find(_.id == id).fold(NoCard(name))(_ => CardBack) case isClosed: RoundState.Viewing =>