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