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