add study pinned comment - closes #5242

required removing the study chapter ID from .analyse CSS classes
so that the pinned comment doesn't redraw when changing chapter,
because it resets the eventual embedded twitch stream.
study-desc
Thibault Duplessis 2019-07-04 18:10:59 -04:00
parent e7c53904ec
commit 023b8f1a79
15 changed files with 109 additions and 64 deletions

View File

@ -201,5 +201,5 @@ private[study] object ChapterMaker {
def hasDescription = description.nonEmpty
}
case class DescData(id: Chapter.Id, description: String)
case class DescData(id: Chapter.Id, desc: String)
}

View File

@ -37,21 +37,19 @@ final class JsonView(
"description" -> study.settings.description
),
"chapters" -> chapters.map(chapterMetadataWrites.writes),
"chapter" -> {
Json.obj(
"id" -> currentChapter.id,
"ownerId" -> currentChapter.ownerId,
"setup" -> currentChapter.setup,
"tags" -> currentChapter.tags,
"features" -> Json.obj(
"computer" -> allowed(study.settings.computer),
"explorer" -> allowed(study.settings.explorer)
)
).add("description", currentChapter.description)
.add("serverEval", currentChapter.serverEval)
.add("relay", currentChapter.relay)(relayWrites) |> addChapterMode(currentChapter)
}
)
"chapter" -> Json.obj(
"id" -> currentChapter.id,
"ownerId" -> currentChapter.ownerId,
"setup" -> currentChapter.setup,
"tags" -> currentChapter.tags,
"features" -> Json.obj(
"computer" -> allowed(study.settings.computer),
"explorer" -> allowed(study.settings.explorer)
)
).add("description", currentChapter.description)
.add("serverEval", currentChapter.serverEval)
.add("relay", currentChapter.relay)(relayWrites).|>(addChapterMode(currentChapter))
).add("description", study.description)
}
}

View File

