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,
118 constructor(props: any, context: any) {
119 super(props, context);
120 this.state = this.emptyState;
121 this.handlePageChange = this.handlePageChange.bind(this);
123 this.parseMessage = this.parseMessage.bind(this);
124 this.subscription = wsSubscribe(this.parseMessage);
128 communityId: this.props.match.params.community_id
129 ? Some(Number(this.props.match.params.community_id))
133 // Only fetch the data if coming from another route
134 if (this.isoData.path == this.context.router.route.match.url) {
137 res: Some(this.isoData.routeData[0] as GetModlogResponse),
140 if (this.isoData.routeData[1]) {
141 // Getting the moderators
142 let communityRes = Some(
143 this.isoData.routeData[1] as GetCommunityResponse
147 communityMods: communityRes.map(c => c.moderators),
151 this.state = { ...this.state, loading: false };
157 componentDidMount() {
158 this.setupUserFilter();
159 this.setupModFilter();
162 componentWillUnmount() {
164 this.subscription.unsubscribe();
168 buildCombined(res: GetModlogResponse): ModlogType[] {
169 let removed_posts: ModlogType[] = res.removed_posts.map(r => ({
170 id: r.mod_remove_post.id,
171 type_: ModlogActionType.ModRemovePost,
173 moderator: r.moderator,
174 when_: r.mod_remove_post.when_,
177 let locked_posts: ModlogType[] = res.locked_posts.map(r => ({
178 id: r.mod_lock_post.id,
179 type_: ModlogActionType.ModLockPost,
181 moderator: r.moderator,
182 when_: r.mod_lock_post.when_,
185 let stickied_posts: ModlogType[] = res.stickied_posts.map(r => ({
186 id: r.mod_sticky_post.id,
187 type_: ModlogActionType.ModStickyPost,
189 moderator: r.moderator,
190 when_: r.mod_sticky_post.when_,
193 let removed_comments: ModlogType[] = res.removed_comments.map(r => ({
194 id: r.mod_remove_comment.id,
195 type_: ModlogActionType.ModRemoveComment,
197 moderator: r.moderator,
198 when_: r.mod_remove_comment.when_,
201 let removed_communities: ModlogType[] = res.removed_communities.map(r => ({
202 id: r.mod_remove_community.id,
203 type_: ModlogActionType.ModRemoveCommunity,
205 moderator: r.moderator,
206 when_: r.mod_remove_community.when_,
209 let banned_from_community: ModlogType[] = res.banned_from_community.map(
211 id: r.mod_ban_from_community.id,
212 type_: ModlogActionType.ModBanFromCommunity,
214 moderator: r.moderator,
215 when_: r.mod_ban_from_community.when_,
219 let added_to_community: ModlogType[] = res.added_to_community.map(r => ({
220 id: r.mod_add_community.id,
221 type_: ModlogActionType.ModAddCommunity,
223 moderator: r.moderator,
224 when_: r.mod_add_community.when_,
227 let transferred_to_community: ModlogType[] =
228 res.transferred_to_community.map(r => ({
229 id: r.mod_transfer_community.id,
230 type_: ModlogActionType.ModTransferCommunity,
232 moderator: r.moderator,
233 when_: r.mod_transfer_community.when_,
236 let added: ModlogType[] = res.added.map(r => ({
238 type_: ModlogActionType.ModAdd,
240 moderator: r.moderator,
241 when_: r.mod_add.when_,
244 let banned: ModlogType[] = res.banned.map(r => ({
246 type_: ModlogActionType.ModBan,
248 moderator: r.moderator,
249 when_: r.mod_ban.when_,
252 let purged_persons: ModlogType[] = res.admin_purged_persons.map(r => ({
253 id: r.admin_purge_person.id,
254 type_: ModlogActionType.AdminPurgePerson,
257 when_: r.admin_purge_person.when_,
260 let purged_communities: ModlogType[] = res.admin_purged_communities.map(
262 id: r.admin_purge_community.id,
263 type_: ModlogActionType.AdminPurgeCommunity,
266 when_: r.admin_purge_community.when_,
270 let purged_posts: ModlogType[] = res.admin_purged_posts.map(r => ({
271 id: r.admin_purge_post.id,
272 type_: ModlogActionType.AdminPurgePost,
275 when_: r.admin_purge_post.when_,
278 let purged_comments: ModlogType[] = res.admin_purged_comments.map(r => ({
279 id: r.admin_purge_comment.id,
280 type_: ModlogActionType.AdminPurgeComment,
283 when_: r.admin_purge_comment.when_,
286 let combined: ModlogType[] = [];
288 combined.push(...removed_posts);
289 combined.push(...locked_posts);
290 combined.push(...stickied_posts);
291 combined.push(...removed_comments);
292 combined.push(...removed_communities);
293 combined.push(...banned_from_community);
294 combined.push(...added_to_community);
295 combined.push(...transferred_to_community);
296 combined.push(...added);
297 combined.push(...banned);
298 combined.push(...purged_persons);
299 combined.push(...purged_communities);
300 combined.push(...purged_posts);
301 combined.push(...purged_comments);
304 combined.sort((a, b) => b.when_.localeCompare(a.when_));
309 renderModlogType(i: ModlogType) {
311 case ModlogActionType.ModRemovePost: {
312 let mrpv = i.view as ModRemovePostView;
316 {mrpv.mod_remove_post.removed.unwrapOr(false)
321 Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link>
324 {mrpv.mod_remove_post.reason.match({
325 some: reason => <div>reason: {reason}</div>,
332 case ModlogActionType.ModLockPost: {
333 let mlpv = i.view as ModLockPostView;
337 {mlpv.mod_lock_post.locked.unwrapOr(false)
342 Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link>
347 case ModlogActionType.ModStickyPost: {
348 let mspv = i.view as ModStickyPostView;
352 {mspv.mod_sticky_post.stickied.unwrapOr(false)
357 Post <Link to={`/post/${mspv.post.id}`}>{mspv.post.name}</Link>
362 case ModlogActionType.ModRemoveComment: {
363 let mrc = i.view as ModRemoveCommentView;
367 {mrc.mod_remove_comment.removed.unwrapOr(false)
373 <Link to={`/post/${mrc.post.id}/comment/${mrc.comment.id}`}>
374 {mrc.comment.content}
379 by <PersonListing person={mrc.commenter} />
382 {mrc.mod_remove_comment.reason.match({
383 some: reason => <div>reason: {reason}</div>,
390 case ModlogActionType.ModRemoveCommunity: {
391 let mrco = i.view as ModRemoveCommunityView;
395 {mrco.mod_remove_community.removed.unwrapOr(false)
400 Community <CommunityLink community={mrco.community} />
403 {mrco.mod_remove_community.reason.match({
404 some: reason => <div>reason: {reason}</div>,
409 {mrco.mod_remove_community.expires.match({
411 <div>expires: {moment.utc(expires).fromNow()}</div>
419 case ModlogActionType.ModBanFromCommunity: {
420 let mbfc = i.view as ModBanFromCommunityView;
424 {mbfc.mod_ban_from_community.banned.unwrapOr(false)
429 <PersonListing person={mbfc.banned_person} />
431 <span> from the community </span>
433 <CommunityLink community={mbfc.community} />
436 {mbfc.mod_ban_from_community.reason.match({
437 some: reason => <div>reason: {reason}</div>,
442 {mbfc.mod_ban_from_community.expires.match({
444 <div>expires: {moment.utc(expires).fromNow()}</div>
452 case ModlogActionType.ModAddCommunity: {
453 let mac = i.view as ModAddCommunityView;
457 {mac.mod_add_community.removed.unwrapOr(false)
462 <PersonListing person={mac.modded_person} />
464 <span> as a mod to the community </span>
466 <CommunityLink community={mac.community} />
471 case ModlogActionType.ModTransferCommunity: {
472 let mtc = i.view as ModTransferCommunityView;
476 {mtc.mod_transfer_community.removed.unwrapOr(false)
478 : "Transferred "}{" "}
481 <CommunityLink community={mtc.community} />
485 <PersonListing person={mtc.modded_person} />
490 case ModlogActionType.ModBan: {
491 let mb = i.view as ModBanView;
495 {mb.mod_ban.banned.unwrapOr(false) ? "Banned " : "Unbanned "}{" "}
498 <PersonListing person={mb.banned_person} />
501 {mb.mod_ban.reason.match({
502 some: reason => <div>reason: {reason}</div>,
507 {mb.mod_ban.expires.match({
509 <div>expires: {moment.utc(expires).fromNow()}</div>
517 case ModlogActionType.ModAdd: {
518 let ma = i.view as ModAddView;
522 {ma.mod_add.removed.unwrapOr(false) ? "Removed " : "Appointed "}{" "}
525 <PersonListing person={ma.modded_person} />
527 <span> as an admin </span>
531 case ModlogActionType.AdminPurgePerson: {
532 let ap = i.view as AdminPurgePersonView;
535 <span>Purged a Person</span>
537 {ap.admin_purge_person.reason.match({
538 some: reason => <div>reason: {reason}</div>,
545 case ModlogActionType.AdminPurgeCommunity: {
546 let ap = i.view as AdminPurgeCommunityView;
549 <span>Purged a Community</span>
551 {ap.admin_purge_community.reason.match({
552 some: reason => <div>reason: {reason}</div>,
559 case ModlogActionType.AdminPurgePost: {
560 let ap = i.view as AdminPurgePostView;
563 <span>Purged a Post from from </span>
564 <CommunityLink community={ap.community} />
566 {ap.admin_purge_post.reason.match({
567 some: reason => <div>reason: {reason}</div>,
574 case ModlogActionType.AdminPurgeComment: {
575 let ap = i.view as AdminPurgeCommentView;
579 Purged a Comment from{" "}
580 <Link to={`/post/${ap.post.id}`}>{ap.post.name}</Link>
583 {ap.admin_purge_comment.reason.match({
584 some: reason => <div>reason: {reason}</div>,
597 let combined = this.state.res.map(this.buildCombined).unwrapOr([]);
604 <MomentTime published={i.when_} updated={None} />
607 {this.amAdminOrMod ? (
608 <PersonListing person={i.moderator.unwrap()} />
610 <div>{this.modOrAdminText(i.moderator)}</div>
613 <td>{this.renderModlogType(i)}</td>
620 get amAdminOrMod(): boolean {
621 return amAdmin() || amMod(this.state.communityMods);
624 modOrAdminText(person: Option<PersonSafe>): string {
625 return person.match({
627 this.isoData.site_res.admins.map(a => a.person.id).includes(res.id)
634 get documentTitle(): string {
635 return this.state.siteRes.site_view.match({
636 some: siteView => `Modlog - ${siteView.site.name}`,
643 <div className="container-lg">
645 title={this.documentTitle}
646 path={this.context.router.route.match.url}
650 {this.state.loading ? (
657 {this.state.communityName.match({
659 <Link className="text-body" to={`/c/${name}`}>
665 <span>{i18n.t("modlog")}</span>
667 <div className="form-row">
668 <div className="form-group col-sm-6">
670 value={this.state.filter_action}
671 onChange={linkEvent(this, this.handleFilterActionChange)}
672 className="custom-select mb-2"
675 <option disabled aria-hidden="true">
676 {i18n.t("filter_by_action")}
678 <option value={ModlogActionType.All}>{i18n.t("all")}</option>
679 <option value={ModlogActionType.ModRemovePost}>
682 <option value={ModlogActionType.ModLockPost}>
685 <option value={ModlogActionType.ModStickyPost}>
688 <option value={ModlogActionType.ModRemoveComment}>
691 <option value={ModlogActionType.ModRemoveCommunity}>
694 <option value={ModlogActionType.ModBanFromCommunity}>
695 Banning From Communities
697 <option value={ModlogActionType.ModAddCommunity}>
698 Adding Mod to Community
700 <option value={ModlogActionType.ModTransferCommunity}>
701 Transfering Communities
703 <option value={ModlogActionType.ModAdd}>
706 <option value={ModlogActionType.ModBan}>
711 {this.state.siteRes.site_view.match({
713 !site_view.site.hide_modlog_mod_names.unwrapOr(false) && (
714 <div className="form-group col-sm-6">
717 className="form-control"
718 value={toUndefined(this.state.filter_mod)}
720 <option>{i18n.t("filter_by_mod")}</option>
726 <div className="form-group col-sm-6">
729 className="form-control"
730 value={toUndefined(this.state.filter_user)}
732 <option>{i18n.t("filter_by_user")}</option>
736 <div className="table-responsive">
737 <table id="modlog_table" className="table table-sm table-hover">
738 <thead className="pointer">
740 <th> {i18n.t("time")}</th>
741 <th>{i18n.t("mod")}</th>
742 <th>{i18n.t("action")}</th>
748 page={this.state.page}
749 onChange={this.handlePageChange}
758 handleFilterActionChange(i: Modlog, event: any) {
759 i.setState({ filter_action: event.target.value });
763 handlePageChange(val: number) {
764 this.setState({ page: val });
769 let modlogForm = new GetModlog({
770 community_id: this.state.communityId,
771 page: Some(this.state.page),
772 limit: Some(fetchLimit),
773 auth: auth(false).ok(),
774 type_: this.state.filter_action,
775 other_person_id: this.state.filter_user,
776 mod_person_id: this.state.filter_mod,
778 WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
780 this.state.communityId.match({
782 let communityForm = new GetCommunity({
785 auth: auth(false).ok(),
787 WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
795 let selectId: any = document.getElementById("filter-user");
797 this.userChoices = new Choices(selectId, choicesConfig);
798 this.userChoices.passedElement.element.addEventListener(
801 this.setState({ filter_user: Some(Number(e.detail.choice.value)) });
806 this.userChoices.passedElement.element.addEventListener(
808 debounce(async (e: any) => {
810 let users = (await fetchUsers(e.detail.value)).users;
811 this.userChoices.setChoices(
814 value: u.person.id.toString(),
815 label: u.person.name,
834 let selectId: any = document.getElementById("filter-mod");
836 this.modChoices = new Choices(selectId, choicesConfig);
837 this.modChoices.passedElement.element.addEventListener(
840 this.setState({ filter_mod: Some(Number(e.detail.choice.value)) });
845 this.modChoices.passedElement.element.addEventListener(
847 debounce(async (e: any) => {
849 let mods = (await fetchUsers(e.detail.value)).users;
850 this.modChoices.setChoices(
853 value: u.person.id.toString(),
854 label: u.person.name,
871 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
872 let pathSplit = req.path.split("/");
873 let communityId = Some(pathSplit[3]).map(Number);
874 let promises: Promise<any>[] = [];
876 let modlogForm = new GetModlog({
878 limit: Some(fetchLimit),
879 community_id: communityId,
882 type_: ModlogActionType.All,
883 other_person_id: None,
886 promises.push(req.client.getModlog(modlogForm));
888 if (communityId.isSome()) {
889 let communityForm = new GetCommunity({
894 promises.push(req.client.getCommunity(communityForm));
896 promises.push(Promise.resolve());
901 parseMessage(msg: any) {
902 let op = wsUserOp(msg);
905 toast(i18n.t(msg.error), "danger");
907 } else if (op == UserOperation.GetModlog) {
908 let data = wsJsonToRes<GetModlogResponse>(msg, GetModlogResponse);
909 window.scrollTo(0, 0);
910 this.setState({ res: Some(data), loading: false });
911 this.setupUserFilter();
912 this.setupModFilter();
913 } else if (op == UserOperation.GetCommunity) {
914 let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
916 communityMods: Some(data.moderators),
917 communityName: Some(data.community_view.community.name),