ui/site refactor WIP

refactor-site-js
Thibault Duplessis 2020-09-01 18:39:44 +02:00
parent 1c05a29dcc
commit bf9aff4c43
27 changed files with 482 additions and 497 deletions

View File

@ -26,7 +26,7 @@ object userAnalysis {
moreJs = frag(
analyseTag,
analyseNvuiTag,
embedJsUnsafe(s"""lichess=lichess||{};lichess.user_analysis=${safeJsonValue(
embedJsUnsafe(s"""lichess.userAnalysis=${safeJsonValue(
Json.obj(
"data" -> data,
"i18n" -> userAnalysisI18n(withForecast = withForecast),

View File

@ -20,7 +20,7 @@ object show {
moreJs = frag(
analyseTag,
analyseNvuiTag,
embedJsUnsafe(s"""lichess=window.lichess||{};lichess.practice=${safeJsonValue(
embedJsUnsafe(s"""lichess.practice=${safeJsonValue(
Json.obj(
"practice" -> data.practice,
"study" -> data.study,

View File

@ -18,9 +18,7 @@ object show {
moreJs = frag(
jsTag("vendor/sparkline.min.js"),
jsModule("puzzle"),
embedJsUnsafe(s"""
lichess = lichess || {};
lichess.puzzle = ${safeJsonValue(
embedJsUnsafe(s"""lichess.puzzle=${safeJsonValue(
Json.obj(
"data" -> data,
"pref" -> pref,

View File

@ -26,7 +26,7 @@ object show {
moreJs = frag(
analyseTag,
analyseNvuiTag,
embedJsUnsafe(s"""lichess=window.lichess||{};lichess.relay=${safeJsonValue(
embedJsUnsafe(s"""lichess.relay=${safeJsonValue(
Json.obj(
"relay" -> data.relay,
"study" -> data.study.add("admin" -> isGranted(_.StudyAdmin)),

View File

@ -24,7 +24,7 @@ object show {
moreJs = frag(
analyseTag,
analyseNvuiTag,
embedJsUnsafe(s"""lichess=window.lichess||{};lichess.study=${safeJsonValue(
embedJsUnsafe(s"""lichess.study=${safeJsonValue(
Json.obj(
"study" -> data.study.add("admin" -> isGranted(_.StudyAdmin)),
"data" -> data.analysis,

View File

@ -26,7 +26,7 @@ object show {
title = s"${tour.name()} #${tour.id}",
moreJs = frag(
jsModule("tournament"),
embedJsUnsafe(s"""lichess=lichess||{};lichess.tournament=${safeJsonValue(
embedJsUnsafe(s"""lichess.tournament=${safeJsonValue(
Json.obj(
"data" -> data,
"i18n" -> bits.jsI18n,

View File

@ -12,7 +12,6 @@ interface Lichess {
powertip: any;
widget: any;
hoverable?: boolean;
isHoverable(): boolean;
spinnerHtml: string;
assetUrl(url: string, opts?: AssetUrlOpts): string;
soundUrl: string;
@ -27,7 +26,9 @@ interface Lichess {
numberFormat(n: number): string;
idleTimer(delay: number, onIdle: () => void, onWakeUp: () => void): void;
pubsub: Pubsub;
hasToReload: boolean;
unload: {
expected: boolean;
};
redirect(o: string | { url: string, cookie: Cookie }): void;
reload(): void;
escapeHtml(str: string): string;
@ -50,17 +51,11 @@ interface Lichess {
update(node: HTMLElement, data: { fen: string, lm: string, wc?: number, bc?: number }): void;
finish(node: HTMLElement, win?: Color): void;
};
challengeApp: any;
ab?: any;
// socket.js
StrongSocket: {
(url: string, version: number | false, cfg: any): any;
defaults: {
events: {
fen(e: any): void;
}
},
firstConnect: Promise<(tpe: string, data: any) => void>
}
@ -70,6 +65,7 @@ interface Lichess {
format(date: number | Date): string;
absolute(date: number | Date): string;
}
timeagoLocale(a: number, b: number, c: number): any;
// misc
advantageChart: {
@ -86,9 +82,6 @@ interface Lichess {
playMusic(): any;
quietMode?: boolean;
keyboardMove?: any;
notifyApp: {
setMsgRead(user: string): void;
};
modal: {
(html: JQuery | string, cls?: string, onClose?: () => void): void;
close(): void;

View File

@ -151,7 +151,7 @@ export default class MsgCtrl {
setRead = () => {
const msg = this.currentContact()?.lastMsg;
if (msg && msg.user != this.data.me.id) {
window.lichess.notifyApp.setMsgRead(msg.user);
window.lichess.pubsub.emit('notify-app.set-read', msg.user);
if (msg.read) return false;
msg.read = true;
network.setRead(msg.user);

View File

@ -82,7 +82,7 @@ export default function(opts: RoundOpts): void {
.change(round.moveOn.toggle)
.prop('checked', round.moveOn.get())
.on('click', 'a', () => {
li.hasToReload = true;
li.unload.expected = true;
return true;
});
if (location.pathname.lastIndexOf('/round-next/', 0) === 0)

View File

@ -700,7 +700,7 @@ export default class RoundController {
window.addEventListener('beforeunload', e => {
const d = this.data;
if (li.hasToReload ||
if (li.unload.expected ||
this.nvui ||
!game.playable(d) ||
!d.clock ||

View File

@ -17,7 +17,7 @@ export default class MoveOn {
private redirect = (href: string) => {
this.ctrl.setRedirecting();
window.lichess.hasToReload = true;
window.lichess.unload.expected = true;
window.location.href = href;
};

View File

@ -139,7 +139,7 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket {
!isPlayerTurn(ctrl.data)) {
ctrl.setRedirecting();
sound.move();
li.hasToReload = true;
li.unload.expected = true;
location.href = '/' + gameId;
}
},

View File

@ -1,25 +1,30 @@
lichess.announce = (() => {
let timeout;
const kill = () => {
if (timeout) clearTimeout(timeout);
timeout = undefined;
$('#announce').remove();
};
const set = d => {
if (!d) return;
kill();
if (d.msg) {
$('body').append(
'<div id="announce" class="announce">' +
lichess.escapeHtml(d.msg) +
(d.date ? '<time class="timeago" datetime="' + d.date + '"></time>' : '') +
'<div class="actions"><a class="close">X</a></div>' +
'</div>'
).find('#announce .close').click(kill);
timeout = setTimeout(kill, d.date ? new Date(d.date) - Date.now() : 5000);
if (d.date) lichess.pubsub.emit('content_loaded');
}
};
set($('body').data('announce'));
return set;
})();
import { escapeHtml } from './functions';
import pubsub from './pubsub';
let timeout;
const kill = () => {
if (timeout) clearTimeout(timeout);
timeout = undefined;
$('#announce').remove();
};
const announce = (d?: { msg: string, date: Date }) => {
if (!d) return;
kill();
if (d.msg) {
$('body').append(
'<div id="announce" class="announce">' +
escapeHtml(d.msg) +
(d.date ? '<time class="timeago" datetime="' + d.date + '"></time>' : '') +
'<div class="actions"><a class="close">X</a></div>' +
'</div>'
).find('#announce .close').click(kill);
timeout = setTimeout(kill, d.date ? new Date(d.date).getTime() - Date.now() : 5000);
if (d.date) pubsub.emit('content_loaded');
}
};
announce($('body').data('announce'));
export default announce;

View File

@ -1,5 +1,6 @@
import widget from './widget';
import trans from './trans';
import pubsub from './pubsub';
widget("friends", (() => {
const getId = function(titleName) {
@ -45,6 +46,14 @@ widget("friends", (() => {
};
self.trans = trans(data.i18n);
self.set(data);
pubsub.on('socket.in.following_onlines', d => {
d.users = d.d;
self.set(d);
});
['enters', 'leaves', 'playing', 'stopped_playing'].forEach(k =>
pubsub.on('socket.in.following_' + k, self[k])
);
},
repaint: function() {
const self: any = this;

View File

@ -1,7 +1,11 @@
import StrongSocket from './socket';
import { makeChat } from './functions';
import trans from './trans';
export default function moduleLaunchers() {
const li: any = window.lichess;
if (li.analyse) window.LichessAnalyse.boot(li.analyse);
else if (li.user_analysis) startUserAnalysis(li.user_analysis);
else if (li.userAnalysis) startUserAnalysis(li.userAnalysis);
else if (li.study) startStudy(li.study);
else if (li.practice) startPractice(li.practice);
else if (li.relay) startRelay(li.relay);
@ -11,42 +15,25 @@ export default function moduleLaunchers() {
}
function startTournament(cfg) {
var element = document.querySelector('main.tour');
const element = document.querySelector('main.tour') as HTMLElement;
$('body').data('tournament-id', cfg.data.id);
let tournament;
lichess.socket = lichess.StrongSocket(
'/tournament/' + cfg.data.id + '/socket/v5', cfg.data.socketVersion, {
window.lichess.socket = StrongSocket(
`/tournament/${cfg.data.id}/socket/v5`, cfg.data.socketVersion, {
receive: (t, d) => tournament.socketReceive(t, d)
});
cfg.socketSend = lichess.socket.send;
cfg.socketSend = window.lichess.socket.send;
cfg.element = element;
tournament = LichessTournament.start(cfg);
}
function startSimul(cfg) {
cfg.element = document.querySelector('main.simul');
$('body').data('simul-id', cfg.data.id);
var simul;
lichess.socket = lichess.StrongSocket(
'/simul/' + cfg.data.id + '/socket/v5', cfg.socketVersion, {
receive: function(t, d) {
simul.socketReceive(t, d);
}
});
cfg.socketSend = lichess.socket.send;
cfg.$side = $('.simul__side').clone();
simul = LichessSimul(cfg);
tournament = window.LichessTournament.start(cfg);
}
function startTeam(cfg) {
lichess.socket = lichess.StrongSocket('/team/' + cfg.id, cfg.socketVersion);
cfg.chat && lichess.makeChat(cfg.chat);
$('#team-subscribe').on('change', function() {
window.lichess.socket = StrongSocket('/team/' + cfg.id, cfg.socketVersion);
cfg.chat && makeChat(cfg.chat);
$('#team-subscribe').on('change', function(this: HTMLInputElement) {
const v = this.checked;
$(this).parents('form').each(function() {
$.post($(this).attr('action'), {
v: v
});
$(this).parents('form').each(function(this: HTMLElement) {
$.post($(this).attr('action'), { v });
});
});
}
@ -54,56 +41,48 @@ function startTeam(cfg) {
function startUserAnalysis(cfg) {
var analyse;
cfg.initialPly = 'url';
cfg.trans = lichess.trans(cfg.i18n);
lichess.socket = lichess.StrongSocket('/analysis/socket/v5', false, {
receive: function(t, d) {
analyse.socketReceive(t, d);
}
cfg.trans = trans(cfg.i18n);
window.lichess.socket = StrongSocket('/analysis/socket/v5', false, {
receive: (t, d) => analyse.socketReceive(t, d)
});
cfg.socketSend = lichess.socket.send;
cfg.socketSend = window.lichess.socket.send;
cfg.$side = $('.analyse__side').clone();
analyse = LichessAnalyse.start(cfg);
analyse = window.LichessAnalyse.start(cfg);
}
function startStudy(cfg) {
var analyse;
cfg.initialPly = 'url';
lichess.socket = lichess.StrongSocket(cfg.socketUrl, cfg.socketVersion, {
receive: function(t, d) {
analyse.socketReceive(t, d);
}
window.lichess.socket = StrongSocket(cfg.socketUrl, cfg.socketVersion, {
receive: (t, d) => analyse.socketReceive(t, d)
});
cfg.socketSend = lichess.socket.send;
cfg.trans = lichess.trans(cfg.i18n);
analyse = LichessAnalyse.start(cfg);
cfg.socketSend = window.lichess.socket.send;
cfg.trans = trans(cfg.i18n);
analyse = window.LichessAnalyse.start(cfg);
}
function startPractice(cfg) {
var analyse;
cfg.trans = lichess.trans(cfg.i18n);
lichess.socket = lichess.StrongSocket('/analysis/socket/v5', false, {
receive: function(t, d) {
analyse.socketReceive(t, d);
}
cfg.trans = trans(cfg.i18n);
window.lichess.socket = StrongSocket('/analysis/socket/v5', false, {
receive: (t, d) => analyse.socketReceive(t, d)
});
cfg.socketSend = lichess.socket.send;
analyse = LichessAnalyse.start(cfg);
cfg.socketSend = window.lichess.socket.send;
analyse = window.LichessAnalyse.start(cfg);
}
function startRelay(cfg) {
var analyse;
cfg.initialPly = 'url';
lichess.socket = lichess.StrongSocket(cfg.socketUrl, cfg.socketVersion, {
receive: function(t, d) {
analyse.socketReceive(t, d);
}
window.lichess.socket = StrongSocket(cfg.socketUrl, cfg.socketVersion, {
receive: (t, d) => analyse.socketReceive(t, d)
});
cfg.socketSend = lichess.socket.send;
cfg.trans = lichess.trans(cfg.i18n);
analyse = LichessAnalyse.start(cfg);
cfg.socketSend = window.lichess.socket.send;
cfg.trans = trans(cfg.i18n);
analyse = window.LichessAnalyse.start(cfg);
}
function startPuzzle(cfg) {
cfg.element = document.querySelector('main.puzzle');
LichessPuzzle(cfg);
window.LichessPuzzle(cfg);
}

View File

@ -1,92 +1,96 @@
{
let hoverable;
function isHoverable() {
if (typeof hoverable === 'undefined')
hoverable = !lichess.hasTouchEvents /* Firefox <= 63 */ || !!getComputedStyle(document.body).getPropertyValue('--hoverable');
return hoverable;
}
import { hasTouchEvents, spinnerHtml } from './intro';
import pubsub from './pubsub';
import { requestIdleCallback } from './functions';
const containedIn = (el, container) => container && container.contains(el);
let hoverable: boolean;
function isHoverable() {
if (typeof hoverable === 'undefined')
hoverable = !hasTouchEvents /* Firefox <= 63 */ || !!getComputedStyle(document.body).getPropertyValue('--hoverable');
return hoverable;
}
const inCrosstable = el => containedIn(el, document.querySelector('.crosstable'));
const inCrosstable = (el: HTMLElement) =>
document.querySelector('.crosstable')?.contains(el);
function onPowertipPreRender(id, preload) {
return function() {
const url = ($(this).data('href') || $(this).attr('href')).replace(/\?.+$/, '');
if (preload) preload(url);
$.ajax({
url: url + '/mini',
success: function(html) {
$('#' + id).html(html);
lichess.pubsub.emit('content_loaded');
}
});
};
};
const uptA = (url, icon) => '<a class="btn-rack__btn" href="' + url + '" data-icon="' + icon + '"></a>';
const userPowertip = (el, pos) => {
pos = pos || el.getAttribute('data-pt-pos') || (
inCrosstable(el) ? 'n' : 's'
);
$(el).removeClass('ulpt').powerTip({
intentPollInterval: 200,
placement: pos,
smartPlacement: true,
closeDelay: 200
}).data('powertip', ' ').on({
powerTipRender: onPowertipPreRender('powerTip', (url) => {
const u = url.substr(3);
const name = $(el).data('name') || $(el).html();
$('#powerTip').html('<div class="upt__info"><div class="upt__info__top"><span class="user-link offline">' + name + '</span></div></div><div class="upt__actions btn-rack">' +
uptA('/@/' + u + '/tv', '1') +
uptA('/inbox/new?user=' + u, 'c') +
uptA('/?user=' + u + '#friend', 'U') +
'<a class="btn-rack__btn relation-button" disabled></a></div>');
})
function onPowertipPreRender(id: string, preload?: (url: string) => void) {
return function(this: HTMLElement) {
const url = ($(this).data('href') || $(this).attr('href')).replace(/\?.+$/, '');
if (preload) preload(url);
$.ajax({
url: url + '/mini',
success(html) {
$('#' + id).html(html);
pubsub.emit('content_loaded');
}
});
};
};
function gamePowertip(el) {
$(el).removeClass('glpt').powerTip({
intentPollInterval: 200,
placement: inCrosstable(el) ? 'n' : 'w',
smartPlacement: true,
closeDelay: 200,
popupId: 'miniGame'
}).on({
powerTipPreRender: onPowertipPreRender('miniGame')
}).data('powertip', lichess.spinnerHtml);
};
const uptA = (url: string, icon: string) =>
`<a class="btn-rack__btn" href="${url}" data-icon="${icon}"></a>`;
function powerTipWith(el, ev, f) {
if (lichess.isHoverable()) {
f(el);
$.powerTip.show(el, ev);
}
};
const userPowertip = (el: HTMLElement, pos?: PowerTip.Placement) => {
pos = pos || (el.getAttribute('data-pt-pos') as PowerTip.Placement) || (
inCrosstable(el) ? 'n' : 's'
);
$(el).removeClass('ulpt').powerTip({
intentPollInterval: 200,
placement: pos,
smartPlacement: true,
closeDelay: 200
}).data('powertip', ' ').on({
powerTipRender: onPowertipPreRender('powerTip', (url: string) => {
const u = url.substr(3);
const name = $(el).data('name') || $(el).html();
$('#powerTip').html('<div class="upt__info"><div class="upt__info__top"><span class="user-link offline">' + name + '</span></div></div><div class="upt__actions btn-rack">' +
uptA('/@/' + u + '/tv', '1') +
uptA('/inbox/new?user=' + u, 'c') +
uptA('/?user=' + u + '#friend', 'U') +
'<a class="btn-rack__btn relation-button" disabled></a></div>');
})
});
};
function onIdleForAll(par, sel, fun) {
lichess.requestIdleCallback(() =>
Array.prototype.forEach.call(par.querySelectorAll(sel), el => fun(el)) // do not codegolf to `fun`
)
function gamePowertip(el: HTMLElement) {
$(el).removeClass('glpt').powerTip({
intentPollInterval: 200,
placement: inCrosstable(el) ? 'n' : 'w',
smartPlacement: true,
closeDelay: 200,
popupId: 'miniGame'
}).on({
powerTipPreRender: onPowertipPreRender('miniGame')
}).data('powertip', spinnerHtml);
};
function powerTipWith(el, ev, f) {
if (isHoverable()) {
f(el);
$.powerTip.show(el, ev);
}
};
lichess.powertip = {
mouseover(e) {
var t = e.target,
cl = t.classList;
if (cl.contains('ulpt')) powerTipWith(t, e, userPowertip);
else if (cl.contains('glpt')) powerTipWith(t, e, gamePowertip);
},
manualGameIn(parent) {
onIdleForAll(parent, '.glpt', gamePowertip);
},
manualGame: gamePowertip,
manualUser: userPowertip,
manualUserIn(parent) {
onIdleForAll(parent, '.ulpt', userPowertip);
}
};
function onIdleForAll(par, sel, fun) {
requestIdleCallback(() =>
Array.prototype.forEach.call(par.querySelectorAll(sel), el => fun(el)) // do not codegolf to `fun`
)
}
const powertip = {
mouseover(e) {
var t = e.target,
cl = t.classList;
if (cl.contains('ulpt')) powerTipWith(t, e, userPowertip);
else if (cl.contains('glpt')) powerTipWith(t, e, gamePowertip);
},
manualGameIn(parent) {
onIdleForAll(parent, '.glpt', gamePowertip);
},
manualGame: gamePowertip,
manualUser: userPowertip,
manualUserIn(parent) {
onIdleForAll(parent, '.ulpt', userPowertip);
}
};
export default powertip;

View File

@ -1,4 +1,3 @@
export let hasToReload = false;
export let redirectInProgress: false | string = false;
export const redirect = obj => {
@ -22,9 +21,13 @@ export const redirect = obj => {
location.href = href;
};
export const unload = {
expected: false
}
export const reload = () => {
if (redirectInProgress) return;
hasToReload = true;
unload.expected = true;
window.lichess.socket?.disconnect();
if (location.hash) location.reload();
else location.href = location.href;

View File

@ -1,19 +1,22 @@
lichess.serviceWorker = () => {
import {assetUrl, jsModule} from "./assets";
import {storage} from "./storage";
export default function() {
if ('serviceWorker' in navigator && 'Notification' in window && 'PushManager' in window) {
const workerUrl = new URL(lichess.assetUrl(lichess.jsModule('serviceWorker'), {
const workerUrl = new URL(assetUrl(jsModule('serviceWorker'), {
sameDomain: true
}), self.location.href);
workerUrl.searchParams.set('asset-url', document.body.getAttribute('data-asset-url'));
workerUrl.searchParams.set('asset-url', document.body.getAttribute('data-asset-url')!);
if (document.body.getAttribute('data-dev')) workerUrl.searchParams.set('dev', '1');
const updateViaCache = document.body.getAttribute('data-dev') ? 'none' : 'all';
navigator.serviceWorker.register(workerUrl.href, {
scope: '/',
updateViaCache
}).then(reg => {
const storage = lichess.storage.make('push-subscribed');
const store = storage.make('push-subscribed');
const vapid = document.body.getAttribute('data-vapid');
if (vapid && Notification.permission == 'granted') return reg.pushManager.getSubscription().then(sub => {
const resub = parseInt(storage.get() || '0', 10) + 43200000 < Date.now(); // 12 hours
const resub = parseInt(store.get() || '0', 10) + 43200000 < Date.now(); // 12 hours
const applicationServerKey = Uint8Array.from(atob(vapid), c => c.charCodeAt(0));
if (!sub || resub) {
return reg.pushManager.subscribe({
@ -26,15 +29,15 @@ lichess.serviceWorker = () => {
},
body: JSON.stringify(sub)
}).then(res => {
if (res.ok) storage.set('' + Date.now());
else console.log('submitting push subscription failed', response.statusText);
if (res.ok) store.set('' + Date.now());
else console.log('submitting push subscription failed', res.statusText);
}), err => {
console.log('push subscribe failed', err.message);
if (sub) sub.unsubscribe();
});
}
});
else storage.remove();
else store.remove();
});
}
}

View File

@ -1,48 +0,0 @@
lichess.setSocketDefaults = $friendsBox =>
$.extend(true, lichess.StrongSocket.defaults, {
events: {
following_onlines(_, d) {
d.users = d.d;
$friendsBox.friends("set", d);
},
following_enters(_, d) {
$friendsBox.friends('enters', d);
},
following_leaves(name) {
$friendsBox.friends('leaves', name);
},
following_playing(name) {
$friendsBox.friends('playing', name);
},
following_stopped_playing(name) {
$friendsBox.friends('stopped_playing', name);
},
redirect(o) {
setTimeout(() => {
lichess.hasToReload = true;
lichess.redirect(o);
}, 200);
},
tournamentReminder(data) {
if ($('#announce').length || $('body').data("tournament-id") == data.id) return;
const url = '/tournament/' + data.id;
$('body').append(
'<div id="announce">' +
'<a data-icon="g" class="text" href="' + url + '">' + data.name + '</a>' +
'<div class="actions">' +
'<a class="withdraw text" href="' + url + '/withdraw" data-icon="Z">Pause</a>' +
'<a class="text" href="' + url + '" data-icon="G">Resume</a>' +
'</div></div>'
).find('#announce .withdraw').click(function() {
$.post($(this).attr("href"));
$('#announce').remove();
return false;
});
},
announce: lichess.announce
},
params: {},
options: {
isAuth: !!$('body').data('user')
}
});

View File

@ -44,7 +44,7 @@ function makeAckable(send) {
}
// versioned events, acks, retries, resync
const StrongSocket = function(url, version, settings) {
const StrongSocket = function(url: string, version: number | false, settings?: any) {
var settings = $.extend(true, {}, StrongSocket.defaults, settings);
var options = settings.options;
@ -175,12 +175,12 @@ const StrongSocket = function(url, version, settings) {
};
var handle = function(m) {
if (m.v) {
if (m.v && version !== false) {
if (m.v <= version) {
debug("already has event " + m.v);
return;
}
// it's impossible but according to previous login, it happens nonetheless
// it's impossible but according to previous logging, it happens nonetheless
if (m.v > version + 1) return reload();
version = m.v;
}
@ -281,7 +281,8 @@ StrongSocket.defaults = {
pingMaxLag: 9000, // time to wait for pong before reseting the connection
pingDelay: 2500, // time between pong and ping
autoReconnectDelay: 3500,
protocol: location.protocol === 'https:' ? 'wss:' : 'ws:'
protocol: location.protocol === 'https:' ? 'wss:' : 'ws:',
isAuth: document.body.hasAttribute('user')
}
};

View File

@ -1,4 +1,7 @@
import { storage } from './storage';
import pubsub from './pubsub';
import soundBox from './soundbox';
import { soundUrl } from './assets';
const api: any = {};
@ -46,14 +49,14 @@ const volumes = {
explode: 0.35,
confirmation: 0.5
};
api.collection = new memoize(k => {
api.collection = memoize(k => {
let set = soundSet;
if (set === 'music' || speechStorage.get()) {
if (['move', 'capture', 'check'].includes(k)) return $.noop;
set = 'standard';
}
lichess.soundBox.loadOggOrMp3(k, `${lichess.soundUrl}/${set}/${names[k]}`);
return () => lichess.soundBox.play(k, volumes[k] || 1);
soundBox.loadOggOrMp3(k, `${soundUrl}/${set}/${names[k]}`);
return () => soundBox.play(k, volumes[k] || 1);
});
const enabled = () => soundSet !== 'silent';
api.load = (name, file) => {
@ -66,7 +69,7 @@ Object.keys(names).forEach(api.load);
api.say = (text, cut, force) => {
if (!speechStorage.get() && !force) return false;
const msg = text.text ? text : new SpeechSynthesisUtterance(text);
msg.volume = lichess.soundBox.getVolume();
msg.volume = soundBox.getVolume();
msg.lang = 'en-US';
if (cut) speechSynthesis.cancel();
speechSynthesis.speak(msg);
@ -74,7 +77,8 @@ api.say = (text, cut, force) => {
return true;
};
const publish = () => lichess.pubsub.emit('sound_set', soundSet);
const publish = () => pubsub.emit('sound_set', soundSet);
if (soundSet == 'music') setTimeout(publish, 500);
api.changeSet = s => {

View File

@ -1,20 +1,22 @@
import { storage } from './storage';
class SoundBox {
sounds = {}; // The loaded sounds and their instances
sounds = new Map(); // The loaded sounds and their instances
volume = lichess.storage.make('sound-volume');
volume = storage.make('sound-volume');
loadOggOrMp3 = (name, path) =>
this.sounds[name] = new Howl({
loadOggOrMp3 = (name: string, path: string) =>
this.sounds.set(name, new window.Howl({
src: ['ogg', 'mp3'].map(ext => `${path}.${ext}`)
});
}));
play(name, volume = 1) {
play(name: string, volume: number = 1) {
const doPlay = () => {
this.sounds[name].volume(volume * this.getVolume());
this.sounds[name].play();
this.sounds.get(name).volume(volume * this.getVolume());
this.sounds.get(name).play();
};
if (Howler.ctx.state == "suspended") Howler.ctx.resume().then(doPlay);
if (window.Howler.ctx.state == "suspended") window.Howler.ctx.resume().then(doPlay);
else doPlay();
};
@ -26,4 +28,7 @@ class SoundBox {
return v >= 0 ? v : 0.7;
}
}
lichess.soundBox = new SoundBox
const soundBox = new SoundBox;
export default soundBox;

View File

@ -1,86 +1,85 @@
/** based on https://github.com/hustcc/timeago.js Copyright (c) 2016 hustcc License: MIT **/
lichess.timeago = (function() {
// divisors for minutes, hours, days, weeks, months, years
const DIVS = [60,
60 * 60,
60 * 60 * 24,
60 * 60 * 24 * 7,
60 * 60 * 2 * 365, // 24/12 = 2
60 * 60 * 24 * 365];
const LIMITS = [...DIVS];
LIMITS[2] *= 2; // Show hours up to 2 days.
// divisors for minutes, hours, days, weeks, months, years
const DIVS = [60,
60 * 60,
60 * 60 * 24,
60 * 60 * 24 * 7,
60 * 60 * 2 * 365, // 24/12 = 2
60 * 60 * 24 * 365];
// format Date / string / timestamp to Date instance.
function toDate(input: any): Date {
return input instanceof Date ? input : (
new Date(isNaN(input) ? input : parseInt(input))
);
}
const LIMITS = [...DIVS];
LIMITS[2] *= 2; // Show hours up to 2 days.
// format Date / string / timestamp to Date instance.
function toDate(input) {
return input instanceof Date ? input : (
new Date(isNaN(input) ? input : parseInt(input))
);
// format the diff second to *** time ago
function formatDiff(diff) {
let agoin = 0;
if (diff < 0) {
agoin = 1;
diff = -diff;
}
const totalSec = diff;
// format the diff second to *** time ago
function formatDiff(diff) {
let agoin = 0;
if (diff < 0) {
agoin = 1;
diff = -diff;
}
var total_sec = diff;
let i = 0;
for (; i < 6 && diff >= LIMITS[i]; i++);
if (i > 0) diff /= DIVS[i - 1];
let i = 0;
for (;i < 6 && diff >= LIMITS[i]; i++);
if (i > 0) diff /= DIVS[i-1];
diff = Math.floor(diff);
i *= 2;
diff = Math.floor(diff);
i *= 2;
if (diff > (i === 0 ? 9 : 1)) i += 1;
return window.lichess.timeagoLocale(diff, i, totalSec)[agoin].replace('%s', diff);
}
if (diff > (i === 0 ? 9 : 1)) i += 1;
return lichess.timeagoLocale(diff, i, total_sec)[agoin].replace('%s', diff);
}
let formatterInst;
var formatterInst;
function formatter() {
return formatterInst = formatterInst || (window.Intl && Intl.DateTimeFormat ?
function formatter() {
return formatterInst = formatterInst || (
window.Intl && Intl.DateTimeFormat ?
new Intl.DateTimeFormat(document.documentElement.lang, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric'
}).format : function(d) { return d.toLocaleString(); })
}
}).format : d => d.toLocaleString()
)
}
return {
render: function(nodes) {
var cl, abs, set, str, diff, now = Date.now();
nodes.forEach(function(node) {
cl = node.classList,
const timeago = {
render: function(nodes) {
var cl, abs, set, str, diff, now = Date.now();
nodes.forEach(function(node) {
cl = node.classList,
abs = cl.contains('abs'),
set = cl.contains('set');
node.date = node.date || toDate(node.getAttribute('datetime'));
if (!set) {
str = formatter()(node.date);
if (abs) node.textContent = str;
else node.setAttribute('title', str);
cl.add('set');
if (abs || cl.contains('once')) cl.remove('timeago');
}
if (!abs) {
diff = (now - node.date) / 1000;
node.textContent = formatDiff(diff);
if (Math.abs(diff) > 9999) cl.remove('timeago'); // ~3h
}
});
},
// relative
format: function(date) {
return formatDiff((Date.now() - toDate(date)) / 1000);
},
absolute: function(date) {
return formatter()(toDate(date));
}
};
})();
node.date = node.date || toDate(node.getAttribute('datetime'));
if (!set) {
str = formatter()(node.date);
if (abs) node.textContent = str;
else node.setAttribute('title', str);
cl.add('set');
if (abs || cl.contains('once')) cl.remove('timeago');
}
if (!abs) {
diff = (now - node.date) / 1000;
node.textContent = formatDiff(diff);
if (Math.abs(diff) > 9999) cl.remove('timeago'); // ~3h
}
});
},
// relative
format: function(date) {
return formatDiff((Date.now() - toDate(date).getTime()) / 1000);
},
absolute: function(date) {
return formatter()(toDate(date));
}
};
export default timeago;

View File

@ -1,19 +1,21 @@
import pubsub from './pubsub';
import { spinnerHtml } from './intro';
import { loadCssPath, loadScript, jsModule } from './assets';
lichess.topBar = () => {
export default function() {
const initiatingHtml = '<div class="initiating">' + lichess.spinnerHtml + '</div>';
const initiatingHtml = '<div class="initiating">' + spinnerHtml + '</div>';
$('#topnav-toggle').on('change', e => {
document.body.classList.toggle('masked', e.target.checked);
document.body.classList.toggle('masked', (e.target as HTMLInputElement).checked);
});
$('#top').on('click', 'a.toggle', function() {
$('#top').on('click', 'a.toggle', function(this: HTMLElement) {
var $p = $(this).parent();
$p.toggleClass('shown');
$p.siblings('.shown').removeClass('shown');
lichess.pubsub.emit('top.toggle.' + $(this).attr('id'));
setTimeout(function() {
pubsub.emit('top.toggle.' + $(this).attr('id'));
setTimeout(() => {
const handler = function(e) {
if ($.contains($p[0], e.target)) return;
$p.removeClass('shown');
@ -28,13 +30,13 @@ lichess.topBar = () => {
let instance, booted;
const $toggle = $('#challenge-toggle');
$toggle.one('mouseover click', () => load());
const load = function(data) {
const load = function(data?: any) {
if (booted) return;
booted = true;
const $el = $('#challenge-app').html(initiatingHtml);
lichess.loadCssPath('challenge');
lichess.loadScript(lichess.jsModule('challenge')).done(function() {
instance = LichessChallenge($el[0], {
loadCssPath('challenge');
loadScript(jsModule('challenge')).done(function() {
instance = window.LichessChallenge($el[0], {
data: data,
show() {
if (!$('#challenge-app').is(':visible')) $toggle.click();
@ -60,13 +62,13 @@ lichess.topBar = () => {
const $toggle = $('#notify-toggle'),
isVisible = () => $('#notify-app').is(':visible');
const load = function(data, incoming) {
const load = (data?: any, incoming = false) => {
if (booted) return;
booted = true;
var $el = $('#notify-app').html(initiatingHtml);
lichess.loadCssPath('notify');
lichess.loadScript(lichess.jsModule('notify')).done(() => {
instance = LichessNotify($el.empty()[0], {
loadCssPath('notify');
loadScript(jsModule('notify')).done(() => {
instance = window.LichessNotify($el.empty()[0], {
data: data,
incoming: incoming,
isVisible: isVisible,
@ -77,7 +79,7 @@ lichess.topBar = () => {
if (!isVisible()) $toggle.click();
},
setNotified() {
lichess.socket.send('notified');
window.lichess.socket.send('notified');
},
pulse() {
$toggle.addClass('pulse');
@ -97,12 +99,10 @@ lichess.topBar = () => {
if (!instance) load(data, true);
else instance.update(data, true);
});
lichess.notifyApp = {
setMsgRead(user) {
if (!instance) load();
else instance.setMsgRead(user);
}
};
pubsub.on('notify-app.set-read', user => {
if (!instance) load();
else instance.setMsgRead(user);
});
}
{ // dasher
@ -112,9 +112,9 @@ lichess.topBar = () => {
booted = true;
const $el = $('#dasher_app').html(initiatingHtml),
playing = $('body').hasClass('playing');
lichess.loadCssPath('dasher');
lichess.loadScript(lichess.jsModule('dasher')).done(() =>
LichessDasher($el.empty()[0], {
loadCssPath('dasher');
loadScript(jsModule('dasher')).done(() =>
window.LichessDasher($el.empty()[0], {
playing
})
);
@ -129,8 +129,8 @@ lichess.topBar = () => {
const boot = () => {
if (booted) return;
booted = true;
lichess.loadScript(lichess.jsModule('cli')).done(() =>
LichessCli.app($wrap, toggle)
loadScript(jsModule('cli')).done(() =>
window.LichessCli.app($wrap, toggle)
);
};
const toggle = () => {
@ -139,12 +139,12 @@ lichess.topBar = () => {
if ($('body').hasClass('clinput')) $input.focus();
};
$wrap.find('a').on('mouseover click', e => (e.type === 'mouseover' ? boot : toggle)());
Mousetrap.bind('/', () => {
window.Mousetrap.bind('/', () => {
$input.val('/');
requestAnimationFrame(() => toggle());
return false;
});
Mousetrap.bind('s', () => requestAnimationFrame(() => toggle()));
window.Mousetrap.bind('s', () => requestAnimationFrame(() => toggle()));
if ($('body').hasClass('blind-mode')) $input.one('focus', () => toggle());
}
}

View File

@ -22,7 +22,6 @@ import './component/trans';
import './component/user-autocomplete';
import './component/infinite-scroll';
import './component/socket';
import './component/socket-defaults';
import './component/top-bar';
import './component/service-worker';
import './component/module-launchers';

View File

@ -1,160 +1,190 @@
const $friendsBox = $('#friend_box');
import StrongSocket from "./component/socket";
import { unload, redirect } from "./component/reload";
import announce from './component/announce';
import moduleLaunchers from "./component/module-launchers";
import pubsub from "./component/pubsub";
import miniBoard from "./component/mini-board";
import miniGame from "./component/mini-game";
import {requestIdleCallback} from "./component/functions";
lichess.setSocketDefaults($friendsBox);
StrongSocket.defaults.events = {
redirect(o) {
setTimeout(() => {
unload.expected = true;
redirect(o);
}, 200);
},
tournamentReminder(data) {
if ($('#announce').length || $('body').data("tournament-id") == data.id) return;
const url = '/tournament/' + data.id;
$('body').append(
'<div id="announce">' +
'<a data-icon="g" class="text" href="' + url + '">' + data.name + '</a>' +
'<div class="actions">' +
'<a class="withdraw text" href="' + url + '/withdraw" data-icon="Z">Pause</a>' +
'<a class="text" href="' + url + '" data-icon="G">Resume</a>' +
'</div></div>'
).find('#announce .withdraw').click(function(this: HTMLElement) {
$.post($(this).attr("href"));
$('#announce').remove();
return false;
});
},
announce
};
$(() => {
lichess.moduleLaunchers();
moduleLaunchers();
lichess.pubsub.on('content_loaded', lichess.miniBoard.initAll);
lichess.pubsub.on('content_loaded', lichess.miniGame.initAll);
pubsub.on('content_loaded', miniBoard.initAll);
pubsub.on('content_loaded', miniGame.initAll);
lichess.pubsub.on('socket.in.fen', e =>
document.querySelectorAll('.mini-game-' + e.id).forEach((el: HTMLElement) => lichess.miniGame.update(el, e))
pubsub.on('socket.in.fen', e =>
document.querySelectorAll('.mini-game-' + e.id).forEach((el: HTMLElement) => miniGame.update(el, e))
);
lichess.pubsub.on('socket.in.finish', e =>
pubsub.on('socket.in.finish', e =>
document.querySelectorAll('.mini-game-' + e.id).forEach((el: HTMLElement) => miniGame.finish(el, e.win))
);
}
lichess.requestIdleCallback(() => {
requestIdleCallback(() => {
$friendsBox.friends();
$('#friend_box').friends();
$('#main-wrap')
.on('click', '.autoselect', function() {
$(this).select();
})
.on('click', 'button.copy', function() {
$('#' + $(this).data('rel')).select();
document.execCommand('copy');
$(this).attr('data-icon', 'E');
$('#main-wrap')
.on('click', '.autoselect', function() {
$(this).select();
})
.on('click', 'button.copy', function() {
$('#' + $(this).data('rel')).select();
document.execCommand('copy');
$(this).attr('data-icon', 'E');
});
$('body').on('click', 'a.relation-button', function() {
var $a = $(this).addClass('processing').css('opacity', 0.3);
$.ajax({
url: $a.attr('href'),
type: 'post',
success: function(html) {
if (html.includes('relation-actions')) $a.parent().replaceWith(html);
else $a.replaceWith(html);
}
});
return false;
});
$('body').on('click', 'a.relation-button', function() {
var $a = $(this).addClass('processing').css('opacity', 0.3);
$.ajax({
url: $a.attr('href'),
type: 'post',
success: function(html) {
if (html.includes('relation-actions')) $a.parent().replaceWith(html);
else $a.replaceWith(html);
$('.mselect .button').on('click', function() {
const $p = $(this).parent();
$p.toggleClass('shown');
setTimeout(function() {
const handler = function(e) {
if ($.contains($p[0], e.target)) return;
$p.removeClass('shown');
$('html').off('click', handler);
};
$('html').on('click', handler);
}, 10);
});
document.body.addEventListener('mouseover', lichess.powertip.mouseover);
{ // timeago
const renderTimeago = () =>
requestAnimationFrame(() =>
lichess.timeago.render([].slice.call(document.getElementsByClassName('timeago'), 0, 99))
);
const setTimeago = interval => {
renderTimeago();
setTimeout(() => setTimeago(interval * 1.1), interval);
}
});
return false;
});
setTimeago(1200);
lichess.pubsub.on('content_loaded', renderTimeago);
}
$('.mselect .button').on('click', function() {
const $p = $(this).parent();
$p.toggleClass('shown');
setTimeout(function() {
const handler = function(e) {
if ($.contains($p[0], e.target)) return;
$p.removeClass('shown');
$('html').off('click', handler);
if (!window.customWS) setTimeout(() => {
if (!lichess.socket)
lichess.socket = lichess.StrongSocket("/socket/v5", false);
}, 300);
lichess.topBar();
window.addEventListener('resize', () => lichess.dispatchEvent(document.body, 'chessground.resize'));
$('.user-autocomplete').each(function() {
const opts = {
focus: 1,
friend: $(this).data('friend'),
tag: $(this).data('tag')
};
$('html').on('click', handler);
}, 10);
});
document.body.addEventListener('mouseover', lichess.powertip.mouseover);
{ // timeago
const renderTimeago = () =>
requestAnimationFrame(() =>
lichess.timeago.render([].slice.call(document.getElementsByClassName('timeago'), 0, 99))
);
const setTimeago = interval => {
renderTimeago();
setTimeout(() => setTimeago(interval * 1.1), interval);
}
setTimeago(1200);
lichess.pubsub.on('content_loaded', renderTimeago);
}
if (!window.customWS) setTimeout(() => {
if (!lichess.socket)
lichess.socket = lichess.StrongSocket("/socket/v5", false);
}, 300);
lichess.topBar();
window.addEventListener('resize', () => lichess.dispatchEvent(document.body, 'chessground.resize'));
$('.user-autocomplete').each(function() {
const opts = {
focus: 1,
friend: $(this).data('friend'),
tag: $(this).data('tag')
};
if ($(this).attr('autofocus')) lichess.userAutocomplete($(this), opts);
else $(this).one('focus', function() {
lichess.userAutocomplete($(this), opts);
if ($(this).attr('autofocus')) lichess.userAutocomplete($(this), opts);
else $(this).one('focus', function() {
lichess.userAutocomplete($(this), opts);
});
});
});
lichess.loadInfiniteScroll('.infinitescroll');
lichess.loadInfiniteScroll('.infinitescroll');
$('a.delete, input.delete').click(() => confirm('Delete?'));
$('input.confirm, button.confirm').click(function() {
return confirm($(this).attr('title') || 'Confirm this action?');
});
$('a.delete, input.delete').click(() => confirm('Delete?'));
$('input.confirm, button.confirm').click(function() {
return confirm($(this).attr('title') || 'Confirm this action?');
});
$('#main-wrap').on('click', 'a.bookmark', function() {
const t = $(this).toggleClass("bookmarked");
$.post(t.attr("href"));
const count = (parseInt(t.text(), 10) || 0) + (t.hasClass("bookmarked") ? 1 : -1);
t.find('span').html(count > 0 ? count : "");
return false;
});
$('#main-wrap').on('click', 'a.bookmark', function() {
const t = $(this).toggleClass("bookmarked");
$.post(t.attr("href"));
const count = (parseInt(t.text(), 10) || 0) + (t.hasClass("bookmarked") ? 1 : -1);
t.find('span').html(count > 0 ? count : "");
return false;
});
// still bind esc even in form fields
Mousetrap.prototype.stopCallback = function(e, el, combo) {
return combo != 'esc' && (el.isContentEditable || el.tagName == 'INPUT' || el.tagName == 'SELECT' || el.tagName == 'TEXTAREA');
};
Mousetrap.bind('esc', function() {
var $oc = $('#modal-wrap .close');
if ($oc.length) $oc.trigger('click');
else {
var $input = $(':focus');
if ($input.length) $input.trigger('blur');
// still bind esc even in form fields
Mousetrap.prototype.stopCallback = function(e, el, combo) {
return combo != 'esc' && (el.isContentEditable || el.tagName == 'INPUT' || el.tagName == 'SELECT' || el.tagName == 'TEXTAREA');
};
Mousetrap.bind('esc', function() {
var $oc = $('#modal-wrap .close');
if ($oc.length) $oc.trigger('click');
else {
var $input = $(':focus');
if ($input.length) $input.trigger('blur');
}
return false;
});
if (!lichess.storage.get('grid')) setTimeout(() => {
if (getComputedStyle(document.body).getPropertyValue('--grid'))
lichess.storage.set('grid', 1);
else
$.get(lichess.assetUrl('oops/browser.html'), html => $('body').prepend(html))
}, 3000);
/* A disgusting hack for a disgusting browser
* Edge randomly fails to rasterize SVG on page load
* A different SVG must be loaded so a new image can be rasterized */
if (navigator.userAgent.includes('Edge/')) setTimeout(() => {
const sprite = $('#piece-sprite');
sprite.attr('href', sprite.attr('href').replace('.css', '.external.css'));
}, 1000);
// prevent zoom when keyboard shows on iOS
if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) {
const el = document.querySelector('meta[name=viewport]');
el.setAttribute('content', el.getAttribute('content') + ',maximum-scale=1.0');
}
return false;
lichess.miniBoard.initAll();
lichess.miniGame.initAll();
$('.chat__members').watchers();
if (location.hash === '#blind' && !$('body').hasClass('blind-mode'))
$.post('/toggle-blind-mode', {
enable: 1,
redirect: '/'
}, lichess.reload);
lichess.serviceWorker();
});
if (!lichess.storage.get('grid')) setTimeout(() => {
if (getComputedStyle(document.body).getPropertyValue('--grid'))
lichess.storage.set('grid', 1);
else
$.get(lichess.assetUrl('oops/browser.html'), html => $('body').prepend(html))
}, 3000);
/* A disgusting hack for a disgusting browser
* Edge randomly fails to rasterize SVG on page load
* A different SVG must be loaded so a new image can be rasterized */
if (navigator.userAgent.includes('Edge/')) setTimeout(() => {
const sprite = $('#piece-sprite');
sprite.attr('href', sprite.attr('href').replace('.css', '.external.css'));
}, 1000);
// prevent zoom when keyboard shows on iOS
if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) {
const el = document.querySelector('meta[name=viewport]');
el.setAttribute('content', el.getAttribute('content') + ',maximum-scale=1.0');
}
lichess.miniBoard.initAll();
lichess.miniGame.initAll();
$('.chat__members').watchers();
if (location.hash === '#blind' && !$('body').hasClass('blind-mode'))
$.post('/toggle-blind-mode', {
enable: 1,
redirect: '/'
}, lichess.reload);
lichess.serviceWorker();
});
});

View File

@ -3,6 +3,7 @@
"include": ["src/*.ts", "src/*.js"],
"compilerOptions": {
"allowJs": true,
"noImplicitAny": false
"noImplicitAny": false,
"noImplicitReturns": false
}
}