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