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