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