]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/profile.tsx
Show deleted on profile page. Fixes #834 (#859)
[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.personRes.match({
242       some: res =>
243         `@${res.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`,
244       none: "",
245     });
246   }
247
248   render() {
249     return (
250       <div className="container-lg">
251         {this.state.loading ? (
252           <h5>
253             <Spinner large />
254           </h5>
255         ) : (
256           this.state.personRes.match({
257             some: res => (
258               <div className="row">
259                 <div className="col-12 col-md-8">
260                   <>
261                     <HtmlTags
262                       title={this.documentTitle}
263                       path={this.context.router.route.match.url}
264                       description={res.person_view.person.bio}
265                       image={res.person_view.person.avatar}
266                     />
267                     {this.userInfo()}
268                     <hr />
269                   </>
270                   {!this.state.loading && this.selects()}
271                   <PersonDetails
272                     personRes={res}
273                     admins={this.state.siteRes.admins}
274                     sort={this.state.sort}
275                     page={this.state.page}
276                     limit={fetchLimit}
277                     enableDownvotes={enableDownvotes(this.state.siteRes)}
278                     enableNsfw={enableNsfw(this.state.siteRes)}
279                     view={this.state.view}
280                     onPageChange={this.handlePageChange}
281                     allLanguages={this.state.siteRes.all_languages}
282                   />
283                 </div>
284
285                 {!this.state.loading && (
286                   <div className="col-12 col-md-4">
287                     {this.moderates()}
288                     {this.amCurrentUser && this.follows()}
289                   </div>
290                 )}
291               </div>
292             ),
293             none: <></>,
294           })
295         )}
296       </div>
297     );
298   }
299
300   viewRadios() {
301     return (
302       <div className="btn-group btn-group-toggle flex-wrap mb-2">
303         <label
304           className={`btn btn-outline-secondary pointer
305             ${this.state.view == PersonDetailsView.Overview && "active"}
306           `}
307         >
308           <input
309             type="radio"
310             value={PersonDetailsView.Overview}
311             checked={this.state.view === PersonDetailsView.Overview}
312             onChange={linkEvent(this, this.handleViewChange)}
313           />
314           {i18n.t("overview")}
315         </label>
316         <label
317           className={`btn btn-outline-secondary pointer
318             ${this.state.view == PersonDetailsView.Comments && "active"}
319           `}
320         >
321           <input
322             type="radio"
323             value={PersonDetailsView.Comments}
324             checked={this.state.view == PersonDetailsView.Comments}
325             onChange={linkEvent(this, this.handleViewChange)}
326           />
327           {i18n.t("comments")}
328         </label>
329         <label
330           className={`btn btn-outline-secondary pointer
331             ${this.state.view == PersonDetailsView.Posts && "active"}
332           `}
333         >
334           <input
335             type="radio"
336             value={PersonDetailsView.Posts}
337             checked={this.state.view == PersonDetailsView.Posts}
338             onChange={linkEvent(this, this.handleViewChange)}
339           />
340           {i18n.t("posts")}
341         </label>
342         <label
343           className={`btn btn-outline-secondary pointer
344             ${this.state.view == PersonDetailsView.Saved && "active"}
345           `}
346         >
347           <input
348             type="radio"
349             value={PersonDetailsView.Saved}
350             checked={this.state.view == PersonDetailsView.Saved}
351             onChange={linkEvent(this, this.handleViewChange)}
352           />
353           {i18n.t("saved")}
354         </label>
355       </div>
356     );
357   }
358
359   selects() {
360     let profileRss = `/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`;
361
362     return (
363       <div className="mb-2">
364         <span className="mr-3">{this.viewRadios()}</span>
365         <SortSelect
366           sort={this.state.sort}
367           onChange={this.handleSortChange}
368           hideHot
369           hideMostComments
370         />
371         <a href={profileRss} rel={relTags} title="RSS">
372           <Icon icon="rss" classes="text-muted small mx-2" />
373         </a>
374         <link rel="alternate" type="application/atom+xml" href={profileRss} />
375       </div>
376     );
377   }
378   handleBlockPerson(personId: number) {
379     if (personId != 0) {
380       let blockUserForm = new BlockPerson({
381         person_id: personId,
382         block: true,
383         auth: auth().unwrap(),
384       });
385       WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
386     }
387   }
388   handleUnblockPerson(recipientId: number) {
389     let blockUserForm = new BlockPerson({
390       person_id: recipientId,
391       block: false,
392       auth: auth().unwrap(),
393     });
394     WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
395   }
396
397   userInfo() {
398     return this.state.personRes
399       .map(r => r.person_view)
400       .match({
401         some: pv => (
402           <div>
403             <BannerIconHeader
404               banner={pv.person.banner}
405               icon={pv.person.avatar}
406             />
407             <div className="mb-3">
408               <div className="">
409                 <div className="mb-0 d-flex flex-wrap">
410                   <div>
411                     {pv.person.display_name.match({
412                       some: displayName => (
413                         <h5 className="mb-0">{displayName}</h5>
414                       ),
415                       none: <></>,
416                     })}
417                     <ul className="list-inline mb-2">
418                       <li className="list-inline-item">
419                         <PersonListing
420                           person={pv.person}
421                           realLink
422                           useApubName
423                           muted
424                           hideAvatar
425                         />
426                       </li>
427                       {isBanned(pv.person) && (
428                         <li className="list-inline-item badge badge-danger">
429                           {i18n.t("banned")}
430                         </li>
431                       )}
432                       {pv.person.deleted && (
433                         <li className="list-inline-item badge badge-danger">
434                           {i18n.t("deleted")}
435                         </li>
436                       )}
437                       {pv.person.admin && (
438                         <li className="list-inline-item badge badge-light">
439                           {i18n.t("admin")}
440                         </li>
441                       )}
442                       {pv.person.bot_account && (
443                         <li className="list-inline-item badge badge-light">
444                           {i18n.t("bot_account").toLowerCase()}
445                         </li>
446                       )}
447                     </ul>
448                   </div>
449                   {this.banDialog()}
450                   <div className="flex-grow-1 unselectable pointer mx-2"></div>
451                   {!this.amCurrentUser &&
452                     UserService.Instance.myUserInfo.isSome() && (
453                       <>
454                         <a
455                           className={`d-flex align-self-start btn btn-secondary mr-2 ${
456                             !pv.person.matrix_user_id && "invisible"
457                           }`}
458                           rel={relTags}
459                           href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
460                         >
461                           {i18n.t("send_secure_message")}
462                         </a>
463                         <Link
464                           className={
465                             "d-flex align-self-start btn btn-secondary mr-2"
466                           }
467                           to={`/create_private_message/recipient/${pv.person.id}`}
468                         >
469                           {i18n.t("send_message")}
470                         </Link>
471                         {this.state.personBlocked ? (
472                           <button
473                             className={
474                               "d-flex align-self-start btn btn-secondary mr-2"
475                             }
476                             onClick={linkEvent(
477                               pv.person.id,
478                               this.handleUnblockPerson
479                             )}
480                           >
481                             {i18n.t("unblock_user")}
482                           </button>
483                         ) : (
484                           <button
485                             className={
486                               "d-flex align-self-start btn btn-secondary mr-2"
487                             }
488                             onClick={linkEvent(
489                               pv.person.id,
490                               this.handleBlockPerson
491                             )}
492                           >
493                             {i18n.t("block_user")}
494                           </button>
495                         )}
496                       </>
497                     )}
498
499                   {canMod(
500                     None,
501                     Some(this.state.siteRes.admins),
502                     pv.person.id
503                   ) &&
504                     !isAdmin(Some(this.state.siteRes.admins), pv.person.id) &&
505                     !this.state.showBanDialog &&
506                     (!isBanned(pv.person) ? (
507                       <button
508                         className={
509                           "d-flex align-self-start btn btn-secondary mr-2"
510                         }
511                         onClick={linkEvent(this, this.handleModBanShow)}
512                         aria-label={i18n.t("ban")}
513                       >
514                         {capitalizeFirstLetter(i18n.t("ban"))}
515                       </button>
516                     ) : (
517                       <button
518                         className={
519                           "d-flex align-self-start btn btn-secondary mr-2"
520                         }
521                         onClick={linkEvent(this, this.handleModBanSubmit)}
522                         aria-label={i18n.t("unban")}
523                       >
524                         {capitalizeFirstLetter(i18n.t("unban"))}
525                       </button>
526                     ))}
527                 </div>
528                 {pv.person.bio.match({
529                   some: bio => (
530                     <div className="d-flex align-items-center mb-2">
531                       <div
532                         className="md-div"
533                         dangerouslySetInnerHTML={mdToHtml(bio)}
534                       />
535                     </div>
536                   ),
537                   none: <></>,
538                 })}
539                 <div>
540                   <ul className="list-inline mb-2">
541                     <li className="list-inline-item badge badge-light">
542                       {i18n.t("number_of_posts", {
543                         count: pv.counts.post_count,
544                         formattedCount: numToSI(pv.counts.post_count),
545                       })}
546                     </li>
547                     <li className="list-inline-item badge badge-light">
548                       {i18n.t("number_of_comments", {
549                         count: pv.counts.comment_count,
550                         formattedCount: numToSI(pv.counts.comment_count),
551                       })}
552                     </li>
553                   </ul>
554                 </div>
555                 <div className="text-muted">
556                   {i18n.t("joined")}{" "}
557                   <MomentTime
558                     published={pv.person.published}
559                     updated={None}
560                     showAgo
561                     ignoreUpdated
562                   />
563                 </div>
564                 <div className="d-flex align-items-center text-muted mb-2">
565                   <Icon icon="cake" />
566                   <span className="ml-2">
567                     {i18n.t("cake_day_title")}{" "}
568                     {moment
569                       .utc(pv.person.published)
570                       .local()
571                       .format("MMM DD, YYYY")}
572                   </span>
573                 </div>
574               </div>
575             </div>
576           </div>
577         ),
578         none: <></>,
579       });
580   }
581
582   banDialog() {
583     return this.state.personRes
584       .map(r => r.person_view)
585       .match({
586         some: pv => (
587           <>
588             {this.state.showBanDialog && (
589               <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
590                 <div className="form-group row col-12">
591                   <label
592                     className="col-form-label"
593                     htmlFor="profile-ban-reason"
594                   >
595                     {i18n.t("reason")}
596                   </label>
597                   <input
598                     type="text"
599                     id="profile-ban-reason"
600                     className="form-control mr-2"
601                     placeholder={i18n.t("reason")}
602                     value={toUndefined(this.state.banReason)}
603                     onInput={linkEvent(this, this.handleModBanReasonChange)}
604                   />
605                   <label className="col-form-label" htmlFor={`mod-ban-expires`}>
606                     {i18n.t("expires")}
607                   </label>
608                   <input
609                     type="number"
610                     id={`mod-ban-expires`}
611                     className="form-control mr-2"
612                     placeholder={i18n.t("number_of_days")}
613                     value={toUndefined(this.state.banExpireDays)}
614                     onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
615                   />
616                   <div className="form-group">
617                     <div className="form-check">
618                       <input
619                         className="form-check-input"
620                         id="mod-ban-remove-data"
621                         type="checkbox"
622                         checked={this.state.removeData}
623                         onChange={linkEvent(
624                           this,
625                           this.handleModRemoveDataChange
626                         )}
627                       />
628                       <label
629                         className="form-check-label"
630                         htmlFor="mod-ban-remove-data"
631                         title={i18n.t("remove_content_more")}
632                       >
633                         {i18n.t("remove_content")}
634                       </label>
635                     </div>
636                   </div>
637                 </div>
638                 {/* TODO hold off on expires until later */}
639                 {/* <div class="form-group row"> */}
640                 {/*   <label class="col-form-label">Expires</label> */}
641                 {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
642                 {/* </div> */}
643                 <div className="form-group row">
644                   <button
645                     type="reset"
646                     className="btn btn-secondary mr-2"
647                     aria-label={i18n.t("cancel")}
648                     onClick={linkEvent(this, this.handleModBanSubmitCancel)}
649                   >
650                     {i18n.t("cancel")}
651                   </button>
652                   <button
653                     type="submit"
654                     className="btn btn-secondary"
655                     aria-label={i18n.t("ban")}
656                   >
657                     {i18n.t("ban")} {pv.person.name}
658                   </button>
659                 </div>
660               </form>
661             )}
662           </>
663         ),
664         none: <></>,
665       });
666   }
667
668   moderates() {
669     return this.state.personRes
670       .map(r => r.moderates)
671       .match({
672         some: moderates => {
673           if (moderates.length > 0) {
674             return (
675               <div className="card border-secondary mb-3">
676                 <div className="card-body">
677                   <h5>{i18n.t("moderates")}</h5>
678                   <ul className="list-unstyled mb-0">
679                     {moderates.map(cmv => (
680                       <li key={cmv.community.id}>
681                         <CommunityLink community={cmv.community} />
682                       </li>
683                     ))}
684                   </ul>
685                 </div>
686               </div>
687             );
688           } else {
689             return <></>;
690           }
691         },
692         none: void 0,
693       });
694   }
695
696   follows() {
697     return UserService.Instance.myUserInfo
698       .map(m => m.follows)
699       .match({
700         some: follows => {
701           if (follows.length > 0) {
702             return (
703               <div className="card border-secondary mb-3">
704                 <div className="card-body">
705                   <h5>{i18n.t("subscribed")}</h5>
706                   <ul className="list-unstyled mb-0">
707                     {follows.map(cfv => (
708                       <li key={cfv.community.id}>
709                         <CommunityLink community={cfv.community} />
710                       </li>
711                     ))}
712                   </ul>
713                 </div>
714               </div>
715             );
716           } else {
717             return <></>;
718           }
719         },
720         none: void 0,
721       });
722   }
723
724   updateUrl(paramUpdates: UrlParams) {
725     const page = paramUpdates.page || this.state.page;
726     const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
727     const sortStr = paramUpdates.sort || this.state.sort;
728
729     let typeView = `/u/${this.state.userName}`;
730
731     this.props.history.push(
732       `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
733     );
734     this.setState({ loading: true });
735     this.fetchUserData();
736   }
737
738   handlePageChange(page: number) {
739     this.updateUrl({ page: page });
740   }
741
742   handleSortChange(val: SortType) {
743     this.updateUrl({ sort: val, page: 1 });
744   }
745
746   handleViewChange(i: Profile, event: any) {
747     i.updateUrl({
748       view: PersonDetailsView[Number(event.target.value)],
749       page: 1,
750     });
751   }
752
753   handleModBanShow(i: Profile) {
754     i.setState({ showBanDialog: true });
755   }
756
757   handleModBanReasonChange(i: Profile, event: any) {
758     i.setState({ banReason: event.target.value });
759   }
760
761   handleModBanExpireDaysChange(i: Profile, event: any) {
762     i.setState({ banExpireDays: event.target.value });
763   }
764
765   handleModRemoveDataChange(i: Profile, event: any) {
766     i.setState({ removeData: event.target.checked });
767   }
768
769   handleModBanSubmitCancel(i: Profile, event?: any) {
770     event.preventDefault();
771     i.setState({ showBanDialog: false });
772   }
773
774   handleModBanSubmit(i: Profile, event?: any) {
775     if (event) event.preventDefault();
776
777     i.state.personRes
778       .map(r => r.person_view.person)
779       .match({
780         some: person => {
781           // If its an unban, restore all their data
782           let ban = !person.banned;
783           if (ban == false) {
784             i.setState({ removeData: false });
785           }
786           let form = new BanPerson({
787             person_id: person.id,
788             ban,
789             remove_data: Some(i.state.removeData),
790             reason: i.state.banReason,
791             expires: i.state.banExpireDays.map(futureDaysToUnixTime),
792             auth: auth().unwrap(),
793           });
794           WebSocketService.Instance.send(wsClient.banPerson(form));
795
796           i.setState({ showBanDialog: false });
797         },
798         none: void 0,
799       });
800   }
801
802   parseMessage(msg: any) {
803     let op = wsUserOp(msg);
804     console.log(msg);
805     if (msg.error) {
806       toast(i18n.t(msg.error), "danger");
807       if (msg.error == "couldnt_find_that_username_or_email") {
808         this.context.router.history.push("/");
809       }
810       return;
811     } else if (msg.reconnect) {
812       this.fetchUserData();
813     } else if (op == UserOperation.GetPersonDetails) {
814       // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
815       // and set the parent state if it is not set or differs
816       // TODO this might need to get abstracted
817       let data = wsJsonToRes<GetPersonDetailsResponse>(
818         msg,
819         GetPersonDetailsResponse
820       );
821       this.setState({ personRes: Some(data), loading: false });
822       this.setPersonBlock();
823       restoreScrollPosition(this.context);
824     } else if (op == UserOperation.AddAdmin) {
825       let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
826       this.setState(s => ((s.siteRes.admins = data.admins), s));
827     } else if (op == UserOperation.CreateCommentLike) {
828       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
829       createCommentLikeRes(
830         data.comment_view,
831         this.state.personRes.map(r => r.comments).unwrapOr([])
832       );
833       this.setState(this.state);
834     } else if (
835       op == UserOperation.EditComment ||
836       op == UserOperation.DeleteComment ||
837       op == UserOperation.RemoveComment
838     ) {
839       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
840       editCommentRes(
841         data.comment_view,
842         this.state.personRes.map(r => r.comments).unwrapOr([])
843       );
844       this.setState(this.state);
845     } else if (op == UserOperation.CreateComment) {
846       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
847       UserService.Instance.myUserInfo.match({
848         some: mui => {
849           if (data.comment_view.creator.id == mui.local_user_view.person.id) {
850             toast(i18n.t("reply_sent"));
851           }
852         },
853         none: void 0,
854       });
855     } else if (op == UserOperation.SaveComment) {
856       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
857       saveCommentRes(
858         data.comment_view,
859         this.state.personRes.map(r => r.comments).unwrapOr([])
860       );
861       this.setState(this.state);
862     } else if (
863       op == UserOperation.EditPost ||
864       op == UserOperation.DeletePost ||
865       op == UserOperation.RemovePost ||
866       op == UserOperation.LockPost ||
867       op == UserOperation.StickyPost ||
868       op == UserOperation.SavePost
869     ) {
870       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
871       editPostFindRes(
872         data.post_view,
873         this.state.personRes.map(r => r.posts).unwrapOr([])
874       );
875       this.setState(this.state);
876     } else if (op == UserOperation.CreatePostLike) {
877       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
878       createPostLikeFindRes(
879         data.post_view,
880         this.state.personRes.map(r => r.posts).unwrapOr([])
881       );
882       this.setState(this.state);
883     } else if (op == UserOperation.BanPerson) {
884       let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
885       this.state.personRes.match({
886         some: res => {
887           res.comments
888             .filter(c => c.creator.id == data.person_view.person.id)
889             .forEach(c => (c.creator.banned = data.banned));
890           res.posts
891             .filter(c => c.creator.id == data.person_view.person.id)
892             .forEach(c => (c.creator.banned = data.banned));
893           let pv = res.person_view;
894
895           if (pv.person.id == data.person_view.person.id) {
896             pv.person.banned = data.banned;
897           }
898           this.setState(this.state);
899         },
900         none: void 0,
901       });
902     } else if (op == UserOperation.BlockPerson) {
903       let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
904       updatePersonBlock(data);
905       this.setPersonBlock();
906       this.setState(this.state);
907     } else if (
908       op == UserOperation.PurgePerson ||
909       op == UserOperation.PurgePost ||
910       op == UserOperation.PurgeComment ||
911       op == UserOperation.PurgeCommunity
912     ) {
913       let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
914       if (data.success) {
915         toast(i18n.t("purge_success"));
916         this.context.router.history.push(`/`);
917       }
918     }
919   }
920 }