]> Untitled Git - lemmy-ui.git/blob - src/shared/components/post/post.tsx
Check to make sure post is correct. Fixes #934 (#949)
[lemmy-ui.git] / src / shared / components / post / post.tsx
1 import autosize from "autosize";
2 import { Component, createRef, linkEvent, RefObject } from "inferno";
3 import {
4   AddAdminResponse,
5   AddModToCommunityResponse,
6   BanFromCommunityResponse,
7   BanPersonResponse,
8   BlockPersonResponse,
9   CommentNode as CommentNodeI,
10   CommentReportResponse,
11   CommentResponse,
12   CommentSortType,
13   CommunityResponse,
14   GetComments,
15   GetCommentsResponse,
16   GetCommunityResponse,
17   GetPost,
18   GetPostResponse,
19   GetSiteResponse,
20   ListingType,
21   PostReportResponse,
22   PostResponse,
23   PostView,
24   PurgeItemResponse,
25   Search,
26   SearchResponse,
27   SearchType,
28   SortType,
29   UserOperation,
30   wsJsonToRes,
31   wsUserOp,
32 } from "lemmy-js-client";
33 import { Subscription } from "rxjs";
34 import { i18n } from "../../i18next";
35 import { CommentViewType, InitialFetchRequest } from "../../interfaces";
36 import { UserService, WebSocketService } from "../../services";
37 import {
38   buildCommentsTree,
39   commentsToFlatNodes,
40   commentTreeMaxDepth,
41   createCommentLikeRes,
42   createPostLikeRes,
43   debounce,
44   editCommentRes,
45   enableDownvotes,
46   enableNsfw,
47   getCommentIdFromProps,
48   getCommentParentId,
49   getDepthFromComment,
50   getIdFromProps,
51   insertCommentIntoTree,
52   isBrowser,
53   isImage,
54   myAuth,
55   restoreScrollPosition,
56   saveCommentRes,
57   saveScrollPosition,
58   setIsoData,
59   setupTippy,
60   toast,
61   trendingFetchLimit,
62   updatePersonBlock,
63   wsClient,
64   wsSubscribe,
65 } from "../../utils";
66 import { CommentForm } from "../comment/comment-form";
67 import { CommentNodes } from "../comment/comment-nodes";
68 import { HtmlTags } from "../common/html-tags";
69 import { Icon, Spinner } from "../common/icon";
70 import { Sidebar } from "../community/sidebar";
71 import { PostListing } from "./post-listing";
72
73 const commentsShownInterval = 15;
74
75 interface PostState {
76   postId?: number;
77   commentId?: number;
78   postRes?: GetPostResponse;
79   commentsRes?: GetCommentsResponse;
80   commentTree: CommentNodeI[];
81   commentSort: CommentSortType;
82   commentViewType: CommentViewType;
83   scrolled?: boolean;
84   loading: boolean;
85   crossPosts?: PostView[];
86   siteRes: GetSiteResponse;
87   commentSectionRef?: RefObject<HTMLDivElement>;
88   showSidebarMobile: boolean;
89   maxCommentsShown: number;
90 }
91
92 export class Post extends Component<any, PostState> {
93   private subscription?: Subscription;
94   private isoData = setIsoData(this.context);
95   private commentScrollDebounced: () => void;
96   state: PostState = {
97     postId: getIdFromProps(this.props),
98     commentId: getCommentIdFromProps(this.props),
99     commentTree: [],
100     commentSort: CommentSortType[CommentSortType.Hot],
101     commentViewType: CommentViewType.Tree,
102     scrolled: false,
103     loading: true,
104     siteRes: this.isoData.site_res,
105     showSidebarMobile: false,
106     maxCommentsShown: commentsShownInterval,
107   };
108
109   constructor(props: any, context: any) {
110     super(props, context);
111
112     this.parseMessage = this.parseMessage.bind(this);
113     this.subscription = wsSubscribe(this.parseMessage);
114
115     this.state = { ...this.state, commentSectionRef: createRef() };
116
117     // Only fetch the data if coming from another route
118     if (this.isoData.path == this.context.router.route.match.url) {
119       this.state = {
120         ...this.state,
121         postRes: this.isoData.routeData[0] as GetPostResponse,
122         commentsRes: this.isoData.routeData[1] as GetCommentsResponse,
123       };
124
125       if (this.state.commentsRes) {
126         this.state = {
127           ...this.state,
128           commentTree: buildCommentsTree(
129             this.state.commentsRes.comments,
130             !!this.state.commentId
131           ),
132         };
133       }
134
135       this.state = { ...this.state, loading: false };
136
137       if (isBrowser()) {
138         if (this.state.postRes) {
139           WebSocketService.Instance.send(
140             wsClient.communityJoin({
141               community_id: this.state.postRes.community_view.community.id,
142             })
143           );
144         }
145
146         if (this.state.postId) {
147           WebSocketService.Instance.send(
148             wsClient.postJoin({ post_id: this.state.postId })
149           );
150         }
151
152         this.fetchCrossPosts();
153
154         if (this.checkScrollIntoCommentsParam) {
155           this.scrollIntoCommentSection();
156         }
157       }
158     } else {
159       this.fetchPost();
160     }
161   }
162
163   fetchPost() {
164     let auth = myAuth(false);
165     let postForm: GetPost = {
166       id: this.state.postId,
167       comment_id: this.state.commentId,
168       auth,
169     };
170     WebSocketService.Instance.send(wsClient.getPost(postForm));
171
172     let commentsForm: GetComments = {
173       post_id: this.state.postId,
174       parent_id: this.state.commentId,
175       max_depth: commentTreeMaxDepth,
176       sort: this.state.commentSort,
177       type_: ListingType.All,
178       saved_only: false,
179       auth,
180     };
181     WebSocketService.Instance.send(wsClient.getComments(commentsForm));
182   }
183
184   fetchCrossPosts() {
185     let q = this.state.postRes?.post_view.post.url;
186     if (q) {
187       let form: Search = {
188         q,
189         type_: SearchType.Url,
190         sort: SortType.TopAll,
191         listing_type: ListingType.All,
192         page: 1,
193         limit: trendingFetchLimit,
194         auth: myAuth(false),
195       };
196       WebSocketService.Instance.send(wsClient.search(form));
197     }
198   }
199
200   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
201     let pathSplit = req.path.split("/");
202     let promises: Promise<any>[] = [];
203
204     let pathType = pathSplit.at(1);
205     let id = pathSplit.at(2) ? Number(pathSplit.at(2)) : undefined;
206     let auth = req.auth;
207
208     let postForm: GetPost = {
209       auth,
210     };
211
212     let commentsForm: GetComments = {
213       max_depth: commentTreeMaxDepth,
214       sort: CommentSortType.Hot,
215       type_: ListingType.All,
216       saved_only: false,
217       auth,
218     };
219
220     // Set the correct id based on the path type
221     if (pathType == "post") {
222       postForm.id = id;
223       commentsForm.post_id = id;
224     } else {
225       postForm.comment_id = id;
226       commentsForm.parent_id = id;
227     }
228
229     promises.push(req.client.getPost(postForm));
230     promises.push(req.client.getComments(commentsForm));
231
232     return promises;
233   }
234
235   componentWillUnmount() {
236     this.subscription?.unsubscribe();
237     document.removeEventListener("scroll", this.commentScrollDebounced);
238
239     saveScrollPosition(this.context);
240   }
241
242   componentDidMount() {
243     autosize(document.querySelectorAll("textarea"));
244
245     this.commentScrollDebounced = debounce(this.trackCommentsBoxScrolling, 100);
246     document.addEventListener("scroll", this.commentScrollDebounced);
247   }
248
249   componentDidUpdate(_lastProps: any) {
250     // Necessary if you are on a post and you click another post (same route)
251     if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
252       // TODO Couldnt get a refresh working. This does for now.
253       location.reload();
254
255       // let currentId = this.props.match.params.id;
256       // WebSocketService.Instance.getPost(currentId);
257       // this.context.refresh();
258       // this.context.router.history.push(_lastProps.location.pathname);
259     }
260   }
261
262   get checkScrollIntoCommentsParam() {
263     return Boolean(
264       new URLSearchParams(this.props.location.search).get("scrollToComments")
265     );
266   }
267
268   scrollIntoCommentSection() {
269     this.state.commentSectionRef?.current?.scrollIntoView();
270   }
271
272   isBottom(el: Element): boolean {
273     return el?.getBoundingClientRect().bottom <= window.innerHeight;
274   }
275
276   /**
277    * Shows new comments when scrolling to the bottom of the comments div
278    */
279   trackCommentsBoxScrolling = () => {
280     const wrappedElement = document.getElementsByClassName("comments")[0];
281     if (wrappedElement && this.isBottom(wrappedElement)) {
282       this.setState({
283         maxCommentsShown: this.state.maxCommentsShown + commentsShownInterval,
284       });
285     }
286   };
287
288   get documentTitle(): string {
289     let name_ = this.state.postRes?.post_view.post.name;
290     let siteName = this.state.siteRes.site_view.site.name;
291     return name_ ? `${name_} - ${siteName}` : "";
292   }
293
294   get imageTag(): string | undefined {
295     let post = this.state.postRes?.post_view.post;
296     let thumbnail = post?.thumbnail_url;
297     let url = post?.url;
298     return thumbnail || (url && isImage(url) ? url : undefined);
299   }
300
301   render() {
302     let res = this.state.postRes;
303     let description = res?.post_view.post.body;
304     return (
305       <div className="container-lg">
306         {this.state.loading ? (
307           <h5>
308             <Spinner large />
309           </h5>
310         ) : (
311           res && (
312             <div className="row">
313               <div className="col-12 col-md-8 mb-3">
314                 <HtmlTags
315                   title={this.documentTitle}
316                   path={this.context.router.route.match.url}
317                   image={this.imageTag}
318                   description={description}
319                 />
320                 <PostListing
321                   post_view={res.post_view}
322                   duplicates={this.state.crossPosts}
323                   showBody
324                   showCommunity
325                   moderators={res.moderators}
326                   admins={this.state.siteRes.admins}
327                   enableDownvotes={enableDownvotes(this.state.siteRes)}
328                   enableNsfw={enableNsfw(this.state.siteRes)}
329                   allLanguages={this.state.siteRes.all_languages}
330                   siteLanguages={this.state.siteRes.discussion_languages}
331                 />
332                 <div ref={this.state.commentSectionRef} className="mb-2" />
333                 <CommentForm
334                   node={res.post_view.post.id}
335                   disabled={res.post_view.post.locked}
336                   allLanguages={this.state.siteRes.all_languages}
337                   siteLanguages={this.state.siteRes.discussion_languages}
338                 />
339                 <div className="d-block d-md-none">
340                   <button
341                     className="btn btn-secondary d-inline-block mb-2 mr-3"
342                     onClick={linkEvent(this, this.handleShowSidebarMobile)}
343                   >
344                     {i18n.t("sidebar")}{" "}
345                     <Icon
346                       icon={
347                         this.state.showSidebarMobile
348                           ? `minus-square`
349                           : `plus-square`
350                       }
351                       classes="icon-inline"
352                     />
353                   </button>
354                   {this.state.showSidebarMobile && this.sidebar()}
355                 </div>
356                 {this.sortRadios()}
357                 {this.state.commentViewType == CommentViewType.Tree &&
358                   this.commentsTree()}
359                 {this.state.commentViewType == CommentViewType.Flat &&
360                   this.commentsFlat()}
361               </div>
362               <div className="d-none d-md-block col-md-4">{this.sidebar()}</div>
363             </div>
364           )
365         )}
366       </div>
367     );
368   }
369
370   sortRadios() {
371     return (
372       <>
373         <div className="btn-group btn-group-toggle flex-wrap mr-3 mb-2">
374           <label
375             className={`btn btn-outline-secondary pointer ${
376               CommentSortType[this.state.commentSort] === CommentSortType.Hot &&
377               "active"
378             }`}
379           >
380             {i18n.t("hot")}
381             <input
382               type="radio"
383               value={CommentSortType.Hot}
384               checked={this.state.commentSort === CommentSortType.Hot}
385               onChange={linkEvent(this, this.handleCommentSortChange)}
386             />
387           </label>
388           <label
389             className={`btn btn-outline-secondary pointer ${
390               CommentSortType[this.state.commentSort] === CommentSortType.Top &&
391               "active"
392             }`}
393           >
394             {i18n.t("top")}
395             <input
396               type="radio"
397               value={CommentSortType.Top}
398               checked={this.state.commentSort === CommentSortType.Top}
399               onChange={linkEvent(this, this.handleCommentSortChange)}
400             />
401           </label>
402           <label
403             className={`btn btn-outline-secondary pointer ${
404               CommentSortType[this.state.commentSort] === CommentSortType.New &&
405               "active"
406             }`}
407           >
408             {i18n.t("new")}
409             <input
410               type="radio"
411               value={CommentSortType.New}
412               checked={this.state.commentSort === CommentSortType.New}
413               onChange={linkEvent(this, this.handleCommentSortChange)}
414             />
415           </label>
416           <label
417             className={`btn btn-outline-secondary pointer ${
418               CommentSortType[this.state.commentSort] === CommentSortType.Old &&
419               "active"
420             }`}
421           >
422             {i18n.t("old")}
423             <input
424               type="radio"
425               value={CommentSortType.Old}
426               checked={this.state.commentSort === CommentSortType.Old}
427               onChange={linkEvent(this, this.handleCommentSortChange)}
428             />
429           </label>
430         </div>
431         <div className="btn-group btn-group-toggle flex-wrap mb-2">
432           <label
433             className={`btn btn-outline-secondary pointer ${
434               this.state.commentViewType === CommentViewType.Flat && "active"
435             }`}
436           >
437             {i18n.t("chat")}
438             <input
439               type="radio"
440               value={CommentViewType.Flat}
441               checked={this.state.commentViewType === CommentViewType.Flat}
442               onChange={linkEvent(this, this.handleCommentViewTypeChange)}
443             />
444           </label>
445         </div>
446       </>
447     );
448   }
449
450   commentsFlat() {
451     // These are already sorted by new
452     let commentsRes = this.state.commentsRes;
453     let postRes = this.state.postRes;
454     return (
455       commentsRes &&
456       postRes && (
457         <div>
458           <CommentNodes
459             nodes={commentsToFlatNodes(commentsRes.comments)}
460             viewType={this.state.commentViewType}
461             maxCommentsShown={this.state.maxCommentsShown}
462             noIndent
463             locked={postRes.post_view.post.locked}
464             moderators={postRes.moderators}
465             admins={this.state.siteRes.admins}
466             enableDownvotes={enableDownvotes(this.state.siteRes)}
467             showContext
468             allLanguages={this.state.siteRes.all_languages}
469             siteLanguages={this.state.siteRes.discussion_languages}
470           />
471         </div>
472       )
473     );
474   }
475
476   sidebar() {
477     let res = this.state.postRes;
478     return (
479       res && (
480         <div className="mb-3">
481           <Sidebar
482             community_view={res.community_view}
483             moderators={res.moderators}
484             admins={this.state.siteRes.admins}
485             online={res.online}
486             enableNsfw={enableNsfw(this.state.siteRes)}
487             showIcon
488             allLanguages={this.state.siteRes.all_languages}
489             siteLanguages={this.state.siteRes.discussion_languages}
490           />
491         </div>
492       )
493     );
494   }
495
496   handleCommentSortChange(i: Post, event: any) {
497     i.setState({
498       commentSort: CommentSortType[event.target.value],
499       commentViewType: CommentViewType.Tree,
500       commentsRes: undefined,
501       postRes: undefined,
502     });
503     i.fetchPost();
504   }
505
506   handleCommentViewTypeChange(i: Post, event: any) {
507     let comments = i.state.commentsRes?.comments;
508     if (comments) {
509       i.setState({
510         commentViewType: Number(event.target.value),
511         commentSort: CommentSortType.New,
512         commentTree: buildCommentsTree(comments, !!i.state.commentId),
513       });
514     }
515   }
516
517   handleShowSidebarMobile(i: Post) {
518     i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
519   }
520
521   handleViewPost(i: Post) {
522     let id = i.state.postRes?.post_view.post.id;
523     if (id) {
524       i.context.router.history.push(`/post/${id}`);
525     }
526   }
527
528   handleViewContext(i: Post) {
529     let parentId = getCommentParentId(
530       i.state.commentsRes?.comments?.at(0)?.comment
531     );
532     if (parentId) {
533       i.context.router.history.push(`/comment/${parentId}`);
534     }
535   }
536
537   commentsTree() {
538     let res = this.state.postRes;
539     let firstComment = this.state.commentTree.at(0)?.comment_view.comment;
540     let depth = getDepthFromComment(firstComment);
541     let showContextButton = depth ? depth > 0 : false;
542
543     return (
544       res && (
545         <div>
546           {!!this.state.commentId && (
547             <>
548               <button
549                 className="pl-0 d-block btn btn-link text-muted"
550                 onClick={linkEvent(this, this.handleViewPost)}
551               >
552                 {i18n.t("view_all_comments")} ➔
553               </button>
554               {showContextButton && (
555                 <button
556                   className="pl-0 d-block btn btn-link text-muted"
557                   onClick={linkEvent(this, this.handleViewContext)}
558                 >
559                   {i18n.t("show_context")} ➔
560                 </button>
561               )}
562             </>
563           )}
564           <CommentNodes
565             nodes={this.state.commentTree}
566             viewType={this.state.commentViewType}
567             maxCommentsShown={this.state.maxCommentsShown}
568             locked={res.post_view.post.locked}
569             moderators={res.moderators}
570             admins={this.state.siteRes.admins}
571             enableDownvotes={enableDownvotes(this.state.siteRes)}
572             allLanguages={this.state.siteRes.all_languages}
573             siteLanguages={this.state.siteRes.discussion_languages}
574           />
575         </div>
576       )
577     );
578   }
579
580   parseMessage(msg: any) {
581     let op = wsUserOp(msg);
582     console.log(msg);
583     if (msg.error) {
584       toast(i18n.t(msg.error), "danger");
585       return;
586     } else if (msg.reconnect) {
587       let post_id = this.state.postRes?.post_view.post.id;
588       if (post_id) {
589         WebSocketService.Instance.send(wsClient.postJoin({ post_id }));
590         WebSocketService.Instance.send(
591           wsClient.getPost({
592             id: post_id,
593             auth: myAuth(false),
594           })
595         );
596       }
597     } else if (op == UserOperation.GetPost) {
598       let data = wsJsonToRes<GetPostResponse>(msg);
599       this.setState({ postRes: data });
600
601       // join the rooms
602       WebSocketService.Instance.send(
603         wsClient.postJoin({ post_id: data.post_view.post.id })
604       );
605       WebSocketService.Instance.send(
606         wsClient.communityJoin({
607           community_id: data.community_view.community.id,
608         })
609       );
610
611       // Get cross-posts
612       // TODO move this into initial fetch and refetch
613       this.fetchCrossPosts();
614       setupTippy();
615       if (!this.state.commentId) restoreScrollPosition(this.context);
616
617       if (this.checkScrollIntoCommentsParam) {
618         this.scrollIntoCommentSection();
619       }
620     } else if (op == UserOperation.GetComments) {
621       let data = wsJsonToRes<GetCommentsResponse>(msg);
622       // This section sets the comments res
623       let comments = this.state.commentsRes?.comments;
624       if (comments) {
625         // You might need to append here, since this could be building more comments from a tree fetch
626         // Remove the first comment, since it is the parent
627         let newComments = data.comments;
628         newComments.shift();
629         comments.push(...newComments);
630       } else {
631         this.setState({ commentsRes: data });
632       }
633
634       let cComments = this.state.commentsRes?.comments ?? [];
635       this.setState({
636         commentTree: buildCommentsTree(cComments, !!this.state.commentId),
637         loading: false,
638       });
639     } else if (op == UserOperation.CreateComment) {
640       let data = wsJsonToRes<CommentResponse>(msg);
641
642       // Don't get comments from the post room, if the creator is blocked
643       let creatorBlocked = UserService.Instance.myUserInfo?.person_blocks
644         .map(pb => pb.target.id)
645         .includes(data.comment_view.creator.id);
646
647       // Necessary since it might be a user reply, which has the recipients, to avoid double
648       let postRes = this.state.postRes;
649       let commentsRes = this.state.commentsRes;
650       if (
651         data.recipient_ids.length == 0 &&
652         !creatorBlocked &&
653         postRes &&
654         data.comment_view.post.id == postRes.post_view.post.id &&
655         commentsRes
656       ) {
657         commentsRes.comments.unshift(data.comment_view);
658         insertCommentIntoTree(
659           this.state.commentTree,
660           data.comment_view,
661           !!this.state.commentId
662         );
663         postRes.post_view.counts.comments++;
664
665         this.setState(this.state);
666         setupTippy();
667       }
668     } else if (
669       op == UserOperation.EditComment ||
670       op == UserOperation.DeleteComment ||
671       op == UserOperation.RemoveComment
672     ) {
673       let data = wsJsonToRes<CommentResponse>(msg);
674       editCommentRes(data.comment_view, this.state.commentsRes?.comments);
675       this.setState(this.state);
676       setupTippy();
677     } else if (op == UserOperation.SaveComment) {
678       let data = wsJsonToRes<CommentResponse>(msg);
679       saveCommentRes(data.comment_view, this.state.commentsRes?.comments);
680       this.setState(this.state);
681       setupTippy();
682     } else if (op == UserOperation.CreateCommentLike) {
683       let data = wsJsonToRes<CommentResponse>(msg);
684       createCommentLikeRes(data.comment_view, this.state.commentsRes?.comments);
685       this.setState(this.state);
686     } else if (op == UserOperation.CreatePostLike) {
687       let data = wsJsonToRes<PostResponse>(msg);
688       createPostLikeRes(data.post_view, this.state.postRes?.post_view);
689       this.setState(this.state);
690     } else if (
691       op == UserOperation.EditPost ||
692       op == UserOperation.DeletePost ||
693       op == UserOperation.RemovePost ||
694       op == UserOperation.LockPost ||
695       op == UserOperation.FeaturePost ||
696       op == UserOperation.SavePost
697     ) {
698       let data = wsJsonToRes<PostResponse>(msg);
699       let res = this.state.postRes;
700       if (res) {
701         res.post_view = data.post_view;
702         this.setState(this.state);
703         setupTippy();
704       }
705     } else if (
706       op == UserOperation.EditCommunity ||
707       op == UserOperation.DeleteCommunity ||
708       op == UserOperation.RemoveCommunity ||
709       op == UserOperation.FollowCommunity
710     ) {
711       let data = wsJsonToRes<CommunityResponse>(msg);
712       let res = this.state.postRes;
713       if (res) {
714         res.community_view = data.community_view;
715         res.post_view.community = data.community_view.community;
716         this.setState(this.state);
717       }
718     } else if (op == UserOperation.BanFromCommunity) {
719       let data = wsJsonToRes<BanFromCommunityResponse>(msg);
720
721       let res = this.state.postRes;
722       if (res) {
723         if (res.post_view.creator.id == data.person_view.person.id) {
724           res.post_view.creator_banned_from_community = data.banned;
725         }
726       }
727
728       this.state.commentsRes?.comments
729         .filter(c => c.creator.id == data.person_view.person.id)
730         .forEach(c => (c.creator_banned_from_community = data.banned));
731       this.setState(this.state);
732     } else if (op == UserOperation.AddModToCommunity) {
733       let data = wsJsonToRes<AddModToCommunityResponse>(msg);
734       let res = this.state.postRes;
735       if (res) {
736         res.moderators = data.moderators;
737         this.setState(this.state);
738       }
739     } else if (op == UserOperation.BanPerson) {
740       let data = wsJsonToRes<BanPersonResponse>(msg);
741       this.state.commentsRes?.comments
742         .filter(c => c.creator.id == data.person_view.person.id)
743         .forEach(c => (c.creator.banned = data.banned));
744
745       let res = this.state.postRes;
746       if (res) {
747         if (res.post_view.creator.id == data.person_view.person.id) {
748           res.post_view.creator.banned = data.banned;
749         }
750       }
751       this.setState(this.state);
752     } else if (op == UserOperation.AddAdmin) {
753       let data = wsJsonToRes<AddAdminResponse>(msg);
754       this.setState(s => ((s.siteRes.admins = data.admins), s));
755     } else if (op == UserOperation.Search) {
756       let data = wsJsonToRes<SearchResponse>(msg);
757       let xPosts = data.posts.filter(
758         p => p.post.ap_id != this.state.postRes?.post_view.post.ap_id
759       );
760       this.setState({ crossPosts: xPosts.length > 0 ? xPosts : undefined });
761     } else if (op == UserOperation.LeaveAdmin) {
762       let data = wsJsonToRes<GetSiteResponse>(msg);
763       this.setState({ siteRes: data });
764     } else if (op == UserOperation.TransferCommunity) {
765       let data = wsJsonToRes<GetCommunityResponse>(msg);
766       let res = this.state.postRes;
767       if (res) {
768         res.community_view = data.community_view;
769         res.post_view.community = data.community_view.community;
770         res.moderators = data.moderators;
771         this.setState(this.state);
772       }
773     } else if (op == UserOperation.BlockPerson) {
774       let data = wsJsonToRes<BlockPersonResponse>(msg);
775       updatePersonBlock(data);
776     } else if (op == UserOperation.CreatePostReport) {
777       let data = wsJsonToRes<PostReportResponse>(msg);
778       if (data) {
779         toast(i18n.t("report_created"));
780       }
781     } else if (op == UserOperation.CreateCommentReport) {
782       let data = wsJsonToRes<CommentReportResponse>(msg);
783       if (data) {
784         toast(i18n.t("report_created"));
785       }
786     } else if (
787       op == UserOperation.PurgePerson ||
788       op == UserOperation.PurgePost ||
789       op == UserOperation.PurgeComment ||
790       op == UserOperation.PurgeCommunity
791     ) {
792       let data = wsJsonToRes<PurgeItemResponse>(msg);
793       if (data.success) {
794         toast(i18n.t("purge_success"));
795         this.context.router.history.push(`/`);
796       }
797     }
798   }
799 }