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