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