ui/simul typescript/snabbdom WIP

pull/7157/head
Thibault Duplessis 2020-08-20 09:51:00 +02:00
parent dd298318a9
commit 858e78f6de
15 changed files with 230 additions and 153 deletions

View File

@ -23,7 +23,7 @@ object show {
title = sim.fullName,
moreJs = frag(
jsModule("simul"),
embedJsUnsafe(s"""lichess.simul=${safeJsonValue(
embedJsUnsafe(s"""LichessSimul.start(${safeJsonValue(
Json.obj(
"data" -> data,
"i18n" -> bits.jsI18n(),
@ -39,7 +39,7 @@ object show {
)
}
)
)}""")
)})""")
)
) {
main(

View File

@ -104,9 +104,8 @@ final class JsonView(
getLightUser(player.user) map { light =>
Json
.obj(
"id" -> player.user,
"variant" -> player.variant.key,
"rating" -> player.rating
"id" -> player.user,
"rating" -> player.rating
)
.add("name" -> light.map(_.name))
.add("title" -> light.map(_.title))
@ -118,6 +117,7 @@ final class JsonView(
playerJson(app.player) map { player =>
Json.obj(
"player" -> player,
"variant" -> app.player.variant.key,
"accepted" -> app.accepted
)
}
@ -146,11 +146,10 @@ final class JsonView(
playerJson(p.player) map { player =>
Json
.obj(
"player" -> player,
"hostColor" -> p.hostColor,
"winnerColor" -> p.winnerColor,
"wins" -> p.wins, // can't be normalized because BC
"game" -> gameJson(hostId, game)
"player" -> player,
"variant" -> p.player.variant.key,
"hostColor" -> p.hostColor,
"game" -> gameJson(hostId, game)
)
.some
}

View File

@ -3,7 +3,6 @@
"version": "2.0.0",
"private": true,
"description": "lichess.org simul",
"main": "src/main.js",
"keywords": [
"chess",
"lichess",
@ -12,12 +11,16 @@
"author": "Thibault Duplessis",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@build/rollupProject": "2.0.0"
"@build/rollupProject": "2.0.0",
"@types/jquery": "^2.0",
"@types/lichess": "2.0.0"
},
"dependencies": {
"chat": "2.0.0",
"chessground": "^7.9.1",
"common": "2.0.0",
"game": "2.0.0",
"mithril": "github:ornicar/mithril.js#lila-1"
"snabbdom": "^0.7.4"
},
"scripts": {
"dev": "rollup --config",

View File

@ -1,38 +0,0 @@
var socket = require('./socket');
var simul = require('./simul');
var xhr = require('./xhr');
module.exports = function(env) {
this.env = env;
this.data = env.data;
this.userId = env.userId;
this.socket = new socket(env.socketSend, this);
this.reload = function(data) {
if (this.data.isCreated && !data.isCreated) {
// hack to change parent class - remove me when moving to snabbdom
$('main.simul-created').removeClass('simul-created');
}
data.team = this.data.simul; // reload data does not contain the simul anymore
this.data = data;
}.bind(this);
if (simul.createdByMe(this) && this.data.isCreated)
lichess.storage.set('lichess.move_on', '1'); // hideous hack :D
this.trans = lichess.trans(env.i18n);
this.teamBlock = this.data.team && !this.data.team.isIn;
this.hostPing = () => {
if (simul.createdByMe(this) && this.data.isCreated) {
xhr.ping(this);
setTimeout(this.hostPing, 10000);
}
};
this.hostPing();
};

View File

@ -0,0 +1,52 @@
import { makeSocket, SimulSocket } from './socket';
import xhr from './xhr';
import simul from './simul';
import * as status from 'game/status';
import {
SimulData,
SimulOpts
} from './interfaces';
export default class SimulCtrl {
data: SimulData;
trans: Trans;
socket: SimulSocket;
constructor(readonly opts: SimulOpts, readonly redraw: () => void) {
this.trans = window.lichess.trans(opts.i18n);
this.socket = makeSocket(opts.socketSend, this);
if (simul.createdByMe(this) && this.data.isCreated)
window.lichess.storage.set('lichess.move_on', '1'); // hideous hack :D
this.hostPing();
}
reload = (data: SimulData) => {
this.data = {
...data,
team: this.data.team // reload data does not contain the team anymore
}
};
teamBlock = () => this.data.team && !this.data.team.isIn;
hostPing = () => {
if (this.createdByMe() && this.data.isCreated) {
xhr.ping(this);
setTimeout(this.hostPing, 10000);
}
}
createdByMe = () => this.opts.userId === this.data.host.id;
candidates = () => this.data.applicants.filter(a => !a.accepted);
accepted = () => this.data.applicants.filter(a => a.accepted);
myCurrentPairing = () =>
this.opts.userId ? this.data.pairings.find(p =>
p.game.status < status.ids.mate && p.player.id === this.opts.userId
) : null;
acceptedContainsMe = () => this.accepted().some(a => a.player.id === this.opts.userId);
applicantsContainsMe = () => this.candidates().some(a => a.player.id === this.opts.userId);
containsMe = () => this.opts.userId && (this.applicantsContainsMe() || this.pairingsContainMe());
pairingsContainMe = () => this.data.pairings.some(a => a.player.id === this.opts.userId);
}

View File

@ -0,0 +1,77 @@
import { VNode } from 'snabbdom/vnode'
export type MaybeVNode = VNode | string | null | undefined;
export type MaybeVNodes = MaybeVNode[];
export type Redraw = () => void;
export interface SimulOpts {
data: SimulData;
userId?: string;
element: HTMLElement;
$side: JQuery;
socketSend: SocketSend;
chat: any;
i18n: any;
}
export interface SimulData {
id: string;
name: string;
fullName: string;
isCreated: boolean;
isRunning: boolean;
isFinished: boolean;
text: string;
host: {
id: string;
name: string;
title?: string;
patron?: boolean;
rating: number;
gameId?: string;
};
applicants: Applicant[];
pairings: Pairing[];
quote?: {
text: string;
author: string;
}
team?: Team;
}
export interface Team {
id: string;
name: string;
isIn: boolean;
}
export interface Player extends LightUser {
rating: number;
provisional?: boolean;
}
export interface Applicant {
player: Player;
variant: VariantKey;
accepted: boolean;
}
export interface Pairing {
player: Player;
variant: VariantKey;
hostColor: Color;
game: Game;
}
export interface Game {
id: string;
status: number;
fen: string;
lastMove: string;
orient: Color;
clock?: {
white: number;
black: number;
}
winner?: Color;
}

View File

@ -1,22 +0,0 @@
var m = require('mithril');
var ctrl = require('./ctrl');
var view = require('./view/main');
module.exports = function(opts) {
var controller = new ctrl(opts);
m.module(opts.element, {
controller: function() {
return controller;
},
view: view
});
return {
socketReceive: controller.socket.receive
};
};
window.LichessChat = require('chat').default;

View File

@ -0,0 +1,45 @@
import { init } from 'snabbdom';
import { VNode } from 'snabbdom/vnode'
import cls from 'snabbdom/modules/class';
import attributes from 'snabbdom/modules/attributes';
import { Chessground } from 'chessground';
import { SimulOpts } from './interfaces';
import SimulCtrl from './ctrl';
import LichessChat from 'chat';
const patch = init([cls, attributes]);
import view from './view/main';
export function start(opts: SimulOpts) {
const li = window.lichess;
const element = document.querySelector('main.swiss') as HTMLElement;
li.socket = li.StrongSocket(
'/simul/' + opts.data.id, opts.data.socketVersion, {
receive: (t: string, d: any) => ctrl.socket.receive(t, d)
});
opts.classes = element.getAttribute('class');
opts.socketSend = li.socket.send;
opts.element = element;
opts.$side = $('.simul__side').clone();
let vnode: VNode;
function redraw() {
vnode = patch(vnode, view(ctrl));
}
const ctrl = new SimulCtrl(opts, redraw);
const blueprint = view(ctrl);
element.innerHTML = '';
vnode = patch(element, blueprint);
redraw();
};
// that's for the rest of lichess to access chessground
// without having to include it a second time
window.Chessground = Chessground;
window.LichessChat = LichessChat;

View File

@ -1,43 +0,0 @@
var status = require('game/status');
function applicantsContainMe(ctrl) {
return ctrl.data.applicants.some(function(a) {
return a.player.id === ctrl.userId;
})
}
function pairingsContainMe(ctrl) {
return ctrl.data.pairings.some(function(a) {
return a.player.id === ctrl.userId;
})
}
module.exports = {
createdByMe: function(ctrl) {
return ctrl.userId && ctrl.userId === ctrl.data.host.id;
},
containsMe: function(ctrl) {
return ctrl.userId && (applicantsContainMe(ctrl) || pairingsContainMe(ctrl));
},
candidates: function(ctrl) {
return ctrl.data.applicants.filter(function(a) {
return !a.accepted;
});
},
accepted: function(ctrl) {
return ctrl.data.applicants.filter(function(a) {
return a.accepted;
});
},
acceptedContainsMe: function(ctrl) {
return ctrl.data.applicants.some(function(a) {
return a.accepted && a.player.id === ctrl.userId;
})
},
myCurrentPairing: function(ctrl) {
if (!ctrl.userId) return null;
return ctrl.data.pairings.find(function(p) {
return p.game.status < status.ids.mate && p.player.id === ctrl.userId;
});
}
};

View File

@ -1,28 +0,0 @@
var m = require('mithril');
module.exports = function(send, ctrl) {
this.send = send;
var handlers = {
reload: function(data) {
ctrl.reload(data);
m.redraw();
},
aborted: function() {
lichess.reload();
},
hostGame: function(gameId) {
ctrl.data.host.gameId = gameId;
m.redraw();
}
};
this.receive = function(type, data) {
if (handlers[type]) {
handlers[type](data);
return true;
}
return false;
}.bind(this);
};

View File

@ -0,0 +1,30 @@
import SimulCtrl from './ctrl';
import { SimulData } from './interfaces';
export interface SimulSocket {
send: SocketSend;
receive(tpe: string, data: any): void;
}
export function makeSocket(send: SocketSend, ctrl: SimulCtrl) {
const handlers: any = {
reload(data: SimulData) {
ctrl.reload(data);
ctrl.redraw();
},
aborted: window.lichess.reload,
hostGame(gameId: string) {
ctrl.data.host.gameId = gameId;
ctrl.redraw();
}
};
return {
send,
receive(tpe: string, data: any) {
if (handlers[tpe]) return handlers[tpe](data);
return false;
}
};
};

View File

@ -0,0 +1,8 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"strictNullChecks": false,
"noImplicitAny": false,
"noImplicitReturns": false
}
}

View File

@ -199,7 +199,6 @@
else if (lichess.relay) startRelay(lichess.relay);
else if (lichess.puzzle) startPuzzle(lichess.puzzle);
else if (lichess.tournament) startTournament(lichess.tournament);
else if (lichess.simul) startSimul(lichess.simul);
else if (lichess.team) startTeam(lichess.team);
// delay so round starts first (just for perceived perf)

View File

@ -1,13 +1,11 @@
import makeSocket from './socket';
import { makeSocket, SwissSocket } from './socket';
import xhr from './xhr';
import throttle from 'common/throttle';
import { myPage, players } from './pagination';
import { SwissData, SwissOpts, Pages, Standing, Player } from './interfaces';
import { SwissSocket } from './socket';
export default class SwissCtrl {
opts: SwissOpts;
data: SwissData;
trans: Trans;
socket: SwissSocket;
@ -19,14 +17,11 @@ export default class SwissCtrl {
playerInfoId?: string;
disableClicks: boolean = true;
searching: boolean = false;
redraw: () => void;
private lastStorage = window.lichess.storage.make('last-redirect');
constructor(opts: SwissOpts, redraw: () => void) {
this.opts = opts;
constructor(readonly opts: SwissOpts, readonly redraw: () => void) {
this.data = this.readData(opts.data);
this.redraw = redraw;
this.trans = window.lichess.trans(opts.i18n);
this.socket = makeSocket(opts.socketSend, this);
this.page = this.data.standing.page;

View File

@ -5,7 +5,7 @@ export interface SwissSocket {
receive(tpe: string, data: any): void;
}
export default function(send: SocketSend, ctrl: SwissCtrl) {
export function makeSocket(send: SocketSend, ctrl: SwissCtrl) {
const handlers: any = {
reload() {