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