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 channel
puzzle-dashboard-api
Thibault Duplessis 2021-01-21 11:47:42 +01:00
commit 652c173e1d
27 changed files with 163 additions and 134 deletions

View File

@ -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
}
}
}
}

View File

@ -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))
}
}
}
}

View File

@ -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"),

View File

@ -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),

View File

@ -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!")
)
)
)
)

View File

@ -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",

View File

@ -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"))
)

View File

@ -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
)
}

View File

@ -339,7 +339,6 @@ gameSearch {
actor.name = game-search
}
round {
moretime = 15 seconds
collection {
note = game_note
forecast = forecast

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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

View File

@ -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) }
}

View File

@ -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
}
}

View File

@ -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))
}
}

View File

@ -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

View File

@ -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) {

View File

@ -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 {

View File

@ -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

View File

@ -14,7 +14,7 @@ object CrudForm {
import TournamentForm._
import lila.common.Form.UTCDate._
val maxHomepageHours = 72
val maxHomepageHours = 24
lazy val apply = Form(
mapping(

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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";