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