]> Untitled Git - lemmy.git/blob - ui/src/components/user.tsx
routes.api: fix get_captcha endpoint (#1135)
[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   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: 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?: 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                   showLocal={
543                     this.state.siteRes.federated_instances &&
544                     this.state.siteRes.federated_instances.length > 0
545                   }
546                   onChange={this.handleUserSettingsListingTypeChange}
547                 />
548               </form>
549               <form className="form-group">
550                 <label>
551                   <div class="mr-2">{i18n.t('type')}</div>
552                 </label>
553                 <SortSelect
554                   sort={
555                     Object.values(SortType)[
556                       this.state.userSettingsForm.default_sort_type
557                     ]
558                   }
559                   onChange={this.handleUserSettingsSortTypeChange}
560                 />
561               </form>
562               <div class="form-group row">
563                 <label class="col-lg-5 col-form-label">
564                   {i18n.t('display_name')}
565                 </label>
566                 <div class="col-lg-7">
567                   <input
568                     type="text"
569                     class="form-control"
570                     placeholder={i18n.t('optional')}
571                     value={this.state.userSettingsForm.preferred_username}
572                     onInput={linkEvent(
573                       this,
574                       this.handleUserSettingsPreferredUsernameChange
575                     )}
576                     pattern="^(?!@)(.+)$"
577                     minLength={3}
578                     maxLength={20}
579                   />
580                 </div>
581               </div>
582               <div class="form-group row">
583                 <label class="col-lg-3 col-form-label" htmlFor="user-bio">
584                   {i18n.t('bio')}
585                 </label>
586                 <div class="col-lg-9">
587                   <MarkdownTextArea
588                     initialContent={this.state.userSettingsForm.bio}
589                     onContentChange={this.handleUserSettingsBioChange}
590                     maxLength={300}
591                     hideNavigationWarnings
592                   />
593                 </div>
594               </div>
595               <div class="form-group row">
596                 <label class="col-lg-3 col-form-label" htmlFor="user-email">
597                   {i18n.t('email')}
598                 </label>
599                 <div class="col-lg-9">
600                   <input
601                     type="email"
602                     id="user-email"
603                     class="form-control"
604                     placeholder={i18n.t('optional')}
605                     value={this.state.userSettingsForm.email}
606                     onInput={linkEvent(
607                       this,
608                       this.handleUserSettingsEmailChange
609                     )}
610                     minLength={3}
611                   />
612                 </div>
613               </div>
614               <div class="form-group row">
615                 <label class="col-lg-5 col-form-label">
616                   <a href={elementUrl} target="_blank" rel="noopener">
617                     {i18n.t('matrix_user_id')}
618                   </a>
619                 </label>
620                 <div class="col-lg-7">
621                   <input
622                     type="text"
623                     class="form-control"
624                     placeholder="@user:example.com"
625                     value={this.state.userSettingsForm.matrix_user_id}
626                     onInput={linkEvent(
627                       this,
628                       this.handleUserSettingsMatrixUserIdChange
629                     )}
630                     minLength={3}
631                   />
632                 </div>
633               </div>
634               <div class="form-group row">
635                 <label class="col-lg-5 col-form-label" htmlFor="user-password">
636                   {i18n.t('new_password')}
637                 </label>
638                 <div class="col-lg-7">
639                   <input
640                     type="password"
641                     id="user-password"
642                     class="form-control"
643                     value={this.state.userSettingsForm.new_password}
644                     autoComplete="new-password"
645                     onInput={linkEvent(
646                       this,
647                       this.handleUserSettingsNewPasswordChange
648                     )}
649                   />
650                 </div>
651               </div>
652               <div class="form-group row">
653                 <label
654                   class="col-lg-5 col-form-label"
655                   htmlFor="user-verify-password"
656                 >
657                   {i18n.t('verify_password')}
658                 </label>
659                 <div class="col-lg-7">
660                   <input
661                     type="password"
662                     id="user-verify-password"
663                     class="form-control"
664                     value={this.state.userSettingsForm.new_password_verify}
665                     autoComplete="new-password"
666                     onInput={linkEvent(
667                       this,
668                       this.handleUserSettingsNewPasswordVerifyChange
669                     )}
670                   />
671                 </div>
672               </div>
673               <div class="form-group row">
674                 <label
675                   class="col-lg-5 col-form-label"
676                   htmlFor="user-old-password"
677                 >
678                   {i18n.t('old_password')}
679                 </label>
680                 <div class="col-lg-7">
681                   <input
682                     type="password"
683                     id="user-old-password"
684                     class="form-control"
685                     value={this.state.userSettingsForm.old_password}
686                     autoComplete="new-password"
687                     onInput={linkEvent(
688                       this,
689                       this.handleUserSettingsOldPasswordChange
690                     )}
691                   />
692                 </div>
693               </div>
694               {this.state.siteRes.site.enable_nsfw && (
695                 <div class="form-group">
696                   <div class="form-check">
697                     <input
698                       class="form-check-input"
699                       id="user-show-nsfw"
700                       type="checkbox"
701                       checked={this.state.userSettingsForm.show_nsfw}
702                       onChange={linkEvent(
703                         this,
704                         this.handleUserSettingsShowNsfwChange
705                       )}
706                     />
707                     <label class="form-check-label" htmlFor="user-show-nsfw">
708                       {i18n.t('show_nsfw')}
709                     </label>
710                   </div>
711                 </div>
712               )}
713               <div class="form-group">
714                 <div class="form-check">
715                   <input
716                     class="form-check-input"
717                     id="user-show-avatars"
718                     type="checkbox"
719                     checked={this.state.userSettingsForm.show_avatars}
720                     onChange={linkEvent(
721                       this,
722                       this.handleUserSettingsShowAvatarsChange
723                     )}
724                   />
725                   <label class="form-check-label" htmlFor="user-show-avatars">
726                     {i18n.t('show_avatars')}
727                   </label>
728                 </div>
729               </div>
730               <div class="form-group">
731                 <div class="form-check">
732                   <input
733                     class="form-check-input"
734                     id="user-send-notifications-to-email"
735                     type="checkbox"
736                     disabled={!this.state.userSettingsForm.email}
737                     checked={
738                       this.state.userSettingsForm.send_notifications_to_email
739                     }
740                     onChange={linkEvent(
741                       this,
742                       this.handleUserSettingsSendNotificationsToEmailChange
743                     )}
744                   />
745                   <label
746                     class="form-check-label"
747                     htmlFor="user-send-notifications-to-email"
748                   >
749                     {i18n.t('send_notifications_to_email')}
750                   </label>
751                 </div>
752               </div>
753               <div class="form-group">
754                 <button type="submit" class="btn btn-block btn-secondary mr-4">
755                   {this.state.userSettingsLoading ? (
756                     <svg class="icon icon-spinner spin">
757                       <use xlinkHref="#icon-spinner"></use>
758                     </svg>
759                   ) : (
760                     capitalizeFirstLetter(i18n.t('save'))
761                   )}
762                 </button>
763               </div>
764               <hr />
765               <div class="form-group mb-0">
766                 <button
767                   class="btn btn-block btn-danger"
768                   onClick={linkEvent(
769                     this,
770                     this.handleDeleteAccountShowConfirmToggle
771                   )}
772                 >
773                   {i18n.t('delete_account')}
774                 </button>
775                 {this.state.deleteAccountShowConfirm && (
776                   <>
777                     <div class="my-2 alert alert-danger" role="alert">
778                       {i18n.t('delete_account_confirm')}
779                     </div>
780                     <input
781                       type="password"
782                       value={this.state.deleteAccountForm.password}
783                       autoComplete="new-password"
784                       onInput={linkEvent(
785                         this,
786                         this.handleDeleteAccountPasswordChange
787                       )}
788                       class="form-control my-2"
789                     />
790                     <button
791                       class="btn btn-danger mr-4"
792                       disabled={!this.state.deleteAccountForm.password}
793                       onClick={linkEvent(this, this.handleDeleteAccount)}
794                     >
795                       {this.state.deleteAccountLoading ? (
796                         <svg class="icon icon-spinner spin">
797                           <use xlinkHref="#icon-spinner"></use>
798                         </svg>
799                       ) : (
800                         capitalizeFirstLetter(i18n.t('delete'))
801                       )}
802                     </button>
803                     <button
804                       class="btn btn-secondary"
805                       onClick={linkEvent(
806                         this,
807                         this.handleDeleteAccountShowConfirmToggle
808                       )}
809                     >
810                       {i18n.t('cancel')}
811                     </button>
812                   </>
813                 )}
814               </div>
815             </form>
816           </div>
817         </div>
818       </div>
819     );
820   }
821
822   moderates() {
823     return (
824       <div>
825         {this.state.moderates.length > 0 && (
826           <div class="card bg-transparent border-secondary mb-3">
827             <div class="card-body">
828               <h5>{i18n.t('moderates')}</h5>
829               <ul class="list-unstyled mb-0">
830                 {this.state.moderates.map(community => (
831                   <li>
832                     <Link to={`/c/${community.community_name}`}>
833                       {community.community_name}
834                     </Link>
835                   </li>
836                 ))}
837               </ul>
838             </div>
839           </div>
840         )}
841       </div>
842     );
843   }
844
845   follows() {
846     return (
847       <div>
848         {this.state.follows.length > 0 && (
849           <div class="card bg-transparent border-secondary mb-3">
850             <div class="card-body">
851               <h5>{i18n.t('subscribed')}</h5>
852               <ul class="list-unstyled mb-0">
853                 {this.state.follows.map(community => (
854                   <li>
855                     <Link to={`/c/${community.community_name}`}>
856                       {community.community_name}
857                     </Link>
858                   </li>
859                 ))}
860               </ul>
861             </div>
862           </div>
863         )}
864       </div>
865     );
866   }
867
868   updateUrl(paramUpdates: UrlParams) {
869     const page = paramUpdates.page || this.state.page;
870     const viewStr = paramUpdates.view || UserDetailsView[this.state.view];
871     const sortStr = paramUpdates.sort || this.state.sort;
872     this.props.history.push(
873       `/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
874     );
875   }
876
877   handlePageChange(page: number) {
878     this.updateUrl({ page });
879   }
880
881   handleSortChange(val: SortType) {
882     this.updateUrl({ sort: val, page: 1 });
883   }
884
885   handleViewChange(i: User, event: any) {
886     i.updateUrl({
887       view: UserDetailsView[Number(event.target.value)],
888       page: 1,
889     });
890   }
891
892   handleUserSettingsShowNsfwChange(i: User, event: any) {
893     i.state.userSettingsForm.show_nsfw = event.target.checked;
894     i.setState(i.state);
895   }
896
897   handleUserSettingsShowAvatarsChange(i: User, event: any) {
898     i.state.userSettingsForm.show_avatars = event.target.checked;
899     UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
900     i.setState(i.state);
901   }
902
903   handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
904     i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
905     i.setState(i.state);
906   }
907
908   handleUserSettingsThemeChange(i: User, event: any) {
909     i.state.userSettingsForm.theme = event.target.value;
910     setTheme(event.target.value, true);
911     i.setState(i.state);
912   }
913
914   handleUserSettingsLangChange(i: User, event: any) {
915     i.state.userSettingsForm.lang = event.target.value;
916     i18n.changeLanguage(getLanguage(i.state.userSettingsForm.lang));
917     i.setState(i.state);
918   }
919
920   handleUserSettingsSortTypeChange(val: SortType) {
921     this.state.userSettingsForm.default_sort_type = Object.keys(
922       SortType
923     ).indexOf(val);
924     this.setState(this.state);
925   }
926
927   handleUserSettingsListingTypeChange(val: ListingType) {
928     this.state.userSettingsForm.default_listing_type = Object.keys(
929       ListingType
930     ).indexOf(val);
931     this.setState(this.state);
932   }
933
934   handleUserSettingsEmailChange(i: User, event: any) {
935     i.state.userSettingsForm.email = event.target.value;
936     i.setState(i.state);
937   }
938
939   handleUserSettingsBioChange(val: string) {
940     this.state.userSettingsForm.bio = val;
941     this.setState(this.state);
942   }
943
944   handleAvatarUpload(url: string) {
945     this.state.userSettingsForm.avatar = url;
946     this.setState(this.state);
947   }
948
949   handleAvatarRemove() {
950     this.state.userSettingsForm.avatar = '';
951     this.setState(this.state);
952   }
953
954   handleBannerUpload(url: string) {
955     this.state.userSettingsForm.banner = url;
956     this.setState(this.state);
957   }
958
959   handleBannerRemove() {
960     this.state.userSettingsForm.banner = '';
961     this.setState(this.state);
962   }
963
964   handleUserSettingsPreferredUsernameChange(i: User, event: any) {
965     i.state.userSettingsForm.preferred_username = event.target.value;
966     i.setState(i.state);
967   }
968
969   handleUserSettingsMatrixUserIdChange(i: User, event: any) {
970     i.state.userSettingsForm.matrix_user_id = event.target.value;
971     if (
972       i.state.userSettingsForm.matrix_user_id == '' &&
973       !i.state.user.matrix_user_id
974     ) {
975       i.state.userSettingsForm.matrix_user_id = undefined;
976     }
977     i.setState(i.state);
978   }
979
980   handleUserSettingsNewPasswordChange(i: User, event: any) {
981     i.state.userSettingsForm.new_password = event.target.value;
982     if (i.state.userSettingsForm.new_password == '') {
983       i.state.userSettingsForm.new_password = undefined;
984     }
985     i.setState(i.state);
986   }
987
988   handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
989     i.state.userSettingsForm.new_password_verify = event.target.value;
990     if (i.state.userSettingsForm.new_password_verify == '') {
991       i.state.userSettingsForm.new_password_verify = undefined;
992     }
993     i.setState(i.state);
994   }
995
996   handleUserSettingsOldPasswordChange(i: User, event: any) {
997     i.state.userSettingsForm.old_password = event.target.value;
998     if (i.state.userSettingsForm.old_password == '') {
999       i.state.userSettingsForm.old_password = undefined;
1000     }
1001     i.setState(i.state);
1002   }
1003
1004   handleUserSettingsSubmit(i: User, event: any) {
1005     event.preventDefault();
1006     i.state.userSettingsLoading = true;
1007     i.setState(i.state);
1008
1009     WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
1010   }
1011
1012   handleDeleteAccountShowConfirmToggle(i: User, event: any) {
1013     event.preventDefault();
1014     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
1015     i.setState(i.state);
1016   }
1017
1018   handleDeleteAccountPasswordChange(i: User, event: any) {
1019     i.state.deleteAccountForm.password = event.target.value;
1020     i.setState(i.state);
1021   }
1022
1023   handleLogoutClick(i: User) {
1024     UserService.Instance.logout();
1025     i.context.router.history.push('/');
1026   }
1027
1028   handleDeleteAccount(i: User, event: any) {
1029     event.preventDefault();
1030     i.state.deleteAccountLoading = true;
1031     i.setState(i.state);
1032
1033     WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
1034   }
1035
1036   parseMessage(msg: WebSocketJsonResponse) {
1037     console.log(msg);
1038     const res = wsJsonToRes(msg);
1039     if (msg.error) {
1040       toast(i18n.t(msg.error), 'danger');
1041       if (msg.error == 'couldnt_find_that_username_or_email') {
1042         this.context.router.history.push('/');
1043       }
1044       this.setState({
1045         deleteAccountLoading: false,
1046         userSettingsLoading: false,
1047       });
1048       return;
1049     } else if (res.op == UserOperation.GetUserDetails) {
1050       // Since the UserDetails contains posts/comments as well as some general user info we listen here as well
1051       // and set the parent state if it is not set or differs
1052       const data = res.data as UserDetailsResponse;
1053
1054       if (this.state.user.id !== data.user.id) {
1055         this.state.user = data.user;
1056         this.state.follows = data.follows;
1057         this.state.moderates = data.moderates;
1058
1059         if (this.isCurrentUser) {
1060           this.state.userSettingsForm.show_nsfw =
1061             UserService.Instance.user.show_nsfw;
1062           this.state.userSettingsForm.theme = UserService.Instance.user.theme
1063             ? UserService.Instance.user.theme
1064             : 'darkly';
1065           this.state.userSettingsForm.default_sort_type =
1066             UserService.Instance.user.default_sort_type;
1067           this.state.userSettingsForm.default_listing_type =
1068             UserService.Instance.user.default_listing_type;
1069           this.state.userSettingsForm.lang = UserService.Instance.user.lang;
1070           this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
1071           this.state.userSettingsForm.banner = UserService.Instance.user.banner;
1072           this.state.userSettingsForm.preferred_username =
1073             UserService.Instance.user.preferred_username;
1074           this.state.userSettingsForm.show_avatars =
1075             UserService.Instance.user.show_avatars;
1076           this.state.userSettingsForm.email = UserService.Instance.user.email;
1077           this.state.userSettingsForm.bio = UserService.Instance.user.bio;
1078           this.state.userSettingsForm.send_notifications_to_email =
1079             UserService.Instance.user.send_notifications_to_email;
1080           this.state.userSettingsForm.matrix_user_id =
1081             UserService.Instance.user.matrix_user_id;
1082         }
1083         this.state.loading = false;
1084         this.setState(this.state);
1085       }
1086     } else if (res.op == UserOperation.SaveUserSettings) {
1087       const data = res.data as LoginResponse;
1088       UserService.Instance.login(data);
1089       this.state.user.bio = this.state.userSettingsForm.bio;
1090       this.state.user.preferred_username = this.state.userSettingsForm.preferred_username;
1091       this.state.user.banner = this.state.userSettingsForm.banner;
1092       this.state.user.avatar = this.state.userSettingsForm.avatar;
1093       this.state.userSettingsLoading = false;
1094       this.setState(this.state);
1095
1096       window.scrollTo(0, 0);
1097     } else if (res.op == UserOperation.DeleteAccount) {
1098       this.setState({
1099         deleteAccountLoading: false,
1100         deleteAccountShowConfirm: false,
1101       });
1102       this.context.router.history.push('/');
1103     } else if (res.op == UserOperation.GetSite) {
1104       const data = res.data as GetSiteResponse;
1105       this.state.siteRes = data;
1106       this.setState(this.state);
1107     } else if (res.op == UserOperation.AddAdmin) {
1108       const data = res.data as AddAdminResponse;
1109       this.state.siteRes.admins = data.admins;
1110       this.setState(this.state);
1111     }
1112   }
1113 }