From f867e4011d38e46a4a22e882b940d186416fd9e2 Mon Sep 17 00:00:00 2001 From: FawzyAshraf Date: Wed, 25 Dec 2024 18:15:09 +0200 Subject: [PATCH 01/27] Send notification to player when opponent time is out --- ui/round/src/ctrl.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index da498a0f2fe92..9e52423fa6152 100644 --- a/ui/round/src/ctrl.ts +++ b/ui/round/src/ctrl.ts @@ -584,6 +584,13 @@ export default class RoundController implements MoveRootCtrl { wakeLock.release(); if (this.data.game.status.name === 'started') site.sound.saySan(this.stepAt(this.ply).san, false); else site.sound.say(viewStatus(this), false, false, true); + if ( + !d.player.spectator && + o.status.name === 'outoftime' && + this.chessground.state.turnColor === d.opponent.color + ) { + notify(viewStatus(this)); + } }; challengeRematch = async (): Promise => { From ad764b069105611038416a0d6bd186c672d31367 Mon Sep 17 00:00:00 2001 From: superuser-does <32982579+superuser-does@users.noreply.github.com> Date: Thu, 26 Dec 2024 09:45:05 +0000 Subject: [PATCH 02/27] Removed languages at 3% or lower completion Spoken languages removed: Arpitan, Assamese, Javanese, Kyrgyz, Tajik, Yoruba Archaic and constructed languages removed: Ido, Sanskrit --- modules/i18n/src/main/LangList.scala | 8 -------- 1 file changed, 8 deletions(-) diff --git a/modules/i18n/src/main/LangList.scala b/modules/i18n/src/main/LangList.scala index f6572897b0e8c..66ce29925038e 100644 --- a/modules/i18n/src/main/LangList.scala +++ b/modules/i18n/src/main/LangList.scala @@ -11,7 +11,6 @@ object LangList extends lila.core.i18n.LangList: Lang("af", "ZA") -> "Afrikaans", Lang("an", "ES") -> "Aragonés", Lang("ar", "SA") -> "العربية", - Lang("as", "IN") -> "অসমীয়া", Lang("ast", "ES") -> "Asturianu", Lang("av", "DA") -> "авар мацӀ", Lang("az", "AZ") -> "Azərbaycanca", @@ -38,7 +37,6 @@ object LangList extends lila.core.i18n.LangList: Lang("fi", "FI") -> "Suomen kieli", Lang("fo", "FO") -> "Føroyskt", Lang("fr", "FR") -> "Français", - Lang("frp", "IT") -> "Arpitan", Lang("fy", "NL") -> "Frysk", Lang("ga", "IE") -> "Gaeilge", Lang("gd", "GB") -> "Gàidhlig", @@ -52,19 +50,16 @@ object LangList extends lila.core.i18n.LangList: Lang("hy", "AM") -> "Հայերեն", Lang("ia", "IA") -> "Interlingua", Lang("id", "ID") -> "Bahasa Indonesia", - Lang("io", "EN") -> "Ido", Lang("is", "IS") -> "Íslenska", Lang("it", "IT") -> "Italiano", Lang("ja", "JP") -> "日本語", Lang("jbo", "EN") -> "Lojban", - Lang("jv", "ID") -> "Basa Jawa", Lang("ka", "GE") -> "ქართული", Lang("kab", "DZ") -> "Taqvaylit", Lang("kk", "KZ") -> "қазақша", Lang("kmr", "TR") -> "Kurdî (Kurmancî)", Lang("kn", "IN") -> "ಕನ್ನಡ", Lang("ko", "KR") -> "한국어", - Lang("ky", "KG") -> "кыргызча", Lang("la", "LA") -> "Lingua Latina", Lang("lb", "LU") -> "Lëtzebuergesch", Lang("lt", "LT") -> "Lietuvių kalba", @@ -86,7 +81,6 @@ object LangList extends lila.core.i18n.LangList: Lang("ro", "RO") -> "Română", Lang("ru", "RU") -> "русский язык", Lang("ry", "UA") -> "Русинська бисїда", - Lang("sa", "IN") -> "संस्कृत", Lang("sk", "SK") -> "Slovenčina", Lang("sl", "SI") -> "Slovenščina", Lang("so", "SO") -> "Af Soomaali", @@ -95,7 +89,6 @@ object LangList extends lila.core.i18n.LangList: Lang("sv", "SE") -> "Svenska", Lang("sw", "KE") -> "Kiswahili", Lang("ta", "IN") -> "தமிழ்", - Lang("tg", "TJ") -> "тоҷикӣ", Lang("th", "TH") -> "ไทย", Lang("tk", "TM") -> "Türkmençe", Lang("tl", "PH") -> "Tagalog", @@ -105,7 +98,6 @@ object LangList extends lila.core.i18n.LangList: Lang("ur", "PK") -> "اُردُو", Lang("uz", "UZ") -> "oʻzbekcha", Lang("vi", "VN") -> "Tiếng Việt", - Lang("yo", "NG") -> "Yorùbá", Lang("zh", "CN") -> "中文", Lang("zh", "TW") -> "繁體中文", Lang("zu", "ZA") -> "isiZulu" From 09822641e1cce954a6c39078c5ef0fc6eebe10b5 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Thu, 26 Dec 2024 19:42:31 +0100 Subject: [PATCH 03/27] fix premature clock start on broadcast previews when the `WhiteClock` tag is set --- ui/analyse/src/study/multiBoard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/analyse/src/study/multiBoard.ts b/ui/analyse/src/study/multiBoard.ts index b388e9108658b..85ce3b0d0e038 100644 --- a/ui/analyse/src/study/multiBoard.ts +++ b/ui/analyse/src/study/multiBoard.ts @@ -291,7 +291,7 @@ export const renderClock = (chapter: ChapterPreview, color: Color) => { const computeTimeLeft = (preview: ChapterPreview, color: Color): number | undefined => { const clock = preview.players?.[color]?.clock; if (notNull(clock)) { - if (defined(preview.lastMoveAt) && fenColor(preview.fen) === color) { + if (defined(preview.lastMoveAt) && defined(preview.lastMove) && fenColor(preview.fen) === color) { const spent = (Date.now() - preview.lastMoveAt) / 1000; return Math.max(0, clock / 100 - spent); } else { From d973e91fbe5336fcbb8eb1512a1ff239cb088639 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Thu, 26 Dec 2024 14:12:42 -0600 Subject: [PATCH 04/27] always close Dialog instance when closing modal --- ui/lobby/src/setupCtrl.ts | 11 ++++------- ui/lobby/src/view/setup/modal.ts | 10 +++++++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ui/lobby/src/setupCtrl.ts b/ui/lobby/src/setupCtrl.ts index dfaa671f2075d..5557d4792820a 100644 --- a/ui/lobby/src/setupCtrl.ts +++ b/ui/lobby/src/setupCtrl.ts @@ -197,10 +197,7 @@ export default class SetupController { this.loadPropsFromStore(forceOptions); }; - closeModal = () => { - this.gameType = null; - this.root.redraw(); - }; + closeModal?: () => void; // managed by view/setup/modal.ts validateFen = debounce(() => { const fen = this.fen(); @@ -292,7 +289,7 @@ export default class SetupController { const poolMember = this.hookToPoolMember(color); if (poolMember) { this.root.enterPool(poolMember); - this.closeModal(); + this.closeModal?.(); return; } @@ -330,13 +327,13 @@ export default class SetupController { if (response.status === 403) { // 403 FORBIDDEN closes this modal because challenges to the recipient // will not be accepted. see friend() in controllers/Setup.scala - this.closeModal(); + this.closeModal?.(); } } else if (redirected) { location.href = url; } else { this.loading = false; - this.closeModal(); + this.closeModal?.(); } }; } diff --git a/ui/lobby/src/view/setup/modal.ts b/ui/lobby/src/view/setup/modal.ts index b4344a0d7c184..d86ae5ff76661 100644 --- a/ui/lobby/src/view/setup/modal.ts +++ b/ui/lobby/src/view/setup/modal.ts @@ -18,9 +18,17 @@ export default function setupModal(ctrl: LobbyController): MaybeVNode { return snabDialog({ class: 'game-setup', css: [{ hashed: 'lobby.setup' }], - onClose: setupCtrl.closeModal, + onClose: () => { + setupCtrl.closeModal = undefined; + setupCtrl.gameType = null; + setupCtrl.root.redraw(); + }, modal: true, vnodes: [...views[setupCtrl.gameType](ctrl), ratingView(ctrl)], + onInsert: dlg => { + setupCtrl.closeModal = dlg.close; + dlg.show(); + }, }); } From 39749730974301020108d3cce3a6c8b1c7692aa4 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Thu, 26 Dec 2024 22:57:27 +0100 Subject: [PATCH 05/27] account for source polling lag in transmission added delay --- modules/relay/src/main/RelayDelay.scala | 2 +- modules/relay/src/main/RelayRound.scala | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/relay/src/main/RelayDelay.scala b/modules/relay/src/main/RelayDelay.scala index 9c380bd031260..cdcb7aa8b2d7d 100644 --- a/modules/relay/src/main/RelayDelay.scala +++ b/modules/relay/src/main/RelayDelay.scala @@ -19,7 +19,7 @@ final private class RelayDelay(colls: RelayColls)(using Executor): ): Fu[RelayGames] = dedupCache(url, round, () => doFetchUrl(url)) .flatMap: latest => - round.sync.delay match + round.sync.delayMinusLag match case Some(delay) if delay > 0 => store.get(url, delay).map(_ | latest.map(_.resetToSetup)) case _ => fuccess(latest) diff --git a/modules/relay/src/main/RelayRound.scala b/modules/relay/src/main/RelayRound.scala index 6d6b7d62a4029..d2fafff3097e4 100644 --- a/modules/relay/src/main/RelayRound.scala +++ b/modules/relay/src/main/RelayRound.scala @@ -126,7 +126,11 @@ object RelayRound: def addLog(event: SyncLog.Event) = copy(log = log.add(event)) def clearLog = copy(log = SyncLog.empty) - def nonEmptyDelay = delay.filter(_.value > 0) + // subtract estimated source polling lag from transmission delay + private def pollingLag = Seconds(if isPush then 1 else 6) + def delayMinusLag = delay.map(_ - pollingLag) + + def nonEmptyDelay = delayMinusLag.filter(_.value > 0) def hasDelay = nonEmptyDelay.isDefined override def toString = upstream.toString From d4e2493c1beda4dccc339f3d8198404fb417446c Mon Sep 17 00:00:00 2001 From: Jonathan Gamble <101470903+schlawg@users.noreply.github.com> Date: Thu, 26 Dec 2024 16:22:28 -0600 Subject: [PATCH 06/27] different color for equal position --- ui/chess/src/glyphs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/chess/src/glyphs.ts b/ui/chess/src/glyphs.ts index ea2dc03da40da..f17cd17020571 100644 --- a/ui/chess/src/glyphs.ts +++ b/ui/chess/src/glyphs.ts @@ -91,7 +91,7 @@ const glyphToSvg: Dictionary = { // Equal position '=': composeGlyph( - '#f5918f', + '#82c2ef', '', ), From f6a1193a8c6a646cef38811f8af9572574c76922 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 08:16:54 +0100 Subject: [PATCH 07/27] New Crowdin updates (#16669) * New translations: site.xml (Northern Sotho) * New translations: site.xml (Hausa) * New translations: site.xml (Northern Sotho) * New translations: site.xml (Hausa) * New translations: site.xml (Malayalam) * New translations: study.xml (Bulgarian) * New translations: streamer.xml (Bulgarian) * New translations: arena.xml (Bengali) * New translations: site.xml (Persian) * New translations: broadcast.xml (Persian) * New translations: site.xml (Kurmanji (Kurdish)) * New translations: site.xml (Kurmanji (Kurdish)) * New translations: timeago.xml (Croatian) * New translations: onboarding.xml (Croatian) * New translations: site.xml (Croatian) * New translations: broadcast.xml (Croatian) * New translations: oauthscope.xml (Croatian) * New translations: appeal.xml (Croatian) * New translations: site.xml (Kurmanji (Kurdish)) --- translation/dest/appeal/hr-HR.xml | 18 +++++++++++++++- translation/dest/arena/bn-BD.xml | 2 ++ translation/dest/broadcast/fa-IR.xml | 4 ++-- translation/dest/broadcast/hr-HR.xml | 31 +++++++++++++++++++++++++++ translation/dest/oauthScope/hr-HR.xml | 3 ++- translation/dest/onboarding/hr-HR.xml | 17 ++++++++++++++- translation/dest/site/fa-IR.xml | 4 ++-- translation/dest/site/ha-NG.xml | 2 ++ translation/dest/site/hr-HR.xml | 5 +++++ translation/dest/site/kmr-TR.xml | 20 +++++++++++++++++ translation/dest/site/ml-IN.xml | 16 ++++++++++---- translation/dest/site/nso-ZA.xml | 2 ++ translation/dest/streamer/bg-BG.xml | 2 ++ translation/dest/study/bg-BG.xml | 1 + translation/dest/timeago/hr-HR.xml | 10 +++++++++ 15 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 translation/dest/site/ha-NG.xml create mode 100644 translation/dest/site/nso-ZA.xml diff --git a/translation/dest/appeal/hr-HR.xml b/translation/dest/appeal/hr-HR.xml index 5aa5c5d675845..9709dd245426e 100644 --- a/translation/dest/appeal/hr-HR.xml +++ b/translation/dest/appeal/hr-HR.xml @@ -1,4 +1,20 @@ - Tvoj profil su zatvorili moderatori. + Vaš korisnički račun je označen zbog vanjske pomoći u partijama. + Definiramo ovo kao korištenje bilo koje vanjske pomoći u znanju i/ili računanju u svrhu postignuća nepoštene prednosti nad Vašim protivnikom. Pogledajte %s stranicu za više detalja. + Vašem korisničkom računu je zabranjen pristup arenama. + Vašem korisničkom računu je zabranjen pristup turnirima sa stvarnim nagradama. + Vaš korisnički račun je označen zbog manipuliranja rejtinga. + Namjernu manipulaciju rejtinga definiramo kao namjerno gubljenje partija ili igranje s protivnikom koji namjerno gubi partije. + Vaš korisnički račun je ušutkan. + Pročitajte naš %s. Kršeći komunikacijske smjernice može rezultirati ušutkavanjem Vašeg korisničkog računa. + Vaš korisnički račun je izuzet s ljestvice. + Definiramo ovo kao bilo koji nepošten način za doći na ljestvicu. + Vaš korisnički račun je zatvoren od strane moderatora. + Vaši blogovi su sakriveni od strane moderatora. + Pročitajte ponovno naše %s. + Imate vremensko ograničenje igranja. + komunikacijske smjernice + pravila bloga + Fair Play diff --git a/translation/dest/arena/bn-BD.xml b/translation/dest/arena/bn-BD.xml index 93771bdca113b..fa5338268eceb 100644 --- a/translation/dest/arena/bn-BD.xml +++ b/translation/dest/arena/bn-BD.xml @@ -54,6 +54,8 @@ এরিনায় ধারাবাহিক জয় ২টি জয়ের পর প্রতি ধারাবাহিক জয়ের জন্য ২ পয়েন্টের বদলে ৪ পয়েন্ট দেয়া হবে। আপনার দল বাছাই করুন + আপনাকে একটি দলে যোগ দিতে হবে! + তৈরী হয়েছে মোট শুধুমাত্র খেতাবধারী দাবাড়ুগণ diff --git a/translation/dest/broadcast/fa-IR.xml b/translation/dest/broadcast/fa-IR.xml index debe3ec660d86..efca650098498 100644 --- a/translation/dest/broadcast/fa-IR.xml +++ b/translation/dest/broadcast/fa-IR.xml @@ -38,7 +38,7 @@ ویرایش مطالعه دور حذف این مسابقات کل مسابقات، شامل همه دورها و بازی‌هایش را به طور کامل حذف کن. - نمایش امتیاز بازیکنان بر پایه نتیجه بازی‌ها + نمایش امتیاز بازیکنان بر پایه نتیجه بازی‌ها اختیاری: عوض کردن نام، درجه‌بندی و عنوان بازیکنان کشورگان‌های فیده ده درجه‌بندی برتر @@ -57,7 +57,7 @@ بارگذاری تصویر مسابقات تاکنون هیچی. وقتی بازی‌ها بارگذاری شدند، میزها پدیدار خواهند شد. میزها را می‌توان از یک منبع یا از راه %s بارگذاری کرد - پخش زنده به زودی آغاز خواهد شد. + پخش زنده به زودی خواهد آغازید. پخش زنده هنوز نیاغازیده است. وبگاه رسمی رده‌بندی diff --git a/translation/dest/broadcast/hr-HR.xml b/translation/dest/broadcast/hr-HR.xml index 9765ee852e055..8b188c079fe0a 100644 --- a/translation/dest/broadcast/hr-HR.xml +++ b/translation/dest/broadcast/hr-HR.xml @@ -49,4 +49,35 @@ Federacija Starost ove godine Nerangiran + Nedavni turniri + Ekipe + Ploče + Pregled + Pretplatite se kako bi bili obaviješteni o početku runde. Možete uključiti obavijesti za prijenose u postavkama korisničkog računa. + Učitajte sliku turnira + Još nema ploča. Pojavit će se kad se partije učitaju. + Ploče mogu biti učitane iz izvora ili preko %s + Počinje nakon %s + Prijenos počinje uskoro. + Prijenos još nije počeo. + Službena stranica + Tablica + Službena tablica + Više mogučnosti na %s + portal za vlasnike web stranica + Javan izvorni PGN za ovu rundu u stvarnom vremenu. Također nudimo %s za bržu i efikasniju sinkronizaciju. + Ugradite ovaj prijenos na svoju web stranicu + Ugradi %s u svoju web stranicu + Razlika rejtinga + Partije u ovom turniru + Rezultat + Sve ekipe + Format turnira + Mjesto turnira + Najbolji igrači + Vremenska zona + FIDE rejting kategorija + Neobavezni detalji + Prijašnji prijenosi + Pogledaj sve prijenose prema mjesecu diff --git a/translation/dest/oauthScope/hr-HR.xml b/translation/dest/oauthScope/hr-HR.xml index 5aeef17794ac2..5d75b2e8b6685 100644 --- a/translation/dest/oauthScope/hr-HR.xml +++ b/translation/dest/oauthScope/hr-HR.xml @@ -33,7 +33,7 @@ Koristite moderatorske alate (unutar granica vašeg dopuštenja) Osobni API pristupni bonovi Možeš zatražiti OAuth zahtijev bez da prolaziš kroz %s. - proces autorizacije koda + proces autorizacije koda Umjesto toga, %s kojeg možeš izravno koristiti u API zahtijevima. stvori osobni pristupni bon Pažljivo čuvaj bonove. Oni su poput lozinke. Prednost korištenja bonova nasuprot postavljanja lozinke u skriptu je ta što se bonovi mogu opozvati te ih možeš generirati gomilu. @@ -48,6 +48,7 @@ Napomena za razvojne programere: Moguće je unaprijed ispuniti ovaj obrazac podešavanjem parametara upita URL-a. Na primjer: %s + označi %1$s i %2$s kateogrije i postavi opis bon. Kodovi opsega mogu se pronaći u HTML kodu obrasca. Davanje ovih unaprijed ispunjenih URL-ova vašim korisnicima pomoći će im da dobiju prave opsege bona. diff --git a/translation/dest/onboarding/hr-HR.xml b/translation/dest/onboarding/hr-HR.xml index 3ea04e700dfa8..e12c3ff166c43 100644 --- a/translation/dest/onboarding/hr-HR.xml +++ b/translation/dest/onboarding/hr-HR.xml @@ -1,2 +1,17 @@ - + + Dobrodošli! + Dobrodošli na Lichess.org! + Ovo je Vaša profilna stranica. + Hoće li dijete koristiti ovaj korisnički račun? Možda bi htjeli uključiti %s. + Što sad? Evo par prijedloga: + Naučite pravila šaha + Poboljšajte se rješavajući zadatke sa šahovskim taktikama. + Igrajte s Umjetnom Inteligencijom. + Igrajte protiv ljudi diljem svijeta. + Pratite svoje prijatelje na Lichessu. + Igrajte u turnirima. + Učite iz %1$s i %2$s. + Podesite Lichess prema svojim željama. + Istražite stranicu i zabavite se :) + diff --git a/translation/dest/site/fa-IR.xml b/translation/dest/site/fa-IR.xml index 86f61bfdfba2b..f7786ae4754ae 100644 --- a/translation/dest/site/fa-IR.xml +++ b/translation/dest/site/fa-IR.xml @@ -936,7 +936,7 @@ مدیریت جریان‌سازی لغو مسابقه توضیحات مسابقه - نکته خاصی را می‌خواهید به شرکت‌کنندگان گویید؟ بکوشید کوتاه باشد. پیوندهای فرونشان موجودند: + نکته خاصی را می‌خواهید به شرکت‌کنندگان گویید؟ بکوشید کوتاه باشد. پیوندهای فرونشان موجودند: [name](https://url) بازی‌ها رسمی هستند و روی درجه‌بندی بازیکنان تاثیر می‌گذارند @@ -982,7 +982,7 @@ شما یک بازی در حال انجام با %s دارید. انصراف از بازی تسلیم - شما نمی توانید تا زمانی که این بازی تمام نشده بازی جدیدی آغاز کنید. + تا وقتی که این بازی تمام نشده، نمی‌توانید بازی جدیدی را بیاغازید. از وقتی که تا وقتی که بازی‌های رسمی برگزاریده در Lichess diff --git a/translation/dest/site/ha-NG.xml b/translation/dest/site/ha-NG.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/site/ha-NG.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/site/hr-HR.xml b/translation/dest/site/hr-HR.xml index 70f85b093576b..3c9c78103c3fb 100644 --- a/translation/dest/site/hr-HR.xml +++ b/translation/dest/site/hr-HR.xml @@ -375,6 +375,11 @@ %s studije %s studija + + %s simultanka + %s simultanke + %s simultanki + Pogledaj turnir Povratak na turnir U švicarskom turniru neriješen rezultat nije moguć prije 30. poteza. diff --git a/translation/dest/site/kmr-TR.xml b/translation/dest/site/kmr-TR.xml index 94ab7ab265e46..29fb35a7046fd 100644 --- a/translation/dest/site/kmr-TR.xml +++ b/translation/dest/site/kmr-TR.xml @@ -70,6 +70,8 @@ Varyasyonê bipêşîne Bike rêza esasî Ji vir pê de jê bibe + Varyasyonan teng bike + Varyasyonan fireh bike Zorê bide bo vê varyasyonê PGNa varyasyonê kopî bike Hemle @@ -101,8 +103,12 @@ Kaşifê destpêkê Geroka despêk/dawîyê %s kaşifê destpêkê + Hemleya pêşî ya kaşîfê vekirin/dawîya lîstikê bilîze Ji ber qaîdeya 50 hemleyê rê li ber serkeftinê hate girtin Rê li ber mexlubiyetê hate girtin ji ber qaîdeya 50 hemleyê + Qezenckirin an 50 hemleyên ji ber xetayên berê + Qeybkirin an 50 hemleyên ji ber xetayên berê + Qezenckirin/qeybkirin tenê gava ku xetên di binkeya tabloyê de pêşnîyarkirî piştî zeftkirin an hemleya pîyonê yê dawî û pê ve were şopandin tê garantîkirin, ji ber giloverkirina nirxên DTZyê yên di binkeyên tabloyê ya Syzygy de ya muhtemel. Temam e! PGNyê bîne navê Jê bibe @@ -112,6 +118,7 @@ Bi CPL Biçalakîne Nîşandana hemleya çêtirîn + Tîrikên varyasyonê nîşan bide Nîşandera nirxandinê Varyantên pirhejmar CPU @@ -143,11 +150,17 @@ %s lîzer %s lîzer + Beranberîya bi qebûla herdu alîyan + Pêncî hemleyên bêyî pêşketinek Lîstikên vê gavê %s lîstik %s lîstik + + %1$s puanên ji ser %2$s lîstikan + %1$s puanên ji ser %2$s lîstikan + %s cihnîşan %s cihnîşan @@ -165,6 +178,7 @@ Şandiyên forumê yên dawîn Lîzer Hevalan + lîstikvanên din Nîqaşan Îro Doh @@ -184,6 +198,10 @@ %s demjimêr %s demjimêr + + %s deqe + %s deqe + Dem Pile Statîstîkên pileyan @@ -364,6 +382,7 @@ Ya reş bi hemleyekê şahmat dike Cardin biceribîne Cardin tê girêdan + Offlayn %s heval serhêl e %s heval serhêl in @@ -428,6 +447,7 @@ Hemleyên lîstî Serkeftinên spî Serkeftinên reş + Rêjeya wekhevmayînê Wekhevî Pêşbirka li dû %s: Dijbera averaj diff --git a/translation/dest/site/ml-IN.xml b/translation/dest/site/ml-IN.xml index 037312962970e..17187d41f2b34 100644 --- a/translation/dest/site/ml-IN.xml +++ b/translation/dest/site/ml-IN.xml @@ -103,6 +103,7 @@ എക്സ്പ്ലോററും ടേബിൾ ബേസും തുറക്കാം ഓപ്പണിങ്ങ്/എൻഡ്ഗെയിം എക്‌സ്‌പ്ലോറർ %s ഓപ്പണിങ് എക്സ്പ്ലോറർ + ആദ്യഘട്ടം/അവസാനഘട്ടം-ആരായകൻ നീക്കം കളിക്കുക 50 നീക്കങ്ങളുടെ നിയമം പ്രകാരം ജയം തടയപ്പെട്ടു 50 നീക്കങ്ങളുടെ നിയമം പ്രകാരം തോൽവി തടയപ്പെട്ടു വിജയം അല്ലെങ്കിൽ മുൻപറ്റിയ പിഴകൊണ്ടു് 50 നീക്കങ്ങളിൽ സമനില @@ -167,9 +168,9 @@ രജിസ്റ്റര്‍ കമ്പ്യൂട്ടറുകള്‍ക്കും കമ്പ്യൂട്ടര്‍ സഹായമുള്ള കളിക്കാര്‍ക്കും കളിക്കാന്‍ അനുവാദമില്ല. ദയവായി കളിക്കിടെ ചെസ്സ്‌ എഞ്ചിനുകളുടെയോ ഡാറ്റാബേസുകളുടെയോ മറ്റു കളിക്കാരുടെയോ സഹായം തേടരുത്. നിരവധി അക്കൗണ്ടുകളുണ്ടാക്കുന്നത് ശക്തമായി നിരുത്സാഹപ്പെടുത്തുകയും അങ്ങനെ ചെയ്താല്‍ വിലക്കേര്‍പ്പെടുത്തും എന്നും പ്രത്യേകം ശ്രദ്ധിക്കുക. കളികള്‍ - ഫോറം + ചർച്ചാവേദി %1$s കുറിച്ചു %2$s എന്ന വിഷയത്തെ പറ്റി - ഏറ്റവും പുതിയ ഫോറം പോസ്റ്റുകള്‍ + ഏറ്റവും പുതിയ കൂട്ടായ്മ പോസ്റ്റുകള്‍ കളിക്കാര് കൂട്ടുകാർ മറ്റേ കളിക്കാർ @@ -214,6 +215,7 @@ പാസ്‌വേഡ് മറന്നുപോയോ? ഈ രഹസ്യവാക്ക് ഊഹിക്കുകാൻ വളരെ എളുപ്പമാണ്. ദയവായി താങ്ങളുടെ ഉപയോക്തൃനാമം രഹസ്യവാക്കയായി ഉപയോഗിക്കരുത്. + താങ്ങൾ ഇതേ രഹസ്യവാക്കു് മറ്റൊരു വെബ്സ്ഥാനത്തിൽ ഉപയോഗിച്ചിട്ടുണ്ടു് പക്ഷേ ആ വെബ്സ്ഥാനത്തിന്റെ ഒത്തുതീർപ്പു് നടന്നിരിക്കുന്നു. താങ്ങളുടെ Lichess അക്കൗണ്ടിന്റെ സുരക്ഷക്കു വേണ്ടി താങ്ങൾക്കു് ഒരു പുതിയ രഹസ്യവാക്കു് ഇടണ്ടി വരും. സഹകരിക്കുന്നതിനു് നന്ദി. താങ്ങൾ Lichess വിടുകയാണ് ഒരിക്കലും താങ്ങളുടെ Lichess രഹസ്യവാക്കു് മറ്റെ വെബ്സ്ഥാനത്തിൽ എഴുതരുതു്! %s വരെ ചെല്ലുക @@ -380,6 +382,7 @@ സൗജന്യ തൽസമയം ചെസ്സ്‌ സെര്‍വര്‍. ഇപ്പോള്‍ മികച്ച രീതിയില്‍ ചെസ്സ്‌ കളിക്കാം. രജിസ്റ്റര്‍ ചെയ്യണ്ട, പരസ്യങ്ങള്‍ ഇല്ല, പ്ലഗിനുകള്‍ വേണ്ട. കമ്പ്യൂട്ടറുമായോ സുഹൃത്തുക്കളുമായോ പുതിയ എതിരാളികളുമായോ ചെസ്സ്‌ കളിക്കാം. %1$s സംഘത്തിൽ %2$s അംഗമായി %1$s നിർമിച്ച %2$s സംഘം + സംപ്രേക്ഷണം ആരംഭിച്ചിരിക്കുന്നു %s സംപ്രേക്ഷണം ആരംഭിച്ചിരിക്കുന്നു ശരാശരി നിലവാരം സ്ഥലം @@ -398,6 +401,8 @@ പഠനം മത്സരം അപ്പ്‌ലോഡ് ചെയ്യുക ഒരു ഗെയിം PGN പേസ്റ്റ് ചെയ്യുമ്പോൾ ബ്രൗസ്‌ ചെയ്യാൻ പറ്റുന്ന റീപ്ലേ, കമ്പ്യൂട്ടർ വിശകലനം, ഗെയിം ചാറ്റ്, ഷെയർ ചെയ്യാൻ പറ്റുന്ന ഒരു URL എന്നിവ ലഭിക്കുന്നു. + വ്യതിയാനങ്ങൾ മായ്ക്കപ്പെടും. അവയെ വയ്ക്കാനായി PGN ഒരു പഠനം വഴി ഇറക്കുമതിക്കുക. + ഈ PGN എല്ലാവൎക്കും കാണാം. ഈ കളി സ്വകാര്യമായി ഇറക്കുമതിക്കാൻ ഒരു പഠനം ഉപയോഗിക്കുക. ഇറക്കിയ കളികൾ %s ഇറക്കിയ കളികൾ %s @@ -504,6 +509,7 @@ സമൂഹമാധ്യമ ലിങ്കുകൾ ഒരു വരിയിൽ ഒരു വിലാസം. ഇൻഫയൽ നോട്ടെഷൻ + കരുതിവയ്ക്കാനും പങ്കിടാനും ഒരു പഠനം ഉണ്ടാക്കാൻ പരിഗണയിൽ എടുക്കുക. നീക്കങ്ങൾ കളയുക Lichess TV യിൽ നേരെത്തെയുള്ളത് തൽസമയ കളിക്കാർ @@ -875,6 +881,8 @@ ഞാൻ Lichess പോളിസികൾ അനുകൂലിക്കുന്നതായി സമ്മതിക്കുന്നു. കണ്ടുപിടിക്കുക അല്ലെങ്കിൽ പുതിയൊരു സംഭാഷണം ആരംഭിക്കുക മാറ്റുക + വെടിയുണ്ടകളി + മിന്നൽകളി റാപിഡ് ക്ലാസിക്കല്‍ തീവ്രവേഗത്തിലുള്ള കളികള്‍: 30 സെക്കന്‍റില്‍ താഴെ @@ -897,8 +905,8 @@ %1$s ഇൽ ചേരുക, ഫോറത്തിൽ പോസ്റ്റ്‌ ചെയുന്നതിനു %1$s ടീം ഇപ്പോൾ ഈ ഫോറത്തിൽ നിങ്ങൾക്കു പോസ്റ്റ്‌ ചെയ്യാൻ കഴിയുകയില്ല കുറച്ചു ഗ്യാമുകൾക്കു ശേഷം ശ്രമിക്കുക! - സബ്സ്ക്രൈബ് - അൺസബ്സ്ക്രൈബ് + പേരെഴുതുക + പേർമാറ്റുക %1$s ഇൽ നിങ്ങളെ പരാമർശിച്ചു. %2$s നിങ്ങളെ %1$s പരാമർശിച്ചു. %1$s ഇലേക്കു നിങ്ങളെ ക്ഷണിക്കുന്നു. diff --git a/translation/dest/site/nso-ZA.xml b/translation/dest/site/nso-ZA.xml new file mode 100644 index 0000000000000..3ea04e700dfa8 --- /dev/null +++ b/translation/dest/site/nso-ZA.xml @@ -0,0 +1,2 @@ + + diff --git a/translation/dest/streamer/bg-BG.xml b/translation/dest/streamer/bg-BG.xml index 4dfdc51015ba2..2a5aeb1872852 100644 --- a/translation/dest/streamer/bg-BG.xml +++ b/translation/dest/streamer/bg-BG.xml @@ -28,8 +28,10 @@ Вашият стрийм е одобрен. Вашият стрийм се преглежда от модератори. Моля, попълнете информацията за своя стрийм и качете снимка. + Изпратете за преглед Страницата за стрийминг на Lichess е насочена към вашата аудитория с езика, предоставен от вашата платформа за стрийминг. Задайте правилния език по подразбиране за вашите шахматни стриймове в приложението или услугата, която използвате за излъчване. Вашето потребителско име в Twitch или URL адрес + Изисква се Twitch или YouTube ID на канала Ви в YouTube Име на вашата стрийм страница в Lichess Видимо на публичната страницата със стриймъри diff --git a/translation/dest/study/bg-BG.xml b/translation/dest/study/bg-BG.xml index fa134eb0d6572..e82197c89deae 100644 --- a/translation/dest/study/bg-BG.xml +++ b/translation/dest/study/bg-BG.xml @@ -166,4 +166,5 @@ Играйте отново Какво бихте играли в тази позиция? Поздравления! Вие завършихте този урок. + %s на страница diff --git a/translation/dest/timeago/hr-HR.xml b/translation/dest/timeago/hr-HR.xml index 18de0577bdb74..534880692699d 100644 --- a/translation/dest/timeago/hr-HR.xml +++ b/translation/dest/timeago/hr-HR.xml @@ -67,5 +67,15 @@ prije %s godine prije %s godina + + Preostala %s minuta + Preostale %s minute + Preostalo %s minuta + + + Preostao %s sat + Preostala %s sata + Preostalo %s sati + završeno From 895db3de2d2b76d0cdfecd62a06318b5b1b7ea25 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 08:33:49 +0100 Subject: [PATCH 08/27] Revert "REVERT ME - bc for vapid key in standard base64 alphabet" This reverts commit e53e7beb5adc98120706a9abcb7b0c928f47496f. --- ui/site/src/serviceWorker.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/ui/site/src/serviceWorker.ts b/ui/site/src/serviceWorker.ts index 872a816c28044..e1e2c08635f97 100644 --- a/ui/site/src/serviceWorker.ts +++ b/ui/site/src/serviceWorker.ts @@ -2,10 +2,6 @@ import { url as assetUrl, jsModule } from './asset'; import { log } from 'common/permalog'; import { storage } from 'common/storage'; -function makeUrlSafe(base64: string): string { - return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); -} - export default async function () { if (!('serviceWorker' in navigator && 'Notification' in window && 'PushManager' in window)) return; const workerUrl = new URL( @@ -25,10 +21,7 @@ export default async function () { if (!vapid || Notification.permission !== 'granted') return store.remove(); else if (sub && !resub) return; - newSub = await reg.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: makeUrlSafe(vapid), - }); + newSub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: vapid }); if (!newSub) throw new Error(JSON.stringify(await reg.pushManager.permissionState())); From a6fdec337af5bb8c01288368cc9d75762e0e35fc Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 08:38:53 +0100 Subject: [PATCH 09/27] broadcast sync delay tweaks --- modules/relay/src/main/RelayDelay.scala | 2 +- modules/relay/src/main/RelayPush.scala | 2 +- modules/relay/src/main/RelayRound.scala | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/modules/relay/src/main/RelayDelay.scala b/modules/relay/src/main/RelayDelay.scala index cdcb7aa8b2d7d..ae43d28b12e7b 100644 --- a/modules/relay/src/main/RelayDelay.scala +++ b/modules/relay/src/main/RelayDelay.scala @@ -48,7 +48,7 @@ final private class RelayDelay(colls: RelayColls)(using Executor): ) .games .addEffect: games => - if round.sync.hasDelay then store.putIfNew(url, games) + if round.sync.delayMinusLag.isDefined then store.putIfNew(url, games) private object store: diff --git a/modules/relay/src/main/RelayPush.scala b/modules/relay/src/main/RelayPush.scala index 70d63663e567e..248f883800954 100644 --- a/modules/relay/src/main/RelayPush.scala +++ b/modules/relay/src/main/RelayPush.scala @@ -40,7 +40,7 @@ final class RelayPush( parsed.map(_.map(g => Success(g.tags, g.root.mainline.size))) val andSyncTargets = response.exists(_.isRight) - rt.round.sync.nonEmptyDelay + rt.round.sync.delayMinusLag .ifTrue(games.exists(_.root.children.nonEmpty)) .match case None => push(rt, games, andSyncTargets).inject(response) diff --git a/modules/relay/src/main/RelayRound.scala b/modules/relay/src/main/RelayRound.scala index d2fafff3097e4..8b1a575559d4d 100644 --- a/modules/relay/src/main/RelayRound.scala +++ b/modules/relay/src/main/RelayRound.scala @@ -128,10 +128,7 @@ object RelayRound: // subtract estimated source polling lag from transmission delay private def pollingLag = Seconds(if isPush then 1 else 6) - def delayMinusLag = delay.map(_ - pollingLag) - - def nonEmptyDelay = delayMinusLag.filter(_.value > 0) - def hasDelay = nonEmptyDelay.isDefined + def delayMinusLag = delay.map(_ - pollingLag).filter(_.value > 0) override def toString = upstream.toString From f53092f237960c8206a30a335c589628eae0fe89 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 08:56:34 +0100 Subject: [PATCH 10/27] try Int.toLong to fix kamon relay.http.get tag --- modules/common/src/main/mon.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/common/src/main/mon.scala b/modules/common/src/main/mon.scala index 7d4ed6f6b172a..855dd94f1ca85 100644 --- a/modules/common/src/main/mon.scala +++ b/modules/common/src/main/mon.scala @@ -289,7 +289,7 @@ object mon: timer("relay.sync.time").withTags(relay(official, id, slug)) def httpGet(code: Int, host: String, etag: String, proxy: Option[String]) = timer("relay.http.get").withTags: - tags("code" -> code, "host" -> host, "etag" -> etag, "proxy" -> proxy.getOrElse("none")) + tags("code" -> code.toLong, "host" -> host, "etag" -> etag, "proxy" -> proxy.getOrElse("none")) val dedup = counter("relay.fetch.dedup").withoutTags() object bot: From 77bb517427341f1613c7c80f43133f1658e81837 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 09:08:07 +0100 Subject: [PATCH 11/27] remove unused css class --- modules/relay/src/main/ui/RelayTourUi.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/relay/src/main/ui/RelayTourUi.scala b/modules/relay/src/main/ui/RelayTourUi.scala index b0a03e27dffd9..52c9dba177298 100644 --- a/modules/relay/src/main/ui/RelayTourUi.scala +++ b/modules/relay/src/main/ui/RelayTourUi.scala @@ -38,7 +38,7 @@ final class RelayTourUi(helpers: Helpers, ui: RelayUi): nonEmptyTier(_.high), nonEmptyTier(_.normal), h2(cls := "relay-index__section")(trc.pastBroadcasts()), - div(cls := "relay-cards relay-cards--past"): + div(cls := "relay-cards"): past.map: t => card.render(t, live = _ => false) , @@ -150,7 +150,7 @@ final class RelayTourUi(helpers: Helpers, ui: RelayUi): div(cls := "page-menu__content box box-pad")( boxTop(h1(dataIcon := Icon.RadioTower, cls := "text")(trc.broadcastCalendar())), dateForm("top"), - div(cls := "relay-cards relay-cards--past"): + div(cls := "relay-cards"): tours.map(card.renderCalendar) , (tours.sizeIs > 8).option(dateForm("bottom")) From e650456187cb35dfb52667db41484c80cba2c96f Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 09:25:26 +0100 Subject: [PATCH 12/27] better select the study clock to render WIP --- ui/analyse/src/ctrl.ts | 2 +- ui/analyse/src/study/playerBars.ts | 7 +++++++ ui/analyse/src/view/clocks.ts | 7 ++----- ui/analyse/src/view/components.ts | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts index 7c93e687c2a0c..bd791711b24b5 100644 --- a/ui/analyse/src/ctrl.ts +++ b/ui/analyse/src/ctrl.ts @@ -99,7 +99,7 @@ export default class AnalyseCtrl { keyboardHelp: boolean = location.hash === '#keyboard'; threatMode: Prop = prop(false); treeView: TreeView; - treeVersion = 1; // increment to recreate tree + treeVersion = 1; // increment to recreate vnode tree cgVersion = { js: 1, // increment to recreate chessground dom: 1, diff --git a/ui/analyse/src/study/playerBars.ts b/ui/analyse/src/study/playerBars.ts index 5c6ac428dc4e9..1861fe30a5f17 100644 --- a/ui/analyse/src/study/playerBars.ts +++ b/ui/analyse/src/study/playerBars.ts @@ -34,6 +34,13 @@ export default function (ctrl: AnalyseCtrl): VNode[] | undefined { ); } +// The tree node whose clocks are displayed. +// Ongoing game: the last mainline node, no matter what +// Finished game: last mainline node of the current variation. +function selectClockNode(ctrl: AnalyseCtrl): Tree.Node { + return isFinished(ctrl.study.data.chapter) ? ctrl.tree.lastMainlineNode(ctrl.path) : ctrl.tree.root; +} + function renderPlayer( ctrl: AnalyseCtrl, tags: TagArray[], diff --git a/ui/analyse/src/view/clocks.ts b/ui/analyse/src/view/clocks.ts index bd0cf6581cb6c..69e291361c239 100644 --- a/ui/analyse/src/view/clocks.ts +++ b/ui/analyse/src/view/clocks.ts @@ -3,14 +3,11 @@ import type AnalyseCtrl from '../ctrl'; import { isFinished } from '../study/studyChapters'; import { notNull } from 'common'; -export default function renderClocks(ctrl: AnalyseCtrl): [VNode, VNode] | undefined { - const node = ctrl.node, - clock = node.clock; - +export default function renderClocks(ctrl: AnalyseCtrl, node: Tree.Node): [VNode, VNode] | undefined { const whitePov = ctrl.bottomIsWhite(), parentClock = ctrl.tree.getParentClock(node, ctrl.path), isWhiteTurn = node.ply % 2 === 0, - centis: Array = isWhiteTurn ? [parentClock, clock] : [clock, parentClock]; + centis: Array = isWhiteTurn ? [parentClock, node.clock] : [node.clock, parentClock]; if (!centis.some(notNull)) return; diff --git a/ui/analyse/src/view/components.ts b/ui/analyse/src/view/components.ts index 625ec4bff1f2b..6e26643e94063 100644 --- a/ui/analyse/src/view/components.ts +++ b/ui/analyse/src/view/components.ts @@ -440,7 +440,7 @@ function renderPlayerStrips(ctrl: AnalyseCtrl): [VNode, VNode] | undefined { const renderPlayerStrip = (cls: string, materialDiff: VNode, clock?: VNode): VNode => h('div.analyse__player_strip.' + cls, [materialDiff, clock]); - const clocks = renderClocks(ctrl), + const clocks = renderClocks(ctrl, ctrl.node), whitePov = ctrl.bottomIsWhite(), materialDiffs = renderMaterialDiffs(ctrl); From 03276230a1243c1ebdbe71b6fe1be74cc8e80395 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 12:50:56 +0100 Subject: [PATCH 13/27] complete selection of study/broadcast clocks to display --- ui/analyse/src/study/playerBars.ts | 21 +++++++++++++++------ ui/analyse/src/study/studyCtrl.ts | 5 ++++- ui/analyse/src/view/clocks.ts | 14 +++++++------- ui/analyse/src/view/components.ts | 2 +- ui/tree/src/path.ts | 6 ++++++ ui/tree/src/tree.ts | 8 +++----- 6 files changed, 36 insertions(+), 20 deletions(-) diff --git a/ui/analyse/src/study/playerBars.ts b/ui/analyse/src/study/playerBars.ts index 1861fe30a5f17..51b9d8823b2b4 100644 --- a/ui/analyse/src/study/playerBars.ts +++ b/ui/analyse/src/study/playerBars.ts @@ -7,6 +7,8 @@ import type { StudyPlayers, Federation, TagArray } from './interfaces'; import { findTag, isFinished, looksLikeLichessGame, resultOf } from './studyChapters'; import { userTitle } from 'common/userLink'; import RelayPlayers, { fidePageLinkAttrs } from './relay/relayPlayers'; +import { StudyCtrl } from './studyDeps'; +import { intersection } from 'tree/path'; export default function (ctrl: AnalyseCtrl): VNode[] | undefined { const study = ctrl.study; @@ -15,8 +17,8 @@ export default function (ctrl: AnalyseCtrl): VNode[] | undefined { const players = study.currentChapter().players, tags = study.data.chapter.tags, - clocks = renderClocks(ctrl), - ticking = !isFinished(study.data.chapter) && ctrl.turnColor(), + clocks = renderClocks(ctrl, selectClockPath(ctrl, study)), + tickingColor = study.isClockTicking(ctrl.path) && ctrl.turnColor(), materialDiffs = renderMaterialDiffs(ctrl); return (['white', 'black'] as Color[]).map(color => @@ -27,7 +29,7 @@ export default function (ctrl: AnalyseCtrl): VNode[] | undefined { materialDiffs, players, color, - ticking === color, + tickingColor === color, study.data.showRatings || !looksLikeLichessGame(tags), relayPlayers, ), @@ -35,10 +37,17 @@ export default function (ctrl: AnalyseCtrl): VNode[] | undefined { } // The tree node whose clocks are displayed. -// Ongoing game: the last mainline node, no matter what // Finished game: last mainline node of the current variation. -function selectClockNode(ctrl: AnalyseCtrl): Tree.Node { - return isFinished(ctrl.study.data.chapter) ? ctrl.tree.lastMainlineNode(ctrl.path) : ctrl.tree.root; +// Ongoing game: the last mainline node, no matter what +function selectClockPath(ctrl: AnalyseCtrl, study: StudyCtrl): Tree.Path { + const gamePath = ctrl.gamePath || study.data.chapter.relayPath; + return isFinished(study.data.chapter) + ? ctrl.node.clock + ? ctrl.path + : gamePath + ? intersection(ctrl.path, gamePath) + : ctrl.path + : gamePath || ctrl.path; } function renderPlayer( diff --git a/ui/analyse/src/study/studyCtrl.ts b/ui/analyse/src/study/studyCtrl.ts index dd815038cd49d..05074fd6cfcac 100644 --- a/ui/analyse/src/study/studyCtrl.ts +++ b/ui/analyse/src/study/studyCtrl.ts @@ -44,7 +44,7 @@ import { MultiBoardCtrl } from './multiBoard'; import type { StudySocketSendParams } from '../socket'; import { storedMap } from 'common/storage'; import { opposite } from 'chessops/util'; -import StudyChaptersCtrl from './studyChapters'; +import StudyChaptersCtrl, { isFinished } from './studyChapters'; import { SearchCtrl } from './studySearch'; import type { GamebookOverride } from './gamebook/interfaces'; import type { EvalHitMulti, EvalHitMultiArray } from '../interfaces'; @@ -526,6 +526,9 @@ export default class StudyCtrl { }; onFlip = () => this.chapterFlipMapProp(this.data.chapter.id, this.ctrl.flipped); + isClockTicking = (path: Tree.Path) => + path !== '' && this.data.chapter.relayPath === path && !isFinished(this.data.chapter); + setPath = (path: Tree.Path, node: Tree.Node) => { this.onSetPath(path); this.commentForm.onSetPath(this.vm.chapterId, path, node); diff --git a/ui/analyse/src/view/clocks.ts b/ui/analyse/src/view/clocks.ts index 69e291361c239..8bee45fb5dada 100644 --- a/ui/analyse/src/view/clocks.ts +++ b/ui/analyse/src/view/clocks.ts @@ -1,11 +1,11 @@ import { h, type VNode } from 'snabbdom'; import type AnalyseCtrl from '../ctrl'; -import { isFinished } from '../study/studyChapters'; import { notNull } from 'common'; -export default function renderClocks(ctrl: AnalyseCtrl, node: Tree.Node): [VNode, VNode] | undefined { - const whitePov = ctrl.bottomIsWhite(), - parentClock = ctrl.tree.getParentClock(node, ctrl.path), +export default function renderClocks(ctrl: AnalyseCtrl, path: Tree.Path): [VNode, VNode] | undefined { + const node = ctrl.tree.nodeAtPath(path), + whitePov = ctrl.bottomIsWhite(), + parentClock = ctrl.tree.getParentClock(node, path), isWhiteTurn = node.ply % 2 === 0, centis: Array = isWhiteTurn ? [parentClock, node.clock] : [node.clock, parentClock]; @@ -14,9 +14,9 @@ export default function renderClocks(ctrl: AnalyseCtrl, node: Tree.Node): [VNode const study = ctrl.study; const lastMoveAt = study - ? study.data.chapter.relayPath !== ctrl.path || ctrl.path === '' || isFinished(study.data.chapter) - ? undefined - : study.relay?.lastMoveAt(study.vm.chapterId) + ? study.isClockTicking(path) + ? study.relay?.lastMoveAt(study.vm.chapterId) + : undefined : ctrl.autoplay.lastMoveAt; if (lastMoveAt) { diff --git a/ui/analyse/src/view/components.ts b/ui/analyse/src/view/components.ts index 6e26643e94063..36fccec9e24ca 100644 --- a/ui/analyse/src/view/components.ts +++ b/ui/analyse/src/view/components.ts @@ -440,7 +440,7 @@ function renderPlayerStrips(ctrl: AnalyseCtrl): [VNode, VNode] | undefined { const renderPlayerStrip = (cls: string, materialDiff: VNode, clock?: VNode): VNode => h('div.analyse__player_strip.' + cls, [materialDiff, clock]); - const clocks = renderClocks(ctrl, ctrl.node), + const clocks = renderClocks(ctrl, ctrl.path), whitePov = ctrl.bottomIsWhite(), materialDiffs = renderMaterialDiffs(ctrl); diff --git a/ui/tree/src/path.ts b/ui/tree/src/path.ts index 8ada21b70119e..5d44486cbdfd3 100644 --- a/ui/tree/src/path.ts +++ b/ui/tree/src/path.ts @@ -20,3 +20,9 @@ export function fromNodeList(nodes: Tree.Node[]): Tree.Path { export const isChildOf = (child: Tree.Path, parent: Tree.Path): boolean => !!child && child.slice(0, -2) === parent; + +export const intersection = (p1: Tree.Path, p2: Tree.Path): Tree.Path => { + const head1 = head(p1), + head2 = head(p2); + return head1 === head2 ? head1 + intersection(tail(p1), tail(p2)) : ''; +}; diff --git a/ui/tree/src/tree.ts b/ui/tree/src/tree.ts index e29acbbc3b7c7..7ae0023307012 100644 --- a/ui/tree/src/tree.ts +++ b/ui/tree/src/tree.ts @@ -74,8 +74,6 @@ export function build(root: Tree.Node): TreeWrapper { const pathIsMainline = (path: Tree.Path): boolean => pathIsMainlineFrom(root, path); - const pathExists = (path: Tree.Path): boolean => !!nodeAtPathOrNull(path); - function pathIsMainlineFrom(node: Tree.Node, path: Tree.Path): boolean { if (path === '') return true; const pathId = treePath.head(path), @@ -84,6 +82,8 @@ export function build(root: Tree.Node): TreeWrapper { return pathIsMainlineFrom(child, treePath.tail(path)); } + const pathExists = (path: Tree.Path): boolean => !!nodeAtPathOrNull(path); + const pathIsForcedVariation = (path: Tree.Path): boolean => !!getNodeList(path).find(n => n.forceVariation); function lastMainlineNodeFrom(node: Tree.Node, path: Tree.Path): Tree.Node { @@ -233,9 +233,7 @@ export function build(root: Tree.Node): TreeWrapper { }, pathIsMainline, pathIsForcedVariation, - lastMainlineNode(path: Tree.Path): Tree.Node { - return lastMainlineNodeFrom(root, path); - }, + lastMainlineNode: (path: Tree.Path): Tree.Node => lastMainlineNodeFrom(root, path), pathExists, deleteNodeAt, promoteAt, From b3930a1df7875464458cef65d64eea82397028b7 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 13:05:33 +0100 Subject: [PATCH 14/27] desaturate board when navigating away from a broadcast live position --- ui/analyse/css/study/relay/_layout.scss | 5 +++++ ui/analyse/src/view/components.ts | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/analyse/css/study/relay/_layout.scss b/ui/analyse/css/study/relay/_layout.scss index f6c9fdc79f63b..f5266613a702b 100644 --- a/ui/analyse/css/study/relay/_layout.scss +++ b/ui/analyse/css/study/relay/_layout.scss @@ -129,4 +129,9 @@ main.analyse.is-relay:not(.has-relay-tour) { flex: 1 0 200px; } } + &.relay-live-away { + .main-board { + filter: saturate(0.5) brightness(0.9); + } + } } diff --git a/ui/analyse/src/view/components.ts b/ui/analyse/src/view/components.ts index 36fccec9e24ca..c50c8ad6a727a 100644 --- a/ui/analyse/src/view/components.ts +++ b/ui/analyse/src/view/components.ts @@ -42,6 +42,7 @@ import type * as studyDeps from '../study/studyDeps'; import { renderPgnError } from '../pgnImport'; import { storage } from 'common/storage'; import { makeChat } from 'chat'; +import { isFinished } from '../study/studyChapters'; export interface ViewContext { ctrl: AnalyseCtrl; @@ -91,6 +92,9 @@ export function renderMain( { ctrl, playerBars, gaugeOn, gamebookPlayView, needsInnerCoords, hasRelayTour }: ViewContext, kids: VNodeKids, ): VNode { + const isRelay = defined(ctrl.study?.relay); + const relayPath = + isRelay && ctrl.study && !isFinished(ctrl.study.data.chapter) && ctrl.study?.data.chapter.relayPath; return h( 'main.analyse.variant-' + ctrl.data.game.variant.key, { @@ -118,9 +122,10 @@ export function renderMain( 'has-players': !!playerBars, 'gamebook-play': !!gamebookPlayView, 'has-relay-tour': hasRelayTour, - 'is-relay': ctrl.study?.relay !== undefined, + 'is-relay': isRelay, 'analyse-hunter': ctrl.opts.hunter, 'analyse--wiki': !!ctrl.wiki && !ctrl.study, + 'relay-live-away': !!relayPath && relayPath !== ctrl.path, }, }, kids, From e3d758680db5c564fbb75ed821ff1cae7f7f159a Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 13:30:22 +0100 Subject: [PATCH 15/27] prevent selection of analysis moves UI they're not suitable for copy-pasting, they're not valid PGN --- ui/analyse/css/_tools.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/analyse/css/_tools.scss b/ui/analyse/css/_tools.scss index 93ff2ee2ee296..add14c15923c1 100644 --- a/ui/analyse/css/_tools.scss +++ b/ui/analyse/css/_tools.scss @@ -50,6 +50,8 @@ flex-direction: column; justify-content: space-between; + @include prevent-select; + // 0 size forces vertical scrollbar overflow-y: auto; overflow-x: hidden; From b909dc5019fdd40a0b5648b8e9b82ff3517fa343 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 13:37:29 +0100 Subject: [PATCH 16/27] add broadcast button to return to live position --- ui/analyse/css/study/relay/_back-to-live.scss | 14 +++++++++++++ ui/analyse/css/study/relay/_show.scss | 1 + ui/analyse/src/study/relay/relayView.ts | 21 ++++++++++++++++++- ui/analyse/src/study/studyCtrl.ts | 6 ++++++ ui/analyse/src/view/components.ts | 7 +++---- 5 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 ui/analyse/css/study/relay/_back-to-live.scss diff --git a/ui/analyse/css/study/relay/_back-to-live.scss b/ui/analyse/css/study/relay/_back-to-live.scss new file mode 100644 index 0000000000000..7e3ff9c9b09a7 --- /dev/null +++ b/ui/analyse/css/study/relay/_back-to-live.scss @@ -0,0 +1,14 @@ +.relay-back-to-live { + @extend %flex-center; + justify-content: center; + padding: 0.5em 0; + background: $c-accent; + color: $c-over; + cursor: pointer; + + flex: 0 0 auto; + + @include mq-is-col1 { + display: none; + } +} diff --git a/ui/analyse/css/study/relay/_show.scss b/ui/analyse/css/study/relay/_show.scss index 340a8892c4aa0..f47865813f126 100644 --- a/ui/analyse/css/study/relay/_show.scss +++ b/ui/analyse/css/study/relay/_show.scss @@ -3,3 +3,4 @@ @import 'rounds'; @import 'teams'; @import 'video-player'; +@import 'back-to-live'; diff --git a/ui/analyse/src/study/relay/relayView.ts b/ui/analyse/src/study/relay/relayView.ts index 546968987e505..716d620fe34c6 100644 --- a/ui/analyse/src/study/relay/relayView.ts +++ b/ui/analyse/src/study/relay/relayView.ts @@ -1,6 +1,6 @@ import { view as cevalView } from 'ceval'; import { onClickAway } from 'common'; -import { looseH as h, onInsert, type VNode } from 'common/snabbdom'; +import { bind, dataIcon, looseH as h, onInsert, type VNode } from 'common/snabbdom'; import * as licon from 'common/licon'; import type AnalyseCtrl from '../../ctrl'; import { view as keyboardView } from '../../keyboard'; @@ -34,6 +34,25 @@ export function relayView( ]); } +export const backToLiveView = (ctrl: AnalyseCtrl) => + ctrl.study?.isRelayAwayFromLive() + ? h( + 'button.fbt.relay-back-to-live.text', + { + attrs: dataIcon(licon.RadioTower), + hook: bind( + 'click', + () => { + const p = ctrl.study?.data.chapter.relayPath; + if (p) ctrl.userJump(p); + }, + ctrl.redraw, + ), + }, + 'Back to live move', + ) + : undefined; + export function renderStreamerMenu(relay: RelayCtrl): VNode { const makeUrl = (id: string) => { const url = new URL(location.href); diff --git a/ui/analyse/src/study/studyCtrl.ts b/ui/analyse/src/study/studyCtrl.ts index 05074fd6cfcac..6a015de8061d7 100644 --- a/ui/analyse/src/study/studyCtrl.ts +++ b/ui/analyse/src/study/studyCtrl.ts @@ -529,6 +529,12 @@ export default class StudyCtrl { isClockTicking = (path: Tree.Path) => path !== '' && this.data.chapter.relayPath === path && !isFinished(this.data.chapter); + isRelayAwayFromLive = (): boolean => + !!this.relay && + !isFinished(this.data.chapter) && + defined(this.data.chapter.relayPath) && + this.ctrl.path !== this.data.chapter.relayPath; + setPath = (path: Tree.Path, node: Tree.Node) => { this.onSetPath(path); this.commentForm.onSetPath(this.vm.chapterId, path, node); diff --git a/ui/analyse/src/view/components.ts b/ui/analyse/src/view/components.ts index c50c8ad6a727a..8ffb899fdd436 100644 --- a/ui/analyse/src/view/components.ts +++ b/ui/analyse/src/view/components.ts @@ -42,7 +42,7 @@ import type * as studyDeps from '../study/studyDeps'; import { renderPgnError } from '../pgnImport'; import { storage } from 'common/storage'; import { makeChat } from 'chat'; -import { isFinished } from '../study/studyChapters'; +import { backToLiveView } from '../study/relay/relayView'; export interface ViewContext { ctrl: AnalyseCtrl; @@ -93,8 +93,6 @@ export function renderMain( kids: VNodeKids, ): VNode { const isRelay = defined(ctrl.study?.relay); - const relayPath = - isRelay && ctrl.study && !isFinished(ctrl.study.data.chapter) && ctrl.study?.data.chapter.relayPath; return h( 'main.analyse.variant-' + ctrl.data.game.variant.key, { @@ -125,7 +123,7 @@ export function renderMain( 'is-relay': isRelay, 'analyse-hunter': ctrl.opts.hunter, 'analyse--wiki': !!ctrl.wiki && !ctrl.study, - 'relay-live-away': !!relayPath && relayPath !== ctrl.path, + 'relay-live-away': !!ctrl.study?.isRelayAwayFromLive(), }, }, kids, @@ -142,6 +140,7 @@ export function renderTools({ ctrl, deps, concealOf, allowVideo }: ViewContext, !ctrl.retro?.isSolving() && !ctrl.practice && cevalView.renderPvs(ctrl), renderMoveList(ctrl, deps, concealOf), deps?.gbEdit.running(ctrl) ? deps?.gbEdit.render(ctrl) : undefined, + backToLiveView(ctrl), forkView(ctrl, concealOf), retroView(ctrl) || practiceView(ctrl) || explorerView(ctrl), ]), From b08d347e8b906fc99ad2707badbeaa0cd2cbc257 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 14:52:24 +0100 Subject: [PATCH 17/27] wait longer on mailcheck ratelimit --- modules/security/src/main/VerifyMail.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/security/src/main/VerifyMail.scala b/modules/security/src/main/VerifyMail.scala index 23edd96d7ab40..9dc04fc0954e4 100644 --- a/modules/security/src/main/VerifyMail.scala +++ b/modules/security/src/main/VerifyMail.scala @@ -71,7 +71,7 @@ final private class VerifyMail( if res.status == 429 then logger.info(s"Mailcheck rate limited $url") - rateLimitedUntil = nowInstant.plusMinutes(2) + rateLimitedUntil = nowInstant.plusMinutes(5) true else (for From 2f9701572ff0042661859ac95937339cac3d056e Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 15:09:08 +0100 Subject: [PATCH 18/27] block infinite email aliases they're being used for mass multiaccounting --- .../src/main/EmailAddressValidator.scala | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/modules/security/src/main/EmailAddressValidator.scala b/modules/security/src/main/EmailAddressValidator.scala index e1ceb1efc5504..b772adf791978 100644 --- a/modules/security/src/main/EmailAddressValidator.scala +++ b/modules/security/src/main/EmailAddressValidator.scala @@ -43,7 +43,8 @@ final class EmailAddressValidator( // only compute valid and non-whitelisted email domains private[security] def apply(e: EmailAddress): Fu[Result] = - e.domain.map(_.lower).fold(fuccess(Result.DomainMissing))(validateDomain) + if isInfiniteAlias(e) then fuccess(Result.Alias) + else e.domain.map(_.lower).fold(fuccess(Result.DomainMissing))(validateDomain) private[security] def validateDomain(domain: Domain.Lower): Fu[Result] = if DisposableEmailDomain.whitelisted(domain.into(Domain)) then fuccess(Result.Passlist) @@ -95,6 +96,14 @@ final class EmailAddressValidator( case (acc, _) => acc if variations.isEmpty then List(email) else variations + private def isInfiniteAlias(e: EmailAddress) = + duckAliases.is(e) + + private object duckAliases: + private val domain = Domain.Lower.from("duck.com") + private val regex = """^\w{3,}-\w{3,}-\w{3,}$""".r + def is(e: EmailAddress) = e.nameAndDomain.exists((n, d) => d.lower == domain && regex.matches(n)) + private def wasUsedTwiceRecently(email: EmailAddress): Fu[Boolean] = userRepo.countRecentByPrevEmail(email.normalize, nowInstant.minusWeeks(1)).dmap(_ >= 2) >>| userRepo.countRecentByPrevEmail(email.normalize, nowInstant.minusMonths(1)).dmap(_ >= 4) @@ -106,10 +115,8 @@ object EmailAddressValidator: case Alright extends Result(none) case DomainMissing extends Result("The email address domain is missing.".some) // no translation needed case Blocklist extends Result("Cannot use disposable email addresses (Blocklist).".some) + case Alias extends Result("Cannot use email address aliases.".some) case DnsMissing extends Result("This email domain doesn't seem to work (missing MX DNS)".some) case DnsTimeout extends Result("This email domain doesn't seem to work (timeout MX DNS)".some) - case DnsBlocklist - extends Result( - "Cannot use disposable email addresses (DNS blocklist).".some - ) - case Reputation extends Result("This email domain has a poor reputation and cannot be used.".some) + case DnsBlocklist extends Result("Cannot use disposable email addresses (DNS blocklist).".some) + case Reputation extends Result("This email domain has a poor reputation and cannot be used.".some) From 016c77fc99e5196f607f65e443ffd6fd8fe5e78d Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 15:19:34 +0100 Subject: [PATCH 19/27] fix possible infinite recursion --- ui/tree/src/path.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tree/src/path.ts b/ui/tree/src/path.ts index 5d44486cbdfd3..2b26407c9a945 100644 --- a/ui/tree/src/path.ts +++ b/ui/tree/src/path.ts @@ -24,5 +24,5 @@ export const isChildOf = (child: Tree.Path, parent: Tree.Path): boolean => export const intersection = (p1: Tree.Path, p2: Tree.Path): Tree.Path => { const head1 = head(p1), head2 = head(p2); - return head1 === head2 ? head1 + intersection(tail(p1), tail(p2)) : ''; + return head1 !== '' && head1 === head2 ? head1 + intersection(tail(p1), tail(p2)) : ''; }; From 1c88f5b2759404bfe7383cf0139478b0e6f2628c Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 15:27:40 +0100 Subject: [PATCH 20/27] scala refactor --- modules/relay/src/main/RelayFetch.scala | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/modules/relay/src/main/RelayFetch.scala b/modules/relay/src/main/RelayFetch.scala index 208c381c72ea8..43fa4b3454e5c 100644 --- a/modules/relay/src/main/RelayFetch.scala +++ b/modules/relay/src/main/RelayFetch.scala @@ -147,16 +147,9 @@ final private class RelayFetch( private def continueRelay(tour: RelayTour, updating: Updating[RelayRound]): Updating[RelayRound] = val round = updating.current round.sync.upstream.fold(updating): upstream => + reportBroadcastFailure(round.withTour(tour)) val seconds: Seconds = if round.sync.log.alwaysFails then - round.sync.log.events.lastOption - .filterNot(_.isTimeout) - .flatMap(_.error) - .ifTrue(tour.official && round.shouldHaveStarted) - .filterNot(_.contains("Cannot parse move")) - .filterNot(_.contains("Cannot parse pgn")) - .filterNot(_.contains("Found an empty PGN")) - .foreach { irc.broadcastError(round.id, round.withTour(tour).fullName, _) } Seconds(tour.tier.fold(60): case RelayTour.Tier.best => 10 case RelayTour.Tier.high => 20 @@ -173,6 +166,17 @@ final private class RelayFetch( }.some ) + private def reportBroadcastFailure(r: RelayRound.WithTour): Unit = + if r.round.sync.log.alwaysFails then + r.round.sync.log.events.lastOption + .filterNot(_.isTimeout) + .flatMap(_.error) + .ifTrue(r.tour.official && r.round.shouldHaveStarted) + .filterNot(_.contains("Cannot parse move")) + .filterNot(_.contains("Cannot parse pgn")) + .filterNot(_.contains("Found an empty PGN")) + .foreach { irc.broadcastError(r.round.id, r.fullName, _) } + private def dynamicPeriod(tour: RelayTour, round: RelayRound, upstream: Sync.Upstream) = Seconds: val base = if upstream.hasLcc then 5 From 3da70d7a56feb959dd7d0595ef87f05ea8bf457e Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 17:10:19 +0100 Subject: [PATCH 21/27] study refactors --- app/controllers/Study.scala | 2 +- modules/practice/src/main/Env.scala | 2 +- modules/relay/src/main/Env.scala | 8 ++-- .../relay/src/main/RelayPlayerEnrich.scala | 2 +- modules/relay/src/main/RelaySync.scala | 37 ++++++++++--------- modules/study/src/main/Env.scala | 2 +- modules/study/src/main/ExplorerGame.scala | 2 +- modules/study/src/main/JsonView.scala | 2 +- modules/study/src/main/StudyApi.scala | 36 +++++++----------- modules/study/src/main/StudySocket.scala | 26 ++++++------- .../src/main/{actorApi.scala => model.scala} | 10 ++++- 11 files changed, 64 insertions(+), 65 deletions(-) rename modules/study/src/main/{actorApi.scala => model.scala} (78%) diff --git a/app/controllers/Study.scala b/app/controllers/Study.scala index 6a9e37603f762..e3d7e00c1aacd 100644 --- a/app/controllers/Study.scala +++ b/app/controllers/Study.scala @@ -16,7 +16,7 @@ import lila.core.study.Order import lila.study.JsonView.JsData import lila.study.PgnDump.WithFlags import lila.study.Study.WithChapter -import lila.study.actorApi.{ BecomeStudyAdmin, Who } +import lila.study.{ BecomeStudyAdmin, Who } import lila.study.{ Chapter, Orders, Settings, Study as StudyModel, StudyForm } import lila.tree.Node.partitionTreeJsonWriter import com.fasterxml.jackson.core.JsonParseException diff --git a/modules/practice/src/main/Env.scala b/modules/practice/src/main/Env.scala index 894ac94cd8dfa..7feda25a58c4e 100644 --- a/modules/practice/src/main/Env.scala +++ b/modules/practice/src/main/Env.scala @@ -21,6 +21,6 @@ final class Env( def getStudies: lila.core.practice.GetStudies = api.structure.getStudies - lila.common.Bus.subscribeFun("study") { case lila.study.actorApi.SaveStudy(study) => + lila.common.Bus.subscribeFun("study") { case lila.study.SaveStudy(study) => api.structure.onSave(study) } diff --git a/modules/relay/src/main/Env.scala b/modules/relay/src/main/Env.scala index 84efd8e52a0f0..4cc805748fd10 100644 --- a/modules/relay/src/main/Env.scala +++ b/modules/relay/src/main/Env.scala @@ -144,19 +144,19 @@ final class Env( "study" -> { case lila.core.study.RemoveStudy(studyId) => api.onStudyRemove(studyId) }, - "relayToggle" -> { case lila.study.actorApi.RelayToggle(id, v, who) => + "relayToggle" -> { case lila.study.RelayToggle(id, v, who) => studyApi .isContributor(id, who.u) .foreach: _.so(api.requestPlay(id.into(RelayRoundId), v, "manual toggle")) }, - "kickStudy" -> { case lila.study.actorApi.Kick(studyId, userId, who) => + "kickStudy" -> { case lila.study.Kick(studyId, userId, who) => roundRepo.tourIdByStudyId(studyId).flatMapz(api.kickBroadcast(userId, _, who)) }, - "adminStudy" -> { case lila.study.actorApi.BecomeStudyAdmin(studyId, me) => + "adminStudy" -> { case lila.study.BecomeStudyAdmin(studyId, me) => api.becomeStudyAdmin(studyId, me) }, - "isOfficialRelay" -> { case lila.study.actorApi.IsOfficialRelay(studyId, promise) => + "isOfficialRelay" -> { case lila.study.IsOfficialRelay(studyId, promise) => promise.completeWith(api.isOfficial(studyId.into(RelayRoundId))) } ) diff --git a/modules/relay/src/main/RelayPlayerEnrich.scala b/modules/relay/src/main/RelayPlayerEnrich.scala index 9baefdfa247c4..f79fc3d72ae10 100644 --- a/modules/relay/src/main/RelayPlayerEnrich.scala +++ b/modules/relay/src/main/RelayPlayerEnrich.scala @@ -204,6 +204,6 @@ private final class RelayPlayerEnrich( chapterId = chapter.id, tags = enriched, newName = newName.filter(_ != chapter.name) - )(lila.study.actorApi.Who(chapter.ownerId, Sri(""))) + )(lila.study.Who(chapter.ownerId, Sri(""))) .runWith(Sink.ignore) yield () diff --git a/modules/relay/src/main/RelaySync.scala b/modules/relay/src/main/RelaySync.scala index ad8f0a5643824..8b56af0aa98c8 100644 --- a/modules/relay/src/main/RelaySync.scala +++ b/modules/relay/src/main/RelaySync.scala @@ -6,6 +6,7 @@ import chess.format.pgn.{ Tag, Tags } import lila.core.socket.Sri import lila.study.* import lila.tree.Branch +import lila.study.AddNode final private class RelaySync( studyApi: StudyApi, @@ -91,20 +92,19 @@ final private class RelaySync( studyId = study.id, position = Position(chapter, path).ref, toMainline = true - )(by) >> chapterRepo.setRelayPath(chapter.id, path) + )(using by) >> chapterRepo.setRelayPath(chapter.id, path) _ <- newNode match case Some(newNode) => newNode.mainline .foldM(Position(chapter, path).ref): (position, n) => - studyApi - .addNode( - studyId = study.id, - position = position, - node = n, - opts = moveOpts, - relay = makeRelayFor(game, position.path + n.id).some - )(by) - .inject(position + n) + val node = AddNode( + studyId = study.id, + positionRef = position, + node = n, + opts = moveOpts, + relay = makeRelayFor(game, position.path + n.id).some + )(using by) + studyApi.addNode(node).inject(position + n) case None => // the chapter already has all the game moves, // but its relayPath might be out of sync. This can happen if the broadcast @@ -121,13 +121,14 @@ final private class RelaySync( game.root.children .nodeAt(gameMainlinePath) .so: lastMainlineNode => - studyApi.addNode( - studyId = study.id, - position = Position(chapter, gameMainlinePath.parent).ref, - node = lastMainlineNode, - opts = moveOpts, - relay = makeRelayFor(game, gameMainlinePath).some - )(by) + studyApi.addNode: + AddNode( + studyId = study.id, + positionRef = Position(chapter, gameMainlinePath.parent).ref, + node = lastMainlineNode, + opts = moveOpts, + relay = makeRelayFor(game, gameMainlinePath).some + )(using by) yield newNode.so(_.mainline.size) private def updateChapterTags( @@ -212,7 +213,7 @@ final private class RelaySync( ) private val sri = Sri("") - private def who(userId: UserId) = actorApi.Who(userId, sri) + private def who(userId: UserId) = Who(userId, sri) private def vs(tags: Tags) = s"${tags(_.White) | "?"} - ${tags(_.Black) | "?"}" diff --git a/modules/study/src/main/Env.scala b/modules/study/src/main/Env.scala index c12c7b968433f..b0a9be849aac4 100644 --- a/modules/study/src/main/Env.scala +++ b/modules/study/src/main/Env.scala @@ -65,7 +65,7 @@ final class Env( private lazy val chapterMaker = wire[ChapterMaker] - private lazy val explorerGame = wire[ExplorerGame] + private lazy val explorerGame = wire[ExplorerGameApi] private lazy val studyMaker = wire[StudyMaker] diff --git a/modules/study/src/main/ExplorerGame.scala b/modules/study/src/main/ExplorerGame.scala index 8666062d10c6d..f4c069093deab 100644 --- a/modules/study/src/main/ExplorerGame.scala +++ b/modules/study/src/main/ExplorerGame.scala @@ -6,7 +6,7 @@ import chess.format.{ Fen, UciPath } import lila.tree.Node.Comment import lila.tree.{ Branch, Node, Root } -final private class ExplorerGame( +final private class ExplorerGameApi( explorer: lila.core.game.Explorer, namer: lila.core.game.Namer, lightUserApi: lila.core.user.LightUserApi, diff --git a/modules/study/src/main/JsonView.scala b/modules/study/src/main/JsonView.scala index 2faec52644254..73e31f3b88a97 100644 --- a/modules/study/src/main/JsonView.scala +++ b/modules/study/src/main/JsonView.scala @@ -218,5 +218,5 @@ object JsonView: private[study] given Writes[Chapter.ServerEval] = Json.writes - private[study] given OWrites[actorApi.Who] = OWrites: w => + private[study] given OWrites[Who] = OWrites: w => Json.obj("u" -> w.u, "s" -> w.sri) diff --git a/modules/study/src/main/StudyApi.scala b/modules/study/src/main/StudyApi.scala index 21197f0e144f5..b37182b242d59 100644 --- a/modules/study/src/main/StudyApi.scala +++ b/modules/study/src/main/StudyApi.scala @@ -13,8 +13,6 @@ import lila.core.timeline.{ Propagate, StudyLike } import lila.tree.Branch import lila.tree.Node.{ Comment, Gamebook, Shapes } -import actorApi.Who - final class StudyApi( studyRepo: StudyRepo, chapterRepo: ChapterRepo, @@ -22,7 +20,7 @@ final class StudyApi( studyMaker: StudyMaker, chapterMaker: ChapterMaker, inviter: StudyInvite, - explorerGameHandler: ExplorerGame, + explorerGameHandler: ExplorerGameApi, topicApi: StudyTopicApi, lightUserApi: lila.core.user.LightUserApi, chatApi: lila.core.chat.ChatApi, @@ -225,27 +223,21 @@ final class StudyApi( yield sendTo(study.id)(_.setPath(position, who)) case _ => funit - def addNode( - studyId: StudyId, - position: Position.Ref, - node: Branch, - opts: MoveOpts, - relay: Option[Chapter.Relay] = None - )(who: Who): Funit = - sequenceStudyWithChapter(studyId, position.chapterId): + def addNode(args: AddNode): Funit = + import args.{ *, given } + sequenceStudyWithChapter(studyId, positionRef.chapterId): case Study.WithChapter(study, chapter) => Contribute(who.u, study): - doAddNode(study, Position(chapter, position.path), node, opts, relay)(who) + doAddNode(args, study, Position(chapter, positionRef.path)) .flatMapz { _() } private def doAddNode( + args: AddNode, study: Study, - position: Position, - rawNode: Branch, - opts: MoveOpts, - relay: Option[Chapter.Relay] - )(who: Who): Fu[Option[() => Funit]] = - val singleNode = rawNode.withoutChildren + position: Position + ): Fu[Option[() => Funit]] = + import args.{ *, given } + val singleNode = args.node.withoutChildren def failReload() = reloadSriBecauseOf(study, who.sri, position.chapter.id) if position.chapter.isOverweight then logger.info(s"Overweight chapter ${study.id}/${position.chapter.id}") @@ -272,7 +264,7 @@ final class StudyApi( isMainline = newPosition.path.isMainline(chapter.root) promoteToMainline = opts.promoteToMainline && !isMainline yield promoteToMainline.option: () => - promote(study.id, position.ref + node, toMainline = true)(who) + promote(study.id, position.ref + node, toMainline = true) } } @@ -326,7 +318,7 @@ final class StudyApi( yield onChapterChange(study.id, chapter.id, who) // rewrites the whole chapter because of `forceVariation`. Very inefficient. - def promote(studyId: StudyId, position: Position.Ref, toMainline: Boolean)(who: Who): Funit = + def promote(studyId: StudyId, position: Position.Ref, toMainline: Boolean)(using who: Who): Funit = sequenceStudyWithChapter(studyId, position.chapterId): case Study.WithChapter(study, chapter) => Contribute(who.u, study): @@ -441,7 +433,7 @@ final class StudyApi( reloadSriBecauseOf(sc.study, who.sri, position.chapterId) fufail(s"Invalid setClock $position $clock") - def setTag(studyId: StudyId, setTag: actorApi.SetTag)(who: Who) = + def setTag(studyId: StudyId, setTag: SetTag)(who: Who) = sequenceStudyWithChapter(studyId, setTag.chapterId): case Study.WithChapter(study, chapter) => Contribute(who.u, study): @@ -545,7 +537,7 @@ final class StudyApi( reloadSriBecauseOf(study, who.sri, chapter.id) fufail(s"Invalid setGamebook $studyId $position") - def explorerGame(studyId: StudyId, data: actorApi.ExplorerGame)(who: Who) = + def explorerGame(studyId: StudyId, data: ExplorerGame)(who: Who) = sequenceStudyWithChapter(studyId, data.position.chapterId): case Study.WithChapter(study, chapter) => Contribute(who.u, study): diff --git a/modules/study/src/main/StudySocket.scala b/modules/study/src/main/StudySocket.scala index a53099cee2154..34c18c50cc60c 100644 --- a/modules/study/src/main/StudySocket.scala +++ b/modules/study/src/main/StudySocket.scala @@ -13,8 +13,6 @@ import lila.tree.Branch import lila.tree.Node.{ Comment, Gamebook, Shape, Shapes } import lila.tree.Node.minimalNodeJsonWriter -import actorApi.Who - final private class StudySocket( api: StudyApi, jsonView: JsonView, @@ -75,13 +73,13 @@ final private class StudySocket( AnaMove .parse(o) .foreach: move => - applyWho(moveOrDrop(studyId, move, MoveOpts.parse(o))) + applyWho(moveOrDrop(studyId, move, MoveOpts.parse(o))(using _)) case "anaDrop" => AnaDrop .parse(o) .foreach: drop => - applyWho(moveOrDrop(studyId, drop, MoveOpts.parse(o))) + applyWho(moveOrDrop(studyId, drop, MoveOpts.parse(o))(using _)) case "deleteNode" => reading[AtPosition](o): position => @@ -96,7 +94,7 @@ final private class StudySocket( (o \ "d" \ "toMainline") .asOpt[Boolean] .foreach: toMainline => - applyWho(api.promote(studyId, position.ref, toMainline)) + applyWho(api.promote(studyId, position.ref, toMainline)(using _)) case "forceVariation" => reading[AtPosition](o): position => @@ -114,7 +112,7 @@ final private class StudySocket( .foreach: username => applyWho: w => api.kick(studyId, username.id, w.myId) - Bus.publish(actorApi.Kick(studyId, username.id, w.myId), "kickStudy") + Bus.publish(Kick(studyId, username.id, w.myId), "kickStudy") case "leave" => who.foreach: w => @@ -177,7 +175,7 @@ final private class StudySocket( applyWho(api.editStudy(studyId, data)) case "setTag" => - reading[actorApi.SetTag](o): setTag => + reading[SetTag](o): setTag => applyWho(api.setTag(studyId, setTag)) case "setComment" => @@ -216,7 +214,7 @@ final private class StudySocket( applyWho(api.setTopics(studyId, topics)) case "explorerGame" => - reading[actorApi.ExplorerGame](o): data => + reading[ExplorerGame](o): data => applyWho(api.explorerGame(studyId, data)) case "requestAnalysis" => @@ -235,7 +233,7 @@ final private class StudySocket( case "relaySync" => applyWho: w => - Bus.publish(actorApi.RelayToggle(studyId, ~(o \ "d").asOpt[Boolean], w), "relayToggle") + Bus.publish(RelayToggle(studyId, ~(o \ "d").asOpt[Boolean], w), "relayToggle") case t => logger.warn(s"Unhandled study socket message: $t") @@ -246,18 +244,18 @@ final private class StudySocket( _ => _ => none, // the "talk" event is handled by the study API localTimeout = Some { (roomId, modId, suspectId) => api.isContributor(roomId, modId) >>& api.isMember(roomId, suspectId).not >>& - Bus.ask("isOfficialRelay") { actorApi.IsOfficialRelay(roomId, _) }.not + Bus.ask("isOfficialRelay") { IsOfficialRelay(roomId, _) }.not }, chatBusChan = _.study ) - private def moveOrDrop(studyId: StudyId, m: AnaAny, opts: MoveOpts)(who: Who) = + private def moveOrDrop(studyId: StudyId, m: AnaAny, opts: MoveOpts)(using Who) = m.branch.foreach: branch => if branch.ply < Node.MAX_PLIES then m.chapterId .ifTrue(opts.write) .foreach: chapterId => - api.addNode(studyId, Position.Ref(chapterId, m.path), branch, opts)(who) + api.addNode(AddNode(studyId, Position.Ref(chapterId, m.path), branch, opts)) private lazy val send = socketKit.send("study-out") @@ -458,9 +456,9 @@ object StudySocket: given Reads[ChapterMaker.EditData] = Json.reads given Reads[ChapterMaker.DescData] = Json.reads given studyDataReads: Reads[Study.Data] = Json.reads - given Reads[actorApi.SetTag] = Json.reads + given Reads[SetTag] = Json.reads given Reads[Gamebook] = Json.reads - given Reads[actorApi.ExplorerGame] = Json.reads + given Reads[ExplorerGame] = Json.reads object Out: def getIsPresent(reqId: Int, studyId: StudyId, userId: UserId) = diff --git a/modules/study/src/main/actorApi.scala b/modules/study/src/main/model.scala similarity index 78% rename from modules/study/src/main/actorApi.scala rename to modules/study/src/main/model.scala index 1378b69ac1131..abba07c496f34 100644 --- a/modules/study/src/main/actorApi.scala +++ b/modules/study/src/main/model.scala @@ -1,7 +1,7 @@ package lila.study -package actorApi import chess.format.UciPath +import lila.tree.Branch case class SaveStudy(study: Study) case class SetTag(chapterId: StudyChapterId, name: String, value: String): @@ -16,3 +16,11 @@ case class RelayToggle(studyId: StudyId, v: Boolean, who: Who) case class Kick(studyId: StudyId, userId: UserId, who: MyId) case class BecomeStudyAdmin(studyId: StudyId, me: Me) case class IsOfficialRelay(studyId: StudyId, promise: Promise[Boolean]) + +case class AddNode( + studyId: StudyId, + positionRef: Position.Ref, + node: Branch, + opts: MoveOpts, + relay: Option[Chapter.Relay] = None +)(using val who: Who) From 4cb8fff00ce75594ffbf2b951ccebece27d7a91a Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 17:49:21 +0100 Subject: [PATCH 22/27] remove superfluous dests field in websocket message --- modules/study/src/main/StudySocket.scala | 5 ++--- ui/analyse/src/study/interfaces.ts | 1 - ui/analyse/src/study/studyCtrl.ts | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/modules/study/src/main/StudySocket.scala b/modules/study/src/main/StudySocket.scala index 34c18c50cc60c..00aeae7fb29e6 100644 --- a/modules/study/src/main/StudySocket.scala +++ b/modules/study/src/main/StudySocket.scala @@ -295,7 +295,6 @@ final private class StudySocket( .obj( "n" -> minimalNodeJsonWriter.writes(node), "p" -> pos, - "d" -> dests.dests, "s" -> sticky ) .add("w", Option.when(relay.isEmpty)(who)) @@ -456,9 +455,9 @@ object StudySocket: given Reads[ChapterMaker.EditData] = Json.reads given Reads[ChapterMaker.DescData] = Json.reads given studyDataReads: Reads[Study.Data] = Json.reads - given Reads[SetTag] = Json.reads + given Reads[SetTag] = Json.reads given Reads[Gamebook] = Json.reads - given Reads[ExplorerGame] = Json.reads + given Reads[ExplorerGame] = Json.reads object Out: def getIsPresent(reqId: Int, studyId: StudyId, userId: UserId) = diff --git a/ui/analyse/src/study/interfaces.ts b/ui/analyse/src/study/interfaces.ts index 5d15a72dc4996..5f8f90b706d3b 100644 --- a/ui/analyse/src/study/interfaces.ts +++ b/ui/analyse/src/study/interfaces.ts @@ -276,7 +276,6 @@ export interface AnaDrop { ch?: string; } export interface ServerNodeMsg extends WithWhoAndPos { - d: string; n: Tree.NodeFromServer; o: Opening; s: boolean; diff --git a/ui/analyse/src/study/studyCtrl.ts b/ui/analyse/src/study/studyCtrl.ts index 6a015de8061d7..1be2b92180b7e 100644 --- a/ui/analyse/src/study/studyCtrl.ts +++ b/ui/analyse/src/study/studyCtrl.ts @@ -665,7 +665,7 @@ export default class StudyCtrl { this.data.chapter.relayPath = d.relayPath; const newPath = this.ctrl.tree.addNode(node, position.path); if (!newPath) return this.xhrReload(); - this.ctrl.tree.addDests(d.d, newPath); + if (d.n.dests) this.ctrl.tree.addDests(d.n.dests, newPath); if (d.relayPath && !this.ctrl.tree.pathIsMainline(d.relayPath)) this.ctrl.tree.promoteAt(d.relayPath, true); if (sticky) this.data.position.path = newPath; From 287ba0b2575035f577b8f8334703592deaed75ed Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 18:13:10 +0100 Subject: [PATCH 23/27] client-side socket batch support --- ui/common/src/socket.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/common/src/socket.ts b/ui/common/src/socket.ts index 06eb0334e25b2..4de4a3add83fe 100644 --- a/ui/common/src/socket.ts +++ b/ui/common/src/socket.ts @@ -286,6 +286,8 @@ class WsSocket { case 'ack': this.ackable.onServerAck(m.d); break; + case 'batch': + m.d.forEach(this.handle); default: // return true in a receive handler to prevent pubsub and events if (!(this.settings.receive && this.settings.receive(m.t, m.d))) { From aa7c03ecfe606ae5bd4ea480ac09493cc1d180d9 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 18:14:26 +0100 Subject: [PATCH 24/27] tweak board saturation --- ui/analyse/css/study/relay/_layout.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/analyse/css/study/relay/_layout.scss b/ui/analyse/css/study/relay/_layout.scss index f5266613a702b..798ae1f41f26a 100644 --- a/ui/analyse/css/study/relay/_layout.scss +++ b/ui/analyse/css/study/relay/_layout.scss @@ -131,7 +131,7 @@ main.analyse.is-relay:not(.has-relay-tour) { } &.relay-live-away { .main-board { - filter: saturate(0.5) brightness(0.9); + filter: saturate(0.65) brightness(0.9); } } } From 82f9f28931fd9951892786b7b4a8d67c7653d362 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Fri, 27 Dec 2024 12:58:29 -0600 Subject: [PATCH 25/27] use licon mappings in analyse.study.tour.ts --- ui/analyse/src/plugins/analyse.study.tour.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ui/analyse/src/plugins/analyse.study.tour.ts b/ui/analyse/src/plugins/analyse.study.tour.ts index ef56ab292b673..ba5f39a07e90d 100644 --- a/ui/analyse/src/plugins/analyse.study.tour.ts +++ b/ui/analyse/src/plugins/analyse.study.tour.ts @@ -2,6 +2,7 @@ import type AnalyseCtrl from '../ctrl'; import Shepherd from 'shepherd.js'; import type { ChapterTab, StudyTour, Tab } from '../study/interfaces'; import { pubsub } from 'common/pubsub'; +import * as licon from 'common/licon'; export function initModule(): StudyTour { return { @@ -48,8 +49,8 @@ export function initModule(): StudyTour { { title: 'Study members', text: - " Spectators can view the study and talk in the chat.
" + - "
Contributors can make moves and update the study.", + ` Spectators can view the study and talk in the chat.
` + + `
Contributors can make moves and update the study.`, attachTo: { element: '.study__members', on: 'right' }, when: onTab('members'), }, @@ -58,7 +59,9 @@ export function initModule(): StudyTour { if (ctrl.study?.members.isOwner()) { steps.push({ title: 'Invite members', - text: "By clicking the button.
" + 'Then decide who can contribute or not.', + text: + `By clicking the button.
` + + 'Then decide who can contribute or not.', attachTo: { element: '.study__members .add', on: 'right' }, when: onTab('members'), }); @@ -76,7 +79,7 @@ export function initModule(): StudyTour { if (ctrl.study?.members.canContribute()) { steps.push({ title: 'Create new chapters', - text: "By clicking the button.", + text: `By clicking the button.`, attachTo: { element: '.study__chapters .add', on: 'right' }, when: onTab('chapters'), scrollTo: true, @@ -84,8 +87,8 @@ export function initModule(): StudyTour { steps.push({ title: 'Comment on a position', text: - "With the button, or a right click on the move list on the right.
" + - 'Comments are shared and persisted.', + `With the button, or a right click on the move ` + + 'list on the right.
Comments are shared and persisted.', attachTo: { element: '.study__buttons .left-buttons .comments', on: 'top' }, }); steps.push({ From af1693bcd8803b72c805382f0a584bd228c6ad8e Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 20:39:24 +0100 Subject: [PATCH 26/27] only dim broadcast board in variations, not history --- ui/analyse/css/study/relay/_layout.scss | 2 +- ui/analyse/src/study/studyCtrl.ts | 3 +++ ui/analyse/src/view/components.ts | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/analyse/css/study/relay/_layout.scss b/ui/analyse/css/study/relay/_layout.scss index 798ae1f41f26a..fbf016606c476 100644 --- a/ui/analyse/css/study/relay/_layout.scss +++ b/ui/analyse/css/study/relay/_layout.scss @@ -129,7 +129,7 @@ main.analyse.is-relay:not(.has-relay-tour) { flex: 1 0 200px; } } - &.relay-live-away { + &.relay-in-variation { .main-board { filter: saturate(0.65) brightness(0.9); } diff --git a/ui/analyse/src/study/studyCtrl.ts b/ui/analyse/src/study/studyCtrl.ts index 1be2b92180b7e..7a728e42d766f 100644 --- a/ui/analyse/src/study/studyCtrl.ts +++ b/ui/analyse/src/study/studyCtrl.ts @@ -535,6 +535,9 @@ export default class StudyCtrl { defined(this.data.chapter.relayPath) && this.ctrl.path !== this.data.chapter.relayPath; + isRelayAndInVariation = (): boolean => + this.isRelayAwayFromLive() && !treePath.contains(this.data.chapter.relayPath!, this.ctrl.path); + setPath = (path: Tree.Path, node: Tree.Node) => { this.onSetPath(path); this.commentForm.onSetPath(this.vm.chapterId, path, node); diff --git a/ui/analyse/src/view/components.ts b/ui/analyse/src/view/components.ts index 8ffb899fdd436..88b7f03fe34dc 100644 --- a/ui/analyse/src/view/components.ts +++ b/ui/analyse/src/view/components.ts @@ -123,7 +123,7 @@ export function renderMain( 'is-relay': isRelay, 'analyse-hunter': ctrl.opts.hunter, 'analyse--wiki': !!ctrl.wiki && !ctrl.study, - 'relay-live-away': !!ctrl.study?.isRelayAwayFromLive(), + 'relay-in-variation': !!ctrl.study?.isRelayAndInVariation(), }, }, kids, From c81103493081123c7f7f186c966b88a9532160de Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 27 Dec 2024 20:40:37 +0100 Subject: [PATCH 27/27] only dim the broadcast board when in a variation --- ui/analyse/css/study/relay/_layout.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/analyse/css/study/relay/_layout.scss b/ui/analyse/css/study/relay/_layout.scss index fbf016606c476..a06bcfdbd70bd 100644 --- a/ui/analyse/css/study/relay/_layout.scss +++ b/ui/analyse/css/study/relay/_layout.scss @@ -130,7 +130,7 @@ main.analyse.is-relay:not(.has-relay-tour) { } } &.relay-in-variation { - .main-board { + .main-board cg-board { filter: saturate(0.65) brightness(0.9); } }