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