show user timeline on homepage

pull/83/head
Thibault Duplessis 2013-05-24 15:49:02 +02:00
parent b6a09ab149
commit f691230d2c
27 changed files with 219 additions and 109 deletions

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "scalachess"]
path = modules/chess
url = git://github.com/ornicar/scalachess.git
[submodule "public/vendor/timeago"]
path = public/vendor/timeago
url = https://github.com/rmm5t/jquery-timeago

View File

@ -3,17 +3,26 @@ package lila.app
import akka.actor._
import com.typesafe.config.Config
final class Env(config: Config, system: ActorSystem) {
final class Env(
config: Config,
system: ActorSystem,
appPath: String) {
val CliUsername = config getString "cli.username"
private val RendererName = config getString "app.renderer.name"
private val RouterName = config getString "app.router.name"
private val WebPath = config getString "app.web_path"
private val TimeagoLocalesPath = config getString "app.timeago_locales_path"
def timeagoLocalesPath = appPath + "/" + TimeagoLocalesPath
lazy val preloader = new mashup.Preload(
lobby = Env.lobby.lobby,
history = Env.lobby.history,
featured = Env.game.featured)
featured = Env.game.featured,
recentGames = () Env.timeline.getter.recentGames,
timelineEntries = Env.timeline.getter.userEntries _)
lazy val userInfo = mashup.UserInfo(
countUsers = () Env.user.countEnabled,
@ -55,7 +64,8 @@ object Env {
lazy val current = "[boot] app" describes new Env(
config = lila.common.PlayApp.loadConfig,
system = lila.common.PlayApp.system)
system = lila.common.PlayApp.system,
appPath = lila.common.PlayApp withApp (_.path.getCanonicalPath))
def api = lila.api.Env.current
def db = lila.db.Env.current

View File

@ -23,9 +23,6 @@ private[app] final class Renderer extends Actor {
sender ! V.tournament.createdTable(tours)
case entry: lila.timeline.GameEntry =>
sender ! V.lobby.gameTimelineEntry(entry)
case entry: lila.timeline.Entry =>
sender ! Html(entry.toString) // V.lobby.timelineEntry(entry)
sender ! V.timeline.gameEntry(entry)
}
}

View File

@ -26,13 +26,12 @@ object Lobby extends LilaController with Results {
private def renderHome[A](status: Status)(implicit ctx: Context): Fu[Result] =
Env.current.preloader(
timeline = Env.timeline.recentGames,
posts = Env.forum.recent(ctx.me, Env.team.cached.teamIds.apply),
tours = TournamentRepo.created,
filter = Env.setup.filter
).map(_.fold(Redirect(_), {
case (preload, entries, posts, tours, featured) status(html.lobby.home(
Json stringify preload, entries, posts, tours, featured)) |> { response
case (preload, entries, gameEntries, posts, tours, featured) status(html.lobby.home(
Json stringify preload, entries, gameEntries, posts, tours, featured)) |> { response
ctx.req.session.data.contains(LilaCookie.sessionId).fold(
response,
response withCookies LilaCookie.makeSessionId(ctx.req)

View File

@ -1,47 +1,49 @@
package lila.app
package mashup
import lila.lobby.{ Hook, HookRepo }
import lila.lobby.actorApi.lobby._
import lila.timeline.GameEntry
import lila.game.{ Game, GameRepo, Featured }
import lila.forum.PostLiteView
import lila.socket.History
import lila.tournament.Created
import lila.setup.FilterConfig
import lila.user.{ User, Context }
import controllers.routes
import makeTimeout.large
import akka.actor.ActorRef
import akka.pattern.ask
import play.api.mvc.Call
import play.api.libs.json.{ Json, JsObject, JsArray }
import play.api.mvc.Call
import controllers.routes
import lila.forum.PostLiteView
import lila.game.{ Game, GameRepo, Featured }
import lila.lobby.actorApi.lobby._
import lila.lobby.{ Hook, HookRepo }
import lila.setup.FilterConfig
import lila.socket.History
import lila.timeline.{ Entry, GameEntry }
import lila.tournament.Created
import lila.user.{ User, Context }
import makeTimeout.large
final class Preload(
lobby: ActorRef,
history: History,
featured: Featured) {
featured: Featured,
recentGames: () Fu[List[GameEntry]],
timelineEntries: String Fu[List[Entry]]) {
private type RightResponse = (JsObject, List[GameEntry], List[PostLiteView], List[Created], Option[Game])
private type RightResponse = (JsObject, List[Entry], List[GameEntry], List[PostLiteView], List[Created], Option[Game])
private type Response = Either[Call, RightResponse]
def apply(
timeline: Fu[List[GameEntry]],
posts: Fu[List[PostLiteView]],
tours: Fu[List[Created]],
filter: Fu[FilterConfig])(implicit ctx: Context): Fu[Response] =
ctx.isAuth.fold(lobby ? GetOpen, lobby ? GetOpenCasual).mapTo[List[Hook]] zip
timeline zip
posts zip
tours zip
featured.one zip
filter map {
case (((((hooks, entries), posts), tours), feat), filter)
recentGames() zip
posts zip
tours zip
featured.one zip
(ctx.userId ?? timelineEntries) zip
filter map {
case ((((((hooks, gameEntries), posts), tours), feat), entries), filter)
(Right((Json.obj(
"version" -> history.version,
"pool" -> JsArray(hooks map (_.render)),
"filter" -> filter.render
), entries, posts, tours, feat)))
), entries, gameEntries, posts, tours, feat)))
}
}

View File

@ -1,19 +1,23 @@
package lila.app
package templating
import lila.user.Context
import org.joda.time.DateTime
import org.joda.time.format.{ DateTimeFormat, DateTimeFormatter }
import java.util.Locale
import scala.collection.mutable
import org.joda.time.DateTime
import org.joda.time.format._
import play.api.templates.Html
import lila.user.Context
trait DateHelper { self: I18nHelper
private val style = "MS"
private val formatters = mutable.Map[String, DateTimeFormatter]()
private val isoFormatter = ISODateTimeFormat.dateTime
private def formatter(ctx: Context): DateTimeFormatter =
formatters.getOrElseUpdate(
lang(ctx).language,
@ -21,4 +25,25 @@ trait DateHelper { self: I18nHelper ⇒
def showDate(date: DateTime)(implicit ctx: Context): String =
formatter(ctx) print date
def timeago(date: DateTime)(implicit ctx: Context): Html = Html(
"""<time class="timeago" datetime="%s">%s</time>"""
.format(isoFormatter print date, showDate(date))
)
def timeagoLocale(implicit ctx: Context): Option[String] =
lang(ctx).language match {
case "en" none
case "pt" "pt-br".some
case "zh" "zh-CN".some
case l timeagoLocales(l) option l
}
private lazy val timeagoLocales: Set[String] = {
import java.io.File
val Regex = """^jquery\.timeago\.(\w{2})\.js$""".r
(new File(Env.current.timeagoLocalesPath).listFiles map (_.getName) collect {
case Regex(l) l
}).toSet: Set[String]
}
}

View File

@ -3,9 +3,10 @@ package templating
import java.text.SimpleDateFormat
import java.util.Date
import java.util.regex.Matcher.quoteReplacement
import org.apache.commons.lang3.StringEscapeUtils.escapeXml
import play.api.templates.Html
import java.util.regex.Matcher.quoteReplacement
trait StringHelper {

View File

@ -141,5 +141,8 @@ moreJs: Html = Html(""))(body: Html)(implicit ctx: Context)
@if(lang.language != "en") {
<script src="@routes.Assets.at("trans/" + lang.language + ".js")?v=@assetVersion"></script>
}
@timeagoLocale.map { l =>
<script src="@routes.Assets.at("vendor/timeago/locales/jquery.timeago." + l + ".js")"></script>
}
</body>
</html>

View File

@ -1,4 +1,4 @@
@(preload: String, gameTimeline: List[lila.timeline.GameEntry], forumRecent: List[lila.forum.PostLiteView], tours: List[lila.tournament.Created], featured: Option[Game])(implicit ctx: Context)
@(preload: String, userTimeline: List[lila.timeline.Entry], gameTimeline: List[lila.timeline.GameEntry], forumRecent: List[lila.forum.PostLiteView], tours: List[lila.tournament.Created], featured: Option[Game])(implicit ctx: Context)
@underchat = {
<div id="featured_game">
@ -13,6 +13,11 @@
}
@goodies = {
<div id="timeline">
@userTimeline.map { entry =>
@timeline.entry(entry)
}
</div>
}
@base.layout(
@ -44,39 +49,7 @@ underchat = underchat.some) {
</div>
</div>
@lobby.buttons()
<div class="open_tournaments undertable">
<div class="undertable_top">
<a class="more" title="See all tournaments" href="@routes.Tournament.home()">More »</a>
<span class="title">Open tournaments</span>
</div>
<div class="undertable_inner">
<table class="tournaments">
@tournament.createdTable(tours)
</table>
</div>
</div>
<div class="lichess_bot undertable">
<div class="undertable_top">
<a class="more" title="@trans.seeTheGamesBeingPlayedInRealTime()" href="@routes.Game.realtime()">@trans.games() »</a>
<span class="title">@trans.gamesBeingPlayedRightNow()</span>
</div>
<div class="undertable_inner">
<div class="content">
<table class="lichess_messages">@gameTimelineEntries(gameTimeline)</table>
</div>
</div>
</div>
<div class="new_posts undertable" data-url="@routes.ForumPost.recent">
<div class="undertable_top">
<a class="more" title="@trans.talkAboutChessAndDiscussLichessFeaturesInTheForum()" href="@routes.ForumCateg.index">@trans.forum() »</a>
<span class="title">@trans.forum()</span>
</div>
<div class="undertable_inner">
<div class="content">
<ol>@forum.post.recent(forumRecent)</ol>
</div>
</div>
</div>
@lobby.undertable(gameTimeline, forumRecent, tours)
</div>
@embedJs("var lichess_preload = " + preload)
}

View File

@ -0,0 +1,35 @@
@(gameTimeline: List[lila.timeline.GameEntry], forumRecent: List[lila.forum.PostLiteView], tours: List[lila.tournament.Created])(implicit ctx: Context)
<div class="open_tournaments undertable">
<div class="undertable_top">
<a class="more" title="See all tournaments" href="@routes.Tournament.home()">More »</a>
<span class="title">Open tournaments</span>
</div>
<div class="undertable_inner">
<table class="tournaments">
@tournament.createdTable(tours)
</table>
</div>
</div>
<div class="lichess_bot undertable">
<div class="undertable_top">
<a class="more" title="@trans.seeTheGamesBeingPlayedInRealTime()" href="@routes.Game.realtime()">@trans.games() »</a>
<span class="title">@trans.gamesBeingPlayedRightNow()</span>
</div>
<div class="undertable_inner">
<div class="content">
<table>@timeline.gameEntries(gameTimeline)</table>
</div>
</div>
</div>
<div class="new_posts undertable" data-url="@routes.ForumPost.recent">
<div class="undertable_top">
<a class="more" title="@trans.talkAboutChessAndDiscussLichessFeaturesInTheForum()" href="@routes.ForumCateg.index">@trans.forum() »</a>
<span class="title">@trans.forum()</span>
</div>
<div class="undertable_inner">
<div class="content">
<ol>@forum.post.recent(forumRecent)</ol>
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
@(e: lila.timeline.Entry)(implicit ctx: Context)
@import lila.timeline.Entry._
@e.decode.map { decoded =>
<div class="entry">
@decoded match {
case Follow(userId) => {
@timeline.follow(userId) @timeago(e.date)
}
}
</div>
}

View File

@ -0,0 +1,3 @@
@(userId: String)(implicit ctx: Context)
@userIdLink(userId.some) @trans.followsYou()

View File

@ -1,5 +1,5 @@
@(entries: List[lila.timeline.GameEntry])
@entries.map { e =>
<tr>@gameTimelineEntry(e)</tr>
<tr>@timeline.gameEntry(e)</tr>
}

View File

@ -17,12 +17,12 @@ object PlayApp {
}
private def enableScheduler = loadConfig getBoolean "app.scheduler.enabled"
def scheduler = new Scheduler(system, enabled = enableScheduler && isServer)
def isDev = isMode(_.Dev)
def isTest = isMode(_.Test)
def isProd = isMode(_.Prod)
// def isServer = !(isDev || isTest)
def isServer = !isTest
def isMode(f: Mode.type Mode.Mode) = withApp { _.mode == f(Mode) }
}

View File

@ -47,7 +47,7 @@ package timeline {
def apply[A: Writes](user: String, typ: MakeEntry.type String, data: A): MakeEntry =
MakeEntry(user, typ(MakeEntry), Json toJson data)
}
case class EntryView(user: String, rendered: String)
case class ReloadTimeline(user: String)
case class GameEntryView(rendered: String)
}

View File

@ -1,20 +1,21 @@
package lila.lobby
import actorApi._
import lila.socket.{ SocketActor, History, Historical }
import lila.socket.actorApi.{ Connected _, _ }
import lila.game.actorApi._
import lila.hub.actorApi.lobby._
import lila.hub.actorApi.timeline._
import lila.hub.actorApi.router.{ Homepage, Player }
import makeTimeout.short
import scala.concurrent.duration._
import actorApi._
import akka.actor._
import akka.pattern.ask
import play.api.libs.json._
import play.api.libs.iteratee._
import play.api.libs.json._
import play.api.templates.Html
import scala.concurrent.duration._
import lila.game.actorApi._
import lila.hub.actorApi.lobby._
import lila.hub.actorApi.router.{ Homepage, Player }
import lila.hub.actorApi.timeline._
import lila.socket.actorApi.{ Connected _, _ }
import lila.socket.{ SocketActor, History, Historical }
import makeTimeout.short
private[lobby] final class Socket(
val history: History,
@ -37,15 +38,15 @@ private[lobby] final class Socket(
sender ! Connected(enumerator, member)
}
case ReloadTournaments(html) notifyTournaments(html)
case ReloadTournaments(html) notifyTournaments(html)
case GameEntryView(rendered) notifyVersion("game_entry", rendered)
case GameEntryView(rendered) notifyVersion("game_entry", rendered)
case EntryView(user, rendered) sendTo(user, makeMessage("entry", rendered))
case ReloadTimeline(user) sendTo(user, makeMessage("reload_timeline", JsNull))
case AddHook(hook) notifyVersion("hook_add", hook.render)
case AddHook(hook) notifyVersion("hook_add", hook.render)
case RemoveHook(hookId) notifyVersion("hook_remove", hookId)
case RemoveHook(hookId) notifyVersion("hook_remove", hookId)
case JoinHook(uid, hook, game)
playerUrl(game fullIdOf game.creatorColor) zip

View File

@ -3,6 +3,8 @@ package lila.timeline
import org.joda.time.DateTime
import play.api.libs.json._
import lila.common.PimpedJson._
case class Entry(
user: String,
typ: String,
@ -13,11 +15,20 @@ case class Entry(
(user == other.user) &&
(typ == other.typ) &&
(data == other.data)
def decode: Option[Entry.Decoded] = typ match {
case "follow" data str "user" map { Entry.Follow(_) }
case _ none
}
}
object Entry {
def make(user: String, typ: String, data: JsValue): Option[Entry] =
sealed trait Decoded
case class Follow(userId: String) extends Decoded
private[timeline] def make(user: String, typ: String, data: JsValue): Option[Entry] =
data.asOpt[JsObject] map { Entry(user, typ, _, DateTime.now) }
import lila.db.Tube

View File

@ -1,9 +1,5 @@
package lila.timeline
import tube.gameEntryTube
import lila.db.api._
import lila.db.Implicits._
import akka.actor._
import com.typesafe.config.Config
@ -22,8 +18,9 @@ final class Env(
private val UserDisplayMax = config getInt "user.display_max"
private val UserActorName = config getString "user.actor.name"
def recentGames: Fu[List[GameEntry]] =
$query[GameEntry]($select.all) sort $sort.naturalOrder toListFlatten GameDisplayMax.some
lazy val getter = new Getter(
gameMax = GameDisplayMax,
userMax = UserDisplayMax)
system.actorOf(Props(new GamePush(
lobbySocket = lobbySocket,

View File

@ -0,0 +1,22 @@
package lila.timeline
import play.api.libs.json.Json
import lila.db.api._
import lila.db.Implicits._
import tube.{ entryTube, gameEntryTube }
private[timeline] final class Getter(
gameMax: Int,
userMax: Int) {
def recentGames: Fu[List[GameEntry]] =
$find[GameEntry](
$query[GameEntry]($select.all) sort $sort.naturalOrder,
gameMax)
def userEntries(userId: String): Fu[List[Entry]] =
$find[Entry](
$query[Entry](Json.obj("user" -> userId)) sort $sort.desc("date"),
userMax)
}

View File

@ -18,9 +18,7 @@ private[timeline] final class Push(
def receive = {
case maker @ MakeEntry(user, typ, data) makeEntry(user, typ, data) foreach { entry
renderer ? entry map {
case view: Html EntryView(user, view.body)
} pipeTo lobbySocket.ref
lobbySocket.ref ! ReloadTimeline(user)
}
}

View File

@ -341,6 +341,12 @@ var lichess_sri = Math.random().toString(36).substring(5); // 8 chars
setTimeout(userPowertips, 600);
$('body').on('lichess.content_loaded', userPowertips);
function setTimeAgo() {
$("time:not(.jsed)").addClass('.jsed').timeago();
}
setTimeAgo();
$('body').on('lichess.content_loaded', setTimeAgo);
// Start game
var $game = $('div.lichess_game').orNot();
if ($game) $game.game(_ld_);
@ -1836,8 +1842,9 @@ var lichess_sri = Math.random().toString(36).substring(5); // 8 chars
game_entry: function(e) {
renderTimeline([e]);
},
entry: function(e) {
console.debug(e);
reload_timeline: function() {
// TODO
console.debug("reload timeline");
},
hook_add: addHook,
hook_remove: removeHook,

View File

@ -19,3 +19,6 @@ if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).
// jQuery.cookie
(function(e,h,j){function k(b){return b}function l(b){return decodeURIComponent(b.replace(m," "))}var m=/\+/g,d=e.cookie=function(b,c,a){if(c!==j){a=e.extend({},d.defaults,a);null===c&&(a.expires=-1);if("number"===typeof a.expires){var f=a.expires,g=a.expires=new Date;g.setDate(g.getDate()+f)}c=d.json?JSON.stringify(c):String(c);return h.cookie=[encodeURIComponent(b),"=",d.raw?c:encodeURIComponent(c),a.expires?"; expires="+a.expires.toUTCString():"",a.path?"; path="+a.path:"",a.domain?"; domain="+ a.domain:"",a.secure?"; secure":""].join("")}c=d.raw?k:l;a=h.cookie.split("; ");for(f=0;g=a[f]&&a[f].split("=");f++)if(c(g.shift())===b)return b=c(g.join("=")),d.json?JSON.parse(b):b;return null};d.defaults={};e.removeCookie=function(b,c){return null!==e.cookie(b)?(e.cookie(b,null,c),!0):!1}})(jQuery,document);
// jquery.timeago 1.2.0 https://github.com/rmm5t/jquery-timeago
(function(d){"function"===typeof define&&define.amd?define(["jquery"],d):d(jQuery)})(function(d){function l(){var a;a=d(this);if(!a.data("timeago")){a.data("timeago",{datetime:e.datetime(a)});var b=d.trim(a.text());e.settings.localeTitle?a.attr("title",a.data("timeago").datetime.toLocaleString()):0<b.length&&(!e.isTime(a)||!a.attr("title"))&&a.attr("title",b)}a=a.data("timeago");b=e.settings;isNaN(a.datetime)||(0==b.cutoff||(new Date).getTime()-a.datetime.getTime()<b.cutoff)&&d(this).text(f(a.datetime));return this}function f(a){return e.inWords((new Date).getTime()-a.getTime())}d.timeago=function(a){return a instanceof Date?f(a):"string"===typeof a?f(d.timeago.parse(a)):"number"===typeof a?f(new Date(a)):f(d.timeago.datetime(a))};var e=d.timeago;d.extend(d.timeago,{settings:{refreshMillis:6E4,allowFuture:!1,localeTitle:!1,cutoff:0,strings:{prefixAgo:null,prefixFromNow:null,suffixAgo:"ago",suffixFromNow:"from now",seconds:"less than a minute",minute:"about a minute",minutes:"%d minutes",hour:"about an hour",hours:"about %d hours",day:"a day",days:"%d days",month:"about a month",months:"%d months",year:"about a year",years:"%d years",wordSeparator:" ",numbers:[]}},inWords:function(a){function b(b,e){return(d.isFunction(b)?b(e,a):b).replace(/%d/i,c.numbers&&c.numbers[e]||e)}var c=this.settings.strings,e=c.prefixAgo,f=c.suffixAgo;this.settings.allowFuture&&0>a&&(e=c.prefixFromNow,f=c.suffixFromNow);var h=Math.abs(a)/1E3,g=h/60,m=g/60,k=m/24,l=k/365,h=45>h&&b(c.seconds,Math.round(h))||90>h&&b(c.minute,1)||45>g&&b(c.minutes,Math.round(g))||90>g&&b(c.hour,1)||24>m&&b(c.hours,Math.round(m))||42>m&&b(c.day,1)||30>k&&b(c.days,Math.round(k))||45>k&&b(c.month,1)||365>k&&b(c.months,Math.round(k/30))||1.5>l&&b(c.year,1)||b(c.years,Math.round(l)),g=c.wordSeparator||"";void 0===c.wordSeparator&&(g=" ");return d.trim([e,h,f].join(g))},parse:function(a){a=d.trim(a);a=a.replace(/\.\d+/,"");a=a.replace(/-/,"/").replace(/-/,"/");a=a.replace(/T/," ").replace(/Z/," UTC");a=a.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2");return new Date(a)},datetime:function(a){a=e.isTime(a)?d(a).attr("datetime"):d(a).attr("title");return e.parse(a)},isTime:function(a){return"time"===d(a).get(0).tagName.toLowerCase()}});var n={init:function(){var a=d.proxy(l,this);a();var b=e.settings;0<b.refreshMillis&&setInterval(a,b.refreshMillis)},update:function(a){d(this).data("timeago",{datetime:e.parse(a)});l.apply(this)}};d.fn.timeago=function(a,b){var c=a?n[a]:n.init;if(!c)throw Error("Unknown function name '"+a+"' for timeago");this.each(function(){c.call(this,b)});return this};document.createElement("abbr");document.createElement("time")});

View File

@ -515,9 +515,6 @@ div.lichess_chat {
div.lichess_chat.small_chat {
left: 0px;
}
div.lichess_chat.lobby_chat {
top: 126px;
}
div.lichess_chat_top {
border-radius: 4px 4px 0 0;
border: 1px solid #ccc;
@ -552,9 +549,6 @@ div.lichess_chat:hover .lichess_messages {
div.lichess_chat.small_chat .lichess_messages {
width: 186px;
}
div.lichess_chat.lobby_chat .lichess_messages {
height: 341px;
}
div.lichess_chat form {
position: relative;

View File

@ -825,6 +825,18 @@ div.game_extra div.bookmarkers {
float: right;
max-width: 48%;
}
#timeline {
margin-top: 2em;
}
#timeline > .entry {
padding-bottom: 1em;
border-bottom: 1px solid #c0c0c0;
margin-bottom: 1em;
}
#timeline time {
font-size: 0.8em;
color: #afafaf;
}
span.bookmark {
position: relative;
float: right;

View File

@ -134,7 +134,8 @@ body.dark div.lichess_chat a.user_link,
body.dark div.lichess_chat a.user_link,
body.dark div.new_posts li span,
body.dark #team .forum a.user_link,
body.dark span.board_mark
body.dark span.board_mark,
body.dark #timeline time
{
color: #808080;
}

1
public/vendor/timeago vendored 160000

@ -0,0 +1 @@
Subproject commit d52af890182a0779598d7001834a3708a0ea5746