]> Untitled Git - lemmy-ui.git/blob - src/shared/components/home/home.tsx
741dfa57aa7576eb0138bf1d687ab2773ae4a70d
[lemmy-ui.git] / src / shared / components / home / home.tsx
1 import {
2   commentsToFlatNodes,
3   editComment,
4   editPost,
5   editWith,
6   enableDownvotes,
7   enableNsfw,
8   getCommentParentId,
9   getDataTypeString,
10   myAuth,
11   postToCommentSortType,
12   setIsoData,
13   showLocal,
14   updatePersonBlock,
15 } from "@utils/app";
16 import {
17   getPageFromString,
18   getQueryParams,
19   getQueryString,
20   getRandomFromList,
21 } from "@utils/helpers";
22 import { canCreateCommunity } from "@utils/roles";
23 import type { QueryParams } from "@utils/types";
24 import { RouteDataResponse } from "@utils/types";
25 import { NoOptionI18nKeys } from "i18next";
26 import { Component, MouseEventHandler, linkEvent } from "inferno";
27 import { T } from "inferno-i18next-dess";
28 import { Link } from "inferno-router";
29 import {
30   AddAdmin,
31   AddModToCommunity,
32   BanFromCommunity,
33   BanFromCommunityResponse,
34   BanPerson,
35   BanPersonResponse,
36   BlockPerson,
37   CommentId,
38   CommentReplyResponse,
39   CommentResponse,
40   CreateComment,
41   CreateCommentLike,
42   CreateCommentReport,
43   CreatePostLike,
44   CreatePostReport,
45   DeleteComment,
46   DeletePost,
47   DistinguishComment,
48   EditComment,
49   EditPost,
50   FeaturePost,
51   GetComments,
52   GetCommentsResponse,
53   GetPosts,
54   GetPostsResponse,
55   GetSiteResponse,
56   ListCommunities,
57   ListCommunitiesResponse,
58   ListingType,
59   LockPost,
60   MarkCommentReplyAsRead,
61   MarkPersonMentionAsRead,
62   PostResponse,
63   PurgeComment,
64   PurgeItemResponse,
65   PurgePerson,
66   PurgePost,
67   RemoveComment,
68   RemovePost,
69   SaveComment,
70   SavePost,
71   SortType,
72   TransferCommunity,
73 } from "lemmy-js-client";
74 import { fetchLimit, relTags, trendingFetchLimit } from "../../config";
75 import {
76   CommentViewType,
77   DataType,
78   InitialFetchRequest,
79 } from "../../interfaces";
80 import { mdToHtml } from "../../markdown";
81 import {
82   FirstLoadService,
83   HomeCacheService,
84   I18NextService,
85   UserService,
86 } from "../../services";
87 import { HttpService, RequestState } from "../../services/HttpService";
88 import { setupTippy } from "../../tippy";
89 import { toast } from "../../toast";
90 import { CommentNodes } from "../comment/comment-nodes";
91 import { DataTypeSelect } from "../common/data-type-select";
92 import { HtmlTags } from "../common/html-tags";
93 import { Icon, Spinner } from "../common/icon";
94 import { ListingTypeSelect } from "../common/listing-type-select";
95 import { Paginator } from "../common/paginator";
96 import { SortSelect } from "../common/sort-select";
97 import { CommunityLink } from "../community/community-link";
98 import { PostListings } from "../post/post-listings";
99 import { SiteSidebar } from "./site-sidebar";
100
101 interface HomeState {
102   postsRes: RequestState<GetPostsResponse>;
103   commentsRes: RequestState<GetCommentsResponse>;
104   trendingCommunitiesRes: RequestState<ListCommunitiesResponse>;
105   showSubscribedMobile: boolean;
106   showTrendingMobile: boolean;
107   showSidebarMobile: boolean;
108   subscribedCollapsed: boolean;
109   scrolled: boolean;
110   tagline?: string;
111   siteRes: GetSiteResponse;
112   finished: Map<CommentId, boolean | undefined>;
113   isIsomorphic: boolean;
114 }
115
116 interface HomeProps {
117   listingType?: ListingType;
118   dataType: DataType;
119   sort: SortType;
120   page: number;
121 }
122
123 type HomeData = RouteDataResponse<{
124   postsRes: GetPostsResponse;
125   commentsRes: GetCommentsResponse;
126   trendingCommunitiesRes: ListCommunitiesResponse;
127 }>;
128
129 function getRss(listingType: ListingType) {
130   const { sort } = getHomeQueryParams();
131   const auth = myAuth();
132
133   let rss: string | undefined = undefined;
134
135   switch (listingType) {
136     case "All": {
137       rss = `/feeds/all.xml?sort=${sort}`;
138       break;
139     }
140     case "Local": {
141       rss = `/feeds/local.xml?sort=${sort}`;
142       break;
143     }
144     case "Subscribed": {
145       rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
146       break;
147     }
148   }
149
150   return (
151     rss && (
152       <>
153         <a href={rss} rel={relTags} title="RSS">
154           <Icon icon="rss" classes="text-muted small" />
155         </a>
156         <link rel="alternate" type="application/atom+xml" href={rss} />
157       </>
158     )
159   );
160 }
161
162 function getDataTypeFromQuery(type?: string): DataType {
163   return type ? DataType[type] : DataType.Post;
164 }
165
166 function getListingTypeFromQuery(type?: string): ListingType | undefined {
167   const myListingType =
168     UserService.Instance.myUserInfo?.local_user_view?.local_user
169       ?.default_listing_type;
170
171   return type ? (type as ListingType) : myListingType;
172 }
173
174 function getSortTypeFromQuery(type?: string): SortType {
175   const mySortType =
176     UserService.Instance.myUserInfo?.local_user_view?.local_user
177       ?.default_sort_type;
178
179   return (type ? (type as SortType) : mySortType) ?? "Active";
180 }
181
182 const getHomeQueryParams = () =>
183   getQueryParams<HomeProps>({
184     sort: getSortTypeFromQuery,
185     listingType: getListingTypeFromQuery,
186     page: getPageFromString,
187     dataType: getDataTypeFromQuery,
188   });
189
190 const MobileButton = ({
191   textKey,
192   show,
193   onClick,
194 }: {
195   textKey: NoOptionI18nKeys;
196   show: boolean;
197   onClick: MouseEventHandler<HTMLButtonElement>;
198 }) => (
199   <button
200     className="btn btn-secondary d-inline-block mb-2 me-3"
201     onClick={onClick}
202   >
203     {I18NextService.i18n.t(textKey)}{" "}
204     <Icon icon={show ? `minus-square` : `plus-square`} classes="icon-inline" />
205   </button>
206 );
207
208 const LinkButton = ({
209   path,
210   translationKey,
211 }: {
212   path: string;
213   translationKey: NoOptionI18nKeys;
214 }) => (
215   <Link className="btn btn-secondary d-block" to={path}>
216     {I18NextService.i18n.t(translationKey)}
217   </Link>
218 );
219
220 export class Home extends Component<any, HomeState> {
221   private isoData = setIsoData<HomeData>(this.context);
222   state: HomeState = {
223     postsRes: { state: "empty" },
224     commentsRes: { state: "empty" },
225     trendingCommunitiesRes: { state: "empty" },
226     scrolled: true,
227     siteRes: this.isoData.site_res,
228     showSubscribedMobile: false,
229     showTrendingMobile: false,
230     showSidebarMobile: false,
231     subscribedCollapsed: false,
232     finished: new Map(),
233     isIsomorphic: false,
234   };
235
236   constructor(props: any, context: any) {
237     super(props, context);
238
239     this.handleSortChange = this.handleSortChange.bind(this);
240     this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
241     this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
242     this.handlePageChange = this.handlePageChange.bind(this);
243
244     this.handleCreateComment = this.handleCreateComment.bind(this);
245     this.handleEditComment = this.handleEditComment.bind(this);
246     this.handleSaveComment = this.handleSaveComment.bind(this);
247     this.handleBlockPerson = this.handleBlockPerson.bind(this);
248     this.handleDeleteComment = this.handleDeleteComment.bind(this);
249     this.handleRemoveComment = this.handleRemoveComment.bind(this);
250     this.handleCommentVote = this.handleCommentVote.bind(this);
251     this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
252     this.handleAddAdmin = this.handleAddAdmin.bind(this);
253     this.handlePurgePerson = this.handlePurgePerson.bind(this);
254     this.handlePurgeComment = this.handlePurgeComment.bind(this);
255     this.handleCommentReport = this.handleCommentReport.bind(this);
256     this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
257     this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
258     this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
259     this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
260     this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
261     this.handleBanPerson = this.handleBanPerson.bind(this);
262     this.handlePostEdit = this.handlePostEdit.bind(this);
263     this.handlePostVote = this.handlePostVote.bind(this);
264     this.handlePostReport = this.handlePostReport.bind(this);
265     this.handleLockPost = this.handleLockPost.bind(this);
266     this.handleDeletePost = this.handleDeletePost.bind(this);
267     this.handleRemovePost = this.handleRemovePost.bind(this);
268     this.handleSavePost = this.handleSavePost.bind(this);
269     this.handlePurgePost = this.handlePurgePost.bind(this);
270     this.handleFeaturePost = this.handleFeaturePost.bind(this);
271
272     // Only fetch the data if coming from another route
273     if (FirstLoadService.isFirstLoad) {
274       const { trendingCommunitiesRes, commentsRes, postsRes } =
275         this.isoData.routeData;
276
277       this.state = {
278         ...this.state,
279         trendingCommunitiesRes,
280         commentsRes,
281         postsRes,
282         isIsomorphic: true,
283       };
284
285       HomeCacheService.postsRes = postsRes;
286     }
287
288     this.state.tagline = getRandomFromList(
289       this.state?.siteRes?.taglines ?? []
290     )?.content;
291   }
292
293   componentWillUnmount() {
294     HomeCacheService.activate();
295   }
296
297   async componentDidMount() {
298     if (
299       !this.state.isIsomorphic ||
300       !Object.values(this.isoData.routeData).some(
301         res => res.state === "success" || res.state === "failed"
302       )
303     ) {
304       await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
305     }
306
307     setupTippy();
308   }
309
310   static async fetchInitialData({
311     client,
312     auth,
313     query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
314     site,
315   }: InitialFetchRequest<QueryParams<HomeProps>>): Promise<HomeData> {
316     const dataType = getDataTypeFromQuery(urlDataType);
317     const type_ =
318       getListingTypeFromQuery(listingType) ??
319       site.site_view.local_site.default_post_listing_type;
320     const sort = getSortTypeFromQuery(urlSort);
321
322     const page = urlPage ? Number(urlPage) : 1;
323
324     let postsRes: RequestState<GetPostsResponse> = { state: "empty" };
325     let commentsRes: RequestState<GetCommentsResponse> = {
326       state: "empty",
327     };
328
329     if (dataType === DataType.Post) {
330       const getPostsForm: GetPosts = {
331         type_,
332         page,
333         limit: fetchLimit,
334         sort,
335         saved_only: false,
336         auth,
337       };
338
339       postsRes = await client.getPosts(getPostsForm);
340     } else {
341       const getCommentsForm: GetComments = {
342         page,
343         limit: fetchLimit,
344         sort: postToCommentSortType(sort),
345         type_,
346         saved_only: false,
347         auth,
348       };
349
350       commentsRes = await client.getComments(getCommentsForm);
351     }
352
353     const trendingCommunitiesForm: ListCommunities = {
354       type_: "Local",
355       sort: "Hot",
356       limit: trendingFetchLimit,
357       auth,
358     };
359
360     return {
361       trendingCommunitiesRes: await client.listCommunities(
362         trendingCommunitiesForm
363       ),
364       commentsRes,
365       postsRes,
366     };
367   }
368
369   get documentTitle(): string {
370     const { name, description } = this.state.siteRes.site_view.site;
371
372     return description ? `${name} - ${description}` : name;
373   }
374
375   render() {
376     const {
377       tagline,
378       siteRes: {
379         site_view: {
380           local_site: { site_setup },
381         },
382       },
383     } = this.state;
384
385     return (
386       <div className="home container-lg">
387         <HtmlTags
388           title={this.documentTitle}
389           path={this.context.router.route.match.url}
390         />
391         {site_setup && (
392           <div className="row">
393             <main role="main" className="col-12 col-md-8 col-lg-9">
394               {tagline && (
395                 <div
396                   id="tagline"
397                   dangerouslySetInnerHTML={mdToHtml(tagline)}
398                 ></div>
399               )}
400               <div className="d-block d-md-none">{this.mobileView}</div>
401               {this.posts}
402             </main>
403             <aside className="d-none d-md-block col-md-4 col-lg-3">
404               {this.mySidebar}
405             </aside>
406           </div>
407         )}
408       </div>
409     );
410   }
411
412   get hasFollows(): boolean {
413     const mui = UserService.Instance.myUserInfo;
414     return !!mui && mui.follows.length > 0;
415   }
416
417   get mobileView() {
418     const {
419       siteRes: {
420         site_view: { counts, site },
421         admins,
422       },
423       showSubscribedMobile,
424       showTrendingMobile,
425       showSidebarMobile,
426     } = this.state;
427
428     return (
429       <div className="row">
430         <div className="col-12">
431           {this.hasFollows && (
432             <MobileButton
433               textKey="subscribed"
434               show={showSubscribedMobile}
435               onClick={linkEvent(this, this.handleShowSubscribedMobile)}
436             />
437           )}
438           <MobileButton
439             textKey="trending"
440             show={showTrendingMobile}
441             onClick={linkEvent(this, this.handleShowTrendingMobile)}
442           />
443           <MobileButton
444             textKey="sidebar"
445             show={showSidebarMobile}
446             onClick={linkEvent(this, this.handleShowSidebarMobile)}
447           />
448           {showSidebarMobile && (
449             <SiteSidebar
450               site={site}
451               admins={admins}
452               counts={counts}
453               showLocal={showLocal(this.isoData)}
454               isMobile={true}
455             />
456           )}
457           {showTrendingMobile && (
458             <div className="card border-secondary mb-3">
459               {this.trendingCommunities()}
460             </div>
461           )}
462           {showSubscribedMobile && (
463             <div className="card border-secondary mb-3">
464               {this.subscribedCommunities(true)}
465             </div>
466           )}
467         </div>
468       </div>
469     );
470   }
471
472   get mySidebar() {
473     const {
474       siteRes: {
475         site_view: { counts, site },
476         admins,
477       },
478     } = this.state;
479
480     return (
481       <div id="sidebarContainer">
482         <section id="sidebarMain" className="card border-secondary mb-3">
483           {this.trendingCommunities()}
484         </section>
485         <SiteSidebar
486           site={site}
487           admins={admins}
488           counts={counts}
489           showLocal={showLocal(this.isoData)}
490         />
491         {this.hasFollows && (
492           <div className="accordion">
493             <section
494               id="sidebarSubscribed"
495               className="card border-secondary mb-3"
496             >
497               {this.subscribedCommunities(false)}
498             </section>
499           </div>
500         )}
501       </div>
502     );
503   }
504
505   trendingCommunities() {
506     switch (this.state.trendingCommunitiesRes?.state) {
507       case "loading":
508         return (
509           <h5>
510             <Spinner large />
511           </h5>
512         );
513       case "success": {
514         const trending = this.state.trendingCommunitiesRes.data.communities;
515         return (
516           <>
517             <header className="card-header d-flex align-items-center">
518               <h5 className="mb-0">
519                 <T i18nKey="trending_communities">
520                   #
521                   <Link className="text-body" to="/communities">
522                     #
523                   </Link>
524                 </T>
525               </h5>
526             </header>
527             <div className="card-body">
528               {trending.length > 0 && (
529                 <ul className="list-inline">
530                   {trending.map(cv => (
531                     <li key={cv.community.id} className="list-inline-item">
532                       <CommunityLink community={cv.community} />
533                     </li>
534                   ))}
535                 </ul>
536               )}
537               {canCreateCommunity(this.state.siteRes) && (
538                 <LinkButton
539                   path="/create_community"
540                   translationKey="create_a_community"
541                 />
542               )}
543               <LinkButton
544                 path="/communities"
545                 translationKey="explore_communities"
546               />
547             </div>
548           </>
549         );
550       }
551     }
552   }
553
554   subscribedCommunities(isMobile = false) {
555     const { subscribedCollapsed } = this.state;
556
557     return (
558       <>
559         <header
560           className="card-header d-flex align-items-center"
561           id="sidebarSubscribedHeader"
562         >
563           <h5 className="mb-0 d-inline">
564             <T class="d-inline" i18nKey="subscribed_to_communities">
565               #
566               <Link className="text-body" to="/communities">
567                 #
568               </Link>
569             </T>
570           </h5>
571           {!isMobile && (
572             <button
573               type="button"
574               className="btn btn-sm text-muted"
575               onClick={linkEvent(this, this.handleCollapseSubscribe)}
576               aria-label={
577                 subscribedCollapsed
578                   ? I18NextService.i18n.t("expand")
579                   : I18NextService.i18n.t("collapse")
580               }
581               data-tippy-content={
582                 subscribedCollapsed
583                   ? I18NextService.i18n.t("expand")
584                   : I18NextService.i18n.t("collapse")
585               }
586               aria-expanded="true"
587               aria-controls="sidebarSubscribedBody"
588             >
589               <Icon
590                 icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
591                 classes="icon-inline"
592               />
593             </button>
594           )}
595         </header>
596         {!subscribedCollapsed && (
597           <div
598             id="sidebarSubscribedBody"
599             aria-labelledby="sidebarSubscribedHeader"
600           >
601             <div className="card-body">
602               <ul className="list-inline mb-0">
603                 {UserService.Instance.myUserInfo?.follows.map(cfv => (
604                   <li
605                     key={cfv.community.id}
606                     className="list-inline-item d-inline-block"
607                   >
608                     <CommunityLink community={cfv.community} />
609                   </li>
610                 ))}
611               </ul>
612             </div>
613           </div>
614         )}
615       </>
616     );
617   }
618
619   async updateUrl({ dataType, listingType, page, sort }: Partial<HomeProps>) {
620     const {
621       dataType: urlDataType,
622       listingType: urlListingType,
623       page: urlPage,
624       sort: urlSort,
625     } = getHomeQueryParams();
626
627     const queryParams: QueryParams<HomeProps> = {
628       dataType: getDataTypeString(dataType ?? urlDataType),
629       listingType: listingType ?? urlListingType,
630       page: (page ?? urlPage).toString(),
631       sort: sort ?? urlSort,
632     };
633
634     this.props.history.push({
635       pathname: "/",
636       search: getQueryString(queryParams),
637     });
638
639     if (!this.state.scrolled) {
640       this.setState({ scrolled: true });
641       setTimeout(() => window.scrollTo(0, 0), 0);
642     }
643
644     await this.fetchData();
645   }
646
647   get posts() {
648     const { page } = getHomeQueryParams();
649
650     return (
651       <div className="main-content-wrapper">
652         <div>
653           {this.selects}
654           {this.listings}
655           <Paginator page={page} onChange={this.handlePageChange} />
656         </div>
657       </div>
658     );
659   }
660
661   get listings() {
662     const { dataType } = getHomeQueryParams();
663     const siteRes = this.state.siteRes;
664
665     if (dataType === DataType.Post) {
666       switch (this.state.postsRes?.state) {
667         case "empty":
668           return <div style="min-height: 20000px;"></div>;
669         case "loading":
670           return (
671             <h5>
672               <Spinner large />
673             </h5>
674           );
675         case "success": {
676           const posts = this.state.postsRes.data.posts;
677           return (
678             <PostListings
679               posts={posts}
680               showCommunity
681               removeDuplicates
682               enableDownvotes={enableDownvotes(siteRes)}
683               enableNsfw={enableNsfw(siteRes)}
684               allLanguages={siteRes.all_languages}
685               siteLanguages={siteRes.discussion_languages}
686               onBlockPerson={this.handleBlockPerson}
687               onPostEdit={this.handlePostEdit}
688               onPostVote={this.handlePostVote}
689               onPostReport={this.handlePostReport}
690               onLockPost={this.handleLockPost}
691               onDeletePost={this.handleDeletePost}
692               onRemovePost={this.handleRemovePost}
693               onSavePost={this.handleSavePost}
694               onPurgePerson={this.handlePurgePerson}
695               onPurgePost={this.handlePurgePost}
696               onBanPerson={this.handleBanPerson}
697               onBanPersonFromCommunity={this.handleBanFromCommunity}
698               onAddModToCommunity={this.handleAddModToCommunity}
699               onAddAdmin={this.handleAddAdmin}
700               onTransferCommunity={this.handleTransferCommunity}
701               onFeaturePost={this.handleFeaturePost}
702             />
703           );
704         }
705       }
706     } else {
707       switch (this.state.commentsRes.state) {
708         case "loading":
709           return (
710             <h5>
711               <Spinner large />
712             </h5>
713           );
714         case "success": {
715           const comments = this.state.commentsRes.data.comments;
716           return (
717             <CommentNodes
718               nodes={commentsToFlatNodes(comments)}
719               viewType={CommentViewType.Flat}
720               finished={this.state.finished}
721               noIndent
722               showCommunity
723               showContext
724               enableDownvotes={enableDownvotes(siteRes)}
725               allLanguages={siteRes.all_languages}
726               siteLanguages={siteRes.discussion_languages}
727               onSaveComment={this.handleSaveComment}
728               onBlockPerson={this.handleBlockPerson}
729               onDeleteComment={this.handleDeleteComment}
730               onRemoveComment={this.handleRemoveComment}
731               onCommentVote={this.handleCommentVote}
732               onCommentReport={this.handleCommentReport}
733               onDistinguishComment={this.handleDistinguishComment}
734               onAddModToCommunity={this.handleAddModToCommunity}
735               onAddAdmin={this.handleAddAdmin}
736               onTransferCommunity={this.handleTransferCommunity}
737               onPurgeComment={this.handlePurgeComment}
738               onPurgePerson={this.handlePurgePerson}
739               onCommentReplyRead={this.handleCommentReplyRead}
740               onPersonMentionRead={this.handlePersonMentionRead}
741               onBanPersonFromCommunity={this.handleBanFromCommunity}
742               onBanPerson={this.handleBanPerson}
743               onCreateComment={this.handleCreateComment}
744               onEditComment={this.handleEditComment}
745             />
746           );
747         }
748       }
749     }
750   }
751
752   get selects() {
753     const { listingType, dataType, sort } = getHomeQueryParams();
754
755     return (
756       <div className="row align-items-center mb-3 g-3">
757         <div className="col-auto">
758           <DataTypeSelect
759             type_={dataType}
760             onChange={this.handleDataTypeChange}
761           />
762         </div>
763         <div className="col-auto">
764           <ListingTypeSelect
765             type_={
766               listingType ??
767               this.state.siteRes.site_view.local_site.default_post_listing_type
768             }
769             showLocal={showLocal(this.isoData)}
770             showSubscribed
771             onChange={this.handleListingTypeChange}
772           />
773         </div>
774         <div className="col-auto">
775           <SortSelect sort={sort} onChange={this.handleSortChange} />
776         </div>
777         <div className="col-auto ps-0">
778           {getRss(
779             listingType ??
780               this.state.siteRes.site_view.local_site.default_post_listing_type
781           )}
782         </div>
783       </div>
784     );
785   }
786
787   async fetchTrendingCommunities() {
788     this.setState({ trendingCommunitiesRes: { state: "loading" } });
789     this.setState({
790       trendingCommunitiesRes: await HttpService.client.listCommunities({
791         type_: "Local",
792         sort: "Hot",
793         limit: trendingFetchLimit,
794         auth: myAuth(),
795       }),
796     });
797   }
798
799   async fetchData() {
800     const auth = myAuth();
801     const { dataType, page, listingType, sort } = getHomeQueryParams();
802
803     if (dataType === DataType.Post) {
804       if (HomeCacheService.active) {
805         const { postsRes, scrollY } = HomeCacheService;
806         HomeCacheService.deactivate();
807         this.setState({ postsRes });
808         window.scrollTo({
809           left: 0,
810           top: scrollY,
811           behavior: "instant",
812         });
813       } else {
814         this.setState({ postsRes: { state: "loading" } });
815         this.setState({
816           postsRes: await HttpService.client.getPosts({
817             page,
818             limit: fetchLimit,
819             sort,
820             saved_only: false,
821             type_: listingType,
822             auth,
823           }),
824         });
825
826         HomeCacheService.postsRes = this.state.postsRes;
827       }
828     } else {
829       this.setState({ commentsRes: { state: "loading" } });
830       this.setState({
831         commentsRes: await HttpService.client.getComments({
832           page,
833           limit: fetchLimit,
834           sort: postToCommentSortType(sort),
835           saved_only: false,
836           type_: listingType,
837           auth,
838         }),
839       });
840     }
841
842     setupTippy();
843   }
844
845   handleShowSubscribedMobile(i: Home) {
846     i.setState({ showSubscribedMobile: !i.state.showSubscribedMobile });
847   }
848
849   handleShowTrendingMobile(i: Home) {
850     i.setState({ showTrendingMobile: !i.state.showTrendingMobile });
851   }
852
853   handleShowSidebarMobile(i: Home) {
854     i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
855   }
856
857   handleCollapseSubscribe(i: Home) {
858     i.setState({ subscribedCollapsed: !i.state.subscribedCollapsed });
859   }
860
861   handlePageChange(page: number) {
862     this.setState({ scrolled: false });
863     this.updateUrl({ page });
864   }
865
866   handleSortChange(val: SortType) {
867     this.setState({ scrolled: false });
868     this.updateUrl({ sort: val, page: 1 });
869   }
870
871   handleListingTypeChange(val: ListingType) {
872     this.setState({ scrolled: false });
873     this.updateUrl({ listingType: val, page: 1 });
874   }
875
876   handleDataTypeChange(val: DataType) {
877     this.setState({ scrolled: false });
878     this.updateUrl({ dataType: val, page: 1 });
879   }
880
881   async handleAddModToCommunity(form: AddModToCommunity) {
882     // TODO not sure what to do here
883     await HttpService.client.addModToCommunity(form);
884   }
885
886   async handlePurgePerson(form: PurgePerson) {
887     const purgePersonRes = await HttpService.client.purgePerson(form);
888     this.purgeItem(purgePersonRes);
889   }
890
891   async handlePurgeComment(form: PurgeComment) {
892     const purgeCommentRes = await HttpService.client.purgeComment(form);
893     this.purgeItem(purgeCommentRes);
894   }
895
896   async handlePurgePost(form: PurgePost) {
897     const purgeRes = await HttpService.client.purgePost(form);
898     this.purgeItem(purgeRes);
899   }
900
901   async handleBlockPerson(form: BlockPerson) {
902     const blockPersonRes = await HttpService.client.blockPerson(form);
903     if (blockPersonRes.state == "success") {
904       updatePersonBlock(blockPersonRes.data);
905     }
906   }
907
908   async handleCreateComment(form: CreateComment) {
909     const createCommentRes = await HttpService.client.createComment(form);
910     this.createAndUpdateComments(createCommentRes);
911
912     return createCommentRes;
913   }
914
915   async handleEditComment(form: EditComment) {
916     const editCommentRes = await HttpService.client.editComment(form);
917     this.findAndUpdateComment(editCommentRes);
918
919     return editCommentRes;
920   }
921
922   async handleDeleteComment(form: DeleteComment) {
923     const deleteCommentRes = await HttpService.client.deleteComment(form);
924     this.findAndUpdateComment(deleteCommentRes);
925   }
926
927   async handleDeletePost(form: DeletePost) {
928     const deleteRes = await HttpService.client.deletePost(form);
929     this.findAndUpdatePost(deleteRes);
930   }
931
932   async handleRemovePost(form: RemovePost) {
933     const removeRes = await HttpService.client.removePost(form);
934     this.findAndUpdatePost(removeRes);
935   }
936
937   async handleRemoveComment(form: RemoveComment) {
938     const removeCommentRes = await HttpService.client.removeComment(form);
939     this.findAndUpdateComment(removeCommentRes);
940   }
941
942   async handleSaveComment(form: SaveComment) {
943     const saveCommentRes = await HttpService.client.saveComment(form);
944     this.findAndUpdateComment(saveCommentRes);
945   }
946
947   async handleSavePost(form: SavePost) {
948     const saveRes = await HttpService.client.savePost(form);
949     this.findAndUpdatePost(saveRes);
950   }
951
952   async handleFeaturePost(form: FeaturePost) {
953     const featureRes = await HttpService.client.featurePost(form);
954     this.findAndUpdatePost(featureRes);
955   }
956
957   async handleCommentVote(form: CreateCommentLike) {
958     const voteRes = await HttpService.client.likeComment(form);
959     this.findAndUpdateComment(voteRes);
960   }
961
962   async handlePostEdit(form: EditPost) {
963     const res = await HttpService.client.editPost(form);
964     this.findAndUpdatePost(res);
965   }
966
967   async handlePostVote(form: CreatePostLike) {
968     const voteRes = await HttpService.client.likePost(form);
969     this.findAndUpdatePost(voteRes);
970   }
971
972   async handleCommentReport(form: CreateCommentReport) {
973     const reportRes = await HttpService.client.createCommentReport(form);
974     if (reportRes.state == "success") {
975       toast(I18NextService.i18n.t("report_created"));
976     }
977   }
978
979   async handlePostReport(form: CreatePostReport) {
980     const reportRes = await HttpService.client.createPostReport(form);
981     if (reportRes.state == "success") {
982       toast(I18NextService.i18n.t("report_created"));
983     }
984   }
985
986   async handleLockPost(form: LockPost) {
987     const lockRes = await HttpService.client.lockPost(form);
988     this.findAndUpdatePost(lockRes);
989   }
990
991   async handleDistinguishComment(form: DistinguishComment) {
992     const distinguishRes = await HttpService.client.distinguishComment(form);
993     this.findAndUpdateComment(distinguishRes);
994   }
995
996   async handleAddAdmin(form: AddAdmin) {
997     const addAdminRes = await HttpService.client.addAdmin(form);
998
999     if (addAdminRes.state == "success") {
1000       this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
1001     }
1002   }
1003
1004   async handleTransferCommunity(form: TransferCommunity) {
1005     await HttpService.client.transferCommunity(form);
1006     toast(I18NextService.i18n.t("transfer_community"));
1007   }
1008
1009   async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
1010     const readRes = await HttpService.client.markCommentReplyAsRead(form);
1011     this.findAndUpdateCommentReply(readRes);
1012   }
1013
1014   async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
1015     // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
1016     await HttpService.client.markPersonMentionAsRead(form);
1017   }
1018
1019   async handleBanFromCommunity(form: BanFromCommunity) {
1020     const banRes = await HttpService.client.banFromCommunity(form);
1021     this.updateBanFromCommunity(banRes);
1022   }
1023
1024   async handleBanPerson(form: BanPerson) {
1025     const banRes = await HttpService.client.banPerson(form);
1026     this.updateBan(banRes);
1027   }
1028
1029   updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
1030     // Maybe not necessary
1031     if (banRes.state == "success") {
1032       this.setState(s => {
1033         if (s.postsRes.state == "success") {
1034           s.postsRes.data.posts
1035             .filter(c => c.creator.id == banRes.data.person_view.person.id)
1036             .forEach(
1037               c => (c.creator_banned_from_community = banRes.data.banned)
1038             );
1039         }
1040         if (s.commentsRes.state == "success") {
1041           s.commentsRes.data.comments
1042             .filter(c => c.creator.id == banRes.data.person_view.person.id)
1043             .forEach(
1044               c => (c.creator_banned_from_community = banRes.data.banned)
1045             );
1046         }
1047         return s;
1048       });
1049     }
1050   }
1051
1052   updateBan(banRes: RequestState<BanPersonResponse>) {
1053     // Maybe not necessary
1054     if (banRes.state == "success") {
1055       this.setState(s => {
1056         if (s.postsRes.state == "success") {
1057           s.postsRes.data.posts
1058             .filter(c => c.creator.id == banRes.data.person_view.person.id)
1059             .forEach(c => (c.creator.banned = banRes.data.banned));
1060         }
1061         if (s.commentsRes.state == "success") {
1062           s.commentsRes.data.comments
1063             .filter(c => c.creator.id == banRes.data.person_view.person.id)
1064             .forEach(c => (c.creator.banned = banRes.data.banned));
1065         }
1066         return s;
1067       });
1068     }
1069   }
1070
1071   purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
1072     if (purgeRes.state == "success") {
1073       toast(I18NextService.i18n.t("purge_success"));
1074       this.context.router.history.push(`/`);
1075     }
1076   }
1077
1078   findAndUpdateComment(res: RequestState<CommentResponse>) {
1079     this.setState(s => {
1080       if (s.commentsRes.state == "success" && res.state == "success") {
1081         s.commentsRes.data.comments = editComment(
1082           res.data.comment_view,
1083           s.commentsRes.data.comments
1084         );
1085         s.finished.set(res.data.comment_view.comment.id, true);
1086       }
1087       return s;
1088     });
1089   }
1090
1091   createAndUpdateComments(res: RequestState<CommentResponse>) {
1092     this.setState(s => {
1093       if (s.commentsRes.state == "success" && res.state == "success") {
1094         s.commentsRes.data.comments.unshift(res.data.comment_view);
1095
1096         // Set finished for the parent
1097         s.finished.set(
1098           getCommentParentId(res.data.comment_view.comment) ?? 0,
1099           true
1100         );
1101       }
1102       return s;
1103     });
1104   }
1105
1106   findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
1107     this.setState(s => {
1108       if (s.commentsRes.state == "success" && res.state == "success") {
1109         s.commentsRes.data.comments = editWith(
1110           res.data.comment_reply_view,
1111           s.commentsRes.data.comments
1112         );
1113       }
1114       return s;
1115     });
1116   }
1117
1118   findAndUpdatePost(res: RequestState<PostResponse>) {
1119     this.setState(s => {
1120       if (s.postsRes.state == "success" && res.state == "success") {
1121         s.postsRes.data.posts = editPost(
1122           res.data.post_view,
1123           s.postsRes.data.posts
1124         );
1125       }
1126       return s;
1127     });
1128   }
1129 }