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