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