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