decline challenges for a reason - closes #7487

also closes #7658
challenge-reason
Thibault Duplessis 2021-01-21 16:04:41 +01:00
parent d2b38f04d9
commit 43fbd61029
14 changed files with 92 additions and 24 deletions

View File

@ -138,19 +138,25 @@ final class Challenge(
def decline(id: String) =
Auth { implicit ctx => _ =>
OptionFuResult(api byId id) { c =>
if (isForMe(c)) api decline c
else notFound
isForMe(c) ?? api.decline(c, ChallengeModel.DeclineReason.default)
}
}
def apiDecline(id: String) =
Scoped(_.Challenge.Write, _.Bot.Play, _.Board.Play) { _ => me =>
ScopedBody(_.Challenge.Write, _.Bot.Play, _.Board.Play) { implicit req => me =>
implicit val lang = reqLang
api.activeByIdFor(id, me) flatMap {
case None =>
env.bot.player.rematchDecline(id, me) flatMap {
case true => jsonOkResult.fuccess
case _ => notFoundJson()
}
case Some(c) => api.decline(c) inject jsonOkResult
case Some(c) =>
env.challenge.forms.decline
.bindFromRequest()
.fold(
newJsonFormError,
data => api.decline(c, data.realReason) inject jsonOkResult
)
}
}
@ -168,7 +174,7 @@ final class Challenge(
case Some(c) => api.cancel(c) inject jsonOkResult
case None =>
api.activeByIdFor(id, me) flatMap {
case Some(c) => api.decline(c) inject jsonOkResult
case Some(c) => api.decline(c, ChallengeModel.DeclineReason.default) inject jsonOkResult
case None =>
env.game.gameRepo game id dmap {
_ flatMap { Pov.ofUserId(_, me.id) }

View File

@ -25,7 +25,7 @@ object mine {
moreCss = cssTag("challenge.page")
) {
val challengeLink = s"$netBaseUrl${routes.Round.watcher(c.id, "white")}"
main(cls := "page-small challenge-page box box-pad")(
main(cls := s"page-small challenge-page box box-pad challenge--${c.status.name}")(
c.status match {
case Status.Created | Status.Offline =>
div(id := "ping-challenge")(
@ -94,6 +94,10 @@ object mine {
case Status.Declined =>
div(cls := "follow-up")(
h1(trans.challenge.challengeDeclined()),
blockquote(cls := "challenge-reason pull-quote")(
p(c.anyDeclineReason.trans()),
footer(userIdLink(c.destUserId))
),
bits.details(c),
a(cls := "button button-fat", href := routes.Lobby.home())(trans.newOpponent())
)

View File

@ -7,6 +7,7 @@ import chess.variant.Variant
import lila.db.BSON
import lila.db.BSON.{ Reader, Writer }
import lila.db.dsl._
import scala.util.Success
private object BSONHandlers {
@ -71,6 +72,10 @@ private object BSONHandlers {
"s" -> a.secret
)
}
implicit val DeclineReasonBSONHandler = tryHandler[DeclineReason](
{ case BSONString(k) => Success(Challenge.DeclineReason(k)) },
r => BSONString(r.key)
)
implicit val ChallengerBSONHandler = new BSON[Challenger] {
def reads(r: Reader) =
if (r contains "id") RegisteredBSONHandler reads r

View File

@ -25,7 +25,8 @@ case class Challenge(
createdAt: DateTime,
seenAt: Option[DateTime], // None for open challenges, so they don't sweep
expiresAt: DateTime,
open: Option[Boolean] = None
open: Option[Boolean] = None,
declineReason: Option[Challenge.DeclineReason] = None
) {
import Challenge._
@ -94,6 +95,13 @@ case class Challenge(
def isOpen = ~open
lazy val perfType = perfTypeOf(variant, timeControl)
def anyDeclineReason = declineReason | DeclineReason.default
def declineWith(reason: DeclineReason) = copy(
status = Status.Declined,
declineReason = reason.some
)
}
object Challenge {
@ -113,11 +121,17 @@ object Challenge {
def apply(id: Int): Option[Status] = all.find(_.id == id)
}
sealed abstract class DeclineReason(key: I18nKey)
sealed abstract class DeclineReason(val trans: I18nKey) {
val key = toString.toLowerCase
}
object DeclineReason {
case object Generic extends DeclineReason(I18nKeys.challenge.declineGeneric)
case object Later extends DeclineReason(I18nKeys.challenge.declineLater)
val default: DeclineReason = Generic
val all: List[DeclineReason] = List(Generic, Later)
def apply(key: String) = all.find(_.key == key) | Generic
}
case class Rating(int: Int, provisional: Boolean) {

View File

@ -73,10 +73,10 @@ final class ChallengeApi(
case _ => fuccess(socketReload(id))
}
def decline(c: Challenge) =
repo.decline(c) >>- {
def decline(c: Challenge, reason: Challenge.DeclineReason) =
repo.decline(c, reason) >>- {
uncacheAndNotify(c)
Bus.publish(Event.Decline(c), "challenge")
Bus.publish(Event.Decline(c declineWith reason), "challenge")
}
private val acceptQueue = new lila.hub.DuctSequencer(maxSize = 64, timeout = 5 seconds, "challengeAccept")
@ -149,7 +149,7 @@ final class ChallengeApi(
}
private def socketReload(id: Challenge.ID): Unit =
socket foreach (_ reload id)
socket.foreach(_ reload id)
private def notify(userId: User.ID): Funit =
for {

View File

@ -0,0 +1,18 @@
package lila.challenge
import play.api.data._
import play.api.data.Forms._
final class ChallengeForm {
val decline = Form(
mapping(
"reason" -> optional(nonEmptyText)
)(DeclineData.apply _)(DeclineData.unapply _)
)
case class DeclineData(reason: Option[String]) {
def realReason = reason.fold(Challenge.DeclineReason.default)(Challenge.DeclineReason.apply)
}
}

View File

@ -114,8 +114,12 @@ final private class ChallengeRepo(coll: Coll, maxPerUser: Max)(implicit
def offline(challenge: Challenge) = setStatus(challenge, Status.Offline, Some(_ plusHours 3))
def cancel(challenge: Challenge) = setStatus(challenge, Status.Canceled, Some(_ plusHours 3))
def decline(challenge: Challenge) = setStatus(challenge, Status.Declined, Some(_ plusHours 3))
def accept(challenge: Challenge) = setStatus(challenge, Status.Accepted, Some(_ plusHours 3))
def decline(challenge: Challenge, reason: Challenge.DeclineReason) =
setStatus(challenge, Status.Declined, Some(_ plusHours 3)) >> {
(reason != Challenge.DeclineReason.default) ??
coll.updateField($id(challenge.id), "declineReason", reason).void
}
def accept(challenge: Challenge) = setStatus(challenge, Status.Accepted, Some(_ plusHours 3))
def statusById(id: Challenge.ID) = coll.primitiveOne[Status]($id(id), "status")

View File

@ -50,6 +50,8 @@ final class Env(
lazy val jsonView = wire[JsonView]
val forms = new ChallengeForm
system.scheduler.scheduleWithFixedDelay(10 seconds, 3 seconds) { () =>
api.sweep.unit
}

View File

@ -80,6 +80,7 @@ final class JsonView(
)
.add("direction" -> direction.map(_.name))
.add("initialFen" -> c.initialFen)
.add("declineReason" -> c.declineReason.map(_.trans.txt()))
private def iconChar(c: Challenge) =
if (c.variant == chess.variant.FromPosition) '*'

View File

@ -16,7 +16,7 @@ final class Env(
private lazy val maxPlaying = appConfig.get[Max]("setup.max_playing")
lazy val forms = wire[FormFactory]
lazy val forms = wire[SetupForm]
lazy val processor = wire[Processor]
}

View File

@ -8,7 +8,7 @@ import play.api.data.Forms._
import lila.rating.RatingRange
import lila.user.{ User, UserContext }
final class FormFactory {
final class SetupForm {
import Mappings._

View File

@ -1,5 +1,3 @@
$c-challenge: $c-secondary;
.challenge-page {
.challenge-id-form {
white-space: nowrap;
@ -41,18 +39,22 @@ $c-challenge: $c-secondary;
.details {
@extend %flex-between;
$c-bg: mix($c-good, $c-bg-box, 10%);
--c-font: #{$c-good};
--c-bg: #{$c-bg};
border-radius: 99px;
background: mix($c-challenge, $c-bg-box, 10%);
border: 1px solid $c-challenge;
padding: .5em 1.1em;
margin-bottom: 3rem;
font-size: 2em;
background: var(--c-bg);
border: 1px solid var(--c-font);
> div {
@extend %flex-center, %roboto;
&::before {
color: $c-challenge;
color: var(--c-font);
font-size: 6rem;
margin-right: .2em;
}
@ -68,11 +70,22 @@ $c-challenge: $c-secondary;
.mode {
font-weight: bold;
color: $c-challenge;
color: var(--c-font);
text-transform: uppercase;
}
}
&.challenge--declined .details {
$c-bg: mix($c-bad, $c-bg-box, 10%);
--c-font: #{$c-bad};
--c-bg: #{$c-bg};
}
.challenge-reason {
margin: 2em auto 5em auto;
max-width: 70ch;
}
.follow-up .button {
display: block;
margin-top: 2em;

View File

@ -1,2 +1,3 @@
@import "../../../common/css/plugin";
@import "../../../common/css/component/quote";
@import "../page";

View File

@ -1,8 +1,8 @@
:root {
--tagify-dd-color-primary: $c-primary;
--tagify-dd-color-primary: #{$c-primary};
// should be same as "$tags-focus-border-color"
--tagify-dd-bg-color: $c-bg-box;
--tagify-dd-bg-color: #{$c-bg-box};
}
.tagify {