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