remove momentjs and fork timeago.js

pull/3349/merge
Thibault Duplessis 2017-07-25 17:00:00 +02:00
parent 767be9543c
commit f9481bfe38
34 changed files with 271 additions and 98 deletions

3
.gitmodules vendored
View File

@ -4,9 +4,6 @@
[submodule "public/vendor/tagmanager"]
path = public/vendor/tagmanager
url = https://github.com/max-favilli/tagmanager
[submodule "public/vendor/moment"]
path = public/vendor/moment
url = https://github.com/moment/moment
[submodule "public/vendor/ChessPursuit"]
path = public/vendor/ChessPursuit
url = https://github.com/ornicar/ChessPursuit

View File

@ -65,21 +65,15 @@ trait AssetHelper { self: I18nHelper =>
"""<script src="//code.highcharts.com/4.1.4/highcharts-more.js"></script>"""
}
private val momentJsMissingLangs = Set("le", "pi", "tp", "ky", "ga", "zu", "la", "tg", "mg", "as", "yo", "ps", "fp", "ur", "tc", "ia", "jb", "gu", "kn", "gd", "kb", "io", "sw", "sa", "kk", "mn")
private val timeagoLangs = Set("ar", "be", "bg", "ca", "da", "de", "el", "en", "en_short", "es", "eu", "fa", "fi", "fr", "gl", "he", "hu", "in_BG", "in_HI", "in_ID", "it", "ja", "ko", "ml", "my", "nb_NO", "nl", "nn_NO", "pl", "pt_BR", "ro", "ru", "sv", "ta", "th", "tr", "uk", "vi", "zh_CN", "zh_TW")
private def timeagoLangUrl(code: String) = s"/assets/vendor/timeago/locales/$code.js"
def momentLangUrl(implicit ctx: lila.api.Context): Option[String] = {
val l = lang(ctx)
if (momentJsMissingLangs contains l.language) none
else ((l.language, l.country.toLowerCase) match {
case ("en", "us") => none
case ("en", "au" | "ca" | "gb") => l.code.some
case ("pt", "br") => l.code.some
case ("zh", "tw") => l.code.some
case ("zh", _) => "zh-cn".some
case ("ar", "ma" | "sa" | "tn") => l.code.some
case ("fr", "ca") => l.code.some
case _ => l.language.some
}).map { locale => s"/assets/vendor/moment/locale/${locale.toLowerCase}.js" }
def timeagoLangUrl(implicit ctx: lila.api.Context): Option[String] = {
Some("/assets/vendor/timeago/dist/timeago.locales.min.js")
// val l = lang(ctx)
// if (timeagoLangs(l.code)) Some(timeagoLangUrl(l.code))
// else if (timeagoLangs(l.language)) Some(timeagoLangUrl(l.language))
// else none
}
val tagmanagerTag = cdnOrLocal(

View File

@ -76,16 +76,14 @@ trait DateHelper { self: I18nHelper =>
def isoDate(date: DateTime): String = isoFormatter print date
def momentFormat(date: DateTime, format: String): Html = Html {
s"""<time class="moment" datetime="${isoDate(date)}" data-format="$format"></time>"""
}
def momentFormat(date: DateTime): Html = momentFormat(date, "calendar")
private val oneDayMillis = 1000 * 60 * 60 * 24
def momentFromNow(date: DateTime)(implicit ctx: Context) = Html {
s"""<time class="moment-from-now" title="${showDate(date)}" datetime="${isoDate(date)}"></time>"""
val rendered = showDateTime(date)
if ((date.getMillis - nowMillis) > oneDayMillis) s"""<time>$rendered</time>"""
else s"""<time class="timeago" title="$rendered" datetime="${isoDate(date)}">$rendered</time>"""
}
def momentFromNowNoCtx(date: DateTime) = Html {
s"""<time class="moment-from-now" datetime="${isoDate(date)}"></time>"""
s"""<time class="timeago" datetime="${isoDate(date)}"></time>"""
}
def secondsFromNow(seconds: Int)(implicit ctx: Context) =

View File

@ -179,9 +179,27 @@ asyncJs: Boolean = false)(body: Html)(implicit ctx: Context)
@moreJs
@embedJs {
lichess.quantity = @i18nJsQuantityFunction();
lichess.timeagoLocale = function(number, index) {
return [
['à l\'instant', 'dans un instant'],
['il y a %s secondes', 'dans %s secondes'],
['il y a 1 minute', 'dans 1 minute'],
['il y a %s minutes', 'dans %s minutes'],
['il y a 1 heure', 'dans 1 heure'],
['il y a %s heures', 'dans %s heures'],
['il y a 1 jour', 'dans 1 jour'],
['il y a %s jours', 'dans %s jours'],
['il y a 1 semaine', 'dans 1 semaine'],
['il y a %s semaines', 'dans %s semaines'],
['il y a 1 mois', 'dans 1 mois'],
['il y a %s mois', 'dans %s mois'],
['il y a 1 an', 'dans 1 an'],
['il y a %s ans', 'dans %s ans']
][index];
};
@if(lang.language != "en") {
@momentLangUrl.map { url =>
window.momentLocaleUrl = "@url";
@timeagoLangUrl.map { url =>
window.timeagoLocaleUrl = "@url";
}
}
}

View File

@ -20,7 +20,7 @@ moreCss = cssTag("event.css")) {
}
</td>
<td>
<span class="date">@momentFormat(e.startsAt)</span>
<span class="date">@momentFromNow(e.startsAt)</span>
@momentFromNow(e.startsAt)
</td>
</tr>

View File

@ -28,7 +28,7 @@ searchText = text
<p>@shortenWithBr(view.post.text, 200)</p>
</td>
<td class="info">
@momentFormat(view.post.createdAt) by @authorLink(view.post)
@momentFromNow(view.post.createdAt) by @authorLink(view.post)
</td>
</tr>
}

View File

@ -34,7 +34,7 @@
}
</span>
@game.pgnImport.flatMap(_.date).getOrElse(
game.isBeingPlayed.fold(trans.playingRightNow(), momentFormat(game.createdAt))
game.isBeingPlayed.fold(trans.playingRightNow(), momentFromNow(game.createdAt))
)
</div>
@game.pgnImport.flatMap(_.date).map { date =>

View File

@ -57,7 +57,7 @@
• @g.rated.fold(trans.rated(), trans.casual())
}
</strong>
@g.pgnImport.flatMap(_.date).getOrElse(momentFormat(g.createdAt))
@g.pgnImport.flatMap(_.date).getOrElse(momentFromNow(g.createdAt))
@g.tournamentId.map { tourId =>
• @tournamentLink(tourId)
}

