Merge branch 'master' into puzzle-dashboard-api
* master: api endpoint to give opponent more time - closes #7955 simplify contact page, remove duplicate report entries - fixes #7962 show swiss streamers - closes #7485 fix #7958 refactor socket redis sender feature tournaments for up to 24h add broadcast event icon and fix event icons styles - closes #7964 New translations: puzzleTheme.xml (Basque) (#7960) always send game messages on the same redis channelpuzzle-dashboard-api
commit
652c173e1d
|
@ -2,6 +2,7 @@ package controllers
|
|||
|
||||
import play.api.libs.json._
|
||||
import play.api.mvc._
|
||||
import scala.concurrent.duration._
|
||||
import views._
|
||||
|
||||
import lila.api.Context
|
||||
|
@ -350,4 +351,17 @@ final class Round(
|
|||
html.game.mini(_)
|
||||
)
|
||||
}
|
||||
|
||||
def apiAddTime(anyId: String, seconds: Int) =
|
||||
Scoped(_.Challenge.Write) { implicit req => me =>
|
||||
import lila.round.actorApi.round.Moretime
|
||||
if (seconds < 1 || seconds > 86400) BadRequest.fuccess
|
||||
else
|
||||
env.round.proxyRepo.game(lila.game.Game takeGameId anyId) map {
|
||||
_.flatMap { Pov(_, me) }.?? { pov =>
|
||||
env.round.tellRound(pov.gameId, Moretime(pov.typedPlayerId, seconds.seconds))
|
||||
jsonOkResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,8 +53,9 @@ final class Swiss(
|
|||
_ <- chat ?? { c =>
|
||||
env.user.lightUserApi.preloadMany(c.chat.userIds)
|
||||
}
|
||||
streamers <- streamerCache get swiss.id
|
||||
isLocalMod <- canChat ?? canModChat(swiss)
|
||||
} yield Ok(html.swiss.show(swiss, verdicts, json, chat, isLocalMod))
|
||||
} yield Ok(html.swiss.show(swiss, verdicts, json, chat, streamers, isLocalMod))
|
||||
},
|
||||
api = _ =>
|
||||
swissOption.fold(notFoundJson("No such swiss tournament")) { swiss =>
|
||||
|
@ -274,4 +275,15 @@ final class Swiss(
|
|||
private def canModChat(swiss: SwissModel)(implicit ctx: Context): Fu[Boolean] =
|
||||
if (isGranted(_.ChatTimeout)) fuTrue
|
||||
else ctx.userId ?? { env.team.cached.isLeader(swiss.teamId, _) }
|
||||
|
||||
private val streamerCache =
|
||||
env.memo.cacheApi[SwissModel.Id, List[lila.user.User.ID]](64, "swiss.streamers") {
|
||||
_.refreshAfterWrite(15.seconds)
|
||||
.maximumSize(64)
|
||||
.buildAsyncFuture { id =>
|
||||
env.streamer.liveStreamApi.all.flatMap { streams =>
|
||||
env.swiss.api.filterPlaying(id, streams.streams.map(_.streamer.userId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import play.api.data.Form
|
|||
import lila.api.Context
|
||||
import lila.app.templating.Environment._
|
||||
import lila.app.ui.ScalatagsTemplate._
|
||||
import lila.event.EventForm
|
||||
import lila.event.{ Event, EventForm }
|
||||
|
||||
object event {
|
||||
|
||||
|
@ -20,7 +20,7 @@ object event {
|
|||
)
|
||||
}
|
||||
|
||||
def edit(event: lila.event.Event, form: Form[_])(implicit ctx: Context) =
|
||||
def edit(event: Event, form: Form[_])(implicit ctx: Context) =
|
||||
layout(title = event.title, css = "mod.form") {
|
||||
div(cls := "crud edit page-menu__content box box-pad")(
|
||||
div(cls := "box__top")(
|
||||
|
@ -37,7 +37,14 @@ object event {
|
|||
)
|
||||
}
|
||||
|
||||
def show(e: lila.event.Event)(implicit ctx: Context) =
|
||||
def iconOf(e: Event) =
|
||||
e.icon match {
|
||||
case None => i(cls := "img", dataIcon := "")
|
||||
case Some(c) if c == EventForm.icon.broadcast => i(cls := "img", dataIcon := "")
|
||||
case Some(c) => img(cls := "img", src := assetUrl(s"images/$c"))
|
||||
}
|
||||
|
||||
def show(e: Event)(implicit ctx: Context) =
|
||||
views.html.base.layout(
|
||||
title = e.title,
|
||||
moreCss = cssTag("event"),
|
||||
|
@ -45,9 +52,7 @@ object event {
|
|||
) {
|
||||
main(cls := "page-small event box box-pad")(
|
||||
div(cls := "box__top")(
|
||||
e.icon map { i =>
|
||||
img(cls := "img", src := assetUrl(s"images/$i"))
|
||||
} getOrElse i(cls := "img", dataIcon := ""),
|
||||
iconOf(e),
|
||||
div(
|
||||
h1(e.title),
|
||||
strong(cls := "headline")(e.headline)
|
||||
|
@ -67,7 +72,7 @@ object event {
|
|||
)
|
||||
}
|
||||
|
||||
def manager(events: List[lila.event.Event])(implicit ctx: Context) = {
|
||||
def manager(events: List[Event])(implicit ctx: Context) = {
|
||||
val title = "Event manager"
|
||||
layout(title = title) {
|
||||
div(cls := "crud page-menu__content box")(
|
||||
|
@ -134,7 +139,7 @@ object event {
|
|||
frag("Icon"),
|
||||
half = true,
|
||||
help = frag("Displayed on the homepage button").some
|
||||
)(form3.select(_, EventForm.iconChoices))
|
||||
)(form3.select(_, EventForm.icon.choices))
|
||||
),
|
||||
form3.group(
|
||||
form("headline"),
|
||||
|
|
|
@ -172,9 +172,7 @@ object bits {
|
|||
"invert" -> e.isNowOrSoon
|
||||
)
|
||||
)(
|
||||
e.icon map { i =>
|
||||
img(cls := "img", src := assetUrl(s"images/$i"))
|
||||
} getOrElse i(cls := "img", dataIcon := ""),
|
||||
views.html.event.iconOf(e),
|
||||
span(cls := "content")(
|
||||
span(cls := "name")(e.title),
|
||||
span(cls := "headline")(e.headline),
|
||||
|
|
|
@ -57,21 +57,13 @@ object dashboard {
|
|||
)
|
||||
}
|
||||
) { dash =>
|
||||
frag(
|
||||
dash.mostPlayed.size > 2 option
|
||||
div(cls := s"${baseClass}__global")(
|
||||
metricsOf(days, PuzzleTheme.mix.key, dash.global),
|
||||
canvas(cls := s"${baseClass}__radar")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// data: {
|
||||
// labels: ['Running', 'Swimming', 'Eating', 'Cycling'],
|
||||
// datasets: [{
|
||||
// data: [20, 10, 4, 2]
|
||||
// }]
|
||||
// }
|
||||
|
||||
def improvementAreas(user: User, dashOpt: Option[PuzzleDashboard], days: Int)(implicit ctx: Context) =
|
||||
dashboardLayout(
|
||||
user = user,
|
||||
|
@ -83,7 +75,7 @@ object dashboard {
|
|||
subtitle = "Train these to optimize your progress!",
|
||||
dashOpt = dashOpt
|
||||
) { dash =>
|
||||
themeSelection(days, dash.weakThemes)
|
||||
dash.weakThemes.nonEmpty option themeSelection(days, dash.weakThemes)
|
||||
}
|
||||
|
||||
def strengths(user: User, dashOpt: Option[PuzzleDashboard], days: Int)(implicit ctx: Context) =
|
||||
|
@ -97,7 +89,7 @@ object dashboard {
|
|||
subtitle = "You perform the best in these themes",
|
||||
dashOpt = dashOpt
|
||||
) { dash =>
|
||||
themeSelection(days, dash.strongThemes)
|
||||
dash.strongThemes.nonEmpty option themeSelection(days, dash.strongThemes)
|
||||
}
|
||||
|
||||
private def dashboardLayout(
|
||||
|
@ -109,7 +101,7 @@ object dashboard {
|
|||
dashOpt: Option[PuzzleDashboard],
|
||||
moreJs: Frag = emptyFrag
|
||||
)(
|
||||
body: PuzzleDashboard => Frag
|
||||
body: PuzzleDashboard => Option[Frag]
|
||||
)(implicit ctx: Context) =
|
||||
views.html.base.layout(
|
||||
title = title,
|
||||
|
@ -136,13 +128,10 @@ object dashboard {
|
|||
}
|
||||
)
|
||||
),
|
||||
dashOpt match {
|
||||
case None =>
|
||||
div(cls := s"${baseClass}__empty")(
|
||||
a(href := routes.Puzzle.home())("Nothing to show, go play some puzzles first!")
|
||||
)
|
||||
case Some(dash) => body(dash)
|
||||
}
|
||||
dashOpt.flatMap(body) |
|
||||
div(cls := s"${baseClass}__empty")(
|
||||
a(href := routes.Puzzle.home())("Nothing to show, go play some puzzles first!")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package views
|
||||
package html.site
|
||||
|
||||
import controllers.routes
|
||||
import scala.util.chaining._
|
||||
|
||||
import controllers.routes
|
||||
import lila.api.Context
|
||||
import lila.app.templating.Environment._
|
||||
import lila.app.ui.ScalatagsTemplate._
|
||||
|
@ -149,39 +149,27 @@ object contact {
|
|||
)
|
||||
)
|
||||
),
|
||||
Branch(
|
||||
Leaf(
|
||||
"report",
|
||||
wantReport(),
|
||||
List(
|
||||
"cheating" -> reportCheating,
|
||||
"sandbagging" -> reportSandbagging,
|
||||
"trolling" -> reportTrolling,
|
||||
"insults" -> reportInsults,
|
||||
"some other reason" -> reportOtherReason
|
||||
).map { case (reason, name) =>
|
||||
Leaf(
|
||||
reason,
|
||||
name(),
|
||||
frag(
|
||||
p(
|
||||
a(href := routes.Report.form())(toReportAPlayer(name())),
|
||||
"."
|
||||
),
|
||||
p(
|
||||
youCanAlsoReachReportPage(button(cls := "thin button button-empty", dataIcon := "!"))
|
||||
),
|
||||
p(
|
||||
doNotMessageModerators(),
|
||||
br,
|
||||
doNotReportInForum(),
|
||||
br,
|
||||
doNotSendReportEmails(),
|
||||
br,
|
||||
onlyReports()
|
||||
)
|
||||
)
|
||||
frag(
|
||||
p(
|
||||
a(href := routes.Report.form())(toReportAPlayerUseForm()),
|
||||
"."
|
||||
),
|
||||
p(
|
||||
youCanAlsoReachReportPage(button(cls := "thin button button-empty", dataIcon := "!"))
|
||||
),
|
||||
p(
|
||||
doNotMessageModerators(),
|
||||
br,
|
||||
doNotReportInForum(),
|
||||
br,
|
||||
doNotSendReportEmails(),
|
||||
br,
|
||||
onlyReports()
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
Branch(
|
||||
"bug",
|
||||
|
|
|
@ -17,6 +17,7 @@ object show {
|
|||
verdicts: SwissCondition.All.WithVerdicts,
|
||||
data: play.api.libs.json.JsObject,
|
||||
chatOption: Option[lila.chat.UserChat.Mine],
|
||||
streamers: List[lila.user.User.ID],
|
||||
isLocalMod: Boolean
|
||||
)(implicit ctx: Context): Frag = {
|
||||
val isDirector = ctx.userId.has(s.createdBy)
|
||||
|
@ -66,7 +67,7 @@ object show {
|
|||
)(
|
||||
main(cls := "swiss")(
|
||||
st.aside(cls := "swiss__side")(
|
||||
swiss.side(s, verdicts, chatOption.isDefined)
|
||||
swiss.side(s, verdicts, streamers, chatOption.isDefined)
|
||||
),
|
||||
div(cls := "swiss__main")(div(cls := "box"))
|
||||
)
|
||||
|
|
|
@ -13,7 +13,12 @@ object side {
|
|||
|
||||
private val separator = " • "
|
||||
|
||||
def apply(s: Swiss, verdicts: SwissCondition.All.WithVerdicts, chat: Boolean)(implicit
|
||||
def apply(
|
||||
s: Swiss,
|
||||
verdicts: SwissCondition.All.WithVerdicts,
|
||||
streamers: List[lila.user.User.ID],
|
||||
chat: Boolean
|
||||
)(implicit
|
||||
ctx: Context
|
||||
) =
|
||||
frag(
|
||||
|
@ -88,6 +93,9 @@ object side {
|
|||
else br,
|
||||
absClientDateTime(s.startsAt)
|
||||
),
|
||||
streamers.nonEmpty option div(cls := "context-streamers")(
|
||||
streamers map views.html.streamer.bits.contextual
|
||||
),
|
||||
chat option views.html.chat.frag
|
||||
)
|
||||
}
|
||||
|
|
|
@ -339,7 +339,6 @@ gameSearch {
|
|||
actor.name = game-search
|
||||
}
|
||||
round {
|
||||
moretime = 15 seconds
|
||||
collection {
|
||||
note = game_note
|
||||
forecast = forecast
|
||||
|
|
|
@ -603,7 +603,8 @@ POST /api/challenge/:user controllers.Challenge.apiCreate(user: Str
|
|||
POST /api/challenge/$id<\w{8}>/accept controllers.Challenge.apiAccept(id: String)
|
||||
POST /api/challenge/$id<\w{8}>/decline controllers.Challenge.apiDecline(id: String)
|
||||
POST /api/challenge/$id<\w{8}>/cancel controllers.Challenge.apiCancel(id: String)
|
||||
POST /api/challenge/$id<\w{8}>/start-clocks controllers.Challenge.apiStartClocks(id: String)
|
||||
POST /api/challenge/$id<\w{8}>/start-clocks controllers.Challenge.apiStartClocks(id: String)
|
||||
POST /api/round/$id<\w{8}>/add-time/:seconds controllers.Round.apiAddTime(id: String, seconds: Int)
|
||||
GET /api/cloud-eval controllers.Api.cloudEval
|
||||
GET /api/broadcast controllers.Relay.apiIndex
|
||||
POST /api/import controllers.Importer.apiSendGame
|
||||
|
|
|
@ -12,13 +12,17 @@ import lila.user.User
|
|||
|
||||
object EventForm {
|
||||
|
||||
val iconChoices = List(
|
||||
"" -> "Microphone",
|
||||
"lichess.event.png" -> "Lichess",
|
||||
"trophy.event.png" -> "Trophy",
|
||||
"offerspill.logo.png" -> "Offerspill"
|
||||
)
|
||||
val imageDefault = ""
|
||||
object icon {
|
||||
val default = ""
|
||||
val broadcast = "broadcast.icon"
|
||||
val choices = List(
|
||||
default -> "Microphone",
|
||||
"lichess.event.png" -> "Lichess",
|
||||
"trophy.event.png" -> "Trophy",
|
||||
broadcast -> "Broadcast",
|
||||
"offerspill.logo.png" -> "Offerspill"
|
||||
)
|
||||
}
|
||||
|
||||
val form = Form(
|
||||
mapping(
|
||||
|
@ -35,7 +39,7 @@ object EventForm {
|
|||
lila.user.UserForm.historicalUsernameField
|
||||
.transform[User.ID](_.toLowerCase, identity)
|
||||
},
|
||||
"icon" -> stringIn(iconChoices),
|
||||
"icon" -> stringIn(icon.choices),
|
||||
"countdown" -> boolean
|
||||
)(Data.apply)(Data.unapply)
|
||||
) fill Data(
|
||||
|
|
|
@ -9,6 +9,8 @@ case class Pov(game: Game, color: Color) {
|
|||
|
||||
def playerId = player.id
|
||||
|
||||
def typedPlayerId = Game.PlayerId(player.id)
|
||||
|
||||
def fullId = game fullIdOf color
|
||||
|
||||
def gameId = game.id
|
||||
|
|
|
@ -1225,12 +1225,7 @@ val `orCloseAccount` = new I18nKey("contact:orCloseAccount")
|
|||
val `wantClearHistory` = new I18nKey("contact:wantClearHistory")
|
||||
val `cantClearHistory` = new I18nKey("contact:cantClearHistory")
|
||||
val `wantReport` = new I18nKey("contact:wantReport")
|
||||
val `reportCheating` = new I18nKey("contact:reportCheating")
|
||||
val `reportSandbagging` = new I18nKey("contact:reportSandbagging")
|
||||
val `reportTrolling` = new I18nKey("contact:reportTrolling")
|
||||
val `reportInsults` = new I18nKey("contact:reportInsults")
|
||||
val `reportOtherReason` = new I18nKey("contact:reportOtherReason")
|
||||
val `toReportAPlayer` = new I18nKey("contact:toReportAPlayer")
|
||||
val `toReportAPlayerUseForm` = new I18nKey("contact:toReportAPlayerUseForm")
|
||||
val `youCanAlsoReachReportPage` = new I18nKey("contact:youCanAlsoReachReportPage")
|
||||
val `doNotReportInForum` = new I18nKey("contact:doNotReportInForum")
|
||||
val `doNotSendReportEmails` = new I18nKey("contact:doNotSendReportEmails")
|
||||
|
|
|
@ -19,8 +19,7 @@ import lila.user.User
|
|||
private class RoundConfig(
|
||||
@ConfigName("collection.note") val noteColl: CollName,
|
||||
@ConfigName("collection.forecast") val forecastColl: CollName,
|
||||
@ConfigName("collection.alarm") val alarmColl: CollName,
|
||||
@ConfigName("moretime") val moretimeDuration: MoretimeDuration
|
||||
@ConfigName("collection.alarm") val alarmColl: CollName
|
||||
)
|
||||
|
||||
@Module
|
||||
|
@ -59,7 +58,6 @@ final class Env(
|
|||
scheduler: akka.actor.Scheduler
|
||||
) {
|
||||
|
||||
implicit private val moretimeLoader = durationLoader(MoretimeDuration.apply)
|
||||
implicit private val animationLoader = durationLoader(AnimationDuration.apply)
|
||||
private val config = appConfig.get[RoundConfig]("round")(AutoConfig.loader)
|
||||
|
||||
|
|
|
@ -21,14 +21,11 @@ final class JsonView(
|
|||
moretimer: Moretimer,
|
||||
divider: lila.game.Divider,
|
||||
evalCache: lila.evalCache.EvalCacheApi,
|
||||
isOfferingRematch: Pov => Boolean,
|
||||
moretime: MoretimeDuration
|
||||
isOfferingRematch: Pov => Boolean
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
||||
import JsonView._
|
||||
|
||||
private val moretimeSeconds = moretime.value.toSeconds.toInt
|
||||
|
||||
private def checkCount(game: Game, color: Color) =
|
||||
(game.variant == chess.variant.ThreeCheck) option game.history.checkCount(color)
|
||||
|
||||
|
@ -263,7 +260,7 @@ final class JsonView(
|
|||
}
|
||||
|
||||
private def clockJson(clock: Clock): JsObject =
|
||||
clockWriter.writes(clock) + ("moretime" -> JsNumber(moretimeSeconds))
|
||||
clockWriter.writes(clock) + ("moretime" -> JsNumber(actorApi.round.Moretime.defaultDuration.toSeconds))
|
||||
|
||||
private def possibleMoves(pov: Pov, apiVersion: ApiVersion): Option[JsValue] =
|
||||
(pov.game playableBy pov.player) option
|
||||
|
|
|
@ -4,15 +4,15 @@ import chess.Color
|
|||
|
||||
import lila.game.{ Event, Game, Pov, Progress }
|
||||
import lila.pref.{ Pref, PrefApi }
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
final private class Moretimer(
|
||||
messenger: Messenger,
|
||||
prefApi: PrefApi,
|
||||
duration: MoretimeDuration
|
||||
prefApi: PrefApi
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
||||
// pov of the player giving more time
|
||||
def apply(pov: Pov): Fu[Option[Progress]] =
|
||||
def apply(pov: Pov, duration: FiniteDuration): Fu[Option[Progress]] =
|
||||
IfAllowed(pov.game) {
|
||||
(pov.game moretimeable !pov.color) ?? {
|
||||
if (pov.game.hasClock) give(pov.game, List(!pov.color), duration).some
|
||||
|
@ -29,14 +29,14 @@ final private class Moretimer(
|
|||
if (game.isMandatory) fuFalse
|
||||
else isAllowedByPrefs(game)
|
||||
|
||||
private[round] def give(game: Game, colors: List[Color], duration: MoretimeDuration): Progress =
|
||||
private[round] def give(game: Game, colors: List[Color], duration: FiniteDuration): Progress =
|
||||
game.clock.fold(Progress(game)) { clock =>
|
||||
val centis = duration.value.toCentis
|
||||
val centis = duration.toCentis
|
||||
val newClock = colors.foldLeft(clock) { case (c, color) =>
|
||||
c.giveTime(color, centis)
|
||||
}
|
||||
colors.foreach { c =>
|
||||
messenger.volatile(game, s"$c + ${duration.value.toSeconds} seconds")
|
||||
messenger.volatile(game, s"$c + ${duration.toSeconds} seconds")
|
||||
}
|
||||
(game withClock newClock) ++ colors.map { Event.ClockInc(_, centis) }
|
||||
}
|
||||
|
|
|
@ -366,9 +366,9 @@ final private[round] class RoundDuct(
|
|||
}
|
||||
}
|
||||
|
||||
case Moretime(playerId) =>
|
||||
case Moretime(playerId, duration) =>
|
||||
handle(playerId) { pov =>
|
||||
moretimer(pov) flatMap {
|
||||
moretimer(pov, duration) flatMap {
|
||||
_ ?? { progress =>
|
||||
proxy save progress inject progress.events
|
||||
}
|
||||
|
@ -404,7 +404,7 @@ final private[round] class RoundDuct(
|
|||
handle { game =>
|
||||
game.playable ?? {
|
||||
messenger.system(game, "Lichess has been updated! Sorry for the inconvenience.")
|
||||
val progress = moretimer.give(game, Color.all, MoretimeDuration(20 seconds))
|
||||
val progress = moretimer.give(game, Color.all, 20 seconds)
|
||||
proxy save progress inject progress.events
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ final class RoundSocket(
|
|||
val duct = new RoundDuct(
|
||||
dependencies = roundDependencies,
|
||||
gameId = id,
|
||||
socketSend = send
|
||||
socketSend = sendForGameId(id)
|
||||
)(ec, proxy)
|
||||
terminationDelay schedule Game.Id(id)
|
||||
duct.getGame dforeach {
|
||||
|
@ -160,17 +160,19 @@ final class RoundSocket(
|
|||
private def finishRound(gameId: Game.Id): Unit =
|
||||
rounds.terminate(gameId.value, _ ! RoundDuct.Stop)
|
||||
|
||||
private lazy val send: String => Unit = remoteSocketApi.makeSender("r-out", parallelism = 8).apply _
|
||||
private lazy val send: Sender = remoteSocketApi.makeSender("r-out", parallelism = 8)
|
||||
|
||||
private lazy val sendForGameId: Game.ID => String => Unit = gameId => msg => send.sticky(gameId, msg)
|
||||
|
||||
remoteSocketApi.subscribeRoundRobin("r-in", Protocol.In.reader, parallelism = 8)(
|
||||
roundHandler orElse remoteSocketApi.baseHandler
|
||||
) >>- send(P.Out.boot)
|
||||
|
||||
Bus.subscribeFun("tvSelect", "roundSocket", "tourStanding", "startGame", "finishGame") {
|
||||
case TvSelect(gameId, speed, json) => send(Protocol.Out.tvSelect(gameId, speed, json))
|
||||
case TvSelect(gameId, speed, json) => sendForGameId(gameId)(Protocol.Out.tvSelect(gameId, speed, json))
|
||||
case Tell(gameId, e @ BotConnected(color, v)) =>
|
||||
rounds.tell(gameId, e)
|
||||
send(Protocol.Out.botConnected(gameId, color, v))
|
||||
sendForGameId(gameId)(Protocol.Out.botConnected(gameId, color, v))
|
||||
case Tell(gameId, msg) => rounds.tell(gameId, msg)
|
||||
case TellIfExists(gameId, msg) => rounds.tellIfPresent(gameId, msg)
|
||||
case TellMany(gameIds, msg) => rounds.tellIds(gameIds, msg)
|
||||
|
@ -179,11 +181,11 @@ final class RoundSocket(
|
|||
case TourStanding(tourId, json) => send(Protocol.Out.tourStanding(tourId, json))
|
||||
case lila.game.actorApi.StartGame(game) if game.hasClock =>
|
||||
game.userIds.some.filter(_.nonEmpty) foreach { usersPlaying =>
|
||||
send(Protocol.Out.startGame(usersPlaying))
|
||||
sendForGameId(game.id)(Protocol.Out.startGame(usersPlaying))
|
||||
}
|
||||
case lila.game.actorApi.FinishGame(game, _, _) if game.hasClock =>
|
||||
game.userIds.some.filter(_.nonEmpty) foreach { usersPlaying =>
|
||||
send(Protocol.Out.finishGame(game.id, game.winnerColor, usersPlaying))
|
||||
sendForGameId(game.id)(Protocol.Out.finishGame(game.id, game.winnerColor, usersPlaying))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package lila.round
|
|||
package actorApi
|
||||
|
||||
import scala.concurrent.Promise
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import chess.format.Uci
|
||||
import chess.{ Color, MoveMetrics }
|
||||
|
@ -48,7 +49,8 @@ package round {
|
|||
case class DrawNo(playerId: PlayerId)
|
||||
case class TakebackYes(playerId: PlayerId)
|
||||
case class TakebackNo(playerId: PlayerId)
|
||||
case class Moretime(playerId: PlayerId)
|
||||
object Moretime { val defaultDuration = 15.seconds }
|
||||
case class Moretime(playerId: PlayerId, seconds: FiniteDuration = Moretime.defaultDuration)
|
||||
case object QuietFlag
|
||||
case class ClientFlag(color: Color, fromPlayerId: Option[PlayerId])
|
||||
case object Abandon
|
||||
|
|
|
@ -6,7 +6,6 @@ import lila.game.{ Game, Pov }
|
|||
import lila.user.User
|
||||
import play.api.libs.json.JsObject
|
||||
|
||||
private case class MoretimeDuration(value: FiniteDuration) extends AnyVal
|
||||
private case class AnimationDuration(value: FiniteDuration) extends AnyVal
|
||||
|
||||
final class OnStart(f: Game.ID => Unit) extends (Game.ID => Unit) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import akka.actor.{ ActorSystem, CoordinatedShutdown }
|
|||
import cats.data.NonEmptyList
|
||||
import chess.{ Centis, Color }
|
||||
import io.lettuce.core._
|
||||
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection
|
||||
import io.lettuce.core.pubsub.{ StatefulRedisPubSubConnection => PubSub }
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import play.api.libs.json._
|
||||
|
@ -116,18 +116,19 @@ final class RemoteSocket(
|
|||
case UnFollow(u1, u2) => send(Out.unfollow(u1, u2))
|
||||
}
|
||||
|
||||
final class StoppableSender(conn: StatefulRedisPubSubConnection[String, String], channel: Channel)
|
||||
extends Sender {
|
||||
def apply(msg: String): Unit = if (!stopping) conn.async.publish(channel, msg).unit
|
||||
final class StoppableSender(conn: PubSub[String, String], channel: Channel) extends Sender {
|
||||
def apply(msg: String): Unit = if (!stopping) conn.async.publish(channel, msg).unit
|
||||
def sticky(_id: String, msg: String): Unit = apply(msg)
|
||||
}
|
||||
|
||||
final class RoundRobinSender(
|
||||
conn: StatefulRedisPubSubConnection[String, String],
|
||||
channel: Channel,
|
||||
parallelism: Int
|
||||
) extends Sender {
|
||||
def apply(msg: String): Unit =
|
||||
if (!stopping) conn.async.publish(s"$channel:${msg.hashCode.abs % parallelism}", msg).unit
|
||||
final class RoundRobinSender(conn: PubSub[String, String], channel: Channel, parallelism: Int)
|
||||
extends Sender {
|
||||
def apply(msg: String): Unit = publish(msg.hashCode.abs % parallelism, msg)
|
||||
// use the ID to select the channel, not the entire message
|
||||
def sticky(id: String, msg: String): Unit = publish(id.hashCode.abs % parallelism, msg)
|
||||
|
||||
private def publish(subChannel: Int, msg: String) =
|
||||
if (!stopping) conn.async.publish(s"$channel:$subChannel", msg).unit
|
||||
}
|
||||
|
||||
def makeSender(channel: Channel, parallelism: Int = 1): Sender =
|
||||
|
@ -195,6 +196,7 @@ object RemoteSocket {
|
|||
|
||||
trait Sender {
|
||||
def apply(msg: String): Unit
|
||||
def sticky(_id: String, msg: String): Unit
|
||||
}
|
||||
|
||||
object Protocol {
|
||||
|
|
|
@ -608,6 +608,23 @@ final class SwissApi(
|
|||
_.map { withdraw(_, user.id) }.sequenceFu.void
|
||||
}
|
||||
|
||||
def isUnfinished(id: Swiss.Id): Fu[Boolean] =
|
||||
colls.swiss.exists($id(id) ++ $doc("finishedAt" $exists false))
|
||||
|
||||
def filterPlaying(id: Swiss.Id, userIds: Seq[User.ID]): Fu[List[User.ID]] =
|
||||
userIds.nonEmpty ??
|
||||
colls.swiss.exists($id(id) ++ $doc("finishedAt" $exists false)) flatMap {
|
||||
_ ?? SwissPlayer.fields { f =>
|
||||
colls.player.distinctEasy[User.ID, List](
|
||||
f.userId,
|
||||
$doc(
|
||||
f.id $in userIds.map(SwissPlayer.makeId(id, _)),
|
||||
f.absent $ne true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def resultStream(swiss: Swiss, perSecond: MaxPerSecond, nb: Int): Source[(SwissPlayer, Long), _] =
|
||||
SwissPlayer.fields { f =>
|
||||
colls.player
|
||||
|
|
|
@ -14,7 +14,7 @@ object CrudForm {
|
|||
import TournamentForm._
|
||||
import lila.common.Form.UTCDate._
|
||||
|
||||
val maxHomepageHours = 72
|
||||
val maxHomepageHours = 24
|
||||
|
||||
lazy val apply = Form(
|
||||
mapping(
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
<string name="advantageDescription">Abantaila osoa lortzen saiatu (200cp ≤ ebaluazioa ≤ 600cp)</string>
|
||||
<string name="anastasiaMate">Anastasiaren matea</string>
|
||||
<string name="anastasiaMateDescription">Zaldun bat eta gaztelua eta damak aurkariaren erregea taularen bazter baten eta bere pieza baten artean harrapatzen dute.</string>
|
||||
<string name="arabianMate">Mate arabiarra</string>
|
||||
<string name="arabianMateDescription">Zaldun eta gaztelu banak elkarrekin lan egiten dute aurkariaren erregea xake-taularen bazter baten harrapatzeko.</string>
|
||||
<string name="attackingF2F7">f2 edo f7 erasotu</string>
|
||||
<string name="attackingF2F7Description">f2 edo f7ko peoia helburu duen erasoa.</string>
|
||||
<string name="attraction">Erakarmena</string>
|
||||
|
@ -14,12 +16,18 @@
|
|||
<string name="backRankMateDescription">Bere piezekin trabatuta dagoenean erregeari bere errenkadan matea ematea.</string>
|
||||
<string name="bishopEndgame">Alfilen bukaera</string>
|
||||
<string name="bishopEndgameDescription">Alfilak eta peoiak bakarrik dituen partida-bukaera.</string>
|
||||
<string name="bodenMate">Bodenen matea</string>
|
||||
<string name="bodenMateDescription">Bi alfilek beren piezen artean trabatuta dagoen erregeari ematen dioten matea.</string>
|
||||
<string name="castling">Endrokea</string>
|
||||
<string name="castlingDescription">Babestu erregea eta ekarri gaztelua erasora.</string>
|
||||
<string name="capturingDefender">Defendatzailea harrapatu</string>
|
||||
<string name="capturingDefenderDescription">Beste pieza bat defendatzeko funtsezkoa den pieza kentzea, hurrengo jokaldietan lehenengo pieza hori harrapatzeko.</string>
|
||||
<string name="crushing">Zapalketa</string>
|
||||
<string name="crushingDescription">Akatsa aurkitu eta guztizko abantaila lortu. (ebaluazioa ≥ 600cp)</string>
|
||||
<string name="doubleBishopMate">Bi alfilen matea</string>
|
||||
<string name="doubleBishopMateDescription">Bi alfilek beren piezen artean trabatuta dagoen erregeari ematen dioten matea.</string>
|
||||
<string name="dovetailMate">Mirubuztanaren matea</string>
|
||||
<string name="dovetailMateDescription">Damak ematen duen matea erregearen ihes-laukiak bere piezekin trabatuta daudenean.</string>
|
||||
<string name="equality">Berdintasuna</string>
|
||||
<string name="equalityDescription">Partida galduta izatetik, berdinketa edo posizio berdintsua lortzera itzuli. (ebaluazioa ≤ 200cp)</string>
|
||||
<string name="kingsideAttack">Erregearen aldeko erasoa</string>
|
||||
|
|
|
@ -27,12 +27,7 @@
|
|||
<string name="wantClearHistory">I want to clear my history or rating</string>
|
||||
<string name="cantClearHistory">It's not possible to clear your game history, puzzle history, or ratings.</string>
|
||||
<string name="wantReport">I want to report a player</string>
|
||||
<string name="reportCheating">Report a player for cheating</string>
|
||||
<string name="reportSandbagging">Report a player for sandbagging</string>
|
||||
<string name="reportTrolling">Report a player for trolling</string>
|
||||
<string name="reportInsults">Report a player for insults</string>
|
||||
<string name="reportOtherReason">Report a player for some other reason</string>
|
||||
<string name="toReportAPlayer">To report a player for %s, use the report form</string>
|
||||
<string name="toReportAPlayerUseForm">To report a player, use the report form</string>
|
||||
<string name="youCanAlsoReachReportPage">You can also reach that page by clicking the %s report button on a profile page.</string>
|
||||
<string name="doNotReportInForum">Do not report players in the forum.</string>
|
||||
<string name="doNotSendReportEmails">Do not send us report emails.</string>
|
||||
|
|
|
@ -29,22 +29,14 @@
|
|||
}
|
||||
|
||||
.img {
|
||||
flex: 0 0 50px;
|
||||
flex: 0 0 42px;
|
||||
width: 44px;
|
||||
margin: 0 .5em 0 .3em;
|
||||
}
|
||||
|
||||
img.img {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
i.img,
|
||||
.img.icon {
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
|
||||
i.img::before {
|
||||
color: #fff;
|
||||
font-size: 50px;
|
||||
font-size: 42px;
|
||||
|
||||
@if $theme-light {
|
||||
text-shadow: 1px 1px 2px $c-link;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
@import "../../../common/css/component/slist";
|
||||
@import "../../../common/css/component/quote";
|
||||
@import "../../../common/css/component/color-icon";
|
||||
@import "../../../common/css/component/context-streamer";
|
||||
@import "../../../common/css/component/now-playing";
|
||||
@import "../../../common/css/component/podium";
|
||||
@import "../../../chat/css/chat";
|
||||
|
|
Loading…
Reference in New Issue