]> Untitled Git - lemmy-ui.git/blob - src/shared/components/post/post.tsx
fix submodule error
[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
393               {/* Only show the top level comment form if its not a context view */}
394               {!this.state.commentId && (
395                 <CommentForm
396                   node={res.post_view.post.id}
397                   disabled={res.post_view.post.locked}
398                   allLanguages={this.state.siteRes.all_languages}
399                   siteLanguages={this.state.siteRes.discussion_languages}
400                   containerClass="post-comment-container"
401                   onUpsertComment={this.handleCreateComment}
402                   finished={this.state.finished.get(0)}
403                 />
404               )}
405               <div className="d-block d-md-none">
406                 <button
407                   className="btn btn-secondary d-inline-block mb-2 me-3"
408                   onClick={linkEvent(this, this.handleShowSidebarMobile)}
409                 >
410                   {I18NextService.i18n.t("sidebar")}{" "}
411                   <Icon
412                     icon={
413                       this.state.showSidebarMobile
414                         ? `minus-square`
415                         : `plus-square`
416                     }
417                     classes="icon-inline"
418                   />
419                 </button>
420                 {this.state.showSidebarMobile && this.sidebar()}
421               </div>
422               {this.sortRadios()}
423               {this.state.commentViewType === CommentViewType.Tree &&
424                 this.commentsTree()}
425               {this.state.commentViewType === CommentViewType.Flat &&
426                 this.commentsFlat()}
427             </main>
428             <aside className="d-none d-md-block col-md-4 col-lg-3">
429               {this.sidebar()}
430             </aside>
431           </div>
432         );
433       }
434     }
435   }
436
437   render() {
438     return <div className="post container-lg">{this.renderPostRes()}</div>;
439   }
440
441   sortRadios() {
442     const radioId =
443       this.state.postRes.state === "success"
444         ? this.state.postRes.data.post_view.post.id
445         : randomStr();
446
447     return (
448       <>
449         <div
450           className="btn-group btn-group-toggle flex-wrap me-3 mb-2"
451           role="group"
452         >
453           <input
454             id={`${radioId}-hot`}
455             type="radio"
456             className="btn-check"
457             value={"Hot"}
458             checked={this.state.commentSort === "Hot"}
459             onChange={linkEvent(this, this.handleCommentSortChange)}
460           />
461           <label
462             htmlFor={`${radioId}-hot`}
463             className={classNames("btn btn-outline-secondary pointer", {
464               active: this.state.commentSort === "Hot",
465             })}
466           >
467             {I18NextService.i18n.t("hot")}
468           </label>
469           <input
470             id={`${radioId}-top`}
471             type="radio"
472             className="btn-check"
473             value={"Top"}
474             checked={this.state.commentSort === "Top"}
475             onChange={linkEvent(this, this.handleCommentSortChange)}
476           />
477           <label
478             htmlFor={`${radioId}-top`}
479             className={classNames("btn btn-outline-secondary pointer", {
480               active: this.state.commentSort === "Top",
481             })}
482           >
483             {I18NextService.i18n.t("top")}
484           </label>
485           <input
486             id={`${radioId}-controversial`}
487             type="radio"
488             className="btn-check"
489             value={"Controversial"}
490             checked={this.state.commentSort === "Controversial"}
491             onChange={linkEvent(this, this.handleCommentSortChange)}
492           />
493           <label
494             htmlFor={`${radioId}-controversial`}
495             className={classNames("btn btn-outline-secondary pointer", {
496               active: this.state.commentSort === "Controversial",
497             })}
498           >
499             {I18NextService.i18n.t("controversial")}
500           </label>
501           <input
502             id={`${radioId}-new`}
503             type="radio"
504             className="btn-check"
505             value={"New"}
506             checked={this.state.commentSort === "New"}
507             onChange={linkEvent(this, this.handleCommentSortChange)}
508           />
509           <label
510             htmlFor={`${radioId}-new`}
511             className={classNames("btn btn-outline-secondary pointer", {
512               active: this.state.commentSort === "New",
513             })}
514           >
515             {I18NextService.i18n.t("new")}
516           </label>
517           <input
518             id={`${radioId}-old`}
519             type="radio"
520             className="btn-check"
521             value={"Old"}
522             checked={this.state.commentSort === "Old"}
523             onChange={linkEvent(this, this.handleCommentSortChange)}
524           />
525           <label
526             htmlFor={`${radioId}-old`}
527             className={classNames("btn btn-outline-secondary pointer", {
528               active: this.state.commentSort === "Old",
529             })}
530           >
531             {I18NextService.i18n.t("old")}
532           </label>
533         </div>
534         <div className="btn-group btn-group-toggle flex-wrap mb-2" role="group">
535           <input
536             id={`${radioId}-chat`}
537             type="radio"
538             className="btn-check"
539             value={CommentViewType.Flat}
540             checked={this.state.commentViewType === CommentViewType.Flat}
541             onChange={linkEvent(this, this.handleCommentViewTypeChange)}
542           />
543           <label
544             htmlFor={`${radioId}-chat`}
545             className={classNames("btn btn-outline-secondary pointer", {
546               active: this.state.commentViewType === CommentViewType.Flat,
547             })}
548           >
549             {I18NextService.i18n.t("chat")}
550           </label>
551         </div>
552       </>
553     );
554   }
555
556   commentsFlat() {
557     // These are already sorted by new
558     const commentsRes = this.state.commentsRes;
559     const postRes = this.state.postRes;
560
561     if (commentsRes.state === "success" && postRes.state === "success") {
562       return (
563         <div>
564           <CommentNodes
565             nodes={commentsToFlatNodes(commentsRes.data.comments)}
566             viewType={this.state.commentViewType}
567             maxCommentsShown={this.state.maxCommentsShown}
568             isTopLevel
569             locked={postRes.data.post_view.post.locked}
570             moderators={postRes.data.moderators}
571             admins={this.state.siteRes.admins}
572             enableDownvotes={enableDownvotes(this.state.siteRes)}
573             showContext
574             finished={this.state.finished}
575             allLanguages={this.state.siteRes.all_languages}
576             siteLanguages={this.state.siteRes.discussion_languages}
577             onSaveComment={this.handleSaveComment}
578             onBlockPerson={this.handleBlockPerson}
579             onDeleteComment={this.handleDeleteComment}
580             onRemoveComment={this.handleRemoveComment}
581             onCommentVote={this.handleCommentVote}
582             onCommentReport={this.handleCommentReport}
583             onDistinguishComment={this.handleDistinguishComment}
584             onAddModToCommunity={this.handleAddModToCommunity}
585             onAddAdmin={this.handleAddAdmin}
586             onTransferCommunity={this.handleTransferCommunity}
587             onFetchChildren={this.handleFetchChildren}
588             onPurgeComment={this.handlePurgeComment}
589             onPurgePerson={this.handlePurgePerson}
590             onCommentReplyRead={this.handleCommentReplyRead}
591             onPersonMentionRead={this.handlePersonMentionRead}
592             onBanPersonFromCommunity={this.handleBanFromCommunity}
593             onBanPerson={this.handleBanPerson}
594             onCreateComment={this.handleCreateComment}
595             onEditComment={this.handleEditComment}
596           />
597         </div>
598       );
599     }
600   }
601
602   sidebar() {
603     const res = this.state.postRes;
604     if (res.state === "success") {
605       return (
606         <Sidebar
607           community_view={res.data.community_view}
608           moderators={res.data.moderators}
609           admins={this.state.siteRes.admins}
610           enableNsfw={enableNsfw(this.state.siteRes)}
611           showIcon
612           allLanguages={this.state.siteRes.all_languages}
613           siteLanguages={this.state.siteRes.discussion_languages}
614           onDeleteCommunity={this.handleDeleteCommunityClick}
615           onLeaveModTeam={this.handleAddModToCommunity}
616           onFollowCommunity={this.handleFollow}
617           onRemoveCommunity={this.handleModRemoveCommunity}
618           onPurgeCommunity={this.handlePurgeCommunity}
619           onBlockCommunity={this.handleBlockCommunity}
620           onEditCommunity={this.handleEditCommunity}
621         />
622       );
623     }
624   }
625
626   commentsTree() {
627     const res = this.state.postRes;
628     const firstComment = this.commentTree().at(0)?.comment_view.comment;
629     const depth = getDepthFromComment(firstComment);
630     const showContextButton = depth ? depth > 0 : false;
631
632     return (
633       res.state === "success" && (
634         <div>
635           {!!this.state.commentId && (
636             <>
637               <button
638                 className="ps-0 d-block btn btn-link text-muted"
639                 onClick={linkEvent(this, this.handleViewPost)}
640               >
641                 {I18NextService.i18n.t("view_all_comments")} âž”
642               </button>
643               {showContextButton && (
644                 <button
645                   className="ps-0 d-block btn btn-link text-muted"
646                   onClick={linkEvent(this, this.handleViewContext)}
647                 >
648                   {I18NextService.i18n.t("show_context")} âž”
649                 </button>
650               )}
651             </>
652           )}
653           <CommentNodes
654             nodes={this.commentTree()}
655             viewType={this.state.commentViewType}
656             maxCommentsShown={this.state.maxCommentsShown}
657             locked={res.data.post_view.post.locked}
658             moderators={res.data.moderators}
659             admins={this.state.siteRes.admins}
660             enableDownvotes={enableDownvotes(this.state.siteRes)}
661             finished={this.state.finished}
662             allLanguages={this.state.siteRes.all_languages}
663             siteLanguages={this.state.siteRes.discussion_languages}
664             onSaveComment={this.handleSaveComment}
665             onBlockPerson={this.handleBlockPerson}
666             onDeleteComment={this.handleDeleteComment}
667             onRemoveComment={this.handleRemoveComment}
668             onCommentVote={this.handleCommentVote}
669             onCommentReport={this.handleCommentReport}
670             onDistinguishComment={this.handleDistinguishComment}
671             onAddModToCommunity={this.handleAddModToCommunity}
672             onAddAdmin={this.handleAddAdmin}
673             onTransferCommunity={this.handleTransferCommunity}
674             onFetchChildren={this.handleFetchChildren}
675             onPurgeComment={this.handlePurgeComment}
676             onPurgePerson={this.handlePurgePerson}
677             onCommentReplyRead={this.handleCommentReplyRead}
678             onPersonMentionRead={this.handlePersonMentionRead}
679             onBanPersonFromCommunity={this.handleBanFromCommunity}
680             onBanPerson={this.handleBanPerson}
681             onCreateComment={this.handleCreateComment}
682             onEditComment={this.handleEditComment}
683           />
684         </div>
685       )
686     );
687   }
688
689   commentTree(): CommentNodeI[] {
690     if (this.state.commentsRes.state === "success") {
691       return buildCommentsTree(
692         this.state.commentsRes.data.comments,
693         !!this.state.commentId,
694       );
695     } else {
696       return [];
697     }
698   }
699
700   async handleCommentSortChange(i: Post, event: any) {
701     i.setState({
702       commentSort: event.target.value as CommentSortType,
703       commentViewType: CommentViewType.Tree,
704       commentsRes: { state: "loading" },
705       postRes: { state: "loading" },
706     });
707     await i.fetchPost();
708   }
709
710   handleCommentViewTypeChange(i: Post, event: any) {
711     i.setState({
712       commentViewType: Number(event.target.value),
713       commentSort: "New",
714     });
715   }
716
717   handleShowSidebarMobile(i: Post) {
718     i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
719   }
720
721   handleViewPost(i: Post) {
722     if (i.state.postRes.state === "success") {
723       const id = i.state.postRes.data.post_view.post.id;
724       i.context.router.history.push(`/post/${id}`);
725     }
726   }
727
728   handleViewContext(i: Post) {
729     if (i.state.commentsRes.state === "success") {
730       const parentId = getCommentParentId(
731         i.state.commentsRes.data.comments.at(0)?.comment,
732       );
733       if (parentId) {
734         i.context.router.history.push(`/comment/${parentId}`);
735       }
736     }
737   }
738
739   async handleDeleteCommunityClick(form: DeleteCommunity) {
740     const deleteCommunityRes = await HttpService.client.deleteCommunity(form);
741     this.updateCommunity(deleteCommunityRes);
742   }
743
744   async handleAddModToCommunity(form: AddModToCommunity) {
745     const addModRes = await HttpService.client.addModToCommunity(form);
746     this.updateModerators(addModRes);
747   }
748
749   async handleFollow(form: FollowCommunity) {
750     const followCommunityRes = await HttpService.client.followCommunity(form);
751     this.updateCommunity(followCommunityRes);
752
753     // Update myUserInfo
754     if (followCommunityRes.state === "success") {
755       const communityId = followCommunityRes.data.community_view.community.id;
756       const mui = UserService.Instance.myUserInfo;
757       if (mui) {
758         mui.follows = mui.follows.filter(i => i.community.id !== communityId);
759       }
760     }
761   }
762
763   async handlePurgeCommunity(form: PurgeCommunity) {
764     const purgeCommunityRes = await HttpService.client.purgeCommunity(form);
765     this.purgeItem(purgeCommunityRes);
766   }
767
768   async handlePurgePerson(form: PurgePerson) {
769     const purgePersonRes = await HttpService.client.purgePerson(form);
770     this.purgeItem(purgePersonRes);
771   }
772
773   async handlePurgeComment(form: PurgeComment) {
774     const purgeCommentRes = await HttpService.client.purgeComment(form);
775     this.purgeItem(purgeCommentRes);
776   }
777
778   async handlePurgePost(form: PurgePost) {
779     const purgeRes = await HttpService.client.purgePost(form);
780     this.purgeItem(purgeRes);
781   }
782
783   async handleBlockCommunity(form: BlockCommunity) {
784     const blockCommunityRes = await HttpService.client.blockCommunity(form);
785     if (blockCommunityRes.state === "success") {
786       updateCommunityBlock(blockCommunityRes.data);
787       this.setState(s => {
788         if (s.postRes.state === "success") {
789           s.postRes.data.community_view.blocked =
790             blockCommunityRes.data.blocked;
791         }
792       });
793     }
794   }
795
796   async handleBlockPerson(form: BlockPerson) {
797     const blockPersonRes = await HttpService.client.blockPerson(form);
798     if (blockPersonRes.state === "success") {
799       updatePersonBlock(blockPersonRes.data);
800     }
801   }
802
803   async handleModRemoveCommunity(form: RemoveCommunity) {
804     const removeCommunityRes = await HttpService.client.removeCommunity(form);
805     this.updateCommunity(removeCommunityRes);
806   }
807
808   async handleEditCommunity(form: EditCommunity) {
809     const res = await HttpService.client.editCommunity(form);
810     this.updateCommunity(res);
811
812     return res;
813   }
814
815   async handleCreateComment(form: CreateComment) {
816     const createCommentRes = await HttpService.client.createComment(form);
817     this.createAndUpdateComments(createCommentRes);
818
819     return createCommentRes;
820   }
821
822   async handleEditComment(form: EditComment) {
823     const editCommentRes = await HttpService.client.editComment(form);
824     this.findAndUpdateComment(editCommentRes);
825
826     return editCommentRes;
827   }
828
829   async handleDeleteComment(form: DeleteComment) {
830     const deleteCommentRes = await HttpService.client.deleteComment(form);
831     this.findAndUpdateComment(deleteCommentRes);
832   }
833
834   async handleDeletePost(form: DeletePost) {
835     const deleteRes = await HttpService.client.deletePost(form);
836     this.updatePost(deleteRes);
837   }
838
839   async handleRemovePost(form: RemovePost) {
840     const removeRes = await HttpService.client.removePost(form);
841     this.updatePost(removeRes);
842   }
843
844   async handleRemoveComment(form: RemoveComment) {
845     const removeCommentRes = await HttpService.client.removeComment(form);
846     this.findAndUpdateComment(removeCommentRes);
847   }
848
849   async handleSaveComment(form: SaveComment) {
850     const saveCommentRes = await HttpService.client.saveComment(form);
851     this.findAndUpdateComment(saveCommentRes);
852   }
853
854   async handleSavePost(form: SavePost) {
855     const saveRes = await HttpService.client.savePost(form);
856     this.updatePost(saveRes);
857   }
858
859   async handleFeaturePost(form: FeaturePost) {
860     const featureRes = await HttpService.client.featurePost(form);
861     this.updatePost(featureRes);
862   }
863
864   async handleCommentVote(form: CreateCommentLike) {
865     const voteRes = await HttpService.client.likeComment(form);
866     this.findAndUpdateComment(voteRes);
867   }
868
869   async handlePostVote(form: CreatePostLike) {
870     const voteRes = await HttpService.client.likePost(form);
871     this.updatePost(voteRes);
872   }
873
874   async handlePostEdit(form: EditPost) {
875     const res = await HttpService.client.editPost(form);
876     this.updatePost(res);
877   }
878
879   async handleCommentReport(form: CreateCommentReport) {
880     const reportRes = await HttpService.client.createCommentReport(form);
881     if (reportRes.state === "success") {
882       toast(I18NextService.i18n.t("report_created"));
883     }
884   }
885
886   async handlePostReport(form: CreatePostReport) {
887     const reportRes = await HttpService.client.createPostReport(form);
888     if (reportRes.state === "success") {
889       toast(I18NextService.i18n.t("report_created"));
890     }
891   }
892
893   async handleLockPost(form: LockPost) {
894     const lockRes = await HttpService.client.lockPost(form);
895     this.updatePost(lockRes);
896   }
897
898   async handleDistinguishComment(form: DistinguishComment) {
899     const distinguishRes = await HttpService.client.distinguishComment(form);
900     this.findAndUpdateComment(distinguishRes);
901   }
902
903   async handleAddAdmin(form: AddAdmin) {
904     const addAdminRes = await HttpService.client.addAdmin(form);
905
906     if (addAdminRes.state === "success") {
907       this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
908     }
909   }
910
911   async handleTransferCommunity(form: TransferCommunity) {
912     const transferCommunityRes = await HttpService.client.transferCommunity(
913       form,
914     );
915     this.updateCommunityFull(transferCommunityRes);
916   }
917
918   async handleFetchChildren(form: GetComments) {
919     const moreCommentsRes = await HttpService.client.getComments(form);
920     if (
921       this.state.commentsRes.state === "success" &&
922       moreCommentsRes.state === "success"
923     ) {
924       const newComments = moreCommentsRes.data.comments;
925       // Remove the first comment, since it is the parent
926       newComments.shift();
927       const newRes = this.state.commentsRes;
928       newRes.data.comments.push(...newComments);
929       this.setState({ commentsRes: newRes });
930     }
931   }
932
933   async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
934     const readRes = await HttpService.client.markCommentReplyAsRead(form);
935     this.findAndUpdateCommentReply(readRes);
936   }
937
938   async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
939     // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
940     await HttpService.client.markPersonMentionAsRead(form);
941   }
942
943   async handleMarkPostAsRead(form: MarkPostAsRead) {
944     const res = await HttpService.client.markPostAsRead(form);
945     this.updatePost(res);
946   }
947
948   async handleBanFromCommunity(form: BanFromCommunity) {
949     const banRes = await HttpService.client.banFromCommunity(form);
950     this.updateBan(banRes);
951   }
952
953   async handleBanPerson(form: BanPerson) {
954     const banRes = await HttpService.client.banPerson(form);
955     this.updateBan(banRes);
956   }
957
958   updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
959     // Maybe not necessary
960     if (banRes.state === "success") {
961       this.setState(s => {
962         if (
963           s.postRes.state === "success" &&
964           s.postRes.data.post_view.creator.id ===
965             banRes.data.person_view.person.id
966         ) {
967           s.postRes.data.post_view.creator_banned_from_community =
968             banRes.data.banned;
969         }
970         if (s.commentsRes.state === "success") {
971           s.commentsRes.data.comments
972             .filter(c => c.creator.id === banRes.data.person_view.person.id)
973             .forEach(
974               c => (c.creator_banned_from_community = banRes.data.banned),
975             );
976         }
977         return s;
978       });
979     }
980   }
981
982   updateBan(banRes: RequestState<BanPersonResponse>) {
983     // Maybe not necessary
984     if (banRes.state === "success") {
985       this.setState(s => {
986         if (
987           s.postRes.state === "success" &&
988           s.postRes.data.post_view.creator.id ===
989             banRes.data.person_view.person.id
990         ) {
991           s.postRes.data.post_view.creator.banned = banRes.data.banned;
992         }
993         if (s.commentsRes.state === "success") {
994           s.commentsRes.data.comments
995             .filter(c => c.creator.id === banRes.data.person_view.person.id)
996             .forEach(c => (c.creator.banned = banRes.data.banned));
997         }
998         return s;
999       });
1000     }
1001   }
1002
1003   updateCommunity(communityRes: RequestState<CommunityResponse>) {
1004     this.setState(s => {
1005       if (s.postRes.state === "success" && communityRes.state === "success") {
1006         s.postRes.data.community_view = communityRes.data.community_view;
1007       }
1008       return s;
1009     });
1010   }
1011
1012   updateCommunityFull(res: RequestState<GetCommunityResponse>) {
1013     this.setState(s => {
1014       if (s.postRes.state === "success" && res.state === "success") {
1015         s.postRes.data.community_view = res.data.community_view;
1016         s.postRes.data.moderators = res.data.moderators;
1017       }
1018       return s;
1019     });
1020   }
1021
1022   updatePost(post: RequestState<PostResponse>) {
1023     this.setState(s => {
1024       if (s.postRes.state === "success" && post.state === "success") {
1025         s.postRes.data.post_view = post.data.post_view;
1026       }
1027       return s;
1028     });
1029   }
1030
1031   purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
1032     if (purgeRes.state === "success") {
1033       toast(I18NextService.i18n.t("purge_success"));
1034       this.context.router.history.push(`/`);
1035     }
1036   }
1037
1038   createAndUpdateComments(res: RequestState<CommentResponse>) {
1039     this.setState(s => {
1040       if (s.commentsRes.state === "success" && res.state === "success") {
1041         // The comment must be inserted not at the very beginning of the list,
1042         // because the buildCommentsTree needs a correct path ordering.
1043         // It should be inserted right after its parent is found
1044         const comments = s.commentsRes.data.comments;
1045         const newComment = res.data.comment_view;
1046         const newCommentParentId = getCommentParentId(newComment.comment);
1047
1048         const foundCommentParentIndex = comments.findIndex(
1049           c => c.comment.id === newCommentParentId,
1050         );
1051
1052         comments.splice(foundCommentParentIndex + 1, 0, newComment);
1053
1054         // Set finished for the parent
1055         s.finished.set(newCommentParentId ?? 0, true);
1056       }
1057       return s;
1058     });
1059   }
1060
1061   findAndUpdateComment(res: RequestState<CommentResponse>) {
1062     this.setState(s => {
1063       if (s.commentsRes.state === "success" && res.state === "success") {
1064         s.commentsRes.data.comments = editComment(
1065           res.data.comment_view,
1066           s.commentsRes.data.comments,
1067         );
1068         s.finished.set(res.data.comment_view.comment.id, true);
1069       }
1070       return s;
1071     });
1072   }
1073
1074   findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
1075     this.setState(s => {
1076       if (s.commentsRes.state === "success" && res.state === "success") {
1077         s.commentsRes.data.comments = editWith(
1078           res.data.comment_reply_view,
1079           s.commentsRes.data.comments,
1080         );
1081       }
1082       return s;
1083     });
1084   }
1085
1086   updateModerators(res: RequestState<AddModToCommunityResponse>) {
1087     // Update the moderators
1088     this.setState(s => {
1089       if (s.postRes.state === "success" && res.state === "success") {
1090         s.postRes.data.moderators = res.data.moderators;
1091       }
1092       return s;
1093     });
1094   }
1095 }