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