]> Untitled Git - lemmy-ui.git/blob - src/shared/components/modlog.tsx
Merge branch 'main' into breakout-role-utils
[lemmy-ui.git] / src / shared / components / modlog.tsx
1 import { NoOptionI18nKeys } from "i18next";
2 import { Component, linkEvent } from "inferno";
3 import { T } from "inferno-i18next-dess";
4 import { Link } from "inferno-router";
5 import { RouteComponentProps } from "inferno-router/dist/Route";
6 import {
7   AdminPurgeCommentView,
8   AdminPurgeCommunityView,
9   AdminPurgePersonView,
10   AdminPurgePostView,
11   GetCommunity,
12   GetCommunityResponse,
13   GetModlog,
14   GetModlogResponse,
15   GetPersonDetails,
16   GetPersonDetailsResponse,
17   ModAddCommunityView,
18   ModAddView,
19   ModBanFromCommunityView,
20   ModBanView,
21   ModFeaturePostView,
22   ModLockPostView,
23   ModRemoveCommentView,
24   ModRemoveCommunityView,
25   ModRemovePostView,
26   ModTransferCommunityView,
27   ModlogActionType,
28   Person,
29 } from "lemmy-js-client";
30 import moment from "moment";
31 import { i18n } from "../i18next";
32 import { InitialFetchRequest } from "../interfaces";
33 import { FirstLoadService } from "../services/FirstLoadService";
34 import { HttpService, RequestState } from "../services/HttpService";
35 import {
36   Choice,
37   fetchLimit,
38   fetchUsers,
39   getIdFromString,
40   getPageFromString,
41   getUpdatedSearchId,
42   myAuth,
43   personToChoice,
44   setIsoData,
45 } from "../utils";
46 import { debounce } from "../utils/helpers/debounce";
47 import { getQueryParams } from "../utils/helpers/get-query-params";
48 import { getQueryString } from "../utils/helpers/get-query-string";
49 import { amAdmin } from "../utils/roles/am-admin";
50 import { amMod } from "../utils/roles/am-mod";
51 import type { QueryParams } from "../utils/types/query-params";
52 import { HtmlTags } from "./common/html-tags";
53 import { Icon, Spinner } from "./common/icon";
54 import { MomentTime } from "./common/moment-time";
55 import { Paginator } from "./common/paginator";
56 import { SearchableSelect } from "./common/searchable-select";
57 import { CommunityLink } from "./community/community-link";
58 import { PersonListing } from "./person/person-listing";
59
60 type FilterType = "mod" | "user";
61
62 type View =
63   | ModRemovePostView
64   | ModLockPostView
65   | ModFeaturePostView
66   | ModRemoveCommentView
67   | ModRemoveCommunityView
68   | ModBanFromCommunityView
69   | ModBanView
70   | ModAddCommunityView
71   | ModTransferCommunityView
72   | ModAddView
73   | AdminPurgePersonView
74   | AdminPurgeCommunityView
75   | AdminPurgePostView
76   | AdminPurgeCommentView;
77
78 type ModlogData = RouteDataResponse<{
79   res: GetModlogResponse;
80   communityRes: GetCommunityResponse;
81   modUserResponse: GetPersonDetailsResponse;
82   userResponse: GetPersonDetailsResponse;
83 }>;
84
85 interface ModlogType {
86   id: number;
87   type_: ModlogActionType;
88   moderator?: Person;
89   view: View;
90   when_: string;
91 }
92
93 const getModlogQueryParams = () =>
94   getQueryParams<ModlogProps>({
95     actionType: getActionFromString,
96     modId: getIdFromString,
97     userId: getIdFromString,
98     page: getPageFromString,
99   });
100
101 interface ModlogState {
102   res: RequestState<GetModlogResponse>;
103   communityRes: RequestState<GetCommunityResponse>;
104   loadingModSearch: boolean;
105   loadingUserSearch: boolean;
106   modSearchOptions: Choice[];
107   userSearchOptions: Choice[];
108 }
109
110 interface ModlogProps {
111   page: number;
112   userId?: number | null;
113   modId?: number | null;
114   actionType: ModlogActionType;
115 }
116
117 function getActionFromString(action?: string): ModlogActionType {
118   return action !== undefined ? (action as ModlogActionType) : "All";
119 }
120
121 const getModlogActionMapper =
122   (
123     actionType: ModlogActionType,
124     getAction: (view: View) => { id: number; when_: string }
125   ) =>
126   (view: View & { moderator?: Person; admin?: Person }): ModlogType => {
127     const { id, when_ } = getAction(view);
128
129     return {
130       id,
131       type_: actionType,
132       view,
133       when_,
134       moderator: view.moderator ?? view.admin,
135     };
136   };
137
138 function buildCombined({
139   removed_comments,
140   locked_posts,
141   featured_posts,
142   removed_communities,
143   removed_posts,
144   added,
145   added_to_community,
146   admin_purged_comments,
147   admin_purged_communities,
148   admin_purged_persons,
149   admin_purged_posts,
150   banned,
151   banned_from_community,
152   transferred_to_community,
153 }: GetModlogResponse): ModlogType[] {
154   const combined = removed_posts
155     .map(
156       getModlogActionMapper(
157         "ModRemovePost",
158         ({ mod_remove_post }: ModRemovePostView) => mod_remove_post
159       )
160     )
161     .concat(
162       locked_posts.map(
163         getModlogActionMapper(
164           "ModLockPost",
165           ({ mod_lock_post }: ModLockPostView) => mod_lock_post
166         )
167       )
168     )
169     .concat(
170       featured_posts.map(
171         getModlogActionMapper(
172           "ModFeaturePost",
173           ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post
174         )
175       )
176     )
177     .concat(
178       removed_comments.map(
179         getModlogActionMapper(
180           "ModRemoveComment",
181           ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment
182         )
183       )
184     )
185     .concat(
186       removed_communities.map(
187         getModlogActionMapper(
188           "ModRemoveCommunity",
189           ({ mod_remove_community }: ModRemoveCommunityView) =>
190             mod_remove_community
191         )
192       )
193     )
194     .concat(
195       banned_from_community.map(
196         getModlogActionMapper(
197           "ModBanFromCommunity",
198           ({ mod_ban_from_community }: ModBanFromCommunityView) =>
199             mod_ban_from_community
200         )
201       )
202     )
203     .concat(
204       added_to_community.map(
205         getModlogActionMapper(
206           "ModAddCommunity",
207           ({ mod_add_community }: ModAddCommunityView) => mod_add_community
208         )
209       )
210     )
211     .concat(
212       transferred_to_community.map(
213         getModlogActionMapper(
214           "ModTransferCommunity",
215           ({ mod_transfer_community }: ModTransferCommunityView) =>
216             mod_transfer_community
217         )
218       )
219     )
220     .concat(
221       added.map(
222         getModlogActionMapper("ModAdd", ({ mod_add }: ModAddView) => mod_add)
223       )
224     )
225     .concat(
226       banned.map(
227         getModlogActionMapper("ModBan", ({ mod_ban }: ModBanView) => mod_ban)
228       )
229     )
230     .concat(
231       admin_purged_persons.map(
232         getModlogActionMapper(
233           "AdminPurgePerson",
234           ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person
235         )
236       )
237     )
238     .concat(
239       admin_purged_communities.map(
240         getModlogActionMapper(
241           "AdminPurgeCommunity",
242           ({ admin_purge_community }: AdminPurgeCommunityView) =>
243             admin_purge_community
244         )
245       )
246     )
247     .concat(
248       admin_purged_posts.map(
249         getModlogActionMapper(
250           "AdminPurgePost",
251           ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post
252         )
253       )
254     )
255     .concat(
256       admin_purged_comments.map(
257         getModlogActionMapper(
258           "AdminPurgeComment",
259           ({ admin_purge_comment }: AdminPurgeCommentView) =>
260             admin_purge_comment
261         )
262       )
263     );
264
265   // Sort them by time
266   combined.sort((a, b) => b.when_.localeCompare(a.when_));
267
268   return combined;
269 }
270
271 function renderModlogType({ type_, view }: ModlogType) {
272   switch (type_) {
273     case "ModRemovePost": {
274       const mrpv = view as ModRemovePostView;
275       const {
276         mod_remove_post: { reason, removed },
277         post: { name, id },
278       } = mrpv;
279
280       return (
281         <>
282           <span>{removed ? "Removed " : "Restored "}</span>
283           <span>
284             Post <Link to={`/post/${id}`}>{name}</Link>
285           </span>
286           {reason && (
287             <span>
288               <div>reason: {reason}</div>
289             </span>
290           )}
291         </>
292       );
293     }
294
295     case "ModLockPost": {
296       const {
297         mod_lock_post: { locked },
298         post: { id, name },
299       } = view as ModLockPostView;
300
301       return (
302         <>
303           <span>{locked ? "Locked " : "Unlocked "}</span>
304           <span>
305             Post <Link to={`/post/${id}`}>{name}</Link>
306           </span>
307         </>
308       );
309     }
310
311     case "ModFeaturePost": {
312       const {
313         mod_feature_post: { featured, is_featured_community },
314         post: { id, name },
315       } = view as ModFeaturePostView;
316
317       return (
318         <>
319           <span>{featured ? "Featured " : "Unfeatured "}</span>
320           <span>
321             Post <Link to={`/post/${id}`}>{name}</Link>
322           </span>
323           <span>{is_featured_community ? " In Community" : " In Local"}</span>
324         </>
325       );
326     }
327     case "ModRemoveComment": {
328       const mrc = view as ModRemoveCommentView;
329       const {
330         mod_remove_comment: { reason, removed },
331         comment: { id, content },
332         commenter,
333       } = mrc;
334
335       return (
336         <>
337           <span>{removed ? "Removed " : "Restored "}</span>
338           <span>
339             Comment <Link to={`/comment/${id}`}>{content}</Link>
340           </span>
341           <span>
342             {" "}
343             by <PersonListing person={commenter} />
344           </span>
345           {reason && (
346             <span>
347               <div>reason: {reason}</div>
348             </span>
349           )}
350         </>
351       );
352     }
353
354     case "ModRemoveCommunity": {
355       const mrco = view as ModRemoveCommunityView;
356       const {
357         mod_remove_community: { reason, expires, removed },
358         community,
359       } = mrco;
360
361       return (
362         <>
363           <span>{removed ? "Removed " : "Restored "}</span>
364           <span>
365             Community <CommunityLink community={community} />
366           </span>
367           {reason && (
368             <span>
369               <div>reason: {reason}</div>
370             </span>
371           )}
372           {expires && (
373             <span>
374               <div>expires: {moment.utc(expires).fromNow()}</div>
375             </span>
376           )}
377         </>
378       );
379     }
380
381     case "ModBanFromCommunity": {
382       const mbfc = view as ModBanFromCommunityView;
383       const {
384         mod_ban_from_community: { reason, expires, banned },
385         banned_person,
386         community,
387       } = mbfc;
388
389       return (
390         <>
391           <span>{banned ? "Banned " : "Unbanned "}</span>
392           <span>
393             <PersonListing person={banned_person} />
394           </span>
395           <span> from the community </span>
396           <span>
397             <CommunityLink community={community} />
398           </span>
399           {reason && (
400             <span>
401               <div>reason: {reason}</div>
402             </span>
403           )}
404           {expires && (
405             <span>
406               <div>expires: {moment.utc(expires).fromNow()}</div>
407             </span>
408           )}
409         </>
410       );
411     }
412
413     case "ModAddCommunity": {
414       const {
415         mod_add_community: { removed },
416         modded_person,
417         community,
418       } = view as ModAddCommunityView;
419
420       return (
421         <>
422           <span>{removed ? "Removed " : "Appointed "}</span>
423           <span>
424             <PersonListing person={modded_person} />
425           </span>
426           <span> as a mod to the community </span>
427           <span>
428             <CommunityLink community={community} />
429           </span>
430         </>
431       );
432     }
433
434     case "ModTransferCommunity": {
435       const { community, modded_person } = view as ModTransferCommunityView;
436
437       return (
438         <>
439           <span>Transferred</span>
440           <span>
441             <CommunityLink community={community} />
442           </span>
443           <span> to </span>
444           <span>
445             <PersonListing person={modded_person} />
446           </span>
447         </>
448       );
449     }
450
451     case "ModBan": {
452       const {
453         mod_ban: { reason, expires, banned },
454         banned_person,
455       } = view as ModBanView;
456
457       return (
458         <>
459           <span>{banned ? "Banned " : "Unbanned "}</span>
460           <span>
461             <PersonListing person={banned_person} />
462           </span>
463           {reason && (
464             <span>
465               <div>reason: {reason}</div>
466             </span>
467           )}
468           {expires && (
469             <span>
470               <div>expires: {moment.utc(expires).fromNow()}</div>
471             </span>
472           )}
473         </>
474       );
475     }
476
477     case "ModAdd": {
478       const {
479         mod_add: { removed },
480         modded_person,
481       } = view as ModAddView;
482
483       return (
484         <>
485           <span>{removed ? "Removed " : "Appointed "}</span>
486           <span>
487             <PersonListing person={modded_person} />
488           </span>
489           <span> as an admin </span>
490         </>
491       );
492     }
493     case "AdminPurgePerson": {
494       const {
495         admin_purge_person: { reason },
496       } = view as AdminPurgePersonView;
497
498       return (
499         <>
500           <span>Purged a Person</span>
501           {reason && (
502             <span>
503               <div>reason: {reason}</div>
504             </span>
505           )}
506         </>
507       );
508     }
509
510     case "AdminPurgeCommunity": {
511       const {
512         admin_purge_community: { reason },
513       } = view as AdminPurgeCommunityView;
514
515       return (
516         <>
517           <span>Purged a Community</span>
518           {reason && (
519             <span>
520               <div>reason: {reason}</div>
521             </span>
522           )}
523         </>
524       );
525     }
526
527     case "AdminPurgePost": {
528       const {
529         admin_purge_post: { reason },
530         community,
531       } = view as AdminPurgePostView;
532
533       return (
534         <>
535           <span>Purged a Post from from </span>
536           <CommunityLink community={community} />
537           {reason && (
538             <span>
539               <div>reason: {reason}</div>
540             </span>
541           )}
542         </>
543       );
544     }
545
546     case "AdminPurgeComment": {
547       const {
548         admin_purge_comment: { reason },
549         post: { id, name },
550       } = view as AdminPurgeCommentView;
551
552       return (
553         <>
554           <span>
555             Purged a Comment from <Link to={`/post/${id}`}>{name}</Link>
556           </span>
557           {reason && (
558             <span>
559               <div>reason: {reason}</div>
560             </span>
561           )}
562         </>
563       );
564     }
565
566     default:
567       return <></>;
568   }
569 }
570
571 const Filter = ({
572   filterType,
573   onChange,
574   value,
575   onSearch,
576   options,
577   loading,
578 }: {
579   filterType: FilterType;
580   onChange: (option: Choice) => void;
581   value?: number | null;
582   onSearch: (text: string) => void;
583   options: Choice[];
584   loading: boolean;
585 }) => (
586   <div className="col-sm-6 form-group">
587     <label className="col-form-label" htmlFor={`filter-${filterType}`}>
588       {i18n.t(`filter_by_${filterType}` as NoOptionI18nKeys)}
589     </label>
590     <SearchableSelect
591       id={`filter-${filterType}`}
592       value={value ?? 0}
593       options={[
594         {
595           label: i18n.t("all"),
596           value: "0",
597         },
598       ].concat(options)}
599       onChange={onChange}
600       onSearch={onSearch}
601       loading={loading}
602     />
603   </div>
604 );
605
606 async function createNewOptions({
607   id,
608   oldOptions,
609   text,
610 }: {
611   id?: number | null;
612   oldOptions: Choice[];
613   text: string;
614 }) {
615   const newOptions: Choice[] = [];
616
617   if (id) {
618     const selectedUser = oldOptions.find(
619       ({ value }) => value === id.toString()
620     );
621
622     if (selectedUser) {
623       newOptions.push(selectedUser);
624     }
625   }
626
627   if (text.length > 0) {
628     newOptions.push(
629       ...(await fetchUsers(text))
630         .slice(0, Number(fetchLimit))
631         .map<Choice>(personToChoice)
632     );
633   }
634
635   return newOptions;
636 }
637
638 export class Modlog extends Component<
639   RouteComponentProps<{ communityId?: string }>,
640   ModlogState
641 > {
642   private isoData = setIsoData<ModlogData>(this.context);
643
644   state: ModlogState = {
645     res: { state: "empty" },
646     communityRes: { state: "empty" },
647     loadingModSearch: false,
648     loadingUserSearch: false,
649     userSearchOptions: [],
650     modSearchOptions: [],
651   };
652
653   constructor(
654     props: RouteComponentProps<{ communityId?: string }>,
655     context: any
656   ) {
657     super(props, context);
658     this.handlePageChange = this.handlePageChange.bind(this);
659     this.handleUserChange = this.handleUserChange.bind(this);
660     this.handleModChange = this.handleModChange.bind(this);
661
662     // Only fetch the data if coming from another route
663     if (FirstLoadService.isFirstLoad) {
664       const { res, communityRes, modUserResponse, userResponse } =
665         this.isoData.routeData;
666
667       this.state = {
668         ...this.state,
669         res,
670         communityRes,
671       };
672
673       if (modUserResponse.state === "success") {
674         this.state = {
675           ...this.state,
676           modSearchOptions: [personToChoice(modUserResponse.data.person_view)],
677         };
678       }
679
680       if (userResponse.state === "success") {
681         this.state = {
682           ...this.state,
683           userSearchOptions: [personToChoice(userResponse.data.person_view)],
684         };
685       }
686     }
687   }
688
689   get combined() {
690     const res = this.state.res;
691     const combined = res.state == "success" ? buildCombined(res.data) : [];
692
693     return (
694       <tbody>
695         {combined.map(i => (
696           <tr key={i.id}>
697             <td>
698               <MomentTime published={i.when_} />
699             </td>
700             <td>
701               {this.amAdminOrMod && i.moderator ? (
702                 <PersonListing person={i.moderator} />
703               ) : (
704                 <div>{this.modOrAdminText(i.moderator)}</div>
705               )}
706             </td>
707             <td>{renderModlogType(i)}</td>
708           </tr>
709         ))}
710       </tbody>
711     );
712   }
713
714   get amAdminOrMod(): boolean {
715     const amMod_ =
716       this.state.communityRes.state == "success" &&
717       amMod(this.state.communityRes.data.moderators);
718     return amAdmin() || amMod_;
719   }
720
721   modOrAdminText(person?: Person): string {
722     return person &&
723       this.isoData.site_res.admins.some(
724         ({ person: { id } }) => id === person.id
725       )
726       ? i18n.t("admin")
727       : i18n.t("mod");
728   }
729
730   get documentTitle(): string {
731     return `Modlog - ${this.isoData.site_res.site_view.site.name}`;
732   }
733
734   render() {
735     const {
736       loadingModSearch,
737       loadingUserSearch,
738       userSearchOptions,
739       modSearchOptions,
740     } = this.state;
741     const { actionType, modId, userId } = getModlogQueryParams();
742
743     return (
744       <div className="container-lg">
745         <HtmlTags
746           title={this.documentTitle}
747           path={this.context.router.route.match.url}
748         />
749
750         <div>
751           <div
752             className="alert alert-warning text-sm-start text-xs-center"
753             role="alert"
754           >
755             <Icon
756               icon="alert-triangle"
757               inline
758               classes="mr-sm-2 mx-auto d-sm-inline d-block"
759             />
760             <T i18nKey="modlog_content_warning" class="d-inline">
761               #<strong>#</strong>#
762             </T>
763           </div>
764           {this.state.communityRes.state === "success" && (
765             <h5>
766               <Link
767                 className="text-body"
768                 to={`/c/${this.state.communityRes.data.community_view.community.name}`}
769               >
770                 /c/{this.state.communityRes.data.community_view.community.name}{" "}
771               </Link>
772               <span>{i18n.t("modlog")}</span>
773             </h5>
774           )}
775           <div className="form-row">
776             <select
777               value={actionType}
778               onChange={linkEvent(this, this.handleFilterActionChange)}
779               className="custom-select col-sm-6"
780               aria-label="action"
781             >
782               <option disabled aria-hidden="true">
783                 {i18n.t("filter_by_action")}
784               </option>
785               <option value={"All"}>{i18n.t("all")}</option>
786               <option value={"ModRemovePost"}>Removing Posts</option>
787               <option value={"ModLockPost"}>Locking Posts</option>
788               <option value={"ModFeaturePost"}>Featuring Posts</option>
789               <option value={"ModRemoveComment"}>Removing Comments</option>
790               <option value={"ModRemoveCommunity"}>Removing Communities</option>
791               <option value={"ModBanFromCommunity"}>
792                 Banning From Communities
793               </option>
794               <option value={"ModAddCommunity"}>Adding Mod to Community</option>
795               <option value={"ModTransferCommunity"}>
796                 Transferring Communities
797               </option>
798               <option value={"ModAdd"}>Adding Mod to Site</option>
799               <option value={"ModBan"}>Banning From Site</option>
800             </select>
801           </div>
802           <div className="form-row mb-2">
803             <Filter
804               filterType="user"
805               onChange={this.handleUserChange}
806               onSearch={this.handleSearchUsers}
807               value={userId}
808               options={userSearchOptions}
809               loading={loadingUserSearch}
810             />
811             {!this.isoData.site_res.site_view.local_site
812               .hide_modlog_mod_names && (
813               <Filter
814                 filterType="mod"
815                 onChange={this.handleModChange}
816                 onSearch={this.handleSearchMods}
817                 value={modId}
818                 options={modSearchOptions}
819                 loading={loadingModSearch}
820               />
821             )}
822           </div>
823           {this.renderModlogTable()}
824         </div>
825       </div>
826     );
827   }
828
829   renderModlogTable() {
830     switch (this.state.res.state) {
831       case "loading":
832         return (
833           <h5>
834             <Spinner large />
835           </h5>
836         );
837       case "success": {
838         const page = getModlogQueryParams().page;
839         return (
840           <div className="table-responsive">
841             <table id="modlog_table" className="table table-sm table-hover">
842               <thead className="pointer">
843                 <tr>
844                   <th> {i18n.t("time")}</th>
845                   <th>{i18n.t("mod")}</th>
846                   <th>{i18n.t("action")}</th>
847                 </tr>
848               </thead>
849               {this.combined}
850             </table>
851             <Paginator page={page} onChange={this.handlePageChange} />
852           </div>
853         );
854       }
855     }
856   }
857
858   handleFilterActionChange(i: Modlog, event: any) {
859     i.updateUrl({
860       actionType: event.target.value as ModlogActionType,
861       page: 1,
862     });
863   }
864
865   handlePageChange(page: number) {
866     this.updateUrl({ page });
867   }
868
869   handleUserChange(option: Choice) {
870     this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 });
871   }
872
873   handleModChange(option: Choice) {
874     this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 });
875   }
876
877   handleSearchUsers = debounce(async (text: string) => {
878     const { userId } = getModlogQueryParams();
879     const { userSearchOptions } = this.state;
880     this.setState({ loadingUserSearch: true });
881
882     const newOptions = await createNewOptions({
883       id: userId,
884       text,
885       oldOptions: userSearchOptions,
886     });
887
888     this.setState({
889       userSearchOptions: newOptions,
890       loadingUserSearch: false,
891     });
892   });
893
894   handleSearchMods = debounce(async (text: string) => {
895     const { modId } = getModlogQueryParams();
896     const { modSearchOptions } = this.state;
897     this.setState({ loadingModSearch: true });
898
899     const newOptions = await createNewOptions({
900       id: modId,
901       text,
902       oldOptions: modSearchOptions,
903     });
904
905     this.setState({
906       modSearchOptions: newOptions,
907       loadingModSearch: false,
908     });
909   });
910
911   async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
912     const {
913       page: urlPage,
914       actionType: urlActionType,
915       modId: urlModId,
916       userId: urlUserId,
917     } = getModlogQueryParams();
918
919     const queryParams: QueryParams<ModlogProps> = {
920       page: (page ?? urlPage).toString(),
921       actionType: actionType ?? urlActionType,
922       modId: getUpdatedSearchId(modId, urlModId),
923       userId: getUpdatedSearchId(userId, urlUserId),
924     };
925
926     const communityId = this.props.match.params.communityId;
927
928     this.props.history.push(
929       `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
930         queryParams
931       )}`
932     );
933
934     await this.refetch();
935   }
936
937   async refetch() {
938     const auth = myAuth();
939     const { actionType, page, modId, userId } = getModlogQueryParams();
940     const { communityId: urlCommunityId } = this.props.match.params;
941     const communityId = getIdFromString(urlCommunityId);
942
943     this.setState({ res: { state: "loading" } });
944     this.setState({
945       res: await HttpService.client.getModlog({
946         community_id: communityId,
947         page,
948         limit: fetchLimit,
949         type_: actionType,
950         other_person_id: userId ?? undefined,
951         mod_person_id: !this.isoData.site_res.site_view.local_site
952           .hide_modlog_mod_names
953           ? modId ?? undefined
954           : undefined,
955         auth,
956       }),
957     });
958
959     if (communityId) {
960       this.setState({ communityRes: { state: "loading" } });
961       this.setState({
962         communityRes: await HttpService.client.getCommunity({
963           id: communityId,
964           auth,
965         }),
966       });
967     }
968   }
969
970   static async fetchInitialData({
971     client,
972     path,
973     query: { modId: urlModId, page, userId: urlUserId, actionType },
974     auth,
975     site,
976   }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> {
977     const pathSplit = path.split("/");
978     const communityId = getIdFromString(pathSplit[2]);
979     const modId = !site.site_view.local_site.hide_modlog_mod_names
980       ? getIdFromString(urlModId)
981       : undefined;
982     const userId = getIdFromString(urlUserId);
983
984     const modlogForm: GetModlog = {
985       page: getPageFromString(page),
986       limit: fetchLimit,
987       community_id: communityId,
988       type_: getActionFromString(actionType),
989       mod_person_id: modId,
990       other_person_id: userId,
991       auth,
992     };
993
994     let communityResponse: RequestState<GetCommunityResponse> = {
995       state: "empty",
996     };
997
998     if (communityId) {
999       const communityForm: GetCommunity = {
1000         id: communityId,
1001         auth,
1002       };
1003
1004       communityResponse = await client.getCommunity(communityForm);
1005     }
1006
1007     let modUserResponse: RequestState<GetPersonDetailsResponse> = {
1008       state: "empty",
1009     };
1010
1011     if (modId) {
1012       const getPersonForm: GetPersonDetails = {
1013         person_id: modId,
1014         auth,
1015       };
1016
1017       modUserResponse = await client.getPersonDetails(getPersonForm);
1018     }
1019
1020     let userResponse: RequestState<GetPersonDetailsResponse> = {
1021       state: "empty",
1022     };
1023
1024     if (userId) {
1025       const getPersonForm: GetPersonDetails = {
1026         person_id: userId,
1027         auth,
1028       };
1029
1030       userResponse = await client.getPersonDetails(getPersonForm);
1031     }
1032
1033     return {
1034       res: await client.getModlog(modlogForm),
1035       communityRes: communityResponse,
1036       modUserResponse,
1037       userResponse,
1038     };
1039   }
1040 }