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