]> Untitled Git - lemmy.git/blob - ui/src/components/user.tsx
Merge branch 'ban_user_bug'
[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           rel="noopener"
321           title="RSS"
322         >
323           <svg class="icon mx-2 text-muted small">
324             <use xlinkHref="#icon-rss">#</use>
325           </svg>
326         </a>
327       </div>
328     );
329   }
330
331   overview() {
332     let combined: Array<{ type_: string; data: Comment | Post }> = [];
333     let comments = this.state.comments.map(e => {
334       return { type_: 'comments', data: e };
335     });
336     let posts = this.state.posts.map(e => {
337       return { type_: 'posts', data: e };
338     });
339
340     combined.push(...comments);
341     combined.push(...posts);
342
343     // Sort it
344     if (this.state.sort == SortType.New) {
345       combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
346     } else {
347       combined.sort((a, b) => b.data.score - a.data.score);
348     }
349
350     return (
351       <div>
352         {combined.map(i => (
353           <div>
354             {i.type_ == 'posts' ? (
355               <PostListing
356                 post={i.data as Post}
357                 admins={this.state.admins}
358                 showCommunity
359               />
360             ) : (
361               <CommentNodes
362                 nodes={[{ comment: i.data as Comment }]}
363                 admins={this.state.admins}
364                 noIndent
365                 showContext
366               />
367             )}
368           </div>
369         ))}
370       </div>
371     );
372   }
373
374   comments() {
375     return (
376       <div>
377         <CommentNodes
378           nodes={commentsToFlatNodes(this.state.comments)}
379           admins={this.state.admins}
380           noIndent
381           showContext
382         />
383       </div>
384     );
385   }
386
387   posts() {
388     return (
389       <div>
390         {this.state.posts.map(post => (
391           <PostListing post={post} admins={this.state.admins} showCommunity />
392         ))}
393       </div>
394     );
395   }
396
397   userInfo() {
398     let user = this.state.user;
399     return (
400       <div>
401         <div class="card border-secondary mb-3">
402           <div class="card-body">
403             <h5>
404               <ul class="list-inline mb-0">
405                 <li className="list-inline-item">
406                   <UserListing user={user} realLink />
407                 </li>
408                 {user.banned && (
409                   <li className="list-inline-item badge badge-danger">
410                     {i18n.t('banned')}
411                   </li>
412                 )}
413               </ul>
414             </h5>
415             <div>
416               {i18n.t('joined')} <MomentTime data={user} showAgo />
417             </div>
418             <div class="table-responsive mt-1">
419               <table class="table table-bordered table-sm mt-2 mb-0">
420                 {/*
421                 <tr>
422                   <td class="text-center" colSpan={2}>
423                     {i18n.t('number_of_points', {
424                       count: user.post_score + user.comment_score,
425                     })}
426                   </td>
427                 </tr>
428                 */}
429                 <tr>
430                   {/* 
431                   <td>
432                     {i18n.t('number_of_points', { count: user.post_score })}
433                   </td>
434                   */}
435                   <td>
436                     {i18n.t('number_of_posts', { count: user.number_of_posts })}
437                   </td>
438                   {/* 
439                 </tr>
440                 <tr>
441                   <td>
442                     {i18n.t('number_of_points', { count: user.comment_score })}
443                   </td>
444                   */}
445                   <td>
446                     {i18n.t('number_of_comments', {
447                       count: user.number_of_comments,
448                     })}
449                   </td>
450                 </tr>
451               </table>
452             </div>
453             {this.isCurrentUser ? (
454               <button
455                 class="btn btn-block btn-secondary mt-3"
456                 onClick={linkEvent(this, this.handleLogoutClick)}
457               >
458                 {i18n.t('logout')}
459               </button>
460             ) : (
461               <>
462                 <a
463                   className={`btn btn-block btn-secondary mt-3 ${
464                     !this.state.user.matrix_user_id && 'disabled'
465                   }`}
466                   target="_blank"
467                   rel="noopener"
468                   href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
469                 >
470                   {i18n.t('send_secure_message')}
471                 </a>
472                 <Link
473                   class="btn btn-block btn-secondary mt-3"
474                   to={`/create_private_message?recipient_id=${this.state.user.id}`}
475                 >
476                   {i18n.t('send_message')}
477                 </Link>
478               </>
479             )}
480           </div>
481         </div>
482       </div>
483     );
484   }
485
486   userSettings() {
487     return (
488       <div>
489         <div class="card border-secondary mb-3">
490           <div class="card-body">
491             <h5>{i18n.t('settings')}</h5>
492             <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
493               <div class="form-group">
494                 <label>{i18n.t('avatar')}</label>
495                 <form class="d-inline">
496                   <label
497                     htmlFor="file-upload"
498                     class="pointer ml-4 text-muted small font-weight-bold"
499                   >
500                     {!this.state.userSettingsForm.avatar ? (
501                       <span class="btn btn-sm btn-secondary">
502                         {i18n.t('upload_avatar')}
503                       </span>
504                     ) : (
505                       <img
506                         height="80"
507                         width="80"
508                         src={this.state.userSettingsForm.avatar}
509                         class="rounded-circle"
510                       />
511                     )}
512                   </label>
513                   <input
514                     id="file-upload"
515                     type="file"
516                     accept="image/*,video/*"
517                     name="file"
518                     class="d-none"
519                     disabled={!UserService.Instance.user}
520                     onChange={linkEvent(this, this.handleImageUpload)}
521                   />
522                 </form>
523               </div>
524               <div class="form-group">
525                 <label>{i18n.t('language')}</label>
526                 <select
527                   value={this.state.userSettingsForm.lang}
528                   onChange={linkEvent(this, this.handleUserSettingsLangChange)}
529                   class="ml-2 custom-select custom-select-sm w-auto"
530                 >
531                   <option disabled>{i18n.t('language')}</option>
532                   <option value="browser">{i18n.t('browser_default')}</option>
533                   <option disabled>──</option>
534                   {languages.map(lang => (
535                     <option value={lang.code}>{lang.name}</option>
536                   ))}
537                 </select>
538               </div>
539               <div class="form-group">
540                 <label>{i18n.t('theme')}</label>
541                 <select
542                   value={this.state.userSettingsForm.theme}
543                   onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
544                   class="ml-2 custom-select custom-select-sm w-auto"
545                 >
546                   <option disabled>{i18n.t('theme')}</option>
547                   {themes.map(theme => (
548                     <option value={theme}>{theme}</option>
549                   ))}
550                 </select>
551               </div>
552               <form className="form-group">
553                 <label>
554                   <div class="mr-2">{i18n.t('sort_type')}</div>
555                 </label>
556                 <ListingTypeSelect
557                   type_={this.state.userSettingsForm.default_listing_type}
558                   onChange={this.handleUserSettingsListingTypeChange}
559                 />
560               </form>
561               <form className="form-group">
562                 <label>
563                   <div class="mr-2">{i18n.t('type')}</div>
564                 </label>
565                 <SortSelect
566                   sort={this.state.userSettingsForm.default_sort_type}
567                   onChange={this.handleUserSettingsSortTypeChange}
568                 />
569               </form>
570               <div class="form-group row">
571                 <label class="col-lg-3 col-form-label" htmlFor="user-email">
572                   {i18n.t('email')}
573                 </label>
574                 <div class="col-lg-9">
575                   <input
576                     type="email"
577                     id="user-email"
578                     class="form-control"
579                     placeholder={i18n.t('optional')}
580                     value={this.state.userSettingsForm.email}
581                     onInput={linkEvent(
582                       this,
583                       this.handleUserSettingsEmailChange
584                     )}
585                     minLength={3}
586                   />
587                 </div>
588               </div>
589               <div class="form-group row">
590                 <label class="col-lg-5 col-form-label">
591                   <a
592                     href="https://about.riot.im/"
593                     target="_blank"
594                     rel="noopener"
595                   >
596                     {i18n.t('matrix_user_id')}
597                   </a>
598                 </label>
599                 <div class="col-lg-7">
600                   <input
601                     type="text"
602                     class="form-control"
603                     placeholder="@user:example.com"
604                     value={this.state.userSettingsForm.matrix_user_id}
605                     onInput={linkEvent(
606                       this,
607                       this.handleUserSettingsMatrixUserIdChange
608                     )}
609                     minLength={3}
610                   />
611                 </div>
612               </div>
613               <div class="form-group row">
614                 <label class="col-lg-5 col-form-label" htmlFor="user-password">
615                   {i18n.t('new_password')}
616                 </label>
617                 <div class="col-lg-7">
618                   <input
619                     type="password"
620                     id="user-password"
621                     class="form-control"
622                     value={this.state.userSettingsForm.new_password}
623                     autoComplete="new-password"
624                     onInput={linkEvent(
625                       this,
626                       this.handleUserSettingsNewPasswordChange
627                     )}
628                   />
629                 </div>
630               </div>
631               <div class="form-group row">
632                 <label
633                   class="col-lg-5 col-form-label"
634                   htmlFor="user-verify-password"
635                 >
636                   {i18n.t('verify_password')}
637                 </label>
638                 <div class="col-lg-7">
639                   <input
640                     type="password"
641                     id="user-verify-password"
642                     class="form-control"
643                     value={this.state.userSettingsForm.new_password_verify}
644                     autoComplete="new-password"
645                     onInput={linkEvent(
646                       this,
647                       this.handleUserSettingsNewPasswordVerifyChange
648                     )}
649                   />
650                 </div>
651               </div>
652               <div class="form-group row">
653                 <label
654                   class="col-lg-5 col-form-label"
655                   htmlFor="user-old-password"
656                 >
657                   {i18n.t('old_password')}
658                 </label>
659                 <div class="col-lg-7">
660                   <input
661                     type="password"
662                     id="user-old-password"
663                     class="form-control"
664                     value={this.state.userSettingsForm.old_password}
665                     autoComplete="new-password"
666                     onInput={linkEvent(
667                       this,
668                       this.handleUserSettingsOldPasswordChange
669                     )}
670                   />
671                 </div>
672               </div>
673               {WebSocketService.Instance.site.enable_nsfw && (
674                 <div class="form-group">
675                   <div class="form-check">
676                     <input
677                       class="form-check-input"
678                       id="user-show-nsfw"
679                       type="checkbox"
680                       checked={this.state.userSettingsForm.show_nsfw}
681                       onChange={linkEvent(
682                         this,
683                         this.handleUserSettingsShowNsfwChange
684                       )}
685                     />
686                     <label class="form-check-label" htmlFor="user-show-nsfw">
687                       {i18n.t('show_nsfw')}
688                     </label>
689                   </div>
690                 </div>
691               )}
692               <div class="form-group">
693                 <div class="form-check">
694                   <input
695                     class="form-check-input"
696                     id="user-show-avatars"
697                     type="checkbox"
698                     checked={this.state.userSettingsForm.show_avatars}
699                     onChange={linkEvent(
700                       this,
701                       this.handleUserSettingsShowAvatarsChange
702                     )}
703                   />
704                   <label class="form-check-label" htmlFor="user-show-avatars">
705                     {i18n.t('show_avatars')}
706                   </label>
707                 </div>
708               </div>
709               <div class="form-group">
710                 <div class="form-check">
711                   <input
712                     class="form-check-input"
713                     id="user-send-notifications-to-email"
714                     type="checkbox"
715                     disabled={!this.state.user.email}
716                     checked={
717                       this.state.userSettingsForm.send_notifications_to_email
718                     }
719                     onChange={linkEvent(
720                       this,
721                       this.handleUserSettingsSendNotificationsToEmailChange
722                     )}
723                   />
724                   <label
725                     class="form-check-label"
726                     htmlFor="user-send-notifications-to-email"
727                   >
728                     {i18n.t('send_notifications_to_email')}
729                   </label>
730                 </div>
731               </div>
732               <div class="form-group">
733                 <button type="submit" class="btn btn-block btn-secondary mr-4">
734                   {this.state.userSettingsLoading ? (
735                     <svg class="icon icon-spinner spin">
736                       <use xlinkHref="#icon-spinner"></use>
737                     </svg>
738                   ) : (
739                     capitalizeFirstLetter(i18n.t('save'))
740                   )}
741                 </button>
742               </div>
743               <hr />
744               <div class="form-group mb-0">
745                 <button
746                   class="btn btn-block btn-danger"
747                   onClick={linkEvent(
748                     this,
749                     this.handleDeleteAccountShowConfirmToggle
750                   )}
751                 >
752                   {i18n.t('delete_account')}
753                 </button>
754                 {this.state.deleteAccountShowConfirm && (
755                   <>
756                     <div class="my-2 alert alert-danger" role="alert">
757                       {i18n.t('delete_account_confirm')}
758                     </div>
759                     <input
760                       type="password"
761                       value={this.state.deleteAccountForm.password}
762                       autoComplete="new-password"
763                       onInput={linkEvent(
764                         this,
765                         this.handleDeleteAccountPasswordChange
766                       )}
767                       class="form-control my-2"
768                     />
769                     <button
770                       class="btn btn-danger mr-4"
771                       disabled={!this.state.deleteAccountForm.password}
772                       onClick={linkEvent(this, this.handleDeleteAccount)}
773                     >
774                       {this.state.deleteAccountLoading ? (
775                         <svg class="icon icon-spinner spin">
776                           <use xlinkHref="#icon-spinner"></use>
777                         </svg>
778                       ) : (
779                         capitalizeFirstLetter(i18n.t('delete'))
780                       )}
781                     </button>
782                     <button
783                       class="btn btn-secondary"
784                       onClick={linkEvent(
785                         this,
786                         this.handleDeleteAccountShowConfirmToggle
787                       )}
788                     >
789                       {i18n.t('cancel')}
790                     </button>
791                   </>
792                 )}
793               </div>
794             </form>
795           </div>
796         </div>
797       </div>
798     );
799   }
800
801   moderates() {
802     return (
803       <div>
804         {this.state.moderates.length > 0 && (
805           <div class="card border-secondary mb-3">
806             <div class="card-body">
807               <h5>{i18n.t('moderates')}</h5>
808               <ul class="list-unstyled mb-0">
809                 {this.state.moderates.map(community => (
810                   <li>
811                     <Link to={`/c/${community.community_name}`}>
812                       {community.community_name}
813                     </Link>
814                   </li>
815                 ))}
816               </ul>
817             </div>
818           </div>
819         )}
820       </div>
821     );
822   }
823
824   follows() {
825     return (
826       <div>
827         {this.state.follows.length > 0 && (
828           <div class="card border-secondary mb-3">
829             <div class="card-body">
830               <h5>{i18n.t('subscribed')}</h5>
831               <ul class="list-unstyled mb-0">
832                 {this.state.follows.map(community => (
833                   <li>
834                     <Link to={`/c/${community.community_name}`}>
835                       {community.community_name}
836                     </Link>
837                   </li>
838                 ))}
839               </ul>
840             </div>
841           </div>
842         )}
843       </div>
844     );
845   }
846
847   paginator() {
848     return (
849       <div class="my-2">
850         {this.state.page > 1 && (
851           <button
852             class="btn btn-sm btn-secondary mr-1"
853             onClick={linkEvent(this, this.prevPage)}
854           >
855             {i18n.t('prev')}
856           </button>
857         )}
858         <button
859           class="btn btn-sm btn-secondary"
860           onClick={linkEvent(this, this.nextPage)}
861         >
862           {i18n.t('next')}
863         </button>
864       </div>
865     );
866   }
867
868   updateUrl() {
869     let viewStr = View[this.state.view].toLowerCase();
870     let sortStr = SortType[this.state.sort].toLowerCase();
871     this.props.history.push(
872       `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
873     );
874   }
875
876   nextPage(i: User) {
877     i.state.page++;
878     i.setState(i.state);
879     i.updateUrl();
880     i.refetch();
881   }
882
883   prevPage(i: User) {
884     i.state.page--;
885     i.setState(i.state);
886     i.updateUrl();
887     i.refetch();
888   }
889
890   refetch() {
891     let form: GetUserDetailsForm = {
892       user_id: this.state.user_id,
893       username: this.state.username,
894       sort: SortType[this.state.sort],
895       saved_only: this.state.view == View.Saved,
896       page: this.state.page,
897       limit: fetchLimit,
898     };
899     WebSocketService.Instance.getUserDetails(form);
900   }
901
902   handleSortChange(val: SortType) {
903     this.state.sort = val;
904     this.state.page = 1;
905     this.setState(this.state);
906     this.updateUrl();
907     this.refetch();
908   }
909
910   handleViewChange(i: User, event: any) {
911     i.state.view = Number(event.target.value);
912     i.state.page = 1;
913     i.setState(i.state);
914     i.updateUrl();
915     i.refetch();
916   }
917
918   handleUserSettingsShowNsfwChange(i: User, event: any) {
919     i.state.userSettingsForm.show_nsfw = event.target.checked;
920     i.setState(i.state);
921   }
922
923   handleUserSettingsShowAvatarsChange(i: User, event: any) {
924     i.state.userSettingsForm.show_avatars = event.target.checked;
925     UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
926     i.setState(i.state);
927   }
928
929   handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
930     i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
931     i.setState(i.state);
932   }
933
934   handleUserSettingsThemeChange(i: User, event: any) {
935     i.state.userSettingsForm.theme = event.target.value;
936     setTheme(event.target.value, true);
937     i.setState(i.state);
938   }
939
940   handleUserSettingsLangChange(i: User, event: any) {
941     i.state.userSettingsForm.lang = event.target.value;
942     i18n.changeLanguage(i.state.userSettingsForm.lang);
943     i.setState(i.state);
944   }
945
946   handleUserSettingsSortTypeChange(val: SortType) {
947     this.state.userSettingsForm.default_sort_type = val;
948     this.setState(this.state);
949   }
950
951   handleUserSettingsListingTypeChange(val: ListingType) {
952     this.state.userSettingsForm.default_listing_type = val;
953     this.setState(this.state);
954   }
955
956   handleUserSettingsEmailChange(i: User, event: any) {
957     i.state.userSettingsForm.email = event.target.value;
958     if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
959       i.state.userSettingsForm.email = undefined;
960     }
961     i.setState(i.state);
962   }
963
964   handleUserSettingsMatrixUserIdChange(i: User, event: any) {
965     i.state.userSettingsForm.matrix_user_id = event.target.value;
966     if (
967       i.state.userSettingsForm.matrix_user_id == '' &&
968       !i.state.user.matrix_user_id
969     ) {
970       i.state.userSettingsForm.matrix_user_id = undefined;
971     }
972     i.setState(i.state);
973   }
974
975   handleUserSettingsNewPasswordChange(i: User, event: any) {
976     i.state.userSettingsForm.new_password = event.target.value;
977     if (i.state.userSettingsForm.new_password == '') {
978       i.state.userSettingsForm.new_password = undefined;
979     }
980     i.setState(i.state);
981   }
982
983   handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
984     i.state.userSettingsForm.new_password_verify = event.target.value;
985     if (i.state.userSettingsForm.new_password_verify == '') {
986       i.state.userSettingsForm.new_password_verify = undefined;
987     }
988     i.setState(i.state);
989   }
990
991   handleUserSettingsOldPasswordChange(i: User, event: any) {
992     i.state.userSettingsForm.old_password = event.target.value;
993     if (i.state.userSettingsForm.old_password == '') {
994       i.state.userSettingsForm.old_password = undefined;
995     }
996     i.setState(i.state);
997   }
998
999   handleImageUpload(i: User, event: any) {
1000     event.preventDefault();
1001     let file = event.target.files[0];
1002     const imageUploadUrl = `/pictrs/image`;
1003     const formData = new FormData();
1004     formData.append('images[]', file);
1005
1006     i.state.avatarLoading = true;
1007     i.setState(i.state);
1008
1009     fetch(imageUploadUrl, {
1010       method: 'POST',
1011       body: formData,
1012     })
1013       .then(res => res.json())
1014       .then(res => {
1015         console.log('pictrs upload:');
1016         console.log(res);
1017         if (res.msg == 'ok') {
1018           let hash = res.files[0].file;
1019           let url = `${window.location.origin}/pictrs/image/${hash}`;
1020           i.state.userSettingsForm.avatar = url;
1021           i.state.avatarLoading = false;
1022           i.setState(i.state);
1023         } else {
1024           i.state.avatarLoading = false;
1025           i.setState(i.state);
1026           toast(JSON.stringify(res), 'danger');
1027         }
1028       })
1029       .catch(error => {
1030         i.state.avatarLoading = false;
1031         i.setState(i.state);
1032         toast(error, 'danger');
1033       });
1034   }
1035
1036   handleUserSettingsSubmit(i: User, event: any) {
1037     event.preventDefault();
1038     i.state.userSettingsLoading = true;
1039     i.setState(i.state);
1040
1041     WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
1042   }
1043
1044   handleDeleteAccountShowConfirmToggle(i: User, event: any) {
1045     event.preventDefault();
1046     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
1047     i.setState(i.state);
1048   }
1049
1050   handleDeleteAccountPasswordChange(i: User, event: any) {
1051     i.state.deleteAccountForm.password = event.target.value;
1052     i.setState(i.state);
1053   }
1054
1055   handleLogoutClick(i: User) {
1056     UserService.Instance.logout();
1057     i.context.router.history.push('/');
1058   }
1059
1060   handleDeleteAccount(i: User, event: any) {
1061     event.preventDefault();
1062     i.state.deleteAccountLoading = true;
1063     i.setState(i.state);
1064
1065     WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
1066   }
1067
1068   parseMessage(msg: WebSocketJsonResponse) {
1069     console.log(msg);
1070     let res = wsJsonToRes(msg);
1071     if (msg.error) {
1072       toast(i18n.t(msg.error), 'danger');
1073       this.state.deleteAccountLoading = false;
1074       this.state.avatarLoading = false;
1075       this.state.userSettingsLoading = false;
1076       if (msg.error == 'couldnt_find_that_username_or_email') {
1077         this.context.router.history.push('/');
1078       }
1079       this.setState(this.state);
1080       return;
1081     } else if (msg.reconnect) {
1082       this.refetch();
1083     } else if (res.op == UserOperation.GetUserDetails) {
1084       let data = res.data as UserDetailsResponse;
1085       this.state.user = data.user;
1086       this.state.comments = data.comments;
1087       this.state.follows = data.follows;
1088       this.state.moderates = data.moderates;
1089       this.state.posts = data.posts;
1090       this.state.admins = data.admins;
1091       this.state.loading = false;
1092       if (this.isCurrentUser) {
1093         this.state.userSettingsForm.show_nsfw =
1094           UserService.Instance.user.show_nsfw;
1095         this.state.userSettingsForm.theme = UserService.Instance.user.theme
1096           ? UserService.Instance.user.theme
1097           : 'darkly';
1098         this.state.userSettingsForm.default_sort_type =
1099           UserService.Instance.user.default_sort_type;
1100         this.state.userSettingsForm.default_listing_type =
1101           UserService.Instance.user.default_listing_type;
1102         this.state.userSettingsForm.lang = UserService.Instance.user.lang;
1103         this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
1104         this.state.userSettingsForm.email = this.state.user.email;
1105         this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
1106         this.state.userSettingsForm.show_avatars =
1107           UserService.Instance.user.show_avatars;
1108         this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
1109       }
1110       document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
1111       window.scrollTo(0, 0);
1112       this.setState(this.state);
1113       setupTippy();
1114     } else if (res.op == UserOperation.EditComment) {
1115       let data = res.data as CommentResponse;
1116       editCommentRes(data, this.state.comments);
1117       this.setState(this.state);
1118     } else if (res.op == UserOperation.CreateComment) {
1119       let data = res.data as CommentResponse;
1120       if (
1121         UserService.Instance.user &&
1122         data.comment.creator_id == UserService.Instance.user.id
1123       ) {
1124         toast(i18n.t('reply_sent'));
1125       }
1126     } else if (res.op == UserOperation.SaveComment) {
1127       let data = res.data as CommentResponse;
1128       saveCommentRes(data, this.state.comments);
1129       this.setState(this.state);
1130     } else if (res.op == UserOperation.CreateCommentLike) {
1131       let data = res.data as CommentResponse;
1132       createCommentLikeRes(data, this.state.comments);
1133       this.setState(this.state);
1134     } else if (res.op == UserOperation.CreatePostLike) {
1135       let data = res.data as PostResponse;
1136       createPostLikeFindRes(data, this.state.posts);
1137       this.setState(this.state);
1138     } else if (res.op == UserOperation.BanUser) {
1139       let data = res.data as BanUserResponse;
1140       this.state.comments
1141         .filter(c => c.creator_id == data.user.id)
1142         .forEach(c => (c.banned = data.banned));
1143       this.state.posts
1144         .filter(c => c.creator_id == data.user.id)
1145         .forEach(c => (c.banned = data.banned));
1146       this.setState(this.state);
1147     } else if (res.op == UserOperation.AddAdmin) {
1148       let data = res.data as AddAdminResponse;
1149       this.state.admins = data.admins;
1150       this.setState(this.state);
1151     } else if (res.op == UserOperation.SaveUserSettings) {
1152       let data = res.data as LoginResponse;
1153       this.state = this.emptyState;
1154       this.state.userSettingsLoading = false;
1155       this.setState(this.state);
1156       UserService.Instance.login(data);
1157     } else if (res.op == UserOperation.DeleteAccount) {
1158       this.state.deleteAccountLoading = false;
1159       this.state.deleteAccountShowConfirm = false;
1160       this.setState(this.state);
1161       this.context.router.history.push('/');
1162     }
1163   }
1164 }