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