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