more work on sticky studies

study-sticky
Thibault Duplessis 2017-06-14 08:50:51 +02:00
parent 005d4b47d4
commit 3c60fdca79
12 changed files with 139 additions and 183 deletions

@ -1 +1 @@
Subproject commit 9ab653c037094c919d59a7bdca1cb49f33e8f11d
Subproject commit c187c7d871717a09f5eea66eca0a259a4134b0e5

View File

@ -16,8 +16,7 @@ case class AnaDrop(
variant: Variant,
fen: String,
path: String,
chapterId: Option[String],
local: Boolean
chapterId: Option[String]
) {
def branch: Valid[Branch] =
@ -64,7 +63,6 @@ object AnaDrop {
variant = variant,
fen = fen,
path = path,
chapterId = d str "ch",
local = ~(d boolean "local")
chapterId = d str "ch"
)
}

View File

@ -16,8 +16,7 @@ case class AnaMove(
fen: String,
path: String,
chapterId: Option[String],
promotion: Option[chess.PromotableRole],
local: Boolean // if local, do not affect the server state
promotion: Option[chess.PromotableRole]
) {
def branch: Valid[Branch] =
@ -64,7 +63,6 @@ object AnaMove {
fen = fen,
path = path,
chapterId = d str "ch",
promotion = d str "promotion" flatMap chess.Role.promotable,
local = ~(d boolean "local")
promotion = d str "promotion" flatMap chess.Role.promotable
)
}

View File

