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