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