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