]> Untitled Git - lemmy-ui.git/blob - src/shared/components/post/post.tsx
49e7348608960a9d12478bc04b152b9e0cb97c3e
[lemmy-ui.git] / src / shared / components / post / post.tsx
1 import {
2   buildCommentsTree,
3   commentsToFlatNodes,
4   editComment,
5   editWith,
6   enableDownvotes,
7   enableNsfw,
8   getCommentIdFromProps,
9   getCommentParentId,
10   getDepthFromComment,
11   getIdFromProps,
12   myAuth,
13   setIsoData,
14   updateCommunityBlock,
15   updatePersonBlock,
16 } from "@utils/app";
17 import {
18   isBrowser,
19   restoreScrollPosition,
20   saveScrollPosition,
21 } from "@utils/browser";
22 import { debounce, randomStr } from "@utils/helpers";
23 import { isImage } from "@utils/media";
24 import { RouteDataResponse } from "@utils/types";
25 import autosize from "autosize";
26 import classNames from "classnames";
27 import { Component, RefObject, createRef, linkEvent } from "inferno";
28 import {
29   AddAdmin,
30   AddModToCommunity,
31   AddModToCommunityResponse,
32   BanFromCommunity,
33   BanFromCommunityResponse,
34   BanPerson,
35   BanPersonResponse,
36   BlockCommunity,
37   BlockPerson,
38   CommentId,
39   CommentReplyResponse,
40   CommentResponse,
41   CommentSortType,
42   CommunityResponse,
43   CreateComment,
44   CreateCommentLike,
45   CreateCommentReport,
46   CreatePostLike,
47   CreatePostReport,
48   DeleteComment,
49   DeleteCommunity,
50   DeletePost,
51   DistinguishComment,
52   EditComment,
53   EditCommunity,
54   EditPost,
55   FeaturePost,
56   FollowCommunity,
57   GetComments,
58   GetCommentsResponse,
59   GetCommunityResponse,
60   GetPost,
61   GetPostResponse,
62   GetSiteResponse,
63   LockPost,
64   MarkCommentReplyAsRead,
65   MarkPersonMentionAsRead,
66   MarkPostAsRead,
67   PostResponse,
68   PurgeComment,
69   PurgeCommunity,
70   PurgeItemResponse,
71   PurgePerson,
72   PurgePost,
73   RemoveComment,
74   RemoveCommunity,
75   RemovePost,
76   SaveComment,
77   SavePost,
78   TransferCommunity,
79 } from "lemmy-js-client";
80 import { commentTreeMaxDepth } from "../../config";
81 import {
82   CommentNodeI,
83   CommentViewType,
84   InitialFetchRequest,
85 } from "../../interfaces";
86 import { FirstLoadService, I18NextService, UserService } from "../../services";
87 import { HttpService, RequestState } from "../../services/HttpService";
88 import { setupTippy } from "../../tippy";
89 import { toast } from "../../toast";
90 import { CommentForm } from "../comment/comment-form";
91 import { CommentNodes } from "../comment/comment-nodes";
92 import { HtmlTags } from "../common/html-tags";
93 import { Icon, Spinner } from "../common/icon";
94 import { Sidebar } from "../community/sidebar";
95 import { PostListing } from "./post-listing";
96
97 const commentsShownInterval = 15;
98
99 type PostData = RouteDataResponse<{
100   postRes: GetPostResponse;
101   commentsRes: GetCommentsResponse;
102 }>;
103
104 interface PostState {
105   postId?: number;
106   commentId?: number;
107   postRes: RequestState<GetPostResponse>;
108   commentsRes: RequestState<GetCommentsResponse>;
109   commentSort: CommentSortType;
110   commentViewType: CommentViewType;
111   scrolled?: boolean;
112   siteRes: GetSiteResponse;
113   commentSectionRef?: RefObject<HTMLDivElement>;
114   showSidebarMobile: boolean;
115   maxCommentsShown: number;
116   finished: Map<CommentId, boolean | undefined>;
117   isIsomorphic: boolean;
118 }
119
120 export class Post extends Component<any, PostState> {
121   private isoData = setIsoData<PostData>(this.context);
122   private commentScrollDebounced: () => void;
123   state: PostState = {
124     postRes: { state: "empty" },
125     commentsRes: { state: "empty" },
126     postId: getIdFromProps(this.props),
127     commentId: getCommentIdFromProps(this.props),
128     commentSort: "Hot",
129     commentViewType: CommentViewType.Tree,
130     scrolled: false,
131     siteRes: this.isoData.site_res,
132     showSidebarMobile: false,
133     maxCommentsShown: commentsShownInterval,
134     finished: new Map(),
135     isIsomorphic: false,
136   };
137
138   constructor(props: any, context: any) {
139     super(props, context);
140
141     this.handleDeleteCommunityClick =
142       this.handleDeleteCommunityClick.bind(this);
143     this.handleEditCommunity = this.handleEditCommunity.bind(this);
144     this.handleFollow = this.handleFollow.bind(this);
145     this.handleModRemoveCommunity = this.handleModRemoveCommunity.bind(this);
146     this.handleCreateComment = this.handleCreateComment.bind(this);
147     this.handleEditComment = this.handleEditComment.bind(this);
148     this.handleSaveComment = this.handleSaveComment.bind(this);
149     this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
150     this.handleBlockPerson = this.handleBlockPerson.bind(this);
151     this.handleDeleteComment = this.handleDeleteComment.bind(this);
152     this.handleRemoveComment = this.handleRemoveComment.bind(this);
153     this.handleCommentVote = this.handleCommentVote.bind(this);
154     this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
155     this.handleAddAdmin = this.handleAddAdmin.bind(this);
156     this.handlePurgePerson = this.handlePurgePerson.bind(this);
157     this.handlePurgeComment = this.handlePurgeComment.bind(this);
158     this.handleCommentReport = this.handleCommentReport.bind(this);
159     this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
160     this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
161     this.handleFetchChildren = this.handleFetchChildren.bind(this);
162     this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
163     this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
164     this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
165     this.handleBanPerson = this.handleBanPerson.bind(this);
166     this.handlePostEdit = this.handlePostEdit.bind(this);
167     this.handlePostVote = this.handlePostVote.bind(this);
168     this.handlePostReport = this.handlePostReport.bind(this);
169     this.handleLockPost = this.handleLockPost.bind(this);
170     this.handleDeletePost = this.handleDeletePost.bind(this);
171     this.handleRemovePost = this.handleRemovePost.bind(this);
172     this.handleSavePost = this.handleSavePost.bind(this);
173     this.handlePurgePost = this.handlePurgePost.bind(this);
174     this.handleFeaturePost = this.handleFeaturePost.bind(this);
175     this.handleMarkPostAsRead = this.handleMarkPostAsRead.bind(this);
176
177     this.state = { ...this.state, commentSectionRef: createRef() };
178
179     // Only fetch the data if coming from another route
180     if (FirstLoadService.isFirstLoad) {
181       const { commentsRes, postRes } = this.isoData.routeData;
182
183       this.state = {
184         ...this.state,
185         postRes,
186         commentsRes,
187         isIsomorphic: true,
188       };
189
190       if (isBrowser()) {
191         if (this.checkScrollIntoCommentsParam) {
192           this.scrollIntoCommentSection();
193         }
194       }
195     }
196   }
197
198   async fetchPost() {
199     this.setState({
200       postRes: { state: "loading" },
201       commentsRes: { state: "loading" },
202     });
203
204     const auth = myAuth();
205
206     this.setState({
207       postRes: await HttpService.client.getPost({
208         id: this.state.postId,
209         comment_id: this.state.commentId,
210         auth,
211       }),
212       commentsRes: await HttpService.client.getComments({
213         post_id: this.state.postId,
214         parent_id: this.state.commentId,
215         max_depth: commentTreeMaxDepth,
216         sort: this.state.commentSort,
217         type_: "All",
218         saved_only: false,
219         auth,
220       }),
221     });
222
223     setupTippy();
224
225     if (!this.state.commentId) restoreScrollPosition(this.context);
226
227     if (this.checkScrollIntoCommentsParam) {
228       this.scrollIntoCommentSection();
229     }
230   }
231
232   static async fetchInitialData({
233     client,
234     path,
235     auth,
236   }: InitialFetchRequest): Promise<PostData> {
237     const pathSplit = path.split("/");
238
239     const pathType = pathSplit.at(1);
240     const id = pathSplit.at(2) ? Number(pathSplit.at(2)) : undefined;
241
242     const postForm: GetPost = {
243       auth,
244     };
245
246     const commentsForm: GetComments = {
247       max_depth: commentTreeMaxDepth,
248       sort: "Hot",
249       type_: "All",
250       saved_only: false,
251       auth,
252     };
253
254     // Set the correct id based on the path type
255     if (pathType === "post") {
256       postForm.id = id;
257       commentsForm.post_id = id;
258     } else {
259       postForm.comment_id = id;
260       commentsForm.parent_id = id;
261     }
262
263     return {
264       postRes: await client.getPost(postForm),
265       commentsRes: await client.getComments(commentsForm),
266     };
267   }
268
269   componentWillUnmount() {
270     document.removeEventListener("scroll", this.commentScrollDebounced);
271
272     saveScrollPosition(this.context);
273   }
274
275   async componentDidMount() {
276     if (!this.state.isIsomorphic) {
277       await this.fetchPost();
278     }
279
280     autosize(document.querySelectorAll("textarea"));
281
282     this.commentScrollDebounced = debounce(this.trackCommentsBoxScrolling, 100);
283     document.addEventListener("scroll", this.commentScrollDebounced);
284   }
285
286   async componentDidUpdate(_lastProps: any) {
287     // Necessary if you are on a post and you click another post (same route)
288     if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
289       await this.fetchPost();
290     }
291   }
292
293   get checkScrollIntoCommentsParam() {
294     return Boolean(
295       new URLSearchParams(this.props.location.search).get("scrollToComments"),
296     );
297   }
298
299   scrollIntoCommentSection() {
300     this.state.commentSectionRef?.current?.scrollIntoView();
301   }
302
303   isBottom(el: Element): boolean {
304     return el?.getBoundingClientRect().bottom <= window.innerHeight;
305   }
306
307   /**
308    * Shows new comments when scrolling to the bottom of the comments div
309    */
310   trackCommentsBoxScrolling = () => {
311     const wrappedElement = document.getElementsByClassName("comments")[0];
312     if (wrappedElement && this.isBottom(wrappedElement)) {
313       const commentCount =
314         this.state.commentsRes.state === "success"
315           ? this.state.commentsRes.data.comments.length
316           : 0;
317
318       if (this.state.maxCommentsShown < commentCount) {
319         this.setState({
320           maxCommentsShown: this.state.maxCommentsShown + commentsShownInterval,
321         });
322       }
323     }
324   };
325
326   get documentTitle(): string {
327     const siteName = this.state.siteRes.site_view.site.name;
328     return this.state.postRes.state === "success"
329       ? `${this.state.postRes.data.post_view.post.name} - ${siteName}`
330       : siteName;
331   }
332
333   get imageTag(): string | undefined {
334     if (this.state.postRes.state === "success") {
335       const post = this.state.postRes.data.post_view.post;
336       const thumbnail = post.thumbnail_url;
337       const url = post.url;
338       return thumbnail || (url && isImage(url) ? url : undefined);
339     } else return undefined;
340   }
341
342   renderPostRes() {
343     switch (this.state.postRes.state) {
344       case "loading":
345         return (
346           <h5>
347             <Spinner large />
348           </h5>
349         );
350       case "success": {
351         const res = this.state.postRes.data;
352         return (
353           <div className="row">
354             <main className="col-12 col-md-8 col-lg-9 mb-3">
355               <HtmlTags
356                 title={this.documentTitle}
357                 path={this.context.router.route.match.url}
358                 canonicalPath={res.post_view.post.ap_id}
359                 image={this.imageTag}
360                 description={res.post_view.post.body}
361               />
362               <PostListing
363                 post_view={res.post_view}
364                 crossPosts={res.cross_posts}
365                 showBody
366                 showCommunity
367                 moderators={res.moderators}
368                 admins={this.state.siteRes.admins}
369                 enableDownvotes={enableDownvotes(this.state.siteRes)}
370                 enableNsfw={enableNsfw(this.state.siteRes)}
371                 allLanguages={this.state.siteRes.all_languages}
372                 siteLanguages={this.state.siteRes.discussion_languages}
373                 onBlockPerson={this.handleBlockPerson}
374                 onPostEdit={this.handlePostEdit}
375                 onPostVote={this.handlePostVote}
376                 onPostReport={this.handlePostReport}
377                 onLockPost={this.handleLockPost}
378                 onDeletePost={this.handleDeletePost}
379                 onRemovePost={this.handleRemovePost}
380                 onSavePost={this.handleSavePost}
381                 onPurgePerson={this.handlePurgePerson}
382                 onPurgePost={this.handlePurgePost}
383                 onBanPerson={this.handleBanPerson}
384                 onBanPersonFromCommunity={this.handleBanFromCommunity}
385                 onAddModToCommunity={this.handleAddModToCommunity}
386                 onAddAdmin={this.handleAddAdmin}
387                 onTransferCommunity={this.handleTransferCommunity}
388                 onFeaturePost={this.handleFeaturePost}
389                 onMarkPostAsRead={this.handleMarkPostAsRead}
390               />
391               <div ref={this.state.commentSectionRef} className="mb-2" />
392               <CommentForm
393                 node={res.post_view.post.id}
394                 disabled={res.post_view.post.locked}
395                 allLanguages={this.state.siteRes.all_languages}
396                 siteLanguages={this.state.siteRes.discussion_languages}
397                 containerClass="post-comment-container"
398                 onUpsertComment={this.handleCreateComment}
399                 finished={this.state.finished.get(0)}
400               />
401               <div className="d-block d-md-none">
402                 <button
403                   className="btn btn-secondary d-inline-block mb-2 me-3"
404                   onClick={linkEvent(this, this.handleShowSidebarMobile)}
405                 >
406                   {I18NextService.i18n.t("sidebar")}{" "}
407                   <Icon
408                     icon={
409                       this.state.showSidebarMobile
410                         ? `minus-square`
411                         : `plus-square`
412                     }
413                     classes="icon-inline"
414                   />
415                 </button>
416                 {this.state.showSidebarMobile && this.sidebar()}
417               </div>
418               {this.sortRadios()}
419               {this.state.commentViewType === CommentViewType.Tree &&
420                 this.commentsTree()}
421               {this.state.commentViewType === CommentViewType.Flat &&
422                 this.commentsFlat()}
423             </main>
424             <aside className="d-none d-md-block col-md-4 col-lg-3">
425               {this.sidebar()}
426             </aside>
427           </div>
428         );
429       }
430     }
431   }
432
433   render() {
434     return <div className="post container-lg">{this.renderPostRes()}</div>;
435   }
436
437   sortRadios() {
438     const radioId =
439       this.state.postRes.state === "success"
440         ? this.state.postRes.data.post_view.post.id
441         : randomStr();
442
443     return (
444       <>
445         <div
446           className="btn-group btn-group-toggle flex-wrap me-3 mb-2"
447           role="group"
448         >
449           <input
450             id={`${radioId}-hot`}
451             type="radio"
452             className="btn-check"
453             value={"Hot"}
454             checked={this.state.commentSort === "Hot"}
455             onChange={linkEvent(this, this.handleCommentSortChange)}
456           />
457           <label
458             htmlFor={`${radioId}-hot`}
459             className={classNames("btn btn-outline-secondary pointer", {
460               active: this.state.commentSort === "Hot",
461             })}
462           >
463             {I18NextService.i18n.t("hot")}
464           </label>
465           <input
466             id={`${radioId}-top`}
467             type="radio"
468             className="btn-check"
469             value={"Top"}
470             checked={this.state.commentSort === "Top"}
471             onChange={linkEvent(this, this.handleCommentSortChange)}
472           />
473           <label
474             htmlFor={`${radioId}-top`}
475             className={classNames("btn btn-outline-secondary pointer", {
476               active: this.state.commentSort === "Top",
477             })}
478           >
479             {I18NextService.i18n.t("top")}
480           </label>
481           <input
482             id={`${radioId}-controversial`}
483             type="radio"
484             className="btn-check"
485             value={"Controversial"}
486             checked={this.state.commentSort === "Controversial"}
487             onChange={linkEvent(this, this.handleCommentSortChange)}
488           />
489           <label
490             htmlFor={`${radioId}-controversial`}
491             className={classNames("btn btn-outline-secondary pointer", {
492               active: this.state.commentSort === "Controversial",
493             })}
494           >
495             {I18NextService.i18n.t("controversial")}
496           </label>
497           <input
498             id={`${radioId}-new`}
499             type="radio"
500             className="btn-check"
501             value={"New"}
502             checked={this.state.commentSort === "New"}
503             onChange={linkEvent(this, this.handleCommentSortChange)}
504           />
505           <label
506             htmlFor={`${radioId}-new`}
507             className={classNames("btn btn-outline-secondary pointer", {
508               active: this.state.commentSort === "New",
509             })}
510           >
511             {I18NextService.i18n.t("new")}
512           </label>
513           <input
514             id={`${radioId}-old`}
515             type="radio"
516             className="btn-check"
517             value={"Old"}
518             checked={this.state.commentSort === "Old"}
519             onChange={linkEvent(this, this.handleCommentSortChange)}
520           />
521           <label
522             htmlFor={`${radioId}-old`}
523             className={classNames("btn btn-outline-secondary pointer", {
524               active: this.state.commentSort === "Old",
525             })}
526           >
527             {I18NextService.i18n.t("old")}
528           </label>
529         </div>
530         <div className="btn-group btn-group-toggle flex-wrap mb-2" role="group">
531           <input
532             id={`${radioId}-chat`}
533             type="radio"
534             className="btn-check"
535             value={CommentViewType.Flat}
536             checked={this.state.commentViewType === CommentViewType.Flat}
537             onChange={linkEvent(this, this.handleCommentViewTypeChange)}
538           />
539           <label
540             htmlFor={`${radioId}-chat`}
541             className={classNames("btn btn-outline-secondary pointer", {
542               active: this.state.commentViewType === CommentViewType.Flat,
543             })}
544           >
545             {I18NextService.i18n.t("chat")}
546           </label>
547         </div>
548       </>
549     );
550   }
551
552   commentsFlat() {
553     // These are already sorted by new
554     const commentsRes = this.state.commentsRes;
555     const postRes = this.state.postRes;
556
557     if (commentsRes.state === "success" && postRes.state === "success") {
558       return (
559         <div>
560           <CommentNodes
561             nodes={commentsToFlatNodes(commentsRes.data.comments)}
562             viewType={this.state.commentViewType}
563             maxCommentsShown={this.state.maxCommentsShown}
564             isTopLevel
565             locked={postRes.data.post_view.post.locked}
566             moderators={postRes.data.moderators}
567             admins={this.state.siteRes.admins}
568             enableDownvotes={enableDownvotes(this.state.siteRes)}
569             showContext
570             finished={this.state.finished}
571             allLanguages={this.state.siteRes.all_languages}
572             siteLanguages={this.state.siteRes.discussion_languages}
573             onSaveComment={this.handleSaveComment}
574             onBlockPerson={this.handleBlockPerson}
575             onDeleteComment={this.handleDeleteComment}
576             onRemoveComment={this.handleRemoveComment}
577             onCommentVote={this.handleCommentVote}
578             onCommentReport={this.handleCommentReport}
579             onDistinguishComment={this.handleDistinguishComment}
580             onAddModToCommunity={this.handleAddModToCommunity}
581             onAddAdmin={this.handleAddAdmin}
582             onTransferCommunity={this.handleTransferCommunity}
583             onFetchChildren={this.handleFetchChildren}
584             onPurgeComment={this.handlePurgeComment}
585             onPurgePerson={this.handlePurgePerson}
586             onCommentReplyRead={this.handleCommentReplyRead}
587             onPersonMentionRead={this.handlePersonMentionRead}
588             onBanPersonFromCommunity={this.handleBanFromCommunity}
589             onBanPerson={this.handleBanPerson}
590             onCreateComment={this.handleCreateComment}
591             onEditComment={this.handleEditComment}
592           />
593         </div>
594       );
595     }
596   }
597
598   sidebar() {
599     const res = this.state.postRes;
600     if (res.state === "success") {
601       return (
602         <Sidebar
603           community_view={res.data.community_view}
604           moderators={res.data.moderators}
605           admins={this.state.siteRes.admins}
606           enableNsfw={enableNsfw(this.state.siteRes)}
607           showIcon
608           allLanguages={this.state.siteRes.all_languages}
609           siteLanguages={this.state.siteRes.discussion_languages}
610           onDeleteCommunity={this.handleDeleteCommunityClick}
611           onLeaveModTeam={this.handleAddModToCommunity}
612           onFollowCommunity={this.handleFollow}
613           onRemoveCommunity={this.handleModRemoveCommunity}
614           onPurgeCommunity={this.handlePurgeCommunity}
615           onBlockCommunity={this.handleBlockCommunity}
616           onEditCommunity={this.handleEditCommunity}
617         />
618       );
619     }
620   }
621
622   commentsTree() {
623     const res = this.state.postRes;
624     const firstComment = this.commentTree().at(0)?.comment_view.comment;
625     const depth = getDepthFromComment(firstComment);
626     const showContextButton = depth ? depth > 0 : false;
627
628     return (
629       res.state === "success" && (
630         <div>
631           {!!this.state.commentId && (
632             <>
633               <button
634                 className="ps-0 d-block btn btn-link text-muted"
635                 onClick={linkEvent(this, this.handleViewPost)}
636               >
637                 {I18NextService.i18n.t("view_all_comments")} âž”
638               </button>
639               {showContextButton && (
640                 <button
641                   className="ps-0 d-block btn btn-link text-muted"
642                   onClick={linkEvent(this, this.handleViewContext)}
643                 >
644                   {I18NextService.i18n.t("show_context")} âž”
645                 </button>
646               )}
647             </>
648           )}
649           <CommentNodes
650             nodes={this.commentTree()}
651             viewType={this.state.commentViewType}
652             maxCommentsShown={this.state.maxCommentsShown}
653             locked={res.data.post_view.post.locked}
654             moderators={res.data.moderators}
655             admins={this.state.siteRes.admins}
656             enableDownvotes={enableDownvotes(this.state.siteRes)}
657             finished={this.state.finished}
658             allLanguages={this.state.siteRes.all_languages}
659             siteLanguages={this.state.siteRes.discussion_languages}
660             onSaveComment={this.handleSaveComment}
661             onBlockPerson={this.handleBlockPerson}
662             onDeleteComment={this.handleDeleteComment}
663             onRemoveComment={this.handleRemoveComment}
664             onCommentVote={this.handleCommentVote}
665             onCommentReport={this.handleCommentReport}
666             onDistinguishComment={this.handleDistinguishComment}
667             onAddModToCommunity={this.handleAddModToCommunity}
668             onAddAdmin={this.handleAddAdmin}
669             onTransferCommunity={this.handleTransferCommunity}
670             onFetchChildren={this.handleFetchChildren}
671             onPurgeComment={this.handlePurgeComment}
672             onPurgePerson={this.handlePurgePerson}
673             onCommentReplyRead={this.handleCommentReplyRead}
674             onPersonMentionRead={this.handlePersonMentionRead}
675             onBanPersonFromCommunity={this.handleBanFromCommunity}
676             onBanPerson={this.handleBanPerson}
677             onCreateComment={this.handleCreateComment}
678             onEditComment={this.handleEditComment}
679           />
680         </div>
681       )
682     );
683   }
684
685   commentTree(): CommentNodeI[] {
686     if (this.state.commentsRes.state === "success") {
687       return buildCommentsTree(
688         this.state.commentsRes.data.comments,
689         !!this.state.commentId,
690       );
691     } else {
692       return [];
693     }
694   }
695
696   async handleCommentSortChange(i: Post, event: any) {
697     i.setState({
698       commentSort: event.target.value as CommentSortType,
699       commentViewType: CommentViewType.Tree,
700       commentsRes: { state: "loading" },
701       postRes: { state: "loading" },
702     });
703     await i.fetchPost();
704   }
705
706   handleCommentViewTypeChange(i: Post, event: any) {
707     i.setState({
708       commentViewType: Number(event.target.value),
709       commentSort: "New",
710     });
711   }
712
713   handleShowSidebarMobile(i: Post) {
714     i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
715   }
716
717   handleViewPost(i: Post) {
718     if (i.state.postRes.state === "success") {
719       const id = i.state.postRes.data.post_view.post.id;
720       i.context.router.history.push(`/post/${id}`);
721     }
722   }
723
724   handleViewContext(i: Post) {
725     if (i.state.commentsRes.state === "success") {
726       const parentId = getCommentParentId(
727         i.state.commentsRes.data.comments.at(0)?.comment,
728       );
729       if (parentId) {
730         i.context.router.history.push(`/comment/${parentId}`);
731       }
732     }
733   }
734
735   async handleDeleteCommunityClick(form: DeleteCommunity) {
736     const deleteCommunityRes = await HttpService.client.deleteCommunity(form);
737     this.updateCommunity(deleteCommunityRes);
738   }
739
740   async handleAddModToCommunity(form: AddModToCommunity) {
741     const addModRes = await HttpService.client.addModToCommunity(form);
742     this.updateModerators(addModRes);
743   }
744
745   async handleFollow(form: FollowCommunity) {
746     const followCommunityRes = await HttpService.client.followCommunity(form);
747     this.updateCommunity(followCommunityRes);
748
749     // Update myUserInfo
750     if (followCommunityRes.state === "success") {
751       const communityId = followCommunityRes.data.community_view.community.id;
752       const mui = UserService.Instance.myUserInfo;
753       if (mui) {
754         mui.follows = mui.follows.filter(i => i.community.id !== communityId);
755       }
756     }
757   }
758
759   async handlePurgeCommunity(form: PurgeCommunity) {
760     const purgeCommunityRes = await HttpService.client.purgeCommunity(form);
761     this.purgeItem(purgeCommunityRes);
762   }
763
764   async handlePurgePerson(form: PurgePerson) {
765     const purgePersonRes = await HttpService.client.purgePerson(form);
766     this.purgeItem(purgePersonRes);
767   }
768
769   async handlePurgeComment(form: PurgeComment) {
770     const purgeCommentRes = await HttpService.client.purgeComment(form);
771     this.purgeItem(purgeCommentRes);
772   }
773
774   async handlePurgePost(form: PurgePost) {
775     const purgeRes = await HttpService.client.purgePost(form);
776     this.purgeItem(purgeRes);
777   }
778
779   async handleBlockCommunity(form: BlockCommunity) {
780     const blockCommunityRes = await HttpService.client.blockCommunity(form);
781     if (blockCommunityRes.state === "success") {
782       updateCommunityBlock(blockCommunityRes.data);
783       this.setState(s => {
784         if (s.postRes.state === "success") {
785           s.postRes.data.community_view.blocked =
786             blockCommunityRes.data.blocked;
787         }
788       });
789     }
790   }
791
792   async handleBlockPerson(form: BlockPerson) {
793     const blockPersonRes = await HttpService.client.blockPerson(form);
794     if (blockPersonRes.state === "success") {
795       updatePersonBlock(blockPersonRes.data);
796     }
797   }
798
799   async handleModRemoveCommunity(form: RemoveCommunity) {
800     const removeCommunityRes = await HttpService.client.removeCommunity(form);
801     this.updateCommunity(removeCommunityRes);
802   }
803
804   async handleEditCommunity(form: EditCommunity) {
805     const res = await HttpService.client.editCommunity(form);
806     this.updateCommunity(res);
807
808     return res;
809   }
810
811   async handleCreateComment(form: CreateComment) {
812     const createCommentRes = await HttpService.client.createComment(form);
813     this.createAndUpdateComments(createCommentRes);
814
815     return createCommentRes;
816   }
817
818   async handleEditComment(form: EditComment) {
819     const editCommentRes = await HttpService.client.editComment(form);
820     this.findAndUpdateComment(editCommentRes);
821
822     return editCommentRes;
823   }
824
825   async handleDeleteComment(form: DeleteComment) {
826     const deleteCommentRes = await HttpService.client.deleteComment(form);
827     this.findAndUpdateComment(deleteCommentRes);
828   }
829
830   async handleDeletePost(form: DeletePost) {
831     const deleteRes = await HttpService.client.deletePost(form);
832     this.updatePost(deleteRes);
833   }
834
835   async handleRemovePost(form: RemovePost) {
836     const removeRes = await HttpService.client.removePost(form);
837     this.updatePost(removeRes);
838   }
839
840   async handleRemoveComment(form: RemoveComment) {
841     const removeCommentRes = await HttpService.client.removeComment(form);
842     this.findAndUpdateComment(removeCommentRes);
843   }
844
845   async handleSaveComment(form: SaveComment) {
846     const saveCommentRes = await HttpService.client.saveComment(form);
847     this.findAndUpdateComment(saveCommentRes);
848   }
849
850   async handleSavePost(form: SavePost) {
851     const saveRes = await HttpService.client.savePost(form);
852     this.updatePost(saveRes);
853   }
854
855   async handleFeaturePost(form: FeaturePost) {
856     const featureRes = await HttpService.client.featurePost(form);
857     this.updatePost(featureRes);
858   }
859
860   async handleCommentVote(form: CreateCommentLike) {
861     const voteRes = await HttpService.client.likeComment(form);
862     this.findAndUpdateComment(voteRes);
863   }
864
865   async handlePostVote(form: CreatePostLike) {
866     const voteRes = await HttpService.client.likePost(form);
867     this.updatePost(voteRes);
868   }
869
870   async handlePostEdit(form: EditPost) {
871     const res = await HttpService.client.editPost(form);
872     this.updatePost(res);
873   }
874
875   async handleCommentReport(form: CreateCommentReport) {
876     const reportRes = await HttpService.client.createCommentReport(form);
877     if (reportRes.state === "success") {
878       toast(I18NextService.i18n.t("report_created"));
879     }
880   }
881
882   async handlePostReport(form: CreatePostReport) {
883     const reportRes = await HttpService.client.createPostReport(form);
884     if (reportRes.state === "success") {
885       toast(I18NextService.i18n.t("report_created"));
886     }
887   }
888
889   async handleLockPost(form: LockPost) {
890     const lockRes = await HttpService.client.lockPost(form);
891     this.updatePost(lockRes);
892   }
893
894   async handleDistinguishComment(form: DistinguishComment) {
895     const distinguishRes = await HttpService.client.distinguishComment(form);
896     this.findAndUpdateComment(distinguishRes);
897   }
898
899   async handleAddAdmin(form: AddAdmin) {
900     const addAdminRes = await HttpService.client.addAdmin(form);
901
902     if (addAdminRes.state === "success") {
903       this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
904     }
905   }
906
907   async handleTransferCommunity(form: TransferCommunity) {
908     const transferCommunityRes = await HttpService.client.transferCommunity(
909       form,
910     );
911     this.updateCommunityFull(transferCommunityRes);
912   }
913
914   async handleFetchChildren(form: GetComments) {
915     const moreCommentsRes = await HttpService.client.getComments(form);
916     if (
917       this.state.commentsRes.state === "success" &&
918       moreCommentsRes.state === "success"
919     ) {
920       const newComments = moreCommentsRes.data.comments;
921       // Remove the first comment, since it is the parent
922       newComments.shift();
923       const newRes = this.state.commentsRes;
924       newRes.data.comments.push(...newComments);
925       this.setState({ commentsRes: newRes });
926     }
927   }
928
929   async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
930     const readRes = await HttpService.client.markCommentReplyAsRead(form);
931     this.findAndUpdateCommentReply(readRes);
932   }
933
934   async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
935     // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
936     await HttpService.client.markPersonMentionAsRead(form);
937   }
938
939   async handleMarkPostAsRead(form: MarkPostAsRead) {
940     const res = await HttpService.client.markPostAsRead(form);
941     this.updatePost(res);
942   }
943
944   async handleBanFromCommunity(form: BanFromCommunity) {
945     const banRes = await HttpService.client.banFromCommunity(form);
946     this.updateBan(banRes);
947   }
948
949   async handleBanPerson(form: BanPerson) {
950     const banRes = await HttpService.client.banPerson(form);
951     this.updateBan(banRes);
952   }
953
954   updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
955     // Maybe not necessary
956     if (banRes.state === "success") {
957       this.setState(s => {
958         if (
959           s.postRes.state === "success" &&
960           s.postRes.data.post_view.creator.id ===
961             banRes.data.person_view.person.id
962         ) {
963           s.postRes.data.post_view.creator_banned_from_community =
964             banRes.data.banned;
965         }
966         if (s.commentsRes.state === "success") {
967           s.commentsRes.data.comments
968             .filter(c => c.creator.id === banRes.data.person_view.person.id)
969             .forEach(
970               c => (c.creator_banned_from_community = banRes.data.banned),
971             );
972         }
973         return s;
974       });
975     }
976   }
977
978   updateBan(banRes: RequestState<BanPersonResponse>) {
979     // Maybe not necessary
980     if (banRes.state === "success") {
981       this.setState(s => {
982         if (
983           s.postRes.state === "success" &&
984           s.postRes.data.post_view.creator.id ===
985             banRes.data.person_view.person.id
986         ) {
987           s.postRes.data.post_view.creator.banned = banRes.data.banned;
988         }
989         if (s.commentsRes.state === "success") {
990           s.commentsRes.data.comments
991             .filter(c => c.creator.id === banRes.data.person_view.person.id)
992             .forEach(c => (c.creator.banned = banRes.data.banned));
993         }
994         return s;
995       });
996     }
997   }
998
999   updateCommunity(communityRes: RequestState<CommunityResponse>) {
1000     this.setState(s => {
1001       if (s.postRes.state === "success" && communityRes.state === "success") {
1002         s.postRes.data.community_view = communityRes.data.community_view;
1003       }
1004       return s;
1005     });
1006   }
1007
1008   updateCommunityFull(res: RequestState<GetCommunityResponse>) {
1009     this.setState(s => {
1010       if (s.postRes.state === "success" && res.state === "success") {
1011         s.postRes.data.community_view = res.data.community_view;
1012         s.postRes.data.moderators = res.data.moderators;
1013       }
1014       return s;
1015     });
1016   }
1017
1018   updatePost(post: RequestState<PostResponse>) {
1019     this.setState(s => {
1020       if (s.postRes.state === "success" && post.state === "success") {
1021         s.postRes.data.post_view = post.data.post_view;
1022       }
1023       return s;
1024     });
1025   }
1026
1027   purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
1028     if (purgeRes.state === "success") {
1029       toast(I18NextService.i18n.t("purge_success"));
1030       this.context.router.history.push(`/`);
1031     }
1032   }
1033
1034   createAndUpdateComments(res: RequestState<CommentResponse>) {
1035     this.setState(s => {
1036       if (s.commentsRes.state === "success" && res.state === "success") {
1037         s.commentsRes.data.comments.unshift(res.data.comment_view);
1038
1039         // Set finished for the parent
1040         s.finished.set(
1041           getCommentParentId(res.data.comment_view.comment) ?? 0,
1042           true,
1043         );
1044       }
1045       return s;
1046     });
1047   }
1048
1049   findAndUpdateComment(res: RequestState<CommentResponse>) {
1050     this.setState(s => {
1051       if (s.commentsRes.state === "success" && res.state === "success") {
1052         s.commentsRes.data.comments = editComment(
1053           res.data.comment_view,
1054           s.commentsRes.data.comments,
1055         );
1056         s.finished.set(res.data.comment_view.comment.id, true);
1057       }
1058       return s;
1059     });
1060   }
1061
1062   findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
1063     this.setState(s => {
1064       if (s.commentsRes.state === "success" && res.state === "success") {
1065         s.commentsRes.data.comments = editWith(
1066           res.data.comment_reply_view,
1067           s.commentsRes.data.comments,
1068         );
1069       }
1070       return s;
1071     });
1072   }
1073
1074   updateModerators(res: RequestState<AddModToCommunityResponse>) {
1075     // Update the moderators
1076     this.setState(s => {
1077       if (s.postRes.state === "success" && res.state === "success") {
1078         s.postRes.data.moderators = res.data.moderators;
1079       }
1080       return s;
1081     });
1082   }
1083 }