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