View File

@ -39,7 +39,7 @@ title = trans.inbox.txt()) {
<td class="subject">
<a href="@routes.Message.thread(thread.id)#bottom">@thread.name</a>
</td>
<td class="date">@momentFormat(thread.updatedAt)</td>
<td class="date">@momentFromNow(thread.updatedAt)</td>
<td class="check">
<input type="checkbox" name="threads" value="@thread.id" />
</td>

View File

@ -16,7 +16,7 @@ title = thread.name) {
@thread.posts.map { post =>
<div class="thread_message embed_analyse" id="message_@post.id">
<span class="infos">
@userIdLink(thread.visibleSenderOf(post), none) <span data-icon="H"></span> @userIdLink(thread.visibleReceiverOf(post), "inline".some) - @momentFormat(post.createdAt)
@userIdLink(thread.visibleSenderOf(post), none) <span data-icon="H"></span> @userIdLink(thread.visibleReceiverOf(post), "inline".some) - @momentFromNow(post.createdAt)
</span>
<div class="thread_message_body">@autoLink(post.text)</div>
</div>

View File

@ -25,7 +25,7 @@ active = "log") {
<tbody>
@logs.map { log =>
<tr>
<td>@userIdLink(log.mod.some)<br />@momentFormat(log.date)</td>
<td>@userIdLink(log.mod.some)<br />@momentFromNow(log.date)</td>
<td>@log.showAction.capitalize @log.user.map { u =>
@userIdLink(u.some)
}</td>

View File

@ -42,7 +42,7 @@ moreCss = cssTag("report.css")) {
<tr class="@r.unprocessed.fold("new", "")">
<td>
@userIdLink(r.createdBy.some)<br />
@momentFormat(r.createdAt)
@momentFromNow(r.createdAt)
@if(accuracy.isDefined) {
<br /><strong>@accuracy%</strong> accuracy
}

View File

@ -15,5 +15,5 @@
<br />
Host color: @sim.color.getOrElse("random").capitalize
</div>
@trans.by(usernameOrId(sim.hostId)), <span title="@showDateTime(sim.createdAt)">@momentFormat(sim.createdAt)</span>
@trans.by(usernameOrId(sim.hostId)), <span title="@showDateTime(sim.createdAt)">@momentFromNow(sim.createdAt)</span>
</div>

View File

@ -17,7 +17,7 @@
<td>@userLink(request.user)</td>
}
<td>@autoLink(request.message)</td>
<td>@momentFormat(request.date)</td>
<td>@momentFromNow(request.date)</td>
<td class="process">
<form class="process-request" action="@routes.Team.requestProcess(request.id)" method="post">
<input type="hidden" name="url" value="@t.fold(routes.Team.requests())(te => routes.Team.show(te.id))" />

View File

@ -55,7 +55,7 @@
<p class="meta clearfix">
<a href="@routes.ForumPost.redirect(p.postId)">@p.topicName</a>
@userIdLink(p.userId, withOnline = false)
@momentFormat(p.createdAt)
@momentFromNow(p.createdAt)
</p>
<p>@shorten(p.text, 200)</p>
</li>

View File

@ -37,7 +37,7 @@ lichess.StrongSocket.defaults.events.reload = app.update;
@if(s.freq != lila.tournament.Schedule.Freq.Hourly) {
<li data-icon="@tournamentIconChar(tour)" class="is-gold @s.freq.name">
<a href="@routes.Tournament.show(tour.id)">@tour.name</a>
@momentFormat(s.at, "calendar")
@momentFromNow(s.at)
</li>
}
}

View File

@ -41,7 +41,7 @@
} else {
@trans.by(usernameOrId(tour.createdBy))
}
• @momentFormat(tour.startsAt)
• @momentFromNow(tour.startsAt)
@if(!tour.position.initial) {
<br /><br />
<a target="_blank" href="@tour.position.url">

View File

@ -36,7 +36,7 @@
} else {
@e.tour.perfType.map(_.name)
} •
@momentFormat(e.tour.startsAt)
@momentFromNow(e.tour.startsAt)
</span>
</a>
</td>

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

