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