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