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