diff --git a/app/controllers/Account.scala b/app/controllers/Account.scala index 632d5b810112a..8efd45e5e345d 100644 --- a/app/controllers/Account.scala +++ b/app/controllers/Account.scala @@ -235,6 +235,12 @@ final class Account( Redirect(routes.Account.twoFactor).flashSuccess } + def network(usingAltSocket: Option[Boolean]) = Auth { _ ?=> _ ?=> + val page = (use: Option[Boolean]) => Ok.page(html.account.network(use)) + if usingAltSocket.isEmpty || usingAltSocket.has(ctx.pref.isUsingAltSocket) then page(none) + else env.pref.api.setPref(ctx.pref.copy(usingAltSocket = usingAltSocket)) >> page(usingAltSocket) + } + def close = Auth { _ ?=> me ?=> env.clas.api.student.isManaged(me) flatMap { managed => env.security.forms.closeAccount.flatMap: form => diff --git a/app/views/account/layout.scala b/app/views/account/layout.scala index cd6ea7adbe45d..b3583f9cd3625 100644 --- a/app/views/account/layout.scala +++ b/app/views/account/layout.scala @@ -51,16 +51,19 @@ object layout: a(activeCls("twofactor"), href := routes.Account.twoFactor)( trans.tfa.twoFactorAuth() ), + a(activeCls("oauth.token"), href := routes.OAuthToken.index)( + trans.oauthScope.apiAccessTokens() + ), a(activeCls("security"), href := routes.Account.security)( trans.security() ), div(cls := "sep"), - a(href := routes.Plan.index)(trans.patron.lichessPatron()), - div(cls := "sep"), - a(activeCls("oauth.token"), href := routes.OAuthToken.index)( - trans.oauthScope.apiAccessTokens() + a(activeCls("network"), href := routes.Account.network(none))( + "Network" + ), + ctx.noBot option a(href := routes.DgtCtrl.index)( + trans.dgt.dgtBoard() ), - ctx.noBot option a(href := routes.DgtCtrl.index)(trans.dgt.dgtBoard()), div(cls := "sep"), a(activeCls("close"), href := routes.Account.close)( trans.settings.closeAccount() diff --git a/app/views/account/network.scala b/app/views/account/network.scala new file mode 100644 index 0000000000000..4614f3decabda --- /dev/null +++ b/app/views/account/network.scala @@ -0,0 +1,43 @@ +package views.html +package account + +import lila.app.templating.Environment.{ given, * } +import lila.app.ui.ScalatagsTemplate.{ *, given } + +import controllers.routes + +object network: + + def apply(cfRouting: Option[Boolean])(using ctx: PageContext) = + account.layout( + title = "Network", + active = "network" + ): + val usingCloudflare = cfRouting.getOrElse(ctx.pref.isUsingAltSocket) + div(cls := "box box-pad")( + h1(cls := "box__top")("Network"), + br, + if usingCloudflare then + frag( + flashMessage("warning")("You are currently using Content Delivery Network (CDN) routing."), + p("This feature is experimental but may improve reliability in some regions.") + ) + else + p("If you have frequent disconnects, Content Delivery Network (CDN) routing may improve things.") + , + br, + st.section(a(href := "#routing")(h2(id := "routing")("Network Routing")))( + st.group(cls := "radio"): + List(("Use direct routing", false), ("Use CDN routing", true)) map: (key, value) => + div( + a(value != usingCloudflare option (href := routes.Account.network(value.some)))( + label(value == usingCloudflare option (cls := "active-soft"))(key) + ) + ) + ), + br, + br, + cfRouting.nonEmpty option p(cls := "saved text", dataIcon := licon.Checkmark)( + trans.preferences.yourPreferencesHaveBeenSaved() + ) + ) diff --git a/app/views/base/layout.scala b/app/views/base/layout.scala index 7369b356fa3e5..f45e8708b3d2b 100644 --- a/app/views/base/layout.scala +++ b/app/views/base/layout.scala @@ -214,6 +214,7 @@ object layout: ) private val dataVapid = attr("data-vapid") + private val dataAltSocket = attr("data-alt-socket") private val dataSocketDomains = attr("data-socket-domains") := netConfig.socketDomains.mkString(",") private val dataNonce = attr("data-nonce") private val dataAnnounce = attr("data-announce") @@ -308,6 +309,7 @@ object layout: dataVapid := (ctx.isAuth && env.lilaCookie.isRememberMe(ctx.req)) option vapidPublicKey, dataUser := ctx.userId, dataSoundSet := pref.currentSoundSet.toString, + pref.isUsingAltSocket option (dataAltSocket := netConfig.altSocket.value), dataSocketDomains, dataAssetUrl, dataAssetVersion := assetVersion, diff --git a/conf/base.conf b/conf/base.conf index deeeedc77d6b5..f93a2ac2c770f 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -14,6 +14,7 @@ mongodb { net { domain = "localhost:9663" socket.domains = [ "localhost:9664" ] + socket.alternate = "" asset.domain = ${net.domain} asset.base_url = "http://"${net.asset.domain} asset.base_url_internal = ${net.asset.base_url} diff --git a/conf/routes b/conf/routes index bdc4449080765..a2670c7a5d524 100644 --- a/conf/routes +++ b/conf/routes @@ -754,6 +754,8 @@ POST /account/reopen/send controllers.Account.reopenApply GET /account/reopen/sent controllers.Account.reopenSent GET /account/reopen/login/:token controllers.Account.reopenLogin(token) GET /account/personal-data controllers.Account.data +GET /account/network controllers.Account.network(usingAltSocket: Option[Boolean] ?= None) + # App BC GET /account/security controllers.Account.security POST /account/signout/:sessionId controllers.Account.signout(sessionId) diff --git a/modules/common/src/main/config.scala b/modules/common/src/main/config.scala index 7b154779fb33f..714a990469438 100644 --- a/modules/common/src/main/config.scala +++ b/modules/common/src/main/config.scala @@ -74,6 +74,7 @@ object config: @ConfigName("stage.banner") stageBanner: Boolean, @ConfigName("site.name") siteName: String, @ConfigName("socket.domains") socketDomains: List[String], + @ConfigName("socket.alternate") altSocket: NetDomain, crawlable: Boolean, @ConfigName("ratelimit") rateLimit: RateLimit, email: EmailAddress diff --git a/modules/pref/src/main/Pref.scala b/modules/pref/src/main/Pref.scala index fc0e1e24eda0c..2954adaa8eaa4 100644 --- a/modules/pref/src/main/Pref.scala +++ b/modules/pref/src/main/Pref.scala @@ -43,6 +43,7 @@ case class Pref( pieceNotation: Int, resizeHandle: Int, agreement: Int, + usingAltSocket: Option[Boolean], tags: Map[String, String] = Map.empty ): @@ -94,7 +95,9 @@ case class Pref( def hasKeyboardMove = keyboardMove == KeyboardMove.YES - def hasVoice = voice.contains(Voice.YES) + def hasVoice = voice.has(Voice.YES) + + def isUsingAltSocket = usingAltSocket.has(true) // atob("aHR0cDovL2NoZXNzLWNoZWF0LmNvbS9ob3dfdG9fY2hlYXRfYXRfbGljaGVzcy5odG1s") def botCompatible = @@ -464,6 +467,7 @@ object Pref: pieceNotation = PieceNotation.SYMBOL, resizeHandle = ResizeHandle.INITIAL, agreement = Agreement.current, + usingAltSocket = none, tags = Map.empty ) diff --git a/modules/pref/src/main/PrefHandlers.scala b/modules/pref/src/main/PrefHandlers.scala index 788886ae82528..df46233c79a47 100644 --- a/modules/pref/src/main/PrefHandlers.scala +++ b/modules/pref/src/main/PrefHandlers.scala @@ -51,50 +51,52 @@ private object PrefHandlers: resizeHandle = r.getD("resizeHandle", Pref.default.resizeHandle), moveEvent = r.getD("moveEvent", Pref.default.moveEvent), agreement = r.getD("agreement", 0), + usingAltSocket = r.getO("usingAltSocket"), tags = r.getD("tags", Pref.default.tags) ) def writes(w: BSON.Writer, o: Pref) = $doc( - "_id" -> o._id, - "bg" -> o.bg, - "bgImg" -> o.bgImg, - "is3d" -> o.is3d, - "theme" -> o.theme, - "pieceSet" -> o.pieceSet, - "theme3d" -> o.theme3d, - "pieceSet3d" -> o.pieceSet3d, - "soundSet" -> SoundSet.name2key(o.soundSet), - "autoQueen" -> o.autoQueen, - "autoThreefold" -> o.autoThreefold, - "takeback" -> o.takeback, - "moretime" -> o.moretime, - "clockTenths" -> o.clockTenths, - "clockBar" -> o.clockBar, - "clockSound" -> o.clockSound, - "premove" -> o.premove, - "animation" -> o.animation, - "captured" -> o.captured, - "follow" -> o.follow, - "highlight" -> o.highlight, - "destination" -> o.destination, - "coords" -> o.coords, - "replay" -> o.replay, - "challenge" -> o.challenge, - "message" -> o.message, - "studyInvite" -> o.studyInvite, - "submitMove" -> o.submitMove, - "confirmResign" -> o.confirmResign, - "insightShare" -> o.insightShare, - "keyboardMove" -> o.keyboardMove, - "voice" -> o.voice, - "zen" -> o.zen, - "ratings" -> o.ratings, - "flairs" -> o.flairs, - "rookCastle" -> o.rookCastle, - "moveEvent" -> o.moveEvent, - "pieceNotation" -> o.pieceNotation, - "resizeHandle" -> o.resizeHandle, - "agreement" -> o.agreement, - "tags" -> o.tags + "_id" -> o._id, + "bg" -> o.bg, + "bgImg" -> o.bgImg, + "is3d" -> o.is3d, + "theme" -> o.theme, + "pieceSet" -> o.pieceSet, + "theme3d" -> o.theme3d, + "pieceSet3d" -> o.pieceSet3d, + "soundSet" -> SoundSet.name2key(o.soundSet), + "autoQueen" -> o.autoQueen, + "autoThreefold" -> o.autoThreefold, + "takeback" -> o.takeback, + "moretime" -> o.moretime, + "clockTenths" -> o.clockTenths, + "clockBar" -> o.clockBar, + "clockSound" -> o.clockSound, + "premove" -> o.premove, + "animation" -> o.animation, + "captured" -> o.captured, + "follow" -> o.follow, + "highlight" -> o.highlight, + "destination" -> o.destination, + "coords" -> o.coords, + "replay" -> o.replay, + "challenge" -> o.challenge, + "message" -> o.message, + "studyInvite" -> o.studyInvite, + "submitMove" -> o.submitMove, + "confirmResign" -> o.confirmResign, + "insightShare" -> o.insightShare, + "keyboardMove" -> o.keyboardMove, + "voice" -> o.voice, + "zen" -> o.zen, + "ratings" -> o.ratings, + "flairs" -> o.flairs, + "rookCastle" -> o.rookCastle, + "moveEvent" -> o.moveEvent, + "pieceNotation" -> o.pieceNotation, + "resizeHandle" -> o.resizeHandle, + "agreement" -> o.agreement, + "usingAltSocket" -> o.usingAltSocket, + "tags" -> o.tags ) diff --git a/ui/site/css/_account.scss b/ui/site/css/_account.scss index e00202f7c4178..e671b35870ea5 100644 --- a/ui/site/css/_account.scss +++ b/ui/site/css/_account.scss @@ -150,7 +150,7 @@ margin-bottom: 4rem; } - form section a { + section a { text-decoration: none; color: inherit; } @@ -238,6 +238,11 @@ } } + .active-soft, + .active-soft:hover { + @extend %active-soft; + } + @include breakpoint($mq-not-xx-small) { td.icon { display: none; diff --git a/ui/site/src/component/socket.ts b/ui/site/src/component/socket.ts index 099beb88201a7..04075313d44c5 100644 --- a/ui/site/src/component/socket.ts +++ b/ui/site/src/component/socket.ts @@ -344,7 +344,7 @@ export default class StrongSocket { }; baseUrl = () => { - if (lichess.storage.get('socket.host')) return lichess.storage.get('socket.host'); // TODO - remove + if (document.body.dataset.altSocket) return document.body.dataset.altSocket; let url = this.storage.get(); if (!url) { url = this.baseUrls[Math.floor(Math.random() * this.baseUrls.length)];