]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person.tsx
Change from using Link to NavLink. resolve #269
[lemmy-ui.git] / src / shared / components / person.tsx
1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
3 import { Subscription } from "rxjs";
4 import ISO6391 from "iso-639-1";
5 import {
6   UserOperation,
7   SortType,
8   ListingType,
9   SaveUserSettings,
10   LoginResponse,
11   DeleteAccount,
12   GetSiteResponse,
13   GetPersonDetailsResponse,
14   AddAdminResponse,
15   GetPersonDetails,
16   CommentResponse,
17   PostResponse,
18   BanPersonResponse,
19   ChangePassword,
20 } from "lemmy-js-client";
21 import { InitialFetchRequest, PersonDetailsView } from "../interfaces";
22 import { WebSocketService, UserService } from "../services";
23 import {
24   wsJsonToRes,
25   fetchLimit,
26   routeSortTypeToEnum,
27   capitalizeFirstLetter,
28   themes,
29   setTheme,
30   languages,
31   toast,
32   setupTippy,
33   getLanguage,
34   mdToHtml,
35   elementUrl,
36   setIsoData,
37   getIdFromProps,
38   getUsernameFromProps,
39   wsSubscribe,
40   createCommentLikeRes,
41   editCommentRes,
42   saveCommentRes,
43   createPostLikeFindRes,
44   previewLines,
45   editPostFindRes,
46   wsUserOp,
47   wsClient,
48   authField,
49   setOptionalAuth,
50   saveScrollPosition,
51   restoreScrollPosition,
52 } from "../utils";
53 import { PersonListing } from "./person-listing";
54 import { HtmlTags } from "./html-tags";
55 import { SortSelect } from "./sort-select";
56 import { ListingTypeSelect } from "./listing-type-select";
57 import { MomentTime } from "./moment-time";
58 import { i18n } from "../i18next";
59 import moment from "moment";
60 import { PersonDetails } from "./person-details";
61 import { MarkdownTextArea } from "./markdown-textarea";
62 import { Icon, Spinner } from "./icon";
63 import { ImageUploadForm } from "./image-upload-form";
64 import { BannerIconHeader } from "./banner-icon-header";
65 import { CommunityLink } from "./community-link";
66
67 interface PersonState {
68   personRes: GetPersonDetailsResponse;
69   personId: number;
70   userName: string;
71   view: PersonDetailsView;
72   sort: SortType;
73   page: number;
74   loading: boolean;
75   saveUserSettingsForm: SaveUserSettings;
76   changePasswordForm: ChangePassword;
77   saveUserSettingsLoading: boolean;
78   changePasswordLoading: boolean;
79   deleteAccountLoading: boolean;
80   deleteAccountShowConfirm: boolean;
81   deleteAccountForm: DeleteAccount;
82   siteRes: GetSiteResponse;
83 }
84
85 interface PersonProps {
86   view: PersonDetailsView;
87   sort: SortType;
88   page: number;
89   person_id: number | null;
90   username: string;
91 }
92
93 interface UrlParams {
94   view?: string;
95   sort?: SortType;
96   page?: number;
97 }
98
99 export class Person extends Component<any, PersonState> {
100   private isoData = setIsoData(this.context);
101   private subscription: Subscription;
102   private emptyState: PersonState = {
103     personRes: undefined,
104     personId: getIdFromProps(this.props),
105     userName: getUsernameFromProps(this.props),
106     loading: true,
107     view: Person.getViewFromProps(this.props.match.view),
108     sort: Person.getSortTypeFromProps(this.props.match.sort),
109     page: Person.getPageFromProps(this.props.match.page),
110     saveUserSettingsForm: {
111       auth: authField(false),
112     },
113     changePasswordForm: {
114       new_password: null,
115       new_password_verify: null,
116       old_password: null,
117       auth: authField(false),
118     },
119     saveUserSettingsLoading: null,
120     changePasswordLoading: false,
121     deleteAccountLoading: null,
122     deleteAccountShowConfirm: false,
123     deleteAccountForm: {
124       password: null,
125       auth: authField(false),
126     },
127     siteRes: this.isoData.site_res,
128   };
129
130   constructor(props: any, context: any) {
131     super(props, context);
132
133     this.state = this.emptyState;
134     this.handleSortChange = this.handleSortChange.bind(this);
135     this.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind(
136       this
137     );
138     this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
139       this
140     );
141     this.handlePageChange = this.handlePageChange.bind(this);
142     this.handleUserSettingsBioChange = this.handleUserSettingsBioChange.bind(
143       this
144     );
145
146     this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
147     this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
148
149     this.handleBannerUpload = this.handleBannerUpload.bind(this);
150     this.handleBannerRemove = this.handleBannerRemove.bind(this);
151
152     this.parseMessage = this.parseMessage.bind(this);
153     this.subscription = wsSubscribe(this.parseMessage);
154
155     // Only fetch the data if coming from another route
156     if (this.isoData.path == this.context.router.route.match.url) {
157       this.state.personRes = this.isoData.routeData[0];
158       this.setUserInfo();
159       this.state.loading = false;
160     } else {
161       this.fetchUserData();
162     }
163
164     setupTippy();
165   }
166
167   fetchUserData() {
168     let form: GetPersonDetails = {
169       person_id: this.state.personId,
170       username: this.state.userName,
171       sort: this.state.sort,
172       saved_only: this.state.view === PersonDetailsView.Saved,
173       page: this.state.page,
174       limit: fetchLimit,
175       auth: authField(false),
176     };
177     WebSocketService.Instance.send(wsClient.getPersonDetails(form));
178   }
179
180   get isCurrentUser() {
181     return (
182       UserService.Instance.localUserView?.person.id ==
183       this.state.personRes.person_view.person.id
184     );
185   }
186
187   static getViewFromProps(view: string): PersonDetailsView {
188     return view ? PersonDetailsView[view] : PersonDetailsView.Overview;
189   }
190
191   static getSortTypeFromProps(sort: string): SortType {
192     return sort ? routeSortTypeToEnum(sort) : SortType.New;
193   }
194
195   static getPageFromProps(page: number): number {
196     return page ? Number(page) : 1;
197   }
198
199   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
200     let pathSplit = req.path.split("/");
201     let promises: Promise<any>[] = [];
202
203     // It can be /u/me, or /username/1
204     let idOrName = pathSplit[2];
205     let person_id: number;
206     let username: string;
207     if (isNaN(Number(idOrName))) {
208       username = idOrName;
209     } else {
210       person_id = Number(idOrName);
211     }
212
213     let view = this.getViewFromProps(pathSplit[4]);
214     let sort = this.getSortTypeFromProps(pathSplit[6]);
215     let page = this.getPageFromProps(Number(pathSplit[8]));
216
217     let form: GetPersonDetails = {
218       sort,
219       saved_only: view === PersonDetailsView.Saved,
220       page,
221       limit: fetchLimit,
222     };
223     setOptionalAuth(form, req.auth);
224     this.setIdOrName(form, person_id, username);
225     promises.push(req.client.getPersonDetails(form));
226     return promises;
227   }
228
229   static setIdOrName(obj: any, id: number, name_: string) {
230     if (id) {
231       obj.person_id = id;
232     } else {
233       obj.username = name_;
234     }
235   }
236
237   componentWillUnmount() {
238     this.subscription.unsubscribe();
239     saveScrollPosition(this.context);
240   }
241
242   static getDerivedStateFromProps(props: any): PersonProps {
243     return {
244       view: this.getViewFromProps(props.match.params.view),
245       sort: this.getSortTypeFromProps(props.match.params.sort),
246       page: this.getPageFromProps(props.match.params.page),
247       person_id: Number(props.match.params.id) || null,
248       username: props.match.params.username,
249     };
250   }
251
252   componentDidUpdate(lastProps: any) {
253     // Necessary if you are on a post and you click another post (same route)
254     if (
255       lastProps.location.pathname.split("/")[2] !==
256       lastProps.history.location.pathname.split("/")[2]
257     ) {
258       // Couldnt get a refresh working. This does for now.
259       location.reload();
260     }
261   }
262
263   get documentTitle(): string {
264     return `@${this.state.personRes.person_view.person.name} - ${this.state.siteRes.site_view.site.name}`;
265   }
266
267   get bioTag(): string {
268     return this.state.personRes.person_view.person.bio
269       ? previewLines(this.state.personRes.person_view.person.bio)
270       : undefined;
271   }
272
273   render() {
274     return (
275       <div class="container">
276         {this.state.loading ? (
277           <h5>
278             <Spinner />
279           </h5>
280         ) : (
281           <div class="row">
282             <div class="col-12 col-md-8">
283               <>
284                 <HtmlTags
285                   title={this.documentTitle}
286                   path={this.context.router.route.match.url}
287                   description={this.bioTag}
288                   image={this.state.personRes.person_view.person.avatar}
289                 />
290                 {this.userInfo()}
291                 <hr />
292               </>
293               {!this.state.loading && this.selects()}
294               <PersonDetails
295                 personRes={this.state.personRes}
296                 admins={this.state.siteRes.admins}
297                 sort={this.state.sort}
298                 page={this.state.page}
299                 limit={fetchLimit}
300                 enableDownvotes={
301                   this.state.siteRes.site_view.site.enable_downvotes
302                 }
303                 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
304                 view={this.state.view}
305                 onPageChange={this.handlePageChange}
306               />
307             </div>
308
309             {!this.state.loading && (
310               <div class="col-12 col-md-4">
311                 {this.isCurrentUser && this.userSettings()}
312                 {this.moderates()}
313                 {this.follows()}
314               </div>
315             )}
316           </div>
317         )}
318       </div>
319     );
320   }
321
322   viewRadios() {
323     return (
324       <div class="btn-group btn-group-toggle flex-wrap mb-2">
325         <label
326           className={`btn btn-outline-secondary pointer
327             ${this.state.view == PersonDetailsView.Overview && "active"}
328           `}
329         >
330           <input
331             type="radio"
332             value={PersonDetailsView.Overview}
333             checked={this.state.view === PersonDetailsView.Overview}
334             onChange={linkEvent(this, this.handleViewChange)}
335           />
336           {i18n.t("overview")}
337         </label>
338         <label
339           className={`btn btn-outline-secondary pointer
340             ${this.state.view == PersonDetailsView.Comments && "active"}
341           `}
342         >
343           <input
344             type="radio"
345             value={PersonDetailsView.Comments}
346             checked={this.state.view == PersonDetailsView.Comments}
347             onChange={linkEvent(this, this.handleViewChange)}
348           />
349           {i18n.t("comments")}
350         </label>
351         <label
352           className={`btn btn-outline-secondary pointer
353             ${this.state.view == PersonDetailsView.Posts && "active"}
354           `}
355         >
356           <input
357             type="radio"
358             value={PersonDetailsView.Posts}
359             checked={this.state.view == PersonDetailsView.Posts}
360             onChange={linkEvent(this, this.handleViewChange)}
361           />
362           {i18n.t("posts")}
363         </label>
364         <label
365           className={`btn btn-outline-secondary pointer
366             ${this.state.view == PersonDetailsView.Saved && "active"}
367           `}
368         >
369           <input
370             type="radio"
371             value={PersonDetailsView.Saved}
372             checked={this.state.view == PersonDetailsView.Saved}
373             onChange={linkEvent(this, this.handleViewChange)}
374           />
375           {i18n.t("saved")}
376         </label>
377       </div>
378     );
379   }
380
381   selects() {
382     return (
383       <div className="mb-2">
384         <span class="mr-3">{this.viewRadios()}</span>
385         <SortSelect
386           sort={this.state.sort}
387           onChange={this.handleSortChange}
388           hideHot
389           hideMostComments
390         />
391         <a
392           href={`/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`}
393           rel="noopener"
394           title="RSS"
395         >
396           <Icon icon="rss" classes="text-muted small mx-2" />
397         </a>
398       </div>
399     );
400   }
401
402   userInfo() {
403     let pv = this.state.personRes?.person_view;
404
405     return (
406       <div>
407         <BannerIconHeader banner={pv.person.banner} icon={pv.person.avatar} />
408         <div class="mb-3">
409           <div class="">
410             <div class="mb-0 d-flex flex-wrap">
411               <div>
412                 {pv.person.display_name && (
413                   <h5 class="mb-0">{pv.person.display_name}</h5>
414                 )}
415                 <ul class="list-inline mb-2">
416                   <li className="list-inline-item">
417                     <PersonListing
418                       person={pv.person}
419                       realLink
420                       useApubName
421                       muted
422                       hideAvatar
423                     />
424                   </li>
425                   {pv.person.banned && (
426                     <li className="list-inline-item badge badge-danger">
427                       {i18n.t("banned")}
428                     </li>
429                   )}
430                 </ul>
431               </div>
432               <div className="flex-grow-1 unselectable pointer mx-2"></div>
433               {this.isCurrentUser ? (
434                 <button
435                   class="d-flex align-self-start btn btn-secondary mr-2"
436                   onClick={linkEvent(this, this.handleLogoutClick)}
437                 >
438                   {i18n.t("logout")}
439                 </button>
440               ) : (
441                 <Link
442                   className={"d-flex align-self-start btn btn-secondary"}
443                   to={`/create_private_message/recipient/${pv.person.id}`}
444                 >
445                   {i18n.t("send_message")}
446                 </Link>
447               )}
448             </div>
449             {pv.person.bio && (
450               <div className="d-flex align-items-center mb-2">
451                 <div
452                   className="md-div"
453                   dangerouslySetInnerHTML={mdToHtml(pv.person.bio)}
454                 />
455               </div>
456             )}
457             <div>
458               <ul class="list-inline mb-2">
459                 <li className="list-inline-item badge badge-light">
460                   {i18n.t("number_of_posts", { count: pv.counts.post_count })}
461                 </li>
462                 <li className="list-inline-item badge badge-light">
463                   {i18n.t("number_of_comments", {
464                     count: pv.counts.comment_count,
465                   })}
466                 </li>
467               </ul>
468             </div>
469             <div class="text-muted">
470               {i18n.t("joined")}{" "}
471               <MomentTime data={pv.person} showAgo ignoreUpdated />
472             </div>
473             <div className="d-flex align-items-center text-muted mb-2">
474               <Icon icon="cake" />
475               <span className="ml-2">
476                 {i18n.t("cake_day_title")}{" "}
477                 {moment.utc(pv.person.published).local().format("MMM DD, YYYY")}
478               </span>
479             </div>
480           </div>
481         </div>
482       </div>
483     );
484   }
485
486   // TODO this isn't used currently
487   // matrixButton() {
488   //   return (
489   //     <a
490   //     className={`d-flex align-self-start btn btn-secondary mr-2 ${
491   //       !pv.local_user.matrix_user_id && "invisible"
492   //     }`}
493   //     rel="noopener"
494   //     href={`https://matrix.to/#/${pv.local_user.matrix_user_id}`}
495   //   >
496   //       {i18n.t("send_secure_message")}
497   //   </a>
498   //   );
499   // }
500
501   userSettings() {
502     return (
503       <div>
504         <div class="card border-secondary mb-3">
505           <div class="card-body">
506             {this.saveUserSettingsHtmlForm()}
507             <br />
508             {this.changePasswordHtmlForm()}
509           </div>
510         </div>
511       </div>
512     );
513   }
514
515   changePasswordHtmlForm() {
516     return (
517       <>
518         <h5>{i18n.t("change_password")}</h5>
519         <form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
520           <div class="form-group row">
521             <label class="col-lg-5 col-form-label" htmlFor="user-password">
522               {i18n.t("new_password")}
523             </label>
524             <div class="col-lg-7">
525               <input
526                 type="password"
527                 id="user-password"
528                 class="form-control"
529                 value={this.state.changePasswordForm.new_password}
530                 autoComplete="new-password"
531                 maxLength={60}
532                 onInput={linkEvent(this, this.handleNewPasswordChange)}
533               />
534             </div>
535           </div>
536           <div class="form-group row">
537             <label
538               class="col-lg-5 col-form-label"
539               htmlFor="user-verify-password"
540             >
541               {i18n.t("verify_password")}
542             </label>
543             <div class="col-lg-7">
544               <input
545                 type="password"
546                 id="user-verify-password"
547                 class="form-control"
548                 value={this.state.changePasswordForm.new_password_verify}
549                 autoComplete="new-password"
550                 maxLength={60}
551                 onInput={linkEvent(this, this.handleNewPasswordVerifyChange)}
552               />
553             </div>
554           </div>
555           <div class="form-group row">
556             <label class="col-lg-5 col-form-label" htmlFor="user-old-password">
557               {i18n.t("old_password")}
558             </label>
559             <div class="col-lg-7">
560               <input
561                 type="password"
562                 id="user-old-password"
563                 class="form-control"
564                 value={this.state.changePasswordForm.old_password}
565                 autoComplete="new-password"
566                 maxLength={60}
567                 onInput={linkEvent(this, this.handleOldPasswordChange)}
568               />
569             </div>
570           </div>
571           <div class="form-group">
572             <button type="submit" class="btn btn-block btn-secondary mr-4">
573               {this.state.changePasswordLoading ? (
574                 <Spinner />
575               ) : (
576                 capitalizeFirstLetter(i18n.t("save"))
577               )}
578             </button>
579           </div>
580         </form>
581       </>
582     );
583   }
584
585   saveUserSettingsHtmlForm() {
586     return (
587       <>
588         <h5>{i18n.t("settings")}</h5>
589         <form onSubmit={linkEvent(this, this.handleSaveUserSettingsSubmit)}>
590           <div class="form-group">
591             <label>{i18n.t("avatar")}</label>
592             <ImageUploadForm
593               uploadTitle={i18n.t("upload_avatar")}
594               imageSrc={this.state.saveUserSettingsForm.avatar}
595               onUpload={this.handleAvatarUpload}
596               onRemove={this.handleAvatarRemove}
597               rounded
598             />
599           </div>
600           <div class="form-group">
601             <label>{i18n.t("banner")}</label>
602             <ImageUploadForm
603               uploadTitle={i18n.t("upload_banner")}
604               imageSrc={this.state.saveUserSettingsForm.banner}
605               onUpload={this.handleBannerUpload}
606               onRemove={this.handleBannerRemove}
607             />
608           </div>
609           <div class="form-group">
610             <label htmlFor="user-language">{i18n.t("language")}</label>
611             <select
612               id="user-language"
613               value={this.state.saveUserSettingsForm.lang}
614               onChange={linkEvent(this, this.handleUserSettingsLangChange)}
615               class="ml-2 custom-select w-auto"
616             >
617               <option disabled aria-hidden="true">
618                 {i18n.t("language")}
619               </option>
620               <option value="browser">{i18n.t("browser_default")}</option>
621               <option disabled aria-hidden="true">
622                 â”€â”€
623               </option>
624               {languages.sort().map(lang => (
625                 <option value={lang.code}>
626                   {ISO6391.getNativeName(lang.code) || lang.code}
627                 </option>
628               ))}
629             </select>
630           </div>
631           <div class="form-group">
632             <label htmlFor="user-theme">{i18n.t("theme")}</label>
633             <select
634               id="user-theme"
635               value={this.state.saveUserSettingsForm.theme}
636               onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
637               class="ml-2 custom-select w-auto"
638             >
639               <option disabled aria-hidden="true">
640                 {i18n.t("theme")}
641               </option>
642               <option value="browser">{i18n.t("browser_default")}</option>
643               {themes.map(theme => (
644                 <option value={theme}>{theme}</option>
645               ))}
646             </select>
647           </div>
648           <form className="form-group">
649             <label>
650               <div class="mr-2">{i18n.t("type")}</div>
651             </label>
652             <ListingTypeSelect
653               type_={
654                 Object.values(ListingType)[
655                   this.state.saveUserSettingsForm.default_listing_type
656                 ]
657               }
658               showLocal={
659                 this.state.siteRes.federated_instances?.linked.length > 0
660               }
661               onChange={this.handleUserSettingsListingTypeChange}
662             />
663           </form>
664           <form className="form-group">
665             <label>
666               <div class="mr-2">{i18n.t("sort_type")}</div>
667             </label>
668             <SortSelect
669               sort={
670                 Object.values(SortType)[
671                   this.state.saveUserSettingsForm.default_sort_type
672                 ]
673               }
674               onChange={this.handleUserSettingsSortTypeChange}
675             />
676           </form>
677           <div class="form-group row">
678             <label class="col-lg-5 col-form-label" htmlFor="display-name">
679               {i18n.t("display_name")}
680             </label>
681             <div class="col-lg-7">
682               <input
683                 id="display-name"
684                 type="text"
685                 class="form-control"
686                 placeholder={i18n.t("optional")}
687                 value={this.state.saveUserSettingsForm.display_name}
688                 onInput={linkEvent(
689                   this,
690                   this.handleUserSettingsPreferredUsernameChange
691                 )}
692                 pattern="^(?!@)(.+)$"
693                 minLength={3}
694                 maxLength={20}
695               />
696             </div>
697           </div>
698           <div class="form-group row">
699             <label class="col-lg-3 col-form-label" htmlFor="user-bio">
700               {i18n.t("bio")}
701             </label>
702             <div class="col-lg-9">
703               <MarkdownTextArea
704                 initialContent={this.state.saveUserSettingsForm.bio}
705                 onContentChange={this.handleUserSettingsBioChange}
706                 maxLength={300}
707                 hideNavigationWarnings
708               />
709             </div>
710           </div>
711           <div class="form-group row">
712             <label class="col-lg-3 col-form-label" htmlFor="user-email">
713               {i18n.t("email")}
714             </label>
715             <div class="col-lg-9">
716               <input
717                 type="email"
718                 id="user-email"
719                 class="form-control"
720                 placeholder={i18n.t("optional")}
721                 value={this.state.saveUserSettingsForm.email}
722                 onInput={linkEvent(this, this.handleUserSettingsEmailChange)}
723                 minLength={3}
724               />
725             </div>
726           </div>
727           <div class="form-group row">
728             <label class="col-lg-5 col-form-label" htmlFor="matrix-user-id">
729               <a href={elementUrl} rel="noopener">
730                 {i18n.t("matrix_user_id")}
731               </a>
732             </label>
733             <div class="col-lg-7">
734               <input
735                 id="matrix-user-id"
736                 type="text"
737                 class="form-control"
738                 placeholder="@user:example.com"
739                 value={this.state.saveUserSettingsForm.matrix_user_id}
740                 onInput={linkEvent(
741                   this,
742                   this.handleUserSettingsMatrixUserIdChange
743                 )}
744                 pattern="^@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
745               />
746             </div>
747           </div>
748           {this.state.siteRes.site_view.site.enable_nsfw && (
749             <div class="form-group">
750               <div class="form-check">
751                 <input
752                   class="form-check-input"
753                   id="user-show-nsfw"
754                   type="checkbox"
755                   checked={this.state.saveUserSettingsForm.show_nsfw}
756                   onChange={linkEvent(
757                     this,
758                     this.handleUserSettingsShowNsfwChange
759                   )}
760                 />
761                 <label class="form-check-label" htmlFor="user-show-nsfw">
762                   {i18n.t("show_nsfw")}
763                 </label>
764               </div>
765             </div>
766           )}
767           <div class="form-group">
768             <div class="form-check">
769               <input
770                 class="form-check-input"
771                 id="user-show-scores"
772                 type="checkbox"
773                 checked={this.state.saveUserSettingsForm.show_scores}
774                 onChange={linkEvent(
775                   this,
776                   this.handleUserSettingsShowScoresChange
777                 )}
778               />
779               <label class="form-check-label" htmlFor="user-show-scores">
780                 {i18n.t("show_scores")}
781               </label>
782             </div>
783           </div>
784           <div class="form-group">
785             <div class="form-check">
786               <input
787                 class="form-check-input"
788                 id="user-show-avatars"
789                 type="checkbox"
790                 checked={this.state.saveUserSettingsForm.show_avatars}
791                 onChange={linkEvent(
792                   this,
793                   this.handleUserSettingsShowAvatarsChange
794                 )}
795               />
796               <label class="form-check-label" htmlFor="user-show-avatars">
797                 {i18n.t("show_avatars")}
798               </label>
799             </div>
800           </div>
801           <div class="form-group">
802             <div class="form-check">
803               <input
804                 class="form-check-input"
805                 id="user-send-notifications-to-email"
806                 type="checkbox"
807                 disabled={!this.state.saveUserSettingsForm.email}
808                 checked={
809                   this.state.saveUserSettingsForm.send_notifications_to_email
810                 }
811                 onChange={linkEvent(
812                   this,
813                   this.handleUserSettingsSendNotificationsToEmailChange
814                 )}
815               />
816               <label
817                 class="form-check-label"
818                 htmlFor="user-send-notifications-to-email"
819               >
820                 {i18n.t("send_notifications_to_email")}
821               </label>
822             </div>
823           </div>
824           <div class="form-group">
825             <button type="submit" class="btn btn-block btn-secondary mr-4">
826               {this.state.saveUserSettingsLoading ? (
827                 <Spinner />
828               ) : (
829                 capitalizeFirstLetter(i18n.t("save"))
830               )}
831             </button>
832           </div>
833           <hr />
834           <div class="form-group">
835             <button
836               class="btn btn-block btn-danger"
837               onClick={linkEvent(
838                 this,
839                 this.handleDeleteAccountShowConfirmToggle
840               )}
841             >
842               {i18n.t("delete_account")}
843             </button>
844             {this.state.deleteAccountShowConfirm && (
845               <>
846                 <div class="my-2 alert alert-danger" role="alert">
847                   {i18n.t("delete_account_confirm")}
848                 </div>
849                 <input
850                   type="password"
851                   value={this.state.deleteAccountForm.password}
852                   autoComplete="new-password"
853                   maxLength={60}
854                   onInput={linkEvent(
855                     this,
856                     this.handleDeleteAccountPasswordChange
857                   )}
858                   class="form-control my-2"
859                 />
860                 <button
861                   class="btn btn-danger mr-4"
862                   disabled={!this.state.deleteAccountForm.password}
863                   onClick={linkEvent(this, this.handleDeleteAccount)}
864                 >
865                   {this.state.deleteAccountLoading ? (
866                     <Spinner />
867                   ) : (
868                     capitalizeFirstLetter(i18n.t("delete"))
869                   )}
870                 </button>
871                 <button
872                   class="btn btn-secondary"
873                   onClick={linkEvent(
874                     this,
875                     this.handleDeleteAccountShowConfirmToggle
876                   )}
877                 >
878                   {i18n.t("cancel")}
879                 </button>
880               </>
881             )}
882           </div>
883         </form>
884       </>
885     );
886   }
887
888   moderates() {
889     return (
890       <div>
891         {this.state.personRes.moderates.length > 0 && (
892           <div class="card border-secondary mb-3">
893             <div class="card-body">
894               <h5>{i18n.t("moderates")}</h5>
895               <ul class="list-unstyled mb-0">
896                 {this.state.personRes.moderates.map(cmv => (
897                   <li>
898                     <CommunityLink community={cmv.community} />
899                   </li>
900                 ))}
901               </ul>
902             </div>
903           </div>
904         )}
905       </div>
906     );
907   }
908
909   follows() {
910     return (
911       <div>
912         {this.state.personRes.follows.length > 0 && (
913           <div class="card border-secondary mb-3">
914             <div class="card-body">
915               <h5>{i18n.t("subscribed")}</h5>
916               <ul class="list-unstyled mb-0">
917                 {this.state.personRes.follows.map(cfv => (
918                   <li>
919                     <CommunityLink community={cfv.community} />
920                   </li>
921                 ))}
922               </ul>
923             </div>
924           </div>
925         )}
926       </div>
927     );
928   }
929
930   updateUrl(paramUpdates: UrlParams) {
931     const page = paramUpdates.page || this.state.page;
932     const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
933     const sortStr = paramUpdates.sort || this.state.sort;
934
935     let typeView = this.state.userName
936       ? `/u/${this.state.userName}`
937       : `/user/${this.state.personId}`;
938
939     this.props.history.push(
940       `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
941     );
942     this.state.loading = true;
943     this.setState(this.state);
944     this.fetchUserData();
945   }
946
947   handlePageChange(page: number) {
948     this.updateUrl({ page });
949   }
950
951   handleSortChange(val: SortType) {
952     this.updateUrl({ sort: val, page: 1 });
953   }
954
955   handleViewChange(i: Person, event: any) {
956     i.updateUrl({
957       view: PersonDetailsView[Number(event.target.value)],
958       page: 1,
959     });
960   }
961
962   handleUserSettingsShowNsfwChange(i: Person, event: any) {
963     i.state.saveUserSettingsForm.show_nsfw = event.target.checked;
964     i.setState(i.state);
965   }
966
967   handleUserSettingsShowAvatarsChange(i: Person, event: any) {
968     i.state.saveUserSettingsForm.show_avatars = event.target.checked;
969     UserService.Instance.localUserView.local_user.show_avatars =
970       event.target.checked; // Just for instant updates
971     i.setState(i.state);
972   }
973
974   handleUserSettingsShowScoresChange(i: Person, event: any) {
975     i.state.saveUserSettingsForm.show_scores = event.target.checked;
976     UserService.Instance.localUserView.local_user.show_scores =
977       event.target.checked; // Just for instant updates
978     i.setState(i.state);
979   }
980
981   handleUserSettingsSendNotificationsToEmailChange(i: Person, event: any) {
982     i.state.saveUserSettingsForm.send_notifications_to_email =
983       event.target.checked;
984     i.setState(i.state);
985   }
986
987   handleUserSettingsThemeChange(i: Person, event: any) {
988     i.state.saveUserSettingsForm.theme = event.target.value;
989     setTheme(event.target.value, true);
990     i.setState(i.state);
991   }
992
993   handleUserSettingsLangChange(i: Person, event: any) {
994     i.state.saveUserSettingsForm.lang = event.target.value;
995     i18n.changeLanguage(getLanguage(i.state.saveUserSettingsForm.lang));
996     i.setState(i.state);
997   }
998
999   handleUserSettingsSortTypeChange(val: SortType) {
1000     this.state.saveUserSettingsForm.default_sort_type = Object.keys(
1001       SortType
1002     ).indexOf(val);
1003     this.setState(this.state);
1004   }
1005
1006   handleUserSettingsListingTypeChange(val: ListingType) {
1007     this.state.saveUserSettingsForm.default_listing_type = Object.keys(
1008       ListingType
1009     ).indexOf(val);
1010     this.setState(this.state);
1011   }
1012
1013   handleUserSettingsEmailChange(i: Person, event: any) {
1014     i.state.saveUserSettingsForm.email = event.target.value;
1015     i.setState(i.state);
1016   }
1017
1018   handleUserSettingsBioChange(val: string) {
1019     this.state.saveUserSettingsForm.bio = val;
1020     this.setState(this.state);
1021   }
1022
1023   handleAvatarUpload(url: string) {
1024     this.state.saveUserSettingsForm.avatar = url;
1025     this.setState(this.state);
1026   }
1027
1028   handleAvatarRemove() {
1029     this.state.saveUserSettingsForm.avatar = "";
1030     this.setState(this.state);
1031   }
1032
1033   handleBannerUpload(url: string) {
1034     this.state.saveUserSettingsForm.banner = url;
1035     this.setState(this.state);
1036   }
1037
1038   handleBannerRemove() {
1039     this.state.saveUserSettingsForm.banner = "";
1040     this.setState(this.state);
1041   }
1042
1043   handleUserSettingsPreferredUsernameChange(i: Person, event: any) {
1044     i.state.saveUserSettingsForm.display_name = event.target.value;
1045     i.setState(i.state);
1046   }
1047
1048   handleUserSettingsMatrixUserIdChange(i: Person, event: any) {
1049     i.state.saveUserSettingsForm.matrix_user_id = event.target.value;
1050     if (
1051       i.state.saveUserSettingsForm.matrix_user_id == "" &&
1052       !UserService.Instance.localUserView.person.matrix_user_id
1053     ) {
1054       i.state.saveUserSettingsForm.matrix_user_id = undefined;
1055     }
1056     i.setState(i.state);
1057   }
1058
1059   handleNewPasswordChange(i: Person, event: any) {
1060     i.state.changePasswordForm.new_password = event.target.value;
1061     if (i.state.changePasswordForm.new_password == "") {
1062       i.state.changePasswordForm.new_password = undefined;
1063     }
1064     i.setState(i.state);
1065   }
1066
1067   handleNewPasswordVerifyChange(i: Person, event: any) {
1068     i.state.changePasswordForm.new_password_verify = event.target.value;
1069     if (i.state.changePasswordForm.new_password_verify == "") {
1070       i.state.changePasswordForm.new_password_verify = undefined;
1071     }
1072     i.setState(i.state);
1073   }
1074
1075   handleOldPasswordChange(i: Person, event: any) {
1076     i.state.changePasswordForm.old_password = event.target.value;
1077     if (i.state.changePasswordForm.old_password == "") {
1078       i.state.changePasswordForm.old_password = undefined;
1079     }
1080     i.setState(i.state);
1081   }
1082
1083   handleSaveUserSettingsSubmit(i: Person, event: any) {
1084     event.preventDefault();
1085     i.state.saveUserSettingsLoading = true;
1086     i.setState(i.state);
1087
1088     WebSocketService.Instance.send(
1089       wsClient.saveUserSettings(i.state.saveUserSettingsForm)
1090     );
1091   }
1092
1093   handleChangePasswordSubmit(i: Person, event: any) {
1094     event.preventDefault();
1095     i.state.changePasswordLoading = true;
1096     i.setState(i.state);
1097
1098     WebSocketService.Instance.send(
1099       wsClient.changePassword(i.state.changePasswordForm)
1100     );
1101   }
1102
1103   handleDeleteAccountShowConfirmToggle(i: Person, event: any) {
1104     event.preventDefault();
1105     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
1106     i.setState(i.state);
1107   }
1108
1109   handleDeleteAccountPasswordChange(i: Person, event: any) {
1110     i.state.deleteAccountForm.password = event.target.value;
1111     i.setState(i.state);
1112   }
1113
1114   handleLogoutClick(i: Person) {
1115     UserService.Instance.logout();
1116     i.context.router.history.push("/");
1117   }
1118
1119   handleDeleteAccount(i: Person, event: any) {
1120     event.preventDefault();
1121     i.state.deleteAccountLoading = true;
1122     i.setState(i.state);
1123
1124     WebSocketService.Instance.send(
1125       wsClient.deleteAccount(i.state.deleteAccountForm)
1126     );
1127     i.handleLogoutClick(i);
1128   }
1129
1130   setUserInfo() {
1131     if (this.isCurrentUser) {
1132       this.state.saveUserSettingsForm.show_nsfw =
1133         UserService.Instance.localUserView.local_user.show_nsfw;
1134       this.state.saveUserSettingsForm.theme = UserService.Instance.localUserView
1135         .local_user.theme
1136         ? UserService.Instance.localUserView.local_user.theme
1137         : "browser";
1138       this.state.saveUserSettingsForm.default_sort_type =
1139         UserService.Instance.localUserView.local_user.default_sort_type;
1140       this.state.saveUserSettingsForm.default_listing_type =
1141         UserService.Instance.localUserView.local_user.default_listing_type;
1142       this.state.saveUserSettingsForm.lang =
1143         UserService.Instance.localUserView.local_user.lang;
1144       this.state.saveUserSettingsForm.avatar =
1145         UserService.Instance.localUserView.person.avatar;
1146       this.state.saveUserSettingsForm.banner =
1147         UserService.Instance.localUserView.person.banner;
1148       this.state.saveUserSettingsForm.display_name =
1149         UserService.Instance.localUserView.person.display_name;
1150       this.state.saveUserSettingsForm.show_avatars =
1151         UserService.Instance.localUserView.local_user.show_avatars;
1152       this.state.saveUserSettingsForm.show_scores =
1153         UserService.Instance.localUserView.local_user.show_scores;
1154       this.state.saveUserSettingsForm.email =
1155         UserService.Instance.localUserView.local_user.email;
1156       this.state.saveUserSettingsForm.bio =
1157         UserService.Instance.localUserView.person.bio;
1158       this.state.saveUserSettingsForm.send_notifications_to_email =
1159         UserService.Instance.localUserView.local_user.send_notifications_to_email;
1160       this.state.saveUserSettingsForm.matrix_user_id =
1161         UserService.Instance.localUserView.person.matrix_user_id;
1162     }
1163   }
1164
1165   parseMessage(msg: any) {
1166     let op = wsUserOp(msg);
1167     console.log(msg);
1168     if (msg.error) {
1169       toast(i18n.t(msg.error), "danger");
1170       if (msg.error == "couldnt_find_that_username_or_email") {
1171         this.context.router.history.push("/");
1172       }
1173       this.setState({
1174         deleteAccountLoading: false,
1175         saveUserSettingsLoading: false,
1176         changePasswordLoading: false,
1177       });
1178       return;
1179     } else if (msg.reconnect) {
1180       this.fetchUserData();
1181     } else if (op == UserOperation.GetPersonDetails) {
1182       // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
1183       // and set the parent state if it is not set or differs
1184       // TODO this might need to get abstracted
1185       let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data;
1186       this.state.personRes = data;
1187       console.log(data);
1188       this.setUserInfo();
1189       this.state.loading = false;
1190       this.setState(this.state);
1191       restoreScrollPosition(this.context);
1192     } else if (op == UserOperation.SaveUserSettings) {
1193       let data = wsJsonToRes<LoginResponse>(msg).data;
1194       UserService.Instance.login(data);
1195       this.state.personRes.person_view.person.bio = this.state.saveUserSettingsForm.bio;
1196       this.state.personRes.person_view.person.display_name = this.state.saveUserSettingsForm.display_name;
1197       this.state.personRes.person_view.person.banner = this.state.saveUserSettingsForm.banner;
1198       this.state.personRes.person_view.person.avatar = this.state.saveUserSettingsForm.avatar;
1199       this.state.saveUserSettingsLoading = false;
1200       this.setState(this.state);
1201
1202       window.scrollTo(0, 0);
1203     } else if (op == UserOperation.ChangePassword) {
1204       let data = wsJsonToRes<LoginResponse>(msg).data;
1205       UserService.Instance.login(data);
1206       this.state.changePasswordLoading = false;
1207       this.setState(this.state);
1208       window.scrollTo(0, 0);
1209       toast(i18n.t("password_changed"));
1210     } else if (op == UserOperation.DeleteAccount) {
1211       this.setState({
1212         deleteAccountLoading: false,
1213         deleteAccountShowConfirm: false,
1214       });
1215       this.context.router.history.push("/");
1216     } else if (op == UserOperation.AddAdmin) {
1217       let data = wsJsonToRes<AddAdminResponse>(msg).data;
1218       this.state.siteRes.admins = data.admins;
1219       this.setState(this.state);
1220     } else if (op == UserOperation.CreateCommentLike) {
1221       let data = wsJsonToRes<CommentResponse>(msg).data;
1222       createCommentLikeRes(data.comment_view, this.state.personRes.comments);
1223       this.setState(this.state);
1224     } else if (
1225       op == UserOperation.EditComment ||
1226       op == UserOperation.DeleteComment ||
1227       op == UserOperation.RemoveComment
1228     ) {
1229       let data = wsJsonToRes<CommentResponse>(msg).data;
1230       editCommentRes(data.comment_view, this.state.personRes.comments);
1231       this.setState(this.state);
1232     } else if (op == UserOperation.CreateComment) {
1233       let data = wsJsonToRes<CommentResponse>(msg).data;
1234       if (
1235         UserService.Instance.localUserView &&
1236         data.comment_view.creator.id ==
1237           UserService.Instance.localUserView.person.id
1238       ) {
1239         toast(i18n.t("reply_sent"));
1240       }
1241     } else if (op == UserOperation.SaveComment) {
1242       let data = wsJsonToRes<CommentResponse>(msg).data;
1243       saveCommentRes(data.comment_view, this.state.personRes.comments);
1244       this.setState(this.state);
1245     } else if (
1246       op == UserOperation.EditPost ||
1247       op == UserOperation.DeletePost ||
1248       op == UserOperation.RemovePost ||
1249       op == UserOperation.LockPost ||
1250       op == UserOperation.StickyPost ||
1251       op == UserOperation.SavePost
1252     ) {
1253       let data = wsJsonToRes<PostResponse>(msg).data;
1254       editPostFindRes(data.post_view, this.state.personRes.posts);
1255       this.setState(this.state);
1256     } else if (op == UserOperation.CreatePostLike) {
1257       let data = wsJsonToRes<PostResponse>(msg).data;
1258       createPostLikeFindRes(data.post_view, this.state.personRes.posts);
1259       this.setState(this.state);
1260     } else if (op == UserOperation.BanPerson) {
1261       let data = wsJsonToRes<BanPersonResponse>(msg).data;
1262       this.state.personRes.comments
1263         .filter(c => c.creator.id == data.person_view.person.id)
1264         .forEach(c => (c.creator.banned = data.banned));
1265       this.state.personRes.posts
1266         .filter(c => c.creator.id == data.person_view.person.id)
1267         .forEach(c => (c.creator.banned = data.banned));
1268       this.setState(this.state);
1269     }
1270   }
1271 }