]> Untitled Git - lemmy.git/blob - ui/src/components/user.tsx
Merge branch 'master' into jmarthernandez-remove-karma-from-search
[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.checkSettingsAvatar ? (
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               {this.checkSettingsAvatar && (
563                 <div class="form-group">
564                   <button
565                     class="btn btn-secondary btn-block"
566                     onClick={linkEvent(this, this.removeAvatar)}
567                   >
568                     {`${capitalizeFirstLetter(i18n.t('remove'))} ${i18n.t(
569                       'avatar'
570                     )}`}
571                   </button>
572                 </div>
573               )}
574               <div class="form-group">
575                 <label>{i18n.t('language')}</label>
576                 <select
577                   value={this.state.userSettingsForm.lang}
578                   onChange={linkEvent(this, this.handleUserSettingsLangChange)}
579                   class="ml-2 custom-select custom-select-sm w-auto"
580                 >
581                   <option disabled>{i18n.t('language')}</option>
582                   <option value="browser">{i18n.t('browser_default')}</option>
583                   <option disabled>──</option>
584                   {languages.map(lang => (
585                     <option value={lang.code}>{lang.name}</option>
586                   ))}
587                 </select>
588               </div>
589               <div class="form-group">
590                 <label>{i18n.t('theme')}</label>
591                 <select
592                   value={this.state.userSettingsForm.theme}
593                   onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
594                   class="ml-2 custom-select custom-select-sm w-auto"
595                 >
596                   <option disabled>{i18n.t('theme')}</option>
597                   {themes.map(theme => (
598                     <option value={theme}>{theme}</option>
599                   ))}
600                 </select>
601               </div>
602               <form className="form-group">
603                 <label>
604                   <div class="mr-2">{i18n.t('sort_type')}</div>
605                 </label>
606                 <ListingTypeSelect
607                   type_={this.state.userSettingsForm.default_listing_type}
608                   onChange={this.handleUserSettingsListingTypeChange}
609                 />
610               </form>
611               <form className="form-group">
612                 <label>
613                   <div class="mr-2">{i18n.t('type')}</div>
614                 </label>
615                 <SortSelect
616                   sort={this.state.userSettingsForm.default_sort_type}
617                   onChange={this.handleUserSettingsSortTypeChange}
618                 />
619               </form>
620               <div class="form-group row">
621                 <label class="col-lg-3 col-form-label" htmlFor="user-email">
622                   {i18n.t('email')}
623                 </label>
624                 <div class="col-lg-9">
625                   <input
626                     type="email"
627                     id="user-email"
628                     class="form-control"
629                     placeholder={i18n.t('optional')}
630                     value={this.state.userSettingsForm.email}
631                     onInput={linkEvent(
632                       this,
633                       this.handleUserSettingsEmailChange
634                     )}
635                     minLength={3}
636                   />
637                 </div>
638               </div>
639               <div class="form-group row">
640                 <label class="col-lg-5 col-form-label">
641                   <a
642                     href="https://about.riot.im/"
643                     target="_blank"
644                     rel="noopener"
645                   >
646                     {i18n.t('matrix_user_id')}
647                   </a>
648                 </label>
649                 <div class="col-lg-7">
650                   <input
651                     type="text"
652                     class="form-control"
653                     placeholder="@user:example.com"
654                     value={this.state.userSettingsForm.matrix_user_id}
655                     onInput={linkEvent(
656                       this,
657                       this.handleUserSettingsMatrixUserIdChange
658                     )}
659                     minLength={3}
660                   />
661                 </div>
662               </div>
663               <div class="form-group row">
664                 <label class="col-lg-5 col-form-label" htmlFor="user-password">
665                   {i18n.t('new_password')}
666                 </label>
667                 <div class="col-lg-7">
668                   <input
669                     type="password"
670                     id="user-password"
671                     class="form-control"
672                     value={this.state.userSettingsForm.new_password}
673                     autoComplete="new-password"
674                     onInput={linkEvent(
675                       this,
676                       this.handleUserSettingsNewPasswordChange
677                     )}
678                   />
679                 </div>
680               </div>
681               <div class="form-group row">
682                 <label
683                   class="col-lg-5 col-form-label"
684                   htmlFor="user-verify-password"
685                 >
686                   {i18n.t('verify_password')}
687                 </label>
688                 <div class="col-lg-7">
689                   <input
690                     type="password"
691                     id="user-verify-password"
692                     class="form-control"
693                     value={this.state.userSettingsForm.new_password_verify}
694                     autoComplete="new-password"
695                     onInput={linkEvent(
696                       this,
697                       this.handleUserSettingsNewPasswordVerifyChange
698                     )}
699                   />
700                 </div>
701               </div>
702               <div class="form-group row">
703                 <label
704                   class="col-lg-5 col-form-label"
705                   htmlFor="user-old-password"
706                 >
707                   {i18n.t('old_password')}
708                 </label>
709                 <div class="col-lg-7">
710                   <input
711                     type="password"
712                     id="user-old-password"
713                     class="form-control"
714                     value={this.state.userSettingsForm.old_password}
715                     autoComplete="new-password"
716                     onInput={linkEvent(
717                       this,
718                       this.handleUserSettingsOldPasswordChange
719                     )}
720                   />
721                 </div>
722               </div>
723               {this.state.site.enable_nsfw && (
724                 <div class="form-group">
725                   <div class="form-check">
726                     <input
727                       class="form-check-input"
728                       id="user-show-nsfw"
729                       type="checkbox"
730                       checked={this.state.userSettingsForm.show_nsfw}
731                       onChange={linkEvent(
732                         this,
733                         this.handleUserSettingsShowNsfwChange
734                       )}
735                     />
736                     <label class="form-check-label" htmlFor="user-show-nsfw">
737                       {i18n.t('show_nsfw')}
738                     </label>
739                   </div>
740                 </div>
741               )}
742               <div class="form-group">
743                 <div class="form-check">
744                   <input
745                     class="form-check-input"
746                     id="user-show-avatars"
747                     type="checkbox"
748                     checked={this.state.userSettingsForm.show_avatars}
749                     onChange={linkEvent(
750                       this,
751                       this.handleUserSettingsShowAvatarsChange
752                     )}
753                   />
754                   <label class="form-check-label" htmlFor="user-show-avatars">
755                     {i18n.t('show_avatars')}
756                   </label>
757                 </div>
758               </div>
759               <div class="form-group">
760                 <div class="form-check">
761                   <input
762                     class="form-check-input"
763                     id="user-send-notifications-to-email"
764                     type="checkbox"
765                     disabled={!this.state.user.email}
766                     checked={
767                       this.state.userSettingsForm.send_notifications_to_email
768                     }
769                     onChange={linkEvent(
770                       this,
771                       this.handleUserSettingsSendNotificationsToEmailChange
772                     )}
773                   />
774                   <label
775                     class="form-check-label"
776                     htmlFor="user-send-notifications-to-email"
777                   >
778                     {i18n.t('send_notifications_to_email')}
779                   </label>
780                 </div>
781               </div>
782               <div class="form-group">
783                 <button type="submit" class="btn btn-block btn-secondary mr-4">
784                   {this.state.userSettingsLoading ? (
785                     <svg class="icon icon-spinner spin">
786                       <use xlinkHref="#icon-spinner"></use>
787                     </svg>
788                   ) : (
789                     capitalizeFirstLetter(i18n.t('save'))
790                   )}
791                 </button>
792               </div>
793               <hr />
794               <div class="form-group mb-0">
795                 <button
796                   class="btn btn-block btn-danger"
797                   onClick={linkEvent(
798                     this,
799                     this.handleDeleteAccountShowConfirmToggle
800                   )}
801                 >
802                   {i18n.t('delete_account')}
803                 </button>
804                 {this.state.deleteAccountShowConfirm && (
805                   <>
806                     <div class="my-2 alert alert-danger" role="alert">
807                       {i18n.t('delete_account_confirm')}
808                     </div>
809                     <input
810                       type="password"
811                       value={this.state.deleteAccountForm.password}
812                       autoComplete="new-password"
813                       onInput={linkEvent(
814                         this,
815                         this.handleDeleteAccountPasswordChange
816                       )}
817                       class="form-control my-2"
818                     />
819                     <button
820                       class="btn btn-danger mr-4"
821                       disabled={!this.state.deleteAccountForm.password}
822                       onClick={linkEvent(this, this.handleDeleteAccount)}
823                     >
824                       {this.state.deleteAccountLoading ? (
825                         <svg class="icon icon-spinner spin">
826                           <use xlinkHref="#icon-spinner"></use>
827                         </svg>
828                       ) : (
829                         capitalizeFirstLetter(i18n.t('delete'))
830                       )}
831                     </button>
832                     <button
833                       class="btn btn-secondary"
834                       onClick={linkEvent(
835                         this,
836                         this.handleDeleteAccountShowConfirmToggle
837                       )}
838                     >
839                       {i18n.t('cancel')}
840                     </button>
841                   </>
842                 )}
843               </div>
844             </form>
845           </div>
846         </div>
847       </div>
848     );
849   }
850
851   moderates() {
852     return (
853       <div>
854         {this.state.moderates.length > 0 && (
855           <div class="card border-secondary mb-3">
856             <div class="card-body">
857               <h5>{i18n.t('moderates')}</h5>
858               <ul class="list-unstyled mb-0">
859                 {this.state.moderates.map(community => (
860                   <li>
861                     <Link to={`/c/${community.community_name}`}>
862                       {community.community_name}
863                     </Link>
864                   </li>
865                 ))}
866               </ul>
867             </div>
868           </div>
869         )}
870       </div>
871     );
872   }
873
874   follows() {
875     return (
876       <div>
877         {this.state.follows.length > 0 && (
878           <div class="card border-secondary mb-3">
879             <div class="card-body">
880               <h5>{i18n.t('subscribed')}</h5>
881               <ul class="list-unstyled mb-0">
882                 {this.state.follows.map(community => (
883                   <li>
884                     <Link to={`/c/${community.community_name}`}>
885                       {community.community_name}
886                     </Link>
887                   </li>
888                 ))}
889               </ul>
890             </div>
891           </div>
892         )}
893       </div>
894     );
895   }
896
897   paginator() {
898     return (
899       <div class="my-2">
900         {this.state.page > 1 && (
901           <button
902             class="btn btn-sm btn-secondary mr-1"
903             onClick={linkEvent(this, this.prevPage)}
904           >
905             {i18n.t('prev')}
906           </button>
907         )}
908         {this.state.comments.length + this.state.posts.length > 0 && (
909           <button
910             class="btn btn-sm btn-secondary"
911             onClick={linkEvent(this, this.nextPage)}
912           >
913             {i18n.t('next')}
914           </button>
915         )}
916       </div>
917     );
918   }
919
920   updateUrl() {
921     let viewStr = View[this.state.view].toLowerCase();
922     let sortStr = SortType[this.state.sort].toLowerCase();
923     this.props.history.push(
924       `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
925     );
926   }
927
928   nextPage(i: User) {
929     i.state.page++;
930     i.setState(i.state);
931     i.updateUrl();
932     i.refetch();
933   }
934
935   prevPage(i: User) {
936     i.state.page--;
937     i.setState(i.state);
938     i.updateUrl();
939     i.refetch();
940   }
941
942   refetch() {
943     let form: GetUserDetailsForm = {
944       user_id: this.state.user_id,
945       username: this.state.username,
946       sort: SortType[this.state.sort],
947       saved_only: this.state.view == View.Saved,
948       page: this.state.page,
949       limit: fetchLimit,
950     };
951     WebSocketService.Instance.getUserDetails(form);
952   }
953
954   handleSortChange(val: SortType) {
955     this.state.sort = val;
956     this.state.page = 1;
957     this.setState(this.state);
958     this.updateUrl();
959     this.refetch();
960   }
961
962   handleViewChange(i: User, event: any) {
963     i.state.view = Number(event.target.value);
964     i.state.page = 1;
965     i.setState(i.state);
966     i.updateUrl();
967     i.refetch();
968   }
969
970   handleUserSettingsShowNsfwChange(i: User, event: any) {
971     i.state.userSettingsForm.show_nsfw = event.target.checked;
972     i.setState(i.state);
973   }
974
975   handleUserSettingsShowAvatarsChange(i: User, event: any) {
976     i.state.userSettingsForm.show_avatars = event.target.checked;
977     UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
978     i.setState(i.state);
979   }
980
981   handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
982     i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
983     i.setState(i.state);
984   }
985
986   handleUserSettingsThemeChange(i: User, event: any) {
987     i.state.userSettingsForm.theme = event.target.value;
988     setTheme(event.target.value, true);
989     i.setState(i.state);
990   }
991
992   handleUserSettingsLangChange(i: User, event: any) {
993     i.state.userSettingsForm.lang = event.target.value;
994     i18n.changeLanguage(i.state.userSettingsForm.lang);
995     i.setState(i.state);
996   }
997
998   handleUserSettingsSortTypeChange(val: SortType) {
999     this.state.userSettingsForm.default_sort_type = val;
1000     this.setState(this.state);
1001   }
1002
1003   handleUserSettingsListingTypeChange(val: ListingType) {
1004     this.state.userSettingsForm.default_listing_type = val;
1005     this.setState(this.state);
1006   }
1007
1008   handleUserSettingsEmailChange(i: User, event: any) {
1009     i.state.userSettingsForm.email = event.target.value;
1010     if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
1011       i.state.userSettingsForm.email = undefined;
1012     }
1013     i.setState(i.state);
1014   }
1015
1016   handleUserSettingsMatrixUserIdChange(i: User, event: any) {
1017     i.state.userSettingsForm.matrix_user_id = event.target.value;
1018     if (
1019       i.state.userSettingsForm.matrix_user_id == '' &&
1020       !i.state.user.matrix_user_id
1021     ) {
1022       i.state.userSettingsForm.matrix_user_id = undefined;
1023     }
1024     i.setState(i.state);
1025   }
1026
1027   handleUserSettingsNewPasswordChange(i: User, event: any) {
1028     i.state.userSettingsForm.new_password = event.target.value;
1029     if (i.state.userSettingsForm.new_password == '') {
1030       i.state.userSettingsForm.new_password = undefined;
1031     }
1032     i.setState(i.state);
1033   }
1034
1035   handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
1036     i.state.userSettingsForm.new_password_verify = event.target.value;
1037     if (i.state.userSettingsForm.new_password_verify == '') {
1038       i.state.userSettingsForm.new_password_verify = undefined;
1039     }
1040     i.setState(i.state);
1041   }
1042
1043   handleUserSettingsOldPasswordChange(i: User, event: any) {
1044     i.state.userSettingsForm.old_password = event.target.value;
1045     if (i.state.userSettingsForm.old_password == '') {
1046       i.state.userSettingsForm.old_password = undefined;
1047     }
1048     i.setState(i.state);
1049   }
1050
1051   handleImageUpload(i: User, event: any) {
1052     event.preventDefault();
1053     let file = event.target.files[0];
1054     const imageUploadUrl = `/pictrs/image`;
1055     const formData = new FormData();
1056     formData.append('images[]', file);
1057
1058     i.state.avatarLoading = true;
1059     i.setState(i.state);
1060
1061     fetch(imageUploadUrl, {
1062       method: 'POST',
1063       body: formData,
1064     })
1065       .then(res => res.json())
1066       .then(res => {
1067         console.log('pictrs upload:');
1068         console.log(res);
1069         if (res.msg == 'ok') {
1070           let hash = res.files[0].file;
1071           let url = `${window.location.origin}/pictrs/image/${hash}`;
1072           i.state.userSettingsForm.avatar = url;
1073           i.state.avatarLoading = false;
1074           i.setState(i.state);
1075         } else {
1076           i.state.avatarLoading = false;
1077           i.setState(i.state);
1078           toast(JSON.stringify(res), 'danger');
1079         }
1080       })
1081       .catch(error => {
1082         i.state.avatarLoading = false;
1083         i.setState(i.state);
1084         toast(error, 'danger');
1085       });
1086   }
1087
1088   removeAvatar(i: User, event: any) {
1089     event.preventDefault();
1090     i.state.userSettingsLoading = true;
1091     i.state.userSettingsForm.avatar = '';
1092     i.setState(i.state);
1093
1094     WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
1095   }
1096
1097   get checkSettingsAvatar(): boolean {
1098     return (
1099       this.state.userSettingsForm.avatar &&
1100       this.state.userSettingsForm.avatar != ''
1101     );
1102   }
1103
1104   handleUserSettingsSubmit(i: User, event: any) {
1105     event.preventDefault();
1106     i.state.userSettingsLoading = true;
1107     i.setState(i.state);
1108
1109     WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
1110   }
1111
1112   handleDeleteAccountShowConfirmToggle(i: User, event: any) {
1113     event.preventDefault();
1114     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
1115     i.setState(i.state);
1116   }
1117
1118   handleDeleteAccountPasswordChange(i: User, event: any) {
1119     i.state.deleteAccountForm.password = event.target.value;
1120     i.setState(i.state);
1121   }
1122
1123   handleLogoutClick(i: User) {
1124     UserService.Instance.logout();
1125     i.context.router.history.push('/');
1126   }
1127
1128   handleDeleteAccount(i: User, event: any) {
1129     event.preventDefault();
1130     i.state.deleteAccountLoading = true;
1131     i.setState(i.state);
1132
1133     WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
1134   }
1135
1136   parseMessage(msg: WebSocketJsonResponse) {
1137     console.log(msg);
1138     let res = wsJsonToRes(msg);
1139     if (msg.error) {
1140       toast(i18n.t(msg.error), 'danger');
1141       this.state.deleteAccountLoading = false;
1142       this.state.avatarLoading = false;
1143       this.state.userSettingsLoading = false;
1144       if (msg.error == 'couldnt_find_that_username_or_email') {
1145         this.context.router.history.push('/');
1146       }
1147       this.setState(this.state);
1148       return;
1149     } else if (msg.reconnect) {
1150       this.refetch();
1151     } else if (res.op == UserOperation.GetUserDetails) {
1152       let data = res.data as UserDetailsResponse;
1153       this.state.user = data.user;
1154       this.state.comments = data.comments;
1155       this.state.follows = data.follows;
1156       this.state.moderates = data.moderates;
1157       this.state.posts = data.posts;
1158       this.state.admins = data.admins;
1159       this.state.loading = false;
1160       if (this.isCurrentUser) {
1161         this.state.userSettingsForm.show_nsfw =
1162           UserService.Instance.user.show_nsfw;
1163         this.state.userSettingsForm.theme = UserService.Instance.user.theme
1164           ? UserService.Instance.user.theme
1165           : 'darkly';
1166         this.state.userSettingsForm.default_sort_type =
1167           UserService.Instance.user.default_sort_type;
1168         this.state.userSettingsForm.default_listing_type =
1169           UserService.Instance.user.default_listing_type;
1170         this.state.userSettingsForm.lang = UserService.Instance.user.lang;
1171         this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
1172         this.state.userSettingsForm.email = this.state.user.email;
1173         this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
1174         this.state.userSettingsForm.show_avatars =
1175           UserService.Instance.user.show_avatars;
1176         this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
1177       }
1178       document.title = `/u/${this.state.user.name} - ${this.state.site.name}`;
1179       window.scrollTo(0, 0);
1180       this.setState(this.state);
1181       setupTippy();
1182     } else if (res.op == UserOperation.EditComment) {
1183       let data = res.data as CommentResponse;
1184       editCommentRes(data, this.state.comments);
1185       this.setState(this.state);
1186     } else if (res.op == UserOperation.CreateComment) {
1187       let data = res.data as CommentResponse;
1188       if (
1189         UserService.Instance.user &&
1190         data.comment.creator_id == UserService.Instance.user.id
1191       ) {
1192         toast(i18n.t('reply_sent'));
1193       }
1194     } else if (res.op == UserOperation.SaveComment) {
1195       let data = res.data as CommentResponse;
1196       saveCommentRes(data, this.state.comments);
1197       this.setState(this.state);
1198     } else if (res.op == UserOperation.CreateCommentLike) {
1199       let data = res.data as CommentResponse;
1200       createCommentLikeRes(data, this.state.comments);
1201       this.setState(this.state);
1202     } else if (res.op == UserOperation.CreatePostLike) {
1203       let data = res.data as PostResponse;
1204       createPostLikeFindRes(data, this.state.posts);
1205       this.setState(this.state);
1206     } else if (res.op == UserOperation.BanUser) {
1207       let data = res.data as BanUserResponse;
1208       this.state.comments
1209         .filter(c => c.creator_id == data.user.id)
1210         .forEach(c => (c.banned = data.banned));
1211       this.state.posts
1212         .filter(c => c.creator_id == data.user.id)
1213         .forEach(c => (c.banned = data.banned));
1214       this.setState(this.state);
1215     } else if (res.op == UserOperation.AddAdmin) {
1216       let data = res.data as AddAdminResponse;
1217       this.state.admins = data.admins;
1218       this.setState(this.state);
1219     } else if (res.op == UserOperation.SaveUserSettings) {
1220       let data = res.data as LoginResponse;
1221       this.state.userSettingsLoading = false;
1222       this.setState(this.state);
1223       UserService.Instance.login(data);
1224     } else if (res.op == UserOperation.DeleteAccount) {
1225       this.state.deleteAccountLoading = false;
1226       this.state.deleteAccountShowConfirm = false;
1227       this.setState(this.state);
1228       this.context.router.history.push('/');
1229     } else if (res.op == UserOperation.GetSite) {
1230       let data = res.data as GetSiteResponse;
1231       this.state.site = data.site;
1232       this.setState(this.state);
1233     }
1234   }
1235 }