Compare commits

..

3 Commits

Author SHA1 Message Date
efim 539b20f419 rooms service methods implementation
removed stubbed room, but thing compiles and maybe will work?
lot's of pain with trying to store Topic inside Ref together with Room.

storing them together in a tuple means for the room there should always
be a topic
but then modifying the room in side of ref is uncomfortable, because i
want to do F[Unit] send updated room together with room modification,
but Ref seems to take in only pure function to update the value

there got to be maybe some semaphors or something like that?
and storing topics where? in a separate state?
that would maybe simplify things, but i'm coding well into the night and
that's not a good idea really

now before testing this with front end, i need a way to create a room.
2023-04-28 02:42:02 +04:00
efim bd38a29b6d backend: move roomService usage to httpService
no longer used in auth module,
ready to connect rest routes to room service
2023-04-28 00:46:25 +04:00
efim ed6d30ec42 models: separate backend room model
not viewable on front end, since doesn't have json codecs, yay!
2023-04-27 22:17:47 +04:00
9 changed files with 198 additions and 144 deletions

View File

@ -25,53 +25,33 @@ trait Auth[F[_]] {
* check that room exists, password is valid call to add user to the players create session * check that room exists, password is valid call to add user to the players create session
* mapping and return cookie * mapping and return cookie
*/ */
def joinRoom( def joinRoom(roomId: RoomID, playerId: PlayerID): F[ResponseCookie]
roomName: String,
roomPassword: String,
nickName: String,
nickPassword: String
): F[Either[String, ResponseCookie]]
def deleteSession( def deleteSession(sessionId: Long): F[Unit]
sessionId: Long
): F[Unit]
} }
object Auth { object Auth {
type SessionsMap = Map[Long, (RoomID, PlayerID)] type SessionsMap = Map[Long, (RoomID, PlayerID)]
class SimpleAuth[F[_]: Sync]( class SimpleAuth[F[_]: Sync](sessions: Ref[F, SessionsMap]) extends Auth[F] {
sessions: Ref[F, SessionsMap],
roomService: RoomService[F]
) extends Auth[F] {
val authcookieName = "authcookie" val authcookieName = "authcookie"
override def joinRoom( override def joinRoom(roomId: RoomID, playerId: PlayerID): F[ResponseCookie] = {
roomName: String,
roomPassword: String,
nickName: String,
nickPassword: String
): F[Either[String, ResponseCookie]] = {
// TODO check for existing session for same room // TODO check for existing session for same room
// and do i want to logout if existing session for another room? ugh // and do i want to logout if existing session for another room? ugh
val roomId = RoomID(roomName)
val result = for {
playerId <- EitherT(
roomService.joinRoom(roomId, nickName, nickPassword, roomPassword)
)
.leftMap(_.toString())
// newSessionId = Random.nextLong() // TODO return after i stop mocking RoomService // newSessionId = Random.nextLong() // TODO return after i stop mocking RoomService
newSessionId = TestModels.testSessionId val newSessionId = TestModels.testSessionId
_ <- EitherT.liftF(sessions.update(_.updated(newSessionId, (roomId, playerId)))) sessions
} yield ResponseCookie( .update(_.updated(newSessionId, (roomId, playerId)))
.as(
ResponseCookie(
name = authcookieName, name = authcookieName,
content = newSessionId.toString(), content = newSessionId.toString(),
secure = true secure = true
) )
)
result.value
} }
override def authUser override def authUser
@ -92,8 +72,8 @@ object Auth {
} }
} }
def make[F[_]: Sync](roomsService: RoomService[F]): F[Auth[F]] = def make[F[_]: Sync](): F[Auth[F]] =
for { for {
sessionsMap <- Ref.of[F, SessionsMap](TestModels.testSessions) sessionsMap <- Ref.of[F, SessionsMap](TestModels.testSessions)
} yield new SimpleAuth(sessionsMap, roomsService) } yield new SimpleAuth(sessionsMap)
} }

View File

