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