]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/profile.tsx
39979e292b490f6cee04a8538c6d3facd736ed87
[lemmy-ui.git] / src / shared / components / person / profile.tsx
1 import {
2   editComment,
3   editPost,
4   editWith,
5   enableDownvotes,
6   enableNsfw,
7   getCommentParentId,
8   myAuth,
9   myAuthRequired,
10   setIsoData,
11   updatePersonBlock,
12 } from "@utils/app";
13 import { restoreScrollPosition, saveScrollPosition } from "@utils/browser";
14 import {
15   capitalizeFirstLetter,
16   futureDaysToUnixTime,
17   getPageFromString,
18   getQueryParams,
19   getQueryString,
20   numToSI,
21   randomStr,
22 } from "@utils/helpers";
23 import { canMod, isAdmin, isBanned } from "@utils/roles";
24 import type { QueryParams } from "@utils/types";
25 import { RouteDataResponse } from "@utils/types";
26 import classNames from "classnames";
27 import format from "date-fns/format";
28 import parseISO from "date-fns/parseISO";
29 import { NoOptionI18nKeys } from "i18next";
30 import { Component, linkEvent } from "inferno";
31 import { Link } from "inferno-router";
32 import { RouteComponentProps } from "inferno-router/dist/Route";
33 import {
34   AddAdmin,
35   AddModToCommunity,
36   BanFromCommunity,
37   BanFromCommunityResponse,
38   BanPerson,
39   BanPersonResponse,
40   BlockPerson,
41   CommentId,
42   CommentReplyResponse,
43   CommentResponse,
44   Community,
45   CommunityModeratorView,
46   CreateComment,
47   CreateCommentLike,
48   CreateCommentReport,
49   CreatePostLike,
50   CreatePostReport,
51   DeleteComment,
52   DeletePost,
53   DistinguishComment,
54   EditComment,
55   EditPost,
56   FeaturePost,
57   GetPersonDetails,
58   GetPersonDetailsResponse,
59   GetSiteResponse,
60   LockPost,
61   MarkCommentReplyAsRead,
62   MarkPersonMentionAsRead,
63   PersonView,
64   PostResponse,
65   PurgeComment,
66   PurgeItemResponse,
67   PurgePerson,
68   PurgePost,
69   RemoveComment,
70   RemovePost,
71   SaveComment,
72   SavePost,
73   SortType,
74   TransferCommunity,
75 } from "lemmy-js-client";
76 import { fetchLimit, relTags } from "../../config";
77 import { InitialFetchRequest, PersonDetailsView } from "../../interfaces";
78 import { mdToHtml } from "../../markdown";
79 import { FirstLoadService, I18NextService, UserService } from "../../services";
80 import { HttpService, RequestState } from "../../services/HttpService";
81 import { setupTippy } from "../../tippy";
82 import { toast } from "../../toast";
83 import { BannerIconHeader } from "../common/banner-icon-header";
84 import { HtmlTags } from "../common/html-tags";
85 import { Icon, Spinner } from "../common/icon";
86 import { MomentTime } from "../common/moment-time";
87 import { SortSelect } from "../common/sort-select";
88 import { UserBadges } from "../common/user-badges";
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         <h2 className="h5">{I18NextService.i18n.t(translationKey)}</h2>
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     this.handleModBanSubmit = this.handleModBanSubmit.bind(this);
211
212     // Only fetch the data if coming from another route
213     if (FirstLoadService.isFirstLoad) {
214       this.state = {
215         ...this.state,
216         personRes: this.isoData.routeData.personResponse,
217         isIsomorphic: true,
218       };
219     }
220   }
221
222   async componentDidMount() {
223     if (!this.state.isIsomorphic) {
224       await this.fetchUserData();
225     }
226     setupTippy();
227   }
228
229   componentWillUnmount() {
230     saveScrollPosition(this.context);
231   }
232
233   async fetchUserData() {
234     const { page, sort, view } = getProfileQueryParams();
235
236     this.setState({ personRes: { state: "loading" } });
237     this.setState({
238       personRes: await HttpService.client.getPersonDetails({
239         username: this.props.match.params.username,
240         sort,
241         saved_only: view === PersonDetailsView.Saved,
242         page,
243         limit: fetchLimit,
244         auth: myAuth(),
245       }),
246     });
247     restoreScrollPosition(this.context);
248     this.setPersonBlock();
249   }
250
251   get amCurrentUser() {
252     if (this.state.personRes.state === "success") {
253       return (
254         UserService.Instance.myUserInfo?.local_user_view.person.id ===
255         this.state.personRes.data.person_view.person.id
256       );
257     } else {
258       return false;
259     }
260   }
261
262   setPersonBlock() {
263     const mui = UserService.Instance.myUserInfo;
264     const res = this.state.personRes;
265
266     if (mui && res.state === "success") {
267       this.setState({
268         personBlocked: mui.person_blocks.some(
269           ({ target: { id } }) => id === res.data.person_view.person.id
270         ),
271       });
272     }
273   }
274
275   static async fetchInitialData({
276     client,
277     path,
278     query: { page, sort, view: urlView },
279     auth,
280   }: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<ProfileData> {
281     const pathSplit = path.split("/");
282
283     const username = pathSplit[2];
284     const view = getViewFromProps(urlView);
285
286     const form: GetPersonDetails = {
287       username: username,
288       sort: getSortTypeFromQuery(sort),
289       saved_only: view === PersonDetailsView.Saved,
290       page: getPageFromString(page),
291       limit: fetchLimit,
292       auth,
293     };
294
295     return {
296       personResponse: await client.getPersonDetails(form),
297     };
298   }
299
300   get documentTitle(): string {
301     const siteName = this.state.siteRes.site_view.site.name;
302     const res = this.state.personRes;
303     return res.state == "success"
304       ? `@${res.data.person_view.person.name} - ${siteName}`
305       : siteName;
306   }
307
308   renderPersonRes() {
309     switch (this.state.personRes.state) {
310       case "loading":
311         return (
312           <h5>
313             <Spinner large />
314           </h5>
315         );
316       case "success": {
317         const siteRes = this.state.siteRes;
318         const personRes = this.state.personRes.data;
319         const { page, sort, view } = getProfileQueryParams();
320
321         return (
322           <div className="row">
323             <div className="col-12 col-md-8">
324               <HtmlTags
325                 title={this.documentTitle}
326                 path={this.context.router.route.match.url}
327                 description={personRes.person_view.person.bio}
328                 image={personRes.person_view.person.avatar}
329               />
330
331               {this.userInfo(personRes.person_view)}
332
333               <hr />
334
335               {this.selects}
336
337               <PersonDetails
338                 personRes={personRes}
339                 admins={siteRes.admins}
340                 sort={sort}
341                 page={page}
342                 limit={fetchLimit}
343                 finished={this.state.finished}
344                 enableDownvotes={enableDownvotes(siteRes)}
345                 enableNsfw={enableNsfw(siteRes)}
346                 view={view}
347                 onPageChange={this.handlePageChange}
348                 allLanguages={siteRes.all_languages}
349                 siteLanguages={siteRes.discussion_languages}
350                 // TODO all the forms here
351                 onSaveComment={this.handleSaveComment}
352                 onBlockPerson={this.handleBlockPersonAlt}
353                 onDeleteComment={this.handleDeleteComment}
354                 onRemoveComment={this.handleRemoveComment}
355                 onCommentVote={this.handleCommentVote}
356                 onCommentReport={this.handleCommentReport}
357                 onDistinguishComment={this.handleDistinguishComment}
358                 onAddModToCommunity={this.handleAddModToCommunity}
359                 onAddAdmin={this.handleAddAdmin}
360                 onTransferCommunity={this.handleTransferCommunity}
361                 onPurgeComment={this.handlePurgeComment}
362                 onPurgePerson={this.handlePurgePerson}
363                 onCommentReplyRead={this.handleCommentReplyRead}
364                 onPersonMentionRead={this.handlePersonMentionRead}
365                 onBanPersonFromCommunity={this.handleBanFromCommunity}
366                 onBanPerson={this.handleBanPerson}
367                 onCreateComment={this.handleCreateComment}
368                 onEditComment={this.handleEditComment}
369                 onPostEdit={this.handlePostEdit}
370                 onPostVote={this.handlePostVote}
371                 onPostReport={this.handlePostReport}
372                 onLockPost={this.handleLockPost}
373                 onDeletePost={this.handleDeletePost}
374                 onRemovePost={this.handleRemovePost}
375                 onSavePost={this.handleSavePost}
376                 onPurgePost={this.handlePurgePost}
377                 onFeaturePost={this.handleFeaturePost}
378               />
379             </div>
380
381             <div className="col-12 col-md-4">
382               <Moderates moderates={personRes.moderates} />
383               {this.amCurrentUser && <Follows />}
384             </div>
385           </div>
386         );
387       }
388     }
389   }
390
391   render() {
392     return (
393       <div className="person-profile container-lg">
394         {this.renderPersonRes()}
395       </div>
396     );
397   }
398
399   get viewRadios() {
400     return (
401       <div className="btn-group btn-group-toggle flex-wrap mb-2" role="group">
402         {this.getRadio(PersonDetailsView.Overview)}
403         {this.getRadio(PersonDetailsView.Comments)}
404         {this.getRadio(PersonDetailsView.Posts)}
405         {this.amCurrentUser && this.getRadio(PersonDetailsView.Saved)}
406       </div>
407     );
408   }
409
410   getRadio(view: PersonDetailsView) {
411     const { view: urlView } = getProfileQueryParams();
412     const active = view === urlView;
413     const radioId = randomStr();
414
415     return (
416       <>
417         <input
418           id={radioId}
419           type="radio"
420           className="btn-check"
421           value={view}
422           checked={active}
423           onChange={linkEvent(this, this.handleViewChange)}
424         />
425         <label
426           htmlFor={radioId}
427           className={classNames("btn btn-outline-secondary pointer", {
428             active,
429           })}
430         >
431           {I18NextService.i18n.t(view.toLowerCase() as NoOptionI18nKeys)}
432         </label>
433       </>
434     );
435   }
436
437   get selects() {
438     const { sort } = getProfileQueryParams();
439     const { username } = this.props.match.params;
440
441     const profileRss = `/feeds/u/${username}.xml?sort=${sort}`;
442
443     return (
444       <div className="mb-2">
445         <span className="me-3">{this.viewRadios}</span>
446         <SortSelect
447           sort={sort}
448           onChange={this.handleSortChange}
449           hideHot
450           hideMostComments
451         />
452         <a href={profileRss} rel={relTags} title="RSS">
453           <Icon icon="rss" classes="text-muted small mx-2" />
454         </a>
455         <link rel="alternate" type="application/atom+xml" href={profileRss} />
456       </div>
457     );
458   }
459
460   userInfo(pv: PersonView) {
461     const {
462       personBlocked,
463       siteRes: { admins },
464       showBanDialog,
465     } = this.state;
466
467     return (
468       pv && (
469         <div>
470           {!isBanned(pv.person) && (
471             <BannerIconHeader
472               banner={pv.person.banner}
473               icon={pv.person.avatar}
474             />
475           )}
476           <div className="mb-3">
477             <div className="">
478               <div className="mb-0 d-flex flex-wrap">
479                 <div>
480                   {pv.person.display_name && (
481                     <h1 className="h4 mb-4">{pv.person.display_name}</h1>
482                   )}
483                   <ul className="list-inline mb-2">
484                     <li className="list-inline-item">
485                       <PersonListing
486                         person={pv.person}
487                         realLink
488                         useApubName
489                         muted
490                         hideAvatar
491                       />
492                     </li>
493                     <li className="list-inline-item">
494                       <UserBadges
495                         classNames="ms-1"
496                         isBanned={isBanned(pv.person)}
497                         isDeleted={pv.person.deleted}
498                         isAdmin={pv.person.admin}
499                         isBot={pv.person.bot_account}
500                       />
501                     </li>
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 me-2 ${
510                         !pv.person.matrix_user_id && "invisible"
511                       }`}
512                       rel={relTags}
513                       href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
514                     >
515                       {I18NextService.i18n.t("send_secure_message")}
516                     </a>
517                     <Link
518                       className={
519                         "d-flex align-self-start btn btn-secondary me-2"
520                       }
521                       to={`/create_private_message/${pv.person.id}`}
522                     >
523                       {I18NextService.i18n.t("send_message")}
524                     </Link>
525                     {personBlocked ? (
526                       <button
527                         className={
528                           "d-flex align-self-start btn btn-secondary me-2"
529                         }
530                         onClick={linkEvent(
531                           pv.person.id,
532                           this.handleUnblockPerson
533                         )}
534                       >
535                         {I18NextService.i18n.t("unblock_user")}
536                       </button>
537                     ) : (
538                       <button
539                         className={
540                           "d-flex align-self-start btn btn-secondary me-2"
541                         }
542                         onClick={linkEvent(
543                           pv.person.id,
544                           this.handleBlockPerson
545                         )}
546                       >
547                         {I18NextService.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 me-2"
560                       }
561                       onClick={linkEvent(this, this.handleModBanShow)}
562                       aria-label={I18NextService.i18n.t("ban")}
563                     >
564                       {capitalizeFirstLetter(I18NextService.i18n.t("ban"))}
565                     </button>
566                   ) : (
567                     <button
568                       className={
569                         "d-flex align-self-start btn btn-secondary me-2"
570                       }
571                       onClick={linkEvent(this, this.handleModBanSubmit)}
572                       aria-label={I18NextService.i18n.t("unban")}
573                     >
574                       {capitalizeFirstLetter(I18NextService.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 text-bg-light">
589                     {I18NextService.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 text-bg-light">
595                     {I18NextService.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                 {I18NextService.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="ms-2">
613                   {I18NextService.i18n.t("cake_day_title")}{" "}
614                   {format(parseISO(pv.person.published), "PPP")}
615                 </span>
616               </div>
617               {!UserService.Instance.myUserInfo && (
618                 <div className="alert alert-info" role="alert">
619                   {I18NextService.i18n.t("profile_not_logged_in_alert")}
620                 </div>
621               )}
622             </div>
623           </div>
624         </div>
625       )
626     );
627   }
628
629   banDialog(pv: PersonView) {
630     const { showBanDialog } = this.state;
631
632     return (
633       showBanDialog && (
634         <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
635           <div className="mb-3 row col-12">
636             <label className="col-form-label" htmlFor="profile-ban-reason">
637               {I18NextService.i18n.t("reason")}
638             </label>
639             <input
640               type="text"
641               id="profile-ban-reason"
642               className="form-control me-2"
643               placeholder={I18NextService.i18n.t("reason")}
644               value={this.state.banReason}
645               onInput={linkEvent(this, this.handleModBanReasonChange)}
646             />
647             <label className="col-form-label" htmlFor="mod-ban-expires">
648               {I18NextService.i18n.t("expires")}
649             </label>
650             <input
651               type="number"
652               id="mod-ban-expires"
653               className="form-control me-2"
654               placeholder={I18NextService.i18n.t("number_of_days")}
655               value={this.state.banExpireDays}
656               onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
657             />
658             <div className="input-group mb-3">
659               <div className="form-check">
660                 <input
661                   className="form-check-input"
662                   id="mod-ban-remove-data"
663                   type="checkbox"
664                   checked={this.state.removeData}
665                   onChange={linkEvent(this, this.handleModRemoveDataChange)}
666                 />
667                 <label
668                   className="form-check-label"
669                   htmlFor="mod-ban-remove-data"
670                   title={I18NextService.i18n.t("remove_content_more")}
671                 >
672                   {I18NextService.i18n.t("remove_content")}
673                 </label>
674               </div>
675             </div>
676           </div>
677           {/* TODO hold off on expires until later */}
678           {/* <div class="mb-3 row"> */}
679           {/*   <label class="col-form-label">Expires</label> */}
680           {/*   <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
681           {/* </div> */}
682           <div className="mb-3 row">
683             <button
684               type="reset"
685               className="btn btn-secondary me-2"
686               aria-label={I18NextService.i18n.t("cancel")}
687               onClick={linkEvent(this, this.handleModBanSubmitCancel)}
688             >
689               {I18NextService.i18n.t("cancel")}
690             </button>
691           </div>
692           <div className="mb-3 row">
693             <button
694               type="submit"
695               className="btn btn-secondary"
696               aria-label={I18NextService.i18n.t("ban")}
697             >
698               {I18NextService.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(I18NextService.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(I18NextService.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(I18NextService.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           s.personRes.data.person_view.person.banned = banRes.data.banned;
990         }
991         return s;
992       });
993     }
994   }
995
996   purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
997     if (purgeRes.state == "success") {
998       toast(I18NextService.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 }