]> Untitled Git - lemmy-ui.git/blob - src/shared/components/modlog.tsx
Adding mod / admin distinguish. (#744)
[lemmy-ui.git] / src / shared / components / modlog.tsx
1 import { None, Option, Some } from "@sniptt/monads";
2 import { Component } from "inferno";
3 import { Link } from "inferno-router";
4 import {
5   AdminPurgeCommentView,
6   AdminPurgeCommunityView,
7   AdminPurgePersonView,
8   AdminPurgePostView,
9   CommunityModeratorView,
10   GetCommunity,
11   GetCommunityResponse,
12   GetModlog,
13   GetModlogResponse,
14   GetSiteResponse,
15   ModAddCommunityView,
16   ModAddView,
17   ModBanFromCommunityView,
18   ModBanView,
19   ModLockPostView,
20   ModRemoveCommentView,
21   ModRemoveCommunityView,
22   ModRemovePostView,
23   ModStickyPostView,
24   ModTransferCommunityView,
25   PersonSafe,
26   UserOperation,
27   wsJsonToRes,
28   wsUserOp,
29 } from "lemmy-js-client";
30 import moment from "moment";
31 import { Subscription } from "rxjs";
32 import { i18n } from "../i18next";
33 import { InitialFetchRequest } from "../interfaces";
34 import { WebSocketService } from "../services";
35 import {
36   amAdmin,
37   amMod,
38   auth,
39   fetchLimit,
40   isBrowser,
41   setIsoData,
42   toast,
43   wsClient,
44   wsSubscribe,
45 } from "../utils";
46 import { HtmlTags } from "./common/html-tags";
47 import { Spinner } from "./common/icon";
48 import { MomentTime } from "./common/moment-time";
49 import { Paginator } from "./common/paginator";
50 import { CommunityLink } from "./community/community-link";
51 import { PersonListing } from "./person/person-listing";
52
53 enum ModlogEnum {
54   ModRemovePost,
55   ModLockPost,
56   ModStickyPost,
57   ModRemoveComment,
58   ModRemoveCommunity,
59   ModBanFromCommunity,
60   ModAddCommunity,
61   ModTransferCommunity,
62   ModAdd,
63   ModBan,
64   AdminPurgePerson,
65   AdminPurgeCommunity,
66   AdminPurgePost,
67   AdminPurgeComment,
68 }
69
70 type ModlogType = {
71   id: number;
72   type_: ModlogEnum;
73   moderator: PersonSafe;
74   view:
75     | ModRemovePostView
76     | ModLockPostView
77     | ModStickyPostView
78     | ModRemoveCommentView
79     | ModRemoveCommunityView
80     | ModBanFromCommunityView
81     | ModBanView
82     | ModAddCommunityView
83     | ModTransferCommunityView
84     | ModAddView
85     | AdminPurgePersonView
86     | AdminPurgeCommunityView
87     | AdminPurgePostView
88     | AdminPurgeCommentView;
89   when_: string;
90 };
91
92 interface ModlogState {
93   res: Option<GetModlogResponse>;
94   communityId: Option<number>;
95   communityMods: Option<CommunityModeratorView[]>;
96   page: number;
97   siteRes: GetSiteResponse;
98   loading: boolean;
99 }
100
101 export class Modlog extends Component<any, ModlogState> {
102   private isoData = setIsoData(
103     this.context,
104     GetModlogResponse,
105     GetCommunityResponse
106   );
107   private subscription: Subscription;
108   private emptyState: ModlogState = {
109     res: None,
110     communityId: None,
111     communityMods: None,
112     page: 1,
113     loading: true,
114     siteRes: this.isoData.site_res,
115   };
116
117   constructor(props: any, context: any) {
118     super(props, context);
119
120     this.state = this.emptyState;
121     this.handlePageChange = this.handlePageChange.bind(this);
122
123     this.state.communityId = this.props.match.params.community_id
124       ? Some(Number(this.props.match.params.community_id))
125       : None;
126
127     this.parseMessage = this.parseMessage.bind(this);
128     this.subscription = wsSubscribe(this.parseMessage);
129
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);
133
134       if (this.isoData.routeData[1]) {
135         // Getting the moderators
136         let communityRes = Some(
137           this.isoData.routeData[1] as GetCommunityResponse
138         );
139         this.state.communityMods = communityRes.map(c => c.moderators);
140       }
141
142       this.state.loading = false;
143     } else {
144       this.refetch();
145     }
146   }
147
148   componentWillUnmount() {
149     if (isBrowser()) {
150       this.subscription.unsubscribe();
151     }
152   }
153
154   buildCombined(res: GetModlogResponse): ModlogType[] {
155     let removed_posts: ModlogType[] = res.removed_posts.map(r => ({
156       id: r.mod_remove_post.id,
157       type_: ModlogEnum.ModRemovePost,
158       view: r,
159       moderator: r.moderator,
160       when_: r.mod_remove_post.when_,
161     }));
162
163     let locked_posts: ModlogType[] = res.locked_posts.map(r => ({
164       id: r.mod_lock_post.id,
165       type_: ModlogEnum.ModLockPost,
166       view: r,
167       moderator: r.moderator,
168       when_: r.mod_lock_post.when_,
169     }));
170
171     let stickied_posts: ModlogType[] = res.stickied_posts.map(r => ({
172       id: r.mod_sticky_post.id,
173       type_: ModlogEnum.ModStickyPost,
174       view: r,
175       moderator: r.moderator,
176       when_: r.mod_sticky_post.when_,
177     }));
178
179     let removed_comments: ModlogType[] = res.removed_comments.map(r => ({
180       id: r.mod_remove_comment.id,
181       type_: ModlogEnum.ModRemoveComment,
182       view: r,
183       moderator: r.moderator,
184       when_: r.mod_remove_comment.when_,
185     }));
186
187     let removed_communities: ModlogType[] = res.removed_communities.map(r => ({
188       id: r.mod_remove_community.id,
189       type_: ModlogEnum.ModRemoveCommunity,
190       view: r,
191       moderator: r.moderator,
192       when_: r.mod_remove_community.when_,
193     }));
194
195     let banned_from_community: ModlogType[] = res.banned_from_community.map(
196       r => ({
197         id: r.mod_ban_from_community.id,
198         type_: ModlogEnum.ModBanFromCommunity,
199         view: r,
200         moderator: r.moderator,
201         when_: r.mod_ban_from_community.when_,
202       })
203     );
204
205     let added_to_community: ModlogType[] = res.added_to_community.map(r => ({
206       id: r.mod_add_community.id,
207       type_: ModlogEnum.ModAddCommunity,
208       view: r,
209       moderator: r.moderator,
210       when_: r.mod_add_community.when_,
211     }));
212
213     let transferred_to_community: ModlogType[] =
214       res.transferred_to_community.map(r => ({
215         id: r.mod_transfer_community.id,
216         type_: ModlogEnum.ModTransferCommunity,
217         view: r,
218         moderator: r.moderator,
219         when_: r.mod_transfer_community.when_,
220       }));
221
222     let added: ModlogType[] = res.added.map(r => ({
223       id: r.mod_add.id,
224       type_: ModlogEnum.ModAdd,
225       view: r,
226       moderator: r.moderator,
227       when_: r.mod_add.when_,
228     }));
229
230     let banned: ModlogType[] = res.banned.map(r => ({
231       id: r.mod_ban.id,
232       type_: ModlogEnum.ModBan,
233       view: r,
234       moderator: r.moderator,
235       when_: r.mod_ban.when_,
236     }));
237
238     let purged_persons: ModlogType[] = res.admin_purged_persons.map(r => ({
239       id: r.admin_purge_person.id,
240       type_: ModlogEnum.AdminPurgePerson,
241       view: r,
242       moderator: r.admin,
243       when_: r.admin_purge_person.when_,
244     }));
245
246     let purged_communities: ModlogType[] = res.admin_purged_communities.map(
247       r => ({
248         id: r.admin_purge_community.id,
249         type_: ModlogEnum.AdminPurgeCommunity,
250         view: r,
251         moderator: r.admin,
252         when_: r.admin_purge_community.when_,
253       })
254     );
255
256     let purged_posts: ModlogType[] = res.admin_purged_posts.map(r => ({
257       id: r.admin_purge_post.id,
258       type_: ModlogEnum.AdminPurgePost,
259       view: r,
260       moderator: r.admin,
261       when_: r.admin_purge_post.when_,
262     }));
263
264     let purged_comments: ModlogType[] = res.admin_purged_comments.map(r => ({
265       id: r.admin_purge_comment.id,
266       type_: ModlogEnum.AdminPurgeComment,
267       view: r,
268       moderator: r.admin,
269       when_: r.admin_purge_comment.when_,
270     }));
271
272     let combined: ModlogType[] = [];
273
274     combined.push(...removed_posts);
275     combined.push(...locked_posts);
276     combined.push(...stickied_posts);
277     combined.push(...removed_comments);
278     combined.push(...removed_communities);
279     combined.push(...banned_from_community);
280     combined.push(...added_to_community);
281     combined.push(...transferred_to_community);
282     combined.push(...added);
283     combined.push(...banned);
284     combined.push(...purged_persons);
285     combined.push(...purged_communities);
286     combined.push(...purged_posts);
287     combined.push(...purged_comments);
288
289     // Sort them by time
290     combined.sort((a, b) => b.when_.localeCompare(a.when_));
291
292     return combined;
293   }
294
295   renderModlogType(i: ModlogType) {
296     switch (i.type_) {
297       case ModlogEnum.ModRemovePost: {
298         let mrpv = i.view as ModRemovePostView;
299         return [
300           mrpv.mod_remove_post.removed.unwrapOr(false)
301             ? "Removed "
302             : "Restored ",
303           <span>
304             Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link>
305           </span>,
306           mrpv.mod_remove_post.reason.match({
307             some: reason => <div>reason: {reason}</div>,
308             none: <></>,
309           }),
310         ];
311       }
312       case ModlogEnum.ModLockPost: {
313         let mlpv = i.view as ModLockPostView;
314         return [
315           mlpv.mod_lock_post.locked.unwrapOr(false) ? "Locked " : "Unlocked ",
316           <span>
317             Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link>
318           </span>,
319         ];
320       }
321       case ModlogEnum.ModStickyPost: {
322         let mspv = i.view as ModStickyPostView;
323         return [
324           mspv.mod_sticky_post.stickied.unwrapOr(false)
325             ? "Stickied "
326             : "Unstickied ",
327           <span>
328             Post <Link to={`/post/${mspv.post.id}`}>{mspv.post.name}</Link>
329           </span>,
330         ];
331       }
332       case ModlogEnum.ModRemoveComment: {
333         let mrc = i.view as ModRemoveCommentView;
334         return [
335           mrc.mod_remove_comment.removed.unwrapOr(false)
336             ? "Removed "
337             : "Restored ",
338           <span>
339             Comment{" "}
340             <Link to={`/post/${mrc.post.id}/comment/${mrc.comment.id}`}>
341               {mrc.comment.content}
342             </Link>
343           </span>,
344           <span>
345             {" "}
346             by <PersonListing person={mrc.commenter} />
347           </span>,
348           mrc.mod_remove_comment.reason.match({
349             some: reason => <div>reason: {reason}</div>,
350             none: <></>,
351           }),
352         ];
353       }
354       case ModlogEnum.ModRemoveCommunity: {
355         let mrco = i.view as ModRemoveCommunityView;
356         return [
357           mrco.mod_remove_community.removed.unwrapOr(false)
358             ? "Removed "
359             : "Restored ",
360           <span>
361             Community <CommunityLink community={mrco.community} />
362           </span>,
363           mrco.mod_remove_community.reason.match({
364             some: reason => <div>reason: {reason}</div>,
365             none: <></>,
366           }),
367           mrco.mod_remove_community.expires.match({
368             some: expires => (
369               <div>expires: {moment.utc(expires).fromNow()}</div>
370             ),
371             none: <></>,
372           }),
373         ];
374       }
375       case ModlogEnum.ModBanFromCommunity: {
376         let mbfc = i.view as ModBanFromCommunityView;
377         return [
378           <span>
379             {mbfc.mod_ban_from_community.banned.unwrapOr(false)
380               ? "Banned "
381               : "Unbanned "}{" "}
382           </span>,
383           <span>
384             <PersonListing person={mbfc.banned_person} />
385           </span>,
386           <span> from the community </span>,
387           <span>
388             <CommunityLink community={mbfc.community} />
389           </span>,
390           mbfc.mod_ban_from_community.reason.match({
391             some: reason => <div>reason: {reason}</div>,
392             none: <></>,
393           }),
394           mbfc.mod_ban_from_community.expires.match({
395             some: expires => (
396               <div>expires: {moment.utc(expires).fromNow()}</div>
397             ),
398             none: <></>,
399           }),
400         ];
401       }
402       case ModlogEnum.ModAddCommunity: {
403         let mac = i.view as ModAddCommunityView;
404         return [
405           <span>
406             {mac.mod_add_community.removed.unwrapOr(false)
407               ? "Removed "
408               : "Appointed "}{" "}
409           </span>,
410           <span>
411             <PersonListing person={mac.modded_person} />
412           </span>,
413           <span> as a mod to the community </span>,
414           <span>
415             <CommunityLink community={mac.community} />
416           </span>,
417         ];
418       }
419       case ModlogEnum.ModTransferCommunity: {
420         let mtc = i.view as ModTransferCommunityView;
421         return [
422           <span>
423             {mtc.mod_transfer_community.removed.unwrapOr(false)
424               ? "Removed "
425               : "Transferred "}{" "}
426           </span>,
427           <span>
428             <CommunityLink community={mtc.community} />
429           </span>,
430           <span> to </span>,
431           <span>
432             <PersonListing person={mtc.modded_person} />
433           </span>,
434         ];
435       }
436       case ModlogEnum.ModBan: {
437         let mb = i.view as ModBanView;
438         return [
439           <span>
440             {mb.mod_ban.banned.unwrapOr(false) ? "Banned " : "Unbanned "}{" "}
441           </span>,
442           <span>
443             <PersonListing person={mb.banned_person} />
444           </span>,
445           mb.mod_ban.reason.match({
446             some: reason => <div>reason: {reason}</div>,
447             none: <></>,
448           }),
449           mb.mod_ban.expires.match({
450             some: expires => (
451               <div>expires: {moment.utc(expires).fromNow()}</div>
452             ),
453             none: <></>,
454           }),
455         ];
456       }
457       case ModlogEnum.ModAdd: {
458         let ma = i.view as ModAddView;
459         return [
460           <span>
461             {ma.mod_add.removed.unwrapOr(false) ? "Removed " : "Appointed "}{" "}
462           </span>,
463           <span>
464             <PersonListing person={ma.modded_person} />
465           </span>,
466           <span> as an admin </span>,
467         ];
468       }
469       case ModlogEnum.AdminPurgePerson: {
470         let ap = i.view as AdminPurgePersonView;
471         return [
472           <span>Purged a Person</span>,
473           ap.admin_purge_person.reason.match({
474             some: reason => <div>reason: {reason}</div>,
475             none: <></>,
476           }),
477         ];
478       }
479       case ModlogEnum.AdminPurgeCommunity: {
480         let ap = i.view as AdminPurgeCommunityView;
481         return [
482           <span>Purged a Community</span>,
483           ap.admin_purge_community.reason.match({
484             some: reason => <div>reason: {reason}</div>,
485             none: <></>,
486           }),
487         ];
488       }
489       case ModlogEnum.AdminPurgePost: {
490         let ap = i.view as AdminPurgePostView;
491         return [
492           <span>Purged a Post from from </span>,
493           <CommunityLink community={ap.community} />,
494           ap.admin_purge_post.reason.match({
495             some: reason => <div>reason: {reason}</div>,
496             none: <></>,
497           }),
498         ];
499       }
500       case ModlogEnum.AdminPurgeComment: {
501         let ap = i.view as AdminPurgeCommentView;
502         return [
503           <span>
504             Purged a Comment from{" "}
505             <Link to={`/post/${ap.post.id}`}>{ap.post.name}</Link>
506           </span>,
507           ap.admin_purge_comment.reason.match({
508             some: reason => <div>reason: {reason}</div>,
509             none: <></>,
510           }),
511         ];
512       }
513       default:
514         return <div />;
515     }
516   }
517
518   combined() {
519     let combined = this.state.res.map(this.buildCombined).unwrapOr([]);
520
521     return (
522       <tbody>
523         {combined.map(i => (
524           <tr>
525             <td>
526               <MomentTime published={i.when_} updated={None} />
527             </td>
528             <td>
529               {this.amAdminOrMod ? (
530                 <PersonListing person={i.moderator} />
531               ) : (
532                 <div>{this.modOrAdminText(i.moderator)}</div>
533               )}
534             </td>
535             <td>{this.renderModlogType(i)}</td>
536           </tr>
537         ))}
538       </tbody>
539     );
540   }
541
542   get amAdminOrMod(): boolean {
543     return (
544       amAdmin(Some(this.state.siteRes.admins)) ||
545       amMod(this.state.communityMods)
546     );
547   }
548
549   modOrAdminText(person: PersonSafe): string {
550     if (
551       this.isoData.site_res.admins.map(a => a.person.id).includes(person.id)
552     ) {
553       return i18n.t("admin");
554     } else {
555       return i18n.t("mod");
556     }
557   }
558
559   get documentTitle(): string {
560     return this.state.siteRes.site_view.match({
561       some: siteView => `Modlog - ${siteView.site.name}`,
562       none: "",
563     });
564   }
565
566   render() {
567     return (
568       <div class="container">
569         <HtmlTags
570           title={this.documentTitle}
571           path={this.context.router.route.match.url}
572           description={None}
573           image={None}
574         />
575         {this.state.loading ? (
576           <h5>
577             <Spinner large />
578           </h5>
579         ) : (
580           <div>
581             <div class="table-responsive">
582               <table id="modlog_table" class="table table-sm table-hover">
583                 <thead class="pointer">
584                   <tr>
585                     <th> {i18n.t("time")}</th>
586                     <th>{i18n.t("mod")}</th>
587                     <th>{i18n.t("action")}</th>
588                   </tr>
589                 </thead>
590                 {this.combined()}
591               </table>
592               <Paginator
593                 page={this.state.page}
594                 onChange={this.handlePageChange}
595               />
596             </div>
597           </div>
598         )}
599       </div>
600     );
601   }
602
603   handlePageChange(val: number) {
604     this.setState({ page: val });
605     this.refetch();
606   }
607
608   refetch() {
609     let modlogForm = new GetModlog({
610       community_id: this.state.communityId,
611       mod_person_id: None,
612       page: Some(this.state.page),
613       limit: Some(fetchLimit),
614       auth: auth(false).ok(),
615     });
616     WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
617
618     this.state.communityId.match({
619       some: id => {
620         let communityForm = new GetCommunity({
621           id: Some(id),
622           name: None,
623           auth: auth(false).ok(),
624         });
625         WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
626       },
627       none: void 0,
628     });
629   }
630
631   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
632     let pathSplit = req.path.split("/");
633     let communityId = Some(pathSplit[3]).map(Number);
634     let promises: Promise<any>[] = [];
635
636     let modlogForm = new GetModlog({
637       page: Some(1),
638       limit: Some(fetchLimit),
639       community_id: communityId,
640       mod_person_id: None,
641       auth: req.auth,
642     });
643
644     promises.push(req.client.getModlog(modlogForm));
645
646     if (communityId.isSome()) {
647       let communityForm = new GetCommunity({
648         id: communityId,
649         name: None,
650         auth: req.auth,
651       });
652       promises.push(req.client.getCommunity(communityForm));
653     } else {
654       promises.push(Promise.resolve());
655     }
656     return promises;
657   }
658
659   parseMessage(msg: any) {
660     let op = wsUserOp(msg);
661     console.log(msg);
662     if (msg.error) {
663       toast(i18n.t(msg.error), "danger");
664       return;
665     } else if (op == UserOperation.GetModlog) {
666       let data = wsJsonToRes<GetModlogResponse>(msg, GetModlogResponse);
667       this.state.loading = false;
668       window.scrollTo(0, 0);
669       this.state.res = Some(data);
670       this.setState(this.state);
671     } else if (op == UserOperation.GetCommunity) {
672       let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
673       this.state.communityMods = Some(data.moderators);
674     }
675   }
676 }