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