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