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