Merge branch 'master' into swiss
* master: (21 commits) show class menu to all titled players - for #6524 let everyone create 3 teams per week - for #6524 let class teachers create more teams - for #6524 make it clearer that a player can join up to 100 teams - closes #6517 index perf stats from secondary assign colors in open challenges - closes #6525 fix socket disconnect tweak crosstable style remove dead code {master} tweak crosstable style {master} close WS on reload {master} FIDE can create up to 48 tournaments per day Move space outside link scalafmt Add a space between two sentences Fix translation source More translations for the teams fix typo Add `gameplay` string and remove some trailing spaces Remove LM string ...swiss
commit
00eca9b56f
|
@ -27,14 +27,19 @@ final class Challenge(
|
|||
}
|
||||
}
|
||||
|
||||
def show(id: String) = Open { implicit ctx =>
|
||||
def show(id: String, _color: Option[String]) = Open { implicit ctx =>
|
||||
showId(id)
|
||||
}
|
||||
|
||||
protected[controllers] def showId(id: String)(implicit ctx: Context): Fu[Result] =
|
||||
protected[controllers] def showId(id: String)(
|
||||
implicit ctx: Context
|
||||
): Fu[Result] =
|
||||
OptionFuResult(api byId id)(showChallenge(_))
|
||||
|
||||
protected[controllers] def showChallenge(c: ChallengeModel, error: Option[String] = None)(
|
||||
protected[controllers] def showChallenge(
|
||||
c: ChallengeModel,
|
||||
error: Option[String] = None
|
||||
)(
|
||||
implicit ctx: Context
|
||||
): Fu[Result] =
|
||||
env.challenge version c.id flatMap { version =>
|
||||
|
@ -54,7 +59,7 @@ final class Challenge(
|
|||
}
|
||||
} else
|
||||
(c.challengerUserId ?? env.user.repo.named) map { user =>
|
||||
Ok(html.challenge.theirs(c, json, user))
|
||||
Ok(html.challenge.theirs(c, json, user, get("color") flatMap chess.Color.apply))
|
||||
},
|
||||
api = _ => Ok(json).fuccess
|
||||
) flatMap withChallengeAnonCookie(mine && c.challengerIsAnon, c, true)
|
||||
|
@ -69,20 +74,23 @@ final class Challenge(
|
|||
private def isForMe(challenge: ChallengeModel)(implicit ctx: Context) =
|
||||
challenge.destUserId.fold(true)(ctx.userId.contains)
|
||||
|
||||
def accept(id: String) = Open { implicit ctx =>
|
||||
def accept(id: String, color: Option[String]) = Open { implicit ctx =>
|
||||
OptionFuResult(api byId id) { c =>
|
||||
isForMe(c) ?? api.accept(c, ctx.me, HTTPRequest sid ctx.req).flatMap {
|
||||
case Some(pov) =>
|
||||
negotiate(
|
||||
html = Redirect(routes.Round.watcher(pov.gameId, "white")).fuccess,
|
||||
api = apiVersion => env.api.roundApi.player(pov, none, apiVersion) map { Ok(_) }
|
||||
) flatMap withChallengeAnonCookie(ctx.isAnon, c, false)
|
||||
case None =>
|
||||
negotiate(
|
||||
html = Redirect(routes.Round.watcher(c.id, "white")).fuccess,
|
||||
api = _ => notFoundJson("Someone else accepted the challenge")
|
||||
)
|
||||
}
|
||||
val cc = color flatMap chess.Color.apply
|
||||
isForMe(c) ?? api
|
||||
.accept(c, ctx.me, HTTPRequest sid ctx.req, cc)
|
||||
.flatMap {
|
||||
case Some(pov) =>
|
||||
negotiate(
|
||||
html = Redirect(routes.Round.watcher(pov.gameId, cc.fold("white")(_.name))).fuccess,
|
||||
api = apiVersion => env.api.roundApi.player(pov, none, apiVersion) map { Ok(_) }
|
||||
) flatMap withChallengeAnonCookie(ctx.isAnon, c, false)
|
||||
case None =>
|
||||
negotiate(
|
||||
html = Redirect(routes.Round.watcher(c.id, cc.fold("white")(_.name))).fuccess,
|
||||
api = _ => notFoundJson("Someone else accepted the challenge")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
def apiAccept(id: String) = Scoped(_.Challenge.Write, _.Bot.Play, _.Board.Play) { _ => me =>
|
||||
|
@ -285,8 +293,10 @@ final class Challenge(
|
|||
(env.challenge.api create challenge) map {
|
||||
case true =>
|
||||
JsonOk(
|
||||
env.challenge.jsonView
|
||||
.show(challenge, SocketVersion(0), none)
|
||||
env.challenge.jsonView.show(challenge, SocketVersion(0), none) ++ Json.obj(
|
||||
"urlWhite" -> s"${env.net.baseUrl}/${challenge.id}?color=white",
|
||||
"urlBlack" -> s"${env.net.baseUrl}/${challenge.id}?color=black"
|
||||
)
|
||||
)
|
||||
case false =>
|
||||
BadRequest(jsonError("Challenge not created"))
|
||||
|
|
|
@ -198,22 +198,30 @@ final class Team(
|
|||
def join(id: String) = AuthOrScoped(_.Team.Write)(
|
||||
auth = implicit ctx =>
|
||||
me =>
|
||||
negotiate(
|
||||
html = api.join(id, me) flatMap {
|
||||
case Some(Joined(team)) => Redirect(routes.Team.show(team.id)).flashSuccess.fuccess
|
||||
case Some(Motivate(team)) => Redirect(routes.Team.requestForm(team.id)).flashSuccess.fuccess
|
||||
case _ => notFound(ctx)
|
||||
},
|
||||
api = _ =>
|
||||
api.join(id, me) flatMap {
|
||||
case Some(Joined(_)) => jsonOkResult.fuccess
|
||||
case Some(Motivate(_)) =>
|
||||
BadRequest(
|
||||
jsonError("This team requires confirmation.")
|
||||
).fuccess
|
||||
case _ => notFoundJson("Team not found")
|
||||
}
|
||||
),
|
||||
api countTeamsOf me flatMap { nb =>
|
||||
if (nb >= TeamModel.maxJoin)
|
||||
negotiate(
|
||||
html = BadRequest(views.html.site.message.teamJoinLimit).fuccess,
|
||||
api = _ => BadRequest(jsonError("You have joined too many teams")).fuccess
|
||||
)
|
||||
else
|
||||
negotiate(
|
||||
html = api.join(id, me) flatMap {
|
||||
case Some(Joined(team)) => Redirect(routes.Team.show(team.id)).flashSuccess.fuccess
|
||||
case Some(Motivate(team)) => Redirect(routes.Team.requestForm(team.id)).flashSuccess.fuccess
|
||||
case _ => notFound(ctx)
|
||||
},
|
||||
api = _ =>
|
||||
api.join(id, me) flatMap {
|
||||
case Some(Joined(_)) => jsonOkResult.fuccess
|
||||
case Some(Motivate(_)) =>
|
||||
BadRequest(
|
||||
jsonError("This team requires confirmation.")
|
||||
).fuccess
|
||||
case _ => notFoundJson("Team not found")
|
||||
}
|
||||
)
|
||||
},
|
||||
scoped = req =>
|
||||
me =>
|
||||
env.oAuth.server.fetchAppAuthor(req) flatMap {
|
||||
|
@ -401,8 +409,9 @@ You received this message because you are part of the team lichess.org${routes.T
|
|||
)
|
||||
|
||||
private def OnePerWeek[A <: Result](me: UserModel)(a: => Fu[A])(implicit ctx: Context): Fu[Result] =
|
||||
api.hasCreatedRecently(me) flatMap { did =>
|
||||
if (did && !Granter(_.ManageTeam)(me)) Forbidden(views.html.site.message.teamCreateLimit).fuccess
|
||||
api.countCreatedRecently(me) flatMap { count =>
|
||||
if (count > 10 || (count > 3 && !Granter(_.Teacher)(me) && !Granter(_.ManageTeam)(me)))
|
||||
Forbidden(views.html.site.message.teamCreateLimit).fuccess
|
||||
else a
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ object topnav {
|
|||
if (ctx.blind) h3(name) else a(href := url)(name)
|
||||
|
||||
private def canSeeClasMenu(implicit ctx: Context) =
|
||||
ctx.hasClas || ctx.me.exists(_.roles contains "ROLE_COACH")
|
||||
ctx.hasClas || ctx.me.exists(u => u.hasTitle || u.roles.contains("ROLE_COACH"))
|
||||
|
||||
def apply()(implicit ctx: Context) = st.nav(id := "topnav", cls := "hover")(
|
||||
st.section(
|
||||
|
|
|
@ -12,13 +12,15 @@ import controllers.routes
|
|||
|
||||
object bits {
|
||||
|
||||
def js(c: Challenge, json: play.api.libs.json.JsObject, owner: Boolean)(implicit ctx: Context) =
|
||||
def js(c: Challenge, json: play.api.libs.json.JsObject, owner: Boolean, color: Option[chess.Color] = None)(
|
||||
implicit ctx: Context
|
||||
) =
|
||||
frag(
|
||||
jsTag("challenge.js", defer = true),
|
||||
embedJsUnsafe(s"""lichess=window.lichess||{};customWs=true;lichess_challenge = ${safeJsonValue(
|
||||
Json.obj(
|
||||
"socketUrl" -> s"/challenge/${c.id}/socket/v$apiVersion",
|
||||
"xhrUrl" -> routes.Challenge.show(c.id).url,
|
||||
"xhrUrl" -> routes.Challenge.show(c.id, color.map(_.name)).url,
|
||||
"owner" -> owner,
|
||||
"data" -> json
|
||||
)
|
||||
|
|
|
@ -3,6 +3,7 @@ package views.html.challenge
|
|||
import lila.api.Context
|
||||
import lila.app.templating.Environment._
|
||||
import lila.app.ui.ScalatagsTemplate._
|
||||
import lila.challenge.Challenge
|
||||
import lila.challenge.Challenge.Status
|
||||
|
||||
import controllers.routes
|
||||
|
@ -10,14 +11,15 @@ import controllers.routes
|
|||
object theirs {
|
||||
|
||||
def apply(
|
||||
c: lila.challenge.Challenge,
|
||||
c: Challenge,
|
||||
json: play.api.libs.json.JsObject,
|
||||
user: Option[lila.user.User]
|
||||
user: Option[lila.user.User],
|
||||
color: Option[chess.Color]
|
||||
)(implicit ctx: Context) =
|
||||
views.html.base.layout(
|
||||
title = challengeTitle(c),
|
||||
openGraph = challengeOpenGraph(c).some,
|
||||
moreJs = bits.js(c, json, false),
|
||||
moreJs = bits.js(c, json, false, color),
|
||||
moreCss = cssTag("challenge.page")
|
||||
) {
|
||||
main(cls := "page-small challenge-page challenge-theirs box box-pad")(
|
||||
|
@ -40,15 +42,20 @@ object theirs {
|
|||
c.notableInitialFen.map { fen =>
|
||||
div(cls := "board-preview", views.html.game.bits.miniBoard(fen, color = !c.finalColor))
|
||||
},
|
||||
if (!c.mode.rated || ctx.isAuth)
|
||||
if (color.map(Challenge.ColorChoice.apply).has(c.colorChoice))
|
||||
badTag(
|
||||
// very rare message, don't translate
|
||||
s"You have the wrong color link for this open challenge. The ${color.??(_.name)} player has already joined."
|
||||
)
|
||||
else if (!c.mode.rated || ctx.isAuth) {
|
||||
frag(
|
||||
(c.mode.rated && c.unlimited) option
|
||||
badTag(trans.bewareTheGameIsRatedButHasNoClock()),
|
||||
postForm(cls := "accept", action := routes.Challenge.accept(c.id))(
|
||||
postForm(cls := "accept", action := routes.Challenge.accept(c.id, color.map(_.name)))(
|
||||
submitButton(cls := "text button button-fat", dataIcon := "G")(trans.joinTheGame())
|
||||
)
|
||||
)
|
||||
else
|
||||
} else
|
||||
frag(
|
||||
hr,
|
||||
badTag(
|
||||
|
|
|
@ -35,6 +35,7 @@ object faq {
|
|||
whyIsLichessCalledLichess.txt(),
|
||||
p(
|
||||
lichessCombinationLiveLightLibrePronounced(em(leechess())),
|
||||
" ",
|
||||
a(href := "https://www.youtube.com/watch?v=KRpPqcrdE-o")(hearItPronouncedBySpecialist())
|
||||
),
|
||||
p(
|
||||
|
@ -111,7 +112,7 @@ object faq {
|
|||
youCanUseOpeningBookNoEngine()
|
||||
)
|
||||
),
|
||||
h2("Gameplay"),
|
||||
h2(gameplay()),
|
||||
question(
|
||||
"time-controls",
|
||||
howBulletBlitzEtcDecided.txt(),
|
||||
|
@ -206,7 +207,7 @@ object faq {
|
|||
p(
|
||||
showYourTitle(
|
||||
a(href := routes.Main.verifyTitle())(verificationForm()),
|
||||
a(href := "#lm")(lichessMasterLM())
|
||||
a(href := "#lm")("Lichess master (LM)")
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
|
@ -84,6 +84,10 @@ object message {
|
|||
"You have already created a team this week."
|
||||
}
|
||||
|
||||
def teamJoinLimit(implicit ctx: Context) = apply("Cannot join the team") {
|
||||
"You have already joined too many teams."
|
||||
}
|
||||
|
||||
def authFailed(implicit ctx: Context) = apply("403 - Access denied!") {
|
||||
"You tried to visit a page you're not authorized to access."
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ object admin {
|
|||
div(cls := "page-menu__content box box-pad")(
|
||||
h1(title),
|
||||
postForm(cls := "leaders", action := routes.Team.leaders(t.id))(
|
||||
form3.group(form("leaders"), frag("Users who can manage this team"))(
|
||||
form3.group(form("leaders"), frag(usersWhoCanManageThisTeam()))(
|
||||
form3.textarea(_)(rows := 2)
|
||||
),
|
||||
form3.actions(
|
||||
|
@ -64,7 +64,7 @@ object admin {
|
|||
implicit ctx: Context
|
||||
) = {
|
||||
|
||||
val title = s"${t.name} • message all members"
|
||||
val title = s"${t.name} • ${messageAllMembers.txt()}"
|
||||
|
||||
views.html.base.layout(
|
||||
title = title,
|
||||
|
@ -81,14 +81,10 @@ object admin {
|
|||
div(cls := "page-menu__content box box-pad")(
|
||||
h1(title),
|
||||
p(
|
||||
"Send a private message to ALL members of the team.",
|
||||
br,
|
||||
"You can use this to call players to join a tournament or a team battle.",
|
||||
br,
|
||||
"Players who don't like receiving your messages might leave the team."
|
||||
),
|
||||
messageAllMembersLongDescription()
|
||||
),
|
||||
tours.nonEmpty option div(cls := "tournaments")(
|
||||
p("You may want to link one of these upcoming tournaments?"),
|
||||
p(youWayWantToLinkOneOfTheseTournaments()),
|
||||
p(
|
||||
ul(
|
||||
tours.map { t =>
|
||||
|
|
|
@ -44,7 +44,7 @@ object list {
|
|||
main(cls := "team-list page-menu")(
|
||||
bits.menu("leader".some),
|
||||
div(cls := "page-menu__content box")(
|
||||
h1("Teams I lead"),
|
||||
h1(teamsIlead()),
|
||||
standardFlash(),
|
||||
table(cls := "slist slist-pad")(
|
||||
if (teams.size > 0) tbody(teams.map(bits.teamTr(_)))
|
||||
|
|
|
@ -45,7 +45,7 @@ object show {
|
|||
"socketVersion" -> v.value,
|
||||
"chat" -> views.html.chat.json(
|
||||
chat.chat,
|
||||
name = if (t.isChatFor(_.LEADERS)) "Leaders chat" else trans.chatRoom.txt(),
|
||||
name = if (t.isChatFor(_.LEADERS)) leadersChat.txt() else trans.chatRoom.txt(),
|
||||
timeout = chat.timeout,
|
||||
public = true,
|
||||
resourceId = lila.chat.Chat.ResourceId(s"team/${chat.chat.id}"),
|
||||
|
@ -68,7 +68,7 @@ object show {
|
|||
(info.mine || t.enabled) option div(cls := "team-show__content")(
|
||||
div(cls := "team-show__content__col1")(
|
||||
st.section(cls := "team-show__meta")(
|
||||
p(teamLeaders(), ": ", fragList(t.leaders.toList.map { l =>
|
||||
p(teamLeaders.pluralSame(t.leaders.size), ": ", fragList(t.leaders.toList.map { l =>
|
||||
userIdLink(l.some)
|
||||
}))
|
||||
),
|
||||
|
|
|
@ -336,8 +336,8 @@ GET /setup/validate-fen controllers.Setup.validateFen
|
|||
|
||||
# Challenge
|
||||
GET /challenge controllers.Challenge.all
|
||||
GET /challenge/$id<\w{8}> controllers.Challenge.show(id: String)
|
||||
POST /challenge/$id<\w{8}>/accept controllers.Challenge.accept(id: String)
|
||||
GET /challenge/$id<\w{8}> controllers.Challenge.show(id: String, color: Option[String] ?= None)
|
||||
POST /challenge/$id<\w{8}>/accept controllers.Challenge.accept(id: String, color: Option[String] ?= None)
|
||||
POST /challenge/$id<\w{8}>/decline controllers.Challenge.decline(id: String)
|
||||
POST /challenge/$id<\w{8}>/cancel controllers.Challenge.cancel(id: String)
|
||||
POST /challenge/$id<\w{8}>/to-friend controllers.Challenge.toFriend(id: String)
|
||||
|
|
|
@ -2,7 +2,7 @@ package lila.challenge
|
|||
|
||||
import chess.format.FEN
|
||||
import chess.variant.{ FromPosition, Horde, RacingKings, Variant }
|
||||
import chess.{ Mode, Speed }
|
||||
import chess.{ Color, Mode, Speed }
|
||||
import org.joda.time.DateTime
|
||||
|
||||
import lila.game.PerfPicker
|
||||
|
@ -135,6 +135,7 @@ object Challenge {
|
|||
case object Random extends ColorChoice
|
||||
case object White extends ColorChoice
|
||||
case object Black extends ColorChoice
|
||||
def apply(c: Color) = c.fold[ColorChoice](White, Black)
|
||||
}
|
||||
|
||||
private def speedOf(timeControl: TimeControl) = timeControl match {
|
||||
|
|
|
@ -66,16 +66,22 @@ final class ChallengeApi(
|
|||
|
||||
private val acceptQueue = new WorkQueue(buffer = 64, timeout = 5 seconds, "challengeAccept")
|
||||
|
||||
def accept(c: Challenge, user: Option[User], sid: Option[String]): Fu[Option[Pov]] = acceptQueue {
|
||||
if (c.challengerIsOpen) repo.setChallenger(c.setChallenger(user, sid)) inject none
|
||||
def accept(
|
||||
c: Challenge,
|
||||
user: Option[User],
|
||||
sid: Option[String],
|
||||
color: Option[chess.Color] = None
|
||||
): Fu[Option[Pov]] = acceptQueue {
|
||||
if (c.challengerIsOpen)
|
||||
repo.setChallenger(c.setChallenger(user, sid), color) inject none
|
||||
else
|
||||
joiner(c, user).flatMap {
|
||||
case None => fuccess(None)
|
||||
case Some(pov) =>
|
||||
joiner(c, user, color).flatMap {
|
||||
_ ?? { pov =>
|
||||
(repo accept c) >>- {
|
||||
uncacheAndNotify(c)
|
||||
Bus.publish(Event.Accept(c, user.map(_.id)), "challenge")
|
||||
} inject pov.some
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,7 +101,7 @@ final class ChallengeApi(
|
|||
}
|
||||
|
||||
def oauthAccept(dest: User, challenge: Challenge): Fu[Option[Game]] =
|
||||
joiner(challenge, dest.some).map2(_.game)
|
||||
joiner(challenge, dest.some, none).map2(_.game)
|
||||
|
||||
private def isLimitedByMaxPlaying(c: Challenge) =
|
||||
if (c.hasClock) fuFalse
|
||||
|
|
|
@ -42,8 +42,15 @@ final private class ChallengeRepo(coll: Coll, maxPerUser: Max)(
|
|||
.sort($doc("createdAt" -> 1))
|
||||
.list[Challenge]()
|
||||
|
||||
def setChallenger(c: Challenge) =
|
||||
coll.update.one($id(c.id), $set("challenger" -> c.challenger)).void
|
||||
def setChallenger(c: Challenge, color: Option[chess.Color]) =
|
||||
coll.update
|
||||
.one(
|
||||
$id(c.id),
|
||||
$set($doc("challenger" -> c.challenger) ++ color.?? { c =>
|
||||
$doc("colorChoice" -> Challenge.ColorChoice(c), "finalColor" -> c)
|
||||
})
|
||||
)
|
||||
.void
|
||||
|
||||
private[challenge] def allWithUserId(userId: String): Fu[List[Challenge]] =
|
||||
createdByChallengerId(userId) zip createdByDestId(userId) dmap {
|
||||
|
|
|
@ -4,7 +4,7 @@ import scala.util.chaining._
|
|||
|
||||
import chess.format.Forsyth
|
||||
import chess.format.Forsyth.SituationPlus
|
||||
import chess.{ Mode, Situation }
|
||||
import chess.{ Color, Mode, Situation }
|
||||
import lila.game.{ Game, Player, Pov, Source }
|
||||
import lila.user.User
|
||||
|
||||
|
@ -14,10 +14,11 @@ final private class Joiner(
|
|||
onStart: lila.round.OnStart
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
||||
def apply(c: Challenge, destUser: Option[User]): Fu[Option[Pov]] =
|
||||
def apply(c: Challenge, destUser: Option[User], color: Option[Color]): Fu[Option[Pov]] =
|
||||
gameRepo exists c.id flatMap {
|
||||
case true => fuccess(None)
|
||||
case false =>
|
||||
case true => fuccess(None)
|
||||
case _ if color.map(Challenge.ColorChoice.apply).has(c.colorChoice) => fuccess(None)
|
||||
case _ =>
|
||||
c.challengerUserId.??(userRepo.byId) flatMap { challengerUser =>
|
||||
def makeChess(variant: chess.variant.Variant): chess.Game =
|
||||
chess.Game(situation = Situation(variant), clock = c.clock.map(_.config.toClock))
|
||||
|
|
|
@ -1533,7 +1533,6 @@ val `quitTeam` = new I18nKey("team:quitTeam")
|
|||
val `anyoneCanJoin` = new I18nKey("team:anyoneCanJoin")
|
||||
val `aConfirmationIsRequiredToJoin` = new I18nKey("team:aConfirmationIsRequiredToJoin")
|
||||
val `joiningPolicy` = new I18nKey("team:joiningPolicy")
|
||||
val `teamLeaders` = new I18nKey("team:teamLeaders")
|
||||
val `teamBestPlayers` = new I18nKey("team:teamBestPlayers")
|
||||
val `teamRecentMembers` = new I18nKey("team:teamRecentMembers")
|
||||
val `kickSomeone` = new I18nKey("team:kickSomeone")
|
||||
|
@ -1546,7 +1545,13 @@ val `teamTournament` = new I18nKey("team:teamTournament")
|
|||
val `teamTournamentOverview` = new I18nKey("team:teamTournamentOverview")
|
||||
val `messageAllMembers` = new I18nKey("team:messageAllMembers")
|
||||
val `messageAllMembersOverview` = new I18nKey("team:messageAllMembersOverview")
|
||||
val `messageAllMembersLongDescription` = new I18nKey("team:messageAllMembersLongDescription")
|
||||
val `teamsIlead` = new I18nKey("team:teamsIlead")
|
||||
val `youWayWantToLinkOneOfTheseTournaments` = new I18nKey("team:youWayWantToLinkOneOfTheseTournaments")
|
||||
val `usersWhoCanManageThisTeam` = new I18nKey("team:usersWhoCanManageThisTeam")
|
||||
val `leadersChat` = new I18nKey("team:leadersChat")
|
||||
val `nbMembers` = new I18nKey("team:nbMembers")
|
||||
val `teamLeaders` = new I18nKey("team:teamLeaders")
|
||||
val `xJoinRequests` = new I18nKey("team:xJoinRequests")
|
||||
}
|
||||
|
||||
|
@ -1680,6 +1685,7 @@ val `howCanIBecomeModerator` = new I18nKey("faq:howCanIBecomeModerator")
|
|||
val `youCannotApply` = new I18nKey("faq:youCannotApply")
|
||||
val `isCorrespondenceDifferent` = new I18nKey("faq:isCorrespondenceDifferent")
|
||||
val `youCanUseOpeningBookNoEngine` = new I18nKey("faq:youCanUseOpeningBookNoEngine")
|
||||
val `gameplay` = new I18nKey("faq:gameplay")
|
||||
val `howBulletBlitzEtcDecided` = new I18nKey("faq:howBulletBlitzEtcDecided")
|
||||
val `basedOnGameDuration` = new I18nKey("faq:basedOnGameDuration")
|
||||
val `durationFormula` = new I18nKey("faq:durationFormula")
|
||||
|
@ -1714,7 +1720,6 @@ val `lichessRecognizeAllOTBtitles` = new I18nKey("faq:lichessRecognizeAllOTBtitl
|
|||
val `showYourTitle` = new I18nKey("faq:showYourTitle")
|
||||
val `asWellAsManyNMtitles` = new I18nKey("faq:asWellAsManyNMtitles")
|
||||
val `verificationForm` = new I18nKey("faq:verificationForm")
|
||||
val `lichessMasterLM` = new I18nKey("faq:lichessMasterLM")
|
||||
val `canIbecomeLM` = new I18nKey("faq:canIbecomeLM")
|
||||
val `noUpperCaseDot` = new I18nKey("faq:noUpperCaseDot")
|
||||
val `lMtitleComesToYouDoNotRequestIt` = new I18nKey("faq:lMtitleComesToYouDoNotRequestIt")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package lila.perfStat
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import reactivemongo.api.ReadPreference
|
||||
|
||||
import lila.game.{ Game, GameRepo, Pov, Query }
|
||||
import lila.rating.PerfType
|
||||
|
@ -12,7 +13,8 @@ final class PerfStatIndexer(
|
|||
storage: PerfStatStorage
|
||||
)(implicit ec: scala.concurrent.ExecutionContext, mat: akka.stream.Materializer) {
|
||||
|
||||
private val workQueue = new WorkQueue(buffer = 64, timeout = 1 minute, "perfStatIndexer")
|
||||
private val workQueue =
|
||||
new WorkQueue(buffer = 64, timeout = 10 seconds, name = "perfStatIndexer")
|
||||
|
||||
private[perfStat] def userPerf(user: User, perfType: PerfType): Fu[PerfStat] = workQueue {
|
||||
storage.find(user.id, perfType) getOrElse gameRepo
|
||||
|
@ -21,7 +23,8 @@ final class PerfStatIndexer(
|
|||
Query.finished ++
|
||||
Query.turnsGt(2) ++
|
||||
Query.variant(PerfType variantOf perfType),
|
||||
Query.sortChronological
|
||||
Query.sortChronological,
|
||||
readPreference = ReadPreference.secondaryPreferred
|
||||
)
|
||||
.fold(PerfStat.init(user.id, perfType)) {
|
||||
case (perfStat, game) if game.perfType.contains(perfType) =>
|
||||
|
|
|
@ -39,11 +39,6 @@ case class AnaDrop(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
// def json(b: Branch): JsObject = Json.obj(
|
||||
// "node" -> b,
|
||||
// "path" -> path
|
||||
// ).add("ch" -> chapterId)
|
||||
}
|
||||
|
||||
object AnaDrop {
|
||||
|
|
|
@ -34,6 +34,8 @@ case class Team(
|
|||
|
||||
object Team {
|
||||
|
||||
val maxJoin = 100
|
||||
|
||||
type ID = String
|
||||
|
||||
type ChatFor = Int
|
||||
|
|
|
@ -30,8 +30,6 @@ final class TeamApi(
|
|||
|
||||
import BSONHandlers._
|
||||
|
||||
val creationPeriod = Period weeks 1
|
||||
|
||||
def team(id: Team.ID) = teamRepo.coll.byId[Team](id)
|
||||
|
||||
def light(id: Team.ID) = teamRepo.coll.byId[LightTeam](id, $doc("name" -> true))
|
||||
|
@ -76,10 +74,13 @@ final class TeamApi(
|
|||
def mine(me: User): Fu[List[Team]] =
|
||||
cached teamIdsList me.id flatMap teamRepo.byIdsSortPopular
|
||||
|
||||
def countTeamsOf(me: User) =
|
||||
cached teamIdsList me.id dmap (_.size)
|
||||
|
||||
def hasTeams(me: User): Fu[Boolean] = cached.teamIds(me.id).map(_.value.nonEmpty)
|
||||
|
||||
def hasCreatedRecently(me: User): Fu[Boolean] =
|
||||
teamRepo.userHasCreatedSince(me.id, creationPeriod)
|
||||
def countCreatedRecently(me: User): Fu[Int] =
|
||||
teamRepo.countCreatedSince(me.id, Period weeks 1)
|
||||
|
||||
def requestsWithUsers(team: Team): Fu[List[RequestWithUser]] =
|
||||
for {
|
||||
|
|
|
@ -53,8 +53,8 @@ final class TeamRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
|
|||
def name(id: String): Fu[Option[String]] =
|
||||
coll.primitiveOne[String]($id(id), "name")
|
||||
|
||||
def userHasCreatedSince(userId: String, duration: Period): Fu[Boolean] =
|
||||
coll.exists(
|
||||
private[team] def countCreatedSince(userId: String, duration: Period): Fu[Int] =
|
||||
coll.countSel(
|
||||
$doc(
|
||||
"createdAt" $gt DateTime.now.minus(duration),
|
||||
"createdBy" -> userId
|
||||
|
|
|
@ -27,46 +27,46 @@
|
|||
<string name="youCannotApply">It’s not possible to apply to become a moderator. If we see someone who we think would be good as a moderator, we will contact them directly.</string>
|
||||
<string name="isCorrespondenceDifferent">Is correspondence different from normal chess?</string>
|
||||
<string name="youCanUseOpeningBookNoEngine">On Lichess, the main difference in rules for correspondence chess is that an opening book is allowed. The use of engines is still prohibited and will result in being flagged for engine assistance. Although ICCF allows engine use in correspondence, Lichess does not.</string>
|
||||
<string name="gameplay">Gameplay</string>
|
||||
<string name="howBulletBlitzEtcDecided">How are Bullet, Blitz and other time controls decided?</string>
|
||||
<string name="basedOnGameDuration">Lichess time controls are based on estimated game duration = %1$s
|
||||
For instance, the estimated duration of a 5+3 game is 5 * 60 + 40 * 3 = 420 seconds.</string>
|
||||
<string name="durationFormula">(clock initial time) + 40 * (clock increment)</string>
|
||||
For instance, the estimated duration of a 5+3 game is 5 × 60 + 40 × 3 = 420 seconds.</string>
|
||||
<string name="durationFormula">(clock initial time) + 40 × (clock increment)</string>
|
||||
<string name="inferiorThanXsEqualYtimeControl">≤ %1$ss = %2$s</string>
|
||||
<string name="superiorThanXsEqualYtimeControl">≥ %1$ss = %2$s</string>
|
||||
<string name="whatVariantsCanIplay">What variants can I play on Lichess?</string>
|
||||
<string name="lichessSupportChessAnd">Lichess supports standard chess and %1$s.</string>
|
||||
<string name="eightVariants">8 chess variants</string>
|
||||
<string name="whatIsACPL">What is "average centipawn loss"/ACPL?</string>
|
||||
<string name="whatIsACPL">What is the average centipawn loss (ACPL)?</string>
|
||||
<string name="acplExplanation">The centipawn is the unit of measure used in chess as representation of the advantage. A centipawn is equal to 1/100th of a pawn. Therefore 100 centipawns = 1 pawn. These values play no formal role in the game but are useful to players, and essential in computer chess, for evaluating positions.
|
||||
|
||||
The top computer move will lose zero centipawns, but lesser moves will result in a deterioration of the position, measured in centipawns.
|
||||
The top computer move will lose zero centipawns, but lesser moves will result in a deterioration of the position, measured in centipawns.
|
||||
|
||||
This value can be used as an indicator of the quality of play. The fewer centipawns one loses per move, the stronger the play.
|
||||
This value can be used as an indicator of the quality of play. The fewer centipawns one loses per move, the stronger the play.
|
||||
|
||||
The computer analysis on Lichess is powered by Stockfish.</string>
|
||||
The computer analysis on Lichess is powered by Stockfish.</string>
|
||||
<string name="insufficientMaterial">Losing on time, drawing and insufficient material</string>
|
||||
<string name="lichessFollowFIDErules">In the event of one player running out of time, that player will usually lose the game. However, the game is drawn if the position is such that the opponent cannot checkmate the player's king by any possible series of legal moves (%1$s).
|
||||
|
||||
In rare cases this can be difficult to decide automatically (forced lines, fortresses). By default we always side with the player who did not run out of time.
|
||||
In rare cases this can be difficult to decide automatically (forced lines, fortresses). By default we always side with the player who did not run out of time.
|
||||
|
||||
Note that it can be possible to mate with a single knight or bishop if the opponent has a piece that could block the king.</string>
|
||||
<string name="linkToFIDErules">FIDE handbook §6.9, pdf</string>
|
||||
Note that it can be possible to mate with a single knight or bishop if the opponent has a piece that could block the king.</string>
|
||||
<string name="linkToFIDErules">FIDE handbook §6.9, PDF</string>
|
||||
<string name="discoveringEnPassant">Why can a pawn capture another pawn when it is already passed? (en passant)</string>
|
||||
<string name="explainingEnPassant">This is a legal move known as "en passant". The Wikipedia article gives a %1$s.
|
||||
|
||||
It is described in section 3.7 (d) of the %2$s:
|
||||
It is described in section 3.7 (d) of the %2$s:
|
||||
|
||||
"A pawn occupying a square on the same rank as and on an adjacent file to an opponent’s pawn which has just advanced two squares in one move from its original square may capture this opponent’s pawn as though the latter had been moved only one square. This capture is only legal on the move following this advance and is called an ‘en passant’ capture."
|
||||
"A pawn occupying a square on the same rank as and on an adjacent file to an opponent’s pawn which has just advanced two squares in one move from its original square may capture this opponent’s pawn as though the latter had been moved only one square. This capture is only legal on the move following this advance and is called an ‘en passant’ capture."
|
||||
|
||||
See the %3$s on this move for some practice with it.
|
||||
</string>
|
||||
See the %3$s on this move for some practice with it.</string>
|
||||
<string name="goodIntroduction">good introduction</string>
|
||||
<string name="officialRulesPDF">official rules (pdf)</string>
|
||||
<string name="officialRulesPDF">official rules (PDF)</string>
|
||||
<string name="lichessTraining">Lichess training</string>
|
||||
<string name="threefoldRepetition">Threefold repetition</string>
|
||||
<string name="threefoldRepetitionExplanation">If a position occurs three times, players can claim a draw by %1$s. Lichess implements the official FIDE rules, as described in Article 9.2 (d) of the %2$s.</string>
|
||||
<string name="threefoldRepetitionLowerCase">threefold repetition</string>
|
||||
<string name="handBookPDF">handbook (pdf)</string>
|
||||
<string name="handBookPDF">handbook (PDF)</string>
|
||||
<string name="notRepeatedMoves">We did not repeat moves. Why was the game still drawn by repetition?</string>
|
||||
<string name="repeatedPositionsThatMatters">Threefold repetition is about repeated %1$s, not moves. Repetition does not have to occur consecutively.</string>
|
||||
<string name="positions">positions</string>
|
||||
|
@ -78,19 +78,18 @@
|
|||
<string name="lichessRecognizeAllOTBtitles">Lichess recognises all FIDE titles gained from OTB (over the board) play, as well as %1$s. Here is a list of FIDE titles:</string>
|
||||
<string name="showYourTitle">If you have an OTB title, you can apply to have this displayed on your account by completing the %1$s, including a clear image of an identifying document/card and a selfie of you holding the document/card.
|
||||
|
||||
Verifying as a titled player on Lichess gives access to play in the Titled Arena events.
|
||||
Verifying as a titled player on Lichess gives access to play in the Titled Arena events.
|
||||
|
||||
Finally there is an honorary %2$s title.</string>
|
||||
Finally there is an honorary %2$s title.</string>
|
||||
<string name="asWellAsManyNMtitles">many national master titles</string>
|
||||
<string name="verificationForm">verification form</string>
|
||||
<string name="lichessMasterLM">Lichess Master (LM)</string>
|
||||
<string name="canIbecomeLM">Can I get the Lichess Master (LM) title?</string>
|
||||
<string name="noUpperCaseDot">No.</string>
|
||||
<string name="lMtitleComesToYouDoNotRequestIt">This honorific title is unofficial and only exists on Lichess.
|
||||
|
||||
We rarely award it to highly notable players who are good citizens of Lichess, at our discretion. You don't get the LM title, the LM title gets to you. If you qualify, you will get a message from us regarding it and the choice to accept or decline.
|
||||
We rarely award it to highly notable players who are good citizens of Lichess, at our discretion. You don't get the LM title, the LM title gets to you. If you qualify, you will get a message from us regarding it and the choice to accept or decline.
|
||||
|
||||
Do not request to get the LM title.</string>
|
||||
Do not ask for the LM title.</string>
|
||||
<string name="whatUsernameCanIchoose">What can my username be?</string>
|
||||
<string name="usernamesNotOffensive">In general, usernames should not be: offensive, impersonating someone else, or advertising. You can read more about the %1$s.</string>
|
||||
<string name="guidelines">guidelines</string>
|
||||
|
@ -103,11 +102,11 @@
|
|||
<string name="whichRatingSystemUsedByLichess">What rating system does Lichess use?</string>
|
||||
<string name="ratingSystemUsedByLichess">Ratings are calculated using the Glicko-2 rating method developed by Mark Glickman. This is a very popular rating method, and is used by a significant number of chess organisations (FIDE being a notable counter-example, as they still use the dated Elo rating system).
|
||||
|
||||
Fundamentally, Glicko ratings use "confidence intervals" when calculating and representing your rating. When you first start using the site, your rating starts at 1500 ± 700. The 1500 represents your rating, and the 700 represents the confidence interval.
|
||||
Fundamentally, Glicko ratings use "confidence intervals" when calculating and representing your rating. When you first start using the site, your rating starts at 1500 ± 700. The 1500 represents your rating, and the 700 represents the confidence interval.
|
||||
|
||||
Basically, the system is 90% sure that your rating is somewhere between 800 and 2200. It is incredibly uncertain. Because of this, when a player is just starting out, their rating will change very dramatically, potentially several hundred points at a time. But after some games against established players the confidence interval will narrow, and the amount of points gained/lost after each game will decrease.
|
||||
Basically, the system is 90% sure that your rating is somewhere between 800 and 2200. It is incredibly uncertain. Because of this, when a player is just starting out, their rating will change very dramatically, potentially several hundred points at a time. But after some games against established players the confidence interval will narrow, and the amount of points gained/lost after each game will decrease.
|
||||
|
||||
Another point to note is that, as time passes, the confidence interval will increase. This allows you to gain/lose points points more rapidly to match any changes in your skill level over that time.</string>
|
||||
Another point to note is that, as time passes, the confidence interval will increase. This allows you to gain/lose points points more rapidly to match any changes in your skill level over that time.</string>
|
||||
<string name="whatIsProvisionalRating">Why is there a question mark (?) next to a rating?</string>
|
||||
<string name="provisionalRatingExplanation">The question mark means the rating is provisional. Reasons include:</string>
|
||||
<string name="notPlayedEnoughRatedGamesAgainstX">The player has not yet finished enough rated games against %1$s in the rating category.</string>
|
||||
|
@ -116,7 +115,7 @@
|
|||
<string name="ratingDeviationMorethanOneHundredTen">Concretely, it means that the Glicko-2 deviation is greater than 110. The deviation is the level of confidence the system has in the rating. The lower the deviation, the more stable is a rating.</string>
|
||||
<string name="howDoLeaderoardsWork">How do ranks and leaderboards work?</string>
|
||||
<string name="inOrderToAppearsYouMust">In order to get on the %1$s you must:</string>
|
||||
<string name="ratingLeaderboards">rating leaderboards:</string>
|
||||
<string name="ratingLeaderboards">rating leaderboard</string>
|
||||
<string name="havePlayedMoreThanThirtyGamesInThatRating">have played at least 30 rated games in a given rating,</string>
|
||||
<string name="havePlayedARatedGameAtLeastOneWeekAgo">have played a rated game within the last week for this rating,</string>
|
||||
<string name="ratingDeviationLowerThanXinChessYinVariants">have a rating deviation lower than %1$s, in standard chess, and lower than %2$s in variants,</string>
|
||||
|
@ -125,7 +124,7 @@
|
|||
<string name="whyAreRatingHigher">Why are ratings higher compared to other sites and organisations such as FIDE, USCF and the ICC?</string>
|
||||
<string name="whyAreRatingHigherExplanation">It is best not to think of ratings as absolute numbers, or compare them against other organisations. Different organisations have different levels of players, different rating systems (Elo, Glicko, Glicko-2, or a modified version of the aforementioned). These factors can drastically affect the absolute numbers (ratings).
|
||||
|
||||
It's best to think of ratings as "relative" figures (as opposed to "absolute" figures): Within a pool of players, their relative differences in ratings will help you estimate who will win/draw/lose, and how often. Saying "I have X rating" means nothing unless there are other players to compare that rating to.</string>
|
||||
It's best to think of ratings as "relative" figures (as opposed to "absolute" figures): Within a pool of players, their relative differences in ratings will help you estimate who will win/draw/lose, and how often. Saying "I have X rating" means nothing unless there are other players to compare that rating to.</string>
|
||||
<string name="howToHideRatingWhilePlaying">How to hide ratings while playing?</string>
|
||||
<string name="enableZenMode">Enable Zen-mode in the %1$s, or by pressins %2$s during a game.</string>
|
||||
<string name="displayPreferences">display preferences</string>
|
||||
|
@ -136,7 +135,7 @@
|
|||
<string name="viewSiteInformationPopUp">View site information popup</string>
|
||||
<string name="lichessCanOptionnalySendPopUps">Lichess can optionally send popup notifications, for example when it is your turn or you received a private message.
|
||||
|
||||
Click the lock icon next to the lichess.org address in the URL bar of your browser.
|
||||
Click the lock icon next to the lichess.org address in the URL bar of your browser.
|
||||
|
||||
Then select whether to allow or block notifications from Lichess.</string>
|
||||
Then select whether to allow or block notifications from Lichess.</string>
|
||||
</resources>
|
||||
|
|
|
@ -859,7 +859,7 @@ computer analysis, game chat and shareable URL.</string>
|
|||
<string name="toRequestSupport">To request support, %1$s</string>
|
||||
<string name="tryTheContactPage">try the contact page</string>
|
||||
<string name="thisTopicIsArchived">This topic has been archived and can no longer be replied to.</string>
|
||||
<string name="joinTheTeamXToPost">Join the %$1s, to post in this forum</string>
|
||||
<string name="joinTheTeamXToPost">Join the %1$s, to post in this forum</string>
|
||||
<string name="teamNamedX">%1$s team</string>
|
||||
<string name="youCannotPostYetPlaySomeGames">You can't post in the forums yet. Play some games!</string>
|
||||
<string name="subscribe">Subscribe</string>
|
||||
|
|
|
@ -15,7 +15,10 @@
|
|||
<string name="anyoneCanJoin">Free for all</string>
|
||||
<string name="aConfirmationIsRequiredToJoin">A confirmation is required to join</string>
|
||||
<string name="joiningPolicy">Joining policy</string>
|
||||
<string name="teamLeaders">Team leaders</string>
|
||||
<plurals name="teamLeaders">
|
||||
<item quantity="one">Team leader</item>
|
||||
<item quantity="other">Team leaders</item>
|
||||
</plurals>
|
||||
<string name="teamBestPlayers">Best players</string>
|
||||
<string name="teamRecentMembers">Recent members</string>
|
||||
<string name="kickSomeone">Kick someone out of the team</string>
|
||||
|
@ -32,4 +35,11 @@
|
|||
<string name="teamTournamentOverview">An Arena tournament that only members of your team can join</string>
|
||||
<string name="messageAllMembers">Message all members</string>
|
||||
<string name="messageAllMembersOverview">Send a private message to every member of the team</string>
|
||||
<string name="messageAllMembersLongDescription">Send a private message to ALL members of the team.
|
||||
You can use this to call players to join a tournament or a team battle.
|
||||
Players who don't like receiving your messages might leave the team.</string>
|
||||
<string name="teamsIlead">Teams I lead</string>
|
||||
<string name="youWayWantToLinkOneOfTheseTournaments">You may want to link one of these upcoming tournaments?</string>
|
||||
<string name="usersWhoCanManageThisTeam">Users who can manage this team</string>
|
||||
<string name="leadersChat">Leaders chat</string>
|
||||
</resources>
|
||||
|
|
|
@ -53,9 +53,13 @@
|
|||
opacity: 1!important;
|
||||
}
|
||||
}
|
||||
a.loss {
|
||||
opacity: .2;
|
||||
}
|
||||
&.current a {
|
||||
background: mix($c-accent, $c-bg-box, 70%);
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
&.new {
|
||||
border: $c-border;
|
||||
|
|
|
@ -360,7 +360,7 @@ lichess.redirect = function(obj) {
|
|||
lichess.reload = function() {
|
||||
if (lichess.redirectInProgress) return;
|
||||
lichess.hasToReload = true;
|
||||
lichess.socket.destroy();
|
||||
lichess.socket.disconnect();
|
||||
if (location.hash) location.reload();
|
||||
else location.href = location.href;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue