diff --git a/app/controllers/Clas.scala b/app/controllers/Clas.scala index b27410747d61d..ce44029ffc7e1 100644 --- a/app/controllers/Clas.scala +++ b/app/controllers/Clas.scala @@ -472,6 +472,24 @@ final class Clas(env: Env, authC: Auth) extends LilaController(env): else redirectTo(clas) } + def studentMove(id: ClasId, username: UserStr) = Secure(_.Teacher) { ctx ?=> me ?=> + WithClassAndStudents(id): (clas, students) => + WithStudent(clas, username): s => + for + classes <- env.clas.api.clas.of(me) + others = classes.filter(_.id != clas.id) + res <- Ok.page(views.clas.student.move(clas, students, s, others)) + yield res + } + + def studentMovePost(id: ClasId, username: UserStr, to: ClasId) = SecureBody(_.Teacher) { ctx ?=> me ?=> + WithClassAndStudents(id): (clas, students) => + WithStudent(clas, username): s => + WithClass(to): toClas => + for _ <- env.clas.api.student.move(s, toClas) + yield Redirect(routes.Clas.show(clas.id)).flashSuccess + } + def becomeTeacher = AuthBody { ctx ?=> me ?=> couldBeTeacher.elseNotFound: val perm = lila.core.perm.Permission.Teacher.dbKey diff --git a/app/controllers/Puzzle.scala b/app/controllers/Puzzle.scala index 291ff737b3760..72b101ba998cd 100644 --- a/app/controllers/Puzzle.scala +++ b/app/controllers/Puzzle.scala @@ -535,12 +535,13 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env): Auth { ctx ?=> me ?=> meOrFetch(username) .flatMapz: user => - (fuccess(isGranted(_.CheatHunter)) >>| + (fuccess(user.is(me) || isGranted(_.CheatHunter)) >>| user.enabled.yes.so(env.clas.api.clas.isTeacherOf(me, user.id))).map { _.option(user) } - .dmap(_ | me.value) - .flatMap(f(_)) + .flatMap: + case Some(user) => f(user) + case None => Redirect(routes.Puzzle.dashboard(Days(30), "home", none)) } def WithPuzzlePerf[A](f: Perf ?=> Fu[A])(using Option[Me]): Fu[A] = diff --git a/app/controllers/User.scala b/app/controllers/User.scala index a2cdadbe12740..0b73d2fbd6440 100644 --- a/app/controllers/User.scala +++ b/app/controllers/User.scala @@ -553,21 +553,24 @@ final class User( } def perfStat(username: UserStr, perfKey: PerfKey) = Open: - Found(env.perfStat.api.data(username, perfKey)): data => - negotiate( - Ok.async: - env.history - .ratingChartApi(data.user.user) - .map: - views.user.perfStatPage(data, _) - , - JsonOk: - getBool("graph") - .soFu: - env.history.ratingChartApi.singlePerf(data.user.user, data.stat.perfType.key) - .map: graph => - env.perfStat.jsonView(data).add("graph", graph) - ) + PerfType + .isLeaderboardable(perfKey) + .so: + Found(env.perfStat.api.data(username, perfKey)): data => + negotiate( + Ok.async: + env.history + .ratingChartApi(data.user.user) + .map: + views.user.perfStatPage(data, _) + , + JsonOk: + getBool("graph") + .soFu: + env.history.ratingChartApi.singlePerf(data.user.user, data.stat.perfType.key) + .map: graph => + env.perfStat.jsonView(data).add("graph", graph) + ) def autocomplete = OpenOrScoped(): ctx ?=> NoTor: diff --git a/app/views/clas.scala b/app/views/clas.scala index 8811ff105f2df..3a8518dc90c0f 100644 --- a/app/views/clas.scala +++ b/app/views/clas.scala @@ -18,7 +18,7 @@ object student: lazy val formUi = lila.clas.ui.StudentFormUi(helpers, views.clas.ui, ui) export ui.{ invite } - export formUi.{ newStudent as form, many as manyForm, edit, release, close } + export formUi.{ newStudent as form, many as manyForm, edit, release, close, move } def show( clas: Clas, diff --git a/bin/mongodb/recap-notif.js b/bin/mongodb/recap-notif.js index c828c51288300..cf22182bc9c4c 100644 --- a/bin/mongodb/recap-notif.js +++ b/bin/mongodb/recap-notif.js @@ -70,16 +70,17 @@ function sendToRandomOfflinePlayers() { print(`+ ${countSent - lastPrinted} = ${countSent} / ${countAll} | ${user.createdAt.toLocaleDateString('fr')}`); lastPrinted = countSent; } - sleep(15 * newUsers.length); + sleep(10 * newUsers.length); } } db.user4.find({ enabled: true, - createdAt: { $lt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7) }, + createdAt: { $lt: new Date(year, 9, 1) }, seenAt: { - $gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14), - $lt: new Date(Date.now() - 1000 * 60 * 20) // avoid the lila notif cache! - } + $gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30 * 3), + // $lt: new Date(Date.now() - 1000 * 60 * 20) // avoid the lila notif cache! + }, + marks: { $nin: ['boost', 'engine', 'troll'] } }).forEach(process); process(); // flush the generator } diff --git a/conf/clas.routes b/conf/clas.routes index 7d119b15396be..893c5a1bc7c8b 100644 --- a/conf/clas.routes +++ b/conf/clas.routes @@ -32,3 +32,5 @@ POST /class/$id<\w{8}>/student/:username/release controllers.clas.Clas.studentR GET /class/$id<\w{8}>/student/:username/close controllers.clas.Clas.studentClose(id: ClasId, username: UserStr) POST /class/$id<\w{8}>/student/:username/close controllers.clas.Clas.studentClosePost(id: ClasId, username: UserStr) POST /class/$id<\w{8}>/invitation/revoke controllers.clas.Clas.invitationRevoke(id: ClasInviteId) +GET /class/$id<\w{8}>/student/:username/move controllers.clas.Clas.studentMove(id: ClasId, username: UserStr) +POST /class/$id<\w{8}>/student/:username/move/$to<\w{8}> controllers.clas.Clas.studentMovePost(id: ClasId, username: UserStr, to: ClasId) diff --git a/modules/clas/src/main/ClasApi.scala b/modules/clas/src/main/ClasApi.scala index 94404a085ef2c..b37969700376e 100644 --- a/modules/clas/src/main/ClasApi.scala +++ b/modules/clas/src/main/ClasApi.scala @@ -258,6 +258,17 @@ final class ClasApi( sendWelcomeMessage(teacher.id, user, clas)).inject(Student.WithPassword(student, password)) } + def move(s: Student.WithUser, toClas: Clas)(using teacher: Me): Fu[Option[Student]] = for + _ <- closeAccount(s) + stu = s.student.copy(id = Student.makeId(s.user.id, toClas.id), clasId = toClas.id) + moved <- colls.student.insert + .one(stu) + .inject(stu.some) + .recoverWith(lila.db.recoverDuplicateKey { _ => + student.get(toClas, s.user.id) + }) + yield moved + def manyCreate( clas: Clas, data: ClasForm.ManyNewStudent, diff --git a/modules/clas/src/main/ClasMatesCache.scala b/modules/clas/src/main/ClasMatesCache.scala index b0285aab0b17f..bc4d0fc45c77c 100644 --- a/modules/clas/src/main/ClasMatesCache.scala +++ b/modules/clas/src/main/ClasMatesCache.scala @@ -12,7 +12,7 @@ final class ClasMatesCache(colls: ClasColls, cacheApi: CacheApi, studentCache: C def get(studentId: UserId): Fu[Set[UserId]] = studentCache.isStudent(studentId).so(cache.get(studentId)) - private val cache = cacheApi[UserId, Set[UserId]](256, "clas.mates"): + private val cache = cacheApi[UserId, Set[UserId]](64, "clas.mates"): _.expireAfterWrite(5 minutes) .buildAsyncFuture(fetchMatesAndTeachers) diff --git a/modules/clas/src/main/ui/DashboardUi.scala b/modules/clas/src/main/ui/DashboardUi.scala index 7d36bbe73c43e..532c19eac7f56 100644 --- a/modules/clas/src/main/ui/DashboardUi.scala +++ b/modules/clas/src/main/ui/DashboardUi.scala @@ -139,11 +139,13 @@ final class DashboardUi(helpers: Helpers, ui: ClasUi)(using NetDomain): tr( td(userIdLink(i.userId.some)), td(i.realName), - td(if i.accepted.has(false) then "Declined" else "Pending"), + td( + if i.accepted.has(false) then trans.clas.declined.txt() else trans.clas.pending.txt() + ), td(momentFromNow(i.created.at)), td: postForm(action := routes.Clas.invitationRevoke(i.id)): - submitButton(cls := "button button-red button-empty")("Revoke") + submitButton(cls := "button button-red button-empty")(trans.site.delete()) ) ) val archivedBox = diff --git a/modules/clas/src/main/ui/StudentFormUi.scala b/modules/clas/src/main/ui/StudentFormUi.scala index 59803a9417101..38095a7e77b63 100644 --- a/modules/clas/src/main/ui/StudentFormUi.scala +++ b/modules/clas/src/main/ui/StudentFormUi.scala @@ -217,7 +217,11 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi cls := "button button-empty button-red", title := trans.clas.closeDesc1.txt() )(trans.clas.closeStudent()) - ) + ), + a( + href := routes.Clas.studentMove(clas.id, s.user.username), + cls := "button button-empty" + )(trans.clas.moveToAnotherClass()) ) ) ) @@ -250,6 +254,32 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi ) ) + def move(clas: Clas, students: List[Student], s: Student.WithUser, otherClasses: List[Clas])(using + Context + ) = + ClasPage(s.user.username.value, Left(clas.withStudents(students)), s.student.some)( + cls := "student-show student-edit" + ): + + val classForms: Frag = otherClasses.map: toClass => + postForm(action := routes.Clas.studentMovePost(clas.id, s.student.userId, toClass.id))( + form3.submit(toClass.name, icon = Icon.InternalArrow.some)( + cls := "yes-no-confirm button-blue button-empty", + title := trans.clas.moveToClass.txt(toClass.name) + ) + ) + + frag( + studentUi.top(clas, s), + div(cls := "box__pad")( + h2(trans.clas.moveToAnotherClass()), + classForms, + form3.actions( + a(href := routes.Clas.studentShow(clas.id, s.user.username))(trans.site.cancel()) + ) + ) + ) + def close(clas: Clas, students: List[Student], s: Student.WithUser)(using Context) = ClasPage(s.user.username.value, Left(clas.withStudents(students)), s.student.some)( cls := "student-show student-edit" diff --git a/modules/coreI18n/src/main/key.scala b/modules/coreI18n/src/main/key.scala index eea2f5f174079..47a1a657abd23 100644 --- a/modules/coreI18n/src/main/key.scala +++ b/modules/coreI18n/src/main/key.scala @@ -282,6 +282,8 @@ object I18nKey: val `welcomeToClass`: I18nKey = "class:welcomeToClass" val `invitationToClass`: I18nKey = "class:invitationToClass" val `clickToViewInvitation`: I18nKey = "class:clickToViewInvitation" + val `pending`: I18nKey = "class:pending" + val `declined`: I18nKey = "class:declined" val `onlyVisibleToTeachers`: I18nKey = "class:onlyVisibleToTeachers" val `lastActiveDate`: I18nKey = "class:lastActiveDate" val `managed`: I18nKey = "class:managed" @@ -330,6 +332,8 @@ object I18nKey: val `anInvitationHasBeenSentToX`: I18nKey = "class:anInvitationHasBeenSentToX" val `xAlreadyHasAPendingInvitation`: I18nKey = "class:xAlreadyHasAPendingInvitation" val `xIsAKidAccountWarning`: I18nKey = "class:xIsAKidAccountWarning" + val `moveToClass`: I18nKey = "class:moveToClass" + val `moveToAnotherClass`: I18nKey = "class:moveToAnotherClass" val `nbPendingInvitations`: I18nKey = "class:nbPendingInvitations" val `nbTeachers`: I18nKey = "class:nbTeachers" val `nbStudents`: I18nKey = "class:nbStudents" @@ -659,8 +663,6 @@ object I18nKey: val `ultraBulletBulletBlitzRapidClassicalAndCorrespondenceChess`: I18nKey = "features:ultraBulletBulletBlitzRapidClassicalAndCorrespondenceChess" val `allFeaturesToCome`: I18nKey = "features:allFeaturesToCome" val `landscapeSupportOnApp`: I18nKey = "features:landscapeSupportOnApp" - val `supportLichess`: I18nKey = "features:supportLichess" - val `contributeToLichessAndGetIcon`: I18nKey = "features:contributeToLichessAndGetIcon" val `everybodyGetsAllFeaturesForFree`: I18nKey = "features:everybodyGetsAllFeaturesForFree" val `weBelieveEveryChessPlayerDeservesTheBest`: I18nKey = "features:weBelieveEveryChessPlayerDeservesTheBest" val `allFeaturesAreFreeForEverybody`: I18nKey = "features:allFeaturesAreFreeForEverybody" diff --git a/modules/perfStat/src/main/PerfStat.scala b/modules/perfStat/src/main/PerfStat.scala index 6a1e171187b74..3c7ac6f84ffd5 100644 --- a/modules/perfStat/src/main/PerfStat.scala +++ b/modules/perfStat/src/main/PerfStat.scala @@ -6,6 +6,7 @@ import java.time.Duration import scalalib.HeapSort import lila.rating.PerfType +import lila.rating.PerfType.GamePerf extension (p: Pov) def loss = p.game.winner.map(_.color != p.color) @@ -41,13 +42,13 @@ object PerfStat: type Getter = (User, PerfType) => Fu[PerfStat] - def makeId(userId: UserId, perfType: PerfType) = s"$userId/${perfType.id}" + def makeId(userId: UserId, perf: GamePerf) = s"$userId/${perf.id}" - def init(userId: UserId, perfType: PerfType) = + def init(userId: UserId, perf: GamePerf) = PerfStat( - id = makeId(userId, perfType), + id = makeId(userId, perf), userId = userId, - perfType = perfType, + perfType = perf, highest = none, lowest = none, bestWins = Results(Nil), diff --git a/modules/perfStat/src/main/PerfStatApi.scala b/modules/perfStat/src/main/PerfStatApi.scala index 03085be657d3a..84dd19376cf09 100644 --- a/modules/perfStat/src/main/PerfStatApi.scala +++ b/modules/perfStat/src/main/PerfStatApi.scala @@ -6,6 +6,7 @@ import lila.core.perm.Granter import lila.rating.Glicko.minRating import lila.rating.PerfExt.established import lila.rating.{ PerfType, UserRankMap } +import lila.rating.PerfType.GamePerf case class PerfStatData( user: UserWithPerfs, @@ -30,37 +31,43 @@ final class PerfStatApi( extends lila.core.perf.PerfStatApi: def data(name: UserStr, perfKey: PerfKey)(using me: Option[Me]): Fu[Option[PerfStatData]] = - userApi.withPerfs(name.id).flatMap { - _.filter: u => - (u.enabled.yes && (!u.lame || me.exists(_.is(u.user)))) || me.soUse(Granter(_.UserModView)) - .filter: u => - !u.isBot || (perfKey != PerfKey.ultraBullet) - .soFu: u => - for - oldPerfStat <- get(u.user.id, perfKey) - perfStat = oldPerfStat.copy(playStreak = oldPerfStat.playStreak.checkCurrent) - distribution <- u - .perfs(perfKey) - .established - .soFu(weeklyRatingDistribution(perfKey)) - percentile = calcPercentile(distribution, u.perfs(perfKey).intRating) - percentileLow = perfStat.lowest.flatMap { r => calcPercentile(distribution, r.int) } - percentileHigh = perfStat.highest.flatMap { r => calcPercentile(distribution, r.int) } - _ = lightUserApi.preloadUser(u.user) - _ <- lightUserApi.preloadMany(perfStat.userIds) - yield PerfStatData(u, perfStat, rankingsOf(u.id), percentile, percentileLow, percentileHigh) - } + PerfType(perfKey) match + case pk: GamePerf => + userApi.withPerfs(name.id).flatMap { + _.filter: u => + (u.enabled.yes && (!u.lame || me.exists(_.is(u.user)))) || me.soUse(Granter(_.UserModView)) + .filter: u => + !u.isBot || (perfKey != PerfKey.ultraBullet) + .soFu: u => + for + oldPerfStat <- get(u.user.id, pk) + perfStat = oldPerfStat.copy(playStreak = oldPerfStat.playStreak.checkCurrent) + distribution <- u + .perfs(perfKey) + .established + .soFu(weeklyRatingDistribution(perfKey)) + percentile = calcPercentile(distribution, u.perfs(perfKey).intRating) + percentileLow = perfStat.lowest.flatMap { r => calcPercentile(distribution, r.int) } + percentileHigh = perfStat.highest.flatMap { r => calcPercentile(distribution, r.int) } + _ = lightUserApi.preloadUser(u.user) + _ <- lightUserApi.preloadMany(perfStat.userIds) + yield PerfStatData(u, perfStat, rankingsOf(u.id), percentile, percentileLow, percentileHigh) + } + case pk => fuccess(none) private def calcPercentile(wrd: Option[List[Int]], intRating: IntRating): Option[Double] = wrd.map: distrib => val (under, sum) = percentileOf(distrib, intRating) Math.round(under * 1000.0 / sum) / 10.0 - def get(user: UserId, perfType: PerfType): Fu[PerfStat] = - storage.find(user, perfType).getOrElse(indexer.userPerf(user, perfType)) + def get(user: UserId, perf: GamePerf): Fu[PerfStat] = + storage.find(user, perf).getOrElse(indexer.userPerf(user, perf)) def highestRating(user: UserId, perfKey: PerfKey): Fu[Option[IntRating]] = - get(user, perfKey).map(_.highest.map(_.int)) + PerfType + .gamePerf(perfKey) + .so: (gp: GamePerf) => + get(user, gp).map(_.highest.map(_.int)) object weeklyRatingDistribution: diff --git a/modules/perfStat/src/main/PerfStatIndexer.scala b/modules/perfStat/src/main/PerfStatIndexer.scala index a53822b437a38..6941fba44deaa 100644 --- a/modules/perfStat/src/main/PerfStatIndexer.scala +++ b/modules/perfStat/src/main/PerfStatIndexer.scala @@ -1,12 +1,15 @@ package lila.perfStat import lila.rating.PerfType +import lila.rating.PerfType.GamePerf final class PerfStatIndexer( gameRepo: lila.core.game.GameRepo, storage: PerfStatStorage )(using Executor, Scheduler): + import PerfType.{ isLeaderboardable as isRelevant } + private val workQueue = scalalib.actor.AsyncActorSequencer( maxSize = Max(64), timeout = 10 seconds, @@ -14,7 +17,7 @@ final class PerfStatIndexer( lila.log.asyncActorMonitor.full ) - private[perfStat] def userPerf(user: UserId, perfKey: PerfKey): Fu[PerfStat] = + private[perfStat] def userPerf(user: UserId, perfKey: GamePerf): Fu[PerfStat] = workQueue: storage .find(user, perfKey) @@ -36,7 +39,10 @@ final class PerfStatIndexer( addPov(Pov(game, player), userId) private def addPov(pov: Pov, userId: UserId): Funit = - storage - .find(userId, pov.game.perfKey) - .flatMapz: perfStat => - storage.update(perfStat, perfStat.agg(pov)) + PerfType + .gamePerf(pov.game.perfKey) + .so: (pk: GamePerf) => + storage + .find(userId, pk) + .flatMapz: perfStat => + storage.update(perfStat, perfStat.agg(pov)) diff --git a/modules/perfStat/src/main/PerfStatStorage.scala b/modules/perfStat/src/main/PerfStatStorage.scala index 5b9e3885f4390..a2a4fb1767b43 100644 --- a/modules/perfStat/src/main/PerfStatStorage.scala +++ b/modules/perfStat/src/main/PerfStatStorage.scala @@ -6,6 +6,7 @@ import lila.db.AsyncCollFailingSilently import lila.db.dsl.{ *, given } import lila.rating.BSONHandlers.perfTypeIdHandler import lila.rating.PerfType +import lila.rating.PerfType.GamePerf final class PerfStatStorage(coll: AsyncCollFailingSilently)(using Executor): @@ -21,8 +22,8 @@ final class PerfStatStorage(coll: AsyncCollFailingSilently)(using Executor): private given BSONDocumentHandler[Count] = Macros.handler private given BSONDocumentHandler[PerfStat] = Macros.handler - def find(userId: UserId, perfType: PerfType): Fu[Option[PerfStat]] = - coll(_.byId[PerfStat](PerfStat.makeId(userId, perfType))) + def find(userId: UserId, perf: GamePerf): Fu[Option[PerfStat]] = + coll(_.byId[PerfStat](PerfStat.makeId(userId, perf))) def insert(perfStat: PerfStat): Funit = coll(_.insert.one(perfStat).void) diff --git a/modules/plan/src/main/ui/PlanPages.scala b/modules/plan/src/main/ui/PlanPages.scala index fd096c0417dc2..86631ee5882bf 100644 --- a/modules/plan/src/main/ui/PlanPages.scala +++ b/modules/plan/src/main/ui/PlanPages.scala @@ -153,19 +153,6 @@ final class PlanPages(helpers: Helpers)(fishnetPerDay: Int): tr(check)( strong(trans.features.allFeaturesToCome()) ) - ), - header(h1(trans.features.supportLichess())), - tbody(cls := "support")( - st.tr( - th(trans.features.contributeToLichessAndGetIcon()), - td("-"), - td(span(dataIcon := patronIconChar, cls := "is is-green text check")(trans.site.yes())) - ), - st.tr(cls := "price")( - th, - td(cls := "green")("$0"), - td(a(href := routes.Plan.index(), cls := "green button")("$5/month")) - ) ) ), p(cls := "explanation")( diff --git a/modules/rating/src/main/PerfType.scala b/modules/rating/src/main/PerfType.scala index 814931a5f7123..56279b9fc7f13 100644 --- a/modules/rating/src/main/PerfType.scala +++ b/modules/rating/src/main/PerfType.scala @@ -163,12 +163,23 @@ enum PerfType( ) object PerfType: + + // all but standard and puzzle + type GamePerf = Bullet.type | Blitz.type | Rapid.type | Classical.type | UltraBullet.type | + Correspondence.type | Crazyhouse.type | Chess960.type | KingOfTheHill.type | ThreeCheck.type | + Antichess.type | Atomic.type | Horde.type | RacingKings.type + + def gamePerf(pt: PerfType): Option[GamePerf] = pt match + case gp: GamePerf => Some(gp) + case _ => None + given Conversion[PerfType, PerfKey] = _.key given Conversion[PerfType, PerfId] = _.id given Conversion[PerfKey, PerfType] = apply(_) - val all: List[PerfType] = values.toList - val byKey = all.mapBy(_.key) - val byId = all.mapBy(_.id) + + val all: List[PerfType] = values.toList + val byKey = all.mapBy(_.key) + val byId = all.mapBy(_.id) def apply(key: PerfKey): PerfType = byKey.getOrElse(key, sys.error(s"Impossible: $key couldn't have been instantiated")) @@ -197,6 +208,7 @@ object PerfType: PerfKey.racingKings ) val isLeaderboardable: Set[PerfKey] = leaderboardable.toSet + val variants: List[PerfKey] = List( PerfKey.crazyhouse, diff --git a/modules/ui/src/main/helper/Form3.scala b/modules/ui/src/main/helper/Form3.scala index d7cfed2820c63..3ad27aa98bf3d 100644 --- a/modules/ui/src/main/helper/Form3.scala +++ b/modules/ui/src/main/helper/Form3.scala @@ -177,9 +177,9 @@ final class Form3(formHelper: FormHelper & I18nHelper & AssetHelper, flairApi: F name := nameValue.map(_._1), value := nameValue.map(_._2), cls := List( - "submit button" -> true, - "text" -> icon.isDefined, - "confirm" -> confirm.nonEmpty + "submit button" -> true, + "text" -> icon.isDefined, + "yes-no-confirm" -> confirm.nonEmpty ), title := confirm )(content) diff --git a/modules/user/src/main/ui/UserShowSide.scala b/modules/user/src/main/ui/UserShowSide.scala index 0056217c986da..487ba7aeeeeb2 100644 --- a/modules/user/src/main/ui/UserShowSide.scala +++ b/modules/user/src/main/ui/UserShowSide.scala @@ -30,7 +30,10 @@ final class UserShowSide(helpers: Helpers): "active" -> active.contains(pk) ), href := ctx.pref.showRatings.so: - if isPuzzle then routes.Puzzle.dashboard(Days(30), "home", u.username.some).url + if isPuzzle + then + val other = ctx.isnt(u).option(u.username) + routes.Puzzle.dashboard(Days(30), "home", other).url else routes.User.perfStat(u.username, pk).url , span( diff --git a/project/Dependencies.scala b/project/Dependencies.scala index e2c1be547ca12..90f25cef40626 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -25,7 +25,7 @@ object Dependencies { val scalatags = "com.lihaoyi" %% "scalatags" % "0.13.1" val lettuce = "io.lettuce" % "lettuce-core" % "6.5.1.RELEASE" val nettyTransport = - ("io.netty" % s"netty-transport-native-$notifier" % "4.1.115.Final").classifier(s"$os-$arch") + ("io.netty" % s"netty-transport-native-$notifier" % "4.1.116.Final").classifier(s"$os-$arch") val lilaSearch = "org.lichess.search" %% "client" % "3.1.0" val munit = "org.scalameta" %% "munit" % "1.0.3" % Test val uaparser = "org.uaparser" %% "uap-scala" % "0.18.0" diff --git a/translation/source/class.xml b/translation/source/class.xml index c037cb3407514..5c2bccacb8046 100644 --- a/translation/source/class.xml +++ b/translation/source/class.xml @@ -57,6 +57,8 @@ Here is the link to access the class. One pending invitation %s pending invitations + Pending + Declined Only visible to the class teachers Active Managed @@ -114,4 +116,6 @@ It will display a horizontal line. An invitation has been sent to %s %s already has a pending invitation %1$s is a kid account and can't receive your message. You must give them the invitation URL manually: %2$s + Move to %s + Move to another class diff --git a/translation/source/features.xml b/translation/source/features.xml index 335cb1336d2fe..0859204b4e081 100644 --- a/translation/source/features.xml +++ b/translation/source/features.xml @@ -24,8 +24,6 @@ UltraBullet, Bullet, Blitz, Rapid, Classical, Correspondence Chess All features to come, forever! iPhone & Android phones and tablets, landscape support - Support Lichess - Contribute to Lichess and get a cool looking Patron icon Yes, both accounts have the same features! We believe every chess player deserves the best, and so: All features are free for everybody, forever! diff --git a/ui/@types/lichess/i18n.d.ts b/ui/@types/lichess/i18n.d.ts index f754e45b9a9e8..3cf9d394c987c 100644 --- a/ui/@types/lichess/i18n.d.ts +++ b/ui/@types/lichess/i18n.d.ts @@ -471,6 +471,8 @@ interface I18n { createMultipleAccounts: string; /** Only create accounts for real students. Do not use this to make multiple accounts for yourself. You would get banned. */ createStudentWarning: string; + /** Declined */ + declined: string; /** Edit news */ editNews: string; /** Features */ @@ -515,6 +517,10 @@ interface I18n { maxStudentsNote: I18nFormat; /** Message all students about new class material */ messageAllStudents: string; + /** Move to another class */ + moveToAnotherClass: string; + /** Move to %s */ + moveToClass: I18nFormat; /** You can also %s to create multiple Lichess accounts from a list of student names. */ multipleAccsFormDescription: I18nFormat; /** N/A */ @@ -555,6 +561,8 @@ interface I18n { overview: string; /** Password: %s */ passwordX: I18nFormat; + /** Pending */ + pending: string; /** Private. Will never be shown outside the class. Helps you remember who the student is. */ privateWillNeverBeShown: string; /** Progress */ @@ -1239,8 +1247,6 @@ interface I18n { chessInsights: string; /** Cloud engine analysis */ cloudEngineAnalysis: string; - /** Contribute to Lichess and get a cool looking Patron icon */ - contributeToLichessAndGetIcon: string; /** Correspondence chess with conditional premoves */ correspondenceWithConditionalPremoves: string; /** Deep %s server analysis */ @@ -1269,8 +1275,6 @@ interface I18n { standardChessAndX: I18nFormat; /** Studies (shareable and persistent analysis) */ studies: string; - /** Support Lichess */ - supportLichess: string; /** Support us with a Patron account! */ supportUsWithAPatronAccount: string; /** Tactical puzzles from user games */ diff --git a/ui/bits/css/_feature.scss b/ui/bits/css/_feature.scss index 7b0fe2b5a13e4..c8d2f7178375a 100644 --- a/ui/bits/css/_feature.scss +++ b/ui/bits/css/_feature.scss @@ -42,15 +42,6 @@ font-size: 1.4em; } - .price { - font-size: 1.4em; - } - - .price > * { - border: none; - padding-top: 30px; - } - .explanation { @extend %box-neat; @@ -101,10 +92,6 @@ font-weight: normal; } - .price { - display: none; - } - .explanation { font-size: 1.1em; margin: 3em 0 1em 0; diff --git a/ui/puzzle/src/report.ts b/ui/puzzle/src/report.ts index ed0fff670bb81..354a12f507d2a 100644 --- a/ui/puzzle/src/report.ts +++ b/ui/puzzle/src/report.ts @@ -14,7 +14,7 @@ export default class Report { tsHideReportDialog: StoredProp; // bump when logic is changed, to distinguish cached clients from new ones - private version = 5; + private version = 6; constructor() { this.tsHideReportDialog = storedIntProp('puzzle.report.hide.ts', 0); @@ -47,7 +47,7 @@ export default class Report { ctrl.mainline.some((n: Tree.Node) => n.id === node.id) ) { const [bestEval, secondBestEval] = [ev.pvs[0], ev.pvs[1]]; - // stricly identical to lichess-puzzler v49 check + // stricter than lichess-puzzler v49 check in how it defines similar moves if ( (ev.depth > 50 || ev.nodes > 25_000_000) && bestEval && @@ -56,7 +56,9 @@ export default class Report { ) { // in all case, we do not want to show the dialog more than once this.reported = true; - const reason = `(v${this.version}) after move ${plyToTurn(node.ply)}. ${node.san}, at depth ${ev.depth}, multiple solutions, pvs ${ev.pvs.map(pv => `${pv.moves[0]}: ${showPv(pv)}`).join(', ')}`; + const engine = ctrl.ceval.engines.active; + const engineName = engine?.short || engine.name; + const reason = `(v${this.version}, ${engineName}) after move ${plyToTurn(node.ply)}. ${node.san}, at depth ${ev.depth}, multiple solutions, pvs ${ev.pvs.map(pv => `${pv.moves[0]}: ${showPv(pv)}`).join(', ')}`; this.reportDialog(ctrl.data.puzzle.id, reason); } }