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