]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/profile.tsx
Merge branch 'main' into breakout-role-utils
[lemmy-ui.git] / src / shared / components / person / profile.tsx
1 import classNames from "classnames";
2 import { NoOptionI18nKeys } from "i18next";
3 import { Component, linkEvent } from "inferno";
4 import { Link } from "inferno-router";
5 import { RouteComponentProps } from "inferno-router/dist/Route";
6 import {
7   AddAdmin,
8   AddModToCommunity,
9   BanFromCommunity,
10   BanFromCommunityResponse,
11   BanPerson,
12   BanPersonResponse,
13   BlockPerson,
14   CommentId,
15   CommentReplyResponse,
16   CommentResponse,
17   Community,
18   CommunityModeratorView,
19   CreateComment,
20   CreateCommentLike,
21   CreateCommentReport,
22   CreatePostLike,
23   CreatePostReport,
24   DeleteComment,
25   DeletePost,
26   DistinguishComment,
27   EditComment,
28   EditPost,
29   FeaturePost,
30   GetPersonDetails,
31   GetPersonDetailsResponse,
32   GetSiteResponse,
33   LockPost,
34   MarkCommentReplyAsRead,
35   MarkPersonMentionAsRead,
36   PersonView,
37   PostResponse,
38   PurgeComment,
39   PurgeItemResponse,
40   PurgePerson,
41   PurgePost,
42   RemoveComment,
43   RemovePost,
44   SaveComment,
45   SavePost,
46   SortType,
47   TransferCommunity,
48 } from "lemmy-js-client";
49 import moment from "moment";
50 import { i18n } from "../../i18next";
51 import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
52 import { UserService } from "../../services";
53 import { FirstLoadService } from "../../services/FirstLoadService";
54 import { HttpService, RequestState } from "../../services/HttpService";
55 import {
56   capitalizeFirstLetter,
57   editComment,
58   editPost,
59   editWith,
60   enableDownvotes,
61   enableNsfw,
62   fetchLimit,
63   futureDaysToUnixTime,
64   getCommentParentId,
65   getPageFromString,
66   mdToHtml,
67   myAuth,
68   myAuthRequired,
69   numToSI,
70   relTags,
71   restoreScrollPosition,
72   saveScrollPosition,
73   setIsoData,
74   setupTippy,
75   toast,
76   updatePersonBlock,
77 } from "../../utils";
78 import { getQueryParams } from "../../utils/helpers/get-query-params";
79 import { getQueryString } from "../../utils/helpers/get-query-string";
80 import { canMod } from "../../utils/roles/can-mod";
81 import { isAdmin } from "../../utils/roles/is-admin";
82 import { isBanned } from "../../utils/roles/is-banned";
83 import type { QueryParams } from "../../utils/types/query-params";
84 import { BannerIconHeader } from "../common/banner-icon-header";
85 import { HtmlTags } from "../common/html-tags";
86 import { Icon, Spinner } from "../common/icon";
87 import { MomentTime } from "../common/moment-time";
88 import { SortSelect } from "../common/sort-select";
89 import { CommunityLink } from "../community/community-link";
90 import { PersonDetails } from "./person-details";
91 import { PersonListing } from "./person-listing";
92
93 type ProfileData = RouteDataResponse<{
94   personResponse: GetPersonDetailsResponse;
95 }>;
96
97 interface ProfileState {
98   personRes: RequestState<GetPersonDetailsResponse>;
99   personBlocked: boolean;
100   banReason?: string;
101   banExpireDays?: number;
102   showBanDialog: boolean;
103   removeData: boolean;
104   siteRes: GetSiteResponse;
105   finished: Map<CommentId, boolean | undefined>;
106   isIsomorphic: boolean;
107 }
108
109 interface ProfileProps {
110   view: PersonDetailsView;
111   sort: SortType;
112   page: number;
113 }
114
115 function getProfileQueryParams() {
116   return getQueryParams<ProfileProps>({
117     view: getViewFromProps,
118     page: getPageFromString,
119     sort: getSortTypeFromQuery,
120   });
121 }
122
123 function getSortTypeFromQuery(sort?: string): SortType {
124   return sort ? (sort as SortType) : "New";
125 }
126
127 function getViewFromProps(view?: string): PersonDetailsView {
128   return view
129     ? PersonDetailsView[view] ?? PersonDetailsView.Overview
130     : PersonDetailsView.Overview;
131 }
132
133 const getCommunitiesListing = (
134   translationKey: NoOptionI18nKeys,
135   communityViews?: { community: Community }[]
136 ) =>
137   communityViews &&
138   communityViews.length > 0 && (
139     <div className="card border-secondary mb-3">
140       <div className="card-body">
141         <h5>{i18n.t(translationKey)}</h5>
142         <ul className="list-unstyled mb-0">
143           {communityViews.map(({ community }) => (
144             <li key={community.id}>
145               <CommunityLink community={community} />
146             </li>
147           ))}
148         </ul>
149       </div>
150     </div>
151   );
152
153 const Moderates = ({ moderates }: { moderates?: CommunityModeratorView[] }) =>
154   getCommunitiesListing("moderates", moderates);
155
156 const Follows = () =>
157   getCommunitiesListing("subscribed", UserService.Instance.myUserInfo?.follows);
158
159 export class Profile extends Component<
160   RouteComponentProps<{ username: string }>,
161   ProfileState
162 > {
163   private isoData = setIsoData<ProfileData>(this.context);
164   state: ProfileState = {
165     personRes: { state: "empty" },
166     personBlocked: false,
167     siteRes: this.isoData.site_res,
168     showBanDialog: false,
169     removeData: false,
170     finished: new Map(),
171     isIsomorphic: false,
172   };
173
174   constructor(props: RouteComponentProps<{ username: string }>, context: any) {
175     super(props, context);
176
177     this.handleSortChange = this.handleSortChange.bind(this);
178     this.handlePageChange = this.handlePageChange.bind(this);
179
180     this.handleBlockPerson = this.handleBlockPerson.bind(this);
181     this.handleUnblockPerson = this.handleUnblockPerson.bind(this);
182
183     this.handleCreateComment = this.handleCreateComment.bind(this);
184     this.handleEditComment = this.handleEditComment.bind(this);
185     this.handleSaveComment = this.handleSaveComment.bind(this);
186     this.handleBlockPersonAlt = this.handleBlockPersonAlt.bind(this);
187     this.handleDeleteComment = this.handleDeleteComment.bind(this);
188     this.handleRemoveComment = this.handleRemoveComment.bind(this);
189     this.handleCommentVote = this.handleCommentVote.bind(this);
190     this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
191     this.handleAddAdmin = this.handleAddAdmin.bind(this);
192     this.handlePurgePerson = this.handlePurgePerson.bind(this);
193     this.handlePurgeComment = this.handlePurgeComment.bind(this);
194     this.handleCommentReport = this.handleCommentReport.bind(this);
195     this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
196     this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
197     this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
198     this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
199     this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
200     this.handleBanPerson = this.handleBanPerson.bind(this);
201     this.handlePostVote = this.handlePostVote.bind(this);
202     this.handlePostEdit = this.handlePostEdit.bind(this);
203     this.handlePostReport = this.handlePostReport.bind(this);
204     this.handleLockPost = this.handleLockPost.bind(this);
205     this.handleDeletePost = this.handleDeletePost.bind(this);
206     this.handleRemovePost = this.handleRemovePost.bind(this);
207     this.handleSavePost = this.handleSavePost.bind(this);
208     this.handlePurgePost = this.handlePurgePost.bind(this);
209     this.handleFeaturePost = this.handleFeaturePost.bind(this);
210
211     // Only fetch the data if coming from another route
212     if (FirstLoadService.isFirstLoad) {
213       this.state = {
214         ...this.state,
215         personRes: this.isoData.routeData.personResponse,
216         isIsomorphic: true,
217       };
218     }
219   }
220
221   async componentDidMount() {
222     if (!this.state.isIsomorphic) {
223       await this.fetchUserData();
224     }
225     setupTippy();
226   }
227
228   componentWillUnmount() {
229     saveScrollPosition(this.context);
230   }
231
232   async fetchUserData() {
233     const { page, sort, view } = getProfileQueryParams();
234
235     this.setState({ personRes: { state: "empty" } });
236     this.setState({
237       personRes: await HttpService.client.getPersonDetails({
238         username: this.props.match.params.username,
239         sort,
240         saved_only: view === PersonDetailsView.Saved,
241         page,
242         limit: fetchLimit,
243         auth: myAuth(),
244       }),
245     });
246     restoreScrollPosition(this.context);
247     this.setPersonBlock();
248   }
249
250   get amCurrentUser() {
251     if (this.state.personRes.state === "success") {
252       return (
253         UserService.Instance.myUserInfo?.local_user_view.person.id ===
254         this.state.personRes.data.person_view.person.id
255       );
256     } else {
257       return false;
258     }
259   }
260
261   setPersonBlock() {
262     const mui = UserService.Instance.myUserInfo;
263     const res = this.state.personRes;
264
265     if (mui && res.state === "success") {
266       this.setState({
267         personBlocked: mui.person_blocks.some(
268           ({ target: { id } }) => id === res.data.person_view.person.id
269         ),
270       });
271     }
272   }
273
274   static async fetchInitialData({
275     client,
276     path,
277     query: { page, sort, view: urlView },
278     auth,
279   }: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<ProfileData> {
280     const pathSplit = path.split("/");
281
282     const username = pathSplit[2];
283     const view = getViewFromProps(urlView);
284
285     const form: GetPersonDetails = {
286       username: username,
287       sort: getSortTypeFromQuery(sort),
288       saved_only: view === PersonDetailsView.Saved,
289       page: getPageFromString(page),
290       limit: fetchLimit,
291       auth,
292     };
293
294     return {
295       personResponse: await client.getPersonDetails(form),
296     };
297   }
298
299   get documentTitle(): string {
300     const siteName = this.state.siteRes.site_view.site.name;
301     const res = this.state.personRes;
302     return res.state == "success"
303       ? `@${res.data.person_view.person.name} - ${siteName}`
304       : siteName;
305   }
306
307   renderPersonRes() {
308     switch (this.state.personRes.state) {
309       case "loading":
310         return (
311           <h5>
312             <Spinner large />
313           </h5>
314         );
315       case "success": {
316         const siteRes = this.state.siteRes;
317         const personRes = this.state.personRes.data;
318         const { page, sort, view } = getProfileQueryParams();
319
320         return (
321           <div className="row">
322             <div className="col-12 col-md-8">
323               <HtmlTags
324                 title={this.documentTitle}
325                 path={this.context.router.route.match.url}
326                 description={personRes.person_view.person.bio}
327                 image={personRes.person_view.person.avatar}
328               />
329
330               {this.userInfo(personRes.person_view)}
331
332               <hr />
333
334               {this.selects}
335
336               <PersonDetails
337                 personRes={personRes}
338                 admins={siteRes.admins}
339                 sort={sort}
340                 page={page}
341                 limit={fetchLimit}
342                 finished={this.state.finished}
343                 enableDownvotes={enableDownvotes(siteRes)}
344                 enableNsfw={enableNsfw(siteRes)}
345                 view={view}
346                 onPageChange={this.handlePageChange}
347                 allLanguages={siteRes.all_languages}
348                 siteLanguages={siteRes.discussion_languages}
349                 // TODO all the forms here
350                 onSaveComment={this.handleSaveComment}
351                 onBlockPerson={this.handleBlockPersonAlt}
352                 onDeleteComment={this.handleDeleteComment}
353                 onRemoveComment={this.handleRemoveComment}
354                 onCommentVote={this.handleCommentVote}
355                 onCommentReport={this.handleCommentReport}
356                 onDistinguishComment={this.handleDistinguishComment}
357                 onAddModToCommunity={this.handleAddModToCommunity}
358                 onAddAdmin={this.handleAddAdmin}
359                 onTransferCommunity={this.handleTransferCommunity}
360                 onPurgeComment={this.handlePurgeComment}
361                 onPurgePerson={this.handlePurgePerson}
362                 onCommentReplyRead={this.handleCommentReplyRead}
363                 onPersonMentionRead={this.handlePersonMentionRead}
364                 onBanPersonFromCommunity={this.handleBanFromCommunity}
365                 onBanPerson={this.handleBanPerson}
366                 onCreateComment={this.handleCreateComment}
367                 onEditComment={this.handleEditComment}
368                 onPostEdit={this.handlePostEdit}
369                 onPostVote={this.handlePostVote}
370                 onPostReport={this.handlePostReport}
371                 onLockPost={this.handleLockPost}
372                 onDeletePost={this.handleDeletePost}
373                 onRemovePost={this.handleRemovePost}
374                 onSavePost={this.handleSavePost}
375                 onPurgePost={this.handlePurgePost}
376                 onFeaturePost={this.handleFeaturePost}
377               />
378             </div>
379
380             <div className="col-12 col-md-4">
381               <Moderates moderates={personRes.moderates} />
382               {this.amCurrentUser && <Follows />}
383             </div>
384           </div>
385         );
386       }
387     }
388   }
389
390   render() {
391     return <div className="container-lg">{this.renderPersonRes()}</div>;
392   }
393
394   get viewRadios() {
395     return (
396       <div className="btn-group btn-group-toggle flex-wrap mb-2">
397         {this.getRadio(PersonDetailsView.Overview)}
398         {this.getRadio(PersonDetailsView.Comments)}
399         {this.getRadio(PersonDetailsView.Posts)}
400         {this.amCurrentUser && this.getRadio(PersonDetailsView.Saved)}
401       </div>
402     );
403   }
404
405   getRadio(view: PersonDetailsView) {
406     const { view: urlView } = getProfileQueryParams();
407     const active = view === urlView;
408
409     return (
410       <label
411         className={classNames("btn btn-outline-secondary pointer", {
412           active,
413         })}
414       >
415         <input
416           type="radio"
417           value={view}
418           checked={active}
419           onChange={linkEvent(this, this.handleViewChange)}
420         />
421         {i18n.t(view.toLowerCase() as NoOptionI18nKeys)}
422       </label>
423     );
424   }
425
426   get selects() {
427     const { sort } = getProfileQueryParams();
428     const { username } = this.props.match.params;
429
430     const profileRss = `/feeds/u/${username}.xml?sort=${sort}`;
431
432     return (
433       <div className="mb-2">
434         <span className="mr-3">{this.viewRadios}</span>
435         <SortSelect
436           sort={sort}
437           onChange={this.handleSortChange}
438           hideHot
439           hideMostComments
440         />
441         <a href={profileRss} rel={relTags} title="RSS">
442           <Icon icon="rss" classes="text-muted small mx-2" />
443         </a>
444         <link rel="alternate" type="application/atom+xml" href={profileRss} />
445       </div>
446     );
447   }
448
449   userInfo(pv: PersonView) {
450     const {
451       personBlocked,
452       siteRes: { admins },
453       showBanDialog,
454     } = this.state;
455
456     return (
457       pv && (
458         <div>
459           {!isBanned(pv.person) && (
460             <BannerIconHeader
461               banner={pv.person.banner}
462               icon={pv.person.avatar}
463             />
464           )}
465           <div className="mb-3">
466             <div className="">
467               <div className="mb-0 d-flex flex-wrap">
468                 <div>
469                   {pv.person.display_name && (
470                     <h5 className="mb-0">{pv.person.display_name}</h5>
471                   )}
472                   <ul className="list-inline mb-2">
473                     <li className="list-inline-item">
474                       <PersonListing
475                         person={pv.person}
476                         realLink
477                         useApubName
478                         muted
479                         hideAvatar
480                       />
481                     </li>
482                     {isBanned(pv.person) && (
483                       <li className="list-inline-item badge badge-danger">
484                         {i18n.t("banned")}
485                       </li>
486                     )}
487                     {pv.person.deleted && (
488                       <li className="list-inline-item badge badge-danger">
489                         {i18n.t("deleted")}
490                       </li>
491                     )}
492                     {pv.person.admin && (
493                       <li className="list-inline-item badge badge-light">
494                         {i18n.t("admin")}
495                       </li>
496                     )}
497                     {pv.person.bot_account && (
498                       <li className="list-inline-item badge badge-light">
499                         {i18n.t("bot_account").toLowerCase()}
500                       </li>
501                     )}
502                   </ul>
503                 </div>
504                 {this.banDialog(pv)}
505                 <div className="flex-grow-1 unselectable pointer mx-2"></div>
506                 {!this.amCurrentUser && UserService.Instance.myUserInfo && (
507                   <>
508                     <a
509                       className={`d-flex align-self-start btn btn-secondary mr-2 ${
510                         !pv.person.matrix_user_id && "invisible"
511                       }`}
512                       rel={relTags}
513                       href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
514                     >
515                       {i18n.t("send_secure_message")}
516                     </a>
517                     <Link
518                       className={
519                         "d-flex align-self-start btn btn-secondary mr-2"
520                       }
521                       to={`/create_private_message/${pv.person.id}`}
522                     >
523                       {i18n.t("send_message")}
524                     </Link>
525                     {personBlocked ? (
526                       <button
527                         className={
528                           "d-flex align-self-start btn btn-secondary mr-2"
529                         }
530                         onClick={linkEvent(
531                           pv.person.id,
532                           this.handleUnblockPerson
533                         )}
534                       >
535                         {i18n.t("unblock_user")}
536                       </button>
537                     ) : (
538                       <button
539                         className={
540                           "d-flex align-self-start btn btn-secondary mr-2"
541                         }
542                         onClick={linkEvent(
543                           pv.person.id,
544                           this.handleBlockPerson
545                         )}
546                       >
547                         {i18n.t("block_user")}
548                       </button>
549                     )}
550                   </>
551                 )}
552
553                 {canMod(pv.person.id, undefined, admins) &&
554                   !isAdmin(pv.person.id, admins) &&
555                   !showBanDialog &&
556                   (!isBanned(pv.person) ? (
557                     <button
558                       className={
559                         "d-flex align-self-start btn btn-secondary mr-2"
560                       }
561                       onClick={linkEvent(this, this.handleModBanShow)}
562                       aria-label={i18n.t("ban")}
563                     >
564                       {capitalizeFirstLetter(i18n.t("ban"))}
565                     </button>
566                   ) : (
567                     <button
568                       className={
569                         "d-flex align-self-start btn btn-secondary mr-2"
570                       }
571                       onClick={linkEvent(this, this.handleModBanSubmit)}
572                       aria-label={i18n.t("unban")}
573                     >
574                       {capitalizeFirstLetter(i18n.t("unban"))}
575                     </button>
576                   ))}
577               </div>
578               {pv.person.bio && (
579                 <div className="d-flex align-items-center mb-2">
580                   <div
581                     className="md-div"
582                     dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
583                   />
584                 </div>
585               )}
586               <div>
587                 <ul className="list-inline mb-2">
588                   <li className="list-inline-item badge badge-light">
589                     {i18n.t("number_of_posts", {
590                       count: Number(pv.counts.post_count),
591                       formattedCount: numToSI(pv.counts.post_count),
592                     })}
593                   </li>
594                   <li className="list-inline-item badge badge-light">
595                     {i18n.t("number_of_comments", {
596                       count: Number(pv.counts.comment_count),
597                       formattedCount: numToSI(pv.counts.comment_count),
598                     })}
599                   </li>
600                 </ul>
601               </div>
602               <div className="text-muted">
603                 {i18n.t("joined")}{" "}
604                 <MomentTime
605                   published={pv.person.published}
606                   showAgo
607                   ignoreUpdated
608                 />
609               </div>
610               <div className="d-flex align-items-center text-muted mb-2">
611                 <Icon icon="cake" />
612                 <span className="ml-2">
613                   {i18n.t("cake_day_title")}{" "}
614                   {moment
615                     .utc(pv.person.published)
616                     .local()
617                     .format("MMM DD, YYYY")}
618                 </span>
619               </div>
620               {!UserService.Instance.myUserInfo && (
621                 <div className="alert alert-info" role="alert">
622                   {i18n.t("profile_not_logged_in_alert")}
623                 </div>
624               )}
625             </div>
626           </div>
627         </div>
628       )
629     );
630   }
631
632   banDialog(pv: PersonView) {
633     const { showBanDialog } = this.state;
634
635     return (
636       showBanDialog && (
637         <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
638           <div className="form-group row col-12">
639             <label className="col-form-label" htmlFor="profile-ban-reason">
640               {i18n.t("reason")}
641             </label>
642             <input
643               type="text"
644               id="profile-ban-reason"
645               className="form-control mr-2"
646               placeholder={i18n.t("reason")}
647               value={this.state.banReason}
648               onInput={linkEvent(this, this.handleModBanReasonChange)}
649             />
650             <label className="col-form-label" htmlFor={`mod-ban-expires`}>
651               {i18n.t("expires")}
652             </label>
653             <input
654               type="number"
655               id={`mod-ban-expires`}
656               className="form-control mr-2"
657               placeholder={i18n.t("number_of_days")}
658               value={this.state.banExpireDays}
659               onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
660             />
661             <div className="form-group">
662               <div className="form-check">
663                 <input
664                   className="form-check-input"
665                   id="mod-ban-remove-data"
666                   type="checkbox"
667                   checked={this.state.removeData}
668                   onChange={linkEvent(this, this.handleModRemoveDataChange)}
669                 />
670                 <label
671                   className="form-check-label"
672                   htmlFor="mod-ban-remove-data"
673                   title={i18n.t("remove_content_more")}
674                 >
675                   {i18n.t("remove_content")}
676                 </label>
677               </div>
678             </div>
679           </div>
680           {/* TODO hold off on expires until later */}
681           {/* <div class="form-group row"> */}
682           {/*   <label class="col-form-label">Expires</label> */}
683           {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
684           {/* </div> */}
685           <div className="form-group row">
686             <button
687               type="reset"
688               className="btn btn-secondary mr-2"
689               aria-label={i18n.t("cancel")}
690               onClick={linkEvent(this, this.handleModBanSubmitCancel)}
691             >
692               {i18n.t("cancel")}
693             </button>
694             <button
695               type="submit"
696               className="btn btn-secondary"
697               aria-label={i18n.t("ban")}
698             >
699               {i18n.t("ban")} {pv.person.name}
700             </button>
701           </div>
702         </form>
703       )
704     );
705   }
706
707   async updateUrl({ page, sort, view }: Partial<ProfileProps>) {
708     const {
709       page: urlPage,
710       sort: urlSort,
711       view: urlView,
712     } = getProfileQueryParams();
713
714     const queryParams: QueryParams<ProfileProps> = {
715       page: (page ?? urlPage).toString(),
716       sort: sort ?? urlSort,
717       view: view ?? urlView,
718     };
719
720     const { username } = this.props.match.params;
721
722     this.props.history.push(`/u/${username}${getQueryString(queryParams)}`);
723     await this.fetchUserData();
724   }
725
726   handlePageChange(page: number) {
727     this.updateUrl({ page });
728   }
729
730   handleSortChange(sort: SortType) {
731     this.updateUrl({ sort, page: 1 });
732   }
733
734   handleViewChange(i: Profile, event: any) {
735     i.updateUrl({
736       view: PersonDetailsView[event.target.value],
737       page: 1,
738     });
739   }
740
741   handleModBanShow(i: Profile) {
742     i.setState({ showBanDialog: true });
743   }
744
745   handleModBanReasonChange(i: Profile, event: any) {
746     i.setState({ banReason: event.target.value });
747   }
748
749   handleModBanExpireDaysChange(i: Profile, event: any) {
750     i.setState({ banExpireDays: event.target.value });
751   }
752
753   handleModRemoveDataChange(i: Profile, event: any) {
754     i.setState({ removeData: event.target.checked });
755   }
756
757   handleModBanSubmitCancel(i: Profile) {
758     i.setState({ showBanDialog: false });
759   }
760
761   async handleModBanSubmit(i: Profile, event: any) {
762     event.preventDefault();
763     const { removeData, banReason, banExpireDays } = i.state;
764
765     const personRes = i.state.personRes;
766
767     if (personRes.state == "success") {
768       const person = personRes.data.person_view.person;
769       const ban = !person.banned;
770
771       // If its an unban, restore all their data
772       if (!ban) {
773         i.setState({ removeData: false });
774       }
775
776       const res = await HttpService.client.banPerson({
777         person_id: person.id,
778         ban,
779         remove_data: removeData,
780         reason: banReason,
781         expires: futureDaysToUnixTime(banExpireDays),
782         auth: myAuthRequired(),
783       });
784       // TODO
785       this.updateBan(res);
786       i.setState({ showBanDialog: false });
787     }
788   }
789
790   async toggleBlockPerson(recipientId: number, block: boolean) {
791     const res = await HttpService.client.blockPerson({
792       person_id: recipientId,
793       block,
794       auth: myAuthRequired(),
795     });
796     if (res.state == "success") {
797       updatePersonBlock(res.data);
798     }
799   }
800
801   handleUnblockPerson(personId: number) {
802     this.toggleBlockPerson(personId, false);
803   }
804
805   handleBlockPerson(personId: number) {
806     this.toggleBlockPerson(personId, true);
807   }
808
809   async handleAddModToCommunity(form: AddModToCommunity) {
810     // TODO not sure what to do here
811     await HttpService.client.addModToCommunity(form);
812   }
813
814   async handlePurgePerson(form: PurgePerson) {
815     const purgePersonRes = await HttpService.client.purgePerson(form);
816     this.purgeItem(purgePersonRes);
817   }
818
819   async handlePurgeComment(form: PurgeComment) {
820     const purgeCommentRes = await HttpService.client.purgeComment(form);
821     this.purgeItem(purgeCommentRes);
822   }
823
824   async handlePurgePost(form: PurgePost) {
825     const purgeRes = await HttpService.client.purgePost(form);
826     this.purgeItem(purgeRes);
827   }
828
829   async handleBlockPersonAlt(form: BlockPerson) {
830     const blockPersonRes = await HttpService.client.blockPerson(form);
831     if (blockPersonRes.state === "success") {
832       updatePersonBlock(blockPersonRes.data);
833     }
834   }
835
836   async handleCreateComment(form: CreateComment) {
837     const createCommentRes = await HttpService.client.createComment(form);
838     this.createAndUpdateComments(createCommentRes);
839
840     return createCommentRes;
841   }
842
843   async handleEditComment(form: EditComment) {
844     const editCommentRes = await HttpService.client.editComment(form);
845     this.findAndUpdateComment(editCommentRes);
846
847     return editCommentRes;
848   }
849
850   async handleDeleteComment(form: DeleteComment) {
851     const deleteCommentRes = await HttpService.client.deleteComment(form);
852     this.findAndUpdateComment(deleteCommentRes);
853   }
854
855   async handleDeletePost(form: DeletePost) {
856     const deleteRes = await HttpService.client.deletePost(form);
857     this.findAndUpdatePost(deleteRes);
858   }
859
860   async handleRemovePost(form: RemovePost) {
861     const removeRes = await HttpService.client.removePost(form);
862     this.findAndUpdatePost(removeRes);
863   }
864
865   async handleRemoveComment(form: RemoveComment) {
866     const removeCommentRes = await HttpService.client.removeComment(form);
867     this.findAndUpdateComment(removeCommentRes);
868   }
869
870   async handleSaveComment(form: SaveComment) {
871     const saveCommentRes = await HttpService.client.saveComment(form);
872     this.findAndUpdateComment(saveCommentRes);
873   }
874
875   async handleSavePost(form: SavePost) {
876     const saveRes = await HttpService.client.savePost(form);
877     this.findAndUpdatePost(saveRes);
878   }
879
880   async handleFeaturePost(form: FeaturePost) {
881     const featureRes = await HttpService.client.featurePost(form);
882     this.findAndUpdatePost(featureRes);
883   }
884
885   async handleCommentVote(form: CreateCommentLike) {
886     const voteRes = await HttpService.client.likeComment(form);
887     this.findAndUpdateComment(voteRes);
888   }
889
890   async handlePostVote(form: CreatePostLike) {
891     const voteRes = await HttpService.client.likePost(form);
892     this.findAndUpdatePost(voteRes);
893   }
894
895   async handlePostEdit(form: EditPost) {
896     const res = await HttpService.client.editPost(form);
897     this.findAndUpdatePost(res);
898   }
899
900   async handleCommentReport(form: CreateCommentReport) {
901     const reportRes = await HttpService.client.createCommentReport(form);
902     if (reportRes.state === "success") {
903       toast(i18n.t("report_created"));
904     }
905   }
906
907   async handlePostReport(form: CreatePostReport) {
908     const reportRes = await HttpService.client.createPostReport(form);
909     if (reportRes.state === "success") {
910       toast(i18n.t("report_created"));
911     }
912   }
913
914   async handleLockPost(form: LockPost) {
915     const lockRes = await HttpService.client.lockPost(form);
916     this.findAndUpdatePost(lockRes);
917   }
918
919   async handleDistinguishComment(form: DistinguishComment) {
920     const distinguishRes = await HttpService.client.distinguishComment(form);
921     this.findAndUpdateComment(distinguishRes);
922   }
923
924   async handleAddAdmin(form: AddAdmin) {
925     const addAdminRes = await HttpService.client.addAdmin(form);
926
927     if (addAdminRes.state == "success") {
928       this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
929     }
930   }
931
932   async handleTransferCommunity(form: TransferCommunity) {
933     await HttpService.client.transferCommunity(form);
934     toast(i18n.t("transfer_community"));
935   }
936
937   async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
938     const readRes = await HttpService.client.markCommentReplyAsRead(form);
939     this.findAndUpdateCommentReply(readRes);
940   }
941
942   async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
943     // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
944     await HttpService.client.markPersonMentionAsRead(form);
945   }
946
947   async handleBanFromCommunity(form: BanFromCommunity) {
948     const banRes = await HttpService.client.banFromCommunity(form);
949     this.updateBanFromCommunity(banRes);
950   }
951
952   async handleBanPerson(form: BanPerson) {
953     const banRes = await HttpService.client.banPerson(form);
954     this.updateBan(banRes);
955   }
956
957   updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
958     // Maybe not necessary
959     if (banRes.state === "success") {
960       this.setState(s => {
961         if (s.personRes.state == "success") {
962           s.personRes.data.posts
963             .filter(c => c.creator.id === banRes.data.person_view.person.id)
964             .forEach(
965               c => (c.creator_banned_from_community = banRes.data.banned)
966             );
967
968           s.personRes.data.comments
969             .filter(c => c.creator.id === banRes.data.person_view.person.id)
970             .forEach(
971               c => (c.creator_banned_from_community = banRes.data.banned)
972             );
973         }
974         return s;
975       });
976     }
977   }
978
979   updateBan(banRes: RequestState<BanPersonResponse>) {
980     // Maybe not necessary
981     if (banRes.state == "success") {
982       this.setState(s => {
983         if (s.personRes.state == "success") {
984           s.personRes.data.posts
985             .filter(c => c.creator.id == banRes.data.person_view.person.id)
986             .forEach(c => (c.creator.banned = banRes.data.banned));
987           s.personRes.data.comments
988             .filter(c => c.creator.id == banRes.data.person_view.person.id)
989             .forEach(c => (c.creator.banned = banRes.data.banned));
990         }
991         return s;
992       });
993     }
994   }
995
996   purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
997     if (purgeRes.state == "success") {
998       toast(i18n.t("purge_success"));
999       this.context.router.history.push(`/`);
1000     }
1001   }
1002
1003   findAndUpdateComment(res: RequestState<CommentResponse>) {
1004     this.setState(s => {
1005       if (s.personRes.state == "success" && res.state == "success") {
1006         s.personRes.data.comments = editComment(
1007           res.data.comment_view,
1008           s.personRes.data.comments
1009         );
1010         s.finished.set(res.data.comment_view.comment.id, true);
1011       }
1012       return s;
1013     });
1014   }
1015
1016   createAndUpdateComments(res: RequestState<CommentResponse>) {
1017     this.setState(s => {
1018       if (s.personRes.state == "success" && res.state == "success") {
1019         s.personRes.data.comments.unshift(res.data.comment_view);
1020         // Set finished for the parent
1021         s.finished.set(
1022           getCommentParentId(res.data.comment_view.comment) ?? 0,
1023           true
1024         );
1025       }
1026       return s;
1027     });
1028   }
1029
1030   findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
1031     this.setState(s => {
1032       if (s.personRes.state == "success" && res.state == "success") {
1033         s.personRes.data.comments = editWith(
1034           res.data.comment_reply_view,
1035           s.personRes.data.comments
1036         );
1037       }
1038       return s;
1039     });
1040   }
1041
1042   findAndUpdatePost(res: RequestState<PostResponse>) {
1043     this.setState(s => {
1044       if (s.personRes.state == "success" && res.state == "success") {
1045         s.personRes.data.posts = editPost(
1046           res.data.post_view,
1047           s.personRes.data.posts
1048         );
1049       }
1050       return s;
1051     });
1052   }
1053 }