]> Untitled Git - lemmy-ui.git/blob - src/shared/components/post/post.tsx
Fix live updating postres edit. Fixes #908 (#911)
[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         commentsRes
655       ) {
656         commentsRes.comments.unshift(data.comment_view);
657         insertCommentIntoTree(
658           this.state.commentTree,
659           data.comment_view,
660           !!this.state.commentId
661         );
662         postRes.post_view.counts.comments++;
663
664         this.setState(this.state);
665         setupTippy();
666       }
667     } else if (
668       op == UserOperation.EditComment ||
669       op == UserOperation.DeleteComment ||
670       op == UserOperation.RemoveComment
671     ) {
672       let data = wsJsonToRes<CommentResponse>(msg);
673       editCommentRes(data.comment_view, this.state.commentsRes?.comments);
674       this.setState(this.state);
675       setupTippy();
676     } else if (op == UserOperation.SaveComment) {
677       let data = wsJsonToRes<CommentResponse>(msg);
678       saveCommentRes(data.comment_view, this.state.commentsRes?.comments);
679       this.setState(this.state);
680       setupTippy();
681     } else if (op == UserOperation.CreateCommentLike) {
682       let data = wsJsonToRes<CommentResponse>(msg);
683       createCommentLikeRes(data.comment_view, this.state.commentsRes?.comments);
684       this.setState(this.state);
685     } else if (op == UserOperation.CreatePostLike) {
686       let data = wsJsonToRes<PostResponse>(msg);
687       createPostLikeRes(data.post_view, this.state.postRes?.post_view);
688       this.setState(this.state);
689     } else if (
690       op == UserOperation.EditPost ||
691       op == UserOperation.DeletePost ||
692       op == UserOperation.RemovePost ||
693       op == UserOperation.LockPost ||
694       op == UserOperation.FeaturePost ||
695       op == UserOperation.SavePost
696     ) {
697       let data = wsJsonToRes<PostResponse>(msg);
698       let res = this.state.postRes;
699       if (res) {
700         res.post_view = data.post_view;
701         this.setState(this.state);
702         setupTippy();
703       }
704     } else if (
705       op == UserOperation.EditCommunity ||
706       op == UserOperation.DeleteCommunity ||
707       op == UserOperation.RemoveCommunity ||
708       op == UserOperation.FollowCommunity
709     ) {
710       let data = wsJsonToRes<CommunityResponse>(msg);
711       let res = this.state.postRes;
712       if (res) {
713         res.community_view = data.community_view;
714         res.post_view.community = data.community_view.community;
715         this.setState(this.state);
716       }
717     } else if (op == UserOperation.BanFromCommunity) {
718       let data = wsJsonToRes<BanFromCommunityResponse>(msg);
719
720       let res = this.state.postRes;
721       if (res) {
722         if (res.post_view.creator.id == data.person_view.person.id) {
723           res.post_view.creator_banned_from_community = data.banned;
724         }
725       }
726
727       this.state.commentsRes?.comments
728         .filter(c => c.creator.id == data.person_view.person.id)
729         .forEach(c => (c.creator_banned_from_community = data.banned));
730       this.setState(this.state);
731     } else if (op == UserOperation.AddModToCommunity) {
732       let data = wsJsonToRes<AddModToCommunityResponse>(msg);
733       let res = this.state.postRes;
734       if (res) {
735         res.moderators = data.moderators;
736         this.setState(this.state);
737       }
738     } else if (op == UserOperation.BanPerson) {
739       let data = wsJsonToRes<BanPersonResponse>(msg);
740       this.state.commentsRes?.comments
741         .filter(c => c.creator.id == data.person_view.person.id)
742         .forEach(c => (c.creator.banned = data.banned));
743
744       let res = this.state.postRes;
745       if (res) {
746         if (res.post_view.creator.id == data.person_view.person.id) {
747           res.post_view.creator.banned = data.banned;
748         }
749       }
750       this.setState(this.state);
751     } else if (op == UserOperation.AddAdmin) {
752       let data = wsJsonToRes<AddAdminResponse>(msg);
753       this.setState(s => ((s.siteRes.admins = data.admins), s));
754     } else if (op == UserOperation.Search) {
755       let data = wsJsonToRes<SearchResponse>(msg);
756       let xPosts = data.posts.filter(
757         p => p.post.id != Number(this.props.match.params.id)
758       );
759       this.setState({ crossPosts: xPosts.length > 0 ? xPosts : undefined });
760     } else if (op == UserOperation.LeaveAdmin) {
761       let data = wsJsonToRes<GetSiteResponse>(msg);
762       this.setState({ siteRes: data });
763     } else if (op == UserOperation.TransferCommunity) {
764       let data = wsJsonToRes<GetCommunityResponse>(msg);
765       let res = this.state.postRes;
766       if (res) {
767         res.community_view = data.community_view;
768         res.post_view.community = data.community_view.community;
769         res.moderators = data.moderators;
770         this.setState(this.state);
771       }
772     } else if (op == UserOperation.BlockPerson) {
773       let data = wsJsonToRes<BlockPersonResponse>(msg);
774       updatePersonBlock(data);
775     } else if (op == UserOperation.CreatePostReport) {
776       let data = wsJsonToRes<PostReportResponse>(msg);
777       if (data) {
778         toast(i18n.t("report_created"));
779       }
780     } else if (op == UserOperation.CreateCommentReport) {
781       let data = wsJsonToRes<CommentReportResponse>(msg);
782       if (data) {
783         toast(i18n.t("report_created"));
784       }
785     } else if (
786       op == UserOperation.PurgePerson ||
787       op == UserOperation.PurgePost ||
788       op == UserOperation.PurgeComment ||
789       op == UserOperation.PurgeCommunity
790     ) {
791       let data = wsJsonToRes<PurgeItemResponse>(msg);
792       if (data.success) {
793         toast(i18n.t("purge_success"));
794         this.context.router.history.push(`/`);
795       }
796     }
797   }
798 }