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