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