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