@ -162,6 +162,11 @@ final class SocketHandler(
}
}
case ("descStudy", o) => for {
desc <- o str "d"
userId <- member.userId
} api.descStudy(userId, studyId, desc, uid)
case ("descChapter", o) =>
reading[ChapterMaker.DescData](o) { data =>
member.userId foreach {

View File

@ -590,7 +590,7 @@ final class StudyApi(
chapterRepo.byIdAndStudy(data.id, studyId) flatMap {
_ ?? { chapter =>
val newChapter = chapter.copy(
description = data.description.nonEmpty option data.description
description = data.desc.nonEmpty option data.desc
)
(chapter != newChapter) ?? {
chapterRepo.update(newChapter) >>- {
@ -633,12 +633,26 @@ final class StudyApi(
}
}
def descStudy(byUserId: User.ID, studyId: Study.Id, desc: String, uid: Uid) = sequenceStudy(studyId) { study =>
Contribute(byUserId, study) {
val newStudy = study.copy(description = desc.nonEmpty option desc)
(study != newStudy) ?? {
studyRepo.updateSomeFields(newStudy) >>-
sendTo(study, StudySocket.DescStudy(uid, newStudy.description)) >>-
indexStudy(study)
}
}
}
def editStudy(byUserId: User.ID, studyId: Study.Id, data: Study.Data) = sequenceStudy(studyId) { study =>
data.settings.ifTrue(study isOwner byUserId) ?? { settings =>
val newStudy = study.copy(
name = Study toName data.name,
settings = settings,
visibility = data.vis
visibility = data.vis,
description = settings.description option {
study.description.filter(_.nonEmpty) | "-"
}
)
if (!study.isPublic && newStudy.isPublic) {
bus.publish(lila.hub.actorApi.study.StudyBecamePublic(studyId.value, study.members.contributorIds), 'study)

View File

@ -66,6 +66,7 @@ final class StudyRepo(private[study] val coll: Coll) {
"name" -> s.name,
"settings" -> s.settings,
"visibility" -> s.visibility,
"description" -> ~s.description,
"updatedAt" -> DateTime.now
)).void

View File

@ -119,7 +119,12 @@ final class StudySocket(
case DescChapter(uid, chapterId, description) => notifyVersion("descChapter", Json.obj(
"chapterId" -> chapterId,
"description" -> description,
"desc" -> description,
"w" -> who(uid)
), noMessadata)
case DescStudy(uid, description) => notifyVersion("descStudy", Json.obj(
"desc" -> description,
"w" -> who(uid)
), noMessadata)
@ -307,7 +312,8 @@ object StudySocket {
case object ReloadAll
case class ChangeChapter(uid: Uid, position: Position.Ref)
case class UpdateChapter(uid: Uid, chapterId: Chapter.Id)
case class DescChapter(uid: Uid, chapterId: Chapter.Id, description: Option[String])
case class DescChapter(uid: Uid, chapterId: Chapter.Id, desc: Option[String])
case class DescStudy(uid: Uid, desc: Option[String])
case class AddChapter(uid: Uid, position: Position.Ref, sticky: Boolean)
case class SetConceal(position: Position.Ref, ply: Option[Chapter.Ply])
case class SetLiking(liking: Study.Liking, uid: Uid)

View File

@ -51,7 +51,9 @@ final class StudySearchApi(
Fields.chapterNames -> s.chapters.collect {
case c if !Chapter.isDefaultName(c.name) => c.name.value
}.mkString(" "),
Fields.chapterTexts -> noMultiSpace(s.chapters.flatMap(chapterText).mkString(" ")),
Fields.chapterTexts -> noMultiSpace {
(s.study.description.toList :+ s.chapters.flatMap(chapterText)).mkString(" ")
},
// Fields.createdAt -> study.createdAt)
// Fields.updatedAt -> study.updatedAt,
Fields.likes -> s.study.likes.value,

View File

@ -2,6 +2,7 @@
@extend %box-neat;
background: $c-bg-box;
padding: .7em 1em;
margin-top: .5em;
position: relative;
&.empty {
text-align: center;

View File

@ -8,7 +8,7 @@ import * as modal from '../modal';
import { chapter as chapterTour } from './studyTour';
import { StudyChapterMeta } from './interfaces';
import { Redraw } from '../interfaces';
import { descTitle } from './chapterDescription';
import { descTitle } from './description';
import AnalyseCtrl from '../ctrl';
export const modeChoices = [

View File

@ -30,12 +30,12 @@ export function descTitle(chapter: boolean) {
}
export function view(study: StudyCtrl, chapter: boolean): VNode | undefined {
const desc = study.desc,
const desc = chapter ? study.chapterDesc : study.studyDesc,
contrib = study.members.canContribute() && !study.gamebookPlay();
if (desc.edit) return edit(desc, chapter ? study.data.chapter.id : study.data.id, chapter);
const isEmpty = desc.text === '-';
if (!desc.text || (isEmpty && !contrib)) return;
return h(`div.study-desc${isEmpty ? '.empty' : ''}`, [
return h(`div.study-desc${chapter ? '.chapter-desc' : ''}${isEmpty ? '.empty' : ''}`, [
contrib && !isEmpty ? h('div.contrib', [
h('span', descTitle(chapter)),
isEmpty ? null : h('a', {

View File

@ -3,7 +3,7 @@ import { NotifCtrl } from './notif';
import { AnalyseData, Redraw } from '../interfaces';
import { StudyPracticeCtrl } from './practice/interfaces';
import { StudyChaptersCtrl } from './studyChapters';
import { DescriptionCtrl } from './chapterDescription';
import { DescriptionCtrl } from './description';
import GamebookPlayCtrl from './gamebook/gamebookPlayCtrl';
import { GamebookOverride } from './gamebook/interfaces';
import { GlyphCtrl } from './studyGlyph';
@ -28,7 +28,8 @@ export interface StudyCtrl {
serverEval: ServerEvalCtrl;
share: any;
tags: any;
desc: DescriptionCtrl;
studyDesc: DescriptionCtrl;
chapterDesc: DescriptionCtrl;
toggleLike(): void;
position(): Position;
isChapterOwner(): boolean;
@ -95,6 +96,7 @@ export interface StudyData {
chapters: StudyChapterMeta[]
chapter: StudyChapter;
secondsSinceUpdate: number;
description?: string;
}
type UserSelection = 'nobody' | 'owner' | 'contributor' | 'member' | 'everyone';

View File

@ -5,7 +5,7 @@ import { StudyCtrl } from '../interfaces';
import { MaybeVNodes } from '../../interfaces';
import { StudyPracticeData, StudyPracticeCtrl } from './interfaces';
import { boolSetting } from '../../boolSetting';
import { view as descView } from '../chapterDescription';
import { view as descView } from '../description';
function selector(data: StudyPracticeData) {
return h('select.selector', {

View File

@ -114,12 +114,13 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
const commentForm: CommentForm = commentFormCtrl(ctrl);
const glyphForm: GlyphCtrl = glyphFormCtrl(ctrl);
const tags = tagsCtrl(ctrl, () => data.chapter, tagTypes);
const desc = new DescriptionCtrl(data.chapter.description, t => {
const studyDesc = new DescriptionCtrl(data.description, t => {
data.description = t;
send("descStudy", t);
}, redraw);
const chapterDesc = new DescriptionCtrl(data.chapter.description, t => {
data.chapter.description = t;
send("descChapter", {
id: vm.chapterId,
description: t
});
send("descChapter", { id: vm.chapterId, desc: t });
}, redraw);
const serverEval = serverEvalCtrl(ctrl, () => vm.chapterId);
@ -131,7 +132,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
function isGamebookPlay() {
return data.chapter.gamebook && vm.gamebookOverride !== 'analyse' &&
(vm.gamebookOverride === 'play' || !members.canContribute());
(vm.gamebookOverride === 'play' || !members.canContribute());
}
if (vm.mode.sticky && !isGamebookPlay()) ctrl.userJump(data.position.path);
@ -164,10 +165,11 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
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;
'position name visibility features settings chapter likes liked'.split(' ').forEach(key => {
'position name visibility features settings chapter likes liked description'.split(' ').forEach(key => {
data[key] = s[key];
});
desc.set(data.chapter.description);
chapterDesc.set(data.chapter.description);
studyDesc.set(data.description);
document.title = data.name;
members.dict(s.members);
chapters.list(s.chapters);
@ -280,7 +282,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
const socketHandlers = {
path(d) {
const position = d.p,
who = d.w;
who = d.w;
setMemberActive(who);
if (!vm.mode.sticky) {
vm.behind++;
@ -297,9 +299,9 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
},
addNode(d) {
const position = d.p,
node = d.n,
who = d.w,
sticky = d.s;
node = d.n,
who = d.w,
sticky = d.s;
setMemberActive(who);
if (vm.toolTab() == 'multiBoard') multiBoard.addNode(d.p, d.n);
if (sticky && !vm.mode.sticky) vm.behind++;
@ -318,13 +320,13 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
if (sticky) data.position.path = newPath;
if ((sticky && vm.mode.sticky) || (
position.path === ctrl.path &&
position.path === treePath.fromNodeList(ctrl.mainline)
position.path === treePath.fromNodeList(ctrl.mainline)
)) ctrl.jump(newPath);
redraw();
},
deleteNode(d) {
const position = d.p,
who = d.w;
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
// deleter already has it done
@ -336,7 +338,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
},
promote(d) {
const position = d.p,
who = d.w;
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
if (who && who.s === sri) return;
@ -361,11 +363,18 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
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);
data.chapter.description = d.desc;
chapterDesc.set(d.desc);
}
redraw();
},
descStudy(d) {
setMemberActive(d.w);
if (d.w && d.w.s === sri) return;
data.description = d.desc;
studyDesc.set(d.desc);
redraw();
},
addChapter(d) {
setMemberActive(d.w);
if (d.s && !vm.mode.sticky) vm.behind++;
@ -391,7 +400,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
},
shapes(d) {
const position = d.p,
who = d.w;
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
if (who && who.s === sri) return;
@ -401,7 +410,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
},
setComment(d) {
const position = d.p,
who = d.w;
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
ctrl.tree.setCommentAt(d.c, position.path);
@ -415,7 +424,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
},
deleteComment(d) {
const position = d.p,
who = d.w;
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
ctrl.tree.deleteCommentAt(d.id, position.path);
@ -423,7 +432,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
},
glyphs(d) {
const position = d.p,
who = d.w;
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
ctrl.tree.setGlyphsAt(d.g, position.path);
@ -431,7 +440,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
},
clock(d) {
const position = d.p,
who = d.w;
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
ctrl.tree.setClockAt(d.c, position.path);
@ -439,7 +448,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
},
forceVariation(d) {
const position = d.p,
who = d.w;
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
ctrl.tree.forceVariationAt(position.path, d.force);
@ -477,7 +486,8 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
serverEval,
share,
tags,
desc,
studyDesc,
chapterDesc,
vm,
relay,
multiBoard,
@ -559,9 +569,9 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
gamebookPlay: () => gamebookPlay,
nextChapter(): StudyChapterMeta | undefined {
const chapters = data.chapters,
currentId = currentChapter().id;
currentId = currentChapter().id;
for (let i in chapters)
if (chapters[i].id === currentId) return chapters[parseInt(i) + 1];
if (chapters[i].id === currentId) return chapters[parseInt(i) + 1];
},
setGamebookOverride(o) {
vm.gamebookOverride = o;

View File

@ -16,7 +16,7 @@ import { view as tagsView } from './studyTags';
import { view as serverEvalView } from './serverEval';
import * as practiceView from './practice/studyPracticeView';
import { playButtons as gbPlayButtons, overrideButton as gbOverrideButton } from './gamebook/gamebookButtons';
import { view as descView } from './chapterDescription';
import { view as descView } from './description';
import AnalyseCtrl from '../ctrl';
import { StudyCtrl, Tab, ToolTab } from './interfaces';
import { MaybeVNodes } from '../interfaces';
@ -195,7 +195,8 @@ export function underboard(ctrl: AnalyseCtrl): MaybeVNodes {
const study = ctrl.study!, toolTab = study.vm.toolTab();
if (study.gamebookPlay()) return [
gbPlayButtons(ctrl),
descView(study),
descView(study, true),
descView(study, false),
metadata(study)
];
let panel;
@ -230,7 +231,8 @@ export function underboard(ctrl: AnalyseCtrl): MaybeVNodes {
}
return [
notifView(study.notif),
descView(study),
descView(study, true),
descView(study, false),
buttons(ctrl),
panel
];

View File

@ -21,6 +21,7 @@ import retroView from './retrospect/retroView';
import practiceView from './practice/practiceView';
import * as gbEdit from './study/gamebook/gamebookEdit';
import * as gbPlay from './study/gamebook/gamebookPlayView';
import { StudyCtrl } from './study/interfaces';
import * as studyView from './study/studyView';
import * as studyPracticeView from './study/practice/studyPracticeView';
import { view as forkView } from './fork'
@ -242,13 +243,16 @@ function forceInnerCoords(ctrl: AnalyseCtrl, v: boolean) {
$('body').toggleClass('coords-in', v).toggleClass('coords-out', !v);
}
function addChapterId(study: StudyCtrl | undefined, cssClass: string) {
return cssClass + (study && study.data.chapter ? '.' + study.data.chapter.id : '');
}
export default function(ctrl: AnalyseCtrl): VNode {
if (ctrl.nvui) return ctrl.nvui.render(ctrl);
const concealOf = makeConcealOf(ctrl),
study = ctrl.study,
showCevalPvs = !(ctrl.retro && ctrl.retro.isSolving()) && !ctrl.practice,
menuIsOpen = ctrl.actionMenu.open,
chapter = study && study.data.chapter,
gamebookPlay = ctrl.gamebookPlay(),
gamebookPlayView = gamebookPlay && gbPlay.render(gamebookPlay),
gamebookEditView = gbEdit.running(ctrl) ? gbEdit.render(ctrl) : undefined,
@ -256,7 +260,7 @@ export default function(ctrl: AnalyseCtrl): VNode {
clocks = !playerBars && renderClocks(ctrl),
gaugeOn = ctrl.showEvalGauge(),
needsInnerCoords = !!gaugeOn || !!playerBars;
return h('main.analyse.variant-' + ctrl.data.game.variant.key + (chapter ? '.' + chapter.id : ''), {
return h('main.analyse.variant-' + ctrl.data.game.variant.key, {
hook: {
insert: vn => {
forceInnerCoords(ctrl, needsInnerCoords);
@ -284,8 +288,8 @@ export default function(ctrl: AnalyseCtrl): VNode {
}
}, [
ctrl.keyboardHelp ? keyboardView(ctrl) : null,
ctrl.study ? studyView.overboard(ctrl.study) : null,
h('div.analyse__board.main-board', {
study ? studyView.overboard(study) : null,
h(addChapterId(study, 'div.analyse__board.main-board'), {
hook: (window.lichess.hasTouchEvents || ctrl.gamebookPlay()) ? undefined : bind('wheel', (e: WheelEvent) => wheel(ctrl, e))
}, [
...(clocks || []),
@ -309,18 +313,18 @@ export default function(ctrl: AnalyseCtrl): VNode {
gamebookPlayView ? null : controls(ctrl),
ctrl.embed ? null : h('div.analyse__underboard', {
hook: (ctrl.synthetic || playable(ctrl.data)) ? undefined : onInsert(elm => serverSideUnderboard(elm, ctrl))
}, ctrl.study ? studyView.underboard(ctrl) : [inputs(ctrl)]),
}, study ? studyView.underboard(ctrl) : [inputs(ctrl)]),
acplView(ctrl),
ctrl.embed ? null : (
ctrl.studyPractice ? studyPracticeView.side(ctrl.study!) :
ctrl.studyPractice ? studyPracticeView.side(study!) :
h('aside.analyse__side', {
hook: onInsert(elm => {
ctrl.opts.$side && ctrl.opts.$side.length && $(elm).replaceWith(ctrl.opts.$side);
$(elm).append($('.streamers').clone().removeClass('none'));
})
},
ctrl.studyPractice ? [studyPracticeView.side(ctrl.study!)] : (
ctrl.study ? [studyView.side(ctrl.study)] : [
ctrl.studyPractice ? [studyPracticeView.side(study!)] : (
study ? [studyView.side(study)] : [
ctrl.forecast ? forecastView(ctrl, ctrl.forecast) : null,
(!ctrl.synthetic && playable(ctrl.data)) ? h('div.back-to-game',
h('a.button.button-empty.text', {