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