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