more new chat WIP

pull/2012/head
Thibault Duplessis 2016-06-14 13:13:41 +02:00
parent 2f804acfec
commit 1f8528f8c7
19 changed files with 364 additions and 193 deletions

View File

@ -225,6 +225,13 @@ object Round extends LilaController with TheftPrevention {
text => Env.round.noteApi.set(gameId, me.id, text.trim take 10000))
}
def readNote(gameId: String) = Auth { implicit ctx =>
me =>
Env.round.noteApi.get(gameId, me.id) map { text =>
Ok(text)
}
}
private def sides(pov: Pov, isPlayer: Boolean)(implicit ctx: Context) =
myTour(pov.game.tournamentId, isPlayer) zip
(pov.game.simulId ?? Env.simul.repo.find) zip

View File

@ -31,6 +31,9 @@ trait I18nHelper {
def i18nJsObject(keys: I18nKey*)(implicit lang: Lang): JsObject =
i18nEnv.jsDump.keysToObject(keys, lang)
def i18nOptionJsObject(keys: Option[I18nKey]*)(implicit lang: Lang): JsObject =
i18nEnv.jsDump.keysToObject(keys.flatten, lang)
def langName(lang: Lang): Option[String] = langName(lang.language)
def langName(lang: String): Option[String] = LangList name lang

View File

@ -23,7 +23,7 @@ tablebaseEndpoint: "@tablebaseEndpoint"
};
}
@myChat.map { c =>
@chat.js(c, name = trans.spectatorRoom.str(), notes = true)
@chat.js(c, name = trans.spectatorRoom.str(), withNote = true)
}
}

View File

