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";
51 import { HtmlTags } from "./common/html-tags";
52 import { Spinner } from "./common/icon";
53 import { MomentTime } from "./common/moment-time";
54 import { Paginator } from "./common/paginator";
55 import { CommunityLink } from "./community/community-link";
56 import { PersonListing } from "./person/person-listing";
59 type_: ModlogActionType;
60 moderator: Option<PersonSafe>;
65 | ModRemoveCommentView
66 | ModRemoveCommunityView
67 | ModBanFromCommunityView
70 | ModTransferCommunityView
72 | AdminPurgePersonView
73 | AdminPurgeCommunityView
75 | AdminPurgeCommentView;
80 Choices = require("choices.js");
83 interface ModlogState {
84 res: Option<GetModlogResponse>;
85 communityId: Option<number>;
86 communityMods: Option<CommunityModeratorView[]>;
87 communityName: Option<string>;
89 siteRes: GetSiteResponse;
91 filter_action: ModlogActionType;
92 filter_user: Option<number>;
93 filter_mod: Option<number>;
96 export class Modlog extends Component<any, ModlogState> {
97 private isoData = setIsoData(
102 private subscription: Subscription;
103 private userChoices: any;
104 private modChoices: any;
105 private emptyState: ModlogState = {
112 siteRes: this.isoData.site_res,
113 filter_action: ModlogActionType.All,
117 constructor(props: any, context: any) {
118 super(props, context);
119 this.state = this.emptyState;
120 this.handlePageChange = this.handlePageChange.bind(this);
122 this.state.communityId = this.props.match.params.community_id
123 ? Some(Number(this.props.match.params.community_id))
126 this.parseMessage = this.parseMessage.bind(this);
127 this.subscription = wsSubscribe(this.parseMessage);
129 // Only fetch the data if coming from another route
130 if (this.isoData.path == this.context.router.route.match.url) {
131 this.state.res = Some(this.isoData.routeData[0] as GetModlogResponse);
133 if (this.isoData.routeData[1]) {
134 // Getting the moderators
135 let communityRes = Some(
136 this.isoData.routeData[1] as GetCommunityResponse
138 this.state.communityMods = communityRes.map(c => c.moderators);
141 this.state.loading = false;
147 componentDidMount() {
148 this.setupUserFilter();
149 this.setupModFilter();
152 componentWillUnmount() {
154 this.subscription.unsubscribe();
158 buildCombined(res: GetModlogResponse): ModlogType[] {
159 let removed_posts: ModlogType[] = res.removed_posts.map(r => ({
160 id: r.mod_remove_post.id,
161 type_: ModlogActionType.ModRemovePost,
163 moderator: r.moderator,
164 when_: r.mod_remove_post.when_,
167 let locked_posts: ModlogType[] = res.locked_posts.map(r => ({
168 id: r.mod_lock_post.id,
169 type_: ModlogActionType.ModLockPost,
171 moderator: r.moderator,
172 when_: r.mod_lock_post.when_,
175 let stickied_posts: ModlogType[] = res.stickied_posts.map(r => ({
176 id: r.mod_sticky_post.id,
177 type_: ModlogActionType.ModStickyPost,
179 moderator: r.moderator,
180 when_: r.mod_sticky_post.when_,
183 let removed_comments: ModlogType[] = res.removed_comments.map(r => ({
184 id: r.mod_remove_comment.id,
185 type_: ModlogActionType.ModRemoveComment,
187 moderator: r.moderator,
188 when_: r.mod_remove_comment.when_,
191 let removed_communities: ModlogType[] = res.removed_communities.map(r => ({
192 id: r.mod_remove_community.id,
193 type_: ModlogActionType.ModRemoveCommunity,
195 moderator: r.moderator,
196 when_: r.mod_remove_community.when_,
199 let banned_from_community: ModlogType[] = res.banned_from_community.map(
201 id: r.mod_ban_from_community.id,
202 type_: ModlogActionType.ModBanFromCommunity,
204 moderator: r.moderator,
205 when_: r.mod_ban_from_community.when_,
209 let added_to_community: ModlogType[] = res.added_to_community.map(r => ({
210 id: r.mod_add_community.id,
211 type_: ModlogActionType.ModAddCommunity,
213 moderator: r.moderator,
214 when_: r.mod_add_community.when_,
217 let transferred_to_community: ModlogType[] =
218 res.transferred_to_community.map(r => ({
219 id: r.mod_transfer_community.id,
220 type_: ModlogActionType.ModTransferCommunity,
222 moderator: r.moderator,
223 when_: r.mod_transfer_community.when_,
226 let added: ModlogType[] = res.added.map(r => ({
228 type_: ModlogActionType.ModAdd,
230 moderator: r.moderator,
231 when_: r.mod_add.when_,
234 let banned: ModlogType[] = res.banned.map(r => ({
236 type_: ModlogActionType.ModBan,
238 moderator: r.moderator,
239 when_: r.mod_ban.when_,
242 let purged_persons: ModlogType[] = res.admin_purged_persons.map(r => ({
243 id: r.admin_purge_person.id,
244 type_: ModlogActionType.AdminPurgePerson,
247 when_: r.admin_purge_person.when_,
250 let purged_communities: ModlogType[] = res.admin_purged_communities.map(
252 id: r.admin_purge_community.id,
253 type_: ModlogActionType.AdminPurgeCommunity,
256 when_: r.admin_purge_community.when_,
260 let purged_posts: ModlogType[] = res.admin_purged_posts.map(r => ({
261 id: r.admin_purge_post.id,
262 type_: ModlogActionType.AdminPurgePost,
265 when_: r.admin_purge_post.when_,
268 let purged_comments: ModlogType[] = res.admin_purged_comments.map(r => ({
269 id: r.admin_purge_comment.id,
270 type_: ModlogActionType.AdminPurgeComment,
273 when_: r.admin_purge_comment.when_,
276 let combined: ModlogType[] = [];
278 combined.push(...removed_posts);
279 combined.push(...locked_posts);
280 combined.push(...stickied_posts);
281 combined.push(...removed_comments);
282 combined.push(...removed_communities);
283 combined.push(...banned_from_community);
284 combined.push(...added_to_community);
285 combined.push(...transferred_to_community);
286 combined.push(...added);
287 combined.push(...banned);
288 combined.push(...purged_persons);
289 combined.push(...purged_communities);
290 combined.push(...purged_posts);
291 combined.push(...purged_comments);
294 combined.sort((a, b) => b.when_.localeCompare(a.when_));
299 renderModlogType(i: ModlogType) {
301 case ModlogActionType.ModRemovePost: {
302 let mrpv = i.view as ModRemovePostView;
304 mrpv.mod_remove_post.removed.unwrapOr(false)
308 Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link>
310 mrpv.mod_remove_post.reason.match({
311 some: reason => <div>reason: {reason}</div>,
316 case ModlogActionType.ModLockPost: {
317 let mlpv = i.view as ModLockPostView;
319 mlpv.mod_lock_post.locked.unwrapOr(false) ? "Locked " : "Unlocked ",
321 Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link>
325 case ModlogActionType.ModStickyPost: {
326 let mspv = i.view as ModStickyPostView;
328 mspv.mod_sticky_post.stickied.unwrapOr(false)
332 Post <Link to={`/post/${mspv.post.id}`}>{mspv.post.name}</Link>
336 case ModlogActionType.ModRemoveComment: {
337 let mrc = i.view as ModRemoveCommentView;
339 mrc.mod_remove_comment.removed.unwrapOr(false)
344 <Link to={`/post/${mrc.post.id}/comment/${mrc.comment.id}`}>
345 {mrc.comment.content}
350 by <PersonListing person={mrc.commenter} />
352 mrc.mod_remove_comment.reason.match({
353 some: reason => <div>reason: {reason}</div>,
358 case ModlogActionType.ModRemoveCommunity: {
359 let mrco = i.view as ModRemoveCommunityView;
361 mrco.mod_remove_community.removed.unwrapOr(false)
365 Community <CommunityLink community={mrco.community} />
367 mrco.mod_remove_community.reason.match({
368 some: reason => <div>reason: {reason}</div>,
371 mrco.mod_remove_community.expires.match({
373 <div>expires: {moment.utc(expires).fromNow()}</div>
379 case ModlogActionType.ModBanFromCommunity: {
380 let mbfc = i.view as ModBanFromCommunityView;
383 {mbfc.mod_ban_from_community.banned.unwrapOr(false)
388 <PersonListing person={mbfc.banned_person} />
390 <span> from the community </span>,
392 <CommunityLink community={mbfc.community} />
394 mbfc.mod_ban_from_community.reason.match({
395 some: reason => <div>reason: {reason}</div>,
398 mbfc.mod_ban_from_community.expires.match({
400 <div>expires: {moment.utc(expires).fromNow()}</div>
406 case ModlogActionType.ModAddCommunity: {
407 let mac = i.view as ModAddCommunityView;
410 {mac.mod_add_community.removed.unwrapOr(false)
415 <PersonListing person={mac.modded_person} />
417 <span> as a mod to the community </span>,
419 <CommunityLink community={mac.community} />
423 case ModlogActionType.ModTransferCommunity: {
424 let mtc = i.view as ModTransferCommunityView;
427 {mtc.mod_transfer_community.removed.unwrapOr(false)
429 : "Transferred "}{" "}
432 <CommunityLink community={mtc.community} />
436 <PersonListing person={mtc.modded_person} />
440 case ModlogActionType.ModBan: {
441 let mb = i.view as ModBanView;
444 {mb.mod_ban.banned.unwrapOr(false) ? "Banned " : "Unbanned "}{" "}
447 <PersonListing person={mb.banned_person} />
449 mb.mod_ban.reason.match({
450 some: reason => <div>reason: {reason}</div>,
453 mb.mod_ban.expires.match({
455 <div>expires: {moment.utc(expires).fromNow()}</div>
461 case ModlogActionType.ModAdd: {
462 let ma = i.view as ModAddView;
465 {ma.mod_add.removed.unwrapOr(false) ? "Removed " : "Appointed "}{" "}
468 <PersonListing person={ma.modded_person} />
470 <span> as an admin </span>,
473 case ModlogActionType.AdminPurgePerson: {
474 let ap = i.view as AdminPurgePersonView;
476 <span>Purged a Person</span>,
477 ap.admin_purge_person.reason.match({
478 some: reason => <div>reason: {reason}</div>,
483 case ModlogActionType.AdminPurgeCommunity: {
484 let ap = i.view as AdminPurgeCommunityView;
486 <span>Purged a Community</span>,
487 ap.admin_purge_community.reason.match({
488 some: reason => <div>reason: {reason}</div>,
493 case ModlogActionType.AdminPurgePost: {
494 let ap = i.view as AdminPurgePostView;
496 <span>Purged a Post from from </span>,
497 <CommunityLink community={ap.community} />,
498 ap.admin_purge_post.reason.match({
499 some: reason => <div>reason: {reason}</div>,
504 case ModlogActionType.AdminPurgeComment: {
505 let ap = i.view as AdminPurgeCommentView;
508 Purged a Comment from{" "}
509 <Link to={`/post/${ap.post.id}`}>{ap.post.name}</Link>
511 ap.admin_purge_comment.reason.match({
512 some: reason => <div>reason: {reason}</div>,
523 let combined = this.state.res.map(this.buildCombined).unwrapOr([]);
530 <MomentTime published={i.when_} updated={None} />
533 {this.amAdminOrMod ? (
534 <PersonListing person={i.moderator.unwrap()} />
536 <div>{this.modOrAdminText(i.moderator)}</div>
539 <td>{this.renderModlogType(i)}</td>
546 get amAdminOrMod(): boolean {
548 amAdmin(Some(this.state.siteRes.admins)) ||
549 amMod(this.state.communityMods)
553 modOrAdminText(person: Option<PersonSafe>): string {
554 return person.match({
556 this.isoData.site_res.admins.map(a => a.person.id).includes(res.id)
563 get documentTitle(): string {
564 return this.state.siteRes.site_view.match({
565 some: siteView => `Modlog - ${siteView.site.name}`,
572 <div className="container">
574 title={this.documentTitle}
575 path={this.context.router.route.match.url}
579 {this.state.loading ? (
586 {this.state.communityName.match({
588 <Link className="text-body" to={`/c/${name}`}>
594 <span>{i18n.t("modlog")}</span>
596 <form className="form-inline mr-2">
598 value={this.state.filter_action}
599 onChange={linkEvent(this, this.handleFilterActionChange)}
600 className="custom-select col-4 mb-2"
603 <option disabled aria-hidden="true">
604 {i18n.t("filter_by_action")}
606 <option value={ModlogActionType.All}>{i18n.t("all")}</option>
607 <option value={ModlogActionType.ModRemovePost}>
610 <option value={ModlogActionType.ModLockPost}>
613 <option value={ModlogActionType.ModStickyPost}>
616 <option value={ModlogActionType.ModRemoveComment}>
619 <option value={ModlogActionType.ModRemoveCommunity}>
622 <option value={ModlogActionType.ModBanFromCommunity}>
623 Banning From Communities
625 <option value={ModlogActionType.ModAddCommunity}>
626 Adding Mod to Community
628 <option value={ModlogActionType.ModTransferCommunity}>
629 Transfering Communities
631 <option value={ModlogActionType.ModAdd}>
634 <option value={ModlogActionType.ModBan}>
638 {this.state.siteRes.site_view.match({
640 !site_view.site.hide_modlog_mod_names.unwrapOr(false) && (
643 value={toUndefined(this.state.filter_mod)}
645 <option>{i18n.t("filter_by_mod")}</option>
652 value={toUndefined(this.state.filter_user)}
654 <option>{i18n.t("filter_by_user")}</option>
657 <div className="table-responsive">
658 <table id="modlog_table" className="table table-sm table-hover">
659 <thead className="pointer">
661 <th> {i18n.t("time")}</th>
662 <th>{i18n.t("mod")}</th>
663 <th>{i18n.t("action")}</th>
669 page={this.state.page}
670 onChange={this.handlePageChange}
679 handleFilterActionChange(i: Modlog, event: any) {
680 i.setState({ filter_action: event.target.value });
684 handlePageChange(val: number) {
685 this.setState({ page: val });
690 let modlogForm = new GetModlog({
691 community_id: this.state.communityId,
692 page: Some(this.state.page),
693 limit: Some(fetchLimit),
694 auth: auth(false).ok(),
695 type_: this.state.filter_action,
696 other_person_id: this.state.filter_user,
697 mod_person_id: this.state.filter_mod,
699 WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
701 this.state.communityId.match({
703 let communityForm = new GetCommunity({
706 auth: auth(false).ok(),
708 WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
716 let selectId: any = document.getElementById("filter-user");
718 this.userChoices = new Choices(selectId, choicesModLogConfig);
719 this.userChoices.passedElement.element.addEventListener(
722 this.state.filter_user = Some(Number(e.detail.choice.value));
723 this.setState(this.state);
728 this.userChoices.passedElement.element.addEventListener(
730 debounce(async (e: any) => {
732 let users = (await fetchUsers(e.detail.value)).users;
733 this.userChoices.setChoices(
736 value: u.person.id.toString(),
737 label: u.person.name,
756 let selectId: any = document.getElementById("filter-mod");
758 this.modChoices = new Choices(selectId, choicesModLogConfig);
759 this.modChoices.passedElement.element.addEventListener(
762 this.state.filter_mod = Some(Number(e.detail.choice.value));
763 this.setState(this.state);
768 this.modChoices.passedElement.element.addEventListener(
770 debounce(async (e: any) => {
772 let mods = (await fetchUsers(e.detail.value)).users;
773 this.modChoices.setChoices(
776 value: u.person.id.toString(),
777 label: u.person.name,
794 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
795 let pathSplit = req.path.split("/");
796 let communityId = Some(pathSplit[3]).map(Number);
797 let promises: Promise<any>[] = [];
799 let modlogForm = new GetModlog({
801 limit: Some(fetchLimit),
802 community_id: communityId,
805 type_: ModlogActionType.All,
806 other_person_id: None,
809 promises.push(req.client.getModlog(modlogForm));
811 if (communityId.isSome()) {
812 let communityForm = new GetCommunity({
817 promises.push(req.client.getCommunity(communityForm));
819 promises.push(Promise.resolve());
824 parseMessage(msg: any) {
825 let op = wsUserOp(msg);
828 toast(i18n.t(msg.error), "danger");
830 } else if (op == UserOperation.GetModlog) {
831 let data = wsJsonToRes<GetModlogResponse>(msg, GetModlogResponse);
832 this.state.loading = false;
833 window.scrollTo(0, 0);
834 this.state.res = Some(data);
835 this.setState(this.state);
836 } else if (op == UserOperation.GetCommunity) {
837 let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
838 this.state.communityMods = Some(data.moderators);
839 this.state.communityName = Some(data.community_view.community.name);