View File

@ -4,10 +4,6 @@ import { ModerationCtrl, ModerationOpts, ModerationData, ModerationReason } from
import { userModInfo } from './xhr'
import { userLink, spinner, bind } from './util';
function isToday(timestamp: number) {
return window.moment(timestamp).isSame(Date.now(), 'day');
}
export function moderationCtrl(opts: ModerationOpts): ModerationCtrl {
let data: ModerationData | undefined;
@ -129,9 +125,8 @@ export function moderationView(ctrl?: ModerationCtrl): VNode[] | undefined {
return h('tr', [
h('td.reason', e.reason),
h('td.mod', e.mod),
h('td', h('time.moment', {
h('td', h('time.timeago', {
attrs: {
'data-format': isToday(e.date) ? 'LT' : 'DD/MM/YY',
datetime: new Date(e.date).toISOString()
}
}))

View File

@ -14,7 +14,7 @@ module.exports = function(element, opts) {
view: view
});
lichess.pubsub.on('moment.locale_loaded', m.redraw);
lichess.pubsub.on('timeago.locale-loaded', m.redraw);
return controller;
};

View File

@ -12,7 +12,7 @@ function formatNumber(dt, n) {
}
function formatSerieName(dt, n) {
if (dt === 'date') return moment(n * 1000).format('LL');
if (dt === 'date') return new Date(n).toLocaleDateString();
return n;
}

View File

@ -29,7 +29,7 @@ var renderMeat = cache(function(ctrl) {
]);
}, function(ctrl) {
var q = ctrl.vm.answer ? ctrl.vm.answer.question : null;
return q ? ctrl.makeUrl(q.dimension, q.metric, q.filters) + moment.locale() : '';
return q ? ctrl.makeUrl(q.dimension, q.metric, q.filters) : '';
});
module.exports = function(ctrl) {

View File

@ -3,10 +3,7 @@ import { Chessground } from 'chessground';
import LobbyController from '../ctrl';
function timer(pov) {
const time = window.moment().add(pov.secondsLeft, 'seconds');
return h('time.moment-from-now', {
attrs: { datetime: time.format() }
}, time.fromNow());
return h('time', window.timeago().format(Date.now() + pov.secondsLeft * 1000));
}
export default function(ctrl: LobbyController) {

View File

@ -201,9 +201,10 @@ function generic(n: Notification, url: string | undefined, icon: string, content
}
function drawTime(n: Notification) {
return h('time.moment-from-now', {
attrs: { datetime: new Date(n.date).toISOString() }
});
var date = new Date(n.date);
return h('time', {
attrs: { title: date.toLocaleString() }
}, window.timeago().format(date));
}
function userFullName(u?: LightUser) {

View File

@ -1,5 +1,7 @@
var m = require('mithril');
const timeago = window.timeago();
function fMap(v, f, orDefault) {
return v ? f(v) : (orDefault || null);
}
@ -12,11 +14,7 @@ function gameLink(id, content) {
}
function date(d) {
return m('time', {
class: 'moment',
datetime: d,
'data-format': 'calendar'
}, '...')
return m('time', timeago.format(d));
}
function fromTo(s) {
@ -64,9 +62,9 @@ module.exports = {
return v;
},
formatSeconds: function(s, format) {
var d = moment.duration(s, 'seconds');
var hours = d.days() * 24 + d.hours();
if (format === 'short') return hours + 'h, ' + d.minutes() + 'm';
return hours + ' hours, ' + d.minutes() + ' minutes';
var hours = Math.floor(s / 3600);
var minutes = Math.floor((s % 3600) / 60);
if (format === 'short') return hours + 'h, ' + minutes + 'm';
return hours + ' hours, ' + minutes + ' minutes';
}
};

View File

@ -16,12 +16,6 @@ function player(p) {
}
module.exports = {
secondsFromNow: function(seconds) {
var time = moment().add(seconds, 'seconds');
return m('time.moment-from-now', {
datetime: time.format()
}, time.fromNow());
},
title: function(ctrl) {
return m('h1.text[data-icon=|]', [
ctrl.data.fullName,

View File

@ -86,7 +86,6 @@ function makeBundle(filename) {
return gulp.src([
'../../public/javascripts/vendor/jquery.min.js',
'./dist/jquery.fill.js',
'../../public/vendor/moment/min/moment.min.js',
'./dep/powertip.min.js',
'./dep/howler.min.js',
'./dep/mousetrap.min.js',

View File

@ -20,7 +20,6 @@
"vinyl-source-stream": "^1"
},
"dependencies": {
"gulp-concat": "^2.6",
"tablesort": "^5.0.0"
}
}

View File

@ -1,4 +1,5 @@
require('./util');
require('./timeago');
require('./trans');
require('./socket');
require('./main');

View File

@ -1,4 +1,3 @@
lichess.topMenuIntent = function() {
$('#topmenu.hover').removeClass('hover').hoverIntent(function() {
$(this).toggleClass('hover');
@ -330,31 +329,20 @@ lichess.topMenuIntent = function() {
document.body.addEventListener('mouseover', lichess.powertip.mouseover);
function setMoment() {
function setTimeago() {
// check that locale was loaded
if (!window.momentLocaleUrl) lichess.requestIdleCallback(function() {
$(".moment-from-now").each(function() {
this.textContent = moment(this.getAttribute('datetime')).fromNow();
});
$("time.moment").removeClass('moment').each(function() {
var parsed = moment(this.getAttribute('datetime'));
var format = this.getAttribute('data-format');
this.textContent = format === 'calendar' ? parsed.calendar(null, {
sameElse: 'DD/MM/YYYY HH:mm'
}) : parsed.format(format);
});
});
if (window.timeagoLocaleUrl) return;
timeago().render($('.timeago').removeClass('timeago'));
}
if (window.momentLocaleUrl) lichess.loadScript(momentLocaleUrl, {noVersion: true}).then(function() {
delete window.momentLocaleUrl;
lichess.pubsub.emit('moment.locale_loaded')();
setMoment();
if (window.timeagoLocaleUrl) lichess.loadScript(timeagoLocaleUrl, {noVersion: true}).then(function() {
window.timeagoLocaleUrl = false;
lichess.pubsub.emit('timeago.locale-loaded')();
setTimeago();
});
else setMoment();
else setTimeago();
lichess.pubsub.on('content_loaded', setMoment);
setInterval(setMoment, 2000);
lichess.pubsub.on('content_loaded', setTimeago);
if ($('body').hasClass('blind_mode')) {
var setBlindMode = function() {

View File

@ -0,0 +1,190 @@
/**
* based on https://github.com/hustcc/timeago.js
* Copyright (c) 2016 hustcc
* License: MIT
**/
window.timeago = (function() {
// second, minute, hour, day, week, month, year(365 days)
var SEC_ARRAY = [60, 60, 24, 7, 365/7/12, 12],
SEC_ARRAY_LEN = 6,
// ATTR_DATETIME = 'datetime',
ATTR_DATA_TID = 'data-tid',
timers = {}; // real-time render timers
// format Date / string / timestamp to Date instance.
function toDate(input) {
if (input instanceof Date) return input;
if (!isNaN(input)) return new Date(parseInt(input));
if (/^\d+$/.test(input)) return new Date(parseInt(input));
input = (input || '').trim().replace(/\.\d+/, '') // remove milliseconds
.replace(/-/, '/').replace(/-/, '/')
.replace(/(\d)T(\d)/, '$1 $2').replace(/Z/, ' UTC') // 2017-2-5T3:57:52Z -> 2017-2-5 3:57:52UTC
.replace(/([\+\-]\d\d)\:?(\d\d)/, ' $1$2'); // -04:00 -> -0400
return new Date(input);
}
// format the diff second to *** time ago
function formatDiff(diff) {
var i = 0,
agoin = diff < 0 ? 1 : 0, // timein or timeago
total_sec = diff = Math.abs(diff);
for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
diff /= SEC_ARRAY[i];
}
diff = parseInt(diff);
i *= 2;
if (diff > (i === 0 ? 9 : 1)) i += 1;
return lichess.timeagoLocale(diff, i, total_sec)[agoin].replace('%s', diff);
}
// calculate the diff second between date to be formated an now date.
function diffSec(date, nowDate) {
nowDate = nowDate ? toDate(nowDate) : new Date();
return (nowDate - toDate(date)) / 1000;
}
/**
* nextInterval: calculate the next interval time.
* - diff: the diff sec between now and date to be formated.
*
* What's the meaning?
* diff = 61 then return 59
* diff = 3601 (an hour + 1 second), then return 3599
* make the interval with high performace.
**/
function nextInterval(diff) {
var rst = 1, i = 0, d = Math.abs(diff);
for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
diff /= SEC_ARRAY[i];
rst *= SEC_ARRAY[i];
}
// return leftSec(d, rst);
d = d % rst;
d = d ? rst - d : rst;
return Math.ceil(d);
}
// get the datetime attribute, `data-timeagp` / `datetime` are supported.
function getDateAttr(node) {
return getAttr(node, 'data-timeago') || getAttr(node, 'datetime');
}
// get the node attribute, native DOM and jquery supported.
function getAttr(node, name) {
if(node.getAttribute) return node.getAttribute(name); // native
if(node.attr) return node.attr(name); // jquery
}
// set the node attribute, native DOM and jquery supported.
function setTidAttr(node, val) {
if(node.setAttribute) return node.setAttribute(ATTR_DATA_TID, val); // native
if(node.attr) return node.attr(ATTR_DATA_TID, val); // jquery
}
// get the timer id of node.
// remove the function, can save some bytes.
// function getTidFromNode(node) {
// return getAttr(node, ATTR_DATA_TID);
// }
/**
* timeago: the function to get `timeago` instance.
* - nowDate: the relative date, default is new Date().
*
* How to use it?
* var timeagoLib = require('timeago.js');
* var timeago = timeagoLib(); // all use default.
* var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
**/
function Timeago(nowDate) {
this.nowDate = nowDate;
}
// what the timer will do
Timeago.prototype.doRender = function(node, date) {
var diff = diffSec(date, this.nowDate),
self = this,
tid;
// delete previously assigned timeout's id to node
node.innerHTML = formatDiff(diff);
// waiting %s seconds, do the next render
timers[tid = setTimeout(function() {
self.doRender(node, date);
delete timers[tid];
}, Math.min(nextInterval(diff) * 1000, 0x7FFFFFFF))] = 0; // there is no need to save node in object.
// set attribute date-tid
setTidAttr(node, tid);
};
/**
* format: format the date to *** time ago
* - date: the date / string / timestamp to be formated
*
* How to use it?
* var timeago = require('timeago.js')();
* timeago.format(new Date(), 'pl'); // Date instance
* timeago.format('2016-09-10', 'fr'); // formated date string
* timeago.format(1473473400269); // timestamp with ms
**/
Timeago.prototype.format = function(date) {
return formatDiff(diffSec(date, this.nowDate));
};
/**
* render: render the DOM real-time.
* - nodes: which nodes will be rendered.
*
* How to use it?
* var timeago = require('timeago.js')();
* // 1. javascript selector
* timeago.render(document.querySelectorAll('.need_to_be_rendered'));
* // 2. use jQuery selector
* timeago.render($('.need_to_be_rendered'));
*
* Notice: please be sure the dom has attribute `datetime`.
**/
Timeago.prototype.render = function(nodes) {
if (nodes.length === undefined) nodes = [nodes];
for (var i = 0, len = nodes.length; i < len; i++) {
this.doRender(nodes[i], getDateAttr(nodes[i])); // render item
}
};
function timeagoFactory(nowDate) {
return new Timeago(nowDate);
}
/**
* cancel: cancels one or all the timers which are doing real-time render.
*
* How to use it?
* For canceling all the timers:
* var timeagoFactory = require('timeago.js');
* var timeago = timeagoFactory();
* timeago.render(document.querySelectorAll('.need_to_be_rendered'));
* timeagoFactory.cancel(); // will stop all the timers, stop render in real time.
*
* For canceling single timer on specific node:
* var timeagoFactory = require('timeago.js');
* var timeago = timeagoFactory();
* var nodes = document.querySelectorAll('.need_to_be_rendered');
* timeago.render(nodes);
* timeagoFactory.cancel(nodes[0]); // will clear a timer attached to the first node, stop render in real time.
**/
timeagoFactory.cancel = function(node) {
var tid;
// assigning in if statement to save space
if (node) {
tid = getAttr(node, ATTR_DATA_TID); // get the timer of DOM node(native / jq).
if (tid) {
clearTimeout(tid);
delete timers[tid];
}
} else {
for (tid in timers) clearTimeout(tid);
timers = {};
}
};
/**
* timeago: the function to get `timeago` instance.
* - nowDate: the relative date, default is new Date().
*
* How to use it?
* var timeagoFactory = require('timeago.js');
* var timeago = timeagoFactory(); // all use default.
* var timeago = timeagoFactory('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
**/
return timeagoFactory;
})();

View File

@ -18,7 +18,12 @@ function clock(d): VNode | undefined {
if (d.isFinished) return;
if (d.secondsToStart) {
if (d.secondsToStart > oneDayInSeconds) return h('div.clock', [
h('time.moment-from-now.shy', { attrs: { datetime: d.startsAt } }, d.startsAt)
h('time.timeago.shy', {
attrs: {
title: new Date(d.startsAt).toLocaleString(),
datetime: Date.now() + (d.secondsToStart * 1000)
}
})
]);
return h('div.clock.created', {
hook: startClock(d.secondsToStart)

View File

@ -46,9 +46,9 @@ function nextTournament(ctrl: TournamentController): MaybeVNodes {
' • ',
...(t.finishesAt ? [
'finishes ',
h('time.moment-from-now', { attrs: { datetime: t.finishesAt } }, t.finishesAt)
h('time.timeago', { attrs: { datetime: t.finishesAt } })
] : [
h('time.moment-from-now', { attrs: { datetime: t.startsAt } }, t.startsAt)
h('time.timeago', { attrs: { datetime: t.startsAt } })
])
])
])