1 import { None, Option, Some } from "@sniptt/monads";
2 import { Component, linkEvent } from "inferno";
3 import { Link } from "inferno-router";
6 AdminPurgeCommunityView,
9 CommunityModeratorView,
17 ModBanFromCommunityView,
22 ModRemoveCommunityView,
25 ModTransferCommunityView,
31 } from "lemmy-js-client";
32 import moment from "moment";
33 import { Subscription } from "rxjs";
34 import { i18n } from "../i18next";
35 import { InitialFetchRequest } from "../interfaces";
36 import { WebSocketService } from "../services";
52 import { HtmlTags } from "./common/html-tags";
53 import { Spinner } from "./common/icon";
54 import { MomentTime } from "./common/moment-time";
55 import { Paginator } from "./common/paginator";
56 import { CommunityLink } from "./community/community-link";
57 import { PersonListing } from "./person/person-listing";
60 type_: ModlogActionType;
61 moderator: Option<PersonSafe>;
66 | ModRemoveCommentView
67 | ModRemoveCommunityView
68 | ModBanFromCommunityView
71 | ModTransferCommunityView
73 | AdminPurgePersonView
74 | AdminPurgeCommunityView
76 | AdminPurgeCommentView;
81 Choices = require("choices.js");
84 interface ModlogState {
85 res: Option<GetModlogResponse>;
86 communityId: Option<number>;
87 communityMods: Option<CommunityModeratorView[]>;
88 communityName: Option<string>,
90 siteRes: GetSiteResponse;
92 filter_action: ModlogActionType,
93 filter_user: Option<number>,
94 filter_mod: Option<number>,
97 export class Modlog extends Component<any, ModlogState> {
98 private isoData = setIsoData(
103 private subscription: Subscription;
104 private userChoices: any;
105 private modChoices: any;
106 private emptyState: ModlogState = {
113 siteRes: this.isoData.site_res,
114 filter_action: ModlogActionType.All,
118 constructor(props: any, context: any) {
119 super(props, context);
120 this.state = this.emptyState;
121 this.handlePageChange = this.handlePageChange.bind(this);
123 this.state.communityId = this.props.match.params.community_id
124 ? Some(Number(this.props.match.params.community_id))
127 this.parseMessage = this.parseMessage.bind(this);
128 this.subscription = wsSubscribe(this.parseMessage);
130 // Only fetch the data if coming from another route
131 if (this.isoData.path == this.context.router.route.match.url) {
132 this.state.res = Some(this.isoData.routeData[0] as GetModlogResponse);
134 if (this.isoData.routeData[1]) {
135 // Getting the moderators
136 let communityRes = Some(
137 this.isoData.routeData[1] as GetCommunityResponse
139 this.state.communityMods = communityRes.map(c => c.moderators);
142 this.state.loading = false;
148 componentDidMount() {
149 this.setupUserFilter();
150 this.setupModFilter();
153 componentWillUnmount() {
155 this.subscription.unsubscribe();
159 buildCombined(res: GetModlogResponse): ModlogType[] {
160 let removed_posts: ModlogType[] = res.removed_posts.map(r => ({
161 id: r.mod_remove_post.id,
162 type_: ModlogActionType.ModRemovePost,
164 moderator: r.moderator,
165 when_: r.mod_remove_post.when_,
168 let locked_posts: ModlogType[] = res.locked_posts.map(r => ({
169 id: r.mod_lock_post.id,
170 type_: ModlogActionType.ModLockPost,
172 moderator: r.moderator,
173 when_: r.mod_lock_post.when_,
176 let stickied_posts: ModlogType[] = res.stickied_posts.map(r => ({
177 id: r.mod_sticky_post.id,
178 type_: ModlogActionType.ModStickyPost,
180 moderator: r.moderator,
181 when_: r.mod_sticky_post.when_,
184 let removed_comments: ModlogType[] = res.removed_comments.map(r => ({
185 id: r.mod_remove_comment.id,
186 type_: ModlogActionType.ModRemoveComment,
188 moderator: r.moderator,
189 when_: r.mod_remove_comment.when_,
192 let removed_communities: ModlogType[] = res.removed_communities.map(r => ({
193 id: r.mod_remove_community.id,
194 type_: ModlogActionType.ModRemoveCommunity,
196 moderator: r.moderator,
197 when_: r.mod_remove_community.when_,
200 let banned_from_community: ModlogType[] = res.banned_from_community.map(
202 id: r.mod_ban_from_community.id,
203 type_: ModlogActionType.ModBanFromCommunity,
205 moderator: r.moderator,
206 when_: r.mod_ban_from_community.when_,
210 let added_to_community: ModlogType[] = res.added_to_community.map(r => ({
211 id: r.mod_add_community.id,
212 type_: ModlogActionType.ModAddCommunity,
214 moderator: r.moderator,
215 when_: r.mod_add_community.when_,
218 let transferred_to_community: ModlogType[] =
219 res.transferred_to_community.map(r => ({
220 id: r.mod_transfer_community.id,
221 type_: ModlogActionType.ModTransferCommunity,
223 moderator: r.moderator,
224 when_: r.mod_transfer_community.when_,
227 let added: ModlogType[] = res.added.map(r => ({
229 type_: ModlogActionType.ModAdd,
231 moderator: r.moderator,
232 when_: r.mod_add.when_,
235 let banned: ModlogType[] = res.banned.map(r => ({
237 type_: ModlogActionType.ModBan,
239 moderator: r.moderator,
240 when_: r.mod_ban.when_,
243 let purged_persons: ModlogType[] = res.admin_purged_persons.map(r => ({
244 id: r.admin_purge_person.id,
245 type_: ModlogActionType.AdminPurgePerson,
248 when_: r.admin_purge_person.when_,
251 let purged_communities: ModlogType[] = res.admin_purged_communities.map(
253 id: r.admin_purge_community.id,
254 type_: ModlogActionType.AdminPurgeCommunity,
257 when_: r.admin_purge_community.when_,
261 let purged_posts: ModlogType[] = res.admin_purged_posts.map(r => ({
262 id: r.admin_purge_post.id,
263 type_: ModlogActionType.AdminPurgePost,
266 when_: r.admin_purge_post.when_,
269 let purged_comments: ModlogType[] = res.admin_purged_comments.map(r => ({
270 id: r.admin_purge_comment.id,
271 type_: ModlogActionType.AdminPurgeComment,
274 when_: r.admin_purge_comment.when_,
277 let combined: ModlogType[] = [];
279 combined.push(...removed_posts);
280 combined.push(...locked_posts);
281 combined.push(...stickied_posts);
282 combined.push(...removed_comments);
283 combined.push(...removed_communities);
284 combined.push(...banned_from_community);
285 combined.push(...added_to_community);
286 combined.push(...transferred_to_community);
287 combined.push(...added);
288 combined.push(...banned);
289 combined.push(...purged_persons);
290 combined.push(...purged_communities);
291 combined.push(...purged_posts);
292 combined.push(...purged_comments);
295 combined.sort((a, b) => b.when_.localeCompare(a.when_));
300 renderModlogType(i: ModlogType) {
302 case ModlogActionType.ModRemovePost: {
303 let mrpv = i.view as ModRemovePostView;
305 mrpv.mod_remove_post.removed.unwrapOr(false)
309 Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link>
311 mrpv.mod_remove_post.reason.match({
312 some: reason => <div>reason: {reason}</div>,
317 case ModlogActionType.ModLockPost: {
318 let mlpv = i.view as ModLockPostView;
320 mlpv.mod_lock_post.locked.unwrapOr(false) ? "Locked " : "Unlocked ",
322 Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link>
326 case ModlogActionType.ModStickyPost: {
327 let mspv = i.view as ModStickyPostView;
329 mspv.mod_sticky_post.stickied.unwrapOr(false)
333 Post <Link to={`/post/${mspv.post.id}`}>{mspv.post.name}</Link>
337 case ModlogActionType.ModRemoveComment: {
338 let mrc = i.view as ModRemoveCommentView;
340 mrc.mod_remove_comment.removed.unwrapOr(false)
345 <Link to={`/post/${mrc.post.id}/comment/${mrc.comment.id}`}>
346 {mrc.comment.content}
351 by <PersonListing person={mrc.commenter} />
353 mrc.mod_remove_comment.reason.match({
354 some: reason => <div>reason: {reason}</div>,
359 case ModlogActionType.ModRemoveCommunity: {
360 let mrco = i.view as ModRemoveCommunityView;
362 mrco.mod_remove_community.removed.unwrapOr(false)
366 Community <CommunityLink community={mrco.community} />
368 mrco.mod_remove_community.reason.match({
369 some: reason => <div>reason: {reason}</div>,
372 mrco.mod_remove_community.expires.match({
374 <div>expires: {moment.utc(expires).fromNow()}</div>
380 case ModlogActionType.ModBanFromCommunity: {
381 let mbfc = i.view as ModBanFromCommunityView;
384 {mbfc.mod_ban_from_community.banned.unwrapOr(false)
389 <PersonListing person={mbfc.banned_person} />
391 <span> from the community </span>,
393 <CommunityLink community={mbfc.community} />
395 mbfc.mod_ban_from_community.reason.match({
396 some: reason => <div>reason: {reason}</div>,
399 mbfc.mod_ban_from_community.expires.match({
401 <div>expires: {moment.utc(expires).fromNow()}</div>
407 case ModlogActionType.ModAddCommunity: {
408 let mac = i.view as ModAddCommunityView;
411 {mac.mod_add_community.removed.unwrapOr(false)
416 <PersonListing person={mac.modded_person} />
418 <span> as a mod to the community </span>,
420 <CommunityLink community={mac.community} />
424 case ModlogActionType.ModTransferCommunity: {
425 let mtc = i.view as ModTransferCommunityView;
428 {mtc.mod_transfer_community.removed.unwrapOr(false)
430 : "Transferred "}{" "}
433 <CommunityLink community={mtc.community} />
437 <PersonListing person={mtc.modded_person} />
441 case ModlogActionType.ModBan: {
442 let mb = i.view as ModBanView;
445 {mb.mod_ban.banned.unwrapOr(false) ? "Banned " : "Unbanned "}{" "}
448 <PersonListing person={mb.banned_person} />
450 mb.mod_ban.reason.match({
451 some: reason => <div>reason: {reason}</div>,
454 mb.mod_ban.expires.match({
456 <div>expires: {moment.utc(expires).fromNow()}</div>
462 case ModlogActionType.ModAdd: {
463 let ma = i.view as ModAddView;
466 {ma.mod_add.removed.unwrapOr(false) ? "Removed " : "Appointed "}{" "}
469 <PersonListing person={ma.modded_person} />
471 <span> as an admin </span>,
474 case ModlogActionType.AdminPurgePerson: {
475 let ap = i.view as AdminPurgePersonView;
477 <span>Purged a Person</span>,
478 ap.admin_purge_person.reason.match({
479 some: reason => <div>reason: {reason}</div>,
484 case ModlogActionType.AdminPurgeCommunity: {
485 let ap = i.view as AdminPurgeCommunityView;
487 <span>Purged a Community</span>,
488 ap.admin_purge_community.reason.match({
489 some: reason => <div>reason: {reason}</div>,
494 case ModlogActionType.AdminPurgePost: {
495 let ap = i.view as AdminPurgePostView;
497 <span>Purged a Post from from </span>,
498 <CommunityLink community={ap.community} />,
499 ap.admin_purge_post.reason.match({
500 some: reason => <div>reason: {reason}</div>,
505 case ModlogActionType.AdminPurgeComment: {
506 let ap = i.view as AdminPurgeCommentView;
509 Purged a Comment from{" "}
510 <Link to={`/post/${ap.post.id}`}>{ap.post.name}</Link>
512 ap.admin_purge_comment.reason.match({
513 some: reason => <div>reason: {reason}</div>,
524 let combined = this.state.res.map(this.buildCombined).unwrapOr([]);
531 <MomentTime published={i.when_} updated={None} />
534 {this.amAdminOrMod ? (
535 <PersonListing person={i.moderator.unwrap()} />
537 <div>{this.modOrAdminText(i.moderator)}</div>
540 <td>{this.renderModlogType(i)}</td>
547 get amAdminOrMod(): boolean {
549 amAdmin(Some(this.state.siteRes.admins)) ||
550 amMod(this.state.communityMods)
554 modOrAdminText(person: Option<PersonSafe>): string {
555 return person.match({
556 some: res => this.isoData.site_res.admins.map(a => a.person.id).includes(res.id) ? i18n.t("admin") : i18n.t("mod"),
561 get documentTitle(): string {
562 return this.state.siteRes.site_view.match({
563 some: siteView => `Modlog - ${siteView.site.name}`,
570 <div className="container">
572 title={this.documentTitle}
573 path={this.context.router.route.match.url}
577 {this.state.loading ? (
585 this.state.communityName.match({
588 className="text-body"
597 <span>{i18n.t("modlog")}</span>
599 <form className="form-inline mr-2">
601 value={this.state.filter_action}
602 onChange={linkEvent(this, this.handleFilterActionChange)}
603 className="custom-select col-4 mb-2"
605 <option disabled aria-hidden="true">{i18n.t("filter_by_action")}</option>
606 <option value={ModlogActionType.All}>{i18n.t("all")}</option>
607 <option value={ModlogActionType.ModRemovePost}>Removing Posts</option>
608 <option value={ModlogActionType.ModLockPost}>Locking Posts</option>
609 <option value={ModlogActionType.ModStickyPost}>Stickying Posts</option>
610 <option value={ModlogActionType.ModRemoveComment}>Removing Comments</option>
611 <option value={ModlogActionType.ModRemoveCommunity}>Removing Communities</option>
612 <option value={ModlogActionType.ModBanFromCommunity}>Banning From Communities</option>
613 <option value={ModlogActionType.ModAddCommunity}>Adding Mod to Community</option>
614 <option value={ModlogActionType.ModTransferCommunity}>Transfering Communities</option>
615 <option value={ModlogActionType.ModAdd}>Adding Mod to Site</option>
616 <option value={ModlogActionType.ModBan}>Banning From Site</option>
619 this.state.siteRes.site_view.match({
620 some: site_view => !site_view.site.hide_modlog_mod_names.unwrapOr(false) && (
623 value={toUndefined(this.state.filter_mod)}>
624 <option>{i18n.t("filter_by_mod")}</option>
632 value={toUndefined(this.state.filter_user)}>
633 <option>{i18n.t("filter_by_user")}</option>
636 <div className="table-responsive">
637 <table id="modlog_table" className="table table-sm table-hover">
638 <thead className="pointer">
640 <th> {i18n.t("time")}</th>
641 <th>{i18n.t("mod")}</th>
642 <th>{i18n.t("action")}</th>
648 page={this.state.page}
649 onChange={this.handlePageChange}
658 handleFilterActionChange(i: Modlog, event: any) {
659 i.setState({ filter_action: event.target.value });
663 handlePageChange(val: number) {
664 this.setState({ page: val });
669 let modlogForm = new GetModlog({
670 community_id: this.state.communityId,
671 page: Some(this.state.page),
672 limit: Some(fetchLimit),
673 auth: auth(false).ok(),
674 type_: this.state.filter_action,
675 other_person_id: this.state.filter_user,
676 mod_person_id: this.state.filter_mod,
678 WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
680 this.state.communityId.match({
682 let communityForm = new GetCommunity({
685 auth: auth(false).ok(),
687 WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
695 let selectId: any = document.getElementById("filter-user");
697 this.userChoices = new Choices(selectId, choicesModLogConfig);
698 this.userChoices.passedElement.element.addEventListener(
701 this.state.filter_user = Some(Number(e.detail.choice.value));
702 this.setState(this.state);
707 this.userChoices.passedElement.element.addEventListener(
709 debounce(async (e: any) => {
711 let users = (await fetchUsers(e.detail.value)).users;
712 this.userChoices.setChoices(
713 users.map(u => {return {
714 value: u.person.id.toString(),
715 label: u.person.name,
733 let selectId: any = document.getElementById("filter-mod");
735 this.modChoices = new Choices(selectId, choicesModLogConfig);
736 this.modChoices.passedElement.element.addEventListener(
739 this.state.filter_mod = Some(Number(e.detail.choice.value));
740 this.setState(this.state);
745 this.modChoices.passedElement.element.addEventListener(
747 debounce(async (e: any) => {
749 let mods = (await fetchUsers(e.detail.value)).users;
750 this.modChoices.setChoices(
751 mods.map(u => {return {
752 value: u.person.id.toString(),
753 label: u.person.name,
769 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
770 let pathSplit = req.path.split("/");
771 let communityId = Some(pathSplit[3]).map(Number);
772 let promises: Promise<any>[] = [];
774 let modlogForm = new GetModlog({
776 limit: Some(fetchLimit),
777 community_id: communityId,
780 type_: ModlogActionType.All,
781 other_person_id: None
784 promises.push(req.client.getModlog(modlogForm));
786 if (communityId.isSome()) {
787 let communityForm = new GetCommunity({
792 promises.push(req.client.getCommunity(communityForm));
794 promises.push(Promise.resolve());
799 parseMessage(msg: any) {
800 let op = wsUserOp(msg);
803 toast(i18n.t(msg.error), "danger");
805 } else if (op == UserOperation.GetModlog) {
806 let data = wsJsonToRes<GetModlogResponse>(msg, GetModlogResponse);
807 this.state.loading = false;
808 window.scrollTo(0, 0);
809 this.state.res = Some(data);
810 this.setState(this.state);
811 } else if (op == UserOperation.GetCommunity) {
812 let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
813 this.state.communityMods = Some(data.moderators);
814 this.state.communityName = Some(data.community_view.community.name);