]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/profile.tsx
Adding new site setup fields. (#840)
[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.admin && (
433                         <li className="list-inline-item badge badge-light">
434                           {i18n.t("admin")}
435                         </li>
436                       )}
437                       {pv.person.bot_account && (
438                         <li className="list-inline-item badge badge-light">
439                           {i18n.t("bot_account").toLowerCase()}
440                         </li>
441                       )}
442                     </ul>
443                   </div>
444                   {this.banDialog()}
445                   <div className="flex-grow-1 unselectable pointer mx-2"></div>
446                   {!this.amCurrentUser &&
447                     UserService.Instance.myUserInfo.isSome() && (
448                       <>
449                         <a
450                           className={`d-flex align-self-start btn btn-secondary mr-2 ${
451                             !pv.person.matrix_user_id && "invisible"
452                           }`}
453                           rel={relTags}
454                           href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
455                         >
456                           {i18n.t("send_secure_message")}
457                         </a>
458                         <Link
459                           className={
460                             "d-flex align-self-start btn btn-secondary mr-2"
461                           }
462                           to={`/create_private_message/recipient/${pv.person.id}`}
463                         >
464                           {i18n.t("send_message")}
465                         </Link>
466                         {this.state.personBlocked ? (
467                           <button
468                             className={
469                               "d-flex align-self-start btn btn-secondary mr-2"
470                             }
471                             onClick={linkEvent(
472                               pv.person.id,
473                               this.handleUnblockPerson
474                             )}
475                           >
476                             {i18n.t("unblock_user")}
477                           </button>
478                         ) : (
479                           <button
480                             className={
481                               "d-flex align-self-start btn btn-secondary mr-2"
482                             }
483                             onClick={linkEvent(
484                               pv.person.id,
485                               this.handleBlockPerson
486                             )}
487                           >
488                             {i18n.t("block_user")}
489                           </button>
490                         )}
491                       </>
492                     )}
493
494                   {canMod(
495                     None,
496                     Some(this.state.siteRes.admins),
497                     pv.person.id
498                   ) &&
499                     !isAdmin(Some(this.state.siteRes.admins), pv.person.id) &&
500                     !this.state.showBanDialog &&
501                     (!isBanned(pv.person) ? (
502                       <button
503                         className={
504                           "d-flex align-self-start btn btn-secondary mr-2"
505                         }
506                         onClick={linkEvent(this, this.handleModBanShow)}
507                         aria-label={i18n.t("ban")}
508                       >
509                         {capitalizeFirstLetter(i18n.t("ban"))}
510                       </button>
511                     ) : (
512                       <button
513                         className={
514                           "d-flex align-self-start btn btn-secondary mr-2"
515                         }
516                         onClick={linkEvent(this, this.handleModBanSubmit)}
517                         aria-label={i18n.t("unban")}
518                       >
519                         {capitalizeFirstLetter(i18n.t("unban"))}
520                       </button>
521                     ))}
522                 </div>
523                 {pv.person.bio.match({
524                   some: bio => (
525                     <div className="d-flex align-items-center mb-2">
526                       <div
527                         className="md-div"
528                         dangerouslySetInnerHTML={mdToHtml(bio)}
529                       />
530                     </div>
531                   ),
532                   none: <></>,
533                 })}
534                 <div>
535                   <ul className="list-inline mb-2">
536                     <li className="list-inline-item badge badge-light">
537                       {i18n.t("number_of_posts", {
538                         count: pv.counts.post_count,
539                         formattedCount: numToSI(pv.counts.post_count),
540                       })}
541                     </li>
542                     <li className="list-inline-item badge badge-light">
543                       {i18n.t("number_of_comments", {
544                         count: pv.counts.comment_count,
545                         formattedCount: numToSI(pv.counts.comment_count),
546                       })}
547                     </li>
548                   </ul>
549                 </div>
550                 <div className="text-muted">
551                   {i18n.t("joined")}{" "}
552                   <MomentTime
553                     published={pv.person.published}
554                     updated={None}
555                     showAgo
556                     ignoreUpdated
557                   />
558                 </div>
559                 <div className="d-flex align-items-center text-muted mb-2">
560                   <Icon icon="cake" />
561                   <span className="ml-2">
562                     {i18n.t("cake_day_title")}{" "}
563                     {moment
564                       .utc(pv.person.published)
565                       .local()
566                       .format("MMM DD, YYYY")}
567                   </span>
568                 </div>
569               </div>
570             </div>
571           </div>
572         ),
573         none: <></>,
574       });
575   }
576
577   banDialog() {
578     return this.state.personRes
579       .map(r => r.person_view)
580       .match({
581         some: pv => (
582           <>
583             {this.state.showBanDialog && (
584               <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
585                 <div className="form-group row col-12">
586                   <label
587                     className="col-form-label"
588                     htmlFor="profile-ban-reason"
589                   >
590                     {i18n.t("reason")}
591                   </label>
592                   <input
593                     type="text"
594                     id="profile-ban-reason"
595                     className="form-control mr-2"
596                     placeholder={i18n.t("reason")}
597                     value={toUndefined(this.state.banReason)}
598                     onInput={linkEvent(this, this.handleModBanReasonChange)}
599                   />
600                   <label className="col-form-label" htmlFor={`mod-ban-expires`}>
601                     {i18n.t("expires")}
602                   </label>
603                   <input
604                     type="number"
605                     id={`mod-ban-expires`}
606                     className="form-control mr-2"
607                     placeholder={i18n.t("number_of_days")}
608                     value={toUndefined(this.state.banExpireDays)}
609                     onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
610                   />
611                   <div className="form-group">
612                     <div className="form-check">
613                       <input
614                         className="form-check-input"
615                         id="mod-ban-remove-data"
616                         type="checkbox"
617                         checked={this.state.removeData}
618                         onChange={linkEvent(
619                           this,
620                           this.handleModRemoveDataChange
621                         )}
622                       />
623                       <label
624                         className="form-check-label"
625                         htmlFor="mod-ban-remove-data"
626                         title={i18n.t("remove_content_more")}
627                       >
628                         {i18n.t("remove_content")}
629                       </label>
630                     </div>
631                   </div>
632                 </div>
633                 {/* TODO hold off on expires until later */}
634                 {/* <div class="form-group row"> */}
635                 {/*   <label class="col-form-label">Expires</label> */}
636                 {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
637                 {/* </div> */}
638                 <div className="form-group row">
639                   <button
640                     type="reset"
641                     className="btn btn-secondary mr-2"
642                     aria-label={i18n.t("cancel")}
643                     onClick={linkEvent(this, this.handleModBanSubmitCancel)}
644                   >
645                     {i18n.t("cancel")}
646                   </button>
647                   <button
648                     type="submit"
649                     className="btn btn-secondary"
650                     aria-label={i18n.t("ban")}
651                   >
652                     {i18n.t("ban")} {pv.person.name}
653                   </button>
654                 </div>
655               </form>
656             )}
657           </>
658         ),
659         none: <></>,
660       });
661   }
662
663   moderates() {
664     return this.state.personRes
665       .map(r => r.moderates)
666       .match({
667         some: moderates => {
668           if (moderates.length > 0) {
669             return (
670               <div className="card border-secondary mb-3">
671                 <div className="card-body">
672                   <h5>{i18n.t("moderates")}</h5>
673                   <ul className="list-unstyled mb-0">
674                     {moderates.map(cmv => (
675                       <li key={cmv.community.id}>
676                         <CommunityLink community={cmv.community} />
677                       </li>
678                     ))}
679                   </ul>
680                 </div>
681               </div>
682             );
683           } else {
684             return <></>;
685           }
686         },
687         none: void 0,
688       });
689   }
690
691   follows() {
692     return UserService.Instance.myUserInfo
693       .map(m => m.follows)
694       .match({
695         some: follows => {
696           if (follows.length > 0) {
697             return (
698               <div className="card border-secondary mb-3">
699                 <div className="card-body">
700                   <h5>{i18n.t("subscribed")}</h5>
701                   <ul className="list-unstyled mb-0">
702                     {follows.map(cfv => (
703                       <li key={cfv.community.id}>
704                         <CommunityLink community={cfv.community} />
705                       </li>
706                     ))}
707                   </ul>
708                 </div>
709               </div>
710             );
711           } else {
712             return <></>;
713           }
714         },
715         none: void 0,
716       });
717   }
718
719   updateUrl(paramUpdates: UrlParams) {
720     const page = paramUpdates.page || this.state.page;
721     const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
722     const sortStr = paramUpdates.sort || this.state.sort;
723
724     let typeView = `/u/${this.state.userName}`;
725
726     this.props.history.push(
727       `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
728     );
729     this.setState({ loading: true });
730     this.fetchUserData();
731   }
732
733   handlePageChange(page: number) {
734     this.updateUrl({ page: page });
735   }
736
737   handleSortChange(val: SortType) {
738     this.updateUrl({ sort: val, page: 1 });
739   }
740
741   handleViewChange(i: Profile, event: any) {
742     i.updateUrl({
743       view: PersonDetailsView[Number(event.target.value)],
744       page: 1,
745     });
746   }
747
748   handleModBanShow(i: Profile) {
749     i.setState({ showBanDialog: true });
750   }
751
752   handleModBanReasonChange(i: Profile, event: any) {
753     i.setState({ banReason: event.target.value });
754   }
755
756   handleModBanExpireDaysChange(i: Profile, event: any) {
757     i.setState({ banExpireDays: event.target.value });
758   }
759
760   handleModRemoveDataChange(i: Profile, event: any) {
761     i.setState({ removeData: event.target.checked });
762   }
763
764   handleModBanSubmitCancel(i: Profile, event?: any) {
765     event.preventDefault();
766     i.setState({ showBanDialog: false });
767   }
768
769   handleModBanSubmit(i: Profile, event?: any) {
770     if (event) event.preventDefault();
771
772     i.state.personRes
773       .map(r => r.person_view.person)
774       .match({
775         some: person => {
776           // If its an unban, restore all their data
777           let ban = !person.banned;
778           if (ban == false) {
779             i.setState({ removeData: false });
780           }
781           let form = new BanPerson({
782             person_id: person.id,
783             ban,
784             remove_data: Some(i.state.removeData),
785             reason: i.state.banReason,
786             expires: i.state.banExpireDays.map(futureDaysToUnixTime),
787             auth: auth().unwrap(),
788           });
789           WebSocketService.Instance.send(wsClient.banPerson(form));
790
791           i.setState({ showBanDialog: false });
792         },
793         none: void 0,
794       });
795   }
796
797   parseMessage(msg: any) {
798     let op = wsUserOp(msg);
799     console.log(msg);
800     if (msg.error) {
801       toast(i18n.t(msg.error), "danger");
802       if (msg.error == "couldnt_find_that_username_or_email") {
803         this.context.router.history.push("/");
804       }
805       return;
806     } else if (msg.reconnect) {
807       this.fetchUserData();
808     } else if (op == UserOperation.GetPersonDetails) {
809       // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
810       // and set the parent state if it is not set or differs
811       // TODO this might need to get abstracted
812       let data = wsJsonToRes<GetPersonDetailsResponse>(
813         msg,
814         GetPersonDetailsResponse
815       );
816       this.setState({ personRes: Some(data), loading: false });
817       this.setPersonBlock();
818       restoreScrollPosition(this.context);
819     } else if (op == UserOperation.AddAdmin) {
820       let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
821       this.setState(s => ((s.siteRes.admins = data.admins), s));
822     } else if (op == UserOperation.CreateCommentLike) {
823       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
824       createCommentLikeRes(
825         data.comment_view,
826         this.state.personRes.map(r => r.comments).unwrapOr([])
827       );
828       this.setState(this.state);
829     } else if (
830       op == UserOperation.EditComment ||
831       op == UserOperation.DeleteComment ||
832       op == UserOperation.RemoveComment
833     ) {
834       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
835       editCommentRes(
836         data.comment_view,
837         this.state.personRes.map(r => r.comments).unwrapOr([])
838       );
839       this.setState(this.state);
840     } else if (op == UserOperation.CreateComment) {
841       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
842       UserService.Instance.myUserInfo.match({
843         some: mui => {
844           if (data.comment_view.creator.id == mui.local_user_view.person.id) {
845             toast(i18n.t("reply_sent"));
846           }
847         },
848         none: void 0,
849       });
850     } else if (op == UserOperation.SaveComment) {
851       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
852       saveCommentRes(
853         data.comment_view,
854         this.state.personRes.map(r => r.comments).unwrapOr([])
855       );
856       this.setState(this.state);
857     } else if (
858       op == UserOperation.EditPost ||
859       op == UserOperation.DeletePost ||
860       op == UserOperation.RemovePost ||
861       op == UserOperation.LockPost ||
862       op == UserOperation.StickyPost ||
863       op == UserOperation.SavePost
864     ) {
865       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
866       editPostFindRes(
867         data.post_view,
868         this.state.personRes.map(r => r.posts).unwrapOr([])
869       );
870       this.setState(this.state);
871     } else if (op == UserOperation.CreatePostLike) {
872       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
873       createPostLikeFindRes(
874         data.post_view,
875         this.state.personRes.map(r => r.posts).unwrapOr([])
876       );
877       this.setState(this.state);
878     } else if (op == UserOperation.BanPerson) {
879       let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
880       this.state.personRes.match({
881         some: res => {
882           res.comments
883             .filter(c => c.creator.id == data.person_view.person.id)
884             .forEach(c => (c.creator.banned = data.banned));
885           res.posts
886             .filter(c => c.creator.id == data.person_view.person.id)
887             .forEach(c => (c.creator.banned = data.banned));
888           let pv = res.person_view;
889
890           if (pv.person.id == data.person_view.person.id) {
891             pv.person.banned = data.banned;
892           }
893           this.setState(this.state);
894         },
895         none: void 0,
896       });
897     } else if (op == UserOperation.BlockPerson) {
898       let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
899       updatePersonBlock(data);
900       this.setPersonBlock();
901       this.setState(this.state);
902     } else if (
903       op == UserOperation.PurgePerson ||
904       op == UserOperation.PurgePost ||
905       op == UserOperation.PurgeComment ||
906       op == UserOperation.PurgeCommunity
907     ) {
908       let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
909       if (data.success) {
910         toast(i18n.t("purge_success"));
911         this.context.router.history.push(`/`);
912       }
913     }
914   }
915 }