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