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