]> Untitled Git - lemmy-ui.git/blob - src/shared/components/post/post.tsx
eed5aad1bcb11109edd3690ebd686ed28fcb9d19
[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   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: "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_: "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_: "Url",
190         sort: "TopAll",
191         listing_type: "All",
192         page: 1n,
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: "Hot",
215       type_: "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               this.state.commentSort === "Hot" && "active"
377             }`}
378           >
379             {i18n.t("hot")}
380             <input
381               type="radio"
382               value={"Hot"}
383               checked={this.state.commentSort === "Hot"}
384               onChange={linkEvent(this, this.handleCommentSortChange)}
385             />
386           </label>
387           <label
388             className={`btn btn-outline-secondary pointer ${
389               this.state.commentSort === "Top" && "active"
390             }`}
391           >
392             {i18n.t("top")}
393             <input
394               type="radio"
395               value={"Top"}
396               checked={this.state.commentSort === "Top"}
397               onChange={linkEvent(this, this.handleCommentSortChange)}
398             />
399           </label>
400           <label
401             className={`btn btn-outline-secondary pointer ${
402               this.state.commentSort === "New" && "active"
403             }`}
404           >
405             {i18n.t("new")}
406             <input
407               type="radio"
408               value={"New"}
409               checked={this.state.commentSort === "New"}
410               onChange={linkEvent(this, this.handleCommentSortChange)}
411             />
412           </label>
413           <label
414             className={`btn btn-outline-secondary pointer ${
415               this.state.commentSort === "Old" && "active"
416             }`}
417           >
418             {i18n.t("old")}
419             <input
420               type="radio"
421               value={"Old"}
422               checked={this.state.commentSort === "Old"}
423               onChange={linkEvent(this, this.handleCommentSortChange)}
424             />
425           </label>
426         </div>
427         <div className="btn-group btn-group-toggle flex-wrap mb-2">
428           <label
429             className={`btn btn-outline-secondary pointer ${
430               this.state.commentViewType === CommentViewType.Flat && "active"
431             }`}
432           >
433             {i18n.t("chat")}
434             <input
435               type="radio"
436               value={CommentViewType.Flat}
437               checked={this.state.commentViewType === CommentViewType.Flat}
438               onChange={linkEvent(this, this.handleCommentViewTypeChange)}
439             />
440           </label>
441         </div>
442       </>
443     );
444   }
445
446   commentsFlat() {
447     // These are already sorted by new
448     let commentsRes = this.state.commentsRes;
449     let postRes = this.state.postRes;
450     return (
451       commentsRes &&
452       postRes && (
453         <div>
454           <CommentNodes
455             nodes={commentsToFlatNodes(commentsRes.comments)}
456             viewType={this.state.commentViewType}
457             maxCommentsShown={this.state.maxCommentsShown}
458             noIndent
459             locked={postRes.post_view.post.locked}
460             moderators={postRes.moderators}
461             admins={this.state.siteRes.admins}
462             enableDownvotes={enableDownvotes(this.state.siteRes)}
463             showContext
464             allLanguages={this.state.siteRes.all_languages}
465             siteLanguages={this.state.siteRes.discussion_languages}
466           />
467         </div>
468       )
469     );
470   }
471
472   sidebar() {
473     let res = this.state.postRes;
474     return (
475       res && (
476         <div className="mb-3">
477           <Sidebar
478             community_view={res.community_view}
479             moderators={res.moderators}
480             admins={this.state.siteRes.admins}
481             online={res.online}
482             enableNsfw={enableNsfw(this.state.siteRes)}
483             showIcon
484             allLanguages={this.state.siteRes.all_languages}
485             siteLanguages={this.state.siteRes.discussion_languages}
486           />
487         </div>
488       )
489     );
490   }
491
492   handleCommentSortChange(i: Post, event: any) {
493     i.setState({
494       commentSort: event.target.value as CommentSortType,
495       commentViewType: CommentViewType.Tree,
496       commentsRes: undefined,
497       postRes: undefined,
498     });
499     i.fetchPost();
500   }
501
502   handleCommentViewTypeChange(i: Post, event: any) {
503     let comments = i.state.commentsRes?.comments;
504     if (comments) {
505       i.setState({
506         commentViewType: Number(event.target.value),
507         commentSort: "New",
508         commentTree: buildCommentsTree(comments, !!i.state.commentId),
509       });
510     }
511   }
512
513   handleShowSidebarMobile(i: Post) {
514     i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
515   }
516
517   handleViewPost(i: Post) {
518     let id = i.state.postRes?.post_view.post.id;
519     if (id) {
520       i.context.router.history.push(`/post/${id}`);
521     }
522   }
523
524   handleViewContext(i: Post) {
525     let parentId = getCommentParentId(
526       i.state.commentsRes?.comments?.at(0)?.comment
527     );
528     if (parentId) {
529       i.context.router.history.push(`/comment/${parentId}`);
530     }
531   }
532
533   commentsTree() {
534     let res = this.state.postRes;
535     let firstComment = this.state.commentTree.at(0)?.comment_view.comment;
536     let depth = getDepthFromComment(firstComment);
537     let showContextButton = depth ? depth > 0 : false;
538
539     return (
540       res && (
541         <div>
542           {!!this.state.commentId && (
543             <>
544               <button
545                 className="pl-0 d-block btn btn-link text-muted"
546                 onClick={linkEvent(this, this.handleViewPost)}
547               >
548                 {i18n.t("view_all_comments")} ➔
549               </button>
550               {showContextButton && (
551                 <button
552                   className="pl-0 d-block btn btn-link text-muted"
553                   onClick={linkEvent(this, this.handleViewContext)}
554                 >
555                   {i18n.t("show_context")} ➔
556                 </button>
557               )}
558             </>
559           )}
560           <CommentNodes
561             nodes={this.state.commentTree}
562             viewType={this.state.commentViewType}
563             maxCommentsShown={this.state.maxCommentsShown}
564             locked={res.post_view.post.locked}
565             moderators={res.moderators}
566             admins={this.state.siteRes.admins}
567             enableDownvotes={enableDownvotes(this.state.siteRes)}
568             allLanguages={this.state.siteRes.all_languages}
569             siteLanguages={this.state.siteRes.discussion_languages}
570           />
571         </div>
572       )
573     );
574   }
575
576   parseMessage(msg: any) {
577     let op = wsUserOp(msg);
578     console.log(msg);
579     if (msg.error) {
580       toast(i18n.t(msg.error), "danger");
581       return;
582     } else if (msg.reconnect) {
583       let post_id = this.state.postRes?.post_view.post.id;
584       if (post_id) {
585         WebSocketService.Instance.send(wsClient.postJoin({ post_id }));
586         WebSocketService.Instance.send(
587           wsClient.getPost({
588             id: post_id,
589             auth: myAuth(false),
590           })
591         );
592       }
593     } else if (op == UserOperation.GetPost) {
594       let data = wsJsonToRes<GetPostResponse>(msg);
595       this.setState({ postRes: data });
596
597       // join the rooms
598       WebSocketService.Instance.send(
599         wsClient.postJoin({ post_id: data.post_view.post.id })
600       );
601       WebSocketService.Instance.send(
602         wsClient.communityJoin({
603           community_id: data.community_view.community.id,
604         })
605       );
606
607       // Get cross-posts
608       // TODO move this into initial fetch and refetch
609       this.fetchCrossPosts();
610       setupTippy();
611       if (!this.state.commentId) restoreScrollPosition(this.context);
612
613       if (this.checkScrollIntoCommentsParam) {
614         this.scrollIntoCommentSection();
615       }
616     } else if (op == UserOperation.GetComments) {
617       let data = wsJsonToRes<GetCommentsResponse>(msg);
618       // This section sets the comments res
619       let comments = this.state.commentsRes?.comments;
620       if (comments) {
621         // You might need to append here, since this could be building more comments from a tree fetch
622         // Remove the first comment, since it is the parent
623         let newComments = data.comments;
624         newComments.shift();
625         comments.push(...newComments);
626       } else {
627         this.setState({ commentsRes: data });
628       }
629
630       let cComments = this.state.commentsRes?.comments ?? [];
631       this.setState({
632         commentTree: buildCommentsTree(cComments, !!this.state.commentId),
633         loading: false,
634       });
635     } else if (op == UserOperation.CreateComment) {
636       let data = wsJsonToRes<CommentResponse>(msg);
637
638       // Don't get comments from the post room, if the creator is blocked
639       let creatorBlocked = UserService.Instance.myUserInfo?.person_blocks
640         .map(pb => pb.target.id)
641         .includes(data.comment_view.creator.id);
642
643       // Necessary since it might be a user reply, which has the recipients, to avoid double
644       let postRes = this.state.postRes;
645       let commentsRes = this.state.commentsRes;
646       if (
647         data.recipient_ids.length == 0 &&
648         !creatorBlocked &&
649         postRes &&
650         data.comment_view.post.id == postRes.post_view.post.id &&
651         commentsRes
652       ) {
653         commentsRes.comments.unshift(data.comment_view);
654         insertCommentIntoTree(
655           this.state.commentTree,
656           data.comment_view,
657           !!this.state.commentId
658         );
659         postRes.post_view.counts.comments++;
660
661         this.setState(this.state);
662         setupTippy();
663       }
664     } else if (
665       op == UserOperation.EditComment ||
666       op == UserOperation.DeleteComment ||
667       op == UserOperation.RemoveComment
668     ) {
669       let data = wsJsonToRes<CommentResponse>(msg);
670       editCommentRes(data.comment_view, this.state.commentsRes?.comments);
671       this.setState(this.state);
672       setupTippy();
673     } else if (op == UserOperation.SaveComment) {
674       let data = wsJsonToRes<CommentResponse>(msg);
675       saveCommentRes(data.comment_view, this.state.commentsRes?.comments);
676       this.setState(this.state);
677       setupTippy();
678     } else if (op == UserOperation.CreateCommentLike) {
679       let data = wsJsonToRes<CommentResponse>(msg);
680       createCommentLikeRes(data.comment_view, this.state.commentsRes?.comments);
681       this.setState(this.state);
682     } else if (op == UserOperation.CreatePostLike) {
683       let data = wsJsonToRes<PostResponse>(msg);
684       createPostLikeRes(data.post_view, this.state.postRes?.post_view);
685       this.setState(this.state);
686     } else if (
687       op == UserOperation.EditPost ||
688       op == UserOperation.DeletePost ||
689       op == UserOperation.RemovePost ||
690       op == UserOperation.LockPost ||
691       op == UserOperation.FeaturePost ||
692       op == UserOperation.SavePost
693     ) {
694       let data = wsJsonToRes<PostResponse>(msg);
695       let res = this.state.postRes;
696       if (res) {
697         res.post_view = data.post_view;
698         this.setState(this.state);
699         setupTippy();
700       }
701     } else if (
702       op == UserOperation.EditCommunity ||
703       op == UserOperation.DeleteCommunity ||
704       op == UserOperation.RemoveCommunity ||
705       op == UserOperation.FollowCommunity
706     ) {
707       let data = wsJsonToRes<CommunityResponse>(msg);
708       let res = this.state.postRes;
709       if (res) {
710         res.community_view = data.community_view;
711         res.post_view.community = data.community_view.community;
712         this.setState(this.state);
713       }
714     } else if (op == UserOperation.BanFromCommunity) {
715       let data = wsJsonToRes<BanFromCommunityResponse>(msg);
716
717       let res = this.state.postRes;
718       if (res) {
719         if (res.post_view.creator.id == data.person_view.person.id) {
720           res.post_view.creator_banned_from_community = data.banned;
721         }
722       }
723
724       this.state.commentsRes?.comments
725         .filter(c => c.creator.id == data.person_view.person.id)
726         .forEach(c => (c.creator_banned_from_community = data.banned));
727       this.setState(this.state);
728     } else if (op == UserOperation.AddModToCommunity) {
729       let data = wsJsonToRes<AddModToCommunityResponse>(msg);
730       let res = this.state.postRes;
731       if (res) {
732         res.moderators = data.moderators;
733         this.setState(this.state);
734       }
735     } else if (op == UserOperation.BanPerson) {
736       let data = wsJsonToRes<BanPersonResponse>(msg);
737       this.state.commentsRes?.comments
738         .filter(c => c.creator.id == data.person_view.person.id)
739         .forEach(c => (c.creator.banned = data.banned));
740
741       let res = this.state.postRes;
742       if (res) {
743         if (res.post_view.creator.id == data.person_view.person.id) {
744           res.post_view.creator.banned = data.banned;
745         }
746       }
747       this.setState(this.state);
748     } else if (op == UserOperation.AddAdmin) {
749       let data = wsJsonToRes<AddAdminResponse>(msg);
750       this.setState(s => ((s.siteRes.admins = data.admins), s));
751     } else if (op == UserOperation.Search) {
752       let data = wsJsonToRes<SearchResponse>(msg);
753       let xPosts = data.posts.filter(
754         p => p.post.ap_id != this.state.postRes?.post_view.post.ap_id
755       );
756       this.setState({ crossPosts: xPosts.length > 0 ? xPosts : undefined });
757     } else if (op == UserOperation.LeaveAdmin) {
758       let data = wsJsonToRes<GetSiteResponse>(msg);
759       this.setState({ siteRes: data });
760     } else if (op == UserOperation.TransferCommunity) {
761       let data = wsJsonToRes<GetCommunityResponse>(msg);
762       let res = this.state.postRes;
763       if (res) {
764         res.community_view = data.community_view;
765         res.post_view.community = data.community_view.community;
766         res.moderators = data.moderators;
767         this.setState(this.state);
768       }
769     } else if (op == UserOperation.BlockPerson) {
770       let data = wsJsonToRes<BlockPersonResponse>(msg);
771       updatePersonBlock(data);
772     } else if (op == UserOperation.CreatePostReport) {
773       let data = wsJsonToRes<PostReportResponse>(msg);
774       if (data) {
775         toast(i18n.t("report_created"));
776       }
777     } else if (op == UserOperation.CreateCommentReport) {
778       let data = wsJsonToRes<CommentReportResponse>(msg);
779       if (data) {
780         toast(i18n.t("report_created"));
781       }
782     } else if (
783       op == UserOperation.PurgePerson ||
784       op == UserOperation.PurgePost ||
785       op == UserOperation.PurgeComment ||
786       op == UserOperation.PurgeCommunity
787     ) {
788       let data = wsJsonToRes<PurgeItemResponse>(msg);
789       if (data.success) {
790         toast(i18n.t("purge_success"));
791         this.context.router.history.push(`/`);
792       }
793     }
794   }
795 }