@ -98,7 +98,8 @@ private final class Socket(
case ReloadAll => notifyVersion("reload", JsNull, noMessadata)
case ChangeChapter(uid) => notifyVersion("changeChapter", Json.obj(
case ChangeChapter(uid, pos) => notifyVersion("changeChapter", Json.obj(
"p" -> pos,
"w" -> who(uid)
), noMessadata)
@ -265,7 +266,7 @@ private object Socket {
case class SetGlyphs(position: Position.Ref, glyphs: Glyphs, uid: Uid)
case class ReloadChapters(chapters: List[Chapter.Metadata])
case object ReloadAll
case class ChangeChapter(uid: Uid)
case class ChangeChapter(uid: Uid, position: Position.Ref)
case class SetConceal(position: Position.Ref, ply: Option[Chapter.Ply])
case class SetLiking(liking: Study.Liking, uid: Uid)
case class SetTags(chapterId: Chapter.Id, tags: List[chess.format.pgn.Tag], uid: Uid)

View File

@ -55,19 +55,21 @@ private[study] final class SocketHandler(
}
case ("anaMove", o) => AnaRateLimit(uid.value, member) {
AnaMove parse o foreach { anaMove =>
val moveOpts = getMoveOpts(o)
anaMove.branch match {
case scalaz.Success(branch) if branch.ply < Node.MAX_PLIES =>
member push makeMessage("node", anaMove json branch)
for {
userId <- member.userId
chapterId <- anaMove.chapterId
if !anaMove.local
if moveOpts.write
} api.addNode(
userId,
studyId,
Position.Ref(Chapter.Id(chapterId), Path(anaMove.path)),
Node.fromBranch(branch),
uid
uid,
sticky = moveOpts.sticky
)
case scalaz.Success(branch) =>
member push makeMessage("stepFailure", s"ply ${branch.ply}/${Node.MAX_PLIES}")
@ -78,19 +80,21 @@ private[study] final class SocketHandler(
}
case ("anaDrop", o) => AnaRateLimit(uid.value, member) {
AnaDrop parse o foreach { anaDrop =>
val moveOpts = getMoveOpts(o)
anaDrop.branch match {
case scalaz.Success(branch) if branch.ply < Node.MAX_PLIES =>
member push makeMessage("node", anaDrop json branch)
for {
userId <- member.userId
chapterId <- anaDrop.chapterId
if !anaDrop.local
if moveOpts.write
} api.addNode(
userId,
studyId,
Position.Ref(Chapter.Id(chapterId), Path(anaDrop.path)),
Node.fromBranch(branch),
uid
uid,
sticky = moveOpts.sticky
)
case scalaz.Success(branch) =>
member push makeMessage("stepFailure", s"ply ${branch.ply}/${Node.MAX_PLIES}")
@ -248,6 +252,16 @@ private[study] final class SocketHandler(
private implicit val StudyDataReader = Json.reads[Study.Data]
private implicit val setTagReader = Json.reads[actorApi.SetTag]
private case class MoveOpts(write: Boolean, sticky: Boolean)
private def getMoveOpts(o: JsObject) = {
val d = (o.pp obj "d").pp | Json.obj()
MoveOpts(
write = d.get[Boolean]("write") | true,
sticky = d.get[Boolean]("sticky") | true
).pp
}
def join(
studyId: Study.Id,
uid: Uid,

View File

@ -163,14 +163,14 @@ final class StudyApi(
}
}
def addNode(userId: User.ID, studyId: Study.Id, position: Position.Ref, node: Node, uid: Uid) = sequenceStudyWithChapter(studyId) {
def addNode(userId: User.ID, studyId: Study.Id, position: Position.Ref, node: Node, uid: Uid, sticky: Boolean) = sequenceStudyWithChapter(studyId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
chapter.addNode(node, position.path) match {
case None => fufail(s"Invalid addNode $studyId $position $node") >>- reloadUid(study, uid)
case Some(chapter) =>
chapter.root.nodeAt(position.path) ?? { parent =>
chapterRepo.setChildren(chapter, position.path, parent.children) >>
studyRepo.setPosition(study.id, position + node) >>
(sticky ?? studyRepo.setPosition(study.id, position + node)) >>
updateConceal(study, chapter, position + node) >>-
sendTo(study, Socket.AddNode(position, node, chapter.setup.variant, uid)) >>-
sendStudyEnters(study, userId)
@ -376,8 +376,9 @@ final class StudyApi(
(study.position.chapterId != chapterId) ?? {
chapterRepo.byIdAndStudy(chapterId, study.id) flatMap {
_ ?? { chapter =>
studyRepo.updateSomeFields(study withChapter chapter) >>-
sendTo(study, Socket.ChangeChapter(uid))
val newStudy = study withChapter chapter
studyRepo.updateSomeFields(newStudy) >>-
sendTo(study, Socket.ChangeChapter(uid, newStudy.position))
}
}
}
@ -411,7 +412,7 @@ final class StudyApi(
(newChapter.setup.orientation != chapter.setup.orientation) ||
(newChapter.practice != chapter.practice)
if (study.position.chapterId == chapter.id && shouldReload)
sendTo(study, Socket.ChangeChapter(uid))
sendTo(study, Socket.ChangeChapter(uid, study.position))
else
reloadChapters(study)
}

View File

@ -62,11 +62,6 @@ lichess.studyTour = function(study) {
text: "With the !? button, or a right click on the move list on the right.<br>" +
"Annotation glyphs are shared and persisted.",
attachTo: "#lichess .member_buttons .glyph top"
} : null, study.isContrib ? {
title: "Connect / disconnect",
text: "Choose whether or not your moves are shared and persisted.<br>" +
"Useful to try out variations before sharing them.",
attachTo: "#study-sync top"
} : null, {
title: "Thanks for your time",
text: "You can find your <a href='/study/mine/hot'>previous studies</a> from your profile page.<br>" +

View File

@ -438,9 +438,6 @@ div.underboard .notif.error {
.study_buttons .button:hover {
color: #3893E8;
}
.study_buttons #study-sync .data-count::after {
right: 8px;
}
.glyph-icon::before {
content: '⁉';
font-size: 1.3em;

View File

@ -13,7 +13,7 @@ module.exports = function(send, ctrl) {
this.clearCache = function() {
anaDestsCache = (
ctrl.data.game.variant.key === 'standard' &&
ctrl.tree.root.fen.split(' ', 1)[0] === initialBoardFen
ctrl.tree.root.fen.split(' ', 1)[0] === initialBoardFen
) ? {
'': {
path: '',
@ -29,13 +29,18 @@ module.exports = function(send, ctrl) {
}, 1000);
var currentChapterId = function() {
if (ctrl.study) return ctrl.study.currentChapter().id;
if (ctrl.study) return ctrl.study.vm.chapterId;
};
var addStudyData = function(req, addLocal) {
var addStudyData = function(req, isWrite) {
var c = currentChapterId();
if (c) {
req.ch = c;
if (addLocal && !ctrl.vm.mode.write) req.local = true;
if (isWrite) {
if (ctrl.study.vm.mode.write) {
if (!ctrl.study.vm.mode.sticky) req.sticky = false;
}
else req.write = false;
}
}
};
@ -46,7 +51,7 @@ module.exports = function(send, ctrl) {
if (data.ch == currentChapterId())
ctrl.addNode(data.node, data.path);
else
console.log('socket handler node got wrong chapter id', data);
console.log('socket handler node got wrong chapter id', data);
},
stepFailure: function(data) {
clearTimeout(anaMoveTimeout);
@ -58,7 +63,7 @@ module.exports = function(send, ctrl) {
anaDestsCache[data.path] = data;
ctrl.addDests(data.dests, data.path, data.opening);
} else
console.log('socket handler node got wrong chapter id', data);
console.log('socket handler node got wrong chapter id', data);
},
destsFailure: function(data) {
console.log(data);

View File

@ -27,12 +27,12 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
loading: false,
nextChapterId: false,
tab: m.prop(data.chapters.length > 1 ? 'chapters' : 'members'),
chapterId: sticked ? data.position.chapterId : data.chapter.id,
// path is at ctrl.vm.path
mode: {
sticky: sticked,
write: true
},
catchingUp: false, // in the process of sticking
chapterId: sticked ? null : data.chapter.id // only useful when not sticking
}
};
})();
@ -59,25 +59,18 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
var chapters = chapterCtrl(data.chapters, send, lichess.partial(vm.tab, 'chapters'), lichess.partial(xhr.chapterConfig, data.id), ctrl);
var currentChapterId = function() {
return vm.chapterId || data.position.chapterId;
}
var currentChapter = function() {
return chapters.get(currentChapterId());
return chapters.get(vm.chapterId);
};
var isChapterOwner = function() {
return ctrl.userId === data.chapter.ownerId;
};
var isWriting = function() {
return vm.mode.write && members.canContribute();
};
var makeChange = function(t, d) {
if (isWriting()) {
if (vm.mode.write) {
send(t, d);
return true;
} else if (!members.canContribute()) vm.mode.sticky = false;
} else vm.mode.sticky = false;
};
var commentForm = commentFormCtrl(ctrl);
@ -117,9 +110,7 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
var onReload = function(d) {
var s = d.study;
if (data.visibility === 'public' && s.visibility === 'private' && !members.myMember())
return lichess.reload();
if (s.position !== data.position) commentForm.close();
if (vm.mode.sticky && s.position !== data.position) commentForm.close();
['position', 'name', 'visibility', 'features', 'settings', 'chapter', 'likes', 'liked'].forEach(function(key) {
data[key] = s[key];
});
@ -133,12 +124,13 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
ctrl.chessground = undefined; // don't apply changes to old cg; wait for new cg
if (vm.mode.sticky || vm.catchingUp) ctrl.userJump(data.position.path);
else ctrl.userJump('');
if (vm.mode.sticky) {
vm.chapterId = data.position.chapterId;
ctrl.userJump(data.position.path);
}
configurePractice();
vm.catchingUp = false;
m.redraw.strategy("all"); // create a new cg
m.redraw();
ctrl.startCeval();
@ -146,15 +138,15 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
var xhrReload = function() {
vm.loading = true;
return xhr.reload(practice ? 'practice/load' : 'study', data.id, vm.chapterId).then(onReload);
};
var activity = function(userId) {
members.setActive(userId);
return xhr.reload(
practice ? 'practice/load' : 'study',
data.id,
vm.mode.sticky ? null : vm.chapterId
).then(onReload, lichess.reload);
};
var onSetPath = throttle(300, false, function(path) {
if (path !== data.position.path) makeChange("setPath", addChapterId({
if (vm.mode.sticky && path !== data.position.path) makeChange("setPath", addChapterId({
path: path
}));
});
@ -171,7 +163,7 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
var mutateCgConfig = function(config) {
config.drawable.onChange = function(shapes) {
if (members.canContribute()) {
if (vm.mode.write) {
ctrl.tree.setShapes(shapes, ctrl.vm.path);
makeChange("shapes", addChapterId({
path: ctrl.vm.path,
@ -181,6 +173,14 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
};
}
var wrongChapter = function(serverPosition) {
if (serverPosition.chapterId !== vm.chapterId) {
// sticky should really be on the same chapter
if (vm.mode.sticky) xhrReload();
return true;
}
};
return {
data: data,
form: form,
@ -209,7 +209,7 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
},
onJump: practice ? practice.onJump : function() {},
withPosition: function(obj) {
obj.ch = currentChapterId();
obj.ch = vm.chapterId;
obj.path = ctrl.vm.path;
return obj;
},
@ -230,8 +230,8 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
}));
},
setChapter: function(id, force) {
if (id === currentChapterId() && !force) return;
if (!makeChange("setChapter", id)) {
if (id === vm.chapterId && !force) return;
if (!vm.mode.sticky || !makeChange("setChapter", id)) {
vm.chapterId = id;
xhrReload();
}
@ -240,27 +240,11 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
m.redraw();
},
toggleSticky: function() {
if (!data.features.sticky) {
vm.mode.sticky = false;
}
else if (vm.mode.sticky) {
vm.mode.sticky = false;
vm.chapterId = currentChapterId();
} else {
vm.mode.sticky = true;
vm.chapterId = null;
vm.catchingUp = true;
xhrReload();
}
vm.mode.sticky = !vm.mode.sticky && data.features.sticky;
xhrReload();
},
toggleWrite: function() {
if (vm.behind !== false) {
tours.onSync();
resync();
} else {
vm.behind = 0;
vm.chapterId = currentChapterId();
}
vm.mode.write = !vm.mode.write && members.canContribute();
},
makeChange: makeChange,
startTour: startTour,
@ -272,13 +256,15 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
path: function(d) {
var position = d.p,
who = d.w;
who && activity(who.u);
if (vm.behind !== false) return;
if (position.chapterId !== data.position.chapterId) return;
if (!ctrl.tree.pathExists(position.path)) xhrReload();
who && members.setActive(who.u);
// #TODO if not sticky, still follow if on the parent path (live games)
if (!vm.mode.sticky) return;
if (position.chapterId !== data.position.chapterId ||
!ctrl.tree.pathExists(position.path)) {
return xhrReload();
}
data.position.path = position.path;
if (who && who.s === sri) return;
data.position.path = position.path;
ctrl.userJump(position.path);
m.redraw();
},
@ -286,44 +272,46 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
var position = d.p,
node = d.n,
who = d.w;
if (position.chapterId !== currentChapterId()) return;
who && activity(who.u);
who && members.setActive(who.u);
if (wrongChapter(position)) return;
// node author already has the node
if (who && who.s === sri) {
data.position.path = position.path + node.id;
return;
}
var newPath = ctrl.tree.addNode(node, position.path);
ctrl.tree.addDests(d.d, newPath, d.o);
if (!newPath) xhrReload();
if (!newPath) return xhrReload();
data.position.path = newPath;
if (vm.behind === false) ctrl.jump(data.position.path);
if (vm.mode.sticky) ctrl.jump(data.position.path);
m.redraw();
},
deleteNode: function(d) {
var position = d.p,
who = d.w;
who && activity(who.u);
if (vm.behind !== false) return;
who && members.setActive(who.u);
if (wrongChapter(position)) return;
// deleter already has it done
if (who && who.s === sri) return;
if (position.chapterId !== data.position.chapterId) return;
if (!ctrl.tree.pathExists(d.p.path)) return xhrReload();
ctrl.tree.deleteNodeAt(position.path);
ctrl.jump(ctrl.vm.path);
if (vm.mode.sticky) ctrl.jump(ctrl.vm.path);
m.redraw();
},
promote: function(d, toMainline) {
var position = d.p,
who = d.w;
who && activity(who.u);
if (vm.behind !== false) return;
who && members.setActive(who.u);
if (wrongChapter(position)) return;
if (who && who.s === sri) return;
if (position.chapterId !== data.position.chapterId) return;
if (!ctrl.tree.pathExists(d.p.path)) return xhrReload();
ctrl.tree.promoteAt(position.path, toMainline);
ctrl.jump(ctrl.vm.path);
if (vm.mode.sticky) ctrl.jump(ctrl.vm.path);
},
reload: xhrReload,
changeChapter: function(d) {
d.w && activity(d.w.u);
d.w && members.setActive(d.w.u);
data.position = d.p;
if (vm.mode.sticky) xhrReload();
},
members: function(d) {
@ -338,51 +326,48 @@ module.exports = function(data, ctrl, tagTypes, practiceData) {
shapes: function(d) {
var position = d.p,
who = d.w;
who && activity(who.u);
if (vm.behind !== false) return;
who && members.setActive(who.u);
if (wrongChapter(position)) return;
if (who && who.s === sri) return;
if (position.chapterId !== data.position.chapterId) return;
ctrl.tree.setShapes(d.s, ctrl.vm.path);
ctrl.chessground && ctrl.chessground.setShapes(d.s);
if (ctrl.vm.path === position.path && ctrl.chessground) ctrl.chessground.setShapes(d.s);
m.redraw();
},
setComment: function(d) {
var position = d.p,
who = d.w;
who && activity(who.u);
who && members.setActive(who.u);
if (wrongChapter(position)) return;
if (who && who.s === sri) commentForm.dirty(false);
if (vm.behind !== false) return;
if (position.chapterId !== data.position.chapterId) return;
ctrl.tree.setCommentAt(d.c, position.path);
m.redraw();
},
setTags: function(d) {
d.w && activity(d.w.u);
if (d.chapterId === data.position.chapterId) data.chapter.tags = d.tags;
d.w && members.setActive(d.w.u);
if (wrongChapter(position)) return;
data.chapter.tags = d.tags;
m.redraw();
},
deleteComment: function(d) {
var position = d.p,
who = d.w;
who && activity(who.u);
if (vm.behind !== false) return;
if (position.chapterId !== data.position.chapterId) return;
who && members.setActive(who.u);
if (wrongChapter(position)) return;
ctrl.tree.deleteCommentAt(d.id, position.path);
m.redraw();
},
glyphs: function(d) {
var position = d.p,
who = d.w;
who && activity(who.u);
who && members.setActive(who.u);
if (wrongChapter(position)) return;
if (who && who.s === sri) glyphForm.dirty(false);
if (vm.behind !== false) return;
if (position.chapterId !== data.position.chapterId) return;
ctrl.tree.setGlyphsAt(d.g, position.path);
m.redraw();
},
conceal: function(d) {
var position = d.p;
if (position.chapterId !== data.position.chapterId) return;
if (wrongChapter(position)) return;
data.chapter.conceal = d.ply;
m.redraw();
},

View File

@ -22,28 +22,5 @@ module.exports = {
}
});
});
},
offline: function() {
lichess.shepherd(function(theme) {
var tour = new Shepherd.Tour({
defaults: {
classes: theme,
scrollTo: true
}
});
tour.addStep('off', {
title: 'Offline mode',
text: 'Your board is no longer shared!<br>Click this button to reconnect.',
attachTo: '#study-sync top',
buttons: [{
text: 'OK',
action: tour.next
}],
});
tour.start();
});
},
// onSync: function() {
// if (window.Shepherd && Shepherd.activeTour) Shepherd.activeTour.next()
// }
}
};

View File

@ -21,67 +21,52 @@ function buttons(root) {
var canContribute = ctrl.members.canContribute();
return m('div.study_buttons', [
m('div.member_buttons', [
ctrl.data.features.sticky ? m('span#study-sticky.hint--top', {
'data-hint': ctrl.vm.mode.stiky ? 'Sticky' : 'Free'
}, m('a', {
class: 'button',
ctrl.data.features.sticky ? m('a.button.text', {
'data-icon': ctrl.vm.mode.sticky ? '"' : '"',
onclick: ctrl.toggleSticky
}),
m('i', {
'data-icon': ctrl.vm.mode.sticky ? '"' : '"'
})) : null,
ctrl.members.canContribute() ? m('span#study-write.hint--top', {
'data-hint': ctrl.vm.mode.write ? 'Write' : 'Read'
}, m('a', {
class: 'button',
}, ctrl.vm.mode.sticky ? 'Sticky' : 'Free') : null,
ctrl.members.canContribute() ? m('a.button.text', {
'data-icon': ctrl.vm.mode.write ? 'E' : 'k',
onclick: ctrl.toggleWrite
}),
m('i', {
'data-icon': ctrl.vm.mode.write ? 'E' : 'k'
})) : null,
m('a.button.share.hint--top', {
class: classSet({
active: ctrl.share.open()
}),
'data-hint': 'Share & export',
config: bindOnce('click', function() {
ctrl.share.toggle();
})
},
m('i.[data-icon=z]')),
canContribute ? [
(function(enabled) {
return m('a.button.comment.hint--top', {
}, ctrl.vm.mode.write ? 'Write' : 'Read') : null,
m('a.button.share.hint--top', {
class: classSet({
active: ctrl.share.open()
}),
'data-hint': 'Share & export',
config: bindOnce('click', ctrl.share.toggle)
},
m('i.[data-icon=z]')),
canContribute ? [
m('a.button.comment.hint--top', {
class: classSet({
active: ctrl.commentForm.current(),
disabled: !enabled
disabled: !ctrl.vm.mode.write
}),
'data-hint': 'Comment this position',
config: bindOnce('click', function() {
if (ctrl.vm.mode.write) ctrl.commentForm.toggle(ctrl.currentChapter().id, root.vm.path, root.vm.node);
})
}, m('i[data-icon=c]'));
})(ctrl.vm.mode.write), (function(enabled) {
return m('a.button.glyph.hint--top', {
}, m('i[data-icon=c]')),
m('a.button.glyph.hint--top', {
class: classSet({
active: ctrl.glyphForm.isOpen(),
disabled: !enabled
disabled: !(root.vm.path && ctrl.vm.write)
}),
'data-hint': 'Annotate with symbols',
config: bindOnce('click', function() {
if (root.vm.path && ctrl.vm.mode.write) ctrl.glyphForm.toggle();
})
},
m('i.glyph-icon'));
})(root.vm.path && ctrl.vm.mode.write)
] : null
m('i.glyph-icon'))
] : null
]),
m('span.button.help.hint--top', {
'data-hint': 'Need help? Get the tour!',
onclick: ctrl.startTour
}, m('i.text', {
'data-icon': ''
}, 'help'))
m('span.button.help.hint--top', {
'data-hint': 'Need help? Get the tour!',
onclick: ctrl.startTour
}, m('i.text', {
'data-icon': ''
}, 'help'))
]);
}