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