connect login and room pages via logged in state
moving websocket into being managed by the room component. if the partent would want access to "user signal" it can ask via observer. that would mean bidirectionality, and i already hear screeching of my intuitions from react, but maybe that's ok and in react i would still scope the websocket to the room page, plus callbacks
This commit is contained in:
parent
90e886c62d
commit
aed4812ccc
|
@ -28,7 +28,7 @@ object MyHttpService {
|
||||||
Stream
|
Stream
|
||||||
.emits(TestModels.testChangesList)
|
.emits(TestModels.testChangesList)
|
||||||
.covary[IO]
|
.covary[IO]
|
||||||
.metered(1.second) ++ Stream.never[IO]
|
.metered(1.second)
|
||||||
)
|
)
|
||||||
.map(state => WebSocketFrame.Text(state.asJson.noSpaces))
|
.map(state => WebSocketFrame.Text(state.asJson.noSpaces))
|
||||||
|
|
||||||
|
|
|
@ -20,36 +20,8 @@ def FrontendMain(): Unit =
|
||||||
)
|
)
|
||||||
|
|
||||||
object Main {
|
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._
|
val loggedIn = Var(true)
|
||||||
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
|
|
||||||
|
|
||||||
import scala.scalajs.js.Dynamic.{global => g}
|
import scala.scalajs.js.Dynamic.{global => g}
|
||||||
def appElement(): Element = {
|
def appElement(): Element = {
|
||||||
|
@ -59,9 +31,11 @@ object Main {
|
||||||
className := "h-24 w-full flex flex-for justify-center items-center bg-green-200",
|
className := "h-24 w-full flex flex-for justify-center items-center bg-green-200",
|
||||||
p(className := "text-2xl", "Here be header")
|
p(className := "text-2xl", "Here be header")
|
||||||
),
|
),
|
||||||
child <-- roomStateWSStream.isConnected.map( if (_) emptyNode else JoinRoomComponent.render()),
|
child <-- loggedIn.signal.map( mybool => s"the 'logged in' signal is $mybool" ),
|
||||||
child <-- roomStateWSStream.isConnected.map( if (_) RoomView.renderRoom(stateSignal) else emptyNode),
|
child <-- loggedIn.signal.map(if (_) emptyNode else JoinRoomComponent.render(loggedIn.writer)),
|
||||||
roomStateWSStream.connect
|
child <-- loggedIn.signal.map(
|
||||||
|
if (_) RoomView.renderRoom(loggedIn.writer) else emptyNode
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,28 +48,35 @@ object JoinRoomComponent {
|
||||||
|
|
||||||
val (responsesStream, responseReceived) = EventStream.withCallback[FetchResponse[String]]
|
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(
|
div(
|
||||||
className := "flex flex-col h-full justify-center",
|
className := "flex flex-col h-full justify-center",
|
||||||
"Logging in:",
|
"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",
|
cls := "flex flex-col space-y-4 p-4 max-h-96 overflow-auto bg-gray-900",
|
||||||
children.command <-- responsesStream.recoverToTry.map {
|
children.command <-- responsesStream.recoverToTry.map {
|
||||||
case Success(response) =>
|
case Success(response) =>
|
||||||
CollectionCommand.Append(
|
{
|
||||||
div(
|
CollectionCommand.Append(
|
||||||
div(
|
div(
|
||||||
cls := "flex space-x-2 items-center",
|
div(
|
||||||
code(cls := "text-green-500", "Status: "),
|
cls := "flex space-x-2 items-center",
|
||||||
code(cls := "text-green-300", s"${response.status} ${response.statusText}")
|
code(cls := "text-green-500", "Status: "),
|
||||||
),
|
code(cls := "text-green-300", s"${response.status} ${response.statusText}")
|
||||||
div(
|
),
|
||||||
cls := "text-green-400 text-xs",
|
div(
|
||||||
code(response.data)
|
cls := "text-green-400 text-xs",
|
||||||
|
code(response.data)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
case Failure(exception) =>
|
case Failure(exception) =>
|
||||||
CollectionCommand.Append(
|
CollectionCommand.Append(
|
||||||
div(
|
div(
|
||||||
|
|
|
@ -3,18 +3,41 @@ package industries.sunshine.planningpoker
|
||||||
import scala.scalajs.js
|
import scala.scalajs.js
|
||||||
import com.raquo.laminar.api.L.{*, given}
|
import com.raquo.laminar.api.L.{*, given}
|
||||||
import industries.sunshine.planningpoker.common.Models.*
|
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 {
|
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
|
/** Rendering of the Room state central table with cards, player control UI and other players
|
||||||
def renderRoom(roomStateSignal: Signal[RoomStateView]): Element = {
|
* description
|
||||||
// i want to number other players, for the star arrangement
|
*
|
||||||
|
* @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 =>
|
val otherPlayers = roomStateSignal.map { state =>
|
||||||
state.players.filterNot(_.id == state.me).zipWithIndex
|
state.players.filterNot(_.id == state.me).zipWithIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val wsFinalDeathSignal = wsStream.closed.collect { case (_, false) => () }
|
||||||
|
|
||||||
div(
|
div(
|
||||||
className := "w-full h-full border-4 border-amber-900 flex flex-col",
|
className := "w-full h-full border-4 border-amber-900 flex flex-col",
|
||||||
div(
|
div(
|
||||||
|
@ -23,7 +46,9 @@ object RoomView {
|
||||||
renderPlayer(playerSignal, roomStateSignal.map(_.allowedCards.size))
|
renderPlayer(playerSignal, roomStateSignal.map(_.allowedCards.size))
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
TableView.renderTable(roomStateSignal)
|
TableView.renderTable(roomStateSignal),
|
||||||
|
wsStream.connect,
|
||||||
|
wsFinalDeathSignal.map(_ => false) --> loggedIn
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,8 @@ object TableView {
|
||||||
def renderTable(roundSignal: Signal[RoomStateView]): Element = {
|
def renderTable(roundSignal: Signal[RoomStateView]): Element = {
|
||||||
val playerIdToCardTypeSignal =
|
val playerIdToCardTypeSignal =
|
||||||
roundSignal
|
roundSignal
|
||||||
.combineWith(Main.appStateSignal.map(_.myId))
|
.map(state =>
|
||||||
.map((state, myIdOpt) =>
|
state.players.map(p => p.id -> getPlayerCardType(p.id, state.round, p.name, state.me))
|
||||||
state.players.map(p => p.id -> getPlayerCardType(p.id, state.round, p.name, myIdOpt))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
div(
|
div(
|
||||||
|
@ -36,11 +35,11 @@ object TableView {
|
||||||
id: PlayerID,
|
id: PlayerID,
|
||||||
state: RoundState,
|
state: RoundState,
|
||||||
name: String,
|
name: String,
|
||||||
myId: Option[PlayerID]
|
myId: PlayerID
|
||||||
): CardState = {
|
): CardState = {
|
||||||
state match {
|
state match {
|
||||||
case isOpen: RoundState.Voting =>
|
case isOpen: RoundState.Voting =>
|
||||||
if (myId.forall(_ == id)) {
|
if (myId == id) {
|
||||||
isOpen.myCard.fold(NoCard(name))(vote => Open(vote))
|
isOpen.myCard.fold(NoCard(name))(vote => Open(vote))
|
||||||
} else isOpen.alreadyVoted.find(_.id == id).fold(NoCard(name))(_ => CardBack)
|
} else isOpen.alreadyVoted.find(_.id == id).fold(NoCard(name))(_ => CardBack)
|
||||||
case isClosed: RoundState.Viewing =>
|
case isClosed: RoundState.Viewing =>
|
||||||
|
|
Loading…
Reference in New Issue