Compare commits
3 Commits
da9b96de84
...
539b20f419
Author | SHA1 | Date |
---|---|---|
|
539b20f419 | |
|
bd38a29b6d | |
|
ed6d30ec42 |
|
@ -25,53 +25,33 @@ trait Auth[F[_]] {
|
|||
* check that room exists, password is valid call to add user to the players create session
|
||||
* mapping and return cookie
|
||||
*/
|
||||
def joinRoom(
|
||||
roomName: String,
|
||||
roomPassword: String,
|
||||
nickName: String,
|
||||
nickPassword: String
|
||||
): F[Either[String, ResponseCookie]]
|
||||
def joinRoom(roomId: RoomID, playerId: PlayerID): F[ResponseCookie]
|
||||
|
||||
def deleteSession(
|
||||
sessionId: Long
|
||||
): F[Unit]
|
||||
def deleteSession(sessionId: Long): F[Unit]
|
||||
|
||||
}
|
||||
|
||||
object Auth {
|
||||
type SessionsMap = Map[Long, (RoomID, PlayerID)]
|
||||
|
||||
class SimpleAuth[F[_]: Sync](
|
||||
sessions: Ref[F, SessionsMap],
|
||||
roomService: RoomService[F]
|
||||
) extends Auth[F] {
|
||||
class SimpleAuth[F[_]: Sync](sessions: Ref[F, SessionsMap]) extends Auth[F] {
|
||||
|
||||
val authcookieName = "authcookie"
|
||||
|
||||
override def joinRoom(
|
||||
roomName: String,
|
||||
roomPassword: String,
|
||||
nickName: String,
|
||||
nickPassword: String
|
||||
): F[Either[String, ResponseCookie]] = {
|
||||
override def joinRoom(roomId: RoomID, playerId: PlayerID): F[ResponseCookie] = {
|
||||
// TODO check for existing session for same room
|
||||
// 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)
|
||||
// newSessionId = Random.nextLong() // TODO return after i stop mocking RoomService
|
||||
val newSessionId = TestModels.testSessionId
|
||||
sessions
|
||||
.update(_.updated(newSessionId, (roomId, playerId)))
|
||||
.as(
|
||||
ResponseCookie(
|
||||
name = authcookieName,
|
||||
content = newSessionId.toString(),
|
||||
secure = true
|
||||
)
|
||||
)
|
||||
.leftMap(_.toString())
|
||||
// newSessionId = Random.nextLong() // TODO return after i stop mocking RoomService
|
||||
newSessionId = TestModels.testSessionId
|
||||
_ <- EitherT.liftF(sessions.update(_.updated(newSessionId, (roomId, playerId))))
|
||||
} yield ResponseCookie(
|
||||
name = authcookieName,
|
||||
content = newSessionId.toString(),
|
||||
secure = true
|
||||
)
|
||||
|
||||
result.value
|
||||
}
|
||||
|
||||
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 {
|
||||
sessionsMap <- Ref.of[F, SessionsMap](TestModels.testSessions)
|
||||
} yield new SimpleAuth(sessionsMap, roomsService)
|
||||
} yield new SimpleAuth(sessionsMap)
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ object BackendApp extends IOApp {
|
|||
|
||||
val wiring = for {
|
||||
roomService <- Resource.eval(RoomService.make[IO])
|
||||
auth <- Resource.eval(Auth.make(roomService))
|
||||
httpService = MyHttpService.create(auth)
|
||||
auth <- Resource.eval(Auth.make[IO]())
|
||||
httpService = MyHttpService.create(auth, roomService)
|
||||
server <- EmberServerBuilder
|
||||
.default[IO]
|
||||
.withHost(host)
|
||||
|
@ -25,9 +25,7 @@ object BackendApp extends IOApp {
|
|||
} yield server
|
||||
|
||||
wiring.use(server =>
|
||||
IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never
|
||||
.as(ExitCode.Success)
|
||||
)
|
||||
IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never.as(ExitCode.Success))
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import cats.effect._
|
|||
import cats.syntax.all._
|
||||
import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._
|
||||
import org.http4s.websocket.WebSocketFrame
|
||||
// import io.circe.generic.auto._
|
||||
import io.circe.syntax._
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import scala.concurrent.duration._
|
||||
|
@ -16,8 +15,8 @@ import org.http4s.server.AuthMiddleware.apply
|
|||
import org.http4s.server.AuthMiddleware
|
||||
|
||||
object MyHttpService {
|
||||
def create(auth: Auth[IO])(
|
||||
wsb: WebSocketBuilder[cats.effect.IO]
|
||||
def create(auth: Auth[IO], roomService: RoomService[IO])(
|
||||
wsb: WebSocketBuilder[IO]
|
||||
): HttpApp[cats.effect.IO] = {
|
||||
|
||||
val authedRoomRoutes: AuthedRoutes[(PlayerID, RoomID), IO] =
|
||||
|
@ -56,26 +55,23 @@ object MyHttpService {
|
|||
val authMiddleware = AuthMiddleware(auth.authUser)
|
||||
val aa = authMiddleware(authedRoomRoutes)
|
||||
|
||||
import cats.data.EitherT
|
||||
val authenticationRoute = HttpRoutes
|
||||
.of[IO] {
|
||||
case req @ POST -> Root / "login" => {
|
||||
for {
|
||||
data <- req.as[Requests.LogIn]
|
||||
authResult <- auth.joinRoom(
|
||||
data.roomName,
|
||||
data.password,
|
||||
data.nickname,
|
||||
data.nickPassword
|
||||
)
|
||||
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))
|
||||
}
|
||||
}
|
||||
val responseOrError = for {
|
||||
data <- EitherT.right(req.as[Requests.LogIn])
|
||||
Requests.LogIn(roomName, nickName, roomPassword, nickPassword) = data
|
||||
roomId = RoomID(roomName)
|
||||
playerId <- EitherT(roomService.joinRoom(roomId, nickName, nickPassword, roomPassword))
|
||||
authCookie <- EitherT.liftF(auth.joinRoom(roomId, playerId))
|
||||
_ <- EitherT.liftF(IO(println(s"> logging in $nickName to $roomName")))
|
||||
resp <- EitherT.liftF(Ok().flatTap(resp => IO(resp.addCookie(authCookie))))
|
||||
} yield resp
|
||||
|
||||
val response = responseOrError.leftSemiflatMap(error => Forbidden(error.toString())).merge
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package industries.sunshine.planningpoker
|
||||
|
||||
import industries.sunshine.planningpoker.common.Models.*
|
||||
import cats.effect.{Ref, Sync}
|
||||
import cats.effect.{Ref, Concurrent}
|
||||
import cats.syntax.all._
|
||||
import fs2.Stream
|
||||
import fs2.concurrent.Topic
|
||||
import cats.data.EitherT
|
||||
|
||||
enum RoomError {
|
||||
|
@ -23,38 +25,69 @@ trait RoomService[F[_]] {
|
|||
): F[Either[RoomError, PlayerID]]
|
||||
def deleteRoom(roomID: RoomID): F[Unit]
|
||||
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]] = {
|
||||
stateRef.modify { rooms =>
|
||||
rooms.get(newRoom.id) match {
|
||||
case Some(_) =>
|
||||
rooms -> RoomError.RoomAlreadyExists(newRoom.id.name).asLeft[Room]
|
||||
case None =>
|
||||
(rooms.updated(newRoom.id, newRoom)) -> newRoom.asRight[RoomError]
|
||||
for {
|
||||
updatesTopic <- Topic[F, Room]
|
||||
room <- stateRef.modify { rooms =>
|
||||
rooms.get(newRoom.id) match {
|
||||
case Some(_) =>
|
||||
rooms -> RoomError.RoomAlreadyExists(newRoom.id.name).asLeft[Room]
|
||||
case None =>
|
||||
(rooms.updated(newRoom.id, (newRoom, updatesTopic))) -> newRoom.asRight[RoomError]
|
||||
}
|
||||
}
|
||||
}
|
||||
} yield room
|
||||
}
|
||||
override def updateRoom(room: Room): F[Unit] = stateRef.update { state =>
|
||||
state.get(room.id).fold(state)(oldRoom => state.updated(room.id, room))
|
||||
override def updateRoom(room: Room): F[Unit] = {
|
||||
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(
|
||||
id: RoomID,
|
||||
nickName: String,
|
||||
nickPassword: String,
|
||||
roomPassword: String
|
||||
): F[Either[RoomError, PlayerID]] = stateRef.modify { rooms =>
|
||||
// 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
|
||||
): F[Either[RoomError, PlayerID]] = {
|
||||
|
||||
/** 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) = {
|
||||
room.players.find(_.name == nickName) match {
|
||||
case Some(player) => player.id -> room
|
||||
|
@ -71,8 +104,19 @@ class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) exten
|
|||
}
|
||||
}
|
||||
|
||||
val joiningWithChecks = for {
|
||||
room <- rooms.get(id).toRight(RoomError.RoomMissing(id.name))
|
||||
/** to be executed under Ref.modify (i.e with state acquired) checks of whether player can be
|
||||
* 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)
|
||||
isNickPassCorrect = room.playersPasswords
|
||||
.get(nickName)
|
||||
|
@ -83,14 +127,54 @@ class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) exten
|
|||
RoomError.NickPassIncorrect
|
||||
)
|
||||
(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 {
|
||||
def make[F[_]: Sync]: F[RoomService[F]] = {
|
||||
Ref.of[F, Map[RoomID, Room]](TestModels.testRooms).map(new InMemoryRoomService[F](_))
|
||||
def make[F[_]: Concurrent]: F[RoomService[F]] = {
|
||||
Ref.of[F, Map[RoomID, (Room, Topic[F, Room])]](Map.empty).map(new InMemoryRoomService[F](_))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,30 +20,27 @@ object Models {
|
|||
players: List[Player],
|
||||
me: PlayerID,
|
||||
allowedCards: List[String],
|
||||
round: RoundState,
|
||||
round: RoundStateView,
|
||||
canCloseRound: Boolean = false
|
||||
) derives Codec.AsObject {
|
||||
def playersCount: Int = players.size
|
||||
}
|
||||
) derives Codec.AsObject
|
||||
|
||||
object RoomStateView {
|
||||
val empty = RoomStateView(
|
||||
List.empty,
|
||||
PlayerID(0),
|
||||
List.empty,
|
||||
RoundState.Voting(None, List.empty),
|
||||
RoundStateView.Voting(None, List.empty),
|
||||
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
|
||||
* players have voted
|
||||
*/
|
||||
case Voting(
|
||||
myCard: Option[String],
|
||||
alreadyVoted: List[Player]
|
||||
alreadyVoted: List[PlayerID]
|
||||
)
|
||||
|
||||
/** view state for round after opening the votes
|
||||
|
@ -51,15 +48,14 @@ object Models {
|
|||
case Viewing(
|
||||
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
|
||||
object Player {
|
||||
def create(name: String) = Player(name, PlayerID(Random.nextLong))
|
||||
}
|
||||
|
||||
final case class RoomID(name: String) derives Codec.AsObject
|
||||
|
||||
final case class Room(
|
||||
id: RoomID,
|
||||
players: List[Player],
|
||||
|
@ -67,9 +63,9 @@ object Models {
|
|||
password: String,
|
||||
allowedCards: List[String],
|
||||
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
|
||||
.find(_.id == playerId)
|
||||
.fold(ifEmpty = RoomStateView.empty)((me: Player) =>
|
||||
|
@ -77,27 +73,28 @@ object Models {
|
|||
players,
|
||||
me.id,
|
||||
allowedCards,
|
||||
round,
|
||||
round.toViewFor(playerId),
|
||||
playerId == owner
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
object Room {
|
||||
val testRoom = Room(
|
||||
id = RoomID("testroom"),
|
||||
players = List(
|
||||
Player("me", PlayerID(1L)),
|
||||
Player("horsey", PlayerID(444L)),
|
||||
Player("froggy", PlayerID(555L)),
|
||||
Player("owley", PlayerID(777L))
|
||||
),
|
||||
owner = PlayerID(1L),
|
||||
password = "password",
|
||||
allowedCards = List("S", "M", "L"),
|
||||
// TODO - this needs to be a different hting
|
||||
round = RoundState.Voting(None, List.empty)
|
||||
)
|
||||
}
|
||||
|
||||
// no need to send to the front end, no deed to derive codec, cool
|
||||
sealed trait RoundState {
|
||||
def toViewFor(player: PlayerID): RoundStateView
|
||||
}
|
||||
object RoundState {
|
||||
final case class Voting(votes: Map[PlayerID, String]) extends RoundState {
|
||||
def toViewFor(playerId: PlayerID): RoundStateView.Voting = RoundStateView.Voting(
|
||||
myCard = votes.get(playerId),
|
||||
alreadyVoted = votes.filterKeys(id => id != playerId).keys.toList
|
||||
)
|
||||
}
|
||||
|
||||
final case class Viewing(votes: Map[PlayerID, String]) extends RoundState {
|
||||
def toViewFor(player: PlayerID): RoundStateView.Viewing =
|
||||
RoundStateView.Viewing(votes.toList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,15 +9,15 @@ object TestModels {
|
|||
val horsey = Player("horsey", PlayerID(12))
|
||||
|
||||
val testRoomBackend = Room(
|
||||
id = RoomID("testroom"),
|
||||
players = List(me, birdy, pony, horsey),
|
||||
owner = me.id,
|
||||
password = "password",
|
||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||
round = RoundState.Viewing(
|
||||
List(me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
|
||||
),
|
||||
playersPasswords = Map("me" -> "nickpassword") // nickname into password
|
||||
id = RoomID("testroom"),
|
||||
players = List(me, birdy, pony, horsey),
|
||||
owner = me.id,
|
||||
password = "password",
|
||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||
round = RoundState.Viewing(
|
||||
Map(me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
|
||||
),
|
||||
playersPasswords = Map("me" -> "nickpassword") // nickname into password
|
||||
)
|
||||
|
||||
val testSessionId = 1L
|
||||
|
@ -29,70 +29,71 @@ object TestModels {
|
|||
players = List(me),
|
||||
me = me.id,
|
||||
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
|
||||
),
|
||||
RoomStateView(
|
||||
players = List(me, pony),
|
||||
me = me.id,
|
||||
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
|
||||
),
|
||||
RoomStateView(
|
||||
players = List(me, birdy, pony),
|
||||
me = me.id,
|
||||
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
|
||||
),
|
||||
RoomStateView(
|
||||
players = List(me, birdy, pony),
|
||||
me = me.id,
|
||||
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
|
||||
),
|
||||
RoomStateView(
|
||||
players = List(me, birdy, pony),
|
||||
me = me.id,
|
||||
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
|
||||
),
|
||||
RoomStateView(
|
||||
players = List(me, birdy, pony),
|
||||
me = me.id,
|
||||
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
|
||||
),
|
||||
RoomStateView(
|
||||
players = List(me, birdy, pony, horsey),
|
||||
me = me.id,
|
||||
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
|
||||
),
|
||||
RoomStateView(
|
||||
players = List(me, birdy, pony, horsey),
|
||||
me = me.id,
|
||||
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
|
||||
),
|
||||
RoomStateView(
|
||||
players = List(me, birdy, pony, horsey),
|
||||
me = me.id,
|
||||
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
|
||||
),
|
||||
RoomStateView(
|
||||
players = List(me, birdy, pony, horsey),
|
||||
me = me.id,
|
||||
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")
|
||||
),
|
||||
canCloseRound = true
|
||||
|
|
|
@ -32,9 +32,9 @@ object OwnHandControls {
|
|||
|
||||
private def myUnselectedCards(state: RoomStateView): List[String] = {
|
||||
state.round match {
|
||||
case RoundState.Voting(myCard, _) =>
|
||||
case RoundStateView.Voting(myCard, _) =>
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,21 +2,19 @@ package industries.sunshine.planningpoker
|
|||
|
||||
import scala.scalajs.js
|
||||
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 industries.sunshine.planningpoker.common.Models.RoundState
|
||||
import industries.sunshine.planningpoker.common.Models.RoundState
|
||||
import io.laminext.fetch.Fetch
|
||||
import scala.scalajs.js.Dynamic.{global => g}
|
||||
|
||||
import concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
object TableControls {
|
||||
def render(roundSignal: Signal[RoundState]): Element = {
|
||||
def render(roundSignal: Signal[RoundStateView]): Element = {
|
||||
div(
|
||||
child <-- roundSignal.map {
|
||||
case RoundState.Viewing(_) => newPollButton()
|
||||
case RoundState.Voting(_, _) => endPollButton()
|
||||
case RoundStateView.Viewing(_) => newPollButton()
|
||||
case RoundStateView.Voting(_, _) => endPollButton()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -39,16 +39,16 @@ object TableView {
|
|||
|
||||
def getPlayerCardType(
|
||||
id: PlayerID,
|
||||
state: RoundState,
|
||||
state: RoundStateView,
|
||||
name: String,
|
||||
myId: PlayerID
|
||||
): CardState = {
|
||||
state match {
|
||||
case isOpen: RoundState.Voting =>
|
||||
case isOpen: RoundStateView.Voting =>
|
||||
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 =>
|
||||
} else isOpen.alreadyVoted.find(_ == id).fold(NoCard(name))(_ => CardBack)
|
||||
case isClosed: RoundStateView.Viewing =>
|
||||
isClosed.votes
|
||||
.find(_._1 == id)
|
||||
.fold {
|
||||
|
|
Loading…
Reference in New Issue