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 ) 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 = 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 diff --git a/ui/chess/src/moveRootCtrl.ts b/ui/chess/src/moveRootCtrl.ts index b81684610d024..9aa7f6fc2fdff 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; + confirmMoveToggle?: () => boolean; } export interface MoveUpdate { diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index 241351a489605..a19346eb2cf49 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,13 @@ export default class RoundController implements MoveRootCtrl { this.keyboardMove?.update({ fen, canMove: this.canMove() }); }; - sendMove = (orig: Key, dest: Key, prom: Role | undefined, meta: CgMoveMetadata): void => { + 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.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); @@ -357,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.data.pref.submitMove && this.confirmMoveToggle() && !isPredrop) { + if (this.confirmMoveToggle() && !isPredrop) { this.toSubmit = drop; this.redraw(); } else { 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; } 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( diff --git a/ui/voice/src/move/voice.move.ts b/ui/voice/src/move/voice.move.ts index 6e6fb1a8bacfe..35966aaccf878 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.confirmMoveToggle?.())) { 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; }