]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/profile.tsx
Make pages use query params instead of route params where appropriate (#977)
[lemmy-ui.git] / src / shared / components / person / profile.tsx
1 import classNames from "classnames";
2 import { NoOptionI18nKeys } from "i18next";
3 import { Component, linkEvent } from "inferno";
4 import { Link } from "inferno-router";
5 import { RouteComponentProps } from "inferno-router/dist/Route";
6 import {
7   AddAdminResponse,
8   BanPerson,
9   BanPersonResponse,
10   BlockPerson,
11   BlockPersonResponse,
12   CommentResponse,
13   CommunityModeratorView,
14   CommunitySafe,
15   GetPersonDetails,
16   GetPersonDetailsResponse,
17   GetSiteResponse,
18   PostResponse,
19   PurgeItemResponse,
20   SortType,
21   UserOperation,
22   wsJsonToRes,
23   wsUserOp,
24 } from "lemmy-js-client";
25 import moment from "moment";
26 import { Subscription } from "rxjs";
27 import { i18n } from "../../i18next";
28 import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
29 import { UserService, WebSocketService } from "../../services";
30 import {
31   canMod,
32   capitalizeFirstLetter,
33   createCommentLikeRes,
34   createPostLikeFindRes,
35   editCommentRes,
36   editPostFindRes,
37   enableDownvotes,
38   enableNsfw,
39   fetchLimit,
40   futureDaysToUnixTime,
41   getPageFromString,
42   getQueryParams,
43   getQueryString,
44   isAdmin,
45   isBanned,
46   mdToHtml,
47   myAuth,
48   numToSI,
49   QueryParams,
50   relTags,
51   restoreScrollPosition,
52   routeSortTypeToEnum,
53   saveCommentRes,
54   saveScrollPosition,
55   setIsoData,
56   setupTippy,
57   toast,
58   updatePersonBlock,
59   wsClient,
60   wsSubscribe,
61 } from "../../utils";
62 import { BannerIconHeader } from "../common/banner-icon-header";
63 import { HtmlTags } from "../common/html-tags";
64 import { Icon, Spinner } from "../common/icon";
65 import { MomentTime } from "../common/moment-time";
66 import { SortSelect } from "../common/sort-select";
67 import { CommunityLink } from "../community/community-link";
68 import { PersonDetails } from "./person-details";
69 import { PersonListing } from "./person-listing";
70
71 interface ProfileState {
72   personRes?: GetPersonDetailsResponse;
73   loading: boolean;
74   personBlocked: boolean;
75   banReason?: string;
76   banExpireDays?: number;
77   showBanDialog: boolean;
78   removeData: boolean;
79   siteRes: GetSiteResponse;
80 }
81
82 interface ProfileProps {
83   view: PersonDetailsView;
84   sort: SortType;
85   page: number;
86 }
87
88 const getProfileQueryParams = () =>
89   getQueryParams<ProfileProps>({
90     view: getViewFromProps,
91     page: getPageFromString,
92     sort: getSortTypeFromQuery,
93   });
94
95 const getSortTypeFromQuery = (sort?: string): SortType =>
96   sort ? routeSortTypeToEnum(sort, SortType.New) : SortType.New;
97
98 const getViewFromProps = (view?: string): PersonDetailsView =>
99   view
100     ? PersonDetailsView[view] ?? PersonDetailsView.Overview
101     : PersonDetailsView.Overview;
102
103 function toggleBlockPerson(recipientId: number, block: boolean) {
104   const auth = myAuth();
105
106   if (auth) {
107     const blockUserForm: BlockPerson = {
108       person_id: recipientId,
109       block,
110       auth,
111     };
112
113     WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
114   }
115 }
116
117 const handleUnblockPerson = (personId: number) =>
118   toggleBlockPerson(personId, false);
119
120 const handleBlockPerson = (personId: number) =>
121   toggleBlockPerson(personId, true);
122
123 const getCommunitiesListing = (
124   translationKey: NoOptionI18nKeys,
125   communityViews?: { community: CommunitySafe }[]
126 ) =>
127   communityViews &&
128   communityViews.length > 0 && (
129     <div className="card border-secondary mb-3">
130       <div className="card-body">
131         <h5>{i18n.t(translationKey)}</h5>
132         <ul className="list-unstyled mb-0">
133           {communityViews.map(({ community }) => (
134             <li key={community.id}>
135               <CommunityLink community={community} />
136             </li>
137           ))}
138         </ul>
139       </div>
140     </div>
141   );
142
143 const Moderates = ({ moderates }: { moderates?: CommunityModeratorView[] }) =>
144   getCommunitiesListing("moderates", moderates);
145
146 const Follows = () =>
147   getCommunitiesListing("subscribed", UserService.Instance.myUserInfo?.follows);
148
149 export class Profile extends Component<
150   RouteComponentProps<{ username: string }>,
151   ProfileState
152 > {
153   private isoData = setIsoData(this.context);
154   private subscription?: Subscription;
155   state: ProfileState = {
156     loading: true,
157     personBlocked: false,
158     siteRes: this.isoData.site_res,
159     showBanDialog: false,
160     removeData: false,
161   };
162
163   constructor(props: RouteComponentProps<{ username: string }>, context: any) {
164     super(props, context);
165
166     this.handleSortChange = this.handleSortChange.bind(this);
167     this.handlePageChange = this.handlePageChange.bind(this);
168
169     this.parseMessage = this.parseMessage.bind(this);
170     this.subscription = wsSubscribe(this.parseMessage);
171
172     // Only fetch the data if coming from another route
173     if (this.isoData.path === this.context.router.route.match.url) {
174       this.state = {
175         ...this.state,
176         personRes: this.isoData.routeData[0] as GetPersonDetailsResponse,
177         loading: false,
178       };
179     } else {
180       this.fetchUserData();
181     }
182   }
183
184   fetchUserData() {
185     const { page, sort, view } = getProfileQueryParams();
186
187     const form: GetPersonDetails = {
188       username: this.props.match.params.username,
189       sort,
190       saved_only: view === PersonDetailsView.Saved,
191       page,
192       limit: fetchLimit,
193       auth: myAuth(false),
194     };
195
196     WebSocketService.Instance.send(wsClient.getPersonDetails(form));
197   }
198
199   get amCurrentUser() {
200     return (
201       UserService.Instance.myUserInfo?.local_user_view.person.id ===
202       this.state.personRes?.person_view.person.id
203     );
204   }
205
206   setPersonBlock() {
207     const mui = UserService.Instance.myUserInfo;
208     const res = this.state.personRes;
209
210     if (mui && res) {
211       this.setState({
212         personBlocked: mui.person_blocks.some(
213           ({ target: { id } }) => id === res.person_view.person.id
214         ),
215       });
216     }
217   }
218
219   static fetchInitialData({
220     client,
221     path,
222     query: { page, sort, view: urlView },
223     auth,
224   }: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<any>[] {
225     const pathSplit = path.split("/");
226
227     const username = pathSplit[2];
228     const view = getViewFromProps(urlView);
229
230     const form: GetPersonDetails = {
231       username: username,
232       sort: getSortTypeFromQuery(sort),
233       saved_only: view === PersonDetailsView.Saved,
234       page: getPageFromString(page),
235       limit: fetchLimit,
236       auth,
237     };
238
239     return [client.getPersonDetails(form)];
240   }
241
242   componentDidMount() {
243     this.setPersonBlock();
244     setupTippy();
245   }
246
247   componentWillUnmount() {
248     this.subscription?.unsubscribe();
249     saveScrollPosition(this.context);
250   }
251
252   get documentTitle(): string {
253     const res = this.state.personRes;
254     return res
255       ? `@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`
256       : "";
257   }
258
259   render() {
260     const { personRes, loading, siteRes } = this.state;
261     const { page, sort, view } = getProfileQueryParams();
262
263     return (
264       <div className="container-lg">
265         {loading ? (
266           <h5>
267             <Spinner large />
268           </h5>
269         ) : (
270           personRes && (
271             <div className="row">
272               <div className="col-12 col-md-8">
273                 <HtmlTags
274                   title={this.documentTitle}
275                   path={this.context.router.route.match.url}
276                   description={personRes.person_view.person.bio}
277                   image={personRes.person_view.person.avatar}
278                 />
279
280                 {this.userInfo}
281
282                 <hr />
283
284                 {this.selects}
285
286                 <PersonDetails
287                   personRes={personRes}
288                   admins={siteRes.admins}
289                   sort={sort}
290                   page={page}
291                   limit={fetchLimit}
292                   enableDownvotes={enableDownvotes(siteRes)}
293                   enableNsfw={enableNsfw(siteRes)}
294                   view={view}
295                   onPageChange={this.handlePageChange}
296                   allLanguages={siteRes.all_languages}
297                   siteLanguages={siteRes.discussion_languages}
298                 />
299               </div>
300
301               <div className="col-12 col-md-4">
302                 <Moderates moderates={personRes.moderates} />
303                 {this.amCurrentUser && <Follows />}
304               </div>
305             </div>
306           )
307         )}
308       </div>
309     );
310   }
311
312   get viewRadios() {
313     return (
314       <div className="btn-group btn-group-toggle flex-wrap mb-2">
315         {this.getRadio(PersonDetailsView.Overview)}
316         {this.getRadio(PersonDetailsView.Comments)}
317         {this.getRadio(PersonDetailsView.Posts)}
318         {this.getRadio(PersonDetailsView.Saved)}
319       </div>
320     );
321   }
322
323   getRadio(view: PersonDetailsView) {
324     const { view: urlView } = getProfileQueryParams();
325     const active = view === urlView;
326
327     return (
328       <label
329         className={classNames("btn btn-outline-secondary pointer", {
330           active,
331         })}
332       >
333         <input
334           type="radio"
335           value={view}
336           checked={active}
337           onChange={linkEvent(this, this.handleViewChange)}
338         />
339         {i18n.t(view.toLowerCase() as NoOptionI18nKeys)}
340       </label>
341     );
342   }
343
344   get selects() {
345     const { sort } = getProfileQueryParams();
346     const { username } = this.props.match.params;
347
348     const profileRss = `/feeds/u/${username}.xml?sort=${sort}`;
349
350     return (
351       <div className="mb-2">
352         <span className="mr-3">{this.viewRadios}</span>
353         <SortSelect
354           sort={sort}
355           onChange={this.handleSortChange}
356           hideHot
357           hideMostComments
358         />
359         <a href={profileRss} rel={relTags} title="RSS">
360           <Icon icon="rss" classes="text-muted small mx-2" />
361         </a>
362         <link rel="alternate" type="application/atom+xml" href={profileRss} />
363       </div>
364     );
365   }
366
367   get userInfo() {
368     const pv = this.state.personRes?.person_view;
369     const {
370       personBlocked,
371       siteRes: { admins },
372       showBanDialog,
373     } = this.state;
374
375     return (
376       pv && (
377         <div>
378           {!isBanned(pv.person) && (
379             <BannerIconHeader
380               banner={pv.person.banner}
381               icon={pv.person.avatar}
382             />
383           )}
384           <div className="mb-3">
385             <div className="">
386               <div className="mb-0 d-flex flex-wrap">
387                 <div>
388                   {pv.person.display_name && (
389                     <h5 className="mb-0">{pv.person.display_name}</h5>
390                   )}
391                   <ul className="list-inline mb-2">
392                     <li className="list-inline-item">
393                       <PersonListing
394                         person={pv.person}
395                         realLink
396                         useApubName
397                         muted
398                         hideAvatar
399                       />
400                     </li>
401                     {isBanned(pv.person) && (
402                       <li className="list-inline-item badge badge-danger">
403                         {i18n.t("banned")}
404                       </li>
405                     )}
406                     {pv.person.deleted && (
407                       <li className="list-inline-item badge badge-danger">
408                         {i18n.t("deleted")}
409                       </li>
410                     )}
411                     {pv.person.admin && (
412                       <li className="list-inline-item badge badge-light">
413                         {i18n.t("admin")}
414                       </li>
415                     )}
416                     {pv.person.bot_account && (
417                       <li className="list-inline-item badge badge-light">
418                         {i18n.t("bot_account").toLowerCase()}
419                       </li>
420                     )}
421                   </ul>
422                 </div>
423                 {this.banDialog}
424                 <div className="flex-grow-1 unselectable pointer mx-2"></div>
425                 {!this.amCurrentUser && UserService.Instance.myUserInfo && (
426                   <>
427                     <a
428                       className={`d-flex align-self-start btn btn-secondary mr-2 ${
429                         !pv.person.matrix_user_id && "invisible"
430                       }`}
431                       rel={relTags}
432                       href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
433                     >
434                       {i18n.t("send_secure_message")}
435                     </a>
436                     <Link
437                       className={
438                         "d-flex align-self-start btn btn-secondary mr-2"
439                       }
440                       to={`/create_private_message/${pv.person.id}`}
441                     >
442                       {i18n.t("send_message")}
443                     </Link>
444                     {personBlocked ? (
445                       <button
446                         className={
447                           "d-flex align-self-start btn btn-secondary mr-2"
448                         }
449                         onClick={linkEvent(pv.person.id, handleUnblockPerson)}
450                       >
451                         {i18n.t("unblock_user")}
452                       </button>
453                     ) : (
454                       <button
455                         className={
456                           "d-flex align-self-start btn btn-secondary mr-2"
457                         }
458                         onClick={linkEvent(pv.person.id, handleBlockPerson)}
459                       >
460                         {i18n.t("block_user")}
461                       </button>
462                     )}
463                   </>
464                 )}
465
466                 {canMod(pv.person.id, undefined, admins) &&
467                   !isAdmin(pv.person.id, admins) &&
468                   !showBanDialog &&
469                   (!isBanned(pv.person) ? (
470                     <button
471                       className={
472                         "d-flex align-self-start btn btn-secondary mr-2"
473                       }
474                       onClick={linkEvent(this, this.handleModBanShow)}
475                       aria-label={i18n.t("ban")}
476                     >
477                       {capitalizeFirstLetter(i18n.t("ban"))}
478                     </button>
479                   ) : (
480                     <button
481                       className={
482                         "d-flex align-self-start btn btn-secondary mr-2"
483                       }
484                       onClick={linkEvent(this, this.handleModBanSubmit)}
485                       aria-label={i18n.t("unban")}
486                     >
487                       {capitalizeFirstLetter(i18n.t("unban"))}
488                     </button>
489                   ))}
490               </div>
491               {pv.person.bio && (
492                 <div className="d-flex align-items-center mb-2">
493                   <div
494                     className="md-div"
495                     dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
496                   />
497                 </div>
498               )}
499               <div>
500                 <ul className="list-inline mb-2">
501                   <li className="list-inline-item badge badge-light">
502                     {i18n.t("number_of_posts", {
503                       count: pv.counts.post_count,
504                       formattedCount: numToSI(pv.counts.post_count),
505                     })}
506                   </li>
507                   <li className="list-inline-item badge badge-light">
508                     {i18n.t("number_of_comments", {
509                       count: pv.counts.comment_count,
510                       formattedCount: numToSI(pv.counts.comment_count),
511                     })}
512                   </li>
513                 </ul>
514               </div>
515               <div className="text-muted">
516                 {i18n.t("joined")}{" "}
517                 <MomentTime
518                   published={pv.person.published}
519                   showAgo
520                   ignoreUpdated
521                 />
522               </div>
523               <div className="d-flex align-items-center text-muted mb-2">
524                 <Icon icon="cake" />
525                 <span className="ml-2">
526                   {i18n.t("cake_day_title")}{" "}
527                   {moment
528                     .utc(pv.person.published)
529                     .local()
530                     .format("MMM DD, YYYY")}
531                 </span>
532               </div>
533             </div>
534           </div>
535         </div>
536       )
537     );
538   }
539
540   get banDialog() {
541     const pv = this.state.personRes?.person_view;
542     const { showBanDialog } = this.state;
543
544     return (
545       pv && (
546         <>
547           {showBanDialog && (
548             <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
549               <div className="form-group row col-12">
550                 <label className="col-form-label" htmlFor="profile-ban-reason">
551                   {i18n.t("reason")}
552                 </label>
553                 <input
554                   type="text"
555                   id="profile-ban-reason"
556                   className="form-control mr-2"
557                   placeholder={i18n.t("reason")}
558                   value={this.state.banReason}
559                   onInput={linkEvent(this, this.handleModBanReasonChange)}
560                 />
561                 <label className="col-form-label" htmlFor={`mod-ban-expires`}>
562                   {i18n.t("expires")}
563                 </label>
564                 <input
565                   type="number"
566                   id={`mod-ban-expires`}
567                   className="form-control mr-2"
568                   placeholder={i18n.t("number_of_days")}
569                   value={this.state.banExpireDays}
570                   onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
571                 />
572                 <div className="form-group">
573                   <div className="form-check">
574                     <input
575                       className="form-check-input"
576                       id="mod-ban-remove-data"
577                       type="checkbox"
578                       checked={this.state.removeData}
579                       onChange={linkEvent(this, this.handleModRemoveDataChange)}
580                     />
581                     <label
582                       className="form-check-label"
583                       htmlFor="mod-ban-remove-data"
584                       title={i18n.t("remove_content_more")}
585                     >
586                       {i18n.t("remove_content")}
587                     </label>
588                   </div>
589                 </div>
590               </div>
591               {/* TODO hold off on expires until later */}
592               {/* <div class="form-group row"> */}
593               {/*   <label class="col-form-label">Expires</label> */}
594               {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
595               {/* </div> */}
596               <div className="form-group row">
597                 <button
598                   type="reset"
599                   className="btn btn-secondary mr-2"
600                   aria-label={i18n.t("cancel")}
601                   onClick={linkEvent(this, this.handleModBanSubmitCancel)}
602                 >
603                   {i18n.t("cancel")}
604                 </button>
605                 <button
606                   type="submit"
607                   className="btn btn-secondary"
608                   aria-label={i18n.t("ban")}
609                 >
610                   {i18n.t("ban")} {pv.person.name}
611                 </button>
612               </div>
613             </form>
614           )}
615         </>
616       )
617     );
618   }
619
620   updateUrl({ page, sort, view }: Partial<ProfileProps>) {
621     const {
622       page: urlPage,
623       sort: urlSort,
624       view: urlView,
625     } = getProfileQueryParams();
626
627     const queryParams: QueryParams<ProfileProps> = {
628       page: (page ?? urlPage).toString(),
629       sort: sort ?? urlSort,
630       view: view ?? urlView,
631     };
632
633     const { username } = this.props.match.params;
634
635     this.props.history.push(`/u/${username}${getQueryString(queryParams)}`);
636
637     this.setState({ loading: true });
638     this.fetchUserData();
639   }
640
641   handlePageChange(page: number) {
642     this.updateUrl({ page });
643   }
644
645   handleSortChange(sort: SortType) {
646     this.updateUrl({ sort, page: 1 });
647   }
648
649   handleViewChange(i: Profile, event: any) {
650     i.updateUrl({
651       view: PersonDetailsView[event.target.value],
652       page: 1,
653     });
654   }
655
656   handleModBanShow(i: Profile) {
657     i.setState({ showBanDialog: true });
658   }
659
660   handleModBanReasonChange(i: Profile, event: any) {
661     i.setState({ banReason: event.target.value });
662   }
663
664   handleModBanExpireDaysChange(i: Profile, event: any) {
665     i.setState({ banExpireDays: event.target.value });
666   }
667
668   handleModRemoveDataChange(i: Profile, event: any) {
669     i.setState({ removeData: event.target.checked });
670   }
671
672   handleModBanSubmitCancel(i: Profile, event?: any) {
673     event.preventDefault();
674     i.setState({ showBanDialog: false });
675   }
676
677   handleModBanSubmit(i: Profile, event?: any) {
678     if (event) event.preventDefault();
679     const { personRes, removeData, banReason, banExpireDays } = i.state;
680
681     const person = personRes?.person_view.person;
682     const auth = myAuth();
683
684     if (person && auth) {
685       const ban = !person.banned;
686
687       // If its an unban, restore all their data
688       if (!ban) {
689         i.setState({ removeData: false });
690       }
691
692       const form: BanPerson = {
693         person_id: person.id,
694         ban,
695         remove_data: removeData,
696         reason: banReason,
697         expires: futureDaysToUnixTime(banExpireDays),
698         auth,
699       };
700       WebSocketService.Instance.send(wsClient.banPerson(form));
701
702       i.setState({ showBanDialog: false });
703     }
704   }
705
706   parseMessage(msg: any) {
707     const op = wsUserOp(msg);
708     console.log(msg);
709
710     if (msg.error) {
711       toast(i18n.t(msg.error), "danger");
712
713       if (msg.error === "couldnt_find_that_username_or_email") {
714         this.context.router.history.push("/");
715       }
716     } else if (msg.reconnect) {
717       this.fetchUserData();
718     } else {
719       switch (op) {
720         case UserOperation.GetPersonDetails: {
721           // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
722           // and set the parent state if it is not set or differs
723           // TODO this might need to get abstracted
724           const data = wsJsonToRes<GetPersonDetailsResponse>(msg);
725           this.setState({ personRes: data, loading: false });
726           this.setPersonBlock();
727           restoreScrollPosition(this.context);
728
729           break;
730         }
731
732         case UserOperation.AddAdmin: {
733           const { admins } = wsJsonToRes<AddAdminResponse>(msg);
734           this.setState(s => ((s.siteRes.admins = admins), s));
735
736           break;
737         }
738
739         case UserOperation.CreateCommentLike: {
740           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
741           createCommentLikeRes(comment_view, this.state.personRes?.comments);
742           this.setState(this.state);
743
744           break;
745         }
746
747         case UserOperation.EditComment:
748         case UserOperation.DeleteComment:
749         case UserOperation.RemoveComment: {
750           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
751           editCommentRes(comment_view, this.state.personRes?.comments);
752           this.setState(this.state);
753
754           break;
755         }
756
757         case UserOperation.CreateComment: {
758           const {
759             comment_view: {
760               creator: { id },
761             },
762           } = wsJsonToRes<CommentResponse>(msg);
763           const mui = UserService.Instance.myUserInfo;
764
765           if (id === mui?.local_user_view.person.id) {
766             toast(i18n.t("reply_sent"));
767           }
768
769           break;
770         }
771
772         case UserOperation.SaveComment: {
773           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
774           saveCommentRes(comment_view, this.state.personRes?.comments);
775           this.setState(this.state);
776
777           break;
778         }
779
780         case UserOperation.EditPost:
781         case UserOperation.DeletePost:
782         case UserOperation.RemovePost:
783         case UserOperation.LockPost:
784         case UserOperation.FeaturePost:
785         case UserOperation.SavePost: {
786           const { post_view } = wsJsonToRes<PostResponse>(msg);
787           editPostFindRes(post_view, this.state.personRes?.posts);
788           this.setState(this.state);
789
790           break;
791         }
792
793         case UserOperation.CreatePostLike: {
794           const { post_view } = wsJsonToRes<PostResponse>(msg);
795           createPostLikeFindRes(post_view, this.state.personRes?.posts);
796           this.setState(this.state);
797
798           break;
799         }
800
801         case UserOperation.BanPerson: {
802           const data = wsJsonToRes<BanPersonResponse>(msg);
803           const res = this.state.personRes;
804           res?.comments
805             .filter(c => c.creator.id === data.person_view.person.id)
806             .forEach(c => (c.creator.banned = data.banned));
807           res?.posts
808             .filter(c => c.creator.id === data.person_view.person.id)
809             .forEach(c => (c.creator.banned = data.banned));
810           const pv = res?.person_view;
811
812           if (pv?.person.id === data.person_view.person.id) {
813             pv.person.banned = data.banned;
814           }
815           this.setState(this.state);
816
817           break;
818         }
819
820         case UserOperation.BlockPerson: {
821           const data = wsJsonToRes<BlockPersonResponse>(msg);
822           updatePersonBlock(data);
823           this.setPersonBlock();
824
825           break;
826         }
827
828         case UserOperation.PurgePerson:
829         case UserOperation.PurgePost:
830         case UserOperation.PurgeComment:
831         case UserOperation.PurgeCommunity: {
832           const { success } = wsJsonToRes<PurgeItemResponse>(msg);
833
834           if (success) {
835             toast(i18n.t("purge_success"));
836             this.context.router.history.push(`/`);
837           }
838         }
839       }
840     }
841   }
842 }