denormalize bookmark count in game doc and show popular games
parent
3f85628258
commit
97699a81a5
|
@ -46,10 +46,18 @@ object Game extends LilaController {
|
|||
}
|
||||
|
||||
def star(page: Int) = Auth { implicit ctx ⇒
|
||||
me =>
|
||||
me ⇒
|
||||
reasonable(page) {
|
||||
IOk(makeListMenu map { menu ⇒
|
||||
html.game.star(starApi.gamePaginatorByUser(me, page), menu)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
def popular(page: Int) = Open { implicit ctx ⇒
|
||||
reasonable(page) {
|
||||
IOk(makeListMenu map { menu ⇒
|
||||
html.game.star(starApi.gamePaginatorByUser(me, page), menu)
|
||||
html.game.popular(paginator popular page, menu)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,13 +15,13 @@ import scalaz.effects._
|
|||
|
||||
object Round extends LilaController {
|
||||
|
||||
private val gameRepo = env.game.gameRepo
|
||||
private val socket = env.round.socket
|
||||
private val hand = env.round.hand
|
||||
private val messenger = env.round.messenger
|
||||
private val rematcher = env.setup.rematcher
|
||||
private val joiner = env.setup.friendJoiner
|
||||
private val starApi = env.star.api
|
||||
def gameRepo = env.game.gameRepo
|
||||
def socket = env.round.socket
|
||||
def hand = env.round.hand
|
||||
def messenger = env.round.messenger
|
||||
def rematcher = env.setup.rematcher
|
||||
def joiner = env.setup.friendJoiner
|
||||
def starApi = env.star.api
|
||||
|
||||
def websocketWatcher(gameId: String, color: String) = WebSocket.async[JsValue] { req ⇒
|
||||
implicit val ctx = reqToCtx(req)
|
||||
|
@ -53,21 +53,21 @@ object Round extends LilaController {
|
|||
|
||||
def watcher(gameId: String, color: String) = Open { implicit ctx ⇒
|
||||
IOptionIOResult(gameRepo.pov(gameId, color)) { pov ⇒
|
||||
pov.game.started.fold(
|
||||
starApi usersByGame pov.game map { bookmarkers ⇒
|
||||
Ok(html.round.watcher(pov, version(pov.gameId), bookmarkers))
|
||||
},
|
||||
join(pov))
|
||||
pov.game.started.fold(watch(pov), join(pov))
|
||||
}
|
||||
}
|
||||
|
||||
private def watch(pov: Pov)(implicit ctx: Context): IO[Result] =
|
||||
pov.game.hasBookmarks.fold(
|
||||
starApi usersByGame pov.game,
|
||||
io(Nil)
|
||||
) map { bookmarkers ⇒
|
||||
Ok(html.round.watcher(pov, version(pov.gameId), bookmarkers))
|
||||
}
|
||||
|
||||
private def join(pov: Pov)(implicit ctx: Context): IO[Result] =
|
||||
joiner(pov.game, ctx.me).fold(
|
||||
err ⇒ putFailures(err) flatMap { _ ⇒
|
||||
starApi usersByGame pov.game map { bookmarkers ⇒
|
||||
Ok(html.round.watcher(pov, version(pov.gameId), bookmarkers))
|
||||
}
|
||||
},
|
||||
err ⇒ putFailures(err) flatMap (_ ⇒ watch(pov)),
|
||||
_ flatMap {
|
||||
case (p, events) ⇒ performEvents(p.gameId)(events) map { _ ⇒
|
||||
Redirect(routes.Round.player(p.fullId))
|
||||
|
|
|
@ -8,16 +8,11 @@ import scalaz.effects._
|
|||
|
||||
object Star extends LilaController {
|
||||
|
||||
val starRepo = env.star.starRepo
|
||||
val api = env.star.api
|
||||
val gameRepo = env.game.gameRepo
|
||||
|
||||
def toggle(gameId: String) = Auth { implicit ctx ⇒
|
||||
me ⇒
|
||||
IOk {
|
||||
for {
|
||||
exists ← gameRepo exists gameId
|
||||
_ ← exists.fold(starRepo.toggle(gameId, me.id), io())
|
||||
} yield ()
|
||||
}
|
||||
IOk(api.toggle(gameId, me))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,17 +19,19 @@ object User extends LilaController {
|
|||
def forms = user.DataForm
|
||||
def eloUpdater = env.user.eloUpdater
|
||||
def lobbyMessenger = env.lobby.messenger
|
||||
def starApi = env.star.api
|
||||
|
||||
def show(username: String) = showFilter(username, "all", 1)
|
||||
|
||||
def showFilter(username: String, filterName: String, page: Int) = Open { implicit ctx ⇒
|
||||
IOptionIOk(userRepo byId username) { u ⇒
|
||||
u.enabled.fold(
|
||||
env.user.userInfo(u, ctx) map { info ⇒
|
||||
env.user.userInfo(u, starApi, ctx) map { info ⇒
|
||||
val filters = user.GameFilterMenu(info, ctx.me, filterName)
|
||||
html.user.show(u, info,
|
||||
games = gamePaginator.recentlyCreated(filters.query)(page),
|
||||
filters = filters)
|
||||
val paginator = filters.query.fold(
|
||||
query ⇒ gamePaginator.recentlyCreated(query)(page),
|
||||
starApi.gamePaginatorByUser(u, page))
|
||||
html.user.show(u, info, paginator, filters)
|
||||
},
|
||||
io(html.user.disabled(u)))
|
||||
}
|
||||
|
|
|
@ -23,12 +23,14 @@ final class Cached(
|
|||
|
||||
def nbGames: Int = memo(NbGames)
|
||||
def nbMates: Int = memo(NbMates)
|
||||
def nbPopular: Int = memo(NbPopular)
|
||||
|
||||
private val memo = ActorMemo(loadFromDb, nbTtl, 5.seconds)
|
||||
|
||||
private def loadFromDb(key: Key) = key match {
|
||||
case NbGames ⇒ gameRepo.count(_.all).unsafePerformIO
|
||||
case NbMates ⇒ gameRepo.count(_.mate).unsafePerformIO
|
||||
case NbPopular ⇒ gameRepo.count(_.popular).unsafePerformIO
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,4 +40,5 @@ object Cached {
|
|||
|
||||
case object NbGames extends Key
|
||||
case object NbMates extends Key
|
||||
case object NbPopular extends Key
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ case class DbGame(
|
|||
variant: Variant = Variant.default,
|
||||
next: Option[String] = None,
|
||||
lastMoveTime: Option[Int] = None,
|
||||
bookmarks: Int = 0,
|
||||
createdAt: Option[DateTime] = None,
|
||||
updatedAt: Option[DateTime] = None) {
|
||||
|
||||
|
@ -325,6 +326,8 @@ case class DbGame(
|
|||
!finishedOrAborted && updatedAt.fold(
|
||||
_ > DateTime.now - 20.seconds, false)
|
||||
|
||||
def hasBookmarks = bookmarks > 0
|
||||
|
||||
def encode = RawDbGame(
|
||||
id = id,
|
||||
players = players map (_.encode),
|
||||
|
@ -341,6 +344,7 @@ case class DbGame(
|
|||
v = variant.id,
|
||||
next = next,
|
||||
lmt = lastMoveTime,
|
||||
bm = bookmarks.some filter (0 <),
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt
|
||||
)
|
||||
|
@ -397,6 +401,7 @@ case class RawDbGame(
|
|||
v: Int = 1,
|
||||
next: Option[String] = None,
|
||||
lmt: Option[Int] = None,
|
||||
bm: Option[Int] = None,
|
||||
createdAt: Option[DateTime],
|
||||
updatedAt: Option[DateTime]) {
|
||||
|
||||
|
@ -425,6 +430,7 @@ case class RawDbGame(
|
|||
variant = trueVariant,
|
||||
next = next,
|
||||
lastMoveTime = lmt,
|
||||
bookmarks = bm | 0,
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt
|
||||
)
|
||||
|
|
|
@ -76,6 +76,10 @@ class GameRepo(collection: MongoCollection)
|
|||
update(idSelector(id), $set(pn + ".userId" -> user.id, pn + ".elo" -> user.elo))
|
||||
}
|
||||
|
||||
def incBookmarks(id: String, value: Int) = io {
|
||||
update(idSelector(id), $inc("bm" -> value))
|
||||
}
|
||||
|
||||
def finish(id: String, winnerId: Option[String]) = io {
|
||||
update(
|
||||
idSelector(id),
|
||||
|
|
|
@ -8,6 +8,7 @@ import scalaz.effects._
|
|||
case class ListMenu(
|
||||
nbGames: Int,
|
||||
nbMates: Int,
|
||||
nbPopular: Int,
|
||||
nbStars: Option[Int])
|
||||
|
||||
object ListMenu {
|
||||
|
@ -22,6 +23,7 @@ object ListMenu {
|
|||
new ListMenu(
|
||||
nbGames = cached.nbGames,
|
||||
nbMates = cached.nbMates,
|
||||
nbPopular = cached.nbPopular,
|
||||
nbStars = nbStars)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@ final class PaginatorBuilder(
|
|||
def checkmate(page: Int): Paginator[DbGame] =
|
||||
paginator(checkmateAdapter, page)
|
||||
|
||||
def popular(page: Int): Paginator[DbGame] =
|
||||
paginator(popularAdapter, page)
|
||||
|
||||
def recentlyUpdated(query: DBObject) = apply(query, Query.sortUpdated) _
|
||||
|
||||
def recentlyCreated(query: DBObject) = apply(query, Query.sortCreated) _
|
||||
|
@ -31,7 +34,10 @@ final class PaginatorBuilder(
|
|||
adapter(DBObject(), Query.sortUpdated)
|
||||
|
||||
private val checkmateAdapter =
|
||||
adapter(DBObject("status" -> Status.Mate.id), Query.sortUpdated)
|
||||
adapter(Query.mate, Query.sortUpdated)
|
||||
|
||||
private val popularAdapter =
|
||||
adapter(Query.popular, Query.sortPopular)
|
||||
|
||||
private def adapter(query: DBObject, sort: DBObject) = SalatAdapter(
|
||||
dao = gameRepo,
|
||||
|
|
|
@ -31,6 +31,8 @@ object Query {
|
|||
|
||||
val notFinished: DBObject = DBObject("status" -> Status.Started.id)
|
||||
|
||||
val popular: DBObject = "bm" $gt 0
|
||||
|
||||
def clock(c: Boolean): DBObject = "clock.l" $exists c
|
||||
|
||||
def user(u: User): DBObject = DBObject("userIds" -> u.id)
|
||||
|
@ -52,4 +54,6 @@ object Query {
|
|||
val sortCreated = DBObject("createdAt" -> -1)
|
||||
|
||||
val sortUpdated = DBObject("updatedAt" -> -1)
|
||||
|
||||
val sortPopular = DBObject("bm" -> -1)
|
||||
}
|
||||
|
|
|
@ -47,7 +47,6 @@ final class I18nKeys(translator: Translator) {
|
|||
val noGameAvailableRightNowCreateOne = new Key("noGameAvailableRightNowCreateOne")
|
||||
val whiteIsVictorious = new Key("whiteIsVictorious")
|
||||
val blackIsVictorious = new Key("blackIsVictorious")
|
||||
val playANewGame = new Key("playANewGame")
|
||||
val rematch = new Key("rematch")
|
||||
val playWithTheSameOpponentAgain = new Key("playWithTheSameOpponentAgain")
|
||||
val newOpponent = new Key("newOpponent")
|
||||
|
@ -87,6 +86,7 @@ final class I18nKeys(translator: Translator) {
|
|||
val viewAllNbGames = new Key("viewAllNbGames")
|
||||
val viewNbCheckmates = new Key("viewNbCheckmates")
|
||||
val nbBookmarks = new Key("nbBookmarks")
|
||||
val nbPopularGames = new Key("nbPopularGames")
|
||||
val bookmarkedByNbPlayers = new Key("bookmarkedByNbPlayers")
|
||||
val viewInFullSize = new Key("viewInFullSize")
|
||||
val logOut = new Key("logOut")
|
||||
|
|
|
@ -1,16 +1,27 @@
|
|||
package lila
|
||||
package star
|
||||
|
||||
import game.DbGame
|
||||
import game.{ DbGame, GameRepo }
|
||||
import user.{ User, UserRepo }
|
||||
|
||||
import scalaz.effects._
|
||||
|
||||
final class StarApi(
|
||||
starRepo: StarRepo,
|
||||
gameRepo: GameRepo,
|
||||
userRepo: UserRepo,
|
||||
paginator: PaginatorBuilder) {
|
||||
|
||||
def toggle(gameId: String, user: User): IO[Unit] = for {
|
||||
gameOption ← gameRepo game gameId
|
||||
_ ← gameOption.fold(
|
||||
game ⇒ for {
|
||||
bookmarked ← starRepo.toggle(game.id, user.id)
|
||||
_ ← gameRepo.incBookmarks(game.id, bookmarked.fold(1, -1))
|
||||
} yield (),
|
||||
io())
|
||||
} yield ()
|
||||
|
||||
def starred(game: DbGame, user: User): IO[Boolean] =
|
||||
starRepo.exists(game.id, user.id)
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ final class StarEnv(
|
|||
|
||||
lazy val api = new StarApi(
|
||||
starRepo = starRepo,
|
||||
gameRepo = gameRepo,
|
||||
userRepo = userRepo,
|
||||
paginator = paginator)
|
||||
}
|
||||
|
|
|
@ -13,12 +13,16 @@ import org.joda.time.DateTime
|
|||
// db.star.ensureIndex({d: -1})
|
||||
final class StarRepo(val collection: MongoCollection) {
|
||||
|
||||
def toggle(gameId: String, userId: String): IO[Unit] = io {
|
||||
private[star] def toggle(gameId: String, userId: String): IO[Boolean] = io {
|
||||
try {
|
||||
add(gameId, userId, DateTime.now)
|
||||
true
|
||||
}
|
||||
catch {
|
||||
case e: DuplicateKey ⇒ remove(gameId, userId)
|
||||
case e: DuplicateKey ⇒ {
|
||||
remove(gameId, userId)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,7 +35,7 @@ final class StarRepo(val collection: MongoCollection) {
|
|||
}
|
||||
|
||||
def userIdsByGameId(gameId: String): IO[List[String]] = io {
|
||||
(collection find gameIdQuery(gameId) sort sortQuery() map { obj ⇒
|
||||
(collection find gameIdQuery(gameId) sort sortQuery(1) map { obj ⇒
|
||||
obj.getAs[String]("u")
|
||||
}).toList.flatten
|
||||
}
|
||||
|
|
|
@ -18,12 +18,13 @@ object GameFilter {
|
|||
case object Loss extends GameFilter("loss")
|
||||
case object Draw extends GameFilter("draw")
|
||||
case object Playing extends GameFilter("playing")
|
||||
case object Bookmark extends GameFilter("bookmark")
|
||||
}
|
||||
|
||||
case class GameFilterMenu(
|
||||
all: NonEmptyList[GameFilter],
|
||||
current: GameFilter,
|
||||
query: DBObject) {
|
||||
query: Option[DBObject]) {
|
||||
|
||||
def list = all.list
|
||||
}
|
||||
|
@ -45,19 +46,21 @@ object GameFilterMenu extends NonEmptyLists {
|
|||
(info.nbWin > 0) option Win,
|
||||
(info.nbLoss > 0) option Loss,
|
||||
(info.nbDraw > 0) option Draw,
|
||||
(info.nbPlaying > 0) option Playing
|
||||
(info.nbPlaying > 0) option Playing,
|
||||
(info.nbBookmark > 0) option Bookmark
|
||||
).flatten)
|
||||
|
||||
val current = (all.list find (_.name == currentName)) | all.head
|
||||
|
||||
val query = current match {
|
||||
case All ⇒ Query user user
|
||||
case Me ⇒ Query.opponents(user, me | user)
|
||||
case Rated ⇒ Query rated user
|
||||
case Win ⇒ Query win user
|
||||
case Loss ⇒ Query loss user
|
||||
case Draw ⇒ Query draw user
|
||||
case Playing ⇒ Query notFinished user
|
||||
val query: Option[DBObject] = current match {
|
||||
case All ⇒ Some(Query user user)
|
||||
case Me ⇒ Some(Query.opponents(user, me | user))
|
||||
case Rated ⇒ Some(Query rated user)
|
||||
case Win ⇒ Some(Query win user)
|
||||
case Loss ⇒ Some(Query loss user)
|
||||
case Draw ⇒ Some(Query draw user)
|
||||
case Playing ⇒ Some(Query notFinished user)
|
||||
case Bookmark ⇒ None
|
||||
}
|
||||
|
||||
new GameFilterMenu(all, current, query)
|
||||
|
|
|
@ -4,25 +4,27 @@ package user
|
|||
import chess.{ EloCalculator, Color }
|
||||
import game.{ GameRepo, DbGame }
|
||||
import http.Context
|
||||
import star.StarApi
|
||||
|
||||
import scalaz.effects._
|
||||
|
||||
case class UserInfo(
|
||||
user: User,
|
||||
rank: Option[(Int, Int)],
|
||||
nbWin: Int,
|
||||
nbDraw: Int,
|
||||
nbLoss: Int,
|
||||
nbPlaying: Int,
|
||||
nbWithMe: Option[Int],
|
||||
eloWithMe: Option[List[(String, Int)]],
|
||||
eloChart: Option[EloChart],
|
||||
winChart: Option[WinChart]) {
|
||||
user: User,
|
||||
rank: Option[(Int, Int)],
|
||||
nbWin: Int,
|
||||
nbDraw: Int,
|
||||
nbLoss: Int,
|
||||
nbPlaying: Int,
|
||||
nbWithMe: Option[Int],
|
||||
nbBookmark: Int,
|
||||
eloWithMe: Option[List[(String, Int)]],
|
||||
eloChart: Option[EloChart],
|
||||
winChart: Option[WinChart]) {
|
||||
|
||||
def nbRated = user.nbRatedGames
|
||||
def nbRated = user.nbRatedGames
|
||||
|
||||
def percentRated: Int = math.round(nbRated / user.nbGames.toFloat * 100)
|
||||
}
|
||||
def percentRated: Int = math.round(nbRated / user.nbGames.toFloat * 100)
|
||||
}
|
||||
|
||||
object UserInfo {
|
||||
|
||||
|
@ -32,6 +34,7 @@ object UserInfo {
|
|||
eloCalculator: EloCalculator,
|
||||
eloChartBuilder: User ⇒ IO[Option[EloChart]])(
|
||||
user: User,
|
||||
starApi: StarApi,
|
||||
ctx: Context): IO[UserInfo] = for {
|
||||
rank ← (user.elo >= 1500).fold(
|
||||
userRepo rank user flatMap { rank ⇒
|
||||
|
@ -51,6 +54,7 @@ object UserInfo {
|
|||
me ⇒ gameRepo count (_.opponents(user, me)) map (_.some),
|
||||
io(none)
|
||||
)
|
||||
nbBookmark ← starApi countByUser user
|
||||
eloChart ← eloChartBuilder(user)
|
||||
winChart = (user.nbRatedGames > 0) option {
|
||||
new WinChart(nbWin, nbDraw, nbLoss)
|
||||
|
@ -69,6 +73,7 @@ object UserInfo {
|
|||
nbLoss = nbLoss,
|
||||
nbPlaying = nbPlaying | 0,
|
||||
nbWithMe = nbWithMe,
|
||||
nbBookmark = nbBookmark,
|
||||
eloWithMe = eloWithMe,
|
||||
eloChart = eloChart,
|
||||
winChart = winChart)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
@(paginator: Paginator[DbGame], listMenu: lila.game.ListMenu)(implicit ctx: Context)
|
||||
|
||||
@game.list(
|
||||
name = trans.nbPopularGames.str(listMenu.nbPopular.localize),
|
||||
paginator = paginator,
|
||||
next = paginator.nextPage map { n => routes.Game.popular(n) },
|
||||
menu = sideMenu(listMenu, "popular"))
|
|
@ -13,4 +13,7 @@
|
|||
<a class="@active.active("star")" href="@routes.Game.star()">
|
||||
@trans.nbBookmarks(nb.localize)
|
||||
</a>
|
||||
<a class="@active.active("popular")" href="@routes.Game.popular()">
|
||||
@trans.nbPopularGames(listMenu.nbPopular.localize)
|
||||
</a>
|
||||
}
|
||||
|
|
|
@ -22,5 +22,8 @@ case lila.user.GameFilter.Draw => {
|
|||
case lila.user.GameFilter.Playing => {
|
||||
@info.nbPlaying playing
|
||||
}
|
||||
case lila.user.GameFilter.Bookmark => {
|
||||
@info.nbBookmark bookmarks
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ gamesBeingPlayedRightNow=Current Games
|
|||
viewAllNbGames=%s Games
|
||||
viewNbCheckmates=%s Checkmates
|
||||
nbBookmarks=%s Bookmarks
|
||||
nbPopularGames=%s Popular Games
|
||||
bookmarkedByNbPlayers=Bookmarked by %s players
|
||||
viewInFullSize=View in full size
|
||||
logOut=Log out
|
||||
|
|
|
@ -7,6 +7,7 @@ GET /games/refresh/:ids controllers.Game.realtimeInner(ids: String)
|
|||
GET /games/all controllers.Game.all(page: Int ?= 1)
|
||||
GET /games/checkmate controllers.Game.checkmate(page: Int ?= 1)
|
||||
GET /games/bookmark controllers.Game.star(page: Int ?= 1)
|
||||
GET /games/popular controllers.Game.popular(page: Int ?= 1)
|
||||
|
||||
# Round
|
||||
GET /$gameId<[\w\-]{8}> controllers.Round.watcher(gameId: String, color: String = "white")
|
||||
|
|
|
@ -212,7 +212,7 @@ div.lichess_goodies div.box .player {
|
|||
}
|
||||
|
||||
div.bookmarkers {
|
||||
margin-top: 1em;
|
||||
margin-top: 2em;
|
||||
}
|
||||
div.bookmarkers li {
|
||||
margin-top: 5px;
|
||||
|
|
5
todo
5
todo
|
@ -24,6 +24,11 @@ star people and games (and forum threads?)
|
|||
autoclose top menus
|
||||
tournaments http://www.chess.com/tournaments/help.html
|
||||
long name display issue http://en.lichess.org/forum/lichess-feedback/long-names-alter-layout
|
||||
remove bookmark when removing game
|
||||
fix game list translations
|
||||
|
||||
next deploy:
|
||||
db.game2.ensureIndex({"bm": -1}, {sparse: true})
|
||||
|
||||
new translations:
|
||||
-rematchOfferCanceled=Rematch offer canceled
|
||||
|
|
Loading…
Reference in New Issue