@ -14,8 +14,8 @@ object BackendApp extends IOApp {
val wiring = for { val wiring = for {
roomService <- Resource.eval(RoomService.make[IO]) roomService <- Resource.eval(RoomService.make[IO])
auth <- Resource.eval(Auth.make(roomService)) auth <- Resource.eval(Auth.make[IO]())
httpService = MyHttpService.create(auth) httpService = MyHttpService.create(auth, roomService)
server <- EmberServerBuilder server <- EmberServerBuilder
.default[IO] .default[IO]
.withHost(host) .withHost(host)
@ -25,9 +25,7 @@ object BackendApp extends IOApp {
} yield server } yield server
wiring.use(server => wiring.use(server =>
IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never.as(ExitCode.Success))
.as(ExitCode.Success)
)
} }
} }

View File

@ -4,7 +4,6 @@ import cats.effect._
import cats.syntax.all._ import cats.syntax.all._
import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._ import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._
import org.http4s.websocket.WebSocketFrame import org.http4s.websocket.WebSocketFrame
// import io.circe.generic.auto._
import io.circe.syntax._ import io.circe.syntax._
import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityDecoder._
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -16,8 +15,8 @@ import org.http4s.server.AuthMiddleware.apply
import org.http4s.server.AuthMiddleware import org.http4s.server.AuthMiddleware
object MyHttpService { object MyHttpService {
def create(auth: Auth[IO])( def create(auth: Auth[IO], roomService: RoomService[IO])(
wsb: WebSocketBuilder[cats.effect.IO] wsb: WebSocketBuilder[IO]
): HttpApp[cats.effect.IO] = { ): HttpApp[cats.effect.IO] = {
val authedRoomRoutes: AuthedRoutes[(PlayerID, RoomID), IO] = val authedRoomRoutes: AuthedRoutes[(PlayerID, RoomID), IO] =
@ -56,26 +55,23 @@ object MyHttpService {
val authMiddleware = AuthMiddleware(auth.authUser) val authMiddleware = AuthMiddleware(auth.authUser)
val aa = authMiddleware(authedRoomRoutes) val aa = authMiddleware(authedRoomRoutes)
import cats.data.EitherT
val authenticationRoute = HttpRoutes val authenticationRoute = HttpRoutes
.of[IO] { .of[IO] {
case req @ POST -> Root / "login" => { case req @ POST -> Root / "login" => {
for { val responseOrError = for {
data <- req.as[Requests.LogIn] data <- EitherT.right(req.as[Requests.LogIn])
authResult <- auth.joinRoom( Requests.LogIn(roomName, nickName, roomPassword, nickPassword) = data
data.roomName, roomId = RoomID(roomName)
data.password, playerId <- EitherT(roomService.joinRoom(roomId, nickName, nickPassword, roomPassword))
data.nickname, authCookie <- EitherT.liftF(auth.joinRoom(roomId, playerId))
data.nickPassword _ <- EitherT.liftF(IO(println(s"> logging in $nickName to $roomName")))
) resp <- EitherT.liftF(Ok().flatTap(resp => IO(resp.addCookie(authCookie))))
resp <- authResult match {
case Left(error) =>
Forbidden(error)
case Right(authCookie) => {
IO(println(s"> logging in ${data.nickname} to ${data.roomName}")) >>
Ok().map(_.addCookie(authCookie))
}
}
} yield resp } yield resp
val response = responseOrError.leftSemiflatMap(error => Forbidden(error.toString())).merge
response
} }
} }

View File

@ -1,8 +1,10 @@
package industries.sunshine.planningpoker package industries.sunshine.planningpoker
import industries.sunshine.planningpoker.common.Models.* import industries.sunshine.planningpoker.common.Models.*
import cats.effect.{Ref, Sync} import cats.effect.{Ref, Concurrent}
import cats.syntax.all._ import cats.syntax.all._
import fs2.Stream
import fs2.concurrent.Topic
import cats.data.EitherT import cats.data.EitherT
enum RoomError { enum RoomError {
@ -23,38 +25,69 @@ trait RoomService[F[_]] {
): F[Either[RoomError, PlayerID]] ): F[Either[RoomError, PlayerID]]
def deleteRoom(roomID: RoomID): F[Unit] def deleteRoom(roomID: RoomID): F[Unit]
def getRoom(roomID: RoomID): F[Option[Room]] def getRoom(roomID: RoomID): F[Option[Room]]
def subscribe(roomID: RoomID): Stream[F, Room]
} }
class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) extends RoomService[F] { class InMemoryRoomService[F[_]: Concurrent](stateRef: Ref[F, Map[RoomID, (Room, Topic[F, Room])]])
extends RoomService[F] {
override def createRoom(newRoom: Room): F[Either[RoomError, Room]] = { override def createRoom(newRoom: Room): F[Either[RoomError, Room]] = {
stateRef.modify { rooms => for {
updatesTopic <- Topic[F, Room]
room <- stateRef.modify { rooms =>
rooms.get(newRoom.id) match { rooms.get(newRoom.id) match {
case Some(_) => case Some(_) =>
rooms -> RoomError.RoomAlreadyExists(newRoom.id.name).asLeft[Room] rooms -> RoomError.RoomAlreadyExists(newRoom.id.name).asLeft[Room]
case None => case None =>
(rooms.updated(newRoom.id, newRoom)) -> newRoom.asRight[RoomError] (rooms.updated(newRoom.id, (newRoom, updatesTopic))) -> newRoom.asRight[RoomError]
} }
} }
} yield room
} }
override def updateRoom(room: Room): F[Unit] = stateRef.update { state => override def updateRoom(room: Room): F[Unit] = {
state.get(room.id).fold(state)(oldRoom => state.updated(room.id, room)) for {
// modify is function to update state and compute auxillary value to return, here - topic
topic <- stateRef.modify[Topic[F, Room]] { state =>
state.get(room.id) match {
case Some((oldRoom, topic)) => state.updated(room.id, (room, topic)) -> topic
case None =>
throw new IllegalStateException(s"updateRoom with ${room.id} on nonexistent room")
}
}
_ <- topic.publish1(room) // update and publish are not atomic, sadly races can happen
} yield ()
} }
override def deleteRoom(roomID: RoomID): F[Unit] = stateRef.update(_.removed(roomID)) override def deleteRoom(roomID: RoomID): F[Unit] = {
for {
topic <- stateRef.modify[Topic[F, Room]](state =>
state.get(roomID) match {
case Some((oldRoom, topic)) => state.removed(roomID) -> topic
case None =>
throw new IllegalStateException(s"call to delete with $roomID on nonexistent room")
// TODO - i'd prefer to swallow these errors
}
)
_ <- topic.close
} yield ()
}
override def getRoom(roomID: RoomID): F[Option[Room]] = stateRef.get.map(_.get(roomID)) override def getRoom(roomID: RoomID): F[Option[Room]] = {
stateRef.get.map(_.get(roomID).map(_._1))
}
override def joinRoom( override def joinRoom(
id: RoomID, id: RoomID,
nickName: String, nickName: String,
nickPassword: String, nickPassword: String,
roomPassword: String roomPassword: String
): F[Either[RoomError, PlayerID]] = stateRef.modify { rooms => ): F[Either[RoomError, PlayerID]] = {
// need to cover cases:
// - player already present, then return as is, i guess
// - nick not known - add new player and new nick-password mapping
// - nick known - add new player
/** pure function that adds the player to the room need to cover cases:
* - player already present, then return as is, i guess
* - nick not known - add new player and new nick-password mapping
* - nick known - add new player
*/
def addPlayer(room: Room): (PlayerID, Room) = { def addPlayer(room: Room): (PlayerID, Room) = {
room.players.find(_.name == nickName) match { room.players.find(_.name == nickName) match {
case Some(player) => player.id -> room case Some(player) => player.id -> room
@ -71,8 +104,19 @@ class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) exten
} }
} }
val joiningWithChecks = for { /** to be executed under Ref.modify (i.e with state acquired) checks of whether player can be
room <- rooms.get(id).toRight(RoomError.RoomMissing(id.name)) * added to the room:
* - room password is correct
* - nickname is either not taken, or correct password was provided
* @returns
* playerId (new or existing), updatedRoom (to be put into state), topic (to send the udpdate
* notification)
*/
def getWithChecks(
rooms: Map[RoomID, (Room, Topic[F, Room])]
): Either[RoomError, (PlayerID, Room, Topic[F, Room])] = for {
roomAndTopic <- rooms.get(id).toRight(RoomError.RoomMissing(id.name))
(room, topic) = roomAndTopic
_ <- Either.cond(room.password == roomPassword, (), RoomError.RoomPassIncorrect) _ <- Either.cond(room.password == roomPassword, (), RoomError.RoomPassIncorrect)
isNickPassCorrect = room.playersPasswords isNickPassCorrect = room.playersPasswords
.get(nickName) .get(nickName)
@ -83,14 +127,54 @@ class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) exten
RoomError.NickPassIncorrect RoomError.NickPassIncorrect
) )
(playerId, updatedRoom) = addPlayer(room) (playerId, updatedRoom) = addPlayer(room)
} yield playerId } yield (playerId, updatedRoom, topic)
rooms -> joiningWithChecks // modify returns tuple (updatedState, valueToReturn)
// this particular update either keeps room as is, or adds the player
// and returns playerId and topic to be used outside
//
// NOTE here i have a lot of handwaving to pass topic outside
// because it's not possible to send F[Unit] update
// inside of the stateRef update (which works with pure functions?),
// so room notification change has to be returned to outside
val maybeUpdatedStateAndNotification = stateRef.modify { rooms =>
val maybeAddedUser = getWithChecks(rooms)
val updatedState =
maybeAddedUser.fold(
_ => rooms,
{ case (playerId, updatedRoom, topic) => rooms.updated(id, (updatedRoom, topic)) }
)
val toReturn = maybeAddedUser.map { case (id, updatedRoom, topic) =>
(id, topic.publish1(updatedRoom).void)
} }
updatedState -> toReturn
}
// now combining the effects : getting (updatedState & notificationEffect) or error
// executing notification
// returning only playerId
val result = for {
updatedState <- EitherT(maybeUpdatedStateAndNotification)
(playerId, notification) = updatedState
_ <- EitherT.liftF(notification)
} yield (playerId)
result.value
}
override def subscribe(roomID: RoomID): Stream[F, Room] =
Stream
.eval(stateRef.get)
.flatMap(rooms =>
rooms.get(roomID) match {
case Some((room, topic)) => topic.subscribe(10)
case None => Stream.empty
}
)
} }
object RoomService { object RoomService {
def make[F[_]: Sync]: F[RoomService[F]] = { def make[F[_]: Concurrent]: F[RoomService[F]] = {
Ref.of[F, Map[RoomID, Room]](TestModels.testRooms).map(new InMemoryRoomService[F](_)) Ref.of[F, Map[RoomID, (Room, Topic[F, Room])]](Map.empty).map(new InMemoryRoomService[F](_))
} }
} }

