]> Untitled Git - lemmy-ui.git/blob - src/shared/components/home/home.tsx
Merge branch 'main' into custom-emojis
[lemmy-ui.git] / src / shared / components / home / home.tsx
1 import { Component, linkEvent } from "inferno";
2 import { T } from "inferno-i18next-dess";
3 import { Link } from "inferno-router";
4 import {
5   AddAdminResponse,
6   BanPersonResponse,
7   BlockPersonResponse,
8   CommentReportResponse,
9   CommentResponse,
10   CommentView,
11   CommunityView,
12   GetComments,
13   GetCommentsResponse,
14   GetPosts,
15   GetPostsResponse,
16   GetSiteResponse,
17   ListCommunities,
18   ListCommunitiesResponse,
19   ListingType,
20   PostReportResponse,
21   PostResponse,
22   PostView,
23   PurgeItemResponse,
24   SiteResponse,
25   SortType,
26   UserOperation,
27   wsJsonToRes,
28   wsUserOp,
29 } from "lemmy-js-client";
30 import { Subscription } from "rxjs";
31 import { i18n } from "../../i18next";
32 import {
33   CommentViewType,
34   DataType,
35   InitialFetchRequest,
36 } from "../../interfaces";
37 import { UserService, WebSocketService } from "../../services";
38 import {
39   canCreateCommunity,
40   commentsToFlatNodes,
41   createCommentLikeRes,
42   createPostLikeFindRes,
43   editCommentRes,
44   editPostFindRes,
45   enableDownvotes,
46   enableNsfw,
47   fetchLimit,
48   getDataTypeFromProps,
49   getListingTypeFromProps,
50   getPageFromProps,
51   getRandomFromList,
52   getSortTypeFromProps,
53   isBrowser,
54   isPostBlocked,
55   mdToHtml,
56   myAuth,
57   notifyPost,
58   nsfwCheck,
59   postToCommentSortType,
60   relTags,
61   restoreScrollPosition,
62   saveCommentRes,
63   saveScrollPosition,
64   setIsoData,
65   setupTippy,
66   showLocal,
67   toast,
68   trendingFetchLimit,
69   updatePersonBlock,
70   wsClient,
71   wsSubscribe,
72 } from "../../utils";
73 import { CommentNodes } from "../comment/comment-nodes";
74 import { DataTypeSelect } from "../common/data-type-select";
75 import { HtmlTags } from "../common/html-tags";
76 import { Icon, Spinner } from "../common/icon";
77 import { ListingTypeSelect } from "../common/listing-type-select";
78 import { Paginator } from "../common/paginator";
79 import { SortSelect } from "../common/sort-select";
80 import { CommunityLink } from "../community/community-link";
81 import { PostListings } from "../post/post-listings";
82 import { SiteSidebar } from "./site-sidebar";
83
84 interface HomeState {
85   trendingCommunities: CommunityView[];
86   siteRes: GetSiteResponse;
87   posts: PostView[];
88   comments: CommentView[];
89   listingType: ListingType;
90   dataType: DataType;
91   sort: SortType;
92   page: number;
93   showSubscribedMobile: boolean;
94   showTrendingMobile: boolean;
95   showSidebarMobile: boolean;
96   subscribedCollapsed: boolean;
97   loading: boolean;
98   tagline?: string;
99 }
100
101 interface HomeProps {
102   listingType: ListingType;
103   dataType: DataType;
104   sort: SortType;
105   page: number;
106 }
107
108 interface UrlParams {
109   listingType?: ListingType;
110   dataType?: string;
111   sort?: SortType;
112   page?: number;
113 }
114
115 export class Home extends Component<any, HomeState> {
116   private isoData = setIsoData(this.context);
117   private subscription?: Subscription;
118   state: HomeState = {
119     trendingCommunities: [],
120     siteRes: this.isoData.site_res,
121     showSubscribedMobile: false,
122     showTrendingMobile: false,
123     showSidebarMobile: false,
124     subscribedCollapsed: false,
125     loading: true,
126     posts: [],
127     comments: [],
128     listingType: getListingTypeFromProps(
129       this.props,
130       ListingType[
131         this.isoData.site_res.site_view.local_site.default_post_listing_type
132       ]
133     ),
134     dataType: getDataTypeFromProps(this.props),
135     sort: getSortTypeFromProps(this.props),
136     page: getPageFromProps(this.props),
137   };
138
139   constructor(props: any, context: any) {
140     super(props, context);
141
142     this.handleSortChange = this.handleSortChange.bind(this);
143     this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
144     this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
145     this.handlePageChange = this.handlePageChange.bind(this);
146
147     this.parseMessage = this.parseMessage.bind(this);
148     this.subscription = wsSubscribe(this.parseMessage);
149
150     // Only fetch the data if coming from another route
151     if (this.isoData.path == this.context.router.route.match.url) {
152       let postsRes = this.isoData.routeData[0] as GetPostsResponse | undefined;
153       let commentsRes = this.isoData.routeData[1] as
154         | GetCommentsResponse
155         | undefined;
156       let trendingRes = this.isoData.routeData[2] as
157         | ListCommunitiesResponse
158         | undefined;
159
160       if (postsRes) {
161         this.state = { ...this.state, posts: postsRes.posts };
162       }
163
164       if (commentsRes) {
165         this.state = { ...this.state, comments: commentsRes.comments };
166       }
167
168       if (isBrowser()) {
169         WebSocketService.Instance.send(
170           wsClient.communityJoin({ community_id: 0 })
171         );
172       }
173       const taglines = this.state?.siteRes?.taglines ?? [];
174       this.state = {
175         ...this.state,
176         trendingCommunities: trendingRes?.communities ?? [],
177         loading: false,
178         tagline: getRandomFromList(taglines)?.content,
179       };
180     } else {
181       this.fetchTrendingCommunities();
182       this.fetchData();
183     }
184   }
185
186   fetchTrendingCommunities() {
187     let listCommunitiesForm: ListCommunities = {
188       type_: ListingType.Local,
189       sort: SortType.Hot,
190       limit: trendingFetchLimit,
191       auth: myAuth(false),
192     };
193     WebSocketService.Instance.send(
194       wsClient.listCommunities(listCommunitiesForm)
195     );
196   }
197
198   componentDidMount() {
199     // This means it hasn't been set up yet
200     if (!this.state.siteRes.site_view.local_site.site_setup) {
201       this.context.router.history.push("/setup");
202     }
203     setupTippy();
204   }
205
206   componentWillUnmount() {
207     saveScrollPosition(this.context);
208     this.subscription?.unsubscribe();
209   }
210
211   static getDerivedStateFromProps(
212     props: HomeProps,
213     state: HomeState
214   ): HomeProps {
215     return {
216       listingType: getListingTypeFromProps(props, state.listingType),
217       dataType: getDataTypeFromProps(props),
218       sort: getSortTypeFromProps(props),
219       page: getPageFromProps(props),
220     };
221   }
222
223   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
224     let pathSplit = req.path.split("/");
225     let dataType: DataType = pathSplit[3]
226       ? DataType[pathSplit[3]]
227       : DataType.Post;
228     let mui = UserService.Instance.myUserInfo;
229     let auth = req.auth;
230
231     // TODO figure out auth default_listingType, default_sort_type
232     let type_: ListingType = pathSplit[5]
233       ? ListingType[pathSplit[5]]
234       : mui
235       ? Object.values(ListingType)[
236           mui.local_user_view.local_user.default_listing_type
237         ]
238       : ListingType.Local;
239     let sort: SortType = pathSplit[7]
240       ? SortType[pathSplit[7]]
241       : mui
242       ? (Object.values(SortType)[
243           mui.local_user_view.local_user.default_sort_type
244         ] as SortType)
245       : SortType.Active;
246
247     let page = pathSplit[9] ? Number(pathSplit[9]) : 1;
248
249     let promises: Promise<any>[] = [];
250
251     if (dataType == DataType.Post) {
252       let getPostsForm: GetPosts = {
253         type_,
254         page,
255         limit: fetchLimit,
256         sort,
257         saved_only: false,
258         auth,
259       };
260
261       promises.push(req.client.getPosts(getPostsForm));
262       promises.push(Promise.resolve());
263     } else {
264       let getCommentsForm: GetComments = {
265         page,
266         limit: fetchLimit,
267         sort: postToCommentSortType(sort),
268         type_,
269         saved_only: false,
270         auth,
271       };
272       promises.push(Promise.resolve());
273       promises.push(req.client.getComments(getCommentsForm));
274     }
275
276     let trendingCommunitiesForm: ListCommunities = {
277       type_: ListingType.Local,
278       sort: SortType.Hot,
279       limit: trendingFetchLimit,
280       auth,
281     };
282     promises.push(req.client.listCommunities(trendingCommunitiesForm));
283
284     return promises;
285   }
286
287   componentDidUpdate(_: any, lastState: HomeState) {
288     if (
289       lastState.listingType !== this.state.listingType ||
290       lastState.dataType !== this.state.dataType ||
291       lastState.sort !== this.state.sort ||
292       lastState.page !== this.state.page
293     ) {
294       this.setState({ loading: true });
295       this.fetchData();
296     }
297   }
298
299   get documentTitle(): string {
300     let siteView = this.state.siteRes.site_view;
301     let desc = this.state.siteRes.site_view.site.description;
302     return desc ? `${siteView.site.name} - ${desc}` : siteView.site.name;
303   }
304
305   render() {
306     let tagline = this.state.tagline;
307
308     return (
309       <div className="container-lg">
310         <HtmlTags
311           title={this.documentTitle}
312           path={this.context.router.route.match.url}
313         />
314         {this.state.siteRes.site_view.local_site.site_setup && (
315           <div className="row">
316             <main role="main" className="col-12 col-md-8">
317               {tagline && (
318                 <div
319                   id="tagline"
320                   dangerouslySetInnerHTML={mdToHtml(tagline)}
321                 ></div>
322               )}
323               <div className="d-block d-md-none">{this.mobileView()}</div>
324               {this.posts()}
325             </main>
326             <aside className="d-none d-md-block col-md-4">
327               {this.mySidebar()}
328             </aside>
329           </div>
330         )}
331       </div>
332     );
333   }
334
335   get hasFollows(): boolean {
336     let mui = UserService.Instance.myUserInfo;
337     return !!mui && mui.follows.length > 0;
338   }
339
340   mobileView() {
341     let siteRes = this.state.siteRes;
342     let siteView = siteRes.site_view;
343     return (
344       <div className="row">
345         <div className="col-12">
346           {this.hasFollows && (
347             <button
348               className="btn btn-secondary d-inline-block mb-2 mr-3"
349               onClick={linkEvent(this, this.handleShowSubscribedMobile)}
350             >
351               {i18n.t("subscribed")}{" "}
352               <Icon
353                 icon={
354                   this.state.showSubscribedMobile
355                     ? `minus-square`
356                     : `plus-square`
357                 }
358                 classes="icon-inline"
359               />
360             </button>
361           )}
362           <button
363             className="btn btn-secondary d-inline-block mb-2 mr-3"
364             onClick={linkEvent(this, this.handleShowTrendingMobile)}
365           >
366             {i18n.t("trending")}{" "}
367             <Icon
368               icon={
369                 this.state.showTrendingMobile ? `minus-square` : `plus-square`
370               }
371               classes="icon-inline"
372             />
373           </button>
374           <button
375             className="btn btn-secondary d-inline-block mb-2 mr-3"
376             onClick={linkEvent(this, this.handleShowSidebarMobile)}
377           >
378             {i18n.t("sidebar")}{" "}
379             <Icon
380               icon={
381                 this.state.showSidebarMobile ? `minus-square` : `plus-square`
382               }
383               classes="icon-inline"
384             />
385           </button>
386           {this.state.showSidebarMobile && (
387             <SiteSidebar
388               site={siteView.site}
389               admins={siteRes.admins}
390               counts={siteView.counts}
391               online={siteRes.online}
392               showLocal={showLocal(this.isoData)}
393             />
394           )}
395           {this.state.showTrendingMobile && (
396             <div className="col-12 card border-secondary mb-3">
397               <div className="card-body">{this.trendingCommunities()}</div>
398             </div>
399           )}
400           {this.state.showSubscribedMobile && (
401             <div className="col-12 card border-secondary mb-3">
402               <div className="card-body">{this.subscribedCommunities()}</div>
403             </div>
404           )}
405         </div>
406       </div>
407     );
408   }
409
410   mySidebar() {
411     let siteRes = this.state.siteRes;
412     let siteView = siteRes.site_view;
413     return (
414       <div>
415         {!this.state.loading && (
416           <div>
417             <div className="card border-secondary mb-3">
418               <div className="card-body">
419                 {this.trendingCommunities()}
420                 {canCreateCommunity(this.state.siteRes) &&
421                   this.createCommunityButton()}
422                 {this.exploreCommunitiesButton()}
423               </div>
424             </div>
425             <SiteSidebar
426               site={siteView.site}
427               admins={siteRes.admins}
428               counts={siteView.counts}
429               online={siteRes.online}
430               showLocal={showLocal(this.isoData)}
431             />
432             {this.hasFollows && (
433               <div className="card border-secondary mb-3">
434                 <div className="card-body">{this.subscribedCommunities()}</div>
435               </div>
436             )}
437           </div>
438         )}
439       </div>
440     );
441   }
442
443   createCommunityButton() {
444     return (
445       <Link className="mt-2 btn btn-secondary btn-block" to="/create_community">
446         {i18n.t("create_a_community")}
447       </Link>
448     );
449   }
450
451   exploreCommunitiesButton() {
452     return (
453       <Link className="btn btn-secondary btn-block" to="/communities">
454         {i18n.t("explore_communities")}
455       </Link>
456     );
457   }
458
459   trendingCommunities() {
460     return (
461       <div>
462         <h5>
463           <T i18nKey="trending_communities">
464             #
465             <Link className="text-body" to="/communities">
466               #
467             </Link>
468           </T>
469         </h5>
470         <ul className="list-inline mb-0">
471           {this.state.trendingCommunities.map(cv => (
472             <li
473               key={cv.community.id}
474               className="list-inline-item d-inline-block"
475             >
476               <CommunityLink community={cv.community} />
477             </li>
478           ))}
479         </ul>
480       </div>
481     );
482   }
483
484   subscribedCommunities() {
485     return (
486       <div>
487         <h5>
488           <T class="d-inline" i18nKey="subscribed_to_communities">
489             #
490             <Link className="text-body" to="/communities">
491               #
492             </Link>
493           </T>
494           <button
495             className="btn btn-sm text-muted"
496             onClick={linkEvent(this, this.handleCollapseSubscribe)}
497             aria-label={i18n.t("collapse")}
498             data-tippy-content={i18n.t("collapse")}
499           >
500             {this.state.subscribedCollapsed ? (
501               <Icon icon="plus-square" classes="icon-inline" />
502             ) : (
503               <Icon icon="minus-square" classes="icon-inline" />
504             )}
505           </button>
506         </h5>
507         {!this.state.subscribedCollapsed && (
508           <ul className="list-inline mb-0">
509             {UserService.Instance.myUserInfo?.follows.map(cfv => (
510               <li
511                 key={cfv.community.id}
512                 className="list-inline-item d-inline-block"
513               >
514                 <CommunityLink community={cfv.community} />
515               </li>
516             ))}
517           </ul>
518         )}
519       </div>
520     );
521   }
522
523   updateUrl(paramUpdates: UrlParams) {
524     const listingTypeStr = paramUpdates.listingType || this.state.listingType;
525     const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
526     const sortStr = paramUpdates.sort || this.state.sort;
527     const page = paramUpdates.page || this.state.page;
528     this.props.history.push(
529       `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
530     );
531   }
532
533   posts() {
534     return (
535       <div className="main-content-wrapper">
536         {this.state.loading ? (
537           <h5>
538             <Spinner large />
539           </h5>
540         ) : (
541           <div>
542             {this.selects()}
543             {this.listings()}
544             <Paginator
545               page={this.state.page}
546               onChange={this.handlePageChange}
547             />
548           </div>
549         )}
550       </div>
551     );
552   }
553
554   listings() {
555     return this.state.dataType == DataType.Post ? (
556       <PostListings
557         posts={this.state.posts}
558         showCommunity
559         removeDuplicates
560         enableDownvotes={enableDownvotes(this.state.siteRes)}
561         enableNsfw={enableNsfw(this.state.siteRes)}
562         allLanguages={this.state.siteRes.all_languages}
563         siteLanguages={this.state.siteRes.discussion_languages}
564       />
565     ) : (
566       <CommentNodes
567         nodes={commentsToFlatNodes(this.state.comments)}
568         viewType={CommentViewType.Flat}
569         noIndent
570         showCommunity
571         showContext
572         enableDownvotes={enableDownvotes(this.state.siteRes)}
573         allLanguages={this.state.siteRes.all_languages}
574         siteLanguages={this.state.siteRes.discussion_languages}
575       />
576     );
577   }
578
579   selects() {
580     let allRss = `/feeds/all.xml?sort=${this.state.sort}`;
581     let localRss = `/feeds/local.xml?sort=${this.state.sort}`;
582     let auth = myAuth(false);
583     let frontRss = auth
584       ? `/feeds/front/${auth}.xml?sort=${this.state.sort}`
585       : undefined;
586
587     return (
588       <div className="mb-3">
589         <span className="mr-3">
590           <DataTypeSelect
591             type_={this.state.dataType}
592             onChange={this.handleDataTypeChange}
593           />
594         </span>
595         <span className="mr-3">
596           <ListingTypeSelect
597             type_={this.state.listingType}
598             showLocal={showLocal(this.isoData)}
599             showSubscribed
600             onChange={this.handleListingTypeChange}
601           />
602         </span>
603         <span className="mr-2">
604           <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
605         </span>
606         {this.state.listingType == ListingType.All && (
607           <>
608             <a href={allRss} rel={relTags} title="RSS">
609               <Icon icon="rss" classes="text-muted small" />
610             </a>
611             <link rel="alternate" type="application/atom+xml" href={allRss} />
612           </>
613         )}
614         {this.state.listingType == ListingType.Local && (
615           <>
616             <a href={localRss} rel={relTags} title="RSS">
617               <Icon icon="rss" classes="text-muted small" />
618             </a>
619             <link rel="alternate" type="application/atom+xml" href={localRss} />
620           </>
621         )}
622         {this.state.listingType == ListingType.Subscribed && frontRss && (
623           <>
624             <a href={frontRss} title="RSS" rel={relTags}>
625               <Icon icon="rss" classes="text-muted small" />
626             </a>
627             <link rel="alternate" type="application/atom+xml" href={frontRss} />
628           </>
629         )}
630       </div>
631     );
632   }
633
634   handleShowSubscribedMobile(i: Home) {
635     i.setState({ showSubscribedMobile: !i.state.showSubscribedMobile });
636   }
637
638   handleShowTrendingMobile(i: Home) {
639     i.setState({ showTrendingMobile: !i.state.showTrendingMobile });
640   }
641
642   handleShowSidebarMobile(i: Home) {
643     i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
644   }
645
646   handleCollapseSubscribe(i: Home) {
647     i.setState({ subscribedCollapsed: !i.state.subscribedCollapsed });
648   }
649
650   handlePageChange(page: number) {
651     this.updateUrl({ page });
652     window.scrollTo(0, 0);
653   }
654
655   handleSortChange(val: SortType) {
656     this.updateUrl({ sort: val, page: 1 });
657     window.scrollTo(0, 0);
658   }
659
660   handleListingTypeChange(val: ListingType) {
661     this.updateUrl({ listingType: val, page: 1 });
662     window.scrollTo(0, 0);
663   }
664
665   handleDataTypeChange(val: DataType) {
666     this.updateUrl({ dataType: DataType[val], page: 1 });
667     window.scrollTo(0, 0);
668   }
669
670   fetchData() {
671     let auth = myAuth(false);
672     if (this.state.dataType == DataType.Post) {
673       let getPostsForm: GetPosts = {
674         page: this.state.page,
675         limit: fetchLimit,
676         sort: this.state.sort,
677         saved_only: false,
678         type_: this.state.listingType,
679         auth,
680       };
681
682       WebSocketService.Instance.send(wsClient.getPosts(getPostsForm));
683     } else {
684       let getCommentsForm: GetComments = {
685         page: this.state.page,
686         limit: fetchLimit,
687         sort: postToCommentSortType(this.state.sort),
688         saved_only: false,
689         type_: this.state.listingType,
690         auth,
691       };
692       WebSocketService.Instance.send(wsClient.getComments(getCommentsForm));
693     }
694   }
695
696   parseMessage(msg: any) {
697     let op = wsUserOp(msg);
698     console.log(msg);
699     if (msg.error) {
700       toast(i18n.t(msg.error), "danger");
701       return;
702     } else if (msg.reconnect) {
703       WebSocketService.Instance.send(
704         wsClient.communityJoin({ community_id: 0 })
705       );
706       this.fetchData();
707     } else if (op == UserOperation.ListCommunities) {
708       let data = wsJsonToRes<ListCommunitiesResponse>(msg);
709       this.setState({ trendingCommunities: data.communities });
710     } else if (op == UserOperation.EditSite) {
711       let data = wsJsonToRes<SiteResponse>(msg);
712       this.setState(s => ((s.siteRes.site_view = data.site_view), s));
713       toast(i18n.t("site_saved"));
714     } else if (op == UserOperation.GetPosts) {
715       let data = wsJsonToRes<GetPostsResponse>(msg);
716       this.setState({ posts: data.posts, loading: false });
717       WebSocketService.Instance.send(
718         wsClient.communityJoin({ community_id: 0 })
719       );
720       restoreScrollPosition(this.context);
721       setupTippy();
722     } else if (op == UserOperation.CreatePost) {
723       let data = wsJsonToRes<PostResponse>(msg);
724       let mui = UserService.Instance.myUserInfo;
725
726       let showPostNotifs = mui?.local_user_view.local_user.show_new_post_notifs;
727
728       // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
729       if (
730         this.state.page == 1 &&
731         nsfwCheck(data.post_view) &&
732         !isPostBlocked(data.post_view)
733       ) {
734         // If you're on subscribed, only push it if you're subscribed.
735         if (this.state.listingType == ListingType.Subscribed) {
736           if (
737             mui?.follows
738               .map(c => c.community.id)
739               .includes(data.post_view.community.id)
740           ) {
741             this.state.posts.unshift(data.post_view);
742             if (showPostNotifs) {
743               notifyPost(data.post_view, this.context.router);
744             }
745           }
746         } else if (this.state.listingType == ListingType.Local) {
747           // If you're on the local view, only push it if its local
748           if (data.post_view.post.local) {
749             this.state.posts.unshift(data.post_view);
750             if (showPostNotifs) {
751               notifyPost(data.post_view, this.context.router);
752             }
753           }
754         } else {
755           this.state.posts.unshift(data.post_view);
756           if (showPostNotifs) {
757             notifyPost(data.post_view, this.context.router);
758           }
759         }
760         this.setState(this.state);
761       }
762     } else if (
763       op == UserOperation.EditPost ||
764       op == UserOperation.DeletePost ||
765       op == UserOperation.RemovePost ||
766       op == UserOperation.LockPost ||
767       op == UserOperation.FeaturePost ||
768       op == UserOperation.SavePost
769     ) {
770       let data = wsJsonToRes<PostResponse>(msg);
771       editPostFindRes(data.post_view, this.state.posts);
772       this.setState(this.state);
773     } else if (op == UserOperation.CreatePostLike) {
774       let data = wsJsonToRes<PostResponse>(msg);
775       createPostLikeFindRes(data.post_view, this.state.posts);
776       this.setState(this.state);
777     } else if (op == UserOperation.AddAdmin) {
778       let data = wsJsonToRes<AddAdminResponse>(msg);
779       this.setState(s => ((s.siteRes.admins = data.admins), s));
780     } else if (op == UserOperation.BanPerson) {
781       let data = wsJsonToRes<BanPersonResponse>(msg);
782       this.state.posts
783         .filter(p => p.creator.id == data.person_view.person.id)
784         .forEach(p => (p.creator.banned = data.banned));
785
786       this.setState(this.state);
787     } else if (op == UserOperation.GetComments) {
788       let data = wsJsonToRes<GetCommentsResponse>(msg);
789       this.setState({ comments: data.comments, loading: false });
790     } else if (
791       op == UserOperation.EditComment ||
792       op == UserOperation.DeleteComment ||
793       op == UserOperation.RemoveComment
794     ) {
795       let data = wsJsonToRes<CommentResponse>(msg);
796       editCommentRes(data.comment_view, this.state.comments);
797       this.setState(this.state);
798     } else if (op == UserOperation.CreateComment) {
799       let data = wsJsonToRes<CommentResponse>(msg);
800
801       // Necessary since it might be a user reply
802       if (data.form_id) {
803         // If you're on subscribed, only push it if you're subscribed.
804         if (this.state.listingType == ListingType.Subscribed) {
805           if (
806             UserService.Instance.myUserInfo?.follows
807               .map(c => c.community.id)
808               .includes(data.comment_view.community.id)
809           ) {
810             this.state.comments.unshift(data.comment_view);
811           }
812         } else {
813           this.state.comments.unshift(data.comment_view);
814         }
815         this.setState(this.state);
816       }
817     } else if (op == UserOperation.SaveComment) {
818       let data = wsJsonToRes<CommentResponse>(msg);
819       saveCommentRes(data.comment_view, this.state.comments);
820       this.setState(this.state);
821     } else if (op == UserOperation.CreateCommentLike) {
822       let data = wsJsonToRes<CommentResponse>(msg);
823       createCommentLikeRes(data.comment_view, this.state.comments);
824       this.setState(this.state);
825     } else if (op == UserOperation.BlockPerson) {
826       let data = wsJsonToRes<BlockPersonResponse>(msg);
827       updatePersonBlock(data);
828     } else if (op == UserOperation.CreatePostReport) {
829       let data = wsJsonToRes<PostReportResponse>(msg);
830       if (data) {
831         toast(i18n.t("report_created"));
832       }
833     } else if (op == UserOperation.CreateCommentReport) {
834       let data = wsJsonToRes<CommentReportResponse>(msg);
835       if (data) {
836         toast(i18n.t("report_created"));
837       }
838     } else if (
839       op == UserOperation.PurgePerson ||
840       op == UserOperation.PurgePost ||
841       op == UserOperation.PurgeComment ||
842       op == UserOperation.PurgeCommunity
843     ) {
844       let data = wsJsonToRes<PurgeItemResponse>(msg);
845       if (data.success) {
846         toast(i18n.t("purge_success"));
847         this.context.router.history.push(`/`);
848       }
849     }
850   }
851 }