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