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