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