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