lila/ui/analyse/src/study/studyCtrl.ts

561 lines
16 KiB
TypeScript

import { throttle, prop } from 'common';
import AnalyseCtrl from '../ctrl';
import { ctrl as memberCtrl } from './studyMembers';
import { ctrl as chapterCtrl } from './studyChapters';
import practiceCtrl from './practice/studyPracticeCtrl';
import { StudyPracticeData, StudyPracticeCtrl } from './practice/interfaces';
import { ctrl as commentFormCtrl } from './commentForm';
import { ctrl as glyphFormCtrl } from './studyGlyph';
import { ctrl as studyFormCtrl, StudyFormCtrl } from './studyForm';
import { ctrl as notifCtrl } from './notif';
import { ctrl as shareCtrl } from './studyShare';
import { ctrl as tagsCtrl } from './studyTags';
import * as tours from './studyTour';
import * as xhr from './studyXhr';
import { path as treePath } from 'tree';
import { StudyCtrl, StudyVm, Tab, TagTypes, StudyData, StudyChapterMeta, ReloadData } from './interfaces';
import GamebookPlayCtrl from './gamebook/gamebookPlayCtrl';
import { ChapterDescriptionCtrl } from './chapterDescription';
import RelayCtrl from './relay/relayCtrl';
import { RelayData } from './relay/interfaces';
const li = window.lichess;
// data.position.path represents the server state
// ctrl.path is the client state
export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes, practiceData?: StudyPracticeData, relayData?: RelayData): StudyCtrl {
const send = ctrl.socket.send;
const redraw = ctrl.redraw;
const sri: string = li.StrongSocket ? li.StrongSocket.sri : '';
const vm: StudyVm = (function() {
const isManualChapter = data.chapter.id !== data.position.chapterId;
const sticked = data.features.sticky && !ctrl.initialPath && !isManualChapter && !practiceData;
return {
loading: false,
tab: prop<Tab>(relayData || data.chapters.length > 1 ? 'chapters' : 'members'),
chapterId: sticked ? data.position.chapterId : data.chapter.id,
// path is at ctrl.path
mode: {
sticky: sticked,
write: true
},
// how many events missed because sync=off
behind: 0,
// how stale is the study
updatedAt: Date.now() - data.secondsSinceUpdate * 1000,
gamebookOverride: undefined
};
})();
const notif = notifCtrl(redraw);
function startTour() {
tours.study(ctrl);
};
const members = memberCtrl({
initDict: data.members,
myId: practiceData ? null : ctrl.opts.userId,
ownerId: data.ownerId,
send,
tab: vm.tab,
startTour,
notif,
onBecomingContributor() {
vm.mode.write = true;
},
redraw
});
const chapters = chapterCtrl(
data.chapters,
send,
() => vm.tab('chapters'),
chapterId => xhr.chapterConfig(data.id, chapterId),
ctrl);
function currentChapter(): StudyChapterMeta {
return chapters.get(vm.chapterId)!;
};
function isChapterOwner() {
return ctrl.opts.userId === data.chapter.ownerId;
};
const relay = relayData ? new RelayCtrl(relayData, send, redraw, members, data.chapter) : undefined;
const form: StudyFormCtrl = studyFormCtrl((d, isNew) => {
send("editStudy", d);
if (isNew && data.chapter.setup.variant.key === 'standard' && ctrl.mainline.length === 1 && !data.chapter.setup.fromFen && !relay)
chapters.newForm.openInitial();
}, () => data, redraw, relay);
function isWriting(): boolean {
return vm.mode.write && !isGamebookPlay();
}
function makeChange(t: string, d: any): boolean {
if (isWriting()) {
send(t, d);
return true;
}
return vm.mode.sticky = false;
};
const commentForm = commentFormCtrl(ctrl);
const glyphForm = glyphFormCtrl(ctrl);
const tags = tagsCtrl(ctrl, () => data.chapter, tagTypes);
const desc = new ChapterDescriptionCtrl(data.chapter.description, t => {
data.chapter.description = t;
send("descChapter", {
id: vm.chapterId,
description: t
});
}, redraw);
function addChapterId(req) {
req.ch = vm.chapterId;
return req;
}
function isGamebookPlay() {
return data.chapter.gamebook && vm.gamebookOverride !== 'analyse' &&
(vm.gamebookOverride === 'play' || !members.canContribute());
}
if (vm.mode.sticky && !isGamebookPlay()) ctrl.userJump(data.position.path);
else if (data.chapter.relay) ctrl.userJump(data.chapter.relay.path);
function configureAnalysis() {
if (ctrl.embed) return;
const canContribute = members.canContribute();
// unwrite if member lost privileges
vm.mode.write = vm.mode.write && canContribute;
li.pubsub.emit('chat.writeable')(data.features.chat);
li.pubsub.emit('chat.permissions')({local: canContribute});
const computer: boolean = !isGamebookPlay() && !!(data.chapter.features.computer || data.chapter.practice);
if (!computer) ctrl.getCeval().enabled(false);
ctrl.getCeval().allowed(computer);
if (!data.chapter.features.explorer) ctrl.explorer.disable();
ctrl.explorer.allowed(data.chapter.features.explorer);
};
configureAnalysis();
function configurePractice() {
if (!data.chapter.practice && ctrl.practice) ctrl.togglePractice();
if (data.chapter.practice) ctrl.restartPractice();
if (practice) practice.onReload();
};
function onReload(d: ReloadData) {
const s = d.study!;
const prevPath = ctrl.path;
const sameChapter = data.chapter.id === s.chapter.id;
vm.mode.sticky = (vm.mode.sticky && s.features.sticky) || (!data.features.sticky && s.features.sticky);
if (vm.mode.sticky) vm.behind = 0;
if (vm.mode.sticky && s.position !== data.position) commentForm.close();
'position name visibility features settings chapter likes liked'.split(' ').forEach(key => {
data[key] = s[key];
});
desc.set(data.chapter.description);
document.title = data.name;
members.dict(s.members);
chapters.list(s.chapters);
ctrl.flipped = false;
const merge = !vm.mode.write && sameChapter;
ctrl.reloadData(d.analysis, merge);
configureAnalysis();
vm.loading = false;
vm.gamebookOverride = undefined;
instanciateGamebookPlay();
if (relay) relay.applyChapterRelay(data.chapter, s.chapter.relay);
let nextPath: Tree.Path;
if (vm.mode.sticky) {
vm.chapterId = data.position.chapterId;
nextPath = (
(vm.justSetChapterId === vm.chapterId) && chapters.localPaths[vm.chapterId]
) || data.position.path;
} else {
nextPath = sameChapter ? prevPath : (
data.chapter.relay ? data.chapter.relay!.path : (chapters.localPaths[vm.chapterId] || treePath.root)
);
}
// path could be gone (because of subtree deletion), go as far as possible
ctrl.userJump(ctrl.tree.longestValidPath(nextPath));
vm.justSetChapterId = undefined;
configurePractice();
redraw();
ctrl.startCeval();
};
function xhrReload() {
vm.loading = true;
return xhr.reload(
practice ? 'practice/load' : 'study',
data.id,
vm.mode.sticky ? undefined : vm.chapterId
).then(onReload, li.reload);
};
const onSetPath = throttle(300, false, (path: Tree.Path) => {
if (vm.mode.sticky && path !== data.position.path) makeChange("setPath", addChapterId({
path
}));
});
if (members.canContribute()) form.openIfNew();
function currentNode() {
return ctrl.node;
};
const share = shareCtrl(data, currentChapter, currentNode, redraw);
const practice: StudyPracticeCtrl | undefined = practiceData && practiceCtrl(ctrl, data, practiceData);
let gamebookPlay: GamebookPlayCtrl | undefined;
function instanciateGamebookPlay() {
if (!isGamebookPlay()) return gamebookPlay = undefined;
if (gamebookPlay && gamebookPlay.chapterId === vm.chapterId) return;
gamebookPlay = new GamebookPlayCtrl(ctrl, vm.chapterId, redraw);
vm.mode.sticky = false;
}
instanciateGamebookPlay();
function mutateCgConfig(config) {
config.drawable.onChange = shapes => {
if (vm.mode.write) {
ctrl.tree.setShapes(shapes, ctrl.path);
makeChange("shapes", addChapterId({
path: ctrl.path,
shapes
}));
}
gamebookPlay && gamebookPlay.onShapeChange(shapes);
};
}
function wrongChapter(serverData) {
if (serverData.p.chapterId !== vm.chapterId) {
// sticky should really be on the same chapter
if (vm.mode.sticky && serverData.sticky) xhrReload();
return true;
}
}
function setMemberActive(who?: {u: string}) {
who && members.setActive(who.u);
vm.updatedAt = Date.now();
}
function withPosition(obj: any) {
obj.ch = vm.chapterId;
obj.path = ctrl.path;
return obj;
}
const socketHandlers = {
path(d) {
const position = d.p,
who = d.w;
setMemberActive(who);
if (!vm.mode.sticky) {
vm.behind++;
return redraw();
}
if (position.chapterId !== data.position.chapterId ||
!ctrl.tree.pathExists(position.path)) {
return xhrReload();
}
data.position.path = position.path;
if (who && who.s === sri) return;
ctrl.userJump(position.path);
redraw();
},
addNode(d) {
const position = d.p,
node = d.n,
who = d.w,
sticky = d.s;
setMemberActive(who);
if (sticky && !vm.mode.sticky) vm.behind++;
if (wrongChapter(d)) {
if (sticky && !vm.mode.sticky) redraw();
return;
}
// node author already has the node
if (sticky && who && who.s === sri) {
data.position.path = position.path + node.id;
return;
}
if (relay) relay.applyChapterRelay(data.chapter, d.relay);
const newPath = ctrl.tree.addNode(node, position.path);
if (!newPath) return xhrReload();
ctrl.tree.addDests(d.d, newPath, d.o);
if (sticky) data.position.path = newPath;
if ((sticky && vm.mode.sticky) || (
position.path === ctrl.path &&
position.path === treePath.fromNodeList(ctrl.mainline)
)) ctrl.jump(newPath);
redraw();
},
deleteNode(d) {
const position = d.p,
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
// deleter already has it done
if (who && who.s === sri) return;
if (!ctrl.tree.pathExists(d.p.path)) return xhrReload();
ctrl.tree.deleteNodeAt(position.path);
if (vm.mode.sticky) ctrl.jump(ctrl.path);
redraw();
},
promote(d) {
const position = d.p,
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
if (who && who.s === sri) return;
if (!ctrl.tree.pathExists(d.p.path)) return xhrReload();
ctrl.tree.promoteAt(position.path, d.toMainline);
if (vm.mode.sticky) ctrl.jump(ctrl.path);
redraw();
},
reload: xhrReload,
changeChapter(d) {
setMemberActive(d.w);
if (!vm.mode.sticky) vm.behind++;
data.position = d.p;
if (vm.mode.sticky) xhrReload();
else redraw();
},
updateChapter(d) {
setMemberActive(d.w);
xhrReload();
},
descChapter(d) {
setMemberActive(d.w);
if (d.w && d.w.s === sri) return;
if (data.chapter.id === d.chapterId) {
data.chapter.description = d.description;
desc.set(d.description);
}
redraw();
},
addChapter(d) {
setMemberActive(d.w);
if (d.s && !vm.mode.sticky) vm.behind++;
if (d.s) data.position = d.p;
else if (d.w && d.w.s === sri) {
vm.mode.write = true;
vm.chapterId = d.p.chapterId;
}
xhrReload();
},
members(d) {
members.update(d);
configureAnalysis();
redraw();
},
chapters(d) {
chapters.list(d);
if (!currentChapter()) {
vm.chapterId = d[0].id;
if (!vm.mode.sticky) xhrReload();
}
redraw();
},
shapes(d) {
const position = d.p,
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
if (who && who.s === sri) return;
ctrl.tree.setShapes(d.s, ctrl.path);
if (ctrl.path === position.path) ctrl.withCg(cg => cg.setShapes(d.s));
redraw();
},
setComment(d) {
const position = d.p,
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
ctrl.tree.setCommentAt(d.c, position.path);
redraw();
},
setTags(d) {
setMemberActive(d.w);
if (d.chapterId !== vm.chapterId) return;
data.chapter.tags = d.tags;
redraw();
},
deleteComment(d) {
const position = d.p,
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
ctrl.tree.deleteCommentAt(d.id, position.path);
redraw();
},
glyphs(d) {
const position = d.p,
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
ctrl.tree.setGlyphsAt(d.g, position.path);
redraw();
},
clock(d) {
const position = d.p,
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
ctrl.tree.setClockAt(d.c, position.path);
redraw();
},
conceal(d) {
if (wrongChapter(d)) return;
data.chapter.conceal = d.ply;
redraw();
},
liking(d) {
data.likes = d.l.likes;
if (d.w && d.w.s === sri) data.liked = d.l.me;
redraw();
},
following_onlines: members.inviteForm.setFollowings,
following_leaves: members.inviteForm.delFollowing,
following_enters: members.inviteForm.addFollowing,
crowd(d) {
members.setSpectators(d.users);
},
error(msg) {
alert(msg);
}
}
return {
data,
form,
members,
chapters,
notif,
commentForm,
glyphForm,
share,
tags,
desc,
vm,
relay,
isUpdatedRecently() {
return Date.now() - vm.updatedAt < 300 * 1000;
},
toggleLike() {
send("like", {
liked: !data.liked
});
},
position() {
return data.position;
},
currentChapter,
isChapterOwner,
canJumpTo(path: Tree.Path) {
if (gamebookPlay) return gamebookPlay.canJumpTo(path);
return data.chapter.conceal === undefined ||
isChapterOwner() ||
treePath.contains(ctrl.path, path) || // can always go back
ctrl.tree.lastMainlineNode(path).ply <= data.chapter.conceal!;
},
onJump() {
chapters.localPaths[vm.chapterId] = ctrl.path;
if (practice) practice.onJump();
if (gamebookPlay) gamebookPlay.onJump();
},
withPosition,
setPath(path, node) {
onSetPath(path);
setTimeout(() => commentForm.onSetPath(path, node), 100);
},
deleteNode(path) {
makeChange("deleteNode", addChapterId({
path,
jumpTo: ctrl.path
}));
},
promote(path, toMainline) {
makeChange("promote", addChapterId({
toMainline,
path
}));
},
setChapter(id, force) {
if (id === vm.chapterId && !force) return;
if (!vm.mode.sticky || !makeChange("setChapter", id)) {
vm.mode.sticky = false;
if (!vm.behind) vm.behind = 1;
vm.chapterId = id;
xhrReload();
}
vm.loading = true;
vm.nextChapterId = id;
vm.justSetChapterId = id;
redraw();
},
toggleSticky: function() {
vm.mode.sticky = !vm.mode.sticky && data.features.sticky;
xhrReload();
},
toggleWrite: function() {
vm.mode.write = !vm.mode.write && members.canContribute();
if (!vm.mode.write) commentForm.close();
xhrReload();
},
isWriting,
makeChange,
startTour,
userJump: ctrl.userJump,
currentNode,
practice,
gamebookPlay: () => gamebookPlay,
nextChapter(): StudyChapterMeta | undefined {
const chapters = data.chapters,
currentId = currentChapter().id;
for (let i in chapters)
if (chapters[i].id === currentId) return chapters[parseInt(i) + 1];
},
setGamebookOverride(o) {
vm.gamebookOverride = o;
instanciateGamebookPlay();
configureAnalysis();
ctrl.userJump(ctrl.path);
if (!o) xhrReload();
},
mutateCgConfig,
explorerGame(gameId: string, insert: boolean) {
makeChange('explorerGame', withPosition({ gameId, insert }));
},
redraw,
socketHandler: (t: string, d: any) => {
const handler = socketHandlers[t];
if (handler) {
handler(d);
return true;
}
return !!relay && relay.socketHandler(t, d);
}
};
};