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