]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/profile.tsx
Upgrade inferno v8.0.0 try2 (#790)
[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     this.setPersonBlock();
134   }
135
136   fetchUserData() {
137     let form = new GetPersonDetails({
138       username: Some(this.state.userName),
139       person_id: None,
140       community_id: None,
141       sort: Some(this.state.sort),
142       saved_only: Some(this.state.view === PersonDetailsView.Saved),
143       page: Some(this.state.page),
144       limit: Some(fetchLimit),
145       auth: auth(false).ok(),
146     });
147     WebSocketService.Instance.send(wsClient.getPersonDetails(form));
148   }
149
150   get amCurrentUser() {
151     return UserService.Instance.myUserInfo.match({
152       some: mui =>
153         this.state.personRes.match({
154           some: res =>
155             mui.local_user_view.person.id == res.person_view.person.id,
156           none: false,
157         }),
158       none: false,
159     });
160   }
161
162   setPersonBlock() {
163     UserService.Instance.myUserInfo.match({
164       some: mui =>
165         this.state.personRes.match({
166           some: res =>
167             this.setState({
168               personBlocked: mui.person_blocks
169                 .map(a => a.target.id)
170                 .includes(res.person_view.person.id),
171             }),
172           none: void 0,
173         }),
174       none: void 0,
175     });
176   }
177
178   static getViewFromProps(view: string): PersonDetailsView {
179     return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
180   }
181
182   static getSortTypeFromProps(sort: string): SortType {
183     return sort ? routeSortTypeToEnum(sort) : SortType.New;
184   }
185
186   static getPageFromProps(page: number): number {
187     return page ? Number(page) : 1;
188   }
189
190   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
191     let pathSplit = req.path.split("/");
192
193     let username = pathSplit[2];
194     let view = this.getViewFromProps(pathSplit[4]);
195     let sort = Some(this.getSortTypeFromProps(pathSplit[6]));
196     let page = Some(this.getPageFromProps(Number(pathSplit[8])));
197
198     let form = new GetPersonDetails({
199       username: Some(username),
200       person_id: None,
201       community_id: None,
202       sort,
203       saved_only: Some(view === PersonDetailsView.Saved),
204       page,
205       limit: Some(fetchLimit),
206       auth: req.auth,
207     });
208     return [req.client.getPersonDetails(form)];
209   }
210
211   componentDidMount() {
212     setupTippy();
213   }
214
215   componentWillUnmount() {
216     this.subscription.unsubscribe();
217     saveScrollPosition(this.context);
218   }
219
220   static getDerivedStateFromProps(props: any): ProfileProps {
221     return {
222       view: this.getViewFromProps(props.match.params.view),
223       sort: this.getSortTypeFromProps(props.match.params.sort),
224       page: this.getPageFromProps(props.match.params.page),
225       person_id: Number(props.match.params.id) || null,
226       username: props.match.params.username,
227     };
228   }
229
230   componentDidUpdate(lastProps: any) {
231     // Necessary if you are on a post and you click another post (same route)
232     if (
233       lastProps.location.pathname.split("/")[2] !==
234       lastProps.history.location.pathname.split("/")[2]
235     ) {
236       // Couldnt get a refresh working. This does for now.
237       location.reload();
238     }
239   }
240
241   get documentTitle(): string {
242     return this.state.siteRes.site_view.match({
243       some: siteView =>
244         this.state.personRes.match({
245           some: res =>
246             `@${res.person_view.person.name} - ${siteView.site.name}`,
247           none: "",
248         }),
249       none: "",
250     });
251   }
252
253   render() {
254     return (
255       <div className="container">
256         {this.state.loading ? (
257           <h5>
258             <Spinner large />
259           </h5>
260         ) : (
261           this.state.personRes.match({
262             some: res => (
263               <div className="row">
264                 <div className="col-12 col-md-8">
265                   <>
266                     <HtmlTags
267                       title={this.documentTitle}
268                       path={this.context.router.route.match.url}
269                       description={res.person_view.person.bio}
270                       image={res.person_view.person.avatar}
271                     />
272                     {this.userInfo()}
273                     <hr />
274                   </>
275                   {!this.state.loading && this.selects()}
276                   <PersonDetails
277                     personRes={res}
278                     admins={this.state.siteRes.admins}
279                     sort={this.state.sort}
280                     page={this.state.page}
281                     limit={fetchLimit}
282                     enableDownvotes={enableDownvotes(this.state.siteRes)}
283                     enableNsfw={enableNsfw(this.state.siteRes)}
284                     view={this.state.view}
285                     onPageChange={this.handlePageChange}
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 && (
416                       <h5 className="mb-0">{pv.person.display_name}</h5>
417                     )}
418                     <ul className="list-inline mb-2">
419                       <li className="list-inline-item">
420                         <PersonListing
421                           person={pv.person}
422                           realLink
423                           useApubName
424                           muted
425                           hideAvatar
426                         />
427                       </li>
428                       {isBanned(pv.person) && (
429                         <li className="list-inline-item badge badge-danger">
430                           {i18n.t("banned")}
431                         </li>
432                       )}
433                       {pv.person.admin && (
434                         <li className="list-inline-item badge badge-light">
435                           {i18n.t("admin")}
436                         </li>
437                       )}
438                       {pv.person.bot_account && (
439                         <li className="list-inline-item badge badge-light">
440                           {i18n.t("bot_account").toLowerCase()}
441                         </li>
442                       )}
443                     </ul>
444                   </div>
445                   {this.banDialog()}
446                   <div className="flex-grow-1 unselectable pointer mx-2"></div>
447                   {!this.amCurrentUser &&
448                     UserService.Instance.myUserInfo.isSome() && (
449                       <>
450                         <a
451                           className={`d-flex align-self-start btn btn-secondary mr-2 ${
452                             !pv.person.matrix_user_id && "invisible"
453                           }`}
454                           rel={relTags}
455                           href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
456                         >
457                           {i18n.t("send_secure_message")}
458                         </a>
459                         <Link
460                           className={
461                             "d-flex align-self-start btn btn-secondary mr-2"
462                           }
463                           to={`/create_private_message/recipient/${pv.person.id}`}
464                         >
465                           {i18n.t("send_message")}
466                         </Link>
467                         {this.state.personBlocked ? (
468                           <button
469                             className={
470                               "d-flex align-self-start btn btn-secondary mr-2"
471                             }
472                             onClick={linkEvent(
473                               pv.person.id,
474                               this.handleUnblockPerson
475                             )}
476                           >
477                             {i18n.t("unblock_user")}
478                           </button>
479                         ) : (
480                           <button
481                             className={
482                               "d-flex align-self-start btn btn-secondary mr-2"
483                             }
484                             onClick={linkEvent(
485                               pv.person.id,
486                               this.handleBlockPerson
487                             )}
488                           >
489                             {i18n.t("block_user")}
490                           </button>
491                         )}
492                       </>
493                     )}
494
495                   {canMod(
496                     None,
497                     Some(this.state.siteRes.admins),
498                     pv.person.id
499                   ) &&
500                     !isAdmin(Some(this.state.siteRes.admins), pv.person.id) &&
501                     !this.state.showBanDialog &&
502                     (!isBanned(pv.person) ? (
503                       <button
504                         className={
505                           "d-flex align-self-start btn btn-secondary mr-2"
506                         }
507                         onClick={linkEvent(this, this.handleModBanShow)}
508                         aria-label={i18n.t("ban")}
509                       >
510                         {capitalizeFirstLetter(i18n.t("ban"))}
511                       </button>
512                     ) : (
513                       <button
514                         className={
515                           "d-flex align-self-start btn btn-secondary mr-2"
516                         }
517                         onClick={linkEvent(this, this.handleModBanSubmit)}
518                         aria-label={i18n.t("unban")}
519                       >
520                         {capitalizeFirstLetter(i18n.t("unban"))}
521                       </button>
522                     ))}
523                 </div>
524                 {pv.person.bio.match({
525                   some: bio => (
526                     <div className="d-flex align-items-center mb-2">
527                       <div
528                         className="md-div"
529                         dangerouslySetInnerHTML={mdToHtml(bio)}
530                       />
531                     </div>
532                   ),
533                   none: <></>,
534                 })}
535                 <div>
536                   <ul className="list-inline mb-2">
537                     <li className="list-inline-item badge badge-light">
538                       {i18n.t("number_of_posts", {
539                         count: pv.counts.post_count,
540                         formattedCount: numToSI(pv.counts.post_count),
541                       })}
542                     </li>
543                     <li className="list-inline-item badge badge-light">
544                       {i18n.t("number_of_comments", {
545                         count: pv.counts.comment_count,
546                         formattedCount: numToSI(pv.counts.comment_count),
547                       })}
548                     </li>
549                   </ul>
550                 </div>
551                 <div className="text-muted">
552                   {i18n.t("joined")}{" "}
553                   <MomentTime
554                     published={pv.person.published}
555                     updated={None}
556                     showAgo
557                     ignoreUpdated
558                   />
559                 </div>
560                 <div className="d-flex align-items-center text-muted mb-2">
561                   <Icon icon="cake" />
562                   <span className="ml-2">
563                     {i18n.t("cake_day_title")}{" "}
564                     {moment
565                       .utc(pv.person.published)
566                       .local()
567                       .format("MMM DD, YYYY")}
568                   </span>
569                 </div>
570               </div>
571             </div>
572           </div>
573         ),
574         none: <></>,
575       });
576   }
577
578   banDialog() {
579     return this.state.personRes
580       .map(r => r.person_view)
581       .match({
582         some: pv => (
583           <>
584             {this.state.showBanDialog && (
585               <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
586                 <div className="form-group row col-12">
587                   <label
588                     className="col-form-label"
589                     htmlFor="profile-ban-reason"
590                   >
591                     {i18n.t("reason")}
592                   </label>
593                   <input
594                     type="text"
595                     id="profile-ban-reason"
596                     className="form-control mr-2"
597                     placeholder={i18n.t("reason")}
598                     value={toUndefined(this.state.banReason)}
599                     onInput={linkEvent(this, this.handleModBanReasonChange)}
600                   />
601                   <label className="col-form-label" htmlFor={`mod-ban-expires`}>
602                     {i18n.t("expires")}
603                   </label>
604                   <input
605                     type="number"
606                     id={`mod-ban-expires`}
607                     className="form-control mr-2"
608                     placeholder={i18n.t("number_of_days")}
609                     value={toUndefined(this.state.banExpireDays)}
610                     onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
611                   />
612                   <div className="form-group">
613                     <div className="form-check">
614                       <input
615                         className="form-check-input"
616                         id="mod-ban-remove-data"
617                         type="checkbox"
618                         checked={this.state.removeData}
619                         onChange={linkEvent(
620                           this,
621                           this.handleModRemoveDataChange
622                         )}
623                       />
624                       <label
625                         className="form-check-label"
626                         htmlFor="mod-ban-remove-data"
627                         title={i18n.t("remove_content_more")}
628                       >
629                         {i18n.t("remove_content")}
630                       </label>
631                     </div>
632                   </div>
633                 </div>
634                 {/* TODO hold off on expires until later */}
635                 {/* <div class="form-group row"> */}
636                 {/*   <label class="col-form-label">Expires</label> */}
637                 {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
638                 {/* </div> */}
639                 <div className="form-group row">
640                   <button
641                     type="reset"
642                     className="btn btn-secondary mr-2"
643                     aria-label={i18n.t("cancel")}
644                     onClick={linkEvent(this, this.handleModBanSubmitCancel)}
645                   >
646                     {i18n.t("cancel")}
647                   </button>
648                   <button
649                     type="submit"
650                     className="btn btn-secondary"
651                     aria-label={i18n.t("ban")}
652                   >
653                     {i18n.t("ban")} {pv.person.name}
654                   </button>
655                 </div>
656               </form>
657             )}
658           </>
659         ),
660         none: <></>,
661       });
662   }
663
664   // TODO test this, make sure its good
665   moderates() {
666     return this.state.personRes
667       .map(r => r.moderates)
668       .match({
669         some: moderates => {
670           if (moderates.length > 0) {
671             <div className="card border-secondary mb-3">
672               <div className="card-body">
673                 <h5>{i18n.t("moderates")}</h5>
674                 <ul className="list-unstyled mb-0">
675                   {moderates.map(cmv => (
676                     <li key={cmv.community.id}>
677                       <CommunityLink community={cmv.community} />
678                     </li>
679                   ))}
680                 </ul>
681               </div>
682             </div>;
683           }
684         },
685         none: void 0,
686       });
687   }
688
689   follows() {
690     return UserService.Instance.myUserInfo
691       .map(m => m.follows)
692       .match({
693         some: follows => {
694           if (follows.length > 0) {
695             <div className="card border-secondary mb-3">
696               <div className="card-body">
697                 <h5>{i18n.t("subscribed")}</h5>
698                 <ul className="list-unstyled mb-0">
699                   {follows.map(cfv => (
700                     <li key={cfv.community.id}>
701                       <CommunityLink community={cfv.community} />
702                     </li>
703                   ))}
704                 </ul>
705               </div>
706             </div>;
707           }
708         },
709         none: void 0,
710       });
711   }
712
713   updateUrl(paramUpdates: UrlParams) {
714     const page = paramUpdates.page || this.state.page;
715     const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
716     const sortStr = paramUpdates.sort || this.state.sort;
717
718     let typeView = `/u/${this.state.userName}`;
719
720     this.props.history.push(
721       `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
722     );
723     this.setState({ loading: true });
724     this.fetchUserData();
725   }
726
727   handlePageChange(page: number) {
728     this.updateUrl({ page: page });
729   }
730
731   handleSortChange(val: SortType) {
732     this.updateUrl({ sort: val, page: 1 });
733   }
734
735   handleViewChange(i: Profile, event: any) {
736     i.updateUrl({
737       view: PersonDetailsView[Number(event.target.value)],
738       page: 1,
739     });
740   }
741
742   handleModBanShow(i: Profile) {
743     i.setState({ showBanDialog: true });
744   }
745
746   handleModBanReasonChange(i: Profile, event: any) {
747     i.setState({ banReason: event.target.value });
748   }
749
750   handleModBanExpireDaysChange(i: Profile, event: any) {
751     i.setState({ banExpireDays: event.target.value });
752   }
753
754   handleModRemoveDataChange(i: Profile, event: any) {
755     i.setState({ removeData: event.target.checked });
756   }
757
758   handleModBanSubmitCancel(i: Profile, event?: any) {
759     event.preventDefault();
760     i.setState({ showBanDialog: false });
761   }
762
763   handleModBanSubmit(i: Profile, event?: any) {
764     if (event) event.preventDefault();
765
766     i.state.personRes
767       .map(r => r.person_view.person)
768       .match({
769         some: person => {
770           // If its an unban, restore all their data
771           let ban = !person.banned;
772           if (ban == false) {
773             i.setState({ removeData: false });
774           }
775           let form = new BanPerson({
776             person_id: person.id,
777             ban,
778             remove_data: Some(i.state.removeData),
779             reason: i.state.banReason,
780             expires: i.state.banExpireDays.map(futureDaysToUnixTime),
781             auth: auth().unwrap(),
782           });
783           WebSocketService.Instance.send(wsClient.banPerson(form));
784
785           i.setState({ showBanDialog: false });
786         },
787         none: void 0,
788       });
789   }
790
791   parseMessage(msg: any) {
792     let op = wsUserOp(msg);
793     console.log(msg);
794     if (msg.error) {
795       toast(i18n.t(msg.error), "danger");
796       if (msg.error == "couldnt_find_that_username_or_email") {
797         this.context.router.history.push("/");
798       }
799       return;
800     } else if (msg.reconnect) {
801       this.fetchUserData();
802     } else if (op == UserOperation.GetPersonDetails) {
803       // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
804       // and set the parent state if it is not set or differs
805       // TODO this might need to get abstracted
806       let data = wsJsonToRes<GetPersonDetailsResponse>(
807         msg,
808         GetPersonDetailsResponse
809       );
810       this.setState({ personRes: Some(data), loading: false });
811       this.setPersonBlock();
812       restoreScrollPosition(this.context);
813     } else if (op == UserOperation.AddAdmin) {
814       let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
815       this.setState(s => ((s.siteRes.admins = data.admins), s));
816     } else if (op == UserOperation.CreateCommentLike) {
817       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
818       createCommentLikeRes(
819         data.comment_view,
820         this.state.personRes.map(r => r.comments).unwrapOr([])
821       );
822       this.setState(this.state);
823     } else if (
824       op == UserOperation.EditComment ||
825       op == UserOperation.DeleteComment ||
826       op == UserOperation.RemoveComment
827     ) {
828       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
829       editCommentRes(
830         data.comment_view,
831         this.state.personRes.map(r => r.comments).unwrapOr([])
832       );
833       this.setState(this.state);
834     } else if (op == UserOperation.CreateComment) {
835       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
836       UserService.Instance.myUserInfo.match({
837         some: mui => {
838           if (data.comment_view.creator.id == mui.local_user_view.person.id) {
839             toast(i18n.t("reply_sent"));
840           }
841         },
842         none: void 0,
843       });
844     } else if (op == UserOperation.SaveComment) {
845       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
846       saveCommentRes(
847         data.comment_view,
848         this.state.personRes.map(r => r.comments).unwrapOr([])
849       );
850       this.setState(this.state);
851     } else if (
852       op == UserOperation.EditPost ||
853       op == UserOperation.DeletePost ||
854       op == UserOperation.RemovePost ||
855       op == UserOperation.LockPost ||
856       op == UserOperation.StickyPost ||
857       op == UserOperation.SavePost
858     ) {
859       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
860       editPostFindRes(
861         data.post_view,
862         this.state.personRes.map(r => r.posts).unwrapOr([])
863       );
864       this.setState(this.state);
865     } else if (op == UserOperation.CreatePostLike) {
866       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
867       createPostLikeFindRes(
868         data.post_view,
869         this.state.personRes.map(r => r.posts).unwrapOr([])
870       );
871       this.setState(this.state);
872     } else if (op == UserOperation.BanPerson) {
873       let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
874       this.state.personRes.match({
875         some: res => {
876           res.comments
877             .filter(c => c.creator.id == data.person_view.person.id)
878             .forEach(c => (c.creator.banned = data.banned));
879           res.posts
880             .filter(c => c.creator.id == data.person_view.person.id)
881             .forEach(c => (c.creator.banned = data.banned));
882           let pv = res.person_view;
883
884           if (pv.person.id == data.person_view.person.id) {
885             pv.person.banned = data.banned;
886           }
887           this.setState(this.state);
888         },
889         none: void 0,
890       });
891     } else if (op == UserOperation.BlockPerson) {
892       let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
893       updatePersonBlock(data);
894       this.setPersonBlock();
895       this.setState(this.state);
896     } else if (
897       op == UserOperation.PurgePerson ||
898       op == UserOperation.PurgePost ||
899       op == UserOperation.PurgeComment ||
900       op == UserOperation.PurgeCommunity
901     ) {
902       let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
903       if (data.success) {
904         toast(i18n.t("purge_success"));
905         this.context.router.history.push(`/`);
906       }
907     }
908   }
909 }