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