]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/profile.tsx
feat: Bootstrap 5 (#1378)
[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 <div className="container-lg">{this.renderPersonRes()}</div>;
393   }
394
395   get viewRadios() {
396     return (
397       <div className="btn-group btn-group-toggle flex-wrap mb-2">
398         {this.getRadio(PersonDetailsView.Overview)}
399         {this.getRadio(PersonDetailsView.Comments)}
400         {this.getRadio(PersonDetailsView.Posts)}
401         {this.amCurrentUser && this.getRadio(PersonDetailsView.Saved)}
402       </div>
403     );
404   }
405
406   getRadio(view: PersonDetailsView) {
407     const { view: urlView } = getProfileQueryParams();
408     const active = view === urlView;
409
410     return (
411       <label
412         className={classNames("btn btn-outline-secondary pointer", {
413           active,
414         })}
415       >
416         <input
417           type="radio"
418           className="btn-check"
419           value={view}
420           checked={active}
421           onChange={linkEvent(this, this.handleViewChange)}
422         />
423         {i18n.t(view.toLowerCase() as NoOptionI18nKeys)}
424       </label>
425     );
426   }
427
428   get selects() {
429     const { sort } = getProfileQueryParams();
430     const { username } = this.props.match.params;
431
432     const profileRss = `/feeds/u/${username}.xml?sort=${sort}`;
433
434     return (
435       <div className="mb-2">
436         <span className="me-3">{this.viewRadios}</span>
437         <SortSelect
438           sort={sort}
439           onChange={this.handleSortChange}
440           hideHot
441           hideMostComments
442         />
443         <a href={profileRss} rel={relTags} title="RSS">
444           <Icon icon="rss" classes="text-muted small mx-2" />
445         </a>
446         <link rel="alternate" type="application/atom+xml" href={profileRss} />
447       </div>
448     );
449   }
450
451   userInfo(pv: PersonView) {
452     const {
453       personBlocked,
454       siteRes: { admins },
455       showBanDialog,
456     } = this.state;
457
458     return (
459       pv && (
460         <div>
461           {!isBanned(pv.person) && (
462             <BannerIconHeader
463               banner={pv.person.banner}
464               icon={pv.person.avatar}
465             />
466           )}
467           <div className="mb-3">
468             <div className="">
469               <div className="mb-0 d-flex flex-wrap">
470                 <div>
471                   {pv.person.display_name && (
472                     <h5 className="mb-0">{pv.person.display_name}</h5>
473                   )}
474                   <ul className="list-inline mb-2">
475                     <li className="list-inline-item">
476                       <PersonListing
477                         person={pv.person}
478                         realLink
479                         useApubName
480                         muted
481                         hideAvatar
482                       />
483                     </li>
484                     {isBanned(pv.person) && (
485                       <li className="list-inline-item badge text-bg-danger">
486                         {i18n.t("banned")}
487                       </li>
488                     )}
489                     {pv.person.deleted && (
490                       <li className="list-inline-item badge text-bg-danger">
491                         {i18n.t("deleted")}
492                       </li>
493                     )}
494                     {pv.person.admin && (
495                       <li className="list-inline-item badge text-bg-light">
496                         {i18n.t("admin")}
497                       </li>
498                     )}
499                     {pv.person.bot_account && (
500                       <li className="list-inline-item badge text-bg-light">
501                         {i18n.t("bot_account").toLowerCase()}
502                       </li>
503                     )}
504                   </ul>
505                 </div>
506                 {this.banDialog(pv)}
507                 <div className="flex-grow-1 unselectable pointer mx-2"></div>
508                 {!this.amCurrentUser && UserService.Instance.myUserInfo && (
509                   <>
510                     <a
511                       className={`d-flex align-self-start btn btn-secondary me-2 ${
512                         !pv.person.matrix_user_id && "invisible"
513                       }`}
514                       rel={relTags}
515                       href={`https://matrix.to/#/${pv.person.matrix_user_id}`}
516                     >
517                       {i18n.t("send_secure_message")}
518                     </a>
519                     <Link
520                       className={
521                         "d-flex align-self-start btn btn-secondary me-2"
522                       }
523                       to={`/create_private_message/${pv.person.id}`}
524                     >
525                       {i18n.t("send_message")}
526                     </Link>
527                     {personBlocked ? (
528                       <button
529                         className={
530                           "d-flex align-self-start btn btn-secondary me-2"
531                         }
532                         onClick={linkEvent(
533                           pv.person.id,
534                           this.handleUnblockPerson
535                         )}
536                       >
537                         {i18n.t("unblock_user")}
538                       </button>
539                     ) : (
540                       <button
541                         className={
542                           "d-flex align-self-start btn btn-secondary me-2"
543                         }
544                         onClick={linkEvent(
545                           pv.person.id,
546                           this.handleBlockPerson
547                         )}
548                       >
549                         {i18n.t("block_user")}
550                       </button>
551                     )}
552                   </>
553                 )}
554
555                 {canMod(pv.person.id, undefined, admins) &&
556                   !isAdmin(pv.person.id, admins) &&
557                   !showBanDialog &&
558                   (!isBanned(pv.person) ? (
559                     <button
560                       className={
561                         "d-flex align-self-start btn btn-secondary me-2"
562                       }
563                       onClick={linkEvent(this, this.handleModBanShow)}
564                       aria-label={i18n.t("ban")}
565                     >
566                       {capitalizeFirstLetter(i18n.t("ban"))}
567                     </button>
568                   ) : (
569                     <button
570                       className={
571                         "d-flex align-self-start btn btn-secondary me-2"
572                       }
573                       onClick={linkEvent(this, this.handleModBanSubmit)}
574                       aria-label={i18n.t("unban")}
575                     >
576                       {capitalizeFirstLetter(i18n.t("unban"))}
577                     </button>
578                   ))}
579               </div>
580               {pv.person.bio && (
581                 <div className="d-flex align-items-center mb-2">
582                   <div
583                     className="md-div"
584                     dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
585                   />
586                 </div>
587               )}
588               <div>
589                 <ul className="list-inline mb-2">
590                   <li className="list-inline-item badge text-bg-light">
591                     {i18n.t("number_of_posts", {
592                       count: Number(pv.counts.post_count),
593                       formattedCount: numToSI(pv.counts.post_count),
594                     })}
595                   </li>
596                   <li className="list-inline-item badge text-bg-light">
597                     {i18n.t("number_of_comments", {
598                       count: Number(pv.counts.comment_count),
599                       formattedCount: numToSI(pv.counts.comment_count),
600                     })}
601                   </li>
602                 </ul>
603               </div>
604               <div className="text-muted">
605                 {i18n.t("joined")}{" "}
606                 <MomentTime
607                   published={pv.person.published}
608                   showAgo
609                   ignoreUpdated
610                 />
611               </div>
612               <div className="d-flex align-items-center text-muted mb-2">
613                 <Icon icon="cake" />
614                 <span className="ms-2">
615                   {i18n.t("cake_day_title")}{" "}
616                   {moment
617                     .utc(pv.person.published)
618                     .local()
619                     .format("MMM DD, YYYY")}
620                 </span>
621               </div>
622               {!UserService.Instance.myUserInfo && (
623                 <div className="alert alert-info" role="alert">
624                   {i18n.t("profile_not_logged_in_alert")}
625                 </div>
626               )}
627             </div>
628           </div>
629         </div>
630       )
631     );
632   }
633
634   banDialog(pv: PersonView) {
635     const { showBanDialog } = this.state;
636
637     return (
638       showBanDialog && (
639         <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
640           <div className="mb-3 row col-12">
641             <label className="col-form-label" htmlFor="profile-ban-reason">
642               {i18n.t("reason")}
643             </label>
644             <input
645               type="text"
646               id="profile-ban-reason"
647               className="form-control me-2"
648               placeholder={i18n.t("reason")}
649               value={this.state.banReason}
650               onInput={linkEvent(this, this.handleModBanReasonChange)}
651             />
652             <label className="col-form-label" htmlFor={`mod-ban-expires`}>
653               {i18n.t("expires")}
654             </label>
655             <input
656               type="number"
657               id={`mod-ban-expires`}
658               className="form-control me-2"
659               placeholder={i18n.t("number_of_days")}
660               value={this.state.banExpireDays}
661               onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
662             />
663             <div className="input-group mb-3">
664               <div className="form-check">
665                 <input
666                   className="form-check-input"
667                   id="mod-ban-remove-data"
668                   type="checkbox"
669                   checked={this.state.removeData}
670                   onChange={linkEvent(this, this.handleModRemoveDataChange)}
671                 />
672                 <label
673                   className="form-check-label"
674                   htmlFor="mod-ban-remove-data"
675                   title={i18n.t("remove_content_more")}
676                 >
677                   {i18n.t("remove_content")}
678                 </label>
679               </div>
680             </div>
681           </div>
682           {/* TODO hold off on expires until later */}
683           {/* <div class="mb-3 row"> */}
684           {/*   <label class="col-form-label">Expires</label> */}
685           {/*   <input type="date" class="form-control me-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
686           {/* </div> */}
687           <div className="mb-3 row">
688             <button
689               type="reset"
690               className="btn btn-secondary me-2"
691               aria-label={i18n.t("cancel")}
692               onClick={linkEvent(this, this.handleModBanSubmitCancel)}
693             >
694               {i18n.t("cancel")}
695             </button>
696             <button
697               type="submit"
698               className="btn btn-secondary"
699               aria-label={i18n.t("ban")}
700             >
701               {i18n.t("ban")} {pv.person.name}
702             </button>
703           </div>
704         </form>
705       )
706     );
707   }
708
709   async updateUrl({ page, sort, view }: Partial<ProfileProps>) {
710     const {
711       page: urlPage,
712       sort: urlSort,
713       view: urlView,
714     } = getProfileQueryParams();
715
716     const queryParams: QueryParams<ProfileProps> = {
717       page: (page ?? urlPage).toString(),
718       sort: sort ?? urlSort,
719       view: view ?? urlView,
720     };
721
722     const { username } = this.props.match.params;
723
724     this.props.history.push(`/u/${username}${getQueryString(queryParams)}`);
725     await this.fetchUserData();
726   }
727
728   handlePageChange(page: number) {
729     this.updateUrl({ page });
730   }
731
732   handleSortChange(sort: SortType) {
733     this.updateUrl({ sort, page: 1 });
734   }
735
736   handleViewChange(i: Profile, event: any) {
737     i.updateUrl({
738       view: PersonDetailsView[event.target.value],
739       page: 1,
740     });
741   }
742
743   handleModBanShow(i: Profile) {
744     i.setState({ showBanDialog: true });
745   }
746
747   handleModBanReasonChange(i: Profile, event: any) {
748     i.setState({ banReason: event.target.value });
749   }
750
751   handleModBanExpireDaysChange(i: Profile, event: any) {
752     i.setState({ banExpireDays: event.target.value });
753   }
754
755   handleModRemoveDataChange(i: Profile, event: any) {
756     i.setState({ removeData: event.target.checked });
757   }
758
759   handleModBanSubmitCancel(i: Profile) {
760     i.setState({ showBanDialog: false });
761   }
762
763   async handleModBanSubmit(i: Profile, event: any) {
764     event.preventDefault();
765     const { removeData, banReason, banExpireDays } = i.state;
766
767     const personRes = i.state.personRes;
768
769     if (personRes.state == "success") {
770       const person = personRes.data.person_view.person;
771       const ban = !person.banned;
772
773       // If its an unban, restore all their data
774       if (!ban) {
775         i.setState({ removeData: false });
776       }
777
778       const res = await HttpService.client.banPerson({
779         person_id: person.id,
780         ban,
781         remove_data: removeData,
782         reason: banReason,
783         expires: futureDaysToUnixTime(banExpireDays),
784         auth: myAuthRequired(),
785       });
786       // TODO
787       this.updateBan(res);
788       i.setState({ showBanDialog: false });
789     }
790   }
791
792   async toggleBlockPerson(recipientId: number, block: boolean) {
793     const res = await HttpService.client.blockPerson({
794       person_id: recipientId,
795       block,
796       auth: myAuthRequired(),
797     });
798     if (res.state == "success") {
799       updatePersonBlock(res.data);
800     }
801   }
802
803   handleUnblockPerson(personId: number) {
804     this.toggleBlockPerson(personId, false);
805   }
806
807   handleBlockPerson(personId: number) {
808     this.toggleBlockPerson(personId, true);
809   }
810
811   async handleAddModToCommunity(form: AddModToCommunity) {
812     // TODO not sure what to do here
813     await HttpService.client.addModToCommunity(form);
814   }
815
816   async handlePurgePerson(form: PurgePerson) {
817     const purgePersonRes = await HttpService.client.purgePerson(form);
818     this.purgeItem(purgePersonRes);
819   }
820
821   async handlePurgeComment(form: PurgeComment) {
822     const purgeCommentRes = await HttpService.client.purgeComment(form);
823     this.purgeItem(purgeCommentRes);
824   }
825
826   async handlePurgePost(form: PurgePost) {
827     const purgeRes = await HttpService.client.purgePost(form);
828     this.purgeItem(purgeRes);
829   }
830
831   async handleBlockPersonAlt(form: BlockPerson) {
832     const blockPersonRes = await HttpService.client.blockPerson(form);
833     if (blockPersonRes.state === "success") {
834       updatePersonBlock(blockPersonRes.data);
835     }
836   }
837
838   async handleCreateComment(form: CreateComment) {
839     const createCommentRes = await HttpService.client.createComment(form);
840     this.createAndUpdateComments(createCommentRes);
841
842     return createCommentRes;
843   }
844
845   async handleEditComment(form: EditComment) {
846     const editCommentRes = await HttpService.client.editComment(form);
847     this.findAndUpdateComment(editCommentRes);
848
849     return editCommentRes;
850   }
851
852   async handleDeleteComment(form: DeleteComment) {
853     const deleteCommentRes = await HttpService.client.deleteComment(form);
854     this.findAndUpdateComment(deleteCommentRes);
855   }
856
857   async handleDeletePost(form: DeletePost) {
858     const deleteRes = await HttpService.client.deletePost(form);
859     this.findAndUpdatePost(deleteRes);
860   }
861
862   async handleRemovePost(form: RemovePost) {
863     const removeRes = await HttpService.client.removePost(form);
864     this.findAndUpdatePost(removeRes);
865   }
866
867   async handleRemoveComment(form: RemoveComment) {
868     const removeCommentRes = await HttpService.client.removeComment(form);
869     this.findAndUpdateComment(removeCommentRes);
870   }
871
872   async handleSaveComment(form: SaveComment) {
873     const saveCommentRes = await HttpService.client.saveComment(form);
874     this.findAndUpdateComment(saveCommentRes);
875   }
876
877   async handleSavePost(form: SavePost) {
878     const saveRes = await HttpService.client.savePost(form);
879     this.findAndUpdatePost(saveRes);
880   }
881
882   async handleFeaturePost(form: FeaturePost) {
883     const featureRes = await HttpService.client.featurePost(form);
884     this.findAndUpdatePost(featureRes);
885   }
886
887   async handleCommentVote(form: CreateCommentLike) {
888     const voteRes = await HttpService.client.likeComment(form);
889     this.findAndUpdateComment(voteRes);
890   }
891
892   async handlePostVote(form: CreatePostLike) {
893     const voteRes = await HttpService.client.likePost(form);
894     this.findAndUpdatePost(voteRes);
895   }
896
897   async handlePostEdit(form: EditPost) {
898     const res = await HttpService.client.editPost(form);
899     this.findAndUpdatePost(res);
900   }
901
902   async handleCommentReport(form: CreateCommentReport) {
903     const reportRes = await HttpService.client.createCommentReport(form);
904     if (reportRes.state === "success") {
905       toast(i18n.t("report_created"));
906     }
907   }
908
909   async handlePostReport(form: CreatePostReport) {
910     const reportRes = await HttpService.client.createPostReport(form);
911     if (reportRes.state === "success") {
912       toast(i18n.t("report_created"));
913     }
914   }
915
916   async handleLockPost(form: LockPost) {
917     const lockRes = await HttpService.client.lockPost(form);
918     this.findAndUpdatePost(lockRes);
919   }
920
921   async handleDistinguishComment(form: DistinguishComment) {
922     const distinguishRes = await HttpService.client.distinguishComment(form);
923     this.findAndUpdateComment(distinguishRes);
924   }
925
926   async handleAddAdmin(form: AddAdmin) {
927     const addAdminRes = await HttpService.client.addAdmin(form);
928
929     if (addAdminRes.state == "success") {
930       this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
931     }
932   }
933
934   async handleTransferCommunity(form: TransferCommunity) {
935     await HttpService.client.transferCommunity(form);
936     toast(i18n.t("transfer_community"));
937   }
938
939   async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
940     const readRes = await HttpService.client.markCommentReplyAsRead(form);
941     this.findAndUpdateCommentReply(readRes);
942   }
943
944   async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
945     // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
946     await HttpService.client.markPersonMentionAsRead(form);
947   }
948
949   async handleBanFromCommunity(form: BanFromCommunity) {
950     const banRes = await HttpService.client.banFromCommunity(form);
951     this.updateBanFromCommunity(banRes);
952   }
953
954   async handleBanPerson(form: BanPerson) {
955     const banRes = await HttpService.client.banPerson(form);
956     this.updateBan(banRes);
957   }
958
959   updateBanFromCommunity(banRes: RequestState<BanFromCommunityResponse>) {
960     // Maybe not necessary
961     if (banRes.state === "success") {
962       this.setState(s => {
963         if (s.personRes.state == "success") {
964           s.personRes.data.posts
965             .filter(c => c.creator.id === banRes.data.person_view.person.id)
966             .forEach(
967               c => (c.creator_banned_from_community = banRes.data.banned)
968             );
969
970           s.personRes.data.comments
971             .filter(c => c.creator.id === banRes.data.person_view.person.id)
972             .forEach(
973               c => (c.creator_banned_from_community = banRes.data.banned)
974             );
975         }
976         return s;
977       });
978     }
979   }
980
981   updateBan(banRes: RequestState<BanPersonResponse>) {
982     // Maybe not necessary
983     if (banRes.state == "success") {
984       this.setState(s => {
985         if (s.personRes.state == "success") {
986           s.personRes.data.posts
987             .filter(c => c.creator.id == banRes.data.person_view.person.id)
988             .forEach(c => (c.creator.banned = banRes.data.banned));
989           s.personRes.data.comments
990             .filter(c => c.creator.id == banRes.data.person_view.person.id)
991             .forEach(c => (c.creator.banned = banRes.data.banned));
992         }
993         return s;
994       });
995     }
996   }
997
998   purgeItem(purgeRes: RequestState<PurgeItemResponse>) {
999     if (purgeRes.state == "success") {
1000       toast(i18n.t("purge_success"));
1001       this.context.router.history.push(`/`);
1002     }
1003   }
1004
1005   findAndUpdateComment(res: RequestState<CommentResponse>) {
1006     this.setState(s => {
1007       if (s.personRes.state == "success" && res.state == "success") {
1008         s.personRes.data.comments = editComment(
1009           res.data.comment_view,
1010           s.personRes.data.comments
1011         );
1012         s.finished.set(res.data.comment_view.comment.id, true);
1013       }
1014       return s;
1015     });
1016   }
1017
1018   createAndUpdateComments(res: RequestState<CommentResponse>) {
1019     this.setState(s => {
1020       if (s.personRes.state == "success" && res.state == "success") {
1021         s.personRes.data.comments.unshift(res.data.comment_view);
1022         // Set finished for the parent
1023         s.finished.set(
1024           getCommentParentId(res.data.comment_view.comment) ?? 0,
1025           true
1026         );
1027       }
1028       return s;
1029     });
1030   }
1031
1032   findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
1033     this.setState(s => {
1034       if (s.personRes.state == "success" && res.state == "success") {
1035         s.personRes.data.comments = editWith(
1036           res.data.comment_reply_view,
1037           s.personRes.data.comments
1038         );
1039       }
1040       return s;
1041     });
1042   }
1043
1044   findAndUpdatePost(res: RequestState<PostResponse>) {
1045     this.setState(s => {
1046       if (s.personRes.state == "success" && res.state == "success") {
1047         s.personRes.data.posts = editPost(
1048           res.data.post_view,
1049           s.personRes.data.posts
1050         );
1051       }
1052       return s;
1053     });
1054   }
1055 }