]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/profile.tsx
Handle when logged out (#986)
[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               {!UserService.Instance.myUserInfo && (
534                 <div className="alert alert-info" role="alert">
535                   {i18n.t("profile_not_logged_in_alert")}
536                 </div>
537               )}
538             </div>
539           </div>
540         </div>
541       )
542     );
543   }
544
545   get banDialog() {
546     const pv = this.state.personRes?.person_view;
547     const { showBanDialog } = this.state;
548
549     return (
550       pv && (
551         <>
552           {showBanDialog && (
553             <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
554               <div className="form-group row col-12">
555                 <label className="col-form-label" htmlFor="profile-ban-reason">
556                   {i18n.t("reason")}
557                 </label>
558                 <input
559                   type="text"
560                   id="profile-ban-reason"
561                   className="form-control mr-2"
562                   placeholder={i18n.t("reason")}
563                   value={this.state.banReason}
564                   onInput={linkEvent(this, this.handleModBanReasonChange)}
565                 />
566                 <label className="col-form-label" htmlFor={`mod-ban-expires`}>
567                   {i18n.t("expires")}
568                 </label>
569                 <input
570                   type="number"
571                   id={`mod-ban-expires`}
572                   className="form-control mr-2"
573                   placeholder={i18n.t("number_of_days")}
574                   value={this.state.banExpireDays}
575                   onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
576                 />
577                 <div className="form-group">
578                   <div className="form-check">
579                     <input
580                       className="form-check-input"
581                       id="mod-ban-remove-data"
582                       type="checkbox"
583                       checked={this.state.removeData}
584                       onChange={linkEvent(this, this.handleModRemoveDataChange)}
585                     />
586                     <label
587                       className="form-check-label"
588                       htmlFor="mod-ban-remove-data"
589                       title={i18n.t("remove_content_more")}
590                     >
591                       {i18n.t("remove_content")}
592                     </label>
593                   </div>
594                 </div>
595               </div>
596               {/* TODO hold off on expires until later */}
597               {/* <div class="form-group row"> */}
598               {/*   <label class="col-form-label">Expires</label> */}
599               {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
600               {/* </div> */}
601               <div className="form-group row">
602                 <button
603                   type="reset"
604                   className="btn btn-secondary mr-2"
605                   aria-label={i18n.t("cancel")}
606                   onClick={linkEvent(this, this.handleModBanSubmitCancel)}
607                 >
608                   {i18n.t("cancel")}
609                 </button>
610                 <button
611                   type="submit"
612                   className="btn btn-secondary"
613                   aria-label={i18n.t("ban")}
614                 >
615                   {i18n.t("ban")} {pv.person.name}
616                 </button>
617               </div>
618             </form>
619           )}
620         </>
621       )
622     );
623   }
624
625   updateUrl({ page, sort, view }: Partial<ProfileProps>) {
626     const {
627       page: urlPage,
628       sort: urlSort,
629       view: urlView,
630     } = getProfileQueryParams();
631
632     const queryParams: QueryParams<ProfileProps> = {
633       page: (page ?? urlPage).toString(),
634       sort: sort ?? urlSort,
635       view: view ?? urlView,
636     };
637
638     const { username } = this.props.match.params;
639
640     this.props.history.push(`/u/${username}${getQueryString(queryParams)}`);
641
642     this.setState({ loading: true });
643     this.fetchUserData();
644   }
645
646   handlePageChange(page: number) {
647     this.updateUrl({ page });
648   }
649
650   handleSortChange(sort: SortType) {
651     this.updateUrl({ sort, page: 1 });
652   }
653
654   handleViewChange(i: Profile, event: any) {
655     i.updateUrl({
656       view: PersonDetailsView[event.target.value],
657       page: 1,
658     });
659   }
660
661   handleModBanShow(i: Profile) {
662     i.setState({ showBanDialog: true });
663   }
664
665   handleModBanReasonChange(i: Profile, event: any) {
666     i.setState({ banReason: event.target.value });
667   }
668
669   handleModBanExpireDaysChange(i: Profile, event: any) {
670     i.setState({ banExpireDays: event.target.value });
671   }
672
673   handleModRemoveDataChange(i: Profile, event: any) {
674     i.setState({ removeData: event.target.checked });
675   }
676
677   handleModBanSubmitCancel(i: Profile, event?: any) {
678     event.preventDefault();
679     i.setState({ showBanDialog: false });
680   }
681
682   handleModBanSubmit(i: Profile, event?: any) {
683     if (event) event.preventDefault();
684     const { personRes, removeData, banReason, banExpireDays } = i.state;
685
686     const person = personRes?.person_view.person;
687     const auth = myAuth();
688
689     if (person && auth) {
690       const ban = !person.banned;
691
692       // If its an unban, restore all their data
693       if (!ban) {
694         i.setState({ removeData: false });
695       }
696
697       const form: BanPerson = {
698         person_id: person.id,
699         ban,
700         remove_data: removeData,
701         reason: banReason,
702         expires: futureDaysToUnixTime(banExpireDays),
703         auth,
704       };
705       WebSocketService.Instance.send(wsClient.banPerson(form));
706
707       i.setState({ showBanDialog: false });
708     }
709   }
710
711   parseMessage(msg: any) {
712     const op = wsUserOp(msg);
713     console.log(msg);
714
715     if (msg.error) {
716       toast(i18n.t(msg.error), "danger");
717
718       if (msg.error === "couldnt_find_that_username_or_email") {
719         this.context.router.history.push("/");
720       }
721     } else if (msg.reconnect) {
722       this.fetchUserData();
723     } else {
724       switch (op) {
725         case UserOperation.GetPersonDetails: {
726           // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
727           // and set the parent state if it is not set or differs
728           // TODO this might need to get abstracted
729           const data = wsJsonToRes<GetPersonDetailsResponse>(msg);
730           this.setState({ personRes: data, loading: false });
731           this.setPersonBlock();
732           restoreScrollPosition(this.context);
733
734           break;
735         }
736
737         case UserOperation.AddAdmin: {
738           const { admins } = wsJsonToRes<AddAdminResponse>(msg);
739           this.setState(s => ((s.siteRes.admins = admins), s));
740
741           break;
742         }
743
744         case UserOperation.CreateCommentLike: {
745           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
746           createCommentLikeRes(comment_view, this.state.personRes?.comments);
747           this.setState(this.state);
748
749           break;
750         }
751
752         case UserOperation.EditComment:
753         case UserOperation.DeleteComment:
754         case UserOperation.RemoveComment: {
755           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
756           editCommentRes(comment_view, this.state.personRes?.comments);
757           this.setState(this.state);
758
759           break;
760         }
761
762         case UserOperation.CreateComment: {
763           const {
764             comment_view: {
765               creator: { id },
766             },
767           } = wsJsonToRes<CommentResponse>(msg);
768           const mui = UserService.Instance.myUserInfo;
769
770           if (id === mui?.local_user_view.person.id) {
771             toast(i18n.t("reply_sent"));
772           }
773
774           break;
775         }
776
777         case UserOperation.SaveComment: {
778           const { comment_view } = wsJsonToRes<CommentResponse>(msg);
779           saveCommentRes(comment_view, this.state.personRes?.comments);
780           this.setState(this.state);
781
782           break;
783         }
784
785         case UserOperation.EditPost:
786         case UserOperation.DeletePost:
787         case UserOperation.RemovePost:
788         case UserOperation.LockPost:
789         case UserOperation.FeaturePost:
790         case UserOperation.SavePost: {
791           const { post_view } = wsJsonToRes<PostResponse>(msg);
792           editPostFindRes(post_view, this.state.personRes?.posts);
793           this.setState(this.state);
794
795           break;
796         }
797
798         case UserOperation.CreatePostLike: {
799           const { post_view } = wsJsonToRes<PostResponse>(msg);
800           createPostLikeFindRes(post_view, this.state.personRes?.posts);
801           this.setState(this.state);
802
803           break;
804         }
805
806         case UserOperation.BanPerson: {
807           const data = wsJsonToRes<BanPersonResponse>(msg);
808           const res = this.state.personRes;
809           res?.comments
810             .filter(c => c.creator.id === data.person_view.person.id)
811             .forEach(c => (c.creator.banned = data.banned));
812           res?.posts
813             .filter(c => c.creator.id === data.person_view.person.id)
814             .forEach(c => (c.creator.banned = data.banned));
815           const pv = res?.person_view;
816
817           if (pv?.person.id === data.person_view.person.id) {
818             pv.person.banned = data.banned;
819           }
820           this.setState(this.state);
821
822           break;
823         }
824
825         case UserOperation.BlockPerson: {
826           const data = wsJsonToRes<BlockPersonResponse>(msg);
827           updatePersonBlock(data);
828           this.setPersonBlock();
829
830           break;
831         }
832
833         case UserOperation.PurgePerson:
834         case UserOperation.PurgePost:
835         case UserOperation.PurgeComment:
836         case UserOperation.PurgeCommunity: {
837           const { success } = wsJsonToRes<PurgeItemResponse>(msg);
838
839           if (success) {
840             toast(i18n.t("purge_success"));
841             this.context.router.history.push(`/`);
842           }
843         }
844       }
845     }
846   }
847 }