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