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