select a reason to decline a challenge from the web UI

also increases TS lib to ES2017

so if something breaks, that's why
pull/7978/head
Thibault Duplessis 2021-01-21 17:11:37 +01:00
parent 43fbd61029
commit 5950f4f06a
10 changed files with 87 additions and 28 deletions

View File

@ -12,9 +12,9 @@ import lila.challenge.{ Challenge => ChallengeModel }
import lila.common.{ HTTPRequest, IpAddress } import lila.common.{ HTTPRequest, IpAddress }
import lila.game.{ AnonCookie, Pov } import lila.game.{ AnonCookie, Pov }
import lila.oauth.{ AccessToken, OAuthScope } import lila.oauth.{ AccessToken, OAuthScope }
import lila.setup.ApiConfig
import lila.socket.Socket.SocketVersion import lila.socket.Socket.SocketVersion
import lila.user.{ User => UserModel } import lila.user.{ User => UserModel }
import lila.setup.ApiConfig
final class Challenge( final class Challenge(
env: Env env: Env
@ -136,9 +136,16 @@ final class Challenge(
} }
def decline(id: String) = def decline(id: String) =
Auth { implicit ctx => _ => AuthBody { implicit ctx => _ =>
OptionFuResult(api byId id) { c => OptionFuResult(api byId id) { c =>
isForMe(c) ?? api.decline(c, ChallengeModel.DeclineReason.default) implicit val req = ctx.body
isForMe(c) ??
api.decline(
c,
env.challenge.forms.decline
.bindFromRequest()
.fold(_ => ChallengeModel.DeclineReason.default, _.realReason)
)
} }
} }
def apiDecline(id: String) = def apiDecline(id: String) =

View File

@ -126,12 +126,20 @@ object Challenge {
} }
object DeclineReason { object DeclineReason {
case object Generic extends DeclineReason(I18nKeys.challenge.declineGeneric) case object Generic extends DeclineReason(I18nKeys.challenge.declineGeneric)
case object Later extends DeclineReason(I18nKeys.challenge.declineLater) case object Later extends DeclineReason(I18nKeys.challenge.declineLater)
case object TooFast extends DeclineReason(I18nKeys.challenge.declineTooFast)
case object TooSlow extends DeclineReason(I18nKeys.challenge.declineTooSlow)
case object TimeControl extends DeclineReason(I18nKeys.challenge.declineTimeControl)
case object Rated extends DeclineReason(I18nKeys.challenge.declineRated)
case object Casual extends DeclineReason(I18nKeys.challenge.declineCasual)
case object Standard extends DeclineReason(I18nKeys.challenge.declineStandard)
case object Variant extends DeclineReason(I18nKeys.challenge.declineVariant)
val default: DeclineReason = Generic val default: DeclineReason = Generic
val all: List[DeclineReason] = List(Generic, Later) val all: List[DeclineReason] =
def apply(key: String) = all.find(_.key == key) | Generic List(Generic, Later, TooFast, TooSlow, TimeControl, Rated, Casual, Standard, Variant)
def apply(key: String) = all.find { d => d.key == key.toLowerCase || d.trans.key == key } | Generic
} }
case class Rating(int: Int, provisional: Boolean) { case class Rating(int: Int, provisional: Boolean) {

View File

@ -35,7 +35,10 @@ final class JsonView(
Json.obj( Json.obj(
"in" -> a.in.map(apply(Direction.In.some)), "in" -> a.in.map(apply(Direction.In.some)),
"out" -> a.out.map(apply(Direction.Out.some)), "out" -> a.out.map(apply(Direction.Out.some)),
"i18n" -> lila.i18n.JsDump.keysToObject(i18nKeys, lang) "i18n" -> lila.i18n.JsDump.keysToObject(i18nKeys, lang),
"reasons" -> JsObject(Challenge.DeclineReason.all.map { r =>
r.key -> JsString(r.trans.txt())
})
) )
def show(challenge: Challenge, socketVersion: SocketVersion, direction: Option[Direction])(implicit def show(challenge: Challenge, socketVersion: SocketVersion, direction: Option[Direction])(implicit

View File

@ -1961,6 +1961,13 @@ val `cannotChallengeDueToProvisionalXRating` = new I18nKey("challenge:cannotChal
val `xOnlyAcceptsChallengesFromFriends` = new I18nKey("challenge:xOnlyAcceptsChallengesFromFriends") val `xOnlyAcceptsChallengesFromFriends` = new I18nKey("challenge:xOnlyAcceptsChallengesFromFriends")
val `declineGeneric` = new I18nKey("challenge:declineGeneric") val `declineGeneric` = new I18nKey("challenge:declineGeneric")
val `declineLater` = new I18nKey("challenge:declineLater") val `declineLater` = new I18nKey("challenge:declineLater")
val `declineTooFast` = new I18nKey("challenge:declineTooFast")
val `declineTooSlow` = new I18nKey("challenge:declineTooSlow")
val `declineTimeControl` = new I18nKey("challenge:declineTimeControl")
val `declineRated` = new I18nKey("challenge:declineRated")
val `declineCasual` = new I18nKey("challenge:declineCasual")
val `declineStandard` = new I18nKey("challenge:declineStandard")
val `declineVariant` = new I18nKey("challenge:declineVariant")
} }
} }

View File

@ -12,5 +12,12 @@
<string name="cannotChallengeDueToProvisionalXRating">Cannot challenge due to provisional %s rating.</string> <string name="cannotChallengeDueToProvisionalXRating">Cannot challenge due to provisional %s rating.</string>
<string name="xOnlyAcceptsChallengesFromFriends">%s only accepts challenges from friends.</string> <string name="xOnlyAcceptsChallengesFromFriends">%s only accepts challenges from friends.</string>
<string name="declineGeneric">I'm not accepting challenges at the moment.</string> <string name="declineGeneric">I'm not accepting challenges at the moment.</string>
<string name="declineLater">I'm not accepting challenges right now, please ask again later.</string> <string name="declineLater">This is not the right time for me, please ask again later.</string>
<string name="declineTooFast">This time control is too fast for me, please challenge again with a slower game.</string>
<string name="declineTooSlow">This time control is too slow for me, please challenge again with a faster game.</string>
<string name="declineTimeControl">I'm not accepting challenges with this time control.</string>
<string name="declineRated">Please send me a rated challenge instead.</string>
<string name="declineCasual">Please send me a casual challenge instead.</string>
<string name="declineStandard">I'm not accepting variant challenges right now.</string>
<string name="declineVariant">I'm not willing to play this variant right now.</string>
</resources> </resources>

View File

@ -64,11 +64,15 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 50%; width: 40%;
} }
.buttons > *:last-child { .buttons .decline {
left: 50%; left: 40%;
}
.buttons .decline-reason {
left: 80%;
width: 20%;
} }
button { button {

View File

@ -1,15 +1,17 @@
import * as xhr from 'common/xhr'; import * as xhr from 'common/xhr';
import notify from 'common/notification'; import notify from 'common/notification';
import { Ctrl, ChallengeOpts, ChallengeData, ChallengeUser } from './interfaces'; import { Ctrl, ChallengeOpts, ChallengeData, ChallengeUser, Reasons } from './interfaces';
export default function(opts: ChallengeOpts, data: ChallengeData, redraw: () => void): Ctrl { export default function(opts: ChallengeOpts, data: ChallengeData, redraw: () => void): Ctrl {
let trans = (key: string) => key; let trans = (key: string) => key;
let redirecting = false; let redirecting = false;
let reasons: Reasons = {};
function update(d: ChallengeData) { function update(d: ChallengeData) {
data = d; data = d;
if (d.i18n) trans = lichess.trans(d.i18n).noarg; if (d.i18n) trans = lichess.trans(d.i18n).noarg;
if (d.reasons) reasons = d.reasons;
opts.setCount(countActiveIn()); opts.setCount(countActiveIn());
notifyNew(); notifyNew();
} }
@ -43,14 +45,15 @@ export default function(opts: ChallengeOpts, data: ChallengeData, redraw: () =>
return { return {
data: () => data, data: () => data,
trans: () => trans, trans: () => trans,
reasons: () => reasons,
update, update,
decline(id) { decline(id, reason) {
data.in.forEach(c => { data.in.forEach(c => {
if (c.id === id) { if (c.id === id) {
c.declined = true; c.declined = true;
xhr.text( xhr.text(
`/challenge/${id}/decline`, `/challenge/${id}/decline`,
{ method: 'post' } { method: 'post', body: xhr.form({reason}) }
).catch(() => lichess.announce({ msg: 'Failed to send challenge decline' })); ).catch(() => lichess.announce({ msg: 'Failed to send challenge decline' }));
} }
}); });

View File

@ -9,7 +9,8 @@ export interface Ctrl {
update(data: ChallengeData): void update(data: ChallengeData): void
data(): ChallengeData data(): ChallengeData
trans(): (key: string) => string trans(): (key: string) => string
decline(id: string): void reasons(): Reasons
decline(id: string, reason: string): void
cancel(id: string): void cancel(id: string): void
onRedirect(): void onRedirect(): void
redirecting(): boolean redirecting(): boolean
@ -56,12 +57,17 @@ export interface Challenge {
declined?: boolean declined?: boolean
} }
export type Reasons = {
[key: string]: string
}
export interface ChallengeData { export interface ChallengeData {
in: Array<Challenge> in: Array<Challenge>
out: Array<Challenge> out: Array<Challenge>
i18n?: { i18n?: {
[key: string]: string [key: string]: string
} }
reasons?: Reasons
} }
export type Redraw = () => void export type Redraw = () => void

View File

@ -1,11 +1,11 @@
import { Ctrl, Challenge, ChallengeData, ChallengeDirection, ChallengeUser, TimeControl } from './interfaces'
import { h } from 'snabbdom' import { h } from 'snabbdom'
import { VNode } from 'snabbdom/vnode' import { VNode } from 'snabbdom/vnode'
import { Ctrl, Challenge, ChallengeData, ChallengeDirection, ChallengeUser, TimeControl } from './interfaces'
export function loaded(ctrl: Ctrl): VNode { export function loaded(ctrl: Ctrl): VNode {
return ctrl.redirecting() ? return ctrl.redirecting() ?
h('div#challenge-app.dropdown', h('div.initiating', spinner())) : h('div#challenge-app.dropdown', h('div.initiating', spinner())) :
h('div#challenge-app.links.dropdown.rendered', renderContent(ctrl)); h('div#challenge-app.links.dropdown.rendered', renderContent(ctrl));
} }
export function loading(): VNode { export function loading(): VNode {
@ -54,7 +54,7 @@ function challenge(ctrl: Ctrl, dir: ChallengeDirection) {
].join(' • ')) ].join(' • '))
]), ]),
h('i', { h('i', {
attrs: {'data-icon': c.perf.icon} attrs: { 'data-icon': c.perf.icon }
}), }),
h('div.buttons', (dir === 'in' ? inButtons : outButtons)(ctrl, c)) h('div.buttons', (dir === 'in' ? inButtons : outButtons)(ctrl, c))
]); ]);
@ -84,8 +84,22 @@ function inButtons(ctrl: Ctrl, c: Challenge): VNode[] {
'data-icon': 'L', 'data-icon': 'L',
title: trans('decline') title: trans('decline')
}, },
hook: onClick(() => ctrl.decline(c.id)) hook: onClick(() => ctrl.decline(c.id, 'generic'))
}) }),
h('select.decline-reason', {
hook: {
insert: (vnode: VNode) => {
const select = (vnode.elm as HTMLSelectElement);
select.addEventListener('change', () =>
ctrl.decline(c.id, select.value)
);
}
}
},
Object.entries(ctrl.reasons()).map(([key, name]) =>
h('option', { attrs: { value: key } }, key == 'generic' ? '' : name)
)
)
]; ];
} }
@ -127,7 +141,7 @@ function renderUser(u?: ChallengeUser): VNode {
if (!u) return h('span', 'Open challenge'); if (!u) return h('span', 'Open challenge');
const rating = u.rating + (u.provisional ? '?' : ''); const rating = u.rating + (u.provisional ? '?' : '');
return h('a.ulpt.user-link', { return h('a.ulpt.user-link', {
attrs: { href: `/@/${u.name}`}, attrs: { href: `/@/${u.name}` },
class: { online: !!u.online } class: { online: !!u.online }
}, [ }, [
h('i.line' + (u.patron ? '.patron' : '')), h('i.line' + (u.patron ? '.patron' : '')),
@ -135,9 +149,9 @@ function renderUser(u?: ChallengeUser): VNode {
u.title && h('span.utitle', u.title == 'BOT' ? { attrs: { 'data-bot': true } } : {}, u.title + ' '), u.title && h('span.utitle', u.title == 'BOT' ? { attrs: { 'data-bot': true } } : {}, u.title + ' '),
u.name + ' (' + rating + ') ' u.name + ' (' + rating + ') '
]), ]),
h('signal', u.lag === undefined ? [] : [1, 2, 3, 4].map((i) => h('i', { h('signal', u.lag === undefined ? [] : [1, 2, 3, 4].map((i) => h('i', {
class: { off: u.lag! < i} class: { off: u.lag! < i }
}))) })))
]); ]);
} }

View File

@ -10,7 +10,7 @@
"moduleResolution": "node", "moduleResolution": "node",
"target": "ES2016", "target": "ES2016",
"module": "commonjs", "module": "commonjs",
"lib": ["DOM", "ES2016", "DOM.iterable"], "lib": ["DOM", "ES2017", "DOM.iterable"],
"types": ["lichess", "cash", "defer-promise"] "types": ["lichess", "cash", "defer-promise"]
} }
} }