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