View File

@ -20,30 +20,27 @@ object Models {
players: List[Player], players: List[Player],
me: PlayerID, me: PlayerID,
allowedCards: List[String], allowedCards: List[String],
round: RoundState, round: RoundStateView,
canCloseRound: Boolean = false canCloseRound: Boolean = false
) derives Codec.AsObject { ) derives Codec.AsObject
def playersCount: Int = players.size
}
object RoomStateView { object RoomStateView {
val empty = RoomStateView( val empty = RoomStateView(
List.empty, List.empty,
PlayerID(0), PlayerID(0),
List.empty, List.empty,
RoundState.Voting(None, List.empty), RoundStateView.Voting(None, List.empty),
false false
) )
} }
enum RoundState derives Codec.AsObject: enum RoundStateView derives Codec.AsObject:
/** view state for round before votes are open player can know their vote and who of the other /** view state for round before votes are open player can know their vote and who of the other
* players have voted * players have voted
*/ */
case Voting( case Voting(
myCard: Option[String], myCard: Option[String],
alreadyVoted: List[Player] alreadyVoted: List[PlayerID]
) )
/** view state for round after opening the votes /** view state for round after opening the votes
@ -51,15 +48,14 @@ object Models {
case Viewing( case Viewing(
votes: List[(PlayerID, String)] votes: List[(PlayerID, String)]
) )
final case class PlayerID(id: Long) derives Codec.AsObject
final case class PlayerID(id: Long) derives Codec.AsObject
final case class Player(name: String, id: PlayerID) derives Codec.AsObject final case class Player(name: String, id: PlayerID) derives Codec.AsObject
object Player { object Player {
def create(name: String) = Player(name, PlayerID(Random.nextLong)) def create(name: String) = Player(name, PlayerID(Random.nextLong))
} }
final case class RoomID(name: String) derives Codec.AsObject final case class RoomID(name: String) derives Codec.AsObject
final case class Room( final case class Room(
id: RoomID, id: RoomID,
players: List[Player], players: List[Player],
@ -67,9 +63,9 @@ object Models {
password: String, password: String,
allowedCards: List[String], allowedCards: List[String],
round: RoundState, round: RoundState,
playersPasswords: Map[String, String] = Map.empty // nickname into password playersPasswords: Map[String, String] = Map.empty, // nickname into password
) { ) {
def toViewFor(playerId: PlayerID): RoomStateView = { def getViewFor(playerId: PlayerID): RoomStateView = {
players players
.find(_.id == playerId) .find(_.id == playerId)
.fold(ifEmpty = RoomStateView.empty)((me: Player) => .fold(ifEmpty = RoomStateView.empty)((me: Player) =>
@ -77,27 +73,28 @@ object Models {
players, players,
me.id, me.id,
allowedCards, allowedCards,
round, round.toViewFor(playerId),
playerId == owner playerId == owner
) )
) )
} }
} }
object Room {
val testRoom = Room( // no need to send to the front end, no deed to derive codec, cool
id = RoomID("testroom"), sealed trait RoundState {
players = List( def toViewFor(player: PlayerID): RoundStateView
Player("me", PlayerID(1L)), }
Player("horsey", PlayerID(444L)), object RoundState {
Player("froggy", PlayerID(555L)), final case class Voting(votes: Map[PlayerID, String]) extends RoundState {
Player("owley", PlayerID(777L)) def toViewFor(playerId: PlayerID): RoundStateView.Voting = RoundStateView.Voting(
), myCard = votes.get(playerId),
owner = PlayerID(1L), alreadyVoted = votes.filterKeys(id => id != playerId).keys.toList
password = "password",
allowedCards = List("S", "M", "L"),
// TODO - this needs to be a different hting
round = RoundState.Voting(None, List.empty)
) )
} }
final case class Viewing(votes: Map[PlayerID, String]) extends RoundState {
def toViewFor(player: PlayerID): RoundStateView.Viewing =
RoundStateView.Viewing(votes.toList)
}
}
} }

View File

@ -15,7 +15,7 @@ object TestModels {
password = "password", password = "password",
allowedCards = List("xs", "s", "m", "l", "xl"), allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Viewing( round = RoundState.Viewing(
List(me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m") Map(me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
), ),
playersPasswords = Map("me" -> "nickpassword") // nickname into password playersPasswords = Map("me" -> "nickpassword") // nickname into password
) )
@ -29,70 +29,71 @@ object TestModels {
players = List(me), players = List(me),
me = me.id, me = me.id,
allowedCards = List("xs", "s", "m", "l", "xl"), allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Voting(myCard = None, alreadyVoted = List.empty), round = RoundStateView.Voting(myCard = None, alreadyVoted = List.empty),
canCloseRound = true canCloseRound = true
), ),
RoomStateView( RoomStateView(
players = List(me, pony), players = List(me, pony),
me = me.id, me = me.id,
allowedCards = List("xs", "s", "m", "l", "xl"), allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Voting(myCard = None, alreadyVoted = List.empty), round = RoundStateView.Voting(myCard = None, alreadyVoted = List.empty),
canCloseRound = true canCloseRound = true
), ),
RoomStateView( RoomStateView(
players = List(me, birdy, pony), players = List(me, birdy, pony),
me = me.id, me = me.id,
allowedCards = List("xs", "s", "m", "l", "xl"), allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Voting(myCard = None, alreadyVoted = List.empty), round = RoundStateView.Voting(myCard = None, alreadyVoted = List.empty),
canCloseRound = true canCloseRound = true
), ),
RoomStateView( RoomStateView(
players = List(me, birdy, pony), players = List(me, birdy, pony),
me = me.id, me = me.id,
allowedCards = List("xs", "s", "m", "l", "xl"), allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Voting(myCard = None, alreadyVoted = List(birdy)), round = RoundStateView.Voting(myCard = None, alreadyVoted = List(birdy.id)),
canCloseRound = true canCloseRound = true
), ),
RoomStateView( RoomStateView(
players = List(me, birdy, pony), players = List(me, birdy, pony),
me = me.id, me = me.id,
allowedCards = List("xs", "s", "m", "l", "xl"), allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy)), round = RoundStateView.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id)),
canCloseRound = true canCloseRound = true
), ),
RoomStateView( RoomStateView(
players = List(me, birdy, pony), players = List(me, birdy, pony),
me = me.id, me = me.id,
allowedCards = List("xs", "s", "m", "l", "xl"), allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy)), round = RoundStateView.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id)),
canCloseRound = true canCloseRound = true
), ),
RoomStateView( RoomStateView(
players = List(me, birdy, pony, horsey), players = List(me, birdy, pony, horsey),
me = me.id, me = me.id,
allowedCards = List("xs", "s", "m", "l", "xl"), allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy)), round = RoundStateView.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id)),
canCloseRound = true canCloseRound = true
), ),
RoomStateView( RoomStateView(
players = List(me, birdy, pony, horsey), players = List(me, birdy, pony, horsey),
me = me.id, me = me.id,
allowedCards = List("xs", "s", "m", "l", "xl"), allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy, horsey)), round = RoundStateView.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id, horsey.id)),
canCloseRound = true canCloseRound = true
), ),
RoomStateView( RoomStateView(
players = List(me, birdy, pony, horsey), players = List(me, birdy, pony, horsey),
me = me.id, me = me.id,
allowedCards = List("xs", "s", "m", "l", "xl"), allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy, horsey, pony)), round = RoundStateView
.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id, horsey.id, pony.id)),
canCloseRound = true canCloseRound = true
), ),
RoomStateView( RoomStateView(
players = List(me, birdy, pony, horsey), players = List(me, birdy, pony, horsey),
me = me.id, me = me.id,
allowedCards = List("xs", "s", "m", "l", "xl"), allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Viewing( round = RoundStateView.Viewing(
List(me.id -> "m", pony.id -> "l", birdy.id -> "s", horsey.id -> "m") List(me.id -> "m", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
), ),
canCloseRound = true canCloseRound = true

View File

@ -32,9 +32,9 @@ object OwnHandControls {
private def myUnselectedCards(state: RoomStateView): List[String] = { private def myUnselectedCards(state: RoomStateView): List[String] = {
state.round match { state.round match {
case RoundState.Voting(myCard, _) => case RoundStateView.Voting(myCard, _) =>
state.allowedCards.filterNot(value => myCard.contains(value)) state.allowedCards.filterNot(value => myCard.contains(value))
case RoundState.Viewing(votes) => case RoundStateView.Viewing(votes) =>
state.allowedCards.filterNot(value => votes.toMap.get(state.me).contains(value)) state.allowedCards.filterNot(value => votes.toMap.get(state.me).contains(value))
} }
} }

View File

@ -2,21 +2,19 @@ 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.RoundState import industries.sunshine.planningpoker.common.Models.RoundStateView
import com.raquo.airstream.core.Signal import com.raquo.airstream.core.Signal
import industries.sunshine.planningpoker.common.Models.RoundState
import industries.sunshine.planningpoker.common.Models.RoundState
import io.laminext.fetch.Fetch import io.laminext.fetch.Fetch
import scala.scalajs.js.Dynamic.{global => g} import scala.scalajs.js.Dynamic.{global => g}
import concurrent.ExecutionContext.Implicits.global import concurrent.ExecutionContext.Implicits.global
object TableControls { object TableControls {
def render(roundSignal: Signal[RoundState]): Element = { def render(roundSignal: Signal[RoundStateView]): Element = {
div( div(
child <-- roundSignal.map { child <-- roundSignal.map {
case RoundState.Viewing(_) => newPollButton() case RoundStateView.Viewing(_) => newPollButton()
case RoundState.Voting(_, _) => endPollButton() case RoundStateView.Voting(_, _) => endPollButton()
} }
) )
} }

View File

@ -39,16 +39,16 @@ object TableView {
def getPlayerCardType( def getPlayerCardType(
id: PlayerID, id: PlayerID,
state: RoundState, state: RoundStateView,
name: String, name: String,
myId: PlayerID myId: PlayerID
): CardState = { ): CardState = {
state match { state match {
case isOpen: RoundState.Voting => case isOpen: RoundStateView.Voting =>
if (myId == 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).fold(NoCard(name))(_ => CardBack)
case isClosed: RoundState.Viewing => case isClosed: RoundStateView.Viewing =>
isClosed.votes isClosed.votes
.find(_._1 == id) .find(_._1 == id)
.fold { .fold {