diff --git a/.gitignore b/.gitignore
index f24a2cb18a..a6277425e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ public/vendor/stockfish.wasm
public/vendor/stockfish-mv.wasm
public/vendor/stockfish.pexe
public/vendor/stockfish.js
+public/css/
target
bin/.translate_version
data/
@@ -17,6 +18,7 @@ node_modules/
local/
ui/*/npm-debug.log
hs_*.log
+yarn-error.log
RUNNING_PID
diff --git a/COPYING.md b/COPYING.md
index aac4bc31b8..0c8d2edfae 100644
--- a/COPYING.md
+++ b/COPYING.md
@@ -36,7 +36,7 @@ Exceptions (free)
Files | Author(s) | License
--- | --- | ---
public/font70 | [Dave Gandy](http://fontawesome.io/), [GitHub](https://github.com/primer/octicons), [Webalys](http://www.webalys.com/), [Zurb](http://zurb.com/playground/foundation-icon-fonts-3), [Daniel Bruce](http://www.entypo.com/), [Shapemade](http://steadysets.com/), [Sergey Shmidt](http://designmodo.com/linecons-free/) and the lichess authors | [OFL](http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), [MIT](https://github.com/primer/octicons/blob/master/LICENSE), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/), AGPLv3+
-ChessSansPiratf in public/fonts | the [pgn4web](http://pgn4web.casaschi.net/home.html) authors | [GPLv2+](https://www.gnu.org/licenses/gpl-2.0.txt)
+ChessSansPiratf in public/font | the [pgn4web](http://pgn4web.casaschi.net/home.html) authors | [GPLv2+](https://www.gnu.org/licenses/gpl-2.0.txt)
public/images/staunton | [James Clarke](https://github.com/clarkerubber/Staunton-Pieces) | [MIT](https://github.com/clarkerubber/Staunton-Pieces/blob/master/LICENSE)
public/images/staunton/piece/CubesAndPi | CubesAndPi | AGPLv3+
public/images/trophy | [James Clarke](https://github.com/clarkerubber/Staunton-Pieces/tree/master/Trophies) | [MIT](https://github.com/clarkerubber/Staunton-Pieces/blob/master/LICENSE)
diff --git a/app/actor/Renderer.scala b/app/actor/Renderer.scala
index 6f84d7096b..c2007fce30 100644
--- a/app/actor/Renderer.scala
+++ b/app/actor/Renderer.scala
@@ -2,7 +2,6 @@ package lila.app
package actor
import akka.actor._
-import play.twirl.api.Html
import lila.game.Pov
import views.{ html => V }
@@ -12,10 +11,10 @@ private[app] final class Renderer extends Actor {
def receive = {
case lila.tv.actorApi.RenderFeaturedJs(game) =>
- sender ! V.game.bits.featuredJs(Pov first game).body
+ sender ! V.game.bits.featuredJs(Pov first game).render
case lila.tournament.actorApi.TournamentTable(tours) =>
- sender ! V.tournament.enterable(tours).render
+ sender ! V.tournament.bits.enterable(tours).render
case lila.simul.actorApi.SimulTable(simuls) =>
sender ! V.simul.bits.allCreated(simuls).render
diff --git a/app/controllers/Analyse.scala b/app/controllers/Analyse.scala
index a755c58139..467da71c0d 100644
--- a/app/controllers/Analyse.scala
+++ b/app/controllers/Analyse.scala
@@ -68,16 +68,16 @@ object Analyse extends LilaController {
}
}
- def embed(gameId: String, color: String) = Open { implicit ctx =>
+ def embed(gameId: String, color: String) = Action.async { implicit req =>
GameRepo.gameWithInitialFen(gameId) flatMap {
case Some((game, initialFen)) =>
val pov = Pov(game, chess.Color(color == "white"))
- Env.api.roundApi.review(pov, lila.api.Mobile.Api.currentVersion,
+ Env.api.roundApi.embed(pov, lila.api.Mobile.Api.currentVersion,
initialFenO = initialFen.some,
withFlags = WithFlags(opening = true)) map { data =>
Ok(html.analyse.embed(pov, data))
}
- case _ => fuccess(NotFound(html.analyse.embed.notFound()))
+ case _ => fuccess(NotFound(html.analyse.embed.notFound))
}
}
diff --git a/app/controllers/Api.scala b/app/controllers/Api.scala
index 6532e424f0..0d920b0491 100644
--- a/app/controllers/Api.scala
+++ b/app/controllers/Api.scala
@@ -43,7 +43,7 @@ object Api extends LilaController {
}
def index = Action {
- Ok(views.html.site.api())
+ Ok(views.html.site.bits.api)
}
def user(name: String) = CookieBasedApiRequest { ctx =>
diff --git a/app/controllers/Auth.scala b/app/controllers/Auth.scala
index fab04dfb06..4e232685e8 100644
--- a/app/controllers/Auth.scala
+++ b/app/controllers/Auth.scala
@@ -42,13 +42,12 @@ object Auth extends LilaController {
}
def authenticateUser(u: UserModel, result: Option[String => Result] = None)(implicit ctx: Context): Fu[Result] = {
- implicit val req = ctx.req
if (u.ipBan) fuccess(Redirect(routes.Lobby.home))
else api.saveAuthentication(u.id, ctx.mobileApiVersion) flatMap { sessionId =>
negotiate(
html = fuccess {
val redirectTo = get("referrer").filter(goodReferrer) orElse
- req.session.get(api.AccessUri) getOrElse
+ ctxReq.session.get(api.AccessUri) getOrElse
routes.Lobby.home.url
result.fold(Redirect(redirectTo))(_(redirectTo))
},
@@ -118,21 +117,19 @@ object Auth extends LilaController {
}
def logout = Open { implicit ctx =>
- implicit val req = ctx.req
- req.session get "sessionId" foreach lila.security.Store.delete
+ ctxReq.session get "sessionId" foreach lila.security.Store.delete
negotiate(
- html = Redirect(routes.Main.mobile).fuccess,
+ html = Redirect(routes.Auth.login).fuccess,
api = _ => Ok(Json.obj("ok" -> true)).fuccess
) map (_ withCookies LilaCookie.newSession)
}
// mobile app BC logout with GET
def logoutGet = Open { implicit ctx =>
- implicit val req = ctx.req
negotiate(
html = notFound,
api = _ => {
- req.session get "sessionId" foreach lila.security.Store.delete
+ ctxReq.session get "sessionId" foreach lila.security.Store.delete
Ok(Json.obj("ok" -> true)).withCookies(LilaCookie.newSession).fuccess
}
)
@@ -248,7 +245,7 @@ object Auth extends LilaController {
private def welcome(user: UserModel, email: EmailAddress)(implicit ctx: Context) = {
garbageCollect(user, email)
- env.welcomeEmail(user, email)
+ env.automaticEmail.welcome(user, email)
}
private def garbageCollect(user: UserModel, email: EmailAddress)(implicit ctx: Context) =
@@ -317,7 +314,6 @@ object Auth extends LilaController {
}
private def redirectNewUser(user: UserModel)(implicit ctx: Context) = {
- implicit val req = ctx.req
api.saveAuthentication(user.id, ctx.mobileApiVersion) flatMap { sessionId =>
negotiate(
html = Redirect(routes.User.show(user.username)).fuccess,
@@ -344,7 +340,7 @@ object Auth extends LilaController {
def passwordReset = Open { implicit ctx =>
forms.passwordResetWithCaptcha map {
- case (form, captcha) => Ok(html.auth.passwordReset(form, captcha))
+ case (form, captcha) => Ok(html.auth.bits.passwordReset(form, captcha))
}
}
@@ -352,7 +348,7 @@ object Auth extends LilaController {
implicit val req = ctx.body
forms.passwordReset.bindFromRequest.fold(
err => forms.anyCaptcha map { captcha =>
- BadRequest(html.auth.passwordReset(err, captcha, false.some))
+ BadRequest(html.auth.bits.passwordReset(err, captcha, false.some))
},
data => {
UserRepo.enabledWithEmail(data.realEmail.normalize) flatMap {
@@ -363,7 +359,7 @@ object Auth extends LilaController {
case _ => {
lila.mon.user.auth.passwordResetRequest("no_email")()
forms.passwordResetWithCaptcha map {
- case (form, captcha) => BadRequest(html.auth.passwordReset(form, captcha, false.some))
+ case (form, captcha) => BadRequest(html.auth.bits.passwordReset(form, captcha, false.some))
}
}
}
@@ -373,7 +369,7 @@ object Auth extends LilaController {
def passwordResetSent(email: String) = Open { implicit ctx =>
fuccess {
- Ok(html.auth.passwordResetSent(email))
+ Ok(html.auth.bits.passwordResetSent(email))
}
}
@@ -386,7 +382,7 @@ object Auth extends LilaController {
case Some(user) => {
authLog(user.username, "Reset password")
lila.mon.user.auth.passwordResetConfirm("token_ok")()
- fuccess(html.auth.passwordResetConfirm(user, token, forms.passwdReset, none))
+ fuccess(html.auth.bits.passwordResetConfirm(user, token, forms.passwdReset, none))
}
}
}
@@ -400,7 +396,7 @@ object Auth extends LilaController {
case Some(user) =>
implicit val req = ctx.body
FormFuResult(forms.passwdReset) { err =>
- fuccess(html.auth.passwordResetConfirm(user, token, err, false.some))
+ fuccess(html.auth.bits.passwordResetConfirm(user, token, err, false.some))
} { data =>
HasherRateLimit(user.username, ctx.req) { _ =>
Env.user.authenticator.setPassword(user.id, ClearPassword(data.newPasswd1)) >>
diff --git a/app/controllers/Blog.scala b/app/controllers/Blog.scala
index 43f4a719a7..7f8aae4ee5 100644
--- a/app/controllers/Blog.scala
+++ b/app/controllers/Blog.scala
@@ -49,7 +49,7 @@ object Blog extends LilaController {
blogApi context req flatMap { implicit prismic =>
blogApi.recent(prismic.api, none, 1, lila.common.MaxPerPage(50)) map {
_ ?? { docs =>
- Ok(views.xml.blog.atom(docs)) as XML
+ Ok(views.html.blog.atom(docs)) as XML
}
}
}
diff --git a/app/controllers/Challenge.scala b/app/controllers/Challenge.scala
index f8f9203835..de10958718 100644
--- a/app/controllers/Challenge.scala
+++ b/app/controllers/Challenge.scala
@@ -41,13 +41,16 @@ object Challenge extends LilaController {
else none
val json = env.jsonView.show(c, version, direction)
negotiate(
- html = fuccess {
- if (mine) error match {
- case Some(e) => BadRequest(html.challenge.mine.apply(c, json, e.some))
- case None => Ok(html.challenge.mine.apply(c, json, none))
+ html =
+ if (mine) fuccess {
+ error match {
+ case Some(e) => BadRequest(html.challenge.mine(c, json, e.some))
+ case None => Ok(html.challenge.mine(c, json, none))
+ }
}
- else Ok(html.challenge.theirs.apply(c, json))
- },
+ else (c.challengerUserId ?? UserRepo.named) map { user =>
+ Ok(html.challenge.theirs(c, json, user))
+ },
api = _ => Ok(json).fuccess
) flatMap withChallengeAnonCookie(mine && c.challengerIsAnon, c, true)
}
@@ -90,7 +93,6 @@ object Challenge extends LilaController {
cond ?? {
GameRepo.game(c.id).map {
_ map { game =>
- implicit val req = ctx.req
LilaCookie.cookie(
AnonCookie.name,
game.player(if (owner) c.finalColor else !c.finalColor).id,
diff --git a/app/controllers/Coordinate.scala b/app/controllers/Coordinate.scala
index 824b425636..a017af1497 100644
--- a/app/controllers/Coordinate.scala
+++ b/app/controllers/Coordinate.scala
@@ -30,8 +30,7 @@ object Coordinate extends LilaController {
err => fuccess(BadRequest),
value => Env.pref.api.setPref(
me,
- (p: lila.pref.Pref) => p.copy(coordColor = value),
- notifyChange = false
+ (p: lila.pref.Pref) => p.copy(coordColor = value)
) inject Ok(())
)
}
diff --git a/app/controllers/Dasher.scala b/app/controllers/Dasher.scala
index ee8ad3cfc3..95e199cd83 100644
--- a/app/controllers/Dasher.scala
+++ b/app/controllers/Dasher.scala
@@ -70,8 +70,7 @@ object Dasher extends LilaController {
"image" -> ctx.pref.bgImgOrDefault
),
"board" -> Json.obj(
- "is3d" -> ctx.pref.is3d,
- "zoom" -> ctx.zoom
+ "is3d" -> ctx.pref.is3d
),
"theme" -> Json.obj(
"d2" -> Json.obj(
diff --git a/app/controllers/Editor.scala b/app/controllers/Editor.scala
index 0ebcb357fe..6418f66fb0 100644
--- a/app/controllers/Editor.scala
+++ b/app/controllers/Editor.scala
@@ -3,7 +3,6 @@ package controllers
import chess.format.Forsyth
import chess.Situation
import play.api.libs.json._
-import play.twirl.api.Html
import lila.app._
import lila.game.GameRepo
diff --git a/app/controllers/Event.scala b/app/controllers/Event.scala
index 2546d17151..fd1ebb9ebd 100644
--- a/app/controllers/Event.scala
+++ b/app/controllers/Event.scala
@@ -7,12 +7,6 @@ object Event extends LilaController {
private def api = Env.event.api
- def index = Open { implicit ctx =>
- api.recentEnabled.map { events =>
- Ok(html.event.index(events))
- }
- }
-
def show(id: String) = Open { implicit ctx =>
OptionOk(api oneEnabled id) { event =>
html.event.show(event)
diff --git a/app/controllers/ForumTopic.scala b/app/controllers/ForumTopic.scala
index fd8823c448..5cd2868671 100644
--- a/app/controllers/ForumTopic.scala
+++ b/app/controllers/ForumTopic.scala
@@ -46,7 +46,7 @@ object ForumTopic extends LilaController with ForumController {
case (categ, topic, posts) => for {
unsub <- ctx.userId ?? Env.timeline.status(s"forum:${topic.id}")
canWrite <- isGrantedWrite(categSlug)
- form <- (!posts.hasNextPage && canWrite && topic.open) ?? forms.postWithCaptcha.map(_.some)
+ form <- (!posts.hasNextPage && canWrite && topic.open && !topic.isOld) ?? forms.postWithCaptcha.map(_.some)
canModCateg <- isGrantedMod(categ.slug)
_ <- Env.user.lightUserApi preloadMany posts.currentPageResults.flatMap(_.userId)
} yield html.forum.topic.show(categ, topic, posts, form, unsub, canModCateg = canModCateg)
diff --git a/app/controllers/LilaController.scala b/app/controllers/LilaController.scala
index 2bfb86af4a..8fb2e23f16 100644
--- a/app/controllers/LilaController.scala
+++ b/app/controllers/LilaController.scala
@@ -6,8 +6,7 @@ import play.api.http._
import play.api.libs.json.{ Json, JsObject, JsArray, JsString, Writes }
import play.api.mvc._
import play.api.mvc.BodyParsers.parse
-import play.twirl.api.Html
-import scalatags.Text.{ TypedTag, Frag }
+import scalatags.Text.Frag
import lila.api.{ PageData, Context, HeaderContext, BodyContext }
import lila.app._
@@ -28,22 +27,11 @@ private[controllers] trait LilaController
protected implicit val LilaResultZero = Zero.instance[Result](Results.NotFound)
- protected implicit val LilaHtmlMonoid = lila.app.templating.Environment.LilaHtmlMonoid
-
protected implicit final class LilaPimpedResult(result: Result) {
def fuccess = scala.concurrent.Future successful result
}
- protected implicit def LilaHtmlToResult(content: Html): Result = Ok(content)
-
- protected implicit def contentTypeOfFrag(implicit codec: Codec): ContentTypeOf[Frag] =
- ContentTypeOf[Frag](Some(ContentTypes.HTML))
- protected implicit def writeableOfFrag(implicit codec: Codec): Writeable[Frag] =
- Writeable(frag => codec.encode(frag.render))
-
- protected implicit def LilaScalatagsToHtml(tags: scalatags.Text.TypedTag[String]): Html = Html(tags.render)
-
- protected implicit def LilaFragToResult(content: Frag): Result = Ok(content)
+ protected implicit def LilaFragToResult(frag: Frag): Result = Ok(frag)
protected implicit def makeApiVersion(v: Int) = ApiVersion(v)
@@ -56,7 +44,9 @@ private[controllers] trait LilaController
api = _ => fuccess(jsonOkResult)
)
- implicit def lang(implicit ctx: Context) = ctx.lang
+ implicit def ctxLang(implicit ctx: Context) = ctx.lang
+ implicit def ctxReq(implicit ctx: Context) = ctx.req
+ implicit def reqConfig(implicit req: RequestHeader) = ui.EmbedConfig(req)
protected def NoCache(res: Result): Result = res.withHeaders(
CACHE_CONTROL -> "no-cache, no-store, must-revalidate", EXPIRES -> "0"
@@ -213,7 +203,7 @@ private[controllers] trait LilaController
protected def NoTor(res: => Fu[Result])(implicit ctx: Context) =
if (Env.security.tor isExitNode HTTPRequest.lastRemoteAddress(ctx.req))
- Unauthorized(views.html.auth.tor()).fuccess
+ Unauthorized(views.html.auth.bits.tor()).fuccess
else res
protected def NoEngine[A <: Result](a: => Fu[A])(implicit ctx: Context): Fu[Result] =
@@ -347,8 +337,7 @@ private[controllers] trait LilaController
protected def authenticationFailed(implicit ctx: Context): Fu[Result] =
negotiate(
html = fuccess {
- implicit val req = ctx.req
- Redirect(routes.Auth.signup) withCookies LilaCookie.session(Env.security.api.AccessUri, req.uri)
+ Redirect(routes.Auth.signup) withCookies LilaCookie.session(Env.security.api.AccessUri, ctx.req.uri)
},
api = _ => ensureSessionId(ctx.req) {
Unauthorized(jsonError("Login required"))
@@ -361,7 +350,7 @@ private[controllers] trait LilaController
html =
if (HTTPRequest isSynchronousHttp ctx.req) fuccess {
lila.mon.http.response.code403()
- Forbidden(views.html.base.authFailed())
+ Forbidden(views.html.site.message.authFailed)
}
else fuccess(Results.Forbidden("Authorization failed")),
api = _ => fuccess(forbiddenJsonResult)
@@ -371,8 +360,8 @@ private[controllers] trait LilaController
if (req.session.data.contains(LilaCookie.sessionId)) res
else res withCookies LilaCookie.makeSessionId(req)
- protected def negotiate(html: => Fu[Result], api: ApiVersion => Fu[Result])(implicit ctx: Context): Fu[Result] =
- lila.api.Mobile.Api.requestVersion(ctx.req).fold(html) { v =>
+ protected def negotiate(html: => Fu[Result], api: ApiVersion => Fu[Result])(implicit req: RequestHeader): Fu[Result] =
+ lila.api.Mobile.Api.requestVersion(req).fold(html) { v =>
api(v) dmap (_ as JSON)
}.dmap(_.withHeaders("Vary" -> "Accept"))
@@ -430,7 +419,9 @@ private[controllers] trait LilaController
}
} dmap {
case Some(d) if !lila.common.PlayApp.isProd =>
- Some(d.copy(user = d.user.addRole(lila.security.Permission.Beta.name)))
+ d.copy(user = d.user
+ .addRole(lila.security.Permission.Beta.name)
+ .addRole(lila.security.Permission.Prismic.name)).some
case d => d
} flatMap {
case None => fuccess(None -> None)
diff --git a/app/controllers/Main.scala b/app/controllers/Main.scala
index 7e483b3b8a..5e9a8294e9 100644
--- a/app/controllers/Main.scala
+++ b/app/controllers/Main.scala
@@ -69,7 +69,7 @@ object Main extends LilaController {
def mobile = Open { implicit ctx =>
pageHit
OptionOk(Prismic getBookmark "mobile-apk") {
- case (doc, resolver) => html.mobile.home(doc, resolver)
+ case (doc, resolver) => html.mobile(doc, resolver)
}
}
@@ -135,10 +135,6 @@ Disallow: /games/export
}
}
- val freeJs = Open { implicit ctx =>
- Ok(html.site.freeJs(ctx)).fuccess
- }
-
def renderNotFound(req: RequestHeader): Fu[Result] =
reqToCtx(req) map renderNotFound
@@ -147,12 +143,8 @@ Disallow: /games/export
NotFound(html.base.notFound()(ctx))
}
- def fpmenu = Open { implicit ctx =>
- Ok(html.base.fpmenu()).fuccess
- }
-
def getFishnet = Open { implicit ctx =>
- Ok(html.site.getFishnet()).fuccess
+ Ok(html.site.bits.getFishnet()).fuccess
}
def costs = Open { implicit ctx =>
@@ -181,6 +173,7 @@ Disallow: /games/export
case 87 => routes.Stat.ratingDistribution("blitz").url
case 110 => s"$faq#name"
case 29 => s"$faq#titles"
+ case 4811 => s"$faq#lm"
case 216 => routes.Main.mobile.url
case 340 => s"$faq#trophies"
case 6 => s"$faq#ratings"
diff --git a/app/controllers/Message.scala b/app/controllers/Message.scala
index 0601d4db27..5f82b237d7 100644
--- a/app/controllers/Message.scala
+++ b/app/controllers/Message.scala
@@ -3,8 +3,8 @@ package controllers
import play.api.data.Form
import play.api.libs.json._
import play.api.mvc.Result
-import play.twirl.api.Html
import scala.concurrent.duration._
+import scalatags.Text.Frag
import lila.api.Context
import lila.app._
@@ -47,7 +47,7 @@ object Message extends LilaController {
negotiate(
html = OptionFuOk(api.thread(id, me)) { thread =>
relationApi.fetchBlocks(thread otherUserId me, me.id) map { blocked =>
- val form = !thread.isTooBig option forms.post
+ val form = thread.isReplyable option forms.post
html.message.thread(thread, form, blocked)
}
} map NoCache,
@@ -131,7 +131,7 @@ object Message extends LilaController {
}
}
- private def renderForm(me: UserModel, title: Option[String], f: Form[_] => Form[_])(implicit ctx: Context): Fu[Html] =
+ private def renderForm(me: UserModel, title: Option[String], f: Form[_] => Form[_])(implicit ctx: Context): Fu[Frag] =
get("user") ?? UserRepo.named flatMap { user =>
user.fold(fuTrue)(u => security.canMessage(me.id, u.id)) map { canMessage =>
html.message.form(
diff --git a/app/controllers/Mod.scala b/app/controllers/Mod.scala
index 620c3ad769..cabee1af2e 100644
--- a/app/controllers/Mod.scala
+++ b/app/controllers/Mod.scala
@@ -7,6 +7,7 @@ import lila.common.{ IpAddress, EmailAddress, HTTPRequest }
import lila.report.{ Suspect, Mod => AsMod, SuspectId }
import lila.security.Permission
import lila.user.{ UserRepo, User => UserModel, Title }
+import lila.mod.UserSearch
import ornicar.scalalib.Zero
import views._
@@ -236,13 +237,18 @@ object Mod extends LilaController {
def search = SecureBody(_.UserSearch) { implicit ctx => me =>
implicit def req = ctx.body
- val f = lila.mod.UserSearch.form
+ val f = UserSearch.form
f.bindFromRequest.fold(
err => BadRequest(html.mod.search(err, Nil)).fuccess,
query => Env.mod.search(query) map { html.mod.search(f.fill(query), _) }
)
}
+ protected[controllers] def searchTerm(q: String)(implicit ctx: Context) = {
+ val query = UserSearch exact q
+ Env.mod.search(query) map { users => Ok(html.mod.search(UserSearch.form fill query, users)) }
+ }
+
def chatUser(username: String) = Secure(_.ChatTimeout) { implicit ctx => me =>
implicit val lightUser = Env.user.lightUserSync _
JsonOptionOk {
@@ -267,7 +273,7 @@ object Mod extends LilaController {
)).bindFromRequest.fold(
err => BadRequest(html.mod.permissions(user)).fuccess,
permissions =>
- modApi.setPermissions(me.id, user.username, Permission(permissions)) >> {
+ modApi.setPermissions(AsMod(me), user.username, Permission(permissions)) >> {
(Permission(permissions) diff Permission(user.roles) contains Permission.Coach) ??
Env.security.automaticEmail.onBecomeCoach(user)
} >> {
@@ -285,7 +291,7 @@ object Mod extends LilaController {
val email = query.headOption.map(EmailAddress.apply) flatMap Env.security.emailAddressValidator.validate
val username = query lift 1
def tryWith(setEmail: EmailAddress, q: String): Fu[Option[Result]] =
- Env.mod.search(lila.mod.UserSearch.exact(q)) flatMap {
+ Env.mod.search(UserSearch.exact(q)) flatMap {
case List(UserModel.WithEmails(user, _)) => (!user.everLoggedIn).?? {
lila.mon.user.register.modConfirmEmail()
modApi.setEmail(me.id, user.id, setEmail)
diff --git a/app/controllers/OAuthApp.scala b/app/controllers/OAuthApp.scala
index edddfa537c..ba2ebfb54f 100644
--- a/app/controllers/OAuthApp.scala
+++ b/app/controllers/OAuthApp.scala
@@ -19,13 +19,13 @@ object OAuthApp extends LilaController {
}
def create = Auth { implicit ctx => me =>
- Ok(html.oAuth.app.create(env.forms.app.create)).fuccess
+ Ok(html.oAuth.app.form.create(env.forms.app.create)).fuccess
}
def createApply = AuthBody { implicit ctx => me =>
implicit val req = ctx.body
env.forms.app.create.bindFromRequest.fold(
- err => BadRequest(html.oAuth.app.create(err)).fuccess,
+ err => BadRequest(html.oAuth.app.form.create(err)).fuccess,
setup => {
val app = setup make me
env.appApi.create(app) inject Redirect(routes.OAuthApp.edit(app.clientId.value))
@@ -35,7 +35,7 @@ object OAuthApp extends LilaController {
def edit(id: String) = Auth { implicit ctx => me =>
OptionFuResult(env.appApi.findBy(App.Id(id), me)) { app =>
- Ok(html.oAuth.app.edit(app, env.forms.app.edit(app))).fuccess
+ Ok(html.oAuth.app.form.edit(app, env.forms.app.edit(app))).fuccess
}
}
@@ -43,7 +43,7 @@ object OAuthApp extends LilaController {
OptionFuResult(env.appApi.findBy(App.Id(id), me)) { app =>
implicit val req = ctx.body
env.forms.app.edit(app).bindFromRequest.fold(
- err => BadRequest(html.oAuth.app.edit(app, err)).fuccess,
+ err => BadRequest(html.oAuth.app.form.edit(app, err)).fuccess,
data => env.appApi.update(app) { data.update(_) } map { r => Redirect(routes.OAuthApp.edit(app.clientId.value)) }
)
}
diff --git a/app/controllers/Page.scala b/app/controllers/Page.scala
index 48611812b1..d01cd3c831 100644
--- a/app/controllers/Page.scala
+++ b/app/controllers/Page.scala
@@ -28,6 +28,13 @@ object Page extends LilaController {
}
}
+ def source = Open { implicit ctx =>
+ pageHit
+ OptionOk(Prismic getBookmark "source") {
+ case (doc, resolver) => views.html.site.help.source(doc, resolver)
+ }
+ }
+
def swag = Open { implicit ctx =>
pageHit
OptionOk(Prismic getBookmark "swag") {
diff --git a/app/controllers/Pref.scala b/app/controllers/Pref.scala
index 44e2685043..210f7fc88b 100644
--- a/app/controllers/Pref.scala
+++ b/app/controllers/Pref.scala
@@ -31,8 +31,7 @@ object Pref extends LilaController {
}
def formApply = AuthBody { implicit ctx => me =>
- def onSuccess(data: lila.pref.DataForm.PrefData) =
- api.setPref(data(ctx.pref), notifyChange = true) inject Ok("saved")
+ def onSuccess(data: lila.pref.DataForm.PrefData) = api.setPref(data(ctx.pref)) inject Ok("saved")
implicit val req = ctx.body
forms.pref.bindFromRequest.fold(
err => forms.pref.bindFromRequest(lila.pref.FormCompatLayer(ctx.body)).fold(
@@ -43,16 +42,16 @@ object Pref extends LilaController {
)
}
- def setZoom = Action { implicit req =>
- val zoom = getInt("v", req) | 100
- Ok(()).withCookies(LilaCookie.session("zoom", zoom.toString))
- }
-
def set(name: String) = OpenBody { implicit ctx =>
implicit val req = ctx.body
- (setters get name) ?? {
- case (form, fn) => FormResult(form) { v =>
- fn(v, ctx) map { cookie => Ok(()).withCookies(cookie) }
+ if (name == "zoom") {
+ Ok.withCookies(LilaCookie.session("zoom", (getInt("v") | 180).toString)).fuccess
+ } else {
+ implicit val req = ctx.body
+ (setters get name) ?? {
+ case (form, fn) => FormResult(form) { v =>
+ fn(v, ctx) map { cookie => Ok(()).withCookies(cookie) }
+ }
}
}
}
@@ -75,6 +74,6 @@ object Pref extends LilaController {
private def save(name: String)(value: String, ctx: Context): Fu[Cookie] =
ctx.me ?? {
- api.setPrefString(_, name, value, notifyChange = false)
+ api.setPrefString(_, name, value)
} inject LilaCookie.session(name, value)(ctx.req)
}
diff --git a/app/controllers/Puzzle.scala b/app/controllers/Puzzle.scala
index 39a083368e..2a53e88ec5 100644
--- a/app/controllers/Puzzle.scala
+++ b/app/controllers/Puzzle.scala
@@ -5,6 +5,7 @@ import play.api.mvc._
import lila.api.Context
import lila.app._
+import lila.common.{ HTTPRequest, IpAddress, MaxPerSecond }
import lila.game.PgnDump
import lila.puzzle.{ PuzzleId, Result, Puzzle => PuzzleModel, UserInfos }
import lila.user.UserRepo
@@ -215,18 +216,37 @@ object Puzzle extends LilaController {
)
}
+ /* For BC */
def embed = Action { req =>
Ok {
val bg = get("bg", req) | "light"
val theme = get("theme", req) | "brown"
val url = s"""${req.domain + routes.Puzzle.frame}?bg=$bg&theme=$theme"""
- s"""document.write("");"""
+ s"""document.write("");"""
} as JAVASCRIPT withHeaders (CACHE_CONTROL -> "max-age=86400")
}
- def frame = Open { implicit ctx =>
- OptionOk(env.daily.get) { daily =>
- html.puzzle.embed(daily)
+ def frame = Action.async { implicit req =>
+ env.daily.get map {
+ case None => NotFound
+ case Some(daily) => html.puzzle.embed(daily)
}
}
+
+ def activity = Scoped(_.Puzzle.Read) { req => me =>
+ Api.GlobalLinearLimitPerIP(HTTPRequest lastRemoteAddress req) {
+ Api.GlobalLinearLimitPerUserOption(me.some) {
+ val config = lila.puzzle.PuzzleActivity.Config(
+ user = me,
+ max = getInt("max", req) map (_ atLeast 1),
+ perSecond = MaxPerSecond(20)
+ )
+ Ok.chunked(env.activity.stream(config)).withHeaders(
+ noProxyBufferHeader,
+ CONTENT_TYPE -> ndJsonContentType
+ ).fuccess
+ }
+ }
+ }
+
}
diff --git a/app/controllers/Relation.scala b/app/controllers/Relation.scala
index 8d3ed5f9da..a1adb55015 100644
--- a/app/controllers/Relation.scala
+++ b/app/controllers/Relation.scala
@@ -35,7 +35,13 @@ object Relation extends LilaController {
}
def follow(userId: String) = Auth { implicit ctx => me =>
- env.api.follow(me.id, UserModel normalize userId).nevermind >> renderActions(userId, getBool("mini"))
+ env.api.reachedMaxFollowing(me.id) flatMap {
+ case true => Env.message.api.sendPresetFromLichess(
+ me,
+ lila.message.ModPreset.maxFollow(me.username, Env.relation.MaxFollow)
+ ).void
+ case _ => env.api.follow(me.id, UserModel normalize userId).nevermind >> renderActions(userId, getBool("mini"))
+ }
}
def unfollow(userId: String) = Auth { implicit ctx => me =>
@@ -56,7 +62,7 @@ object Relation extends LilaController {
RelatedPager(env.api.followingPaginatorAdapter(user.id), page) flatMap { pag =>
negotiate(
html = env.api countFollowers user.id map { nbFollowers =>
- Ok(html.relation.following(user, pag, nbFollowers))
+ Ok(html.relation.bits.following(user, pag, nbFollowers))
},
api = _ => Ok(jsonRelatedPaginator(pag)).fuccess
)
@@ -71,7 +77,7 @@ object Relation extends LilaController {
RelatedPager(env.api.followersPaginatorAdapter(user.id), page) flatMap { pag =>
negotiate(
html = env.api countFollowing user.id map { nbFollowing =>
- Ok(html.relation.followers(user, pag, nbFollowing))
+ Ok(html.relation.bits.followers(user, pag, nbFollowing))
},
api = _ => Ok(jsonRelatedPaginator(pag)).fuccess
)
@@ -112,7 +118,7 @@ object Relation extends LilaController {
def blocks(page: Int) = Auth { implicit ctx => me =>
Reasonable(page, 20) {
RelatedPager(env.api.blockingPaginatorAdapter(me.id), page) map { pag =>
- html.relation.blocks(me, pag)
+ html.relation.bits.blocks(me, pag)
}
}
}
diff --git a/app/controllers/Relay.scala b/app/controllers/Relay.scala
index 64fce59101..a214ce107c 100644
--- a/app/controllers/Relay.scala
+++ b/app/controllers/Relay.scala
@@ -23,14 +23,14 @@ object Relay extends LilaController {
def form = Auth { implicit ctx => me =>
NoLame {
- Ok(html.relay.create(env.forms.create)).fuccess
+ Ok(html.relay.form.create(env.forms.create)).fuccess
}
}
def create = AuthBody { implicit ctx => me =>
implicit val req = ctx.body
env.forms.create.bindFromRequest.fold(
- err => BadRequest(html.relay.create(err)).fuccess,
+ err => BadRequest(html.relay.form.create(err)).fuccess,
setup => env.api.create(setup, me) map { relay =>
Redirect(showRoute(relay))
}
@@ -39,7 +39,7 @@ object Relay extends LilaController {
def edit(slug: String, id: String) = Auth { implicit ctx => me =>
OptionFuResult(env.api.byIdAndContributor(id, me)) { relay =>
- Ok(html.relay.edit(relay, env.forms.edit(relay))).fuccess
+ Ok(html.relay.form.edit(relay, env.forms.edit(relay))).fuccess
}
}
@@ -47,7 +47,7 @@ object Relay extends LilaController {
OptionFuResult(env.api.byIdAndContributor(id, me)) { relay =>
implicit val req = ctx.body
env.forms.edit(relay).bindFromRequest.fold(
- err => BadRequest(html.relay.edit(relay, err)).fuccess,
+ err => BadRequest(html.relay.form.edit(relay, err)).fuccess,
data => env.api.update(relay) { data.update(_, me) } map { r =>
Redirect(showRoute(r))
}
diff --git a/app/controllers/Round.scala b/app/controllers/Round.scala
index 6fcf23d0b5..186588f6db 100644
--- a/app/controllers/Round.scala
+++ b/app/controllers/Round.scala
@@ -73,7 +73,7 @@ object Round extends LilaController with TheftPrevention {
simul foreach Env.simul.api.onPlayerConnection(pov.game, ctx.me)
Ok(html.round.player(pov, data,
tour = tour,
- simul = simul.filter(_ isHost ctx.me),
+ simul = simul,
cross = crosstable,
playing = playing,
chatOption = chatOption,
@@ -192,7 +192,7 @@ object Round extends LilaController with TheftPrevention {
else for { // web crawlers don't need the full thing
initialFen <- GameRepo.initialFen(pov.gameId)
pgn <- Env.api.pgnDump(pov.game, initialFen, none, PgnDump.WithFlags(clocks = false))
- } yield Ok(html.round.watcherBot(pov, initialFen, pgn))
+ } yield Ok(html.round.watcher.crawler(pov, initialFen, pgn))
}.mon(_.http.response.watcher.website),
api = apiVersion => for {
data <- Env.api.roundApi.watcher(pov, apiVersion, tv = none)
@@ -210,7 +210,7 @@ object Round extends LilaController with TheftPrevention {
tourId ?? { Env.tournament.api.miniView(_, withTop) }
private[controllers] def getWatcherChat(game: GameModel)(implicit ctx: Context): Fu[Option[lila.chat.UserChat.Mine]] = {
- ctx.noKid && ctx.me.exists(Env.chat.panic.allowed) && {
+ ctx.noKid && ctx.me.fold(true)(Env.chat.panic.allowed) && {
game.finishedOrAborted || !ctx.userId.exists(game.userIds.contains)
}
} ?? {
@@ -222,7 +222,7 @@ object Round extends LilaController with TheftPrevention {
private[controllers] def getPlayerChat(game: GameModel, tour: Option[Tour])(implicit ctx: Context): Fu[Option[Chat.GameOrEvent]] = ctx.noKid ?? {
(game.tournamentId, game.simulId) match {
case (Some(tid), _) => {
- ctx.isAuth && tour.fold(true)(Tournament.canHaveChat)
+ ctx.isAuth && tour.fold(true)(Tournament.canHaveChat(_, none))
} ??
Env.chat.api.userChat.cached.findMine(Chat.Id(tid), ctx.me).map { chat =>
Chat.GameOrEvent(Right(chat truncate 50)).some
@@ -294,14 +294,10 @@ object Round extends LilaController with TheftPrevention {
}
def mini(gameId: String, color: String) = Open { implicit ctx =>
- OptionOk(GameRepo.pov(gameId, color)) { pov =>
- html.game.bits.mini(pov)
- }
+ OptionOk(GameRepo.pov(gameId, color))(html.game.bits.mini)
}
def miniFullId(fullId: String) = Open { implicit ctx =>
- OptionOk(GameRepo pov fullId) { pov =>
- html.game.bits.mini(pov)
- }
+ OptionOk(GameRepo pov fullId)(html.game.bits.mini)
}
}
diff --git a/app/controllers/Setup.scala b/app/controllers/Setup.scala
index 15473d9e66..734e0a3640 100644
--- a/app/controllers/Setup.scala
+++ b/app/controllers/Setup.scala
@@ -218,7 +218,6 @@ object Setup extends LilaController with TheftPrevention {
}
private[controllers] def redirectPov(pov: Pov)(implicit ctx: Context) = {
- implicit val req = ctx.req
val redir = Redirect(routes.Round.watcher(pov.gameId, "white"))
if (ctx.isAuth) redir
else redir withCookies LilaCookie.cookie(
diff --git a/app/controllers/Simul.scala b/app/controllers/Simul.scala
index 50355e99e3..82e7d5616a 100644
--- a/app/controllers/Simul.scala
+++ b/app/controllers/Simul.scala
@@ -5,9 +5,9 @@ import play.api.mvc._
import lila.api.Context
import lila.app._
+import lila.chat.Chat
import lila.common.HTTPRequest
import lila.simul.{ Simul => Sim }
-import lila.chat.Chat
import views._
object Simul extends LilaController {
@@ -48,36 +48,50 @@ object Simul extends LilaController {
} map NoCache
}
- private[controllers] def canHaveChat(implicit ctx: Context): Boolean = ctx.me ?? { u =>
- if (ctx.kid) false
- else Env.chat.panic allowed u
- }
+ private[controllers] def canHaveChat(implicit ctx: Context): Boolean =
+ !ctx.kid && // no public chats for kids
+ ctx.me.fold(true) { // anon can see public chats
+ Env.chat.panic.allowed
+ }
def start(simulId: String) = Open { implicit ctx =>
AsHost(simulId) { simul =>
env.api start simul.id
- Ok(Json.obj("ok" -> true)) as JSON
+ jsonOkResult
}
}
def abort(simulId: String) = Open { implicit ctx =>
AsHost(simulId) { simul =>
env.api abort simul.id
- Ok(Json.obj("ok" -> true)) as JSON
+ jsonOkResult
}
}
def accept(simulId: String, userId: String) = Open { implicit ctx =>
AsHost(simulId) { simul =>
env.api.accept(simul.id, userId, true)
- Ok(Json.obj("ok" -> true)) as JSON
+ jsonOkResult
}
}
def reject(simulId: String, userId: String) = Open { implicit ctx =>
AsHost(simulId) { simul =>
env.api.accept(simul.id, userId, false)
- Ok(Json.obj("ok" -> true)) as JSON
+ jsonOkResult
+ }
+ }
+
+ def setText(simulId: String) = OpenBody { implicit ctx =>
+ AsHost(simulId) { simul =>
+ implicit val req = ctx.body
+ env.forms.setText.bindFromRequest.fold(
+ err => BadRequest,
+ text => {
+ env.api.setText(simul.id, text)
+ jsonOkResult
+ }
+ )
}
}
diff --git a/app/controllers/Streamer.scala b/app/controllers/Streamer.scala
index aaaebbb9e3..2a1238af87 100644
--- a/app/controllers/Streamer.scala
+++ b/app/controllers/Streamer.scala
@@ -47,7 +47,7 @@ object Streamer extends LilaController {
def create = AuthBody { implicit ctx => me =>
NoLame {
NoShadowban {
- api.find(me) flatMap {
+ api find me flatMap {
case None => api.create(me) inject Redirect(routes.Streamer.edit)
case _ => Redirect(routes.Streamer.edit).fuccess
}
@@ -120,7 +120,7 @@ object Streamer extends LilaController {
private def AsStreamer(f: StreamerModel.WithUser => Fu[Result])(implicit ctx: Context) =
ctx.me.fold(notFound) { me =>
api.find(get("u").ifTrue(isGranted(_.Streamers)) | me.id) flatMap {
- _.fold(Ok(html.streamer.create(me)).fuccess)(f)
+ _.fold(Ok(html.streamer.bits.create(me)).fuccess)(f)
}
}
diff --git a/app/controllers/Study.scala b/app/controllers/Study.scala
index f56174b954..353e0dbe0b 100644
--- a/app/controllers/Study.scala
+++ b/app/controllers/Study.scala
@@ -32,7 +32,7 @@ object Study extends LilaController {
}
else Env.studySearch(ctx.me)(text, page) flatMap { pag =>
negotiate(
- html = Ok(html.study.search(pag, text)).fuccess,
+ html = Ok(html.study.list.search(pag, text)).fuccess,
api = _ => apiStudies(pag)
)
}
@@ -209,10 +209,12 @@ object Study extends LilaController {
}
}
- private[controllers] def chatOf(study: lila.study.Study)(implicit ctx: Context) =
- (ctx.noKid && ctx.me.exists { me =>
- study.isMember(me.id) || Env.chat.panic.allowed(me)
- }) ?? Env.chat.api.userChat.findMine(Chat.Id(study.id.value), ctx.me).map(some)
+ private[controllers] def chatOf(study: lila.study.Study)(implicit ctx: Context) = {
+ !ctx.kid && // no public chats for kids
+ ctx.me.fold(true) { // anon can see public chats
+ Env.chat.panic.allowed
+ }
+ } ?? Env.chat.api.userChat.findMine(Chat.Id(study.id.value), ctx.me).map(some)
def websocket(id: String, apiVersion: Int) = SocketOption[JsValue] { implicit ctx =>
get("sri") ?? { uid =>
@@ -278,17 +280,17 @@ object Study extends LilaController {
)
}
- def embed(id: String, chapterId: String) = Open { implicit ctx =>
- env.api.byIdWithChapter(id, chapterId) flatMap {
+ def embed(id: String, chapterId: String) = Action.async { implicit req =>
+ env.api.byIdWithChapter(id, chapterId).map(_.filterNot(_.study.isPrivate)) flatMap {
_.fold(embedNotFound) {
- case WithChapter(study, chapter) => CanViewResult(study) {
+ case WithChapter(study, chapter) =>
env.jsonView(study.copy(
members = lila.study.StudyMembers(Map.empty) // don't need no members
- ), List(chapter.metadata), chapter, ctx.me) flatMap { studyJson =>
+ ), List(chapter.metadata), chapter, none) flatMap { studyJson =>
val setup = chapter.setup
val initialFen = chapter.root.fen.some
val pov = UserAnalysis.makePov(initialFen, setup.variant)
- val baseData = Env.round.jsonView.userAnalysisJson(pov, ctx.pref, initialFen, setup.orientation, owner = false, me = ctx.me)
+ val baseData = Env.round.jsonView.userAnalysisJson(pov, lila.pref.Pref.default, initialFen, setup.orientation, owner = false, me = none)
val analysis = baseData ++ Json.obj(
"treeParts" -> partitionTreeJsonWriter.writes {
lila.study.TreeBuilder.makeRoot(chapter.root, setup.variant)
@@ -303,13 +305,12 @@ object Study extends LilaController {
api = _ => Ok(Json.obj("study" -> data.study, "analysis" -> data.analysis)).fuccess
)
}
- }
}
} map NoCache
}
- private def embedNotFound(implicit ctx: Context): Fu[Result] =
- fuccess(NotFound(html.study.embed.notFound()))
+ private def embedNotFound(implicit req: RequestHeader): Fu[Result] =
+ fuccess(NotFound(html.study.embed.notFound))
def cloneStudy(id: String) = Auth { implicit ctx => me =>
OptionFuResult(env.api.byId(id)) { study =>
diff --git a/app/controllers/Team.scala b/app/controllers/Team.scala
index 4b7d672709..503323b032 100644
--- a/app/controllers/Team.scala
+++ b/app/controllers/Team.scala
@@ -19,7 +19,7 @@ object Team extends LilaController {
def all(page: Int) = Open { implicit ctx =>
NotForKids {
- paginator popularTeams page map { html.team.all(_) }
+ paginator popularTeams page map { html.team.list.all(_) }
}
}
@@ -40,8 +40,8 @@ object Team extends LilaController {
def search(text: String, page: Int) = OpenBody { implicit ctx =>
NotForKids {
- if (text.trim.isEmpty) paginator popularTeams page map { html.team.all(_) }
- else Env.teamSearch(text, page) map { html.team.search(text, _) }
+ if (text.trim.isEmpty) paginator popularTeams page map { html.team.list.all(_) }
+ else Env.teamSearch(text, page) map { html.team.list.search(text, _) }
}
}
@@ -68,7 +68,7 @@ object Team extends LilaController {
def edit(id: String) = Auth { implicit ctx => me =>
OptionFuResult(api team id) { team =>
- Owner(team) { fuccess(html.team.edit(team, forms edit team)) }
+ Owner(team) { fuccess(html.team.form.edit(team, forms edit team)) }
}
}
@@ -77,7 +77,7 @@ object Team extends LilaController {
Owner(team) {
implicit val req = ctx.body
forms.edit(team).bindFromRequest.fold(
- err => BadRequest(html.team.edit(team, err)).fuccess,
+ err => BadRequest(html.team.form.edit(team, err)).fuccess,
data => api.update(team, data, me) inject Redirect(routes.Team.show(team.id))
)
}
@@ -88,7 +88,7 @@ object Team extends LilaController {
OptionFuResult(api team id) { team =>
Owner(team) {
MemberRepo userIdsByTeam team.id map { userIds =>
- html.team.kick(team, userIds - me.id)
+ html.team.admin.kick(team, userIds - me.id)
}
}
}
@@ -98,7 +98,7 @@ object Team extends LilaController {
OptionFuResult(api team id) { team =>
Owner(team) {
implicit val req = ctx.body
- forms.selectMember.bindFromRequest.value ?? { api.kick(team, _, me) } inject Redirect(routes.Team.show(team.id))
+ forms.selectMember.bindFromRequest.value.pp ?? { api.kick(team, _, me) } inject Redirect(routes.Team.show(team.id))
}
}
}
@@ -107,7 +107,7 @@ object Team extends LilaController {
OptionFuResult(api team id) { team =>
Owner(team) {
MemberRepo userIdsByTeam team.id map { userIds =>
- html.team.changeOwner(team, userIds - team.createdBy)
+ html.team.admin.changeOwner(team, userIds - team.createdBy)
}
}
}
@@ -134,7 +134,7 @@ object Team extends LilaController {
NotForKids {
OnePerWeek(me) {
forms.anyCaptcha map { captcha =>
- Ok(html.team.form(forms.create, captcha))
+ Ok(html.team.form.create(forms.create, captcha))
}
}
}
@@ -145,7 +145,7 @@ object Team extends LilaController {
implicit val req = ctx.body
forms.create.bindFromRequest.fold(
err => forms.anyCaptcha map { captcha =>
- BadRequest(html.team.form(err, captcha))
+ BadRequest(html.team.form.create(err, captcha))
},
data => api.create(data, me) ?? {
_ map { team => Redirect(routes.Team.show(team.id)): Result }
@@ -155,7 +155,7 @@ object Team extends LilaController {
}
def mine = Auth { implicit ctx => me =>
- api mine me map { html.team.mine(_) }
+ api mine me map { html.team.list.mine(_) }
}
def join(id: String) = Auth { implicit ctx => implicit me =>
@@ -168,12 +168,12 @@ object Team extends LilaController {
def requests = Auth { implicit ctx => me =>
Env.team.cached.nbRequests invalidate me.id
- api requestsWithUsers me map { html.team.allRequests(_) }
+ api requestsWithUsers me map { html.team.request.all(_) }
}
def requestForm(id: String) = Auth { implicit ctx => me =>
OptionFuOk(api.requestable(id, me)) { team =>
- forms.anyCaptcha map { html.team.requestForm(team, forms.request, _) }
+ forms.anyCaptcha map { html.team.request.requestForm(team, forms.request, _) }
}
}
@@ -182,7 +182,7 @@ object Team extends LilaController {
implicit val req = ctx.body
forms.request.bindFromRequest.fold(
err => forms.anyCaptcha map { captcha =>
- BadRequest(html.team.requestForm(team, err, captcha))
+ BadRequest(html.team.request.requestForm(team, err, captcha))
},
setup => api.createRequest(team, setup, me) inject Redirect(routes.Team.show(team.id))
)
diff --git a/app/controllers/Tournament.scala b/app/controllers/Tournament.scala
index 353df2ca5a..6855a14735 100644
--- a/app/controllers/Tournament.scala
+++ b/app/controllers/Tournament.scala
@@ -19,7 +19,7 @@ object Tournament extends LilaController {
private def env = Env.tournament
private def repo = TournamentRepo
- private def tournamentNotFound(implicit ctx: Context) = NotFound(html.tournament.notFound())
+ private def tournamentNotFound(implicit ctx: Context) = NotFound(html.tournament.bits.notFound())
private[controllers] val upcomingCache = Env.memo.asyncCache.single[(VisibleTournaments, List[Tour])](
name = "tournament.home",
@@ -64,7 +64,7 @@ object Tournament extends LilaController {
case "arena" => System.Arena.some
case _ => none
}
- Ok(html.tournament.faqPage(system)).fuccess
+ Ok(html.tournament.faq.page(system)).fuccess
}
def leaderboard = Open { implicit ctx =>
@@ -74,11 +74,14 @@ object Tournament extends LilaController {
} yield Ok(html.tournament.leaderboard(winners))
}
- private[controllers] def canHaveChat(tour: Tour)(implicit ctx: Context): Boolean = ctx.me ?? { u =>
- if (ctx.kid) false
- else if (tour.isPrivate) true
- else Env.chat.panic.allowed(u, tighter = tour.variant == chess.variant.Antichess)
- }
+ private[controllers] def canHaveChat(tour: Tour, json: Option[JsObject])(implicit ctx: Context): Boolean =
+ !ctx.kid && // no public chats for kids
+ ctx.me.fold(!tour.isPrivate) { u => // anon can see public chats, except for private tournaments
+ (!tour.isPrivate || json.fold(true)(jsonHasMe)) && // private tournament that I joined
+ Env.chat.panic.allowed(u, tighter = tour.variant == chess.variant.Antichess)
+ }
+
+ private def jsonHasMe(js: JsObject): Boolean = (js \ "me").toOption.isDefined
def show(id: String) = Open { implicit ctx =>
val page = getInt("page")
@@ -88,8 +91,8 @@ object Tournament extends LilaController {
(for {
verdicts <- env.api.verdicts(tour, ctx.me, getUserTeamIds)
version <- env.version(tour.id)
- chat <- canHaveChat(tour) ?? Env.chat.api.userChat.cached.findMine(Chat.Id(tour.id), ctx.me).map(some)
json <- env.jsonView(tour, page, ctx.me, getUserTeamIds, none, version.some, partial = false, ctx.lang)
+ chat <- canHaveChat(tour, json.some) ?? Env.chat.api.userChat.cached.findMine(Chat.Id(tour.id), ctx.me).map(some)
_ <- chat ?? { c => Env.user.lightUserApi.preloadMany(c.chat.userIds) }
streamers <- streamerCache get tour.id
shieldOwner <- env.shieldApi currentOwner tour
@@ -284,11 +287,19 @@ object Tournament extends LilaController {
def shields = Open { implicit ctx =>
for {
- history <- env.shieldApi.history
+ history <- env.shieldApi.history(5.some)
_ <- Env.user.lightUserApi preloadMany history.userIds
} yield html.tournament.shields(history)
}
+ def categShields(k: String) = Open { implicit ctx =>
+ OptionFuOk(env.shieldApi.byCategKey(k)) {
+ case (categ, awards) =>
+ Env.user.lightUserApi preloadMany awards.map(_.owner.value) inject
+ html.tournament.shields.byCateg(categ, awards)
+ }
+ }
+
def calendar = Open { implicit ctx =>
env.api.calendar map { tours =>
Ok(html.tournament.calendar(env.scheduleJsonView calendar tours))
diff --git a/app/controllers/Tv.scala b/app/controllers/Tv.scala
index 31bfe5d1ff..b80b28b49a 100644
--- a/app/controllers/Tv.scala
+++ b/app/controllers/Tv.scala
@@ -15,16 +15,11 @@ object Tv extends LilaController {
(lila.tv.Tv.Channel.byKey get chanKey).fold(notFound)(lichessTv)
}
- def sides(chanKey: String, gameId: String, color: String) = Open { implicit ctx =>
- lila.tv.Tv.Channel.byKey get chanKey match {
- case None => notFound
- case Some(channel) =>
- OptionFuResult(GameRepo.pov(gameId, color)) { pov =>
- Env.tv.tv.getChampions zip
- Env.game.crosstableApi.withMatchup(pov.game) map {
- case (champions, crosstable) => Ok(html.tv.side.sides(channel, champions, pov, crosstable))
- }
- }
+ def sides(gameId: String, color: String) = Open { implicit ctx =>
+ OptionFuResult(GameRepo.pov(gameId, color)) { pov =>
+ Env.game.crosstableApi.withMatchup(pov.game) map { ct =>
+ Ok(html.tv.side.sides(pov, ct))
+ }
}
}
@@ -62,7 +57,7 @@ object Tv extends LilaController {
def gamesChannel(chanKey: String) = Open { implicit ctx =>
(lila.tv.Tv.Channel.byKey get chanKey) ?? { channel =>
- Env.tv.tv.getChampions zip Env.tv.tv.getGames(channel, 9) map {
+ Env.tv.tv.getChampions zip Env.tv.tv.getGames(channel, 12) map {
case (champs, games) => NoCache {
Ok(html.tv.games(channel, games map lila.game.Pov.first, champs))
}
@@ -81,23 +76,19 @@ object Tv extends LilaController {
}
}
+ /* for BC */
def embed = Action { req =>
Ok {
- val bg = get("bg", req) | "light"
- val theme = get("theme", req) | "brown"
- val url = s"""${req.domain + routes.Tv.frame}?bg=$bg&theme=$theme"""
- s"""document.write("");"""
+ val config = ui.EmbedConfig(req)
+ val url = s"""${req.domain + routes.Tv.frame}?bg=${config.bg}&theme=${config.board}"""
+ s"""document.write("");"""
} as JAVASCRIPT withHeaders (CACHE_CONTROL -> "max-age=86400")
}
def frame = Action.async { implicit req =>
Env.tv.tv.getBestGame map {
case None => NotFound
- case Some(game) => Ok(views.html.tv.embed(
- Pov first game,
- get("bg", req) | "light",
- lila.pref.Theme(~get("theme", req)).cssClass
- ))
+ case Some(game) => Ok(views.html.tv.embed(Pov first game))
}
}
}
diff --git a/app/controllers/User.scala b/app/controllers/User.scala
index c39d678a56..033e9252e0 100644
--- a/app/controllers/User.scala
+++ b/app/controllers/User.scala
@@ -4,7 +4,6 @@ import play.api.data.Form
import play.api.libs.iteratee._
import play.api.libs.json._
import play.api.mvc._
-import play.twirl.api.Html
import scala.concurrent.duration._
import lila.api.{ Context, BodyContext }
@@ -68,7 +67,7 @@ object User extends LilaController {
nbs ← Env.current.userNbGames(u, ctx)
info ← Env.current.userInfo(u, nbs, ctx)
social ← Env.current.socialInfo(u, ctx)
- } yield status(html.user.show.activity(u, as, info, social))
+ } yield status(html.user.show.page.activity(u, as, info, social))
}.mon(_.http.response.user.show.website)
else Env.activity.read.recent(u) map { as =>
status(html.activity(u, as))
@@ -98,7 +97,7 @@ object User extends LilaController {
_ <- Env.team.cached.nameCache preloadMany info.teamIds
social ← Env.current.socialInfo(u, ctx)
searchForm = (filters.current == GameFilter.Search) option GameFilterMenu.searchForm(userGameSearch, filters.current)(ctx.body)
- } yield html.user.show.games(u, info, pag, filters, searchForm, social)
+ } yield html.user.show.page.games(u, info, pag, filters, searchForm, social)
else fuccess(html.user.show.gamesContent(u, nbs, pag, filters, filter))
} yield res,
api = _ => apiGames(u, filter, page)
@@ -108,12 +107,14 @@ object User extends LilaController {
}
private def EnabledUser(username: String)(f: UserModel => Fu[Result])(implicit ctx: Context): Fu[Result] =
- OptionFuResult(UserRepo named username) { u =>
- if (u.enabled || isGranted(_.UserSpy)) f(u)
- else negotiate(
+ UserRepo named username flatMap {
+ case None if isGranted(_.UserSpy) => Mod.searchTerm(username.trim)
+ case None => notFound
+ case Some(u) if (u.enabled || isGranted(_.UserSpy)) => f(u)
+ case Some(u) => negotiate(
html = UserRepo isErased u flatMap { erased =>
if (erased.value) notFound
- else NotFound(html.user.disabled(u)).fuccess
+ else NotFound(html.user.show.page.disabled(u)).fuccess
},
api = _ => fuccess(NotFound(jsonError("No such user, or account closed")))
)
@@ -145,12 +146,12 @@ object User extends LilaController {
}
}
- def online = Open { implicit req =>
+ def online = Action.async { implicit req =>
val max = 50
negotiate(
- html = notFound,
+ html = notFoundJson(),
api = _ => env.cached.getTop50Online map { list =>
- Ok(Json.toJson(list.take(getInt("nb").fold(10)(_ min max)).map(env.jsonView(_))))
+ Ok(Json.toJson(list.take(getInt("nb", req).fold(10)(_ min max)).map(env.jsonView(_))))
}
)
}
@@ -291,7 +292,7 @@ object User extends LilaController {
}
val irwin = Env.irwin.api.reports.withPovs(user) map {
_ ?? { reps =>
- html.irwin.irwinReport(reps).some
+ html.irwin.report(reps).some
}
}
val assess = Env.mod.assessApi.getPlayerAggregateAssessmentWithGames(user.id) flatMap {
@@ -300,7 +301,7 @@ object User extends LilaController {
}
}
import play.api.libs.EventSource
- implicit val extractor = EventSource.EventDataExtractor[Html](_.toString)
+ implicit val extractor = EventSource.EventDataExtractor[scalatags.Text.Frag](_.render)
Ok.chunked {
(Enumerator(html.user.mod.menu(user)) interleave
futureToEnumerator(parts.logTimeIfGt(s"$username parts", 2 seconds)) interleave
diff --git a/app/controllers/UserTournament.scala b/app/controllers/UserTournament.scala
index 91a429def9..702887619b 100644
--- a/app/controllers/UserTournament.scala
+++ b/app/controllers/UserTournament.scala
@@ -12,11 +12,11 @@ object UserTournament extends LilaController {
path match {
case "recent" =>
Env.tournament.leaderboardApi.recentByUser(user, page).map { entries =>
- Ok(html.userTournament.recent(user, entries))
+ Ok(html.userTournament.bits.recent(user, entries))
}
case "best" =>
Env.tournament.leaderboardApi.bestByUser(user, page).map { entries =>
- Ok(html.userTournament.best(user, entries))
+ Ok(html.userTournament.bits.best(user, entries))
}
case "chart" => Env.tournament.leaderboardApi.chart(user).map { data =>
Ok(html.userTournament.chart(user, data))
diff --git a/app/controllers/Video.scala b/app/controllers/Video.scala
index 9e1f3c3d21..beb9bf672e 100644
--- a/app/controllers/Video.scala
+++ b/app/controllers/Video.scala
@@ -41,7 +41,7 @@ object Video extends LilaController {
def show(id: String) = Open { implicit ctx =>
WithUserControl { control =>
env.api.video.find(id) flatMap {
- case None => fuccess(NotFound(html.video.notFound(control)))
+ case None => fuccess(NotFound(html.video.bits.notFound(control)))
case Some(video) => env.api.video.similar(ctx.me, video, 9) zip
ctx.userId.?? { userId =>
env.api.view.add(View.make(videoId = video.id, userId = userId))
@@ -56,7 +56,7 @@ object Video extends LilaController {
def author(author: String) = Open { implicit ctx =>
WithUserControl { control =>
env.api.video.byAuthor(ctx.me, author, getInt("page") | 1) map { videos =>
- Ok(html.video.author(author, videos, control))
+ Ok(html.video.bits.author(author, videos, control))
}
}
}
@@ -64,7 +64,7 @@ object Video extends LilaController {
def tags = Open { implicit ctx =>
WithUserControl { control =>
env.api.tag.allPopular map { tags =>
- Ok(html.video.tags(tags, control))
+ Ok(html.video.bits.tags(tags, control))
}
}
}
diff --git a/app/mashup/Preload.scala b/app/mashup/Preload.scala
index 07e5984ef7..55525330f9 100644
--- a/app/mashup/Preload.scala
+++ b/app/mashup/Preload.scala
@@ -28,7 +28,7 @@ final class Preload(
lightUserApi: LightUserApi
) {
- private type Response = (JsObject, Vector[Entry], List[MiniForumPost], List[Tournament], List[Event], List[Simul], Option[Game], List[User.LightPerf], List[Winner], Option[lila.puzzle.DailyPuzzle], LiveStreams.WithTitles, List[lila.blog.MiniPost], Option[TempBan], Option[Preload.CurrentGame], Int)
+ private type Response = (JsObject, Vector[Entry], List[MiniForumPost], List[Tournament], List[Event], List[Simul], Option[Game], List[User.LightPerf], List[Winner], Option[lila.puzzle.DailyPuzzle], LiveStreams.WithTitles, List[lila.blog.MiniPost], Option[TempBan], Option[Preload.CurrentGame], Int, List[Pov])
def apply(
posts: Fu[List[MiniForumPost]],
@@ -46,16 +46,17 @@ final class Preload(
leaderboard(()) zip
tourneyWinners zip
(ctx.noBot ?? dailyPuzzle()) zip
- liveStreams().dmap(_.autoFeatured.withTitles(lightUserApi)) zip
- (ctx.userId ?? getPlayban) flatMap {
- case (data, povs) ~ posts ~ tours ~ events ~ simuls ~ feat ~ entries ~ lead ~ tWinners ~ puzzle ~ streams ~ playban =>
+ liveStreams().dmap(_.autoFeatured withTitles lightUserApi) zip
+ (ctx.userId ?? getPlayban) zip
+ (ctx.blind ?? ctx.me ?? GameRepo.urgentGames) flatMap {
+ case (data, povs) ~ posts ~ tours ~ events ~ simuls ~ feat ~ entries ~ lead ~ tWinners ~ puzzle ~ streams ~ playban ~ blindGames =>
val currentGame = ctx.me ?? Preload.currentGameMyTurn(povs, lightUserApi.sync) _
lightUserApi.preloadMany {
tWinners.map(_.userId) :::
posts.flatMap(_.userId) :::
entries.flatMap(_.userIds).toList
} inject
- (data, entries, posts, tours, events, simuls, feat, lead, tWinners, puzzle, streams, Env.blog.lastPostCache.apply, playban, currentGame, countRounds())
+ (data, entries, posts, tours, events, simuls, feat, lead, tWinners, puzzle, streams, Env.blog.lastPostCache.apply, playban, currentGame, countRounds(), blindGames)
}
}
diff --git a/app/package.scala b/app/package.scala
index a38045b553..a331405a4c 100644
--- a/app/package.scala
+++ b/app/package.scala
@@ -1,3 +1,14 @@
package lila
-package object app extends PackageObject with socket.WithSocket
+import play.api.http._
+import play.api.mvc.Codec
+import scalatags.Text.Frag
+
+package object app extends PackageObject with socket.WithSocket {
+
+ implicit def contentTypeOfFrag(implicit codec: Codec): ContentTypeOf[Frag] =
+ ContentTypeOf[Frag](Some(ContentTypes.HTML))
+
+ implicit def writeableOfFrag(implicit codec: Codec): Writeable[Frag] =
+ Writeable(frag => codec.encode(frag.render))
+}
diff --git a/app/templating/AiHelper.scala b/app/templating/AiHelper.scala
index ac3eac616e..b678436090 100644
--- a/app/templating/AiHelper.scala
+++ b/app/templating/AiHelper.scala
@@ -1,8 +1,7 @@
package lila.app
package templating
-import play.twirl.api.Html
-
+import lila.app.ui.ScalatagsTemplate._
import lila.user.UserContext
trait AiHelper { self: I18nHelper =>
@@ -15,8 +14,8 @@ trait AiHelper { self: I18nHelper =>
s"$name$rating"
}
- def aiNameHtml(level: Int, withRating: Boolean = true)(implicit ctx: UserContext) =
- Html(aiName(level, withRating).replace(" ", " "))
+ def aiNameFrag(level: Int, withRating: Boolean = true)(implicit ctx: UserContext) =
+ raw(aiName(level, withRating).replace(" ", " "))
def aiRating(level: Int): Option[Int] = Env.fishnet.aiPerfApi.intRatings get level
}
diff --git a/app/templating/AssetHelper.scala b/app/templating/AssetHelper.scala
index 671361bb68..2680c42b8f 100644
--- a/app/templating/AssetHelper.scala
+++ b/app/templating/AssetHelper.scala
@@ -3,18 +3,21 @@ package templating
import controllers.routes
import play.api.mvc.RequestHeader
-import play.twirl.api.Html
import lila.api.Context
-import lila.common.{ AssetVersion, ContentSecurityPolicy }
+import lila.app.ui.ScalatagsTemplate._
+import lila.common.{ Nonce, AssetVersion, ContentSecurityPolicy }
trait AssetHelper { self: I18nHelper with SecurityHelper =>
def isProd: Boolean
+ val siteDomain = lila.api.Env.current.Net.Domain
val assetDomain = lila.api.Env.current.Net.AssetDomain
val socketDomain = lila.api.Env.current.Net.SocketDomain
+ val sameAssetDomain = siteDomain == assetDomain
+
val assetBaseUrl = s"//$assetDomain"
def assetVersion = AssetVersion.current
@@ -26,75 +29,72 @@ trait AssetHelper { self: I18nHelper with SecurityHelper =>
def dbImageUrl(path: String) = s"$assetBaseUrl/image/$path"
- def cssTag(name: String): Html = cssAt("stylesheets/" + name)
+ def cssTag(name: String)(implicit ctx: Context): Frag =
+ cssTagWithTheme(name, ctx.currentBg)
- def cssTags(names: String*): Html = Html {
- names.map { name =>
- cssTag(name).body
- } mkString ""
- }
- def cssTags(names: List[(String, Boolean)]): Html =
- cssTags(names.collect { case (k, true) => k }: _*)
+ def cssTagWithTheme(name: String, theme: String): Frag =
+ cssAt(s"css/$name.$theme.${if (isProd) "min" else "dev"}.css")
- def cssVendorTag(name: String) = cssAt("vendor/" + name)
+ def cssTagNoTheme(name: String)(implicit ctx: Context): Frag =
+ cssAt(s"css/$name.${if (isProd) "min" else "dev"}.css")
- def cssAt(path: String): Html = Html {
- s""""""
- }
+ private def cssAt(path: String): Frag =
+ link(href := assetUrl(path), tpe := "text/css", rel := "stylesheet")
- def jsTag(name: String, async: Boolean = false) =
- jsAt("javascripts/" + name, async = async)
+ def jsTag(name: String, defer: Boolean = false): Frag =
+ jsAt("javascripts/" + name, defer = defer)
- def jsAt(path: String, async: Boolean = false): Html = Html {
- val src = assetUrl(path)
- s""""""
- }
+ /* about async & defer, see https://flaviocopes.com/javascript-async-defer/
+ * we want defer only, to ensure scripts are executed in order of declaration,
+ * so that round.js doesn't run before site.js */
+ def jsAt(path: String, defer: Boolean = false): Frag = script(
+ defer option deferAttr,
+ src := assetUrl(path)
+ )
- val jQueryTag = Html {
+ val jQueryTag = raw {
s""""""
}
- def roundTag = jsAt(s"compiled/lichess.round${isProd ?? (".min")}.js", async = true)
+ def roundTag = jsAt(s"compiled/lichess.round${isProd ?? (".min")}.js", defer = true)
def roundNvuiTag(implicit ctx: Context) = ctx.blind option
- jsAt(s"compiled/lichess.round.nvui.min.js", async = true)
+ jsAt(s"compiled/lichess.round.nvui.min.js", defer = true)
def analyseTag = jsAt(s"compiled/lichess.analyse${isProd ?? (".min")}.js")
def analyseNvuiTag(implicit ctx: Context) = ctx.blind option
jsAt(s"compiled/lichess.analyse.nvui.min.js")
- val highchartsLatestTag = Html {
+ def captchaTag = jsAt(s"compiled/captcha.js")
+
+ val highchartsLatestTag = raw {
s""""""
}
- val highchartsMoreTag = Html {
+ val highchartsMoreTag = raw {
s""""""
}
- val typeaheadTag = Html {
- s""""""
+ val fingerprintTag = raw {
+ s""""""
}
- val fingerprintTag = Html {
- s""""""
- }
-
- val flatpickrTag = Html {
- s""""""
- }
-
- val nonAsyncFlatpickrTag = Html {
+ val flatpickrTag = raw {
s""""""
}
- def delayFlatpickrStart(implicit ctx: Context) = embedJs {
+ val nonAsyncFlatpickrTag = raw {
+ s""""""
+ }
+
+ def delayFlatpickrStart(implicit ctx: Context) = embedJsUnsafe {
"""$(function() { setTimeout(function() { $(".flatpickr").flatpickr(); }, 2000) });"""
}
val infiniteScrollTag = jsTag("vendor/jquery.infinitescroll.min.js")
- def prismicJs(implicit ctx: Context) = Html {
+ def prismicJs(implicit ctx: Context): Frag = raw {
isGranted(_.Prismic) ?? {
- embedJsUnsafe("""window.prismic={endpoint:'https://lichess.prismic.io/api/v2'}""").body ++
+ embedJsUnsafe("""window.prismic={endpoint:'https://lichess.prismic.io/api/v2'}""").render ++
""""""
}
}
@@ -105,7 +105,7 @@ trait AssetHelper { self: I18nHelper with SecurityHelper =>
ContentSecurityPolicy(
defaultSrc = List("'self'", assets),
connectSrc = List("'self'", assets, socket, lila.api.Env.current.ExplorerEndpoint, lila.api.Env.current.TablebaseEndpoint),
- styleSrc = List("'self'", "'unsafe-inline'", assets, "https://fonts.googleapis.com"),
+ styleSrc = List("'self'", "'unsafe-inline'", assets),
fontSrc = List("'self'", assetDomain, "https://fonts.gstatic.com"),
frameSrc = List("'self'", assets, "https://www.youtube.com"),
workerSrc = List("'self'", assets),
@@ -120,15 +120,12 @@ trait AssetHelper { self: I18nHelper with SecurityHelper =>
ctx.nonce.fold(csp)(csp.withNonce(_))
}
- def embedJsUnsafe(js: String)(implicit ctx: Context): Html = Html {
- val nonce = ctx.nonce ?? { nonce => s""" nonce="$nonce"""" }
- s""""""
- }
- def embedJsUnsafe(js: scalatags.Text.RawFrag)(implicit ctx: Context): scalatags.Text.RawFrag = scalatags.Text.all.raw {
+ def embedJsUnsafe(js: String)(implicit ctx: Context): Frag = raw {
val nonce = ctx.nonce ?? { nonce => s""" nonce="$nonce"""" }
s""""""
}
- def embedJs(js: Html)(implicit ctx: Context): Html = embedJsUnsafe(js.body)
- def embedJs(js: String)(implicit ctx: Context): Html = embedJsUnsafe(js)
+ def embedJsUnsafe(js: String, nonce: Nonce): Frag = raw {
+ s""""""
+ }
}
diff --git a/app/templating/ChessgroundHelper.scala b/app/templating/ChessgroundHelper.scala
index 8f039b31d9..e09e1c76f9 100644
--- a/app/templating/ChessgroundHelper.scala
+++ b/app/templating/ChessgroundHelper.scala
@@ -3,32 +3,34 @@ package templating
import chess.{ Color, Board, Pos }
import lila.api.Context
-import play.twirl.api.Html
+import lila.app.ui.ScalatagsTemplate._
import lila.game.Pov
trait ChessgroundHelper {
- def chessground(board: Board, orient: Color, lastMove: List[Pos] = Nil)(implicit ctx: Context): Html = wrap {
- if (ctx.pref.is3d) ""
- else {
- def top(p: Pos) = orient.fold(8 - p.y, p.y - 1) * 12.5
- def left(p: Pos) = orient.fold(p.x - 1, 8 - p.x) * 12.5
- val highlights = ctx.pref.highlight ?? lastMove.distinct.map { pos =>
- s"""
${transKey(error.message, I18nDb.Site, error.args)}
""" - } + def errMsg(error: FormError)(implicit ctx: Context): Frag = + p(cls := "error")(transKey(error.message, I18nDb.Site, error.args)) - def errMsg(errors: Seq[FormError])(implicit ctx: Context): Html = Html { + def errMsg(errors: Seq[FormError])(implicit ctx: Context): Frag = errors map errMsg mkString - } - def globalError(form: Form[_])(implicit ctx: Context): Option[Html] = + def globalError(form: Form[_])(implicit ctx: Context): Option[Frag] = form.globalError map errMsg val booleanChoices = Seq("true" -> "✓ Yes", "false" -> "✗ No") - object form3 extends ui.ScalatagsPlay { - - import ui.ScalatagsTemplate._ + object form3 { private val idPrefix = "form3" @@ -47,20 +43,14 @@ trait FormHelper { self: I18nHelper => * such as `optional(nonEmptyText)`. * And we can't tell from the Field whether it's optional or not :( */ - // case ("constraint.required", _) => required := true + // case ("constraint.required", _) => required case ("constraint.minLength", Seq(m: Int)) => minlength := m case ("constraint.maxLength", Seq(m: Int)) => maxlength := m case ("constraint.min", Seq(m: Int)) => min := m case ("constraint.max", Seq(m: Int)) => max := m } - /* All public methods must return HTML - * because twirl just calls toString on scalatags frags - * and that escapes the content :( */ - - def split(html: Html): Html = div(cls := "form-split")(html) - - def split(frags: Frag*): Html = div(cls := "form-split")(frags) + val split = div(cls := "form-split") def group( field: Field, @@ -68,94 +58,84 @@ trait FormHelper { self: I18nHelper => klass: String = "", half: Boolean = false, help: Option[Frag] = None - )(content: Field => Frag)(implicit ctx: Context): Html = - div(cls := List( - "form-group" -> true, - "is-invalid" -> field.hasErrors, - "form-half" -> half, - klass -> klass.nonEmpty - ))( - groupLabel(field)(labelContent), - content(field), - errors(field), - help map { helper(_) } - ) + )(content: Field => Frag)(implicit ctx: Context): Frag = div(cls := List( + "form-group" -> true, + "is-invalid" -> field.hasErrors, + "form-half" -> half, + klass -> klass.nonEmpty + ))( + groupLabel(field)(labelContent), + content(field), + errors(field), + help map { helper(_) } + ) def input(field: Field, typ: String = "", klass: String = ""): BaseTagType = st.input( st.id := id(field), name := field.name, value := field.value, - `type` := typ.nonEmpty.option(typ), + tpe := typ.nonEmpty.option(typ), cls := List("form-control" -> true, klass -> klass.nonEmpty) )(validationModifiers(field)) - def inputHtml(field: Field, typ: String = "", klass: String = "")(modifiers: Modifier*): Html = - input(field, typ, klass)(modifiers) - def checkbox( field: Field, labelContent: Frag, half: Boolean = false, help: Option[Frag] = None, disabled: Boolean = false - ): Html = - div(cls := List( - "form-check form-group" -> true, - "form-half" -> half - ))( - div( - span(cls := "form-check-input")( - st.input( - st.id := id(field), - name := field.name, - value := "true", - `type` := "checkbox", - cls := "form-control cmn-toggle", - checked := field.value.has("true").option(true), - st.disabled := disabled.option(true) - ), - label(`for` := id(field)) + ): Frag = div(cls := List( + "form-check form-group" -> true, + "form-half" -> half + ))( + div( + span(cls := "form-check-input")( + st.input( + st.id := id(field), + name := field.name, + value := "true", + tpe := "checkbox", + cls := "form-control cmn-toggle", + field.value.has("true") option checked, + disabled option st.disabled ), - groupLabel(field)(labelContent) + label(`for` := id(field)) ), - help map { helper(_) } - ) + groupLabel(field)(labelContent) + ), + help map { helper(_) } + ) def select( field: Field, options: Iterable[(Any, String)], default: Option[String] = None - ): Html = - st.select( - st.id := id(field), - name := field.name, - cls := "form-control" - )(validationModifiers(field))( - default map { option(value := "")(_) }, - options.toSeq map { - case (value, name) => option( - st.value := value.toString, - selected := field.value.has(value.toString).option(true) - )(name) - } - ) + ): Frag = st.select( + st.id := id(field), + name := field.name, + cls := "form-control" + )(validationModifiers(field))( + default map { option(value := "")(_) }, + options.toSeq map { + case (value, name) => option( + st.value := value.toString, + field.value.has(value.toString) option selected + )(name) + } + ) def textarea( field: Field, klass: String = "" - )(modifiers: Modifier*): Html = - st.textarea( - st.id := id(field), - name := field.name, - cls := List("form-control" -> true, klass -> klass.nonEmpty) - )(validationModifiers(field))(modifiers)(~field.value) + )(modifiers: Modifier*): Frag = st.textarea( + st.id := id(field), + name := field.name, + cls := List("form-control" -> true, klass -> klass.nonEmpty) + )(validationModifiers(field))(modifiers)(~field.value) val actions = div(cls := "form-actions") - def actionsHtml(html: Frag): Html = actions(html) - val action = div(cls := "form-actions single") - def actionHtml(html: Frag): Html = div(cls := "form-actions single")(html) def submit( content: Frag, @@ -163,8 +143,8 @@ trait FormHelper { self: I18nHelper => nameValue: Option[(String, String)] = None, klass: String = "", confirm: Option[String] = None - ): Html = button( - `type` := "submit", + ): Frag = button( + tpe := "submit", dataIcon := icon, name := nameValue.map(_._1), value := nameValue.map(_._2), @@ -177,34 +157,33 @@ trait FormHelper { self: I18nHelper => title := confirm )(content) - def hidden(field: Field, value: Option[String] = None): Html = - st.input( - st.id := id(field), - name := field.name, - st.value := value.orElse(field.value), - `type` := "hidden" - ) + def hidden(field: Field, value: Option[String] = None): Frag = st.input( + st.id := id(field), + name := field.name, + st.value := value.orElse(field.value), + tpe := "hidden" + ) - def password(field: Field, content: Html)(implicit ctx: Context): Frag = - group(field, content)(input(_, typ = "password")(required := true)) + def password(field: Field, content: Frag)(implicit ctx: Context): Frag = + group(field, content)(input(_, typ = "password")(required)) - def passwordNoAutocomplete(field: Field, content: Html)(implicit ctx: Context): Frag = - group(field, content)(input(_, typ = "password")(autocomplete := "off")(required := true)) + def passwordModified(field: Field, content: Frag)(modifiers: Modifier*)(implicit ctx: Context): Frag = + group(field, content)(input(_, typ = "password")(required)(modifiers)) - def globalError(form: Form[_])(implicit ctx: Context): Option[Html] = + def globalError(form: Form[_])(implicit ctx: Context): Option[Frag] = form.globalError map { err => div(cls := "form-group is-invalid")(error(err)) } - def flatpickr(field: Field, withTime: Boolean = true): Html = + def flatpickr(field: Field, withTime: Boolean = true): Frag = input(field, klass = "flatpickr")( dataEnableTime := withTime, datatime24h := withTime ) object file { - def image(name: String): Html = st.input(`type` := "file", st.name := name, accept := "image/*") - def pgn(name: String): Html = st.input(`type` := "file", st.name := name, accept := ".pgn") + def image(name: String): Frag = st.input(tpe := "file", st.name := name, accept := "image/*") + def pgn(name: String): Frag = st.input(tpe := "file", st.name := name, accept := ".pgn") } } } diff --git a/app/templating/ForumHelper.scala b/app/templating/ForumHelper.scala index ef7fa3fed3..2a587aa208 100644 --- a/app/templating/ForumHelper.scala +++ b/app/templating/ForumHelper.scala @@ -1,9 +1,8 @@ package lila.app package templating -import play.twirl.api.Html - import lila.api.Context +import lila.app.ui.ScalatagsTemplate._ import lila.forum.Post trait ForumHelper { self: UserHelper with StringHelper => @@ -22,7 +21,7 @@ trait ForumHelper { self: UserHelper with StringHelper => def authorName(post: Post) = post.userId match { case Some(userId) => userIdSpanMini(userId, withOnline = true) - case None => Html(lila.user.User.anonymous) + case None => frag(lila.user.User.anonymous) } def authorLink( @@ -30,9 +29,9 @@ trait ForumHelper { self: UserHelper with StringHelper => cssClass: Option[String] = None, withOnline: Boolean = true, modIcon: Boolean = false - ) = - if (post.erased) Html(s""" """) - else post.userId.fold(Html(lila.user.User.anonymous)) { userId => + ): Frag = + if (post.erased) span(cls := "author")("@trans.thisIsAListOfDevicesThatHaveLoggedIntoYourAccount()
- @if(sessions.length > 1) { -- - | -
- @s.session.ip
- @s.location
- @s.session.ua - @s.session.date.map { date => -- @momentFromNow(date) - @if(s.session.id == curSessionId) { [CURRENT] } - - } - |
- - @if(s.session.id != curSessionId) { - - } - | -
@trans.weHaveSentYouAnEmailClickTheLink()
-@trans.usernameAlreadyUsed()
-} -@form3.group(password, trans.password.frag())(form3.input(_, typ = "password")) -@emailOption.map { email => -@form3.group(email, trans.email.frag())(form3.input(_, typ = "email")) -} diff --git a/app/views/auth/layout.scala.html b/app/views/auth/layout.scala.html deleted file mode 100644 index 85c2252572..0000000000 --- a/app/views/auth/layout.scala.html +++ /dev/null @@ -1,6 +0,0 @@ -@(title: String, moreJs: Html = emptyHtml, formCss: Boolean = false, csp: Option[lila.common.ContentSecurityPolicy] = None)(body: Html)(implicit ctx: Context) -@base.layout( -title = title, -moreCss = cssTags(List("user-signup.css" -> true, "form3.css" -> formCss)), -moreJs = moreJs, -csp = csp)(body).toHtml diff --git a/app/views/auth/login.scala b/app/views/auth/login.scala new file mode 100644 index 0000000000..3eb4091de8 --- /dev/null +++ b/app/views/auth/login.scala @@ -0,0 +1,47 @@ +package views.html +package auth + +import play.api.data.Form + +import lila.api.Context +import lila.app.templating.Environment._ +import lila.app.ui.ScalatagsTemplate._ + +import controllers.routes + +object login { + + val twoFactorHelp = span(dataIcon := "")( + "Open the two-factor authentication app on your device to view your authentication code and verify your identity." + ) + + def apply(form: Form[_], referrer: Option[String])(implicit ctx: Context) = views.html.base.layout( + title = trans.signIn.txt(), + moreJs = jsTag("login.js"), + moreCss = cssTag("auth") + ) { + main(cls := "auth auth-login box box-pad")( + h1(trans.signIn()), + st.form( + cls := "form3", + action := s"${routes.Auth.authenticate}${referrer.?? { ref => s"?referrer=${java.net.URLEncoder.encode(ref, "US-ASCII")}" }}", + method := "post" + )( + div(cls := "one-factor")( + form3.globalError(form), + auth.bits.formFields(form("username"), form("password"), none, register = false), + form3.submit(trans.signIn(), icon = none) + ), + div(cls := "two-factor none")( + form3.group(form("token"), raw("Authentication code"), help = Some(twoFactorHelp))(form3.input(_)(autocomplete := "off", pattern := "[0-9]{6}")), + p(cls := "error none")("Invalid code."), + form3.submit(trans.signIn(), icon = none) + ) + ), + div(cls := "alternative")( + a(href := routes.Auth.signup())(trans.signUp()), + a(href := routes.Auth.passwordReset())(trans.passwordReset()) + ) + ) + } +} diff --git a/app/views/auth/login.scala.html b/app/views/auth/login.scala.html deleted file mode 100644 index 1896c44dcb..0000000000 --- a/app/views/auth/login.scala.html +++ /dev/null @@ -1,42 +0,0 @@ -@(form: Form[_], referrer: Option[String])(implicit ctx: Context) -@import lila.app.ui.ScalatagsTwirlForm._ - -@twoFactorHelp = { -Open the two-factor authentication app on your device to view your authentication code and verify your identity. -} - -@auth.layout( -title = trans.signIn.txt(), -moreJs = jsTag("login.js"), -formCss = true) { -@trans.weHaveSentYouAnEmailTo(email)
-@trans.ifYouDoNotSeeTheEmailCheckOtherPlaces()
-
- Sorry, you can't signup to lichess through TOR!
-
-
- As an Anonymous user, you can play, train, and use all lichess features.
-
You tried to visit a page you're not authorized to access. Return to the homepage.
-
If the problem persists, please report the bug.
-@ex.getMessage
-Return to the homepage, or play this mini-game!
-
- ChessPursuit - courtesy of - Saturnyn -
-