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