From b82705dd7b7477656921eb75aa2daf2d79859266 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 12 Nov 2024 09:38:37 -0600 Subject: [PATCH 1/8] consolidate MoveMetadata types --- ui/round/src/interfaces.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/round/src/interfaces.ts b/ui/round/src/interfaces.ts index 7ca4876fef623..16c4662476874 100644 --- a/ui/round/src/interfaces.ts +++ b/ui/round/src/interfaces.ts @@ -7,6 +7,7 @@ import type { ChatCtrl, ChatPlugin } from 'chat'; import * as Prefs from 'common/prefs'; import type { EnhanceOpts } from 'common/richText'; import type { RoundSocket } from './socket'; +import type { MoveMetadata as CgMoveMetadata } from 'chessground/types'; export { type RoundSocket } from './socket'; export { type CorresClockData } from './corresClock/corresClockCtrl'; @@ -196,8 +197,8 @@ export interface Pref { resizeHandle: Prefs.ShowResizeHandle; } -export interface MoveMetadata { - premove?: boolean; +export interface MoveMetadata extends CgMoveMetadata { + preConfirmed?: boolean; justDropped?: Role; justCaptured?: Piece; } From 9eff9281bd80b125604ac1f916fa90a9a23cf2ae Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 12 Nov 2024 09:39:38 -0600 Subject: [PATCH 2/8] allow voice users to enable move confirmation when disabled in prefs --- ui/round/src/view/boardMenu.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/round/src/view/boardMenu.ts b/ui/round/src/view/boardMenu.ts index 6c4d6f5a2ae0c..22387c30eee23 100644 --- a/ui/round/src/view/boardMenu.ts +++ b/ui/round/src/view/boardMenu.ts @@ -19,7 +19,9 @@ export default function (ctrl: RoundController): LooseVNode { ), menu.voiceInput(boolPrefXhrToggle('voice', !!ctrl.voiceMove), !spectator), menu.keyboardInput(boolPrefXhrToggle('keyboardMove', !!ctrl.keyboardMove), !spectator), - !spectator && d.pref.submitMove ? menu.confirmMove(ctrl.confirmMoveToggle) : undefined, + !spectator && (d.pref.submitMove || ctrl.voiceMove) + ? menu.confirmMove(ctrl.confirmMoveToggle) + : undefined, ]), h('section.board-menu__links', [ h( From 9d0a6fbfca21a69f1c2d7810b550a612c7035d9d Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 12 Nov 2024 09:41:29 -0600 Subject: [PATCH 3/8] voice.move.ts appetite for RoundController functionality is insatiable --- ui/chess/src/moveRootCtrl.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/chess/src/moveRootCtrl.ts b/ui/chess/src/moveRootCtrl.ts index b81684610d024..18f7d89b72096 100644 --- a/ui/chess/src/moveRootCtrl.ts +++ b/ui/chess/src/moveRootCtrl.ts @@ -1,5 +1,5 @@ export interface MoveRootCtrl { - pluginMove: (orig: Key, dest: Key, prom: Role | undefined) => void; + pluginMove: (orig: Key, dest: Key, prom: Role | undefined, preConfirmed?: boolean /* = false */) => void; redraw: () => void; flipNow: () => void; offerDraw?: (v: boolean, immediately?: boolean) => void; @@ -12,6 +12,7 @@ export interface MoveRootCtrl { blindfold?: (v?: boolean) => boolean; speakClock?: () => void; goBerserk?: () => void; + shouldConfirmMove?: () => boolean; } export interface MoveUpdate { From 208d12d2cc335bc0bdd56794aa2b3cfa1f6424a0 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 12 Nov 2024 09:42:38 -0600 Subject: [PATCH 4/8] make voice move confirm and standard move confirm complementary --- ui/round/src/ctrl.ts | 28 +++++++++++++++------------- ui/voice/src/move/voice.move.ts | 10 +++++----- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index 241351a489605..3b1dd0d2f4866 100644 --- a/ui/round/src/ctrl.ts +++ b/ui/round/src/ctrl.ts @@ -11,7 +11,6 @@ import { make as makeSocket, type RoundSocket } from './socket'; import * as title from './title'; import * as blur from './blur'; import viewStatus from 'game/view/status'; -import type { MoveMetadata as CgMoveMetadata } from 'chessground/types'; import { ClockController } from './clock/clockCtrl'; import { CorresClockController } from './corresClock/corresClockCtrl'; import MoveOn from './moveOn'; @@ -72,7 +71,7 @@ export default class RoundController implements MoveRootCtrl { firstSeconds = true; flip = false; menu: Toggle; - confirmMoveToggle: Toggle = toggle(true); + confirmMoveToggle: Toggle; loading = false; loadingTimeout: number; redirecting = false; @@ -123,7 +122,7 @@ export default class RoundController implements MoveRootCtrl { ); this.setQuietMode(); - + this.confirmMoveToggle = toggle(d.pref.submitMove); this.moveOn = new MoveOn(this, 'move-on'); if (!opts.local) this.transientMove = new TransientMove(this.socket); @@ -158,12 +157,12 @@ export default class RoundController implements MoveRootCtrl { setTimeout(this.showExpiration, 250); }; - private onUserMove = (orig: Key, dest: Key, meta: CgMoveMetadata) => { + private onUserMove = (orig: Key, dest: Key, meta: MoveMetadata) => { if (!this.keyboardMove?.usedSan) ab.move(this, meta, pubsub.emit); if (!this.startPromotion(orig, dest, meta)) this.sendMove(orig, dest, undefined, meta); }; - private onUserNewPiece = (role: Role, key: Key, meta: CgMoveMetadata) => { + private onUserNewPiece = (role: Role, key: Key, meta: MoveMetadata) => { if (!this.replaying() && crazyValid(this.data, role, key)) { this.sendNewPiece(role, key, !!meta.predrop); } else this.jump(this.ply); @@ -178,7 +177,7 @@ export default class RoundController implements MoveRootCtrl { } else site.sound.move({ name: 'move', filter: 'game' }); }; - private startPromotion = (orig: Key, dest: Key, meta: CgMoveMetadata) => + private startPromotion = (orig: Key, dest: Key, meta: MoveMetadata) => this.promotion.start( orig, dest, @@ -190,7 +189,7 @@ export default class RoundController implements MoveRootCtrl { this.keyboardMove?.justSelected(), ); - private onPremove = (orig: Key, dest: Key, meta: CgMoveMetadata) => this.startPromotion(orig, dest, meta); + private onPremove = (orig: Key, dest: Key, meta: MoveMetadata) => this.startPromotion(orig, dest, meta); private onCancelPremove = () => this.promotion.cancelPrePromotion(); @@ -293,7 +292,7 @@ export default class RoundController implements MoveRootCtrl { setTitle = (): void => title.set(this); - actualSendMove = (tpe: string, data: any, meta: MoveMetadata = {}): void => { + actualSendMove = (tpe: string, data: any, meta: MoveMetadata = { premove: false }): void => { const socketOpts: SocketOpts = { sign: this.sign, ackable: true, @@ -319,7 +318,7 @@ export default class RoundController implements MoveRootCtrl { this.redraw(); }; - pluginMove = (orig: Key, dest: Key, role?: Role): void => { + pluginMove = (orig: Key, dest: Key, role?: Role, preConfirmed?: boolean): void => { if (!role) { this.chessground.move(orig, dest); this.chessground.state.movable.dests = undefined; @@ -327,7 +326,7 @@ export default class RoundController implements MoveRootCtrl { if (this.startPromotion(orig, dest, { premove: false })) return; } - this.sendMove(orig, dest, role, { premove: false }); + this.sendMove(orig, dest, role, { premove: false, preConfirmed }); }; pluginUpdate = (fen: string): void => { @@ -335,13 +334,16 @@ export default class RoundController implements MoveRootCtrl { this.keyboardMove?.update({ fen, canMove: this.canMove() }); }; - sendMove = (orig: Key, dest: Key, prom: Role | undefined, meta: CgMoveMetadata): void => { + shouldConfirmMove = (): boolean => + (this.data.pref.submitMove && this.confirmMoveToggle()) || this.confirmMoveToggle(); + + sendMove = (orig: Key, dest: Key, prom: Role | undefined, meta: MoveMetadata): void => { const move: SocketMove = { u: orig + dest }; if (prom) move.u += prom === 'knight' ? 'n' : prom[0]; if (blur.get()) move.b = 1; this.resign(false); - if (this.data.pref.submitMove && this.confirmMoveToggle() && !meta.premove) { + if (!meta.preConfirmed && this.shouldConfirmMove() && !meta.premove) { if (site.sound.speech()) { const spoken = `${speakable(sanOf(readFen(this.stepAt(this.ply).fen), move.u))}. confirm?`; site.sound.say(spoken, false, true); @@ -357,7 +359,7 @@ export default class RoundController implements MoveRootCtrl { const drop: SocketDrop = { role, pos: key }; if (blur.get()) drop.b = 1; this.resign(false); - if (this.data.pref.submitMove && this.confirmMoveToggle() && !isPredrop) { + if (this.shouldConfirmMove() && !isPredrop) { this.toSubmit = drop; this.redraw(); } else { diff --git a/ui/voice/src/move/voice.move.ts b/ui/voice/src/move/voice.move.ts index 6e6fb1a8bacfe..a2c11c4f6bdba 100644 --- a/ui/voice/src/move/voice.move.ts +++ b/ui/voice/src/move/voice.move.ts @@ -297,9 +297,9 @@ export function initModule({ // trim choices to clarity window options = options.filter(([, m]) => m.cost - lowestCost <= clarityThreshold); - if (!timer() && options.length === 1 && options[0][1].cost < 0.3) { + if (!timer() && options.length === 1 && (options[0][1].cost < 0.3 || root.shouldConfirmMove?.())) { console.info('chooseMoves', `chose '${options[0][0]}' cost=${options[0][1].cost}`); - submit(options[0][0]); + submit(options[0][0], false); return true; } return ambiguate(options); @@ -321,7 +321,7 @@ export function initModule({ if (preferred && timer()) { choiceTimeout = setTimeout( () => { - submit(options[0][0]); + submit(options[0][0], false); choiceTimeout = undefined; voice.mic.setRecognizer('default'); }, @@ -349,7 +349,7 @@ export function initModule({ buildSquares(); } - function submit(uci: Uci) { + function submit(uci: Uci, preConfirmed = true) { clearMoveProgress(); if (uci.length < 3) { const dests = [...new Set(ucis.filter(x => x.length === 4 && x.startsWith(uci)))]; @@ -362,7 +362,7 @@ export function initModule({ const role = promo(uci); cg.cancelMove(); if (role) promote(cg, dest(uci), role); - root.pluginMove(src(uci), dest(uci), role); + root.pluginMove(src(uci), dest(uci), role, preConfirmed); return true; } From 498f5c950c0ce5b919b48590311567ca5ea1c32e Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Wed, 13 Nov 2024 17:40:39 -0600 Subject: [PATCH 5/8] remove redundant function --- ui/chess/src/moveRootCtrl.ts | 2 +- ui/round/src/ctrl.ts | 7 ++----- ui/voice/src/move/voice.move.ts | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/ui/chess/src/moveRootCtrl.ts b/ui/chess/src/moveRootCtrl.ts index 18f7d89b72096..9aa7f6fc2fdff 100644 --- a/ui/chess/src/moveRootCtrl.ts +++ b/ui/chess/src/moveRootCtrl.ts @@ -12,7 +12,7 @@ export interface MoveRootCtrl { blindfold?: (v?: boolean) => boolean; speakClock?: () => void; goBerserk?: () => void; - shouldConfirmMove?: () => boolean; + confirmMoveToggle?: () => boolean; } export interface MoveUpdate { diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index 3b1dd0d2f4866..a19346eb2cf49 100644 --- a/ui/round/src/ctrl.ts +++ b/ui/round/src/ctrl.ts @@ -334,16 +334,13 @@ export default class RoundController implements MoveRootCtrl { this.keyboardMove?.update({ fen, canMove: this.canMove() }); }; - shouldConfirmMove = (): boolean => - (this.data.pref.submitMove && this.confirmMoveToggle()) || this.confirmMoveToggle(); - sendMove = (orig: Key, dest: Key, prom: Role | undefined, meta: MoveMetadata): void => { const move: SocketMove = { u: orig + dest }; if (prom) move.u += prom === 'knight' ? 'n' : prom[0]; if (blur.get()) move.b = 1; this.resign(false); - if (!meta.preConfirmed && this.shouldConfirmMove() && !meta.premove) { + if (!meta.preConfirmed && this.confirmMoveToggle() && !meta.premove) { if (site.sound.speech()) { const spoken = `${speakable(sanOf(readFen(this.stepAt(this.ply).fen), move.u))}. confirm?`; site.sound.say(spoken, false, true); @@ -359,7 +356,7 @@ export default class RoundController implements MoveRootCtrl { const drop: SocketDrop = { role, pos: key }; if (blur.get()) drop.b = 1; this.resign(false); - if (this.shouldConfirmMove() && !isPredrop) { + if (this.confirmMoveToggle() && !isPredrop) { this.toSubmit = drop; this.redraw(); } else { diff --git a/ui/voice/src/move/voice.move.ts b/ui/voice/src/move/voice.move.ts index a2c11c4f6bdba..35966aaccf878 100644 --- a/ui/voice/src/move/voice.move.ts +++ b/ui/voice/src/move/voice.move.ts @@ -297,7 +297,7 @@ export function initModule({ // trim choices to clarity window options = options.filter(([, m]) => m.cost - lowestCost <= clarityThreshold); - if (!timer() && options.length === 1 && (options[0][1].cost < 0.3 || root.shouldConfirmMove?.())) { + if (!timer() && options.length === 1 && (options[0][1].cost < 0.3 || root.confirmMoveToggle?.())) { console.info('chooseMoves', `chose '${options[0][0]}' cost=${options[0][1].cost}`); submit(options[0][0], false); return true; From 59eba198f04fc549d64f76f0e800e39a51c4fb6e Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Thu, 14 Nov 2024 17:45:00 -0600 Subject: [PATCH 6/8] make CI happy now that aggregateEvents does a different ordering for PlayBans --- modules/api/src/test/ModTimelineTest.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/api/src/test/ModTimelineTest.scala b/modules/api/src/test/ModTimelineTest.scala index 7f761e54bce36..fd7878c40d9f8 100644 --- a/modules/api/src/test/ModTimelineTest.scala +++ b/modules/api/src/test/ModTimelineTest.scala @@ -70,11 +70,11 @@ class ModTimelineTest extends munit.FunSuite: test("merge mixed events"): assertEquals( aggregateEvents(List(ban1, ban2)), - List(bans(ban1, ban2)) + List(bans(ban2, ban1)) ) assertEquals( aggregateEvents(List(ban1, l1, ban2)), - List[Event](bans(ban1, ban2), l1) + List[Event](bans(ban2, ban1), l1) ) assertEquals( aggregateEvents(List(ban1, l1, l1)), @@ -82,17 +82,17 @@ class ModTimelineTest extends munit.FunSuite: ) assertEquals( aggregateEvents(List(ban1, l1, l1, ban2)), - List[Event](bans(ban1, ban2), l1) + List[Event](bans(ban2, ban1), l1) ) assertEquals( aggregateEvents(List(ban1, l1, l4, l1, ban2)), - List[Event](bans(ban1, ban2), l1, l4) + List[Event](bans(ban2, ban1), l1, l4) ) assertEquals( aggregateEvents(List(l1, ban1, l4, ban2, l2, l3, l5, ban3, ban3)), List[Event]( l1.copy(text = s"linguine${sep}fusilli"), - bans(ban1, ban2, ban3, ban3), + bans(ban3, ban3, ban2, ban1), l4.copy(text = s"bucatini${sep}rigatoni"), l3 ) From 0182a4c1357565f2b894ab5acb3977e22551aa42 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 15 Nov 2024 08:50:17 +0100 Subject: [PATCH 7/8] filter game chat messages at server level as a temporary fix for the new mobile app. TODOS: - also filter incoming messages in lila-ws - eventually filter at the app level - revert this commit and the lila-ws commit --- modules/round/src/main/RoundMobile.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/round/src/main/RoundMobile.scala b/modules/round/src/main/RoundMobile.scala index 7dd29ffd19b79..3b180304683f6 100644 --- a/modules/round/src/main/RoundMobile.scala +++ b/modules/round/src/main/RoundMobile.scala @@ -119,6 +119,7 @@ final class RoundMobile( private def getPlayerChat(game: Game, isAuth: Boolean): Fu[Option[Chat.Restricted]] = game.hasChat.so: for - chat <- chatApi.playerChat.findIf(game.id.into(ChatId), game.secondsSinceCreation > 1) + chat <- chatApi.playerChat.findIf(game.id.into(ChatId), game.secondsSinceCreation > 1) + filtered = chat.copy(lines = chat.lines.filterNot(l => l.troll || l.deleted)) lines <- lila.chat.JsonView.asyncLines(chat) yield Chat.Restricted(chat, lines, restricted = game.sourceIs(_.Lobby) && !isAuth).some From 64c9ce0cb3aa6858bacf7962d4deccaf2e4385f9 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 15 Nov 2024 11:11:51 +0100 Subject: [PATCH 8/8] always ignore the first quick resign of the day --- modules/playban/src/main/PlaybanApi.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/playban/src/main/PlaybanApi.scala b/modules/playban/src/main/PlaybanApi.scala index 646225dab8e6d..e982e8267e407 100644 --- a/modules/playban/src/main/PlaybanApi.scala +++ b/modules/playban/src/main/PlaybanApi.scala @@ -151,7 +151,7 @@ final class PlaybanApi( val veryQuick = (game.clock.fold(600)(_.estimateTotalSeconds / 3)).atMost(60) game.durationSeconds.exists(_ < veryQuick) } && { - game.mode.rated || game.loserUserId.exists(loser => !quickResignCasualOnce(loser)) + game.loserUserId.exists(loser => !quickResignCasualOnce(loser)) } private def handleQuickResign(game: Game, userId: UserId): Funit =