]> Untitled Git - lemmy.git/blob - ui/src/components/user.tsx
Adding user avatars / icons. Requires pictshare.
[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 } from '../interfaces';
22 import { WebSocketService, UserService } from '../services';
23 import {
24   msgOp,
25   fetchLimit,
26   routeSortTypeToEnum,
27   capitalizeFirstLetter,
28   themes,
29   setTheme,
30   languages,
31 } from '../utils';
32 import { PostListing } from './post-listing';
33 import { SortSelect } from './sort-select';
34 import { ListingTypeSelect } from './listing-type-select';
35 import { CommentNodes } from './comment-nodes';
36 import { MomentTime } from './moment-time';
37 import { i18n } from '../i18next';
38 import { T } from 'inferno-i18next';
39
40 enum View {
41   Overview,
42   Comments,
43   Posts,
44   Saved,
45 }
46
47 interface UserState {
48   user: UserView;
49   user_id: number;
50   username: string;
51   follows: Array<CommunityUser>;
52   moderates: Array<CommunityUser>;
53   comments: Array<Comment>;
54   posts: Array<Post>;
55   saved?: Array<Post>;
56   admins: Array<UserView>;
57   view: View;
58   sort: SortType;
59   page: number;
60   loading: boolean;
61   avatarLoading: boolean;
62   userSettingsForm: UserSettingsForm;
63   userSettingsLoading: boolean;
64   deleteAccountLoading: boolean;
65   deleteAccountShowConfirm: boolean;
66   deleteAccountForm: DeleteAccountForm;
67 }
68
69 export class User extends Component<any, UserState> {
70   private subscription: Subscription;
71   private emptyState: UserState = {
72     user: {
73       id: null,
74       name: null,
75       fedi_name: null,
76       published: null,
77       number_of_posts: null,
78       post_score: null,
79       number_of_comments: null,
80       comment_score: null,
81       banned: null,
82       avatar: null,
83     },
84     user_id: null,
85     username: null,
86     follows: [],
87     moderates: [],
88     comments: [],
89     posts: [],
90     admins: [],
91     loading: true,
92     avatarLoading: false,
93     view: this.getViewFromProps(this.props),
94     sort: this.getSortTypeFromProps(this.props),
95     page: this.getPageFromProps(this.props),
96     userSettingsForm: {
97       show_nsfw: null,
98       theme: null,
99       default_sort_type: null,
100       default_listing_type: null,
101       lang: null,
102       avatar: null,
103       auth: null,
104     },
105     userSettingsLoading: null,
106     deleteAccountLoading: null,
107     deleteAccountShowConfirm: false,
108     deleteAccountForm: {
109       password: null,
110     },
111   };
112
113   constructor(props: any, context: any) {
114     super(props, context);
115
116     this.state = this.emptyState;
117     this.handleSortChange = this.handleSortChange.bind(this);
118     this.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind(
119       this
120     );
121     this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
122       this
123     );
124
125     this.state.user_id = Number(this.props.match.params.id);
126     this.state.username = this.props.match.params.username;
127
128     this.subscription = WebSocketService.Instance.subject
129       .pipe(
130         retryWhen(errors =>
131           errors.pipe(
132             delay(3000),
133             take(10)
134           )
135         )
136       )
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           <h5>
203             <svg class="icon icon-spinner spin">
204               <use xlinkHref="#icon-spinner"></use>
205             </svg>
206           </h5>
207         ) : (
208           <div class="row">
209             <div class="col-12 col-md-8">
210               <h5>
211                 {this.state.user.avatar && (
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             <T i18nKey="view">#</T>
250           </option>
251           <option value={View.Overview}>
252             <T i18nKey="overview">#</T>
253           </option>
254           <option value={View.Comments}>
255             <T i18nKey="comments">#</T>
256           </option>
257           <option value={View.Posts}>
258             <T i18nKey="posts">#</T>
259           </option>
260           <option value={View.Saved}>
261             <T i18nKey="saved">#</T>
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                 viewOnly
314               />
315             ) : (
316               <CommentNodes
317                 nodes={[{ comment: i.data as Comment }]}
318                 admins={this.state.admins}
319                 noIndent
320               />
321             )}
322           </div>
323         ))}
324       </div>
325     );
326   }
327
328   comments() {
329     return (
330       <div>
331         {this.state.comments.map(comment => (
332           <CommentNodes
333             nodes={[{ comment: comment }]}
334             admins={this.state.admins}
335             noIndent
336           />
337         ))}
338       </div>
339     );
340   }
341
342   posts() {
343     return (
344       <div>
345         {this.state.posts.map(post => (
346           <PostListing
347             post={post}
348             admins={this.state.admins}
349             showCommunity
350             viewOnly
351           />
352         ))}
353       </div>
354     );
355   }
356
357   userInfo() {
358     let user = this.state.user;
359     return (
360       <div>
361         <div class="card border-secondary mb-3">
362           <div class="card-body">
363             <h5>
364               <ul class="list-inline mb-0">
365                 <li className="list-inline-item">{user.name}</li>
366                 {user.banned && (
367                   <li className="list-inline-item badge badge-danger">
368                     <T i18nKey="banned">#</T>
369                   </li>
370                 )}
371               </ul>
372             </h5>
373             <div>
374               {i18n.t('joined')} <MomentTime data={user} />
375             </div>
376             <div class="table-responsive">
377               <table class="table table-bordered table-sm mt-2 mb-0">
378                 <tr>
379                   <td>
380                     <T
381                       i18nKey="number_of_points"
382                       interpolation={{ count: user.post_score }}
383                     >
384                       #
385                     </T>
386                   </td>
387                   <td>
388                     <T
389                       i18nKey="number_of_posts"
390                       interpolation={{ count: user.number_of_posts }}
391                     >
392                       #
393                     </T>
394                   </td>
395                 </tr>
396                 <tr>
397                   <td>
398                     <T
399                       i18nKey="number_of_points"
400                       interpolation={{ count: user.comment_score }}
401                     >
402                       #
403                     </T>
404                   </td>
405                   <td>
406                     <T
407                       i18nKey="number_of_comments"
408                       interpolation={{ count: user.number_of_comments }}
409                     >
410                       #
411                     </T>
412                   </td>
413                 </tr>
414               </table>
415             </div>
416             {this.isCurrentUser && (
417               <button
418                 class="btn btn-block btn-secondary mt-3"
419                 onClick={linkEvent(this, this.handleLogoutClick)}
420               >
421                 <T i18nKey="logout">#</T>
422               </button>
423             )}
424           </div>
425         </div>
426       </div>
427     );
428   }
429
430   userSettings() {
431     return (
432       <div>
433         <div class="card border-secondary mb-3">
434           <div class="card-body">
435             <h5>
436               <T i18nKey="settings">#</T>
437             </h5>
438             <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
439               <div class="form-group">
440                 <div class="col-12">
441                   <label>
442                     <T i18nKey="avatar">#</T>
443                   </label>
444                   <form class="d-inline">
445                     <label
446                       htmlFor="file-upload"
447                       class="pointer ml-4 text-muted small font-weight-bold"
448                     >
449                       <img
450                         height="80"
451                         width="80"
452                         src={
453                           this.state.userSettingsForm.avatar
454                             ? this.state.userSettingsForm.avatar
455                             : 'https://via.placeholder.com/300/000?text=Avatar'
456                         }
457                         class="rounded-circle"
458                       />
459                     </label>
460                     <input
461                       id="file-upload"
462                       type="file"
463                       accept="image/*,video/*"
464                       name="file"
465                       class="d-none"
466                       disabled={!UserService.Instance.user}
467                       onChange={linkEvent(this, this.handleImageUpload)}
468                     />
469                   </form>
470                 </div>
471               </div>
472               <div class="form-group">
473                 <div class="col-12">
474                   <label>
475                     <T i18nKey="language">#</T>
476                   </label>
477                   <select
478                     value={this.state.userSettingsForm.lang}
479                     onChange={linkEvent(
480                       this,
481                       this.handleUserSettingsLangChange
482                     )}
483                     class="ml-2 custom-select custom-select-sm w-auto"
484                   >
485                     <option disabled>
486                       <T i18nKey="language">#</T>
487                     </option>
488                     <option value="browser">
489                       <T i18nKey="browser_default">#</T>
490                     </option>
491                     <option disabled>──</option>
492                     {languages.map(lang => (
493                       <option value={lang.code}>{lang.name}</option>
494                     ))}
495                   </select>
496                 </div>
497               </div>
498               <div class="form-group">
499                 <div class="col-12">
500                   <label>
501                     <T i18nKey="theme">#</T>
502                   </label>
503                   <select
504                     value={this.state.userSettingsForm.theme}
505                     onChange={linkEvent(
506                       this,
507                       this.handleUserSettingsThemeChange
508                     )}
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               </div>
520               <form className="form-group">
521                 <div class="col-12">
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                 </div>
532               </form>
533               <form className="form-group">
534                 <div class="col-12">
535                   <label>
536                     <T i18nKey="type" class="mr-2">
537                       #
538                     </T>
539                   </label>
540                   <SortSelect
541                     sort={this.state.userSettingsForm.default_sort_type}
542                     onChange={this.handleUserSettingsSortTypeChange}
543                   />
544                 </div>
545               </form>
546               {WebSocketService.Instance.site.enable_nsfw && (
547                 <div class="form-group">
548                   <div class="col-12">
549                     <div class="form-check">
550                       <input
551                         class="form-check-input"
552                         type="checkbox"
553                         checked={this.state.userSettingsForm.show_nsfw}
554                         onChange={linkEvent(
555                           this,
556                           this.handleUserSettingsShowNsfwChange
557                         )}
558                       />
559                       <label class="form-check-label">
560                         <T i18nKey="show_nsfw">#</T>
561                       </label>
562                     </div>
563                   </div>
564                 </div>
565               )}
566               <div class="form-group">
567                 <div class="col-12">
568                   <button
569                     type="submit"
570                     class="btn btn-block btn-secondary mr-4"
571                   >
572                     {this.state.userSettingsLoading ? (
573                       <svg class="icon icon-spinner spin">
574                         <use xlinkHref="#icon-spinner"></use>
575                       </svg>
576                     ) : (
577                       capitalizeFirstLetter(i18n.t('save'))
578                     )}
579                   </button>
580                 </div>
581               </div>
582               <hr />
583               <div class="form-group mb-0">
584                 <div class="col-12">
585                   <button
586                     class="btn btn-block btn-danger"
587                     onClick={linkEvent(
588                       this,
589                       this.handleDeleteAccountShowConfirmToggle
590                     )}
591                   >
592                     <T i18nKey="delete_account">#</T>
593                   </button>
594                   {this.state.deleteAccountShowConfirm && (
595                     <>
596                       <div class="my-2 alert alert-danger" role="alert">
597                         <T i18nKey="delete_account_confirm">#</T>
598                       </div>
599                       <input
600                         type="password"
601                         value={this.state.deleteAccountForm.password}
602                         onInput={linkEvent(
603                           this,
604                           this.handleDeleteAccountPasswordChange
605                         )}
606                         class="form-control my-2"
607                       />
608                       <button
609                         class="btn btn-danger mr-4"
610                         disabled={!this.state.deleteAccountForm.password}
611                         onClick={linkEvent(this, this.handleDeleteAccount)}
612                       >
613                         {this.state.deleteAccountLoading ? (
614                           <svg class="icon icon-spinner spin">
615                             <use xlinkHref="#icon-spinner"></use>
616                           </svg>
617                         ) : (
618                           capitalizeFirstLetter(i18n.t('delete'))
619                         )}
620                       </button>
621                       <button
622                         class="btn btn-secondary"
623                         onClick={linkEvent(
624                           this,
625                           this.handleDeleteAccountShowConfirmToggle
626                         )}
627                       >
628                         <T i18nKey="cancel">#</T>
629                       </button>
630                     </>
631                   )}
632                 </div>
633               </div>
634             </form>
635           </div>
636         </div>
637       </div>
638     );
639   }
640
641   moderates() {
642     return (
643       <div>
644         {this.state.moderates.length > 0 && (
645           <div class="card border-secondary mb-3">
646             <div class="card-body">
647               <h5>
648                 <T i18nKey="moderates">#</T>
649               </h5>
650               <ul class="list-unstyled mb-0">
651                 {this.state.moderates.map(community => (
652                   <li>
653                     <Link to={`/c/${community.community_name}`}>
654                       {community.community_name}
655                     </Link>
656                   </li>
657                 ))}
658               </ul>
659             </div>
660           </div>
661         )}
662       </div>
663     );
664   }
665
666   follows() {
667     return (
668       <div>
669         {this.state.follows.length > 0 && (
670           <div class="card border-secondary mb-3">
671             <div class="card-body">
672               <h5>
673                 <T i18nKey="subscribed">#</T>
674               </h5>
675               <ul class="list-unstyled mb-0">
676                 {this.state.follows.map(community => (
677                   <li>
678                     <Link to={`/c/${community.community_name}`}>
679                       {community.community_name}
680                     </Link>
681                   </li>
682                 ))}
683               </ul>
684             </div>
685           </div>
686         )}
687       </div>
688     );
689   }
690
691   paginator() {
692     return (
693       <div class="my-2">
694         {this.state.page > 1 && (
695           <button
696             class="btn btn-sm btn-secondary mr-1"
697             onClick={linkEvent(this, this.prevPage)}
698           >
699             <T i18nKey="prev">#</T>
700           </button>
701         )}
702         <button
703           class="btn btn-sm btn-secondary"
704           onClick={linkEvent(this, this.nextPage)}
705         >
706           <T i18nKey="next">#</T>
707         </button>
708       </div>
709     );
710   }
711
712   updateUrl() {
713     let viewStr = View[this.state.view].toLowerCase();
714     let sortStr = SortType[this.state.sort].toLowerCase();
715     this.props.history.push(
716       `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
717     );
718   }
719
720   nextPage(i: User) {
721     i.state.page++;
722     i.setState(i.state);
723     i.updateUrl();
724     i.refetch();
725   }
726
727   prevPage(i: User) {
728     i.state.page--;
729     i.setState(i.state);
730     i.updateUrl();
731     i.refetch();
732   }
733
734   refetch() {
735     let form: GetUserDetailsForm = {
736       user_id: this.state.user_id,
737       username: this.state.username,
738       sort: SortType[this.state.sort],
739       saved_only: this.state.view == View.Saved,
740       page: this.state.page,
741       limit: fetchLimit,
742     };
743     WebSocketService.Instance.getUserDetails(form);
744   }
745
746   handleSortChange(val: SortType) {
747     this.state.sort = val;
748     this.state.page = 1;
749     this.setState(this.state);
750     this.updateUrl();
751     this.refetch();
752   }
753
754   handleViewChange(i: User, event: any) {
755     i.state.view = Number(event.target.value);
756     i.state.page = 1;
757     i.setState(i.state);
758     i.updateUrl();
759     i.refetch();
760   }
761
762   handleUserSettingsShowNsfwChange(i: User, event: any) {
763     i.state.userSettingsForm.show_nsfw = event.target.checked;
764     i.setState(i.state);
765   }
766
767   handleUserSettingsThemeChange(i: User, event: any) {
768     i.state.userSettingsForm.theme = event.target.value;
769     setTheme(event.target.value);
770     i.setState(i.state);
771   }
772
773   handleUserSettingsLangChange(i: User, event: any) {
774     i.state.userSettingsForm.lang = event.target.value;
775     i18n.changeLanguage(i.state.userSettingsForm.lang);
776     i.setState(i.state);
777   }
778
779   handleUserSettingsSortTypeChange(val: SortType) {
780     this.state.userSettingsForm.default_sort_type = val;
781     this.setState(this.state);
782   }
783
784   handleUserSettingsListingTypeChange(val: ListingType) {
785     this.state.userSettingsForm.default_listing_type = val;
786     this.setState(this.state);
787   }
788
789   handleImageUpload(i: User, event: any) {
790     event.preventDefault();
791     let file = event.target.files[0];
792     const imageUploadUrl = `/pictshare/api/upload.php`;
793     const formData = new FormData();
794     formData.append('file', file);
795
796     i.state.avatarLoading = true;
797     i.setState(i.state);
798
799     fetch(imageUploadUrl, {
800       method: 'POST',
801       body: formData,
802     })
803       .then(res => res.json())
804       .then(res => {
805         let url = `${window.location.origin}/pictshare/${res.url}`;
806         if (res.filetype == 'mp4') {
807           url += '/raw';
808         }
809         i.state.userSettingsForm.avatar = url;
810         console.log(url);
811         i.state.avatarLoading = false;
812         i.setState(i.state);
813       })
814       .catch(error => {
815         i.state.avatarLoading = false;
816         i.setState(i.state);
817         alert(error);
818       });
819   }
820
821   handleUserSettingsSubmit(i: User, event: any) {
822     event.preventDefault();
823     i.state.userSettingsLoading = true;
824     i.setState(i.state);
825
826     WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
827   }
828
829   handleDeleteAccountShowConfirmToggle(i: User, event: any) {
830     event.preventDefault();
831     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
832     i.setState(i.state);
833   }
834
835   handleDeleteAccountPasswordChange(i: User, event: any) {
836     i.state.deleteAccountForm.password = event.target.value;
837     i.setState(i.state);
838   }
839
840   handleLogoutClick(i: User) {
841     UserService.Instance.logout();
842     i.context.router.history.push('/');
843   }
844
845   handleDeleteAccount(i: User, event: any) {
846     event.preventDefault();
847     i.state.deleteAccountLoading = true;
848     i.setState(i.state);
849
850     WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
851   }
852
853   parseMessage(msg: any) {
854     console.log(msg);
855     let op: UserOperation = msgOp(msg);
856     if (msg.error) {
857       alert(i18n.t(msg.error));
858       this.state.deleteAccountLoading = false;
859       if (msg.error == 'couldnt_find_that_username_or_email') {
860         this.context.router.history.push('/');
861       }
862       this.setState(this.state);
863       return;
864     } else if (op == UserOperation.GetUserDetails) {
865       let res: UserDetailsResponse = msg;
866       this.state.user = res.user;
867       this.state.comments = res.comments;
868       this.state.follows = res.follows;
869       this.state.moderates = res.moderates;
870       this.state.posts = res.posts;
871       this.state.admins = res.admins;
872       this.state.loading = false;
873       if (this.isCurrentUser) {
874         this.state.userSettingsForm.show_nsfw =
875           UserService.Instance.user.show_nsfw;
876         this.state.userSettingsForm.theme = UserService.Instance.user.theme
877           ? UserService.Instance.user.theme
878           : 'darkly';
879         this.state.userSettingsForm.default_sort_type =
880           UserService.Instance.user.default_sort_type;
881         this.state.userSettingsForm.default_listing_type =
882           UserService.Instance.user.default_listing_type;
883         this.state.userSettingsForm.lang = UserService.Instance.user.lang;
884         this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
885       }
886       document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
887       window.scrollTo(0, 0);
888       this.setState(this.state);
889     } else if (op == UserOperation.EditComment) {
890       let res: CommentResponse = msg;
891
892       let found = this.state.comments.find(c => c.id == res.comment.id);
893       found.content = res.comment.content;
894       found.updated = res.comment.updated;
895       found.removed = res.comment.removed;
896       found.deleted = res.comment.deleted;
897       found.upvotes = res.comment.upvotes;
898       found.downvotes = res.comment.downvotes;
899       found.score = res.comment.score;
900
901       this.setState(this.state);
902     } else if (op == UserOperation.CreateComment) {
903       // let res: CommentResponse = msg;
904       alert(i18n.t('reply_sent'));
905       // this.state.comments.unshift(res.comment); // TODO do this right
906       // this.setState(this.state);
907     } else if (op == UserOperation.SaveComment) {
908       let res: CommentResponse = msg;
909       let found = this.state.comments.find(c => c.id == res.comment.id);
910       found.saved = res.comment.saved;
911       this.setState(this.state);
912     } else if (op == UserOperation.CreateCommentLike) {
913       let res: CommentResponse = msg;
914       let found: Comment = this.state.comments.find(
915         c => c.id === res.comment.id
916       );
917       found.score = res.comment.score;
918       found.upvotes = res.comment.upvotes;
919       found.downvotes = res.comment.downvotes;
920       if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
921       this.setState(this.state);
922     } else if (op == UserOperation.BanUser) {
923       let res: BanUserResponse = msg;
924       this.state.comments
925         .filter(c => c.creator_id == res.user.id)
926         .forEach(c => (c.banned = res.banned));
927       this.state.posts
928         .filter(c => c.creator_id == res.user.id)
929         .forEach(c => (c.banned = res.banned));
930       this.setState(this.state);
931     } else if (op == UserOperation.AddAdmin) {
932       let res: AddAdminResponse = msg;
933       this.state.admins = res.admins;
934       this.setState(this.state);
935     } else if (op == UserOperation.SaveUserSettings) {
936       this.state = this.emptyState;
937       this.state.userSettingsLoading = false;
938       this.setState(this.state);
939       let res: LoginResponse = msg;
940       UserService.Instance.login(res);
941     } else if (op == UserOperation.DeleteAccount) {
942       this.state.deleteAccountLoading = false;
943       this.state.deleteAccountShowConfirm = false;
944       this.setState(this.state);
945       this.context.router.history.push('/');
946     }
947   }
948 }