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