]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/person.tsx
Remove max length constraints on actors. Fixes #350 (#351)
[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-send-notifications-to-email"
838                 type="checkbox"
839                 disabled={!this.state.saveUserSettingsForm.email}
840                 checked={
841                   this.state.saveUserSettingsForm.send_notifications_to_email
842                 }
843                 onChange={linkEvent(
844                   this,
845                   this.handleUserSettingsSendNotificationsToEmailChange
846                 )}
847               />
848               <label
849                 class="form-check-label"
850                 htmlFor="user-send-notifications-to-email"
851               >
852                 {i18n.t("send_notifications_to_email")}
853               </label>
854             </div>
855           </div>
856           <div class="form-group">
857             <button type="submit" class="btn btn-block btn-secondary mr-4">
858               {this.state.saveUserSettingsLoading ? (
859                 <Spinner />
860               ) : (
861                 capitalizeFirstLetter(i18n.t("save"))
862               )}
863             </button>
864           </div>
865           <hr />
866           <div class="form-group">
867             <button
868               class="btn btn-block btn-danger"
869               onClick={linkEvent(
870                 this,
871                 this.handleDeleteAccountShowConfirmToggle
872               )}
873             >
874               {i18n.t("delete_account")}
875             </button>
876             {this.state.deleteAccountShowConfirm && (
877               <>
878                 <div class="my-2 alert alert-danger" role="alert">
879                   {i18n.t("delete_account_confirm")}
880                 </div>
881                 <input
882                   type="password"
883                   value={this.state.deleteAccountForm.password}
884                   autoComplete="new-password"
885                   maxLength={60}
886                   onInput={linkEvent(
887                     this,
888                     this.handleDeleteAccountPasswordChange
889                   )}
890                   class="form-control my-2"
891                 />
892                 <button
893                   class="btn btn-danger mr-4"
894                   disabled={!this.state.deleteAccountForm.password}
895                   onClick={linkEvent(this, this.handleDeleteAccount)}
896                 >
897                   {this.state.deleteAccountLoading ? (
898                     <Spinner />
899                   ) : (
900                     capitalizeFirstLetter(i18n.t("delete"))
901                   )}
902                 </button>
903                 <button
904                   class="btn btn-secondary"
905                   onClick={linkEvent(
906                     this,
907                     this.handleDeleteAccountShowConfirmToggle
908                   )}
909                 >
910                   {i18n.t("cancel")}
911                 </button>
912               </>
913             )}
914           </div>
915         </form>
916       </>
917     );
918   }
919
920   moderates() {
921     return (
922       <div>
923         {this.state.personRes.moderates.length > 0 && (
924           <div class="card border-secondary mb-3">
925             <div class="card-body">
926               <h5>{i18n.t("moderates")}</h5>
927               <ul class="list-unstyled mb-0">
928                 {this.state.personRes.moderates.map(cmv => (
929                   <li>
930                     <CommunityLink community={cmv.community} />
931                   </li>
932                 ))}
933               </ul>
934             </div>
935           </div>
936         )}
937       </div>
938     );
939   }
940
941   follows() {
942     return (
943       <div>
944         {this.state.personRes.follows.length > 0 && (
945           <div class="card border-secondary mb-3">
946             <div class="card-body">
947               <h5>{i18n.t("subscribed")}</h5>
948               <ul class="list-unstyled mb-0">
949                 {this.state.personRes.follows.map(cfv => (
950                   <li>
951                     <CommunityLink community={cfv.community} />
952                   </li>
953                 ))}
954               </ul>
955             </div>
956           </div>
957         )}
958       </div>
959     );
960   }
961
962   updateUrl(paramUpdates: UrlParams) {
963     const page = paramUpdates.page || this.state.page;
964     const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
965     const sortStr = paramUpdates.sort || this.state.sort;
966
967     let typeView = `/u/${this.state.userName}`;
968
969     this.props.history.push(
970       `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
971     );
972     this.state.loading = true;
973     this.setState(this.state);
974     this.fetchUserData();
975   }
976
977   handlePageChange(page: number) {
978     this.updateUrl({ page });
979   }
980
981   handleSortChange(val: SortType) {
982     this.updateUrl({ sort: val, page: 1 });
983   }
984
985   handleViewChange(i: Person, event: any) {
986     i.updateUrl({
987       view: PersonDetailsView[Number(event.target.value)],
988       page: 1,
989     });
990   }
991
992   handleUserSettingsShowNsfwChange(i: Person, event: any) {
993     i.state.saveUserSettingsForm.show_nsfw = event.target.checked;
994     i.setState(i.state);
995   }
996
997   handleUserSettingsShowAvatarsChange(i: Person, event: any) {
998     i.state.saveUserSettingsForm.show_avatars = event.target.checked;
999     UserService.Instance.localUserView.local_user.show_avatars =
1000       event.target.checked; // Just for instant updates
1001     i.setState(i.state);
1002   }
1003
1004   handleUserSettingsBotAccount(i: Person, event: any) {
1005     i.state.saveUserSettingsForm.bot_account = event.target.checked;
1006     i.setState(i.state);
1007   }
1008
1009   handleUserSettingsShowBotAccounts(i: Person, event: any) {
1010     i.state.saveUserSettingsForm.show_bot_accounts = event.target.checked;
1011     i.setState(i.state);
1012   }
1013
1014   handleUserSettingsShowReadPosts(i: Person, event: any) {
1015     i.state.saveUserSettingsForm.show_read_posts = event.target.checked;
1016     i.setState(i.state);
1017   }
1018
1019   handleUserSettingsShowScoresChange(i: Person, event: any) {
1020     i.state.saveUserSettingsForm.show_scores = event.target.checked;
1021     UserService.Instance.localUserView.local_user.show_scores =
1022       event.target.checked; // Just for instant updates
1023     i.setState(i.state);
1024   }
1025
1026   handleUserSettingsSendNotificationsToEmailChange(i: Person, event: any) {
1027     i.state.saveUserSettingsForm.send_notifications_to_email =
1028       event.target.checked;
1029     i.setState(i.state);
1030   }
1031
1032   handleUserSettingsThemeChange(i: Person, event: any) {
1033     i.state.saveUserSettingsForm.theme = event.target.value;
1034     setTheme(event.target.value, true);
1035     i.setState(i.state);
1036   }
1037
1038   handleUserSettingsLangChange(i: Person, event: any) {
1039     i.state.saveUserSettingsForm.lang = event.target.value;
1040     i18n.changeLanguage(getLanguage(i.state.saveUserSettingsForm.lang));
1041     i.setState(i.state);
1042   }
1043
1044   handleUserSettingsSortTypeChange(val: SortType) {
1045     this.state.saveUserSettingsForm.default_sort_type =
1046       Object.keys(SortType).indexOf(val);
1047     this.setState(this.state);
1048   }
1049
1050   handleUserSettingsListingTypeChange(val: ListingType) {
1051     this.state.saveUserSettingsForm.default_listing_type =
1052       Object.keys(ListingType).indexOf(val);
1053     this.setState(this.state);
1054   }
1055
1056   handleUserSettingsEmailChange(i: Person, event: any) {
1057     i.state.saveUserSettingsForm.email = event.target.value;
1058     i.setState(i.state);
1059   }
1060
1061   handleUserSettingsBioChange(val: string) {
1062     this.state.saveUserSettingsForm.bio = val;
1063     this.setState(this.state);
1064   }
1065
1066   handleAvatarUpload(url: string) {
1067     this.state.saveUserSettingsForm.avatar = url;
1068     this.setState(this.state);
1069   }
1070
1071   handleAvatarRemove() {
1072     this.state.saveUserSettingsForm.avatar = "";
1073     this.setState(this.state);
1074   }
1075
1076   handleBannerUpload(url: string) {
1077     this.state.saveUserSettingsForm.banner = url;
1078     this.setState(this.state);
1079   }
1080
1081   handleBannerRemove() {
1082     this.state.saveUserSettingsForm.banner = "";
1083     this.setState(this.state);
1084   }
1085
1086   handleUserSettingsPreferredUsernameChange(i: Person, event: any) {
1087     i.state.saveUserSettingsForm.display_name = event.target.value;
1088     i.setState(i.state);
1089   }
1090
1091   handleUserSettingsMatrixUserIdChange(i: Person, event: any) {
1092     i.state.saveUserSettingsForm.matrix_user_id = event.target.value;
1093     if (
1094       i.state.saveUserSettingsForm.matrix_user_id == "" &&
1095       !UserService.Instance.localUserView.person.matrix_user_id
1096     ) {
1097       i.state.saveUserSettingsForm.matrix_user_id = undefined;
1098     }
1099     i.setState(i.state);
1100   }
1101
1102   handleNewPasswordChange(i: Person, event: any) {
1103     i.state.changePasswordForm.new_password = event.target.value;
1104     if (i.state.changePasswordForm.new_password == "") {
1105       i.state.changePasswordForm.new_password = undefined;
1106     }
1107     i.setState(i.state);
1108   }
1109
1110   handleNewPasswordVerifyChange(i: Person, event: any) {
1111     i.state.changePasswordForm.new_password_verify = event.target.value;
1112     if (i.state.changePasswordForm.new_password_verify == "") {
1113       i.state.changePasswordForm.new_password_verify = undefined;
1114     }
1115     i.setState(i.state);
1116   }
1117
1118   handleOldPasswordChange(i: Person, event: any) {
1119     i.state.changePasswordForm.old_password = event.target.value;
1120     if (i.state.changePasswordForm.old_password == "") {
1121       i.state.changePasswordForm.old_password = undefined;
1122     }
1123     i.setState(i.state);
1124   }
1125
1126   handleSaveUserSettingsSubmit(i: Person, event: any) {
1127     event.preventDefault();
1128     i.state.saveUserSettingsLoading = true;
1129     i.setState(i.state);
1130
1131     WebSocketService.Instance.send(
1132       wsClient.saveUserSettings(i.state.saveUserSettingsForm)
1133     );
1134   }
1135
1136   handleChangePasswordSubmit(i: Person, event: any) {
1137     event.preventDefault();
1138     i.state.changePasswordLoading = true;
1139     i.setState(i.state);
1140
1141     WebSocketService.Instance.send(
1142       wsClient.changePassword(i.state.changePasswordForm)
1143     );
1144   }
1145
1146   handleDeleteAccountShowConfirmToggle(i: Person, event: any) {
1147     event.preventDefault();
1148     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
1149     i.setState(i.state);
1150   }
1151
1152   handleDeleteAccountPasswordChange(i: Person, event: any) {
1153     i.state.deleteAccountForm.password = event.target.value;
1154     i.setState(i.state);
1155   }
1156
1157   handleLogoutClick(i: Person) {
1158     UserService.Instance.logout();
1159     i.context.router.history.push("/");
1160   }
1161
1162   handleDeleteAccount(i: Person, event: any) {
1163     event.preventDefault();
1164     i.state.deleteAccountLoading = true;
1165     i.setState(i.state);
1166
1167     WebSocketService.Instance.send(
1168       wsClient.deleteAccount(i.state.deleteAccountForm)
1169     );
1170   }
1171
1172   setUserInfo() {
1173     if (this.isCurrentUser) {
1174       this.state.saveUserSettingsForm.show_nsfw =
1175         UserService.Instance.localUserView.local_user.show_nsfw;
1176       this.state.saveUserSettingsForm.theme = UserService.Instance.localUserView
1177         .local_user.theme
1178         ? UserService.Instance.localUserView.local_user.theme
1179         : "browser";
1180       this.state.saveUserSettingsForm.default_sort_type =
1181         UserService.Instance.localUserView.local_user.default_sort_type;
1182       this.state.saveUserSettingsForm.default_listing_type =
1183         UserService.Instance.localUserView.local_user.default_listing_type;
1184       this.state.saveUserSettingsForm.lang =
1185         UserService.Instance.localUserView.local_user.lang;
1186       this.state.saveUserSettingsForm.avatar =
1187         UserService.Instance.localUserView.person.avatar;
1188       this.state.saveUserSettingsForm.banner =
1189         UserService.Instance.localUserView.person.banner;
1190       this.state.saveUserSettingsForm.display_name =
1191         UserService.Instance.localUserView.person.display_name;
1192       this.state.saveUserSettingsForm.show_avatars =
1193         UserService.Instance.localUserView.local_user.show_avatars;
1194       this.state.saveUserSettingsForm.bot_account =
1195         UserService.Instance.localUserView.person.bot_account;
1196       this.state.saveUserSettingsForm.show_bot_accounts =
1197         UserService.Instance.localUserView.local_user.show_bot_accounts;
1198       this.state.saveUserSettingsForm.show_scores =
1199         UserService.Instance.localUserView.local_user.show_scores;
1200       this.state.saveUserSettingsForm.show_read_posts =
1201         UserService.Instance.localUserView.local_user.show_read_posts;
1202       this.state.saveUserSettingsForm.email =
1203         UserService.Instance.localUserView.local_user.email;
1204       this.state.saveUserSettingsForm.bio =
1205         UserService.Instance.localUserView.person.bio;
1206       this.state.saveUserSettingsForm.send_notifications_to_email =
1207         UserService.Instance.localUserView.local_user.send_notifications_to_email;
1208       this.state.saveUserSettingsForm.matrix_user_id =
1209         UserService.Instance.localUserView.person.matrix_user_id;
1210     }
1211   }
1212
1213   parseMessage(msg: any) {
1214     let op = wsUserOp(msg);
1215     console.log(msg);
1216     if (msg.error) {
1217       toast(i18n.t(msg.error), "danger");
1218       if (msg.error == "couldnt_find_that_username_or_email") {
1219         this.context.router.history.push("/");
1220       }
1221       this.setState({
1222         deleteAccountLoading: false,
1223         saveUserSettingsLoading: false,
1224         changePasswordLoading: false,
1225       });
1226       return;
1227     } else if (msg.reconnect) {
1228       this.fetchUserData();
1229     } else if (op == UserOperation.GetPersonDetails) {
1230       // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
1231       // and set the parent state if it is not set or differs
1232       // TODO this might need to get abstracted
1233       let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data;
1234       this.state.personRes = data;
1235       console.log(data);
1236       this.setUserInfo();
1237       this.state.loading = false;
1238       this.setState(this.state);
1239       restoreScrollPosition(this.context);
1240     } else if (op == UserOperation.SaveUserSettings) {
1241       let data = wsJsonToRes<LoginResponse>(msg).data;
1242       UserService.Instance.login(data);
1243       this.state.personRes.person_view.person.bio =
1244         this.state.saveUserSettingsForm.bio;
1245       this.state.personRes.person_view.person.display_name =
1246         this.state.saveUserSettingsForm.display_name;
1247       this.state.personRes.person_view.person.banner =
1248         this.state.saveUserSettingsForm.banner;
1249       this.state.personRes.person_view.person.avatar =
1250         this.state.saveUserSettingsForm.avatar;
1251       this.state.saveUserSettingsLoading = false;
1252       this.setState(this.state);
1253
1254       window.scrollTo(0, 0);
1255     } else if (op == UserOperation.ChangePassword) {
1256       let data = wsJsonToRes<LoginResponse>(msg).data;
1257       UserService.Instance.login(data);
1258       this.state.changePasswordLoading = false;
1259       this.setState(this.state);
1260       window.scrollTo(0, 0);
1261       toast(i18n.t("password_changed"));
1262     } else if (op == UserOperation.DeleteAccount) {
1263       this.setState({
1264         deleteAccountLoading: false,
1265         deleteAccountShowConfirm: false,
1266       });
1267       UserService.Instance.logout();
1268       window.location.href = "/";
1269     } else if (op == UserOperation.AddAdmin) {
1270       let data = wsJsonToRes<AddAdminResponse>(msg).data;
1271       this.state.siteRes.admins = data.admins;
1272       this.setState(this.state);
1273     } else if (op == UserOperation.CreateCommentLike) {
1274       let data = wsJsonToRes<CommentResponse>(msg).data;
1275       createCommentLikeRes(data.comment_view, this.state.personRes.comments);
1276       this.setState(this.state);
1277     } else if (
1278       op == UserOperation.EditComment ||
1279       op == UserOperation.DeleteComment ||
1280       op == UserOperation.RemoveComment
1281     ) {
1282       let data = wsJsonToRes<CommentResponse>(msg).data;
1283       editCommentRes(data.comment_view, this.state.personRes.comments);
1284       this.setState(this.state);
1285     } else if (op == UserOperation.CreateComment) {
1286       let data = wsJsonToRes<CommentResponse>(msg).data;
1287       if (
1288         UserService.Instance.localUserView &&
1289         data.comment_view.creator.id ==
1290           UserService.Instance.localUserView.person.id
1291       ) {
1292         toast(i18n.t("reply_sent"));
1293       }
1294     } else if (op == UserOperation.SaveComment) {
1295       let data = wsJsonToRes<CommentResponse>(msg).data;
1296       saveCommentRes(data.comment_view, this.state.personRes.comments);
1297       this.setState(this.state);
1298     } else if (
1299       op == UserOperation.EditPost ||
1300       op == UserOperation.DeletePost ||
1301       op == UserOperation.RemovePost ||
1302       op == UserOperation.LockPost ||
1303       op == UserOperation.StickyPost ||
1304       op == UserOperation.SavePost
1305     ) {
1306       let data = wsJsonToRes<PostResponse>(msg).data;
1307       editPostFindRes(data.post_view, this.state.personRes.posts);
1308       this.setState(this.state);
1309     } else if (op == UserOperation.CreatePostLike) {
1310       let data = wsJsonToRes<PostResponse>(msg).data;
1311       createPostLikeFindRes(data.post_view, this.state.personRes.posts);
1312       this.setState(this.state);
1313     } else if (op == UserOperation.BanPerson) {
1314       let data = wsJsonToRes<BanPersonResponse>(msg).data;
1315       this.state.personRes.comments
1316         .filter(c => c.creator.id == data.person_view.person.id)
1317         .forEach(c => (c.creator.banned = data.banned));
1318       this.state.personRes.posts
1319         .filter(c => c.creator.id == data.person_view.person.id)
1320         .forEach(c => (c.creator.banned = data.banned));
1321       this.setState(this.state);
1322     }
1323   }
1324 }