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