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