]> Untitled Git - lemmy.git/blob - ui/src/components/user.tsx
Merge branch 'master' into federation_merge_from_master_1
[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} />
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                     onInput={linkEvent(
618                       this,
619                       this.handleUserSettingsNewPasswordChange
620                     )}
621                   />
622                 </div>
623               </div>
624               <div class="form-group row">
625                 <label
626                   class="col-lg-5 col-form-label"
627                   htmlFor="user-verify-password"
628                 >
629                   {i18n.t('verify_password')}
630                 </label>
631                 <div class="col-lg-7">
632                   <input
633                     type="password"
634                     id="user-verify-password"
635                     class="form-control"
636                     value={this.state.userSettingsForm.new_password_verify}
637                     onInput={linkEvent(
638                       this,
639                       this.handleUserSettingsNewPasswordVerifyChange
640                     )}
641                   />
642                 </div>
643               </div>
644               <div class="form-group row">
645                 <label
646                   class="col-lg-5 col-form-label"
647                   htmlFor="user-old-password"
648                 >
649                   {i18n.t('old_password')}
650                 </label>
651                 <div class="col-lg-7">
652                   <input
653                     type="password"
654                     id="user-old-password"
655                     class="form-control"
656                     value={this.state.userSettingsForm.old_password}
657                     onInput={linkEvent(
658                       this,
659                       this.handleUserSettingsOldPasswordChange
660                     )}
661                   />
662                 </div>
663               </div>
664               {WebSocketService.Instance.site.enable_nsfw && (
665                 <div class="form-group">
666                   <div class="form-check">
667                     <input
668                       class="form-check-input"
669                       id="user-show-nsfw"
670                       type="checkbox"
671                       checked={this.state.userSettingsForm.show_nsfw}
672                       onChange={linkEvent(
673                         this,
674                         this.handleUserSettingsShowNsfwChange
675                       )}
676                     />
677                     <label class="form-check-label" htmlFor="user-show-nsfw">
678                       {i18n.t('show_nsfw')}
679                     </label>
680                   </div>
681                 </div>
682               )}
683               <div class="form-group">
684                 <div class="form-check">
685                   <input
686                     class="form-check-input"
687                     id="user-show-avatars"
688                     type="checkbox"
689                     checked={this.state.userSettingsForm.show_avatars}
690                     onChange={linkEvent(
691                       this,
692                       this.handleUserSettingsShowAvatarsChange
693                     )}
694                   />
695                   <label class="form-check-label" htmlFor="user-show-avatars">
696                     {i18n.t('show_avatars')}
697                   </label>
698                 </div>
699               </div>
700               <div class="form-group">
701                 <div class="form-check">
702                   <input
703                     class="form-check-input"
704                     id="user-send-notifications-to-email"
705                     type="checkbox"
706                     disabled={!this.state.user.email}
707                     checked={
708                       this.state.userSettingsForm.send_notifications_to_email
709                     }
710                     onChange={linkEvent(
711                       this,
712                       this.handleUserSettingsSendNotificationsToEmailChange
713                     )}
714                   />
715                   <label
716                     class="form-check-label"
717                     htmlFor="user-send-notifications-to-email"
718                   >
719                     {i18n.t('send_notifications_to_email')}
720                   </label>
721                 </div>
722               </div>
723               <div class="form-group">
724                 <button type="submit" class="btn btn-block btn-secondary mr-4">
725                   {this.state.userSettingsLoading ? (
726                     <svg class="icon icon-spinner spin">
727                       <use xlinkHref="#icon-spinner"></use>
728                     </svg>
729                   ) : (
730                     capitalizeFirstLetter(i18n.t('save'))
731                   )}
732                 </button>
733               </div>
734               <hr />
735               <div class="form-group mb-0">
736                 <button
737                   class="btn btn-block btn-danger"
738                   onClick={linkEvent(
739                     this,
740                     this.handleDeleteAccountShowConfirmToggle
741                   )}
742                 >
743                   {i18n.t('delete_account')}
744                 </button>
745                 {this.state.deleteAccountShowConfirm && (
746                   <>
747                     <div class="my-2 alert alert-danger" role="alert">
748                       {i18n.t('delete_account_confirm')}
749                     </div>
750                     <input
751                       type="password"
752                       value={this.state.deleteAccountForm.password}
753                       onInput={linkEvent(
754                         this,
755                         this.handleDeleteAccountPasswordChange
756                       )}
757                       class="form-control my-2"
758                     />
759                     <button
760                       class="btn btn-danger mr-4"
761                       disabled={!this.state.deleteAccountForm.password}
762                       onClick={linkEvent(this, this.handleDeleteAccount)}
763                     >
764                       {this.state.deleteAccountLoading ? (
765                         <svg class="icon icon-spinner spin">
766                           <use xlinkHref="#icon-spinner"></use>
767                         </svg>
768                       ) : (
769                         capitalizeFirstLetter(i18n.t('delete'))
770                       )}
771                     </button>
772                     <button
773                       class="btn btn-secondary"
774                       onClick={linkEvent(
775                         this,
776                         this.handleDeleteAccountShowConfirmToggle
777                       )}
778                     >
779                       {i18n.t('cancel')}
780                     </button>
781                   </>
782                 )}
783               </div>
784             </form>
785           </div>
786         </div>
787       </div>
788     );
789   }
790
791   moderates() {
792     return (
793       <div>
794         {this.state.moderates.length > 0 && (
795           <div class="card border-secondary mb-3">
796             <div class="card-body">
797               <h5>{i18n.t('moderates')}</h5>
798               <ul class="list-unstyled mb-0">
799                 {this.state.moderates.map(community => (
800                   <li>
801                     <Link to={`/c/${community.community_name}`}>
802                       {community.community_name}
803                     </Link>
804                   </li>
805                 ))}
806               </ul>
807             </div>
808           </div>
809         )}
810       </div>
811     );
812   }
813
814   follows() {
815     return (
816       <div>
817         {this.state.follows.length > 0 && (
818           <div class="card border-secondary mb-3">
819             <div class="card-body">
820               <h5>{i18n.t('subscribed')}</h5>
821               <ul class="list-unstyled mb-0">
822                 {this.state.follows.map(community => (
823                   <li>
824                     <Link to={`/c/${community.community_name}`}>
825                       {community.community_name}
826                     </Link>
827                   </li>
828                 ))}
829               </ul>
830             </div>
831           </div>
832         )}
833       </div>
834     );
835   }
836
837   paginator() {
838     return (
839       <div class="my-2">
840         {this.state.page > 1 && (
841           <button
842             class="btn btn-sm btn-secondary mr-1"
843             onClick={linkEvent(this, this.prevPage)}
844           >
845             {i18n.t('prev')}
846           </button>
847         )}
848         <button
849           class="btn btn-sm btn-secondary"
850           onClick={linkEvent(this, this.nextPage)}
851         >
852           {i18n.t('next')}
853         </button>
854       </div>
855     );
856   }
857
858   updateUrl() {
859     let viewStr = View[this.state.view].toLowerCase();
860     let sortStr = SortType[this.state.sort].toLowerCase();
861     this.props.history.push(
862       `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
863     );
864   }
865
866   nextPage(i: User) {
867     i.state.page++;
868     i.setState(i.state);
869     i.updateUrl();
870     i.refetch();
871   }
872
873   prevPage(i: User) {
874     i.state.page--;
875     i.setState(i.state);
876     i.updateUrl();
877     i.refetch();
878   }
879
880   refetch() {
881     let form: GetUserDetailsForm = {
882       user_id: this.state.user_id,
883       username: this.state.username,
884       sort: SortType[this.state.sort],
885       saved_only: this.state.view == View.Saved,
886       page: this.state.page,
887       limit: fetchLimit,
888     };
889     WebSocketService.Instance.getUserDetails(form);
890   }
891
892   handleSortChange(val: SortType) {
893     this.state.sort = val;
894     this.state.page = 1;
895     this.setState(this.state);
896     this.updateUrl();
897     this.refetch();
898   }
899
900   handleViewChange(i: User, event: any) {
901     i.state.view = Number(event.target.value);
902     i.state.page = 1;
903     i.setState(i.state);
904     i.updateUrl();
905     i.refetch();
906   }
907
908   handleUserSettingsShowNsfwChange(i: User, event: any) {
909     i.state.userSettingsForm.show_nsfw = event.target.checked;
910     i.setState(i.state);
911   }
912
913   handleUserSettingsShowAvatarsChange(i: User, event: any) {
914     i.state.userSettingsForm.show_avatars = event.target.checked;
915     UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
916     i.setState(i.state);
917   }
918
919   handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
920     i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
921     i.setState(i.state);
922   }
923
924   handleUserSettingsThemeChange(i: User, event: any) {
925     i.state.userSettingsForm.theme = event.target.value;
926     setTheme(event.target.value);
927     i.setState(i.state);
928   }
929
930   handleUserSettingsLangChange(i: User, event: any) {
931     i.state.userSettingsForm.lang = event.target.value;
932     i18n.changeLanguage(i.state.userSettingsForm.lang);
933     i.setState(i.state);
934   }
935
936   handleUserSettingsSortTypeChange(val: SortType) {
937     this.state.userSettingsForm.default_sort_type = val;
938     this.setState(this.state);
939   }
940
941   handleUserSettingsListingTypeChange(val: ListingType) {
942     this.state.userSettingsForm.default_listing_type = val;
943     this.setState(this.state);
944   }
945
946   handleUserSettingsEmailChange(i: User, event: any) {
947     i.state.userSettingsForm.email = event.target.value;
948     if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
949       i.state.userSettingsForm.email = undefined;
950     }
951     i.setState(i.state);
952   }
953
954   handleUserSettingsMatrixUserIdChange(i: User, event: any) {
955     i.state.userSettingsForm.matrix_user_id = event.target.value;
956     if (
957       i.state.userSettingsForm.matrix_user_id == '' &&
958       !i.state.user.matrix_user_id
959     ) {
960       i.state.userSettingsForm.matrix_user_id = undefined;
961     }
962     i.setState(i.state);
963   }
964
965   handleUserSettingsNewPasswordChange(i: User, event: any) {
966     i.state.userSettingsForm.new_password = event.target.value;
967     if (i.state.userSettingsForm.new_password == '') {
968       i.state.userSettingsForm.new_password = undefined;
969     }
970     i.setState(i.state);
971   }
972
973   handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
974     i.state.userSettingsForm.new_password_verify = event.target.value;
975     if (i.state.userSettingsForm.new_password_verify == '') {
976       i.state.userSettingsForm.new_password_verify = undefined;
977     }
978     i.setState(i.state);
979   }
980
981   handleUserSettingsOldPasswordChange(i: User, event: any) {
982     i.state.userSettingsForm.old_password = event.target.value;
983     if (i.state.userSettingsForm.old_password == '') {
984       i.state.userSettingsForm.old_password = undefined;
985     }
986     i.setState(i.state);
987   }
988
989   handleImageUpload(i: User, event: any) {
990     event.preventDefault();
991     let file = event.target.files[0];
992     const imageUploadUrl = `/pictshare/api/upload.php`;
993     const formData = new FormData();
994     formData.append('file', file);
995
996     i.state.avatarLoading = true;
997     i.setState(i.state);
998
999     fetch(imageUploadUrl, {
1000       method: 'POST',
1001       body: formData,
1002     })
1003       .then(res => res.json())
1004       .then(res => {
1005         let url = `${window.location.origin}/pictshare/${res.url}`;
1006         if (res.filetype == 'mp4') {
1007           url += '/raw';
1008         }
1009         i.state.userSettingsForm.avatar = url;
1010         console.log(url);
1011         i.state.avatarLoading = false;
1012         i.setState(i.state);
1013       })
1014       .catch(error => {
1015         i.state.avatarLoading = false;
1016         i.setState(i.state);
1017         toast(error, 'danger');
1018       });
1019   }
1020
1021   handleUserSettingsSubmit(i: User, event: any) {
1022     event.preventDefault();
1023     i.state.userSettingsLoading = true;
1024     i.setState(i.state);
1025
1026     WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
1027   }
1028
1029   handleDeleteAccountShowConfirmToggle(i: User, event: any) {
1030     event.preventDefault();
1031     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
1032     i.setState(i.state);
1033   }
1034
1035   handleDeleteAccountPasswordChange(i: User, event: any) {
1036     i.state.deleteAccountForm.password = event.target.value;
1037     i.setState(i.state);
1038   }
1039
1040   handleLogoutClick(i: User) {
1041     UserService.Instance.logout();
1042     i.context.router.history.push('/');
1043   }
1044
1045   handleDeleteAccount(i: User, event: any) {
1046     event.preventDefault();
1047     i.state.deleteAccountLoading = true;
1048     i.setState(i.state);
1049
1050     WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
1051   }
1052
1053   parseMessage(msg: WebSocketJsonResponse) {
1054     console.log(msg);
1055     let res = wsJsonToRes(msg);
1056     if (msg.error) {
1057       toast(i18n.t(msg.error), 'danger');
1058       this.state.deleteAccountLoading = false;
1059       this.state.avatarLoading = false;
1060       this.state.userSettingsLoading = false;
1061       if (msg.error == 'couldnt_find_that_username_or_email') {
1062         this.context.router.history.push('/');
1063       }
1064       this.setState(this.state);
1065       return;
1066     } else if (msg.reconnect) {
1067       this.refetch();
1068     } else if (res.op == UserOperation.GetUserDetails) {
1069       let data = res.data as UserDetailsResponse;
1070       this.state.user = data.user;
1071       this.state.comments = data.comments;
1072       this.state.follows = data.follows;
1073       this.state.moderates = data.moderates;
1074       this.state.posts = data.posts;
1075       this.state.admins = data.admins;
1076       this.state.loading = false;
1077       if (this.isCurrentUser) {
1078         this.state.userSettingsForm.show_nsfw =
1079           UserService.Instance.user.show_nsfw;
1080         this.state.userSettingsForm.theme = UserService.Instance.user.theme
1081           ? UserService.Instance.user.theme
1082           : 'darkly';
1083         this.state.userSettingsForm.default_sort_type =
1084           UserService.Instance.user.default_sort_type;
1085         this.state.userSettingsForm.default_listing_type =
1086           UserService.Instance.user.default_listing_type;
1087         this.state.userSettingsForm.lang = UserService.Instance.user.lang;
1088         this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
1089         this.state.userSettingsForm.email = this.state.user.email;
1090         this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
1091         this.state.userSettingsForm.show_avatars =
1092           UserService.Instance.user.show_avatars;
1093         this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
1094       }
1095       document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
1096       window.scrollTo(0, 0);
1097       this.setState(this.state);
1098       setupTippy();
1099     } else if (res.op == UserOperation.EditComment) {
1100       let data = res.data as CommentResponse;
1101       editCommentRes(data, this.state.comments);
1102       this.setState(this.state);
1103     } else if (res.op == UserOperation.CreateComment) {
1104       let data = res.data as CommentResponse;
1105       if (
1106         UserService.Instance.user &&
1107         data.comment.creator_id == UserService.Instance.user.id
1108       ) {
1109         toast(i18n.t('reply_sent'));
1110       }
1111     } else if (res.op == UserOperation.SaveComment) {
1112       let data = res.data as CommentResponse;
1113       saveCommentRes(data, this.state.comments);
1114       this.setState(this.state);
1115     } else if (res.op == UserOperation.CreateCommentLike) {
1116       let data = res.data as CommentResponse;
1117       createCommentLikeRes(data, this.state.comments);
1118       this.setState(this.state);
1119     } else if (res.op == UserOperation.CreatePostLike) {
1120       let data = res.data as PostResponse;
1121       createPostLikeFindRes(data, this.state.posts);
1122       this.setState(this.state);
1123     } else if (res.op == UserOperation.BanUser) {
1124       let data = res.data as BanUserResponse;
1125       this.state.comments
1126         .filter(c => c.creator_id == data.user.id)
1127         .forEach(c => (c.banned = data.banned));
1128       this.state.posts
1129         .filter(c => c.creator_id == data.user.id)
1130         .forEach(c => (c.banned = data.banned));
1131       this.setState(this.state);
1132     } else if (res.op == UserOperation.AddAdmin) {
1133       let data = res.data as AddAdminResponse;
1134       this.state.admins = data.admins;
1135       this.setState(this.state);
1136     } else if (res.op == UserOperation.SaveUserSettings) {
1137       let data = res.data as LoginResponse;
1138       this.state = this.emptyState;
1139       this.state.userSettingsLoading = false;
1140       this.setState(this.state);
1141       UserService.Instance.login(data);
1142     } else if (res.op == UserOperation.DeleteAccount) {
1143       this.state.deleteAccountLoading = false;
1144       this.state.deleteAccountShowConfirm = false;
1145       this.setState(this.state);
1146       this.context.router.history.push('/');
1147     }
1148   }
1149 }