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