@ -1,14 +1,13 @@
@(c: lila.chat.UserChat.Mine, name: String, notes: Boolean = false, writeable: Boolean = true)(implicit ctx: Context)
@(c: lila.chat.UserChat.Mine, name: String, withNote: Boolean = false, writeable: Boolean = true)(implicit ctx: Context)
@embedJs {
lichess.makeChat('chat', {
lines: @Html(J.stringify(lila.chat.JsonView(c.chat))),
i18n: @Html(J.stringify(i18nJsObject(
trans.talkInChat,
trans.toggleTheChat
))),
i18n: @jsI18n(withNote = withNote),
id: "@c.chat.id",
name: "@name",
userId: @Html(ctx.userId.fold("null")(id => s""""$id"""")),
writeable: @writeable,
noteId: @if(withNote) {"@c.chat.id.take(8)"} else {null},
kobold: @ctx.troll,
mod: @isGranted(_.MarkTroll),
timeout: @c.timeout,

View File

@ -0,0 +1,7 @@
@(withNote: Boolean = false)(implicit ctx: Context)
@Html(J.stringify(i18nOptionJsObject(
trans.talkInChat.some,
trans.toggleTheChat.some,
withNote option trans.notes,
withNote option trans.typePrivateNotesHere
)))

View File

@ -13,7 +13,7 @@ i18n: @jsI18n()
});
}
@myChat.map { c =>
@chat.js(c, name = trans.spectatorRoom.str(), notes = true)
@chat.js(c, name = trans.spectatorRoom.str(), withNote = true)
}
}

View File

@ -142,6 +142,7 @@ GET /$gameId<\w{8}>/$color<white|black>/sides/watcher controllers.Round.sidesW
GET /$gameId<\w{8}>/$color<white|black>/sides/player controllers.Round.sidesPlayer(gameId: String, color: String)
GET /$gameId<\w{8}>/others controllers.Round.others(gameId: String)
GET /$gameId<\w{8}>/continue/:mode controllers.Round.continue(gameId: String, mode: String)
GET /$gameId<\w{8}>/note controllers.Round.readNote(gameId: String)
POST /$gameId<\w{8}>/note controllers.Round.writeNote(gameId: String)
GET /$gameId<\w{8}>/mini controllers.Round.mini(gameId: String, color: String = "white")
GET /$gameId<\w{8}>/$color<white|black>/mini controllers.Round.mini(gameId: String, color: String)

View File

@ -14,7 +14,6 @@ final class Env(
system: ActorSystem,
scheduler: lila.common.Scheduler,
roundJsonView: lila.round.JsonView,
noteApi: lila.round.NoteApi,
forecastApi: lila.round.ForecastApi,
relationApi: lila.relation.RelationApi,
bookmarkApi: lila.bookmark.BookmarkApi,
@ -96,7 +95,6 @@ final class Env(
val roundApi = new RoundApiBalancer(
api = new RoundApi(
jsonView = roundJsonView,
noteApi = noteApi,
forecastApi = forecastApi,
bookmarkApi = bookmarkApi,
getTourAndRanks = getTourAndRanks,
@ -135,7 +133,6 @@ object Env {
getSimulName = lila.simul.Env.current.cached.name,
getTournamentName = lila.tournament.Env.current.cached.name,
roundJsonView = lila.round.Env.current.jsonView,
noteApi = lila.round.Env.current.noteApi,
forecastApi = lila.round.Env.current.forecastApi,
relationApi = lila.relation.Env.current.api,
bookmarkApi = lila.bookmark.Env.current.api,

View File

@ -15,7 +15,6 @@ import lila.user.User
private[api] final class RoundApi(
jsonView: JsonView,
noteApi: lila.round.NoteApi,
forecastApi: lila.round.ForecastApi,
bookmarkApi: lila.bookmark.BookmarkApi,
getTourAndRanks: Game => Fu[Option[TourAndRanks]],
@ -29,14 +28,12 @@ private[api] final class RoundApi(
initialFen = initialFen) zip
getTourAndRanks(pov.game) zip
(pov.game.simulId ?? getSimul) zip
(ctx.me ?? (me => noteApi.get(pov.gameId, me.id))) zip
forecastApi.loadForDisplay(pov) map {
case ((((json, tourOption), simulOption), note), forecast) => (
case (((json, tourOption), simulOption), forecast) => (
blindMode _ compose
withTournament(pov, tourOption)_ compose
withSimul(pov, simulOption)_ compose
withSteps(pov, initialFen)_ compose
withNote(note)_ compose
withBookmark(ctx.me ?? { bookmarkApi.bookmarked(pov.game, _) })_ compose
withForecastCount(forecast.map(_.steps.size))_
)(json)
@ -52,13 +49,11 @@ private[api] final class RoundApi(
withMoveTimes = false,
withDivision = false) zip
getTourAndRanks(pov.game) zip
(pov.game.simulId ?? getSimul) zip
(ctx.me ?? (me => noteApi.get(pov.gameId, me.id))) map {
case (((json, tourOption), simulOption), note) => (
(pov.game.simulId ?? getSimul) map {
case ((json, tourOption), simulOption) => (
blindMode _ compose
withTournament(pov, tourOption)_ compose
withSimul(pov, simulOption)_ compose
withNote(note)_ compose
withBookmark(ctx.me ?? { bookmarkApi.bookmarked(pov.game, _) })_ compose
withSteps(pov, initialFen)_
)(json)
@ -78,13 +73,11 @@ private[api] final class RoundApi(
withMoveTimes = withMoveTimes,
withDivision = withDivision) zip
getTourAndRanks(pov.game) zip
(pov.game.simulId ?? getSimul) zip
(ctx.me ?? (me => noteApi.get(pov.gameId, me.id))) map {
case (((json, tourOption), simulOption), note) => (
(pov.game.simulId ?? getSimul) map {
case ((json, tourOption), simulOption) => (
blindMode _ compose
withTournament(pov, tourOption)_ compose
withSimul(pov, simulOption)_ compose
withNote(note)_ compose
withBookmark(ctx.me ?? { bookmarkApi.bookmarked(pov.game, _) })_ compose
withTree(pov, analysis, initialFen, withOpening = withOpening)_ compose
withAnalysis(pov.game, analysis)_
@ -120,9 +113,6 @@ private[api] final class RoundApi(
variant = pov.game.variant,
initialFen = initialFen | pov.game.variant.initialFen))
private def withNote(note: String)(json: JsObject) =
if (note.isEmpty) json else json + ("note" -> JsString(note))
private def withBookmark(v: Boolean)(json: JsObject) =
if (v) json + ("bookmarked" -> JsBoolean(true)) else json

View File

@ -115,8 +115,10 @@ lichess.shepherd = function(f) {
});
};
lichess.makeChat = function(id, data) {
var isDev = $('body').data('dev');
lichess.loadCss('/assets/stylesheets/chat.css');
if (data.mod) lichess.loadCss('/assets/stylesheets/chat.mod.css');
lichess.loadScript('/assets/compiled/lichess.chat.js').then(function() {
lichess.loadScript("/assets/compiled/lichess.chat" + (isDev ? '' : '.min') + '.js').done(function() {
LichessChat(document.getElementById(id), data);
});
};

View File

@ -0,0 +1,71 @@
div.mchat {
height: 350px;
display: flex;
flex-flow: column nowrap;
}
div.mchat .chat_tabs {
display: flex;
}
div.mchat .chat_tabs .tab {
flex: 1 1 auto;
border: 1px solid #c0c0c0;
border-width: 0 1px 1px 0;
padding: 3px 8px;
text-align: center;
white-space: nowrap;
overflow: hidden;
}
div.mchat .chat_tabs .tab:not(.active) {
background: #e0e0e0;
opacity: 0.7;
cursor: pointer;
}
div.mchat .chat_tabs .tab:not(.active):hover {
opacity: 1;
}
div.mchat .chat_tabs .tab input {
cursor: pointer;
}
div.mchat .chat_tabs .tab.discussion {
display: flex;
justify-content: space-between;
}
div.mchat .chat_tabs .tab span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
div.mchat .chat_tabs .tab:last-child {
border-right: none;
}
div.mchat .chat_tabs .tab.active {
border-bottom-color: transparent;
}
div.mchat .content {
display: flex;
flex-flow: column nowrap;
height: 100%;
}
div.mchat .note textarea {
height: 100%;
padding: 8px;
box-sizing: border-box;
border: none;
background-color: transparent;
color: #707070;
line-height: 1.7em;
outline: none;
}
div.mchat .lichess_say {
border-width: 1px 0 0 0;
}
div.mchat .deleted {
opacity: 0.5;
}
div.mchat .system {
display: block;
opacity: 0.8;
font-style: italic;
text-align: center;
font-size: 0.8em;
}

View File

@ -32,7 +32,6 @@ body.dark div.mchat.mod li i.mod {
}
div.mchat.mod .moderation {
justify-content: flex-start;
overflow: auto;
}

View File

@ -733,28 +733,6 @@ div.chat_panels .preferences a.prefs {
display: block;
text-align: center;
}
div.mchat {
height: 350px;
}
div.mchat .discussion,
div.mchat .moderation {
display: flex;
flex-flow: column nowrap;
height: 100%;
}
div.mchat .lichess_say {
border-width: 1px 0 0 0;
}
div.mchat .deleted {
opacity: 0.5;
}
div.mchat .system {
display: block;
opacity: 0.8;
font-style: italic;
text-align: center;
font-size: 0.8em;
}
.notification .message_infos,
.notification .game_infos,
div.side_box .game_infos {

View File

@ -1,12 +1,16 @@
var m = require('mithril');
var makeModeration = require('./moderation').ctrl;
var makeNote = require('./note').ctrl;
module.exports = function(opts) {
var lines = opts.lines;
var data = {
id: opts.id,
name: opts.name,
lines: opts.lines
};
var vm = {
chatName: opts.name,
enabled: m.prop(!lichess.storage.get('nochat')),
writeable: m.prop(opts.writeable),
isTroll: opts.kobold,
@ -14,11 +18,14 @@ module.exports = function(opts) {
isTimeout: m.prop(opts.timeout),
placeholderKey: 'talkInChat',
moderating: m.prop(null),
loading: m.prop(false),
tab: m.prop('discussion'),
loading: m.prop(false)
};
var trans = lichess.trans(opts.i18n);
var onTimeout = function(username) {
lines.forEach(function(l) {
data.lines.forEach(function(l) {
if (l.u === username) l.d = true;
});
if (username.toLowerCase() === opts.userId) vm.isTimeout(true);
@ -33,8 +40,8 @@ module.exports = function(opts) {
};
var onMessage = function(line) {
if (lines.length > 64) lines.shift();
lines.push(line);
if (data.lines.length > 64) data.lines.shift();
data.lines.push(line);
m.redraw();
};
@ -43,6 +50,11 @@ module.exports = function(opts) {
send: lichess.pubsub.emit('socket.send')
}) : null;
var note = opts.noteId ? makeNote({
id: opts.noteId,
trans: trans
}) : null;
var setWriteable = function(v) {
vm.writeable(v);
m.redraw();
@ -54,8 +66,10 @@ module.exports = function(opts) {
lichess.pubsub.on('chat.writeable', setWriteable);
return {
lines: lines,
data: data,
vm: vm,
moderation: moderation,
note: note,
post: function(text) {
text = $.trim(text);
if (!text) return false;
@ -66,8 +80,7 @@ module.exports = function(opts) {
lichess.pubsub.emit('socket.send')('talk', text);
return false;
},
moderation: moderation,
trans: lichess.trans(opts.i18n),
trans: trans,
setEnabled: function(v) {
vm.enabled(v);
if (!v) lichess.storage.set('nochat', 1);

View File

@ -0,0 +1,74 @@
var m = require('mithril');
var moderationView = require('./moderation').view;
function renderLine(ctrl) {
return function(line) {
if (line.u === 'lichess') return m('li', m('em.system', line.t));
return m('li', {
'data-username': line.u
}, [
ctrl.vm.isMod ? moderationView.lineAction() : null,
m.trust($.userLinkLimit(line.u, 14)),
line.d ? m('em.deleted', '<deleted>') : line.t
]);
};
}
function sameLines(l1, l2) {
return l1.d && l2.d && l1.u === l2.u;
}
function selectLines(lines) {
var prev, ls = [];
lines.forEach(function(l) {
if (!prev || !sameLines(prev, l))
if (!l.r || ctrl.vm.isTroll) ls.push(l);
prev = l;
});
return ls;
}
function input(ctrl) {
var placeholder;
if (ctrl.vm.isTimeout()) placeholder = 'You have been timed out.';
else if (!ctrl.vm.writeable()) placeholder = 'Invited members only.';
else placeholder = ctrl.trans(ctrl.vm.placeholderKey);
return m('input', {
class: 'lichess_say',
placeholder: placeholder,
autocomplete: 'off',
maxlength: 140,
disabled: ctrl.vm.isTimeout() || !ctrl.vm.writeable(),
config: function(el, isUpdate) {
if (!isUpdate) el.addEventListener('keypress', function(e) {
if (e.which == 10 || e.which == 13) {
ctrl.post(e.target.value);
e.target.value = '';
}
});
}
})
}
module.exports = {
view: function(ctrl) {
if (!ctrl.vm.enabled()) return null;
return [
m('ol.messages.content.scroll-shadow-soft', {
config: function(el, isUpdate, ctx) {
if (!isUpdate && ctrl.moderation) $(el).on('click', 'i.mod', function(e) {
ctrl.moderation.open($(e.target).parent().data('username'));
});
var autoScroll = (el.scrollTop === 0 || (el.scrollTop > (el.scrollHeight - el.clientHeight - 100)));
el.scrollTop = 999999;
if (autoScroll) setTimeout(function() {
el.scrollTop = 999999;
}, 500);
}
},
selectLines(ctrl.data.lines).map(renderLine(ctrl))
),
input(ctrl)
];
}
};

View File

@ -50,7 +50,7 @@ module.exports = {
if (ctrl.vm.loading()) return m.trust(lichess.spinnerHtml);
var data = ctrl.vm.data();
if (!data) return;
return m('div.moderation', [
return [
m('div.top', [
m('span.toggle_chat', {
'data-icon': 'L',
@ -60,66 +60,68 @@ module.exports = {
'data-icon': '',
}, m.trust($.userLink(data.username)))
]),
m('div.infos.block', [
[
data.games + ' games',
data.troll ? 'TROLL' : null,
data.engine ? 'ENGINE' : null,
data.booster ? 'BOOSTER' : null
].filter(function(x) {
return x;
}).join(' • '),
' • ',
m('a[href=/@/' + data.username + '?mod]', 'profile'),
' • ',
m('a[href=/mod/' + data.username + '/communication]', 'coms')
]),
m('div.timeout.block', [
m('h2', 'Timeout 10 minutes for'),
ctrl.reasons.map(function(r) {
return m('a.text[data-icon=p]', {
onclick: function() {
ctrl.timeout(r.key)
}
}, r.name);
}),
data.troll ? null : m('div.shadowban', [
'Or ',
m('form', {
action: '/mod/' + data.id + '/troll?set=1',
method: 'post',
config: function(el, isUpdate) {
if (!isUpdate) $(el).submit(function() {
$.post($(this).attr('action'), function() {
ctrl.open(data.username);
m('div.content.moderation', [
m('div.infos.block', [
[
data.games + ' games',
data.troll ? 'TROLL' : null,
data.engine ? 'ENGINE' : null,
data.booster ? 'BOOSTER' : null
].filter(function(x) {
return x;
}).join(' • '),
' • ',
m('a[href=/@/' + data.username + '?mod]', 'profile'),
' • ',
m('a[href=/mod/' + data.username + '/communication]', 'coms')
]),
m('div.timeout.block', [
m('h2', 'Timeout 10 minutes for'),
ctrl.reasons.map(function(r) {
return m('a.text[data-icon=p]', {
onclick: function() {
ctrl.timeout(r.key)
}
}, r.name);
}),
data.troll ? null : m('div.shadowban', [
'Or ',
m('form', {
action: '/mod/' + data.id + '/troll?set=1',
method: 'post',
config: function(el, isUpdate) {
if (!isUpdate) $(el).submit(function() {
$.post($(this).attr('action'), function() {
ctrl.open(data.username);
});
ctrl.vm.loading(true);
m.redraw();
return false;
});
ctrl.vm.loading(true);
m.redraw();
return false;
});
}
}, m('button.button[type=submit]', 'shadowban'))
])
]),
m('div.history.block', [
m('h2', 'Timeout history'),
m('table', m('tbody.slist', {
config: function(el, isUpdate) {
if (!isUpdate) $('body').trigger('lichess.content_loaded');
}
}, m('button.button[type=submit]', 'Permanently shadowban'))
}, data.history.map(function(e) {
return m('tr', [
m('td.reason', e.reason),
m('td.mod', e.mod),
m('td', m('time', {
class: "moment",
'data-format': isToday(e.date) ? 'LT' : 'DD/MM/YY',
datetime: new Date(e.date).toISOString()
}))
]);
})))
])
]),
m('div.history.block', [
m('h2', 'Timeout history'),
m('table', m('tbody.slist', {
config: function(el, isUpdate) {
if (!isUpdate) $('body').trigger('lichess.content_loaded');
}
}, data.history.map(function(e) {
return m('tr', [
m('td.reason', e.reason),
m('td.mod', e.mod),
m('td', m('time', {
class: "moment",
'data-format': isToday(e.date) ? 'LT' : 'DD/MM/YY',
datetime: new Date(e.date).toISOString()
}))
]);
})))
])
]);
];
}
}
};

View File

@ -0,0 +1,46 @@
var m = require('mithril');
var xhr = require('./xhr');
module.exports = {
ctrl: function(opts) {
var id = opts.id;
var vm = {
text: m.prop(null)
};
var doPost = $.fp.debounce(function() {
xhr.setNote(id, vm.text());
}, 1000);
return {
id: id,
vm: vm,
trans: opts.trans,
fetch: function() {
xhr.getNote(id).then(function(t) {
vm.text(t || '');
m.redraw();
});
},
post: function(text) {
vm.text(text);
doPost();
}
}
},
view: function(ctrl) {
var text = ctrl.vm.text();
if (text === null) return m('div.loading', {
config: function(el, isUpdate) {
if (!isUpdate) ctrl.fetch();
}
}, m.trust(lichess.spinnerHtml));
return m('textarea', {
placeholder: ctrl.trans('typePrivateNotesHere'),
config: function(el, isUpdate) {
if (isUpdate) return;
$(el).val(text).on('change keyup paste', function() {
ctrl.post($(el).val());
});
}
});
}
};

View File

@ -1,92 +1,52 @@
var m = require('mithril');
var moderationView = require('./moderation').view;
var discussionView = require('./discussion').view;
var noteView = require('./note').view;
var deletedDom = m('em.deleted', '<deleted>');
function renderLine(ctrl) {
return function(line) {
if (line.u === 'lichess') return m('li', m('em.system', line.t));
return m('li', {
'data-username': line.u
}, [
ctrl.vm.isMod ? moderationView.lineAction() : null,
m.trust($.userLinkLimit(line.u, 14)),
line.d ? deletedDom : line.t
]);
};
function tabName(ctrl, t) {
switch (t) {
case 'discussion':
return [
m('span', ctrl.data.name),
m('input', {
type: 'checkbox',
class: 'toggle_chat',
title: ctrl.trans('toggleTheChat'),
onchange: m.withAttr('checked', ctrl.setEnabled),
checked: ctrl.vm.enabled()
})
];
case 'note':
return ctrl.trans('notes');
}
}
function sameLines(l1, l2) {
return l1.d && l2.d && l1.u === l2.u;
function tabContent(ctrl, t) {
if (t === 'note' && ctrl.note)
return noteView(ctrl.note);
return discussionView(ctrl);
}
function selectLines(lines) {
var prev, ls = [];
lines.forEach(function(l) {
if (!prev || !sameLines(prev, l))
if (!l.r || ctrl.vm.isTroll) ls.push(l);
prev = l;
});
return ls;
}
function input(ctrl) {
var placeholder;
if (ctrl.vm.isTimeout()) placeholder = 'You have been timed out.';
else if (!ctrl.vm.writeable()) placeholder = 'Invited members only.';
else placeholder = ctrl.trans(ctrl.vm.placeholderKey);
return m('input', {
class: 'lichess_say',
placeholder: placeholder,
autocomplete: 'off',
maxlength: 140,
disabled: ctrl.vm.isTimeout() || !ctrl.vm.writeable(),
config: function(el, isUpdate) {
if (!isUpdate) el.addEventListener('keypress', function(e) {
if (e.which == 10 || e.which == 13) {
ctrl.post(e.target.value);
e.target.value = '';
}
});
}
})
}
function discussion(ctrl) {
return m('div.discussion', [
m('div.top', [
m('span', ctrl.vm.chatName),
m('input', {
type: 'checkbox',
class: 'toggle_chat',
title: ctrl.trans('toggleTheChat'),
onchange: m.withAttr('checked', ctrl.setEnabled),
checked: ctrl.vm.enabled()
})
]),
ctrl.vm.enabled() ? [
m('ol.messages.content.scroll-shadow-soft', {
config: function(el, isUpdate, ctx) {
if (!isUpdate && ctrl.moderation) $(el).on('click', 'i.mod', function(e) {
ctrl.moderation.open($(e.target).parent().data('username'));
});
var autoScroll = (el.scrollTop === 0 || (el.scrollTop > (el.scrollHeight - el.clientHeight - 100)));
el.scrollTop = 999999;
if (autoScroll) setTimeout(function() {
el.scrollTop = 999999;
}, 500);
}
function normalView(ctrl) {
var tab = ctrl.vm.tab();
var tabs = ['discussion'];
if (ctrl.note) tabs.push('note');
return [
m('div.chat_tabs', tabs.map(function(t) {
return m('div', {
onclick: function() {
ctrl.vm.tab(t);
},
selectLines(ctrl.lines).map(renderLine(ctrl))
),
input(ctrl)
] : null
])
class: 'tab ' + t + (tab === t ? ' active' : '')
}, tabName(ctrl, t));
})),
m('div.content.' + tab, tabContent(ctrl, tab))
];
}
module.exports = function(ctrl) {
return m('div', {
class: 'mchat' + (ctrl.vm.isMod ? ' mod' : '')
},
moderationView.ui(ctrl.moderation) || discussion(ctrl));
moderationView.ui(ctrl.moderation) || normalView(ctrl));
};

View File

@ -18,6 +18,28 @@ function userModInfo(username) {
});
}
function noteUrl(id) {
return uncache('/' + id + '/note');
}
function getNote(id) {
return $.get(noteUrl(id));
}
function setNote(id, text) {
return m.request({
background: true,
method: 'POST',
url: noteUrl(id),
data: {
text: text
},
config: xhrConfig
});
}
module.exports = {
userModInfo: userModInfo
userModInfo: userModInfo,
getNote: getNote,
setNote: setNote
};