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