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