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