user mod zone WIP

pull/6698/head
Thibault Duplessis 2020-05-24 22:22:41 -06:00
parent 75c7898008
commit d4a3507070
8 changed files with 148 additions and 137 deletions

View File

@ -321,32 +321,38 @@ final class User(
if (HTTPRequest isEventSource ctx.req) renderModZone(username)
else fuccess(modC.redirect(username))
private def modZoneSegment[A](fu: Fu[Option[A]], name: String, user: UserModel): Source[A, _] =
private def modZoneSegment(fu: Fu[Frag], name: String, user: UserModel): Source[Frag, _] =
Source futureSource {
fu.monSuccess(_.mod zoneSegment name)
.logFailure(lila.log("modZoneSegment").branch(s"$name ${user.id}"))
.map(_.fold(Source.empty[A])(Source.single))
.map(Source.single)
}
protected[controllers] def renderModZone(username: String)(implicit ctx: Context): Fu[Result] = {
env.user.repo withEmails username orFail s"No such user $username" map {
case UserModel.WithEmails(user, emails) =>
val parts =
env.mod.logApi.userHistory(user.id).logTimeIfGt(s"$username logApi.userHistory", 2 seconds) zip
env.plan.api.recentChargesOf(user).logTimeIfGt(s"$username plan.recentChargesOf", 2 seconds) zip
env.report.api.byAndAbout(user, 20).logTimeIfGt(s"$username report.byAndAbout", 2 seconds) zip
env.pref.api.getPref(user).logTimeIfGt(s"$username pref.getPref", 2 seconds) zip
env.playban.api.getRageSit(user.id) flatMap {
case history ~ charges ~ reports ~ pref ~ rageSit =>
env.user.lightUserApi
.preloadMany(reports.userIds)
.logTimeIfGt(s"$username lightUserApi.preloadMany", 2 seconds) inject
html.user.mod.parts(user, history, charges, reports, pref, rageSit).some
import html.user.{ mod => view }
import lila.app.ui.ScalatagsExtensions.LilaFragZero
val modLog = env.mod.logApi.userHistory(user.id).map(view.modLog)
val plan = env.plan.api.recentChargesOf(user).map(view.plan).dmap(~_)
val reportLog = env.report.api
.byAndAbout(user, 20)
.flatMap { rs =>
env.user.lightUserApi.preloadMany(rs.userIds) inject rs
}
.map(view.reportLog(user))
val prefs = env.pref.api.getPref(user).map(view.prefs)
val rageSit = env.playban.api.getRageSit(user.id).map(view.showRageSit)
val actions = env.user.repo.isErased(user) map { erased =>
html.user.mod.actions(user, emails, erased).some
html.user.mod.actions(user, emails, erased)
}
val spyFu = env.security.userSpy(user).logTimeIfGt(s"$username security.userSpy", 2 seconds)
val spyFu = env.security.userSpy(user)
val others = spyFu flatMap { spy =>
val familyUserIds = user.id :: spy.otherUserIds.toList
(isGranted(_.ModNote) ?? env.user.noteApi
@ -355,29 +361,33 @@ final class User(
env.playban.api.bans(familyUserIds).logTimeIfGt(s"$username playban.bans", 2 seconds) zip
lila.security.UserSpy.withMeSortedWithEmails(env.user.repo, user, spy.otherUsers) map {
case notes ~ bans ~ othersWithEmail =>
html.user.mod.otherUsers(user, spy, othersWithEmail, notes, bans).some
html.user.mod.otherUsers(user, spy, othersWithEmail, notes, bans)
}
}
val identification = spyFu map { spy =>
(isGranted(_.Doxing) || (user.lameOrAlt && !user.hasTitle)) option
(isGranted(_.Doxing) || (user.lameOrAlt && !user.hasTitle)) ??
html.user.mod.identification(spy, env.security.printBan.blocks)
}
val irwin = env.irwin.api.reports.withPovs(user) map {
_ ?? { reps =>
html.irwin.report(reps).some
html.irwin.report(reps)
}
}
val assess = env.mod.assessApi.getPlayerAggregateAssessmentWithGames(user.id) flatMap {
_ ?? { as =>
env.user.lightUserApi
.preloadMany(as.games.flatMap(_.userIds)) inject html.user.mod.assessments(as).some
.preloadMany(as.games.flatMap(_.userIds)) inject html.user.mod.assessments(as)
}
}
implicit val extractor = EventSource.EventDataExtractor[Frag](_.render)
Ok.chunked {
Source.single(html.user.mod.menu(user)) merge
modZoneSegment(parts, "parts", user) merge
modZoneSegment(actions, "actions", user) merge
modZoneSegment(modLog, "modLog", user) merge
modZoneSegment(plan, "plan", user) merge
modZoneSegment(reportLog, "reportLog", user) merge
modZoneSegment(prefs, "prefs", user) merge
modZoneSegment(rageSit, "rageSit", user) merge
modZoneSegment(others, "others", user) merge
modZoneSegment(identification, "identification", user) merge
modZoneSegment(irwin, "irwin", user) merge

View File

@ -52,7 +52,7 @@ object Environment
def isChatPanicEnabled = env.chat.panic.enabled
def blockingReportNbOpen: Int = env.report.api.nbOpen.awaitOrElse(10.millis, "nbReports", 0)
def blockingReportNbOpen: Int = env.report.api.nbOpen.awaitOrElse(20.millis, "nbReports", 0)
val spinner: Frag = raw(
"""<div class="spinner"><svg viewBox="0 0 40 40"><circle cx=20 cy=20 r=18 fill="none"></circle></svg></div>"""

View File

@ -170,3 +170,5 @@ trait ScalatagsExtensions {
def titleOrText(v: String)(implicit ctx: Context): Modifier = titleOrText(ctx.blind, v)
}
object ScalatagsExtensions extends ScalatagsExtensions

View File

@ -16,17 +16,13 @@ object mod {
def menu(u: User)(implicit ctx: Context) =
div(id := "mz_menu")(
a(href := "#mz_actions")("Actions"),
canViewRoles(u) option a(href := "#mz_roles")("Roles"),
a(href := "#mz_actions")("Overview"),
a(href := "#mz_irwin")("Irwin"),
a(href := "#mz_assessments")("Evaluation"),
a(href := "#mz_plan", cls := "mz_plan")("Patron"),
a(href := "#mz_mod_log")("Mod log"),
a(href := "#mz_reports_out")("Reports sent"),
a(href := "#mz_reports_in")("Reports received"),
a(href := "#mz_reports")("Reports"),
a(href := "#mz_others")("Accounts"),
a(href := "#mz_identification")("Identification"),
a(href := "#us_profile")("Profile")
a(href := "#mz_identification")("Identification")
)
def actions(u: User, emails: User.Emails, erased: User.Erased)(implicit ctx: Context): Frag =
@ -189,23 +185,6 @@ object mod {
)
)
def parts(
u: User,
history: List[lila.mod.Modlog],
charges: List[lila.plan.Charge],
reports: lila.report.Report.ByAndAbout,
pref: lila.pref.Pref,
rageSit: RageSit
)(implicit ctx: Context) =
frag(
roles(u),
prefs(pref),
plan(charges),
showRageSit(rageSit),
modLog(history),
reportLog(u, reports)
)
def roles(u: User)(implicit ctx: Context) =
canViewRoles(u) option div(cls := "mz_roles")(
(if (isGranted(_.ChangePermission)) a(href := routes.Mod.permissions(u.username)) else span)(
@ -238,7 +217,7 @@ object mod {
span(cls := "text inline")(rageSit.counterView)
)
def plan(charges: List[lila.plan.Charge])(implicit ctx: Context) =
def plan(charges: List[lila.plan.Charge])(implicit ctx: Context): Option[Frag] =
charges.headOption.map { firstCharge =>
div(id := "mz_plan")(
strong(cls := "text", dataIcon := patronIconChar)(
@ -287,9 +266,9 @@ object mod {
)
)
def reportLog(u: User, reports: lila.report.Report.ByAndAbout)(implicit lang: Lang) =
frag(
div(id := "mz_reports_out", cls := "mz_reports")(
def reportLog(u: User)(reports: lila.report.Report.ByAndAbout)(implicit lang: Lang) =
div(id := "mz_reports")(
div(cls := "mz_reports mz_reports--out")(
strong(cls := "text", dataIcon := "!")(
s"Reports sent by ${u.username}",
reports.by.isEmpty option ": nothing to show."
@ -308,7 +287,7 @@ object mod {
}
}
),
div(id := "mz_reports_in", cls := "mz_reports")(
div(cls := "mz_reports mz_reports--in")(
strong(cls := "text", dataIcon := "!")(
s"Reports concerning ${u.username}",
reports.about.isEmpty option ": nothing to show."

View File

@ -93,7 +93,7 @@ object header {
a(
cls := "btn-rack__btn mod-zone-toggle",
href := routes.User.mod(u.username),
titleOrText("Mod zone"),
titleOrText("Mod zone (Hotkey: m)"),
dataIcon := ""
),
a(

View File

@ -29,6 +29,9 @@
%box-neat {
@extend %box-shadow, %box-radius;
}
%box-neat-top {
@extend %box-shadow, %box-radius-top;
}
%box-neat-force {
@extend %box-shadow, %box-radius-force;
}

View File

@ -13,41 +13,43 @@
}
}
#mz_actions { order: 1; }
#mz_roles { order: 2; }
#mz_preferences { order: 3; }
#mz_irwin { order: 4; }
#mz_assessments { order: 5; }
#mz_plan { order: 6; }
#mz_mod_log { order: 7; }
#mz_reports_out { order: 8; }
#mz_reports_in { order: 9; }
#mz_others { order: 10; }
#mz_identification { order: 11; }
#mz_actions { order: 1 }
#mz_sitdccounter { order: 2 }
#mz_roles { order: 3 }
#mz_preferences { order: 4 }
#mz_plan { order: 5 }
#mz_irwin { order: 6 }
#mz_assessments { order: 7 }
#mz_mod_log { order: 8 }
#mz_reports { order: 9 }
#mz_others { order: 10 }
#mz_identification { order: 11 }
#mz_menu {
@extend %flex-center;
border-bottom: 3px solid $c-brag;
height: 4em;
@extend %flex-center, %box-neat-top;
position: fixed;
bottom: 0;
background: $c-brag;
z-index: 1;
// border-top: 1px solid $c-brag;
// border-width: 3px 2px 0 2px;
padding: 0;
a {
padding: .9em 1.2em;
color: $c-font;
padding: 1.2em 1.2em;
color: $c-brag-over;
font-size: 1.1em;
&.disabled {
opacity: 0.6;
font-weight: bold;
opacity: 0.5;
&.available {
opacity: 1;
}
&:not(.disabled):hover {
color: $c-brag;
background: mix(white, $c-brag, 20%);
}
i {
margin-right: .3em;
opacity: .5;
}
}
.mod-zone.stick-menu & {
@extend %box-neat;
position: fixed;
top: 0;
height: auto;
background: $c-bg-box;
z-index: 1;
}
}
@ -172,20 +174,26 @@
color: $c-link;
}
}
.mz_reports form {
display: block;
padding: 0 10px;
}
.mz_reports button {
padding: 0;
margin-left: -2px;
}
.mz_reports button:not(:hover) {
border-color: transparent;
background: transparent;
}
.mz_reports button strong {
display: inline;
#mz_reports {
@extend %flex-wrap;
.mz_reports {
flex: 1 0 50ch;
}
form {
display: block;
padding: 0 10px;
}
button {
padding: 0;
margin-left: -2px;
&:not(:hover) {
border-color: transparent;
background: transparent;
}
strong {
display: inline;
}
}
}
#mz_assessments table {
margin-top: 10px;

View File

@ -1,41 +1,39 @@
var tablesort = require('tablesort');
function streamLoad(opts) {
var source = new EventSource(opts.url), first = true;
source.addEventListener('message', function(e) {
var newHtml = e.data;
if (!newHtml) return;
if (first) {
first = false;
opts.node.innerHTML = newHtml;
} else {
opts.node.innerHTML += newHtml;
}
let source = new EventSource(opts.url);
source.addEventListener('message', e => {
if (!e.data) return;
opts.node.innerHTML += e.data;
opts.callback();
});
source.onerror = function() { source.close(); };
source.onerror = () => source.close();
}
var $toggle = $('.user-show .mod-zone-toggle');
var $zone = $('.user-show .mod-zone');
let $toggle = $('.user-show .mod-zone-toggle');
let $zone = $('.user-show .mod-zone');
function loadZone() {
$('.user-show').css('overflow', 'visible'); // required for mz_menu to be displayed
$zone.html(lichess.spinnerHtml).removeClass('none');
$('#main-wrap').addClass('full-screen-force');
$zone.html('');
streamLoad({
node: $zone[0],
url: $toggle.attr('href'),
callback: lichess.debounce(function() {
userMod($zone);
}, 300)
callback: lichess.debounce(() => userMod($zone), 300)
});
window.addEventListener('scroll', onScroll);
window.addEventListener('scroll', onScroll);
scrollTo('.mod-zone');
}
function unloadZone() {
$zone.addClass('none');
$('#main-wrap').removeClass('full-screen-force');
window.removeEventListener('scroll', onScroll);
window.removeEventListener('scroll', onScroll);
scrollTo('#top');
}
function scrollTo(el) {
window.scrollTo(0, document.querySelector(el).offsetTop);
}
$toggle.click(function() {
@ -48,10 +46,20 @@ function userMod($zone) {
lichess.pubsub.emit('content_loaded');
$('#mz_menu .mz_plan').toggleClass('disabled', !$('#mz_plan').length);
$('#mz_menu > a:not(.available)').each(function() {
$(this).toggleClass('available', !!$($(this).attr('href')).length);
});
$('#mz_menu > a:not(.hotkey)').each(function(i) {
const id = this.href.replace(/.+(#\w+)$/, '$1'), n = '' + (i + 1);
$(this).addClass('hotkey').prepend(`<i>${n}</i>`);
Mousetrap.bind(n, () => {
console.log(id, n);
scrollTo(id);
});
});
$zone.find('form.xhr').submit(function() {
$(this).find('input').attr('disabled', true);
$zone.find('form.xhr:not(.ready)').submit(function() {
$(this).addClass('ready').find('input').attr('disabled', true);
$.ajax({
...lichess.formAjax($(this)),
success: function(html) {
@ -78,28 +86,6 @@ function userMod($zone) {
});
}
(function(){
var cleanNumber = function(i) {
return i.replace(/[^\-?0-9.]/g, '');
},
compareNumber = function(a, b) {
a = parseFloat(a);
b = parseFloat(b);
a = isNaN(a) ? 0 : a;
b = isNaN(b) ? 0 : b;
return a - b;
};
tablesort.extend('number', function(item) {
return item.match(/^[-+]?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/); // Number
}, function(a, b) {
return compareNumber(cleanNumber(b), cleanNumber(a));
});
}());
$zone.find('#mz_others table').each(function() {
tablesort(this, {
descending: true
@ -109,7 +95,30 @@ function userMod($zone) {
const onScroll = e => requestAnimationFrame(() => {
if ($zone.hasClass('none')) return;
$zone.toggleClass('stick-menu', window.scrollY > 220);
$zone.toggleClass('stick-menu', window.scrollY > 200);
});
(function(){
var cleanNumber = function(i) {
return i.replace(/[^\-?0-9.]/g, '');
},
compareNumber = function(a, b) {
a = parseFloat(a);
b = parseFloat(b);
a = isNaN(a) ? 0 : a;
b = isNaN(b) ? 0 : b;
return a - b;
};
tablesort.extend('number', function(item) {
return item.match(/^[-+]?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/); // Number
}, function(a, b) {
return compareNumber(cleanNumber(b), cleanNumber(a));
});
}());
if (location.search.startsWith('?mod')) $toggle.click();
Mousetrap.bind('m', () => $toggle.click());