]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/person.tsx
320c046192f32562d4f12e12a2604f8d70b5a8b1
[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                 maxLength={20}
683               />
684             </div>
685           </div>
686           <div class="form-group row">
687             <label class="col-lg-3 col-form-label" htmlFor="user-bio">
688               {i18n.t("bio")}
689             </label>
690             <div class="col-lg-9">
691               <MarkdownTextArea
692                 initialContent={this.state.saveUserSettingsForm.bio}
693                 onContentChange={this.handleUserSettingsBioChange}
694                 maxLength={300}
695                 hideNavigationWarnings
696               />
697             </div>
698           </div>
699           <div class="form-group row">
700             <label class="col-lg-3 col-form-label" htmlFor="user-email">
701               {i18n.t("email")}
702             </label>
703             <div class="col-lg-9">
704               <input
705                 type="email"
706                 id="user-email"
707                 class="form-control"
708                 placeholder={i18n.t("optional")}
709                 value={this.state.saveUserSettingsForm.email}
710                 onInput={linkEvent(this, this.handleUserSettingsEmailChange)}
711                 minLength={3}
712               />
713             </div>
714           </div>
715           <div class="form-group row">
716             <label class="col-lg-5 col-form-label" htmlFor="matrix-user-id">
717               <a href={elementUrl} rel="noopener">
718                 {i18n.t("matrix_user_id")}
719               </a>
720             </label>
721             <div class="col-lg-7">
722               <input
723                 id="matrix-user-id"
724                 type="text"
725                 class="form-control"
726                 placeholder="@user:example.com"
727                 value={this.state.saveUserSettingsForm.matrix_user_id}
728                 onInput={linkEvent(
729                   this,
730                   this.handleUserSettingsMatrixUserIdChange
731                 )}
732                 pattern="^@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
733               />
734             </div>
735           </div>
736           {this.state.siteRes.site_view.site.enable_nsfw && (
737             <div class="form-group">
738               <div class="form-check">
739                 <input
740                   class="form-check-input"
741                   id="user-show-nsfw"
742                   type="checkbox"
743                   checked={this.state.saveUserSettingsForm.show_nsfw}
744                   onChange={linkEvent(
745                     this,
746                     this.handleUserSettingsShowNsfwChange
747                   )}
748                 />
749                 <label class="form-check-label" htmlFor="user-show-nsfw">
750                   {i18n.t("show_nsfw")}
751                 </label>
752               </div>
753             </div>
754           )}
755           <div class="form-group">
756             <div class="form-check">
757               <input
758                 class="form-check-input"
759                 id="user-show-scores"
760                 type="checkbox"
761                 checked={this.state.saveUserSettingsForm.show_scores}
762                 onChange={linkEvent(
763                   this,
764                   this.handleUserSettingsShowScoresChange
765                 )}
766               />
767               <label class="form-check-label" htmlFor="user-show-scores">
768                 {i18n.t("show_scores")}
769               </label>
770             </div>
771           </div>
772           <div class="form-group">
773             <div class="form-check">
774               <input
775                 class="form-check-input"
776                 id="user-show-avatars"
777                 type="checkbox"
778                 checked={this.state.saveUserSettingsForm.show_avatars}
779                 onChange={linkEvent(
780                   this,
781                   this.handleUserSettingsShowAvatarsChange
782                 )}
783               />
784               <label class="form-check-label" htmlFor="user-show-avatars">
785                 {i18n.t("show_avatars")}
786               </label>
787             </div>
788           </div>
789           <div class="form-group">
790             <div class="form-check">
791               <input
792                 class="form-check-input"
793                 id="user-bot-account"
794                 type="checkbox"
795                 checked={this.state.saveUserSettingsForm.bot_account}
796                 onChange={linkEvent(this, this.handleUserSettingsBotAccount)}
797               />
798               <label class="form-check-label" htmlFor="user-bot-account">
799                 {i18n.t("bot_account")}
800               </label>
801             </div>
802           </div>
803           <div class="form-group">
804             <div class="form-check">
805               <input
806                 class="form-check-input"
807                 id="user-show-bot-accounts"
808                 type="checkbox"
809                 checked={this.state.saveUserSettingsForm.show_bot_accounts}
810                 onChange={linkEvent(
811                   this,
812                   this.handleUserSettingsShowBotAccounts
813                 )}
814               />
815               <label class="form-check-label" htmlFor="user-show-bot-accounts">
816                 {i18n.t("show_bot_accounts")}
817               </label>
818             </div>
819           </div>
820           <div class="form-group">
821             <div class="form-check">
822               <input
823                 class="form-check-input"
824                 id="user-show-read-posts"
825                 type="checkbox"
826                 checked={this.state.saveUserSettingsForm.show_read_posts}
827                 onChange={linkEvent(this, this.handleUserSettingsShowReadPosts)}
828               />
829               <label class="form-check-label" htmlFor="user-show-read-posts">
830                 {i18n.t("show_read_posts")}
831               </label>
832             </div>
833           </div>
834           <div class="form-group">
835             <div class="form-check">
836               <input
837                 class="form-check-input"
838                 id="user-send-notifications-to-email"
839                 type="checkbox"
840                 disabled={!this.state.saveUserSettingsForm.email}
841                 checked={
842                   this.state.saveUserSettingsForm.send_notifications_to_email
843                 }
844                 onChange={linkEvent(
845                   this,
846                   this.handleUserSettingsSendNotificationsToEmailChange
847                 )}
848               />
849               <label
850                 class="form-check-label"
851                 htmlFor="user-send-notifications-to-email"
852               >
853                 {i18n.t("send_notifications_to_email")}
854               </label>
855             </div>
856           </div>
857           <div class="form-group">
858             <button type="submit" class="btn btn-block btn-secondary mr-4">
859               {this.state.saveUserSettingsLoading ? (
860                 <Spinner />
861               ) : (
862                 capitalizeFirstLetter(i18n.t("save"))
863               )}
864             </button>
865           </div>
866           <hr />
867           <div class="form-group">
868             <button
869               class="btn btn-block btn-danger"
870               onClick={linkEvent(
871                 this,
872                 this.handleDeleteAccountShowConfirmToggle
873               )}
874             >
875               {i18n.t("delete_account")}
876             </button>
877             {this.state.deleteAccountShowConfirm && (
878               <>
879                 <div class="my-2 alert alert-danger" role="alert">
880                   {i18n.t("delete_account_confirm")}
881                 </div>
882                 <input
883                   type="password"
884                   value={this.state.deleteAccountForm.password}
885                   autoComplete="new-password"
886                   maxLength={60}
887                   onInput={linkEvent(
888                     this,
889                     this.handleDeleteAccountPasswordChange
890                   )}
891                   class="form-control my-2"
892                 />
893                 <button
894                   class="btn btn-danger mr-4"
895                   disabled={!this.state.deleteAccountForm.password}
896                   onClick={linkEvent(this, this.handleDeleteAccount)}
897                 >
898                   {this.state.deleteAccountLoading ? (
899                     <Spinner />
900                   ) : (
901                     capitalizeFirstLetter(i18n.t("delete"))
902                   )}
903                 </button>
904                 <button
905                   class="btn btn-secondary"
906                   onClick={linkEvent(
907                     this,
908                     this.handleDeleteAccountShowConfirmToggle
909                   )}
910                 >
911                   {i18n.t("cancel")}
912                 </button>
913               </>
914             )}
915           </div>
916         </form>
917       </>
918     );
919   }
920
921   moderates() {
922     return (
923       <div>
924         {this.state.personRes.moderates.length > 0 && (
925           <div class="card border-secondary mb-3">
926             <div class="card-body">
927               <h5>{i18n.t("moderates")}</h5>
928               <ul class="list-unstyled mb-0">
929                 {this.state.personRes.moderates.map(cmv => (
930                   <li>
931                     <CommunityLink community={cmv.community} />
932                   </li>
933                 ))}
934               </ul>
935             </div>
936           </div>
937         )}
938       </div>
939     );
940   }
941
942   follows() {
943     return (
944       <div>
945         {this.state.personRes.follows.length > 0 && (
946           <div class="card border-secondary mb-3">
947             <div class="card-body">
948               <h5>{i18n.t("subscribed")}</h5>
949               <ul class="list-unstyled mb-0">
950                 {this.state.personRes.follows.map(cfv => (
951                   <li>
952                     <CommunityLink community={cfv.community} />
953                   </li>
954                 ))}
955               </ul>
956             </div>
957           </div>
958         )}
959       </div>
960     );
961   }
962
963   updateUrl(paramUpdates: UrlParams) {
964     const page = paramUpdates.page || this.state.page;
965     const viewStr = paramUpdates.view || PersonDetailsView[this.state.view];
966     const sortStr = paramUpdates.sort || this.state.sort;
967
968     let typeView = `/u/${this.state.userName}`;
969
970     this.props.history.push(
971       `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
972     );
973     this.state.loading = true;
974     this.setState(this.state);
975     this.fetchUserData();
976   }
977
978   handlePageChange(page: number) {
979     this.updateUrl({ page });
980   }
981
982   handleSortChange(val: SortType) {
983     this.updateUrl({ sort: val, page: 1 });
984   }
985
986   handleViewChange(i: Person, event: any) {
987     i.updateUrl({
988       view: PersonDetailsView[Number(event.target.value)],
989       page: 1,
990     });
991   }
992
993   handleUserSettingsShowNsfwChange(i: Person, event: any) {
994     i.state.saveUserSettingsForm.show_nsfw = event.target.checked;
995     i.setState(i.state);
996   }
997
998   handleUserSettingsShowAvatarsChange(i: Person, event: any) {
999     i.state.saveUserSettingsForm.show_avatars = event.target.checked;
1000     UserService.Instance.localUserView.local_user.show_avatars =
1001       event.target.checked; // Just for instant updates
1002     i.setState(i.state);
1003   }
1004
1005   handleUserSettingsBotAccount(i: Person, event: any) {
1006     i.state.saveUserSettingsForm.bot_account = event.target.checked;
1007     i.setState(i.state);
1008   }
1009
1010   handleUserSettingsShowBotAccounts(i: Person, event: any) {
1011     i.state.saveUserSettingsForm.show_bot_accounts = event.target.checked;
1012     i.setState(i.state);
1013   }
1014
1015   handleUserSettingsShowReadPosts(i: Person, event: any) {
1016     i.state.saveUserSettingsForm.show_read_posts = event.target.checked;
1017     i.setState(i.state);
1018   }
1019
1020   handleUserSettingsShowScoresChange(i: Person, event: any) {
1021     i.state.saveUserSettingsForm.show_scores = event.target.checked;
1022     UserService.Instance.localUserView.local_user.show_scores =
1023       event.target.checked; // Just for instant updates
1024     i.setState(i.state);
1025   }
1026
1027   handleUserSettingsSendNotificationsToEmailChange(i: Person, event: any) {
1028     i.state.saveUserSettingsForm.send_notifications_to_email =
1029       event.target.checked;
1030     i.setState(i.state);
1031   }
1032
1033   handleUserSettingsThemeChange(i: Person, event: any) {
1034     i.state.saveUserSettingsForm.theme = event.target.value;
1035     setTheme(event.target.value, true);
1036     i.setState(i.state);
1037   }
1038
1039   handleUserSettingsLangChange(i: Person, event: any) {
1040     i.state.saveUserSettingsForm.lang = event.target.value;
1041     i18n.changeLanguage(getLanguage(i.state.saveUserSettingsForm.lang));
1042     i.setState(i.state);
1043   }
1044
1045   handleUserSettingsSortTypeChange(val: SortType) {
1046     this.state.saveUserSettingsForm.default_sort_type =
1047       Object.keys(SortType).indexOf(val);
1048     this.setState(this.state);
1049   }
1050
1051   handleUserSettingsListingTypeChange(val: ListingType) {
1052     this.state.saveUserSettingsForm.default_listing_type =
1053       Object.keys(ListingType).indexOf(val);
1054     this.setState(this.state);
1055   }
1056
1057   handleUserSettingsEmailChange(i: Person, event: any) {
1058     i.state.saveUserSettingsForm.email = event.target.value;
1059     i.setState(i.state);
1060   }
1061
1062   handleUserSettingsBioChange(val: string) {
1063     this.state.saveUserSettingsForm.bio = val;
1064     this.setState(this.state);
1065   }
1066
1067   handleAvatarUpload(url: string) {
1068     this.state.saveUserSettingsForm.avatar = url;
1069     this.setState(this.state);
1070   }
1071
1072   handleAvatarRemove() {
1073     this.state.saveUserSettingsForm.avatar = "";
1074     this.setState(this.state);
1075   }
1076
1077   handleBannerUpload(url: string) {
1078     this.state.saveUserSettingsForm.banner = url;
1079     this.setState(this.state);
1080   }
1081
1082   handleBannerRemove() {
1083     this.state.saveUserSettingsForm.banner = "";
1084     this.setState(this.state);
1085   }
1086
1087   handleUserSettingsPreferredUsernameChange(i: Person, event: any) {
1088     i.state.saveUserSettingsForm.display_name = event.target.value;
1089     i.setState(i.state);
1090   }
1091
1092   handleUserSettingsMatrixUserIdChange(i: Person, event: any) {
1093     i.state.saveUserSettingsForm.matrix_user_id = event.target.value;
1094     if (
1095       i.state.saveUserSettingsForm.matrix_user_id == "" &&
1096       !UserService.Instance.localUserView.person.matrix_user_id
1097     ) {
1098       i.state.saveUserSettingsForm.matrix_user_id = undefined;
1099     }
1100     i.setState(i.state);
1101   }
1102
1103   handleNewPasswordChange(i: Person, event: any) {
1104     i.state.changePasswordForm.new_password = event.target.value;
1105     if (i.state.changePasswordForm.new_password == "") {
1106       i.state.changePasswordForm.new_password = undefined;
1107     }
1108     i.setState(i.state);
1109   }
1110
1111   handleNewPasswordVerifyChange(i: Person, event: any) {
1112     i.state.changePasswordForm.new_password_verify = event.target.value;
1113     if (i.state.changePasswordForm.new_password_verify == "") {
1114       i.state.changePasswordForm.new_password_verify = undefined;
1115     }
1116     i.setState(i.state);
1117   }
1118
1119   handleOldPasswordChange(i: Person, event: any) {
1120     i.state.changePasswordForm.old_password = event.target.value;
1121     if (i.state.changePasswordForm.old_password == "") {
1122       i.state.changePasswordForm.old_password = undefined;
1123     }
1124     i.setState(i.state);
1125   }
1126
1127   handleSaveUserSettingsSubmit(i: Person, event: any) {
1128     event.preventDefault();
1129     i.state.saveUserSettingsLoading = true;
1130     i.setState(i.state);
1131
1132     WebSocketService.Instance.send(
1133       wsClient.saveUserSettings(i.state.saveUserSettingsForm)
1134     );
1135   }
1136
1137   handleChangePasswordSubmit(i: Person, event: any) {
1138     event.preventDefault();
1139     i.state.changePasswordLoading = true;
1140     i.setState(i.state);
1141
1142     WebSocketService.Instance.send(
1143       wsClient.changePassword(i.state.changePasswordForm)
1144     );
1145   }
1146
1147   handleDeleteAccountShowConfirmToggle(i: Person, event: any) {
1148     event.preventDefault();
1149     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
1150     i.setState(i.state);
1151   }
1152
1153   handleDeleteAccountPasswordChange(i: Person, event: any) {
1154     i.state.deleteAccountForm.password = event.target.value;
1155     i.setState(i.state);
1156   }
1157
1158   handleLogoutClick(i: Person) {
1159     UserService.Instance.logout();
1160     i.context.router.history.push("/");
1161   }
1162
1163   handleDeleteAccount(i: Person, event: any) {
1164     event.preventDefault();
1165     i.state.deleteAccountLoading = true;
1166     i.setState(i.state);
1167
1168     WebSocketService.Instance.send(
1169       wsClient.deleteAccount(i.state.deleteAccountForm)
1170     );
1171   }
1172
1173   setUserInfo() {
1174     if (this.isCurrentUser) {
1175       this.state.saveUserSettingsForm.show_nsfw =
1176         UserService.Instance.localUserView.local_user.show_nsfw;
1177       this.state.saveUserSettingsForm.theme = UserService.Instance.localUserView
1178         .local_user.theme
1179         ? UserService.Instance.localUserView.local_user.theme
1180         : "browser";
1181       this.state.saveUserSettingsForm.default_sort_type =
1182         UserService.Instance.localUserView.local_user.default_sort_type;
1183       this.state.saveUserSettingsForm.default_listing_type =
1184         UserService.Instance.localUserView.local_user.default_listing_type;
1185       this.state.saveUserSettingsForm.lang =
1186         UserService.Instance.localUserView.local_user.lang;
1187       this.state.saveUserSettingsForm.avatar =
1188         UserService.Instance.localUserView.person.avatar;
1189       this.state.saveUserSettingsForm.banner =
1190         UserService.Instance.localUserView.person.banner;
1191       this.state.saveUserSettingsForm.display_name =
1192         UserService.Instance.localUserView.person.display_name;
1193       this.state.saveUserSettingsForm.show_avatars =
1194         UserService.Instance.localUserView.local_user.show_avatars;
1195       this.state.saveUserSettingsForm.bot_account =
1196         UserService.Instance.localUserView.person.bot_account;
1197       this.state.saveUserSettingsForm.show_bot_accounts =
1198         UserService.Instance.localUserView.local_user.show_bot_accounts;
1199       this.state.saveUserSettingsForm.show_scores =
1200         UserService.Instance.localUserView.local_user.show_scores;
1201       this.state.saveUserSettingsForm.show_read_posts =
1202         UserService.Instance.localUserView.local_user.show_read_posts;
1203       this.state.saveUserSettingsForm.email =
1204         UserService.Instance.localUserView.local_user.email;
1205       this.state.saveUserSettingsForm.bio =
1206         UserService.Instance.localUserView.person.bio;
1207       this.state.saveUserSettingsForm.send_notifications_to_email =
1208         UserService.Instance.localUserView.local_user.send_notifications_to_email;
1209       this.state.saveUserSettingsForm.matrix_user_id =
1210         UserService.Instance.localUserView.person.matrix_user_id;
1211     }
1212   }
1213
1214   parseMessage(msg: any) {
1215     let op = wsUserOp(msg);
1216     console.log(msg);
1217     if (msg.error) {
1218       toast(i18n.t(msg.error), "danger");
1219       if (msg.error == "couldnt_find_that_username_or_email") {
1220         this.context.router.history.push("/");
1221       }
1222       this.setState({
1223         deleteAccountLoading: false,
1224         saveUserSettingsLoading: false,
1225         changePasswordLoading: false,
1226       });
1227       return;
1228     } else if (msg.reconnect) {
1229       this.fetchUserData();
1230     } else if (op == UserOperation.GetPersonDetails) {
1231       // Since the PersonDetails contains posts/comments as well as some general user info we listen here as well
1232       // and set the parent state if it is not set or differs
1233       // TODO this might need to get abstracted
1234       let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data;
1235       this.state.personRes = data;
1236       console.log(data);
1237       this.setUserInfo();
1238       this.state.loading = false;
1239       this.setState(this.state);
1240       restoreScrollPosition(this.context);
1241     } else if (op == UserOperation.SaveUserSettings) {
1242       let data = wsJsonToRes<LoginResponse>(msg).data;
1243       UserService.Instance.login(data);
1244       this.state.personRes.person_view.person.bio =
1245         this.state.saveUserSettingsForm.bio;
1246       this.state.personRes.person_view.person.display_name =
1247         this.state.saveUserSettingsForm.display_name;
1248       this.state.personRes.person_view.person.banner =
1249         this.state.saveUserSettingsForm.banner;
1250       this.state.personRes.person_view.person.avatar =
1251         this.state.saveUserSettingsForm.avatar;
1252       this.state.saveUserSettingsLoading = false;
1253       this.setState(this.state);
1254
1255       window.scrollTo(0, 0);
1256     } else if (op == UserOperation.ChangePassword) {
1257       let data = wsJsonToRes<LoginResponse>(msg).data;
1258       UserService.Instance.login(data);
1259       this.state.changePasswordLoading = false;
1260       this.setState(this.state);
1261       window.scrollTo(0, 0);
1262       toast(i18n.t("password_changed"));
1263     } else if (op == UserOperation.DeleteAccount) {
1264       this.setState({
1265         deleteAccountLoading: false,
1266         deleteAccountShowConfirm: false,
1267       });
1268       UserService.Instance.logout();
1269       window.location.href = "/";
1270     } else if (op == UserOperation.AddAdmin) {
1271       let data = wsJsonToRes<AddAdminResponse>(msg).data;
1272       this.state.siteRes.admins = data.admins;
1273       this.setState(this.state);
1274     } else if (op == UserOperation.CreateCommentLike) {
1275       let data = wsJsonToRes<CommentResponse>(msg).data;
1276       createCommentLikeRes(data.comment_view, this.state.personRes.comments);
1277       this.setState(this.state);
1278     } else if (
1279       op == UserOperation.EditComment ||
1280       op == UserOperation.DeleteComment ||
1281       op == UserOperation.RemoveComment
1282     ) {
1283       let data = wsJsonToRes<CommentResponse>(msg).data;
1284       editCommentRes(data.comment_view, this.state.personRes.comments);
1285       this.setState(this.state);
1286     } else if (op == UserOperation.CreateComment) {
1287       let data = wsJsonToRes<CommentResponse>(msg).data;
1288       if (
1289         UserService.Instance.localUserView &&
1290         data.comment_view.creator.id ==
1291           UserService.Instance.localUserView.person.id
1292       ) {
1293         toast(i18n.t("reply_sent"));
1294       }
1295     } else if (op == UserOperation.SaveComment) {
1296       let data = wsJsonToRes<CommentResponse>(msg).data;
1297       saveCommentRes(data.comment_view, this.state.personRes.comments);
1298       this.setState(this.state);
1299     } else if (
1300       op == UserOperation.EditPost ||
1301       op == UserOperation.DeletePost ||
1302       op == UserOperation.RemovePost ||
1303       op == UserOperation.LockPost ||
1304       op == UserOperation.StickyPost ||
1305       op == UserOperation.SavePost
1306     ) {
1307       let data = wsJsonToRes<PostResponse>(msg).data;
1308       editPostFindRes(data.post_view, this.state.personRes.posts);
1309       this.setState(this.state);
1310     } else if (op == UserOperation.CreatePostLike) {
1311       let data = wsJsonToRes<PostResponse>(msg).data;
1312       createPostLikeFindRes(data.post_view, this.state.personRes.posts);
1313       this.setState(this.state);
1314     } else if (op == UserOperation.BanPerson) {
1315       let data = wsJsonToRes<BanPersonResponse>(msg).data;
1316       this.state.personRes.comments
1317         .filter(c => c.creator.id == data.person_view.person.id)
1318         .forEach(c => (c.creator.banned = data.banned));
1319       this.state.personRes.posts
1320         .filter(c => c.creator.id == data.person_view.person.id)
1321         .forEach(c => (c.creator.banned = data.banned));
1322       this.setState(this.state);
1323     }
1324   }
1325 }