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