]> Untitled Git - lemmy.git/blob - ui/src/components/user.tsx
Merge branch 'master' into federation
[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 } from '../interfaces';
22 import { WebSocketService, UserService } from '../services';
23 import {
24   msgOp,
25   fetchLimit,
26   routeSortTypeToEnum,
27   capitalizeFirstLetter,
28   themes,
29   setTheme,
30   languages,
31   showAvatars,
32 } from '../utils';
33 import { PostListing } from './post-listing';
34 import { SortSelect } from './sort-select';
35 import { ListingTypeSelect } from './listing-type-select';
36 import { CommentNodes } from './comment-nodes';
37 import { MomentTime } from './moment-time';
38 import { i18n } from '../i18next';
39 import { T } from 'inferno-i18next';
40
41 enum View {
42   Overview,
43   Comments,
44   Posts,
45   Saved,
46 }
47
48 interface UserState {
49   user: UserView;
50   user_id: number;
51   username: string;
52   follows: Array<CommunityUser>;
53   moderates: Array<CommunityUser>;
54   comments: Array<Comment>;
55   posts: Array<Post>;
56   saved?: Array<Post>;
57   admins: Array<UserView>;
58   view: View;
59   sort: SortType;
60   page: number;
61   loading: boolean;
62   avatarLoading: boolean;
63   userSettingsForm: UserSettingsForm;
64   userSettingsLoading: boolean;
65   deleteAccountLoading: boolean;
66   deleteAccountShowConfirm: boolean;
67   deleteAccountForm: DeleteAccountForm;
68 }
69
70 export class User extends Component<any, UserState> {
71   private subscription: Subscription;
72   private emptyState: UserState = {
73     user: {
74       id: null,
75       name: null,
76       fedi_name: null,
77       published: null,
78       number_of_posts: null,
79       post_score: null,
80       number_of_comments: null,
81       comment_score: null,
82       banned: null,
83       avatar: null,
84       show_avatars: null,
85       send_notifications_to_email: null,
86     },
87     user_id: null,
88     username: null,
89     follows: [],
90     moderates: [],
91     comments: [],
92     posts: [],
93     admins: [],
94     loading: true,
95     avatarLoading: false,
96     view: this.getViewFromProps(this.props),
97     sort: this.getSortTypeFromProps(this.props),
98     page: this.getPageFromProps(this.props),
99     userSettingsForm: {
100       show_nsfw: null,
101       theme: null,
102       default_sort_type: null,
103       default_listing_type: null,
104       lang: null,
105       show_avatars: null,
106       send_notifications_to_email: null,
107       auth: null,
108     },
109     userSettingsLoading: null,
110     deleteAccountLoading: null,
111     deleteAccountShowConfirm: false,
112     deleteAccountForm: {
113       password: null,
114     },
115   };
116
117   constructor(props: any, context: any) {
118     super(props, context);
119
120     this.state = this.emptyState;
121     this.handleSortChange = this.handleSortChange.bind(this);
122     this.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind(
123       this
124     );
125     this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
126       this
127     );
128
129     this.state.user_id = Number(this.props.match.params.id);
130     this.state.username = this.props.match.params.username;
131
132     this.subscription = WebSocketService.Instance.subject
133       .pipe(
134         retryWhen(errors =>
135           errors.pipe(
136             delay(3000),
137             take(10)
138           )
139         )
140       )
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>
253             <T i18nKey="view">#</T>
254           </option>
255           <option value={View.Overview}>
256             <T i18nKey="overview">#</T>
257           </option>
258           <option value={View.Comments}>
259             <T i18nKey="comments">#</T>
260           </option>
261           <option value={View.Posts}>
262             <T i18nKey="posts">#</T>
263           </option>
264           <option value={View.Saved}>
265             <T i18nKey="saved">#</T>
266           </option>
267         </select>
268         <span class="ml-2">
269           <SortSelect
270             sort={this.state.sort}
271             onChange={this.handleSortChange}
272             hideHot
273           />
274         </span>
275         <a
276           href={`/feeds/u/${this.state.username}.xml?sort=${
277             SortType[this.state.sort]
278           }`}
279           target="_blank"
280         >
281           <svg class="icon mx-2 text-muted small">
282             <use xlinkHref="#icon-rss">#</use>
283           </svg>
284         </a>
285       </div>
286     );
287   }
288
289   overview() {
290     let combined: Array<{ type_: string; data: Comment | Post }> = [];
291     let comments = this.state.comments.map(e => {
292       return { type_: 'comments', data: e };
293     });
294     let posts = this.state.posts.map(e => {
295       return { type_: 'posts', data: e };
296     });
297
298     combined.push(...comments);
299     combined.push(...posts);
300
301     // Sort it
302     if (this.state.sort == SortType.New) {
303       combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
304     } else {
305       combined.sort((a, b) => b.data.score - a.data.score);
306     }
307
308     return (
309       <div>
310         {combined.map(i => (
311           <div>
312             {i.type_ == 'posts' ? (
313               <PostListing
314                 post={i.data as Post}
315                 admins={this.state.admins}
316                 showCommunity
317                 viewOnly
318               />
319             ) : (
320               <CommentNodes
321                 nodes={[{ comment: i.data as Comment }]}
322                 admins={this.state.admins}
323                 noIndent
324               />
325             )}
326           </div>
327         ))}
328       </div>
329     );
330   }
331
332   comments() {
333     return (
334       <div>
335         {this.state.comments.map(comment => (
336           <CommentNodes
337             nodes={[{ comment: comment }]}
338             admins={this.state.admins}
339             noIndent
340           />
341         ))}
342       </div>
343     );
344   }
345
346   posts() {
347     return (
348       <div>
349         {this.state.posts.map(post => (
350           <PostListing
351             post={post}
352             admins={this.state.admins}
353             showCommunity
354             viewOnly
355           />
356         ))}
357       </div>
358     );
359   }
360
361   userInfo() {
362     let user = this.state.user;
363     return (
364       <div>
365         <div class="card border-secondary mb-3">
366           <div class="card-body">
367             <h5>
368               <ul class="list-inline mb-0">
369                 <li className="list-inline-item">{user.name}</li>
370                 {user.banned && (
371                   <li className="list-inline-item badge badge-danger">
372                     <T i18nKey="banned">#</T>
373                   </li>
374                 )}
375               </ul>
376             </h5>
377             <div>
378               {i18n.t('joined')} <MomentTime data={user} />
379             </div>
380             <div class="table-responsive">
381               <table class="table table-bordered table-sm mt-2 mb-0">
382                 <tr>
383                   <td>
384                     <T
385                       i18nKey="number_of_points"
386                       interpolation={{ count: user.post_score }}
387                     >
388                       #
389                     </T>
390                   </td>
391                   <td>
392                     <T
393                       i18nKey="number_of_posts"
394                       interpolation={{ count: user.number_of_posts }}
395                     >
396                       #
397                     </T>
398                   </td>
399                 </tr>
400                 <tr>
401                   <td>
402                     <T
403                       i18nKey="number_of_points"
404                       interpolation={{ count: user.comment_score }}
405                     >
406                       #
407                     </T>
408                   </td>
409                   <td>
410                     <T
411                       i18nKey="number_of_comments"
412                       interpolation={{ count: user.number_of_comments }}
413                     >
414                       #
415                     </T>
416                   </td>
417                 </tr>
418               </table>
419             </div>
420             {this.isCurrentUser && (
421               <button
422                 class="btn btn-block btn-secondary mt-3"
423                 onClick={linkEvent(this, this.handleLogoutClick)}
424               >
425                 <T i18nKey="logout">#</T>
426               </button>
427             )}
428           </div>
429         </div>
430       </div>
431     );
432   }
433
434   userSettings() {
435     return (
436       <div>
437         <div class="card border-secondary mb-3">
438           <div class="card-body">
439             <h5>
440               <T i18nKey="settings">#</T>
441             </h5>
442             <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
443               <div class="form-group">
444                 <label>
445                   <T i18nKey="avatar">#</T>
446                 </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                     <img
453                       height="80"
454                       width="80"
455                       src={
456                         this.state.userSettingsForm.avatar
457                           ? this.state.userSettingsForm.avatar
458                           : 'https://via.placeholder.com/300/000?text=Avatar'
459                       }
460                       class="rounded-circle"
461                     />
462                   </label>
463                   <input
464                     id="file-upload"
465                     type="file"
466                     accept="image/*,video/*"
467                     name="file"
468                     class="d-none"
469                     disabled={!UserService.Instance.user}
470                     onChange={linkEvent(this, this.handleImageUpload)}
471                   />
472                 </form>
473               </div>
474               <div class="form-group">
475                 <label>
476                   <T i18nKey="language">#</T>
477                 </label>
478                 <select
479                   value={this.state.userSettingsForm.lang}
480                   onChange={linkEvent(this, this.handleUserSettingsLangChange)}
481                   class="ml-2 custom-select custom-select-sm w-auto"
482                 >
483                   <option disabled>
484                     <T i18nKey="language">#</T>
485                   </option>
486                   <option value="browser">
487                     <T i18nKey="browser_default">#</T>
488                   </option>
489                   <option disabled>──</option>
490                   {languages.map(lang => (
491                     <option value={lang.code}>{lang.name}</option>
492                   ))}
493                 </select>
494               </div>
495               <div class="form-group">
496                 <label>
497                   <T i18nKey="theme">#</T>
498                 </label>
499                 <select
500                   value={this.state.userSettingsForm.theme}
501                   onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
502                   class="ml-2 custom-select custom-select-sm w-auto"
503                 >
504                   <option disabled>
505                     <T i18nKey="theme">#</T>
506                   </option>
507                   {themes.map(theme => (
508                     <option value={theme}>{theme}</option>
509                   ))}
510                 </select>
511               </div>
512               <form className="form-group">
513                 <label>
514                   <T i18nKey="sort_type" class="mr-2">
515                     #
516                   </T>
517                 </label>
518                 <ListingTypeSelect
519                   type_={this.state.userSettingsForm.default_listing_type}
520                   onChange={this.handleUserSettingsListingTypeChange}
521                 />
522               </form>
523               <form className="form-group">
524                 <label>
525                   <T i18nKey="type" class="mr-2">
526                     #
527                   </T>
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">
536                   <T i18nKey="email">#</T>
537                 </label>
538                 <div class="col-lg-9">
539                   <input
540                     type="email"
541                     class="form-control"
542                     placeholder={i18n.t('optional')}
543                     value={this.state.userSettingsForm.email}
544                     onInput={linkEvent(
545                       this,
546                       this.handleUserSettingsEmailChange
547                     )}
548                     minLength={3}
549                   />
550                 </div>
551               </div>
552               <div class="form-group row">
553                 <label class="col-lg-5 col-form-label">
554                   <T i18nKey="new_password">#</T>
555                 </label>
556                 <div class="col-lg-7">
557                   <input
558                     type="password"
559                     class="form-control"
560                     value={this.state.userSettingsForm.new_password}
561                     onInput={linkEvent(
562                       this,
563                       this.handleUserSettingsNewPasswordChange
564                     )}
565                   />
566                 </div>
567               </div>
568               <div class="form-group row">
569                 <label class="col-lg-5 col-form-label">
570                   <T i18nKey="verify_password">#</T>
571                 </label>
572                 <div class="col-lg-7">
573                   <input
574                     type="password"
575                     class="form-control"
576                     value={this.state.userSettingsForm.new_password_verify}
577                     onInput={linkEvent(
578                       this,
579                       this.handleUserSettingsNewPasswordVerifyChange
580                     )}
581                   />
582                 </div>
583               </div>
584               <div class="form-group row">
585                 <label class="col-lg-5 col-form-label">
586                   <T i18nKey="old_password">#</T>
587                 </label>
588                 <div class="col-lg-7">
589                   <input
590                     type="password"
591                     class="form-control"
592                     value={this.state.userSettingsForm.old_password}
593                     onInput={linkEvent(
594                       this,
595                       this.handleUserSettingsOldPasswordChange
596                     )}
597                   />
598                 </div>
599               </div>
600               {WebSocketService.Instance.site.enable_nsfw && (
601                 <div class="form-group">
602                   <div class="form-check">
603                     <input
604                       class="form-check-input"
605                       type="checkbox"
606                       checked={this.state.userSettingsForm.show_nsfw}
607                       onChange={linkEvent(
608                         this,
609                         this.handleUserSettingsShowNsfwChange
610                       )}
611                     />
612                     <label class="form-check-label">
613                       <T i18nKey="show_nsfw">#</T>
614                     </label>
615                   </div>
616                 </div>
617               )}
618               <div class="form-group">
619                 <div class="form-check">
620                   <input
621                     class="form-check-input"
622                     type="checkbox"
623                     checked={this.state.userSettingsForm.show_avatars}
624                     onChange={linkEvent(
625                       this,
626                       this.handleUserSettingsShowAvatarsChange
627                     )}
628                   />
629                   <label class="form-check-label">
630                     <T i18nKey="show_avatars">#</T>
631                   </label>
632                 </div>
633               </div>
634               <div class="form-group">
635                 <div class="form-check">
636                   <input
637                     class="form-check-input"
638                     type="checkbox"
639                     disabled={!this.state.user.email}
640                     checked={
641                       this.state.userSettingsForm.send_notifications_to_email
642                     }
643                     onChange={linkEvent(
644                       this,
645                       this.handleUserSettingsSendNotificationsToEmailChange
646                     )}
647                   />
648                   <label class="form-check-label">
649                     <T i18nKey="send_notifications_to_email">#</T>
650                   </label>
651                 </div>
652               </div>
653               <div class="form-group">
654                 <button type="submit" class="btn btn-block btn-secondary mr-4">
655                   {this.state.userSettingsLoading ? (
656                     <svg class="icon icon-spinner spin">
657                       <use xlinkHref="#icon-spinner"></use>
658                     </svg>
659                   ) : (
660                     capitalizeFirstLetter(i18n.t('save'))
661                   )}
662                 </button>
663               </div>
664               <hr />
665               <div class="form-group mb-0">
666                 <button
667                   class="btn btn-block btn-danger"
668                   onClick={linkEvent(
669                     this,
670                     this.handleDeleteAccountShowConfirmToggle
671                   )}
672                 >
673                   <T i18nKey="delete_account">#</T>
674                 </button>
675                 {this.state.deleteAccountShowConfirm && (
676                   <>
677                     <div class="my-2 alert alert-danger" role="alert">
678                       <T i18nKey="delete_account_confirm">#</T>
679                     </div>
680                     <input
681                       type="password"
682                       value={this.state.deleteAccountForm.password}
683                       onInput={linkEvent(
684                         this,
685                         this.handleDeleteAccountPasswordChange
686                       )}
687                       class="form-control my-2"
688                     />
689                     <button
690                       class="btn btn-danger mr-4"
691                       disabled={!this.state.deleteAccountForm.password}
692                       onClick={linkEvent(this, this.handleDeleteAccount)}
693                     >
694                       {this.state.deleteAccountLoading ? (
695                         <svg class="icon icon-spinner spin">
696                           <use xlinkHref="#icon-spinner"></use>
697                         </svg>
698                       ) : (
699                         capitalizeFirstLetter(i18n.t('delete'))
700                       )}
701                     </button>
702                     <button
703                       class="btn btn-secondary"
704                       onClick={linkEvent(
705                         this,
706                         this.handleDeleteAccountShowConfirmToggle
707                       )}
708                     >
709                       <T i18nKey="cancel">#</T>
710                     </button>
711                   </>
712                 )}
713               </div>
714             </form>
715           </div>
716         </div>
717       </div>
718     );
719   }
720
721   moderates() {
722     return (
723       <div>
724         {this.state.moderates.length > 0 && (
725           <div class="card border-secondary mb-3">
726             <div class="card-body">
727               <h5>
728                 <T i18nKey="moderates">#</T>
729               </h5>
730               <ul class="list-unstyled mb-0">
731                 {this.state.moderates.map(community => (
732                   <li>
733                     <Link to={`/c/${community.community_name}`}>
734                       {community.community_name}
735                     </Link>
736                   </li>
737                 ))}
738               </ul>
739             </div>
740           </div>
741         )}
742       </div>
743     );
744   }
745
746   follows() {
747     return (
748       <div>
749         {this.state.follows.length > 0 && (
750           <div class="card border-secondary mb-3">
751             <div class="card-body">
752               <h5>
753                 <T i18nKey="subscribed">#</T>
754               </h5>
755               <ul class="list-unstyled mb-0">
756                 {this.state.follows.map(community => (
757                   <li>
758                     <Link to={`/c/${community.community_name}`}>
759                       {community.community_name}
760                     </Link>
761                   </li>
762                 ))}
763               </ul>
764             </div>
765           </div>
766         )}
767       </div>
768     );
769   }
770
771   paginator() {
772     return (
773       <div class="my-2">
774         {this.state.page > 1 && (
775           <button
776             class="btn btn-sm btn-secondary mr-1"
777             onClick={linkEvent(this, this.prevPage)}
778           >
779             <T i18nKey="prev">#</T>
780           </button>
781         )}
782         <button
783           class="btn btn-sm btn-secondary"
784           onClick={linkEvent(this, this.nextPage)}
785         >
786           <T i18nKey="next">#</T>
787         </button>
788       </div>
789     );
790   }
791
792   updateUrl() {
793     let viewStr = View[this.state.view].toLowerCase();
794     let sortStr = SortType[this.state.sort].toLowerCase();
795     this.props.history.push(
796       `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
797     );
798   }
799
800   nextPage(i: User) {
801     i.state.page++;
802     i.setState(i.state);
803     i.updateUrl();
804     i.refetch();
805   }
806
807   prevPage(i: User) {
808     i.state.page--;
809     i.setState(i.state);
810     i.updateUrl();
811     i.refetch();
812   }
813
814   refetch() {
815     let form: GetUserDetailsForm = {
816       user_id: this.state.user_id,
817       username: this.state.username,
818       sort: SortType[this.state.sort],
819       saved_only: this.state.view == View.Saved,
820       page: this.state.page,
821       limit: fetchLimit,
822     };
823     WebSocketService.Instance.getUserDetails(form);
824   }
825
826   handleSortChange(val: SortType) {
827     this.state.sort = val;
828     this.state.page = 1;
829     this.setState(this.state);
830     this.updateUrl();
831     this.refetch();
832   }
833
834   handleViewChange(i: User, event: any) {
835     i.state.view = Number(event.target.value);
836     i.state.page = 1;
837     i.setState(i.state);
838     i.updateUrl();
839     i.refetch();
840   }
841
842   handleUserSettingsShowNsfwChange(i: User, event: any) {
843     i.state.userSettingsForm.show_nsfw = event.target.checked;
844     i.setState(i.state);
845   }
846
847   handleUserSettingsShowAvatarsChange(i: User, event: any) {
848     i.state.userSettingsForm.show_avatars = event.target.checked;
849     UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
850     i.setState(i.state);
851   }
852
853   handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
854     i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
855     i.setState(i.state);
856   }
857
858   handleUserSettingsThemeChange(i: User, event: any) {
859     i.state.userSettingsForm.theme = event.target.value;
860     setTheme(event.target.value);
861     i.setState(i.state);
862   }
863
864   handleUserSettingsLangChange(i: User, event: any) {
865     i.state.userSettingsForm.lang = event.target.value;
866     i18n.changeLanguage(i.state.userSettingsForm.lang);
867     i.setState(i.state);
868   }
869
870   handleUserSettingsSortTypeChange(val: SortType) {
871     this.state.userSettingsForm.default_sort_type = val;
872     this.setState(this.state);
873   }
874
875   handleUserSettingsListingTypeChange(val: ListingType) {
876     this.state.userSettingsForm.default_listing_type = val;
877     this.setState(this.state);
878   }
879
880   handleUserSettingsEmailChange(i: User, event: any) {
881     i.state.userSettingsForm.email = event.target.value;
882     if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
883       i.state.userSettingsForm.email = undefined;
884     }
885     i.setState(i.state);
886   }
887
888   handleUserSettingsNewPasswordChange(i: User, event: any) {
889     i.state.userSettingsForm.new_password = event.target.value;
890     if (i.state.userSettingsForm.new_password == '') {
891       i.state.userSettingsForm.new_password = undefined;
892     }
893     i.setState(i.state);
894   }
895
896   handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
897     i.state.userSettingsForm.new_password_verify = event.target.value;
898     if (i.state.userSettingsForm.new_password_verify == '') {
899       i.state.userSettingsForm.new_password_verify = undefined;
900     }
901     i.setState(i.state);
902   }
903
904   handleUserSettingsOldPasswordChange(i: User, event: any) {
905     i.state.userSettingsForm.old_password = event.target.value;
906     if (i.state.userSettingsForm.old_password == '') {
907       i.state.userSettingsForm.old_password = undefined;
908     }
909     i.setState(i.state);
910   }
911
912   handleImageUpload(i: User, event: any) {
913     event.preventDefault();
914     let file = event.target.files[0];
915     const imageUploadUrl = `/pictshare/api/upload.php`;
916     const formData = new FormData();
917     formData.append('file', file);
918
919     i.state.avatarLoading = true;
920     i.setState(i.state);
921
922     fetch(imageUploadUrl, {
923       method: 'POST',
924       body: formData,
925     })
926       .then(res => res.json())
927       .then(res => {
928         let url = `${window.location.origin}/pictshare/${res.url}`;
929         if (res.filetype == 'mp4') {
930           url += '/raw';
931         }
932         i.state.userSettingsForm.avatar = url;
933         console.log(url);
934         i.state.avatarLoading = false;
935         i.setState(i.state);
936       })
937       .catch(error => {
938         i.state.avatarLoading = false;
939         i.setState(i.state);
940         alert(error);
941       });
942   }
943
944   handleUserSettingsSubmit(i: User, event: any) {
945     event.preventDefault();
946     i.state.userSettingsLoading = true;
947     i.setState(i.state);
948
949     WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
950   }
951
952   handleDeleteAccountShowConfirmToggle(i: User, event: any) {
953     event.preventDefault();
954     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
955     i.setState(i.state);
956   }
957
958   handleDeleteAccountPasswordChange(i: User, event: any) {
959     i.state.deleteAccountForm.password = event.target.value;
960     i.setState(i.state);
961   }
962
963   handleLogoutClick(i: User) {
964     UserService.Instance.logout();
965     i.context.router.history.push('/');
966   }
967
968   handleDeleteAccount(i: User, event: any) {
969     event.preventDefault();
970     i.state.deleteAccountLoading = true;
971     i.setState(i.state);
972
973     WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
974   }
975
976   parseMessage(msg: any) {
977     console.log(msg);
978     let op: UserOperation = msgOp(msg);
979     if (msg.error) {
980       alert(i18n.t(msg.error));
981       this.state.deleteAccountLoading = false;
982       this.state.avatarLoading = false;
983       this.state.userSettingsLoading = false;
984       if (msg.error == 'couldnt_find_that_username_or_email') {
985         this.context.router.history.push('/');
986       }
987       this.setState(this.state);
988       return;
989     } else if (op == UserOperation.GetUserDetails) {
990       let res: UserDetailsResponse = msg;
991       this.state.user = res.user;
992       this.state.comments = res.comments;
993       this.state.follows = res.follows;
994       this.state.moderates = res.moderates;
995       this.state.posts = res.posts;
996       this.state.admins = res.admins;
997       this.state.loading = false;
998       if (this.isCurrentUser) {
999         this.state.userSettingsForm.show_nsfw =
1000           UserService.Instance.user.show_nsfw;
1001         this.state.userSettingsForm.theme = UserService.Instance.user.theme
1002           ? UserService.Instance.user.theme
1003           : 'darkly';
1004         this.state.userSettingsForm.default_sort_type =
1005           UserService.Instance.user.default_sort_type;
1006         this.state.userSettingsForm.default_listing_type =
1007           UserService.Instance.user.default_listing_type;
1008         this.state.userSettingsForm.lang = UserService.Instance.user.lang;
1009         this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
1010         this.state.userSettingsForm.email = this.state.user.email;
1011         this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
1012         this.state.userSettingsForm.show_avatars =
1013           UserService.Instance.user.show_avatars;
1014       }
1015       document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
1016       window.scrollTo(0, 0);
1017       this.setState(this.state);
1018     } else if (op == UserOperation.EditComment) {
1019       let res: CommentResponse = msg;
1020
1021       let found = this.state.comments.find(c => c.id == res.comment.id);
1022       found.content = res.comment.content;
1023       found.updated = res.comment.updated;
1024       found.removed = res.comment.removed;
1025       found.deleted = res.comment.deleted;
1026       found.upvotes = res.comment.upvotes;
1027       found.downvotes = res.comment.downvotes;
1028       found.score = res.comment.score;
1029
1030       this.setState(this.state);
1031     } else if (op == UserOperation.CreateComment) {
1032       // let res: CommentResponse = msg;
1033       alert(i18n.t('reply_sent'));
1034       // this.state.comments.unshift(res.comment); // TODO do this right
1035       // this.setState(this.state);
1036     } else if (op == UserOperation.SaveComment) {
1037       let res: CommentResponse = msg;
1038       let found = this.state.comments.find(c => c.id == res.comment.id);
1039       found.saved = res.comment.saved;
1040       this.setState(this.state);
1041     } else if (op == UserOperation.CreateCommentLike) {
1042       let res: CommentResponse = msg;
1043       let found: Comment = this.state.comments.find(
1044         c => c.id === res.comment.id
1045       );
1046       found.score = res.comment.score;
1047       found.upvotes = res.comment.upvotes;
1048       found.downvotes = res.comment.downvotes;
1049       if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
1050       this.setState(this.state);
1051     } else if (op == UserOperation.BanUser) {
1052       let res: BanUserResponse = msg;
1053       this.state.comments
1054         .filter(c => c.creator_id == res.user.id)
1055         .forEach(c => (c.banned = res.banned));
1056       this.state.posts
1057         .filter(c => c.creator_id == res.user.id)
1058         .forEach(c => (c.banned = res.banned));
1059       this.setState(this.state);
1060     } else if (op == UserOperation.AddAdmin) {
1061       let res: AddAdminResponse = msg;
1062       this.state.admins = res.admins;
1063       this.setState(this.state);
1064     } else if (op == UserOperation.SaveUserSettings) {
1065       this.state = this.emptyState;
1066       this.state.userSettingsLoading = false;
1067       this.setState(this.state);
1068       let res: LoginResponse = msg;
1069       UserService.Instance.login(res);
1070     } else if (op == UserOperation.DeleteAccount) {
1071       this.state.deleteAccountLoading = false;
1072       this.state.deleteAccountShowConfirm = false;
1073       this.setState(this.state);
1074       this.context.router.history.push('/');
1075     }
1076   }
1077 }