]> Untitled Git - lemmy.git/blob - ui/src/components/user.tsx
Add post title to user overview and user comment list
[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                 showCommunity
387                 showContext
388                 enableDownvotes={this.state.site.enable_downvotes}
389               />
390             )}
391           </div>
392         ))}
393       </div>
394     );
395   }
396
397   comments() {
398     return (
399       <div>
400         <CommentNodes
401           nodes={commentsToFlatNodes(this.state.comments)}
402           admins={this.state.admins}
403           noIndent
404           showCommunity
405           showContext
406           enableDownvotes={this.state.site.enable_downvotes}
407         />
408       </div>
409     );
410   }
411
412   posts() {
413     return (
414       <div>
415         {this.state.posts.map(post => (
416           <PostListing
417             post={post}
418             admins={this.state.admins}
419             showCommunity
420             enableDownvotes={this.state.site.enable_downvotes}
421             enableNsfw={this.state.site.enable_nsfw}
422           />
423         ))}
424       </div>
425     );
426   }
427
428   userInfo() {
429     let user = this.state.user;
430     return (
431       <div>
432         <div class="card border-secondary mb-3">
433           <div class="card-body">
434             <h5>
435               <ul class="list-inline mb-0">
436                 <li className="list-inline-item">
437                   <UserListing user={user} realLink />
438                 </li>
439                 {user.banned && (
440                   <li className="list-inline-item badge badge-danger">
441                     {i18n.t('banned')}
442                   </li>
443                 )}
444               </ul>
445             </h5>
446             <div className="d-flex align-items-center mb-2">
447               <svg class="icon">
448                 <use xlinkHref="#icon-cake"></use>
449               </svg>
450               <span className="ml-2">
451                 {i18n.t('cake_day_title')}{' '}
452                 {moment.utc(user.published).local().format('MMM DD, YYYY')}
453               </span>
454             </div>
455             <div>
456               {i18n.t('joined')} <MomentTime data={user} showAgo />
457             </div>
458             <div class="table-responsive mt-1">
459               <table class="table table-bordered table-sm mt-2 mb-0">
460                 {/*
461                 <tr>
462                   <td class="text-center" colSpan={2}>
463                     {i18n.t('number_of_points', {
464                       count: user.post_score + user.comment_score,
465                     })}
466                   </td>
467                 </tr>
468                 */}
469                 <tr>
470                   {/* 
471                   <td>
472                     {i18n.t('number_of_points', { count: user.post_score })}
473                   </td>
474                   */}
475                   <td>
476                     {i18n.t('number_of_posts', { count: user.number_of_posts })}
477                   </td>
478                   {/* 
479                 </tr>
480                 <tr>
481                   <td>
482                     {i18n.t('number_of_points', { count: user.comment_score })}
483                   </td>
484                   */}
485                   <td>
486                     {i18n.t('number_of_comments', {
487                       count: user.number_of_comments,
488                     })}
489                   </td>
490                 </tr>
491               </table>
492             </div>
493             {this.isCurrentUser ? (
494               <button
495                 class="btn btn-block btn-secondary mt-3"
496                 onClick={linkEvent(this, this.handleLogoutClick)}
497               >
498                 {i18n.t('logout')}
499               </button>
500             ) : (
501               <>
502                 <a
503                   className={`btn btn-block btn-secondary mt-3 ${
504                     !this.state.user.matrix_user_id && 'disabled'
505                   }`}
506                   target="_blank"
507                   rel="noopener"
508                   href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
509                 >
510                   {i18n.t('send_secure_message')}
511                 </a>
512                 <Link
513                   class="btn btn-block btn-secondary mt-3"
514                   to={`/create_private_message?recipient_id=${this.state.user.id}`}
515                 >
516                   {i18n.t('send_message')}
517                 </Link>
518               </>
519             )}
520           </div>
521         </div>
522       </div>
523     );
524   }
525
526   userSettings() {
527     return (
528       <div>
529         <div class="card border-secondary mb-3">
530           <div class="card-body">
531             <h5>{i18n.t('settings')}</h5>
532             <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
533               <div class="form-group">
534                 <label>{i18n.t('avatar')}</label>
535                 <form class="d-inline">
536                   <label
537                     htmlFor="file-upload"
538                     class="pointer ml-4 text-muted small font-weight-bold"
539                   >
540                     {!this.checkSettingsAvatar ? (
541                       <span class="btn btn-sm btn-secondary">
542                         {i18n.t('upload_avatar')}
543                       </span>
544                     ) : (
545                       <img
546                         height="80"
547                         width="80"
548                         src={this.state.userSettingsForm.avatar}
549                         class="rounded-circle"
550                       />
551                     )}
552                   </label>
553                   <input
554                     id="file-upload"
555                     type="file"
556                     accept="image/*,video/*"
557                     name="file"
558                     class="d-none"
559                     disabled={!UserService.Instance.user}
560                     onChange={linkEvent(this, this.handleImageUpload)}
561                   />
562                 </form>
563               </div>
564               {this.checkSettingsAvatar && (
565                 <div class="form-group">
566                   <button
567                     class="btn btn-secondary btn-block"
568                     onClick={linkEvent(this, this.removeAvatar)}
569                   >
570                     {`${capitalizeFirstLetter(i18n.t('remove'))} ${i18n.t(
571                       'avatar'
572                     )}`}
573                   </button>
574                 </div>
575               )}
576               <div class="form-group">
577                 <label>{i18n.t('language')}</label>
578                 <select
579                   value={this.state.userSettingsForm.lang}
580                   onChange={linkEvent(this, this.handleUserSettingsLangChange)}
581                   class="ml-2 custom-select custom-select-sm w-auto"
582                 >
583                   <option disabled>{i18n.t('language')}</option>
584                   <option value="browser">{i18n.t('browser_default')}</option>
585                   <option disabled>──</option>
586                   {languages.map(lang => (
587                     <option value={lang.code}>{lang.name}</option>
588                   ))}
589                 </select>
590               </div>
591               <div class="form-group">
592                 <label>{i18n.t('theme')}</label>
593                 <select
594                   value={this.state.userSettingsForm.theme}
595                   onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
596                   class="ml-2 custom-select custom-select-sm w-auto"
597                 >
598                   <option disabled>{i18n.t('theme')}</option>
599                   {themes.map(theme => (
600                     <option value={theme}>{theme}</option>
601                   ))}
602                 </select>
603               </div>
604               <form className="form-group">
605                 <label>
606                   <div class="mr-2">{i18n.t('sort_type')}</div>
607                 </label>
608                 <ListingTypeSelect
609                   type_={this.state.userSettingsForm.default_listing_type}
610                   onChange={this.handleUserSettingsListingTypeChange}
611                 />
612               </form>
613               <form className="form-group">
614                 <label>
615                   <div class="mr-2">{i18n.t('type')}</div>
616                 </label>
617                 <SortSelect
618                   sort={this.state.userSettingsForm.default_sort_type}
619                   onChange={this.handleUserSettingsSortTypeChange}
620                 />
621               </form>
622               <div class="form-group row">
623                 <label class="col-lg-3 col-form-label" htmlFor="user-email">
624                   {i18n.t('email')}
625                 </label>
626                 <div class="col-lg-9">
627                   <input
628                     type="email"
629                     id="user-email"
630                     class="form-control"
631                     placeholder={i18n.t('optional')}
632                     value={this.state.userSettingsForm.email}
633                     onInput={linkEvent(
634                       this,
635                       this.handleUserSettingsEmailChange
636                     )}
637                     minLength={3}
638                   />
639                 </div>
640               </div>
641               <div class="form-group row">
642                 <label class="col-lg-5 col-form-label">
643                   <a
644                     href="https://about.riot.im/"
645                     target="_blank"
646                     rel="noopener"
647                   >
648                     {i18n.t('matrix_user_id')}
649                   </a>
650                 </label>
651                 <div class="col-lg-7">
652                   <input
653                     type="text"
654                     class="form-control"
655                     placeholder="@user:example.com"
656                     value={this.state.userSettingsForm.matrix_user_id}
657                     onInput={linkEvent(
658                       this,
659                       this.handleUserSettingsMatrixUserIdChange
660                     )}
661                     minLength={3}
662                   />
663                 </div>
664               </div>
665               <div class="form-group row">
666                 <label class="col-lg-5 col-form-label" htmlFor="user-password">
667                   {i18n.t('new_password')}
668                 </label>
669                 <div class="col-lg-7">
670                   <input
671                     type="password"
672                     id="user-password"
673                     class="form-control"
674                     value={this.state.userSettingsForm.new_password}
675                     autoComplete="new-password"
676                     onInput={linkEvent(
677                       this,
678                       this.handleUserSettingsNewPasswordChange
679                     )}
680                   />
681                 </div>
682               </div>
683               <div class="form-group row">
684                 <label
685                   class="col-lg-5 col-form-label"
686                   htmlFor="user-verify-password"
687                 >
688                   {i18n.t('verify_password')}
689                 </label>
690                 <div class="col-lg-7">
691                   <input
692                     type="password"
693                     id="user-verify-password"
694                     class="form-control"
695                     value={this.state.userSettingsForm.new_password_verify}
696                     autoComplete="new-password"
697                     onInput={linkEvent(
698                       this,
699                       this.handleUserSettingsNewPasswordVerifyChange
700                     )}
701                   />
702                 </div>
703               </div>
704               <div class="form-group row">
705                 <label
706                   class="col-lg-5 col-form-label"
707                   htmlFor="user-old-password"
708                 >
709                   {i18n.t('old_password')}
710                 </label>
711                 <div class="col-lg-7">
712                   <input
713                     type="password"
714                     id="user-old-password"
715                     class="form-control"
716                     value={this.state.userSettingsForm.old_password}
717                     autoComplete="new-password"
718                     onInput={linkEvent(
719                       this,
720                       this.handleUserSettingsOldPasswordChange
721                     )}
722                   />
723                 </div>
724               </div>
725               {this.state.site.enable_nsfw && (
726                 <div class="form-group">
727                   <div class="form-check">
728                     <input
729                       class="form-check-input"
730                       id="user-show-nsfw"
731                       type="checkbox"
732                       checked={this.state.userSettingsForm.show_nsfw}
733                       onChange={linkEvent(
734                         this,
735                         this.handleUserSettingsShowNsfwChange
736                       )}
737                     />
738                     <label class="form-check-label" htmlFor="user-show-nsfw">
739                       {i18n.t('show_nsfw')}
740                     </label>
741                   </div>
742                 </div>
743               )}
744               <div class="form-group">
745                 <div class="form-check">
746                   <input
747                     class="form-check-input"
748                     id="user-show-avatars"
749                     type="checkbox"
750                     checked={this.state.userSettingsForm.show_avatars}
751                     onChange={linkEvent(
752                       this,
753                       this.handleUserSettingsShowAvatarsChange
754                     )}
755                   />
756                   <label class="form-check-label" htmlFor="user-show-avatars">
757                     {i18n.t('show_avatars')}
758                   </label>
759                 </div>
760               </div>
761               <div class="form-group">
762                 <div class="form-check">
763                   <input
764                     class="form-check-input"
765                     id="user-send-notifications-to-email"
766                     type="checkbox"
767                     disabled={!this.state.user.email}
768                     checked={
769                       this.state.userSettingsForm.send_notifications_to_email
770                     }
771                     onChange={linkEvent(
772                       this,
773                       this.handleUserSettingsSendNotificationsToEmailChange
774                     )}
775                   />
776                   <label
777                     class="form-check-label"
778                     htmlFor="user-send-notifications-to-email"
779                   >
780                     {i18n.t('send_notifications_to_email')}
781                   </label>
782                 </div>
783               </div>
784               <div class="form-group">
785                 <button type="submit" class="btn btn-block btn-secondary mr-4">
786                   {this.state.userSettingsLoading ? (
787                     <svg class="icon icon-spinner spin">
788                       <use xlinkHref="#icon-spinner"></use>
789                     </svg>
790                   ) : (
791                     capitalizeFirstLetter(i18n.t('save'))
792                   )}
793                 </button>
794               </div>
795               <hr />
796               <div class="form-group mb-0">
797                 <button
798                   class="btn btn-block btn-danger"
799                   onClick={linkEvent(
800                     this,
801                     this.handleDeleteAccountShowConfirmToggle
802                   )}
803                 >
804                   {i18n.t('delete_account')}
805                 </button>
806                 {this.state.deleteAccountShowConfirm && (
807                   <>
808                     <div class="my-2 alert alert-danger" role="alert">
809                       {i18n.t('delete_account_confirm')}
810                     </div>
811                     <input
812                       type="password"
813                       value={this.state.deleteAccountForm.password}
814                       autoComplete="new-password"
815                       onInput={linkEvent(
816                         this,
817                         this.handleDeleteAccountPasswordChange
818                       )}
819                       class="form-control my-2"
820                     />
821                     <button
822                       class="btn btn-danger mr-4"
823                       disabled={!this.state.deleteAccountForm.password}
824                       onClick={linkEvent(this, this.handleDeleteAccount)}
825                     >
826                       {this.state.deleteAccountLoading ? (
827                         <svg class="icon icon-spinner spin">
828                           <use xlinkHref="#icon-spinner"></use>
829                         </svg>
830                       ) : (
831                         capitalizeFirstLetter(i18n.t('delete'))
832                       )}
833                     </button>
834                     <button
835                       class="btn btn-secondary"
836                       onClick={linkEvent(
837                         this,
838                         this.handleDeleteAccountShowConfirmToggle
839                       )}
840                     >
841                       {i18n.t('cancel')}
842                     </button>
843                   </>
844                 )}
845               </div>
846             </form>
847           </div>
848         </div>
849       </div>
850     );
851   }
852
853   moderates() {
854     return (
855       <div>
856         {this.state.moderates.length > 0 && (
857           <div class="card border-secondary mb-3">
858             <div class="card-body">
859               <h5>{i18n.t('moderates')}</h5>
860               <ul class="list-unstyled mb-0">
861                 {this.state.moderates.map(community => (
862                   <li>
863                     <Link to={`/c/${community.community_name}`}>
864                       {community.community_name}
865                     </Link>
866                   </li>
867                 ))}
868               </ul>
869             </div>
870           </div>
871         )}
872       </div>
873     );
874   }
875
876   follows() {
877     return (
878       <div>
879         {this.state.follows.length > 0 && (
880           <div class="card border-secondary mb-3">
881             <div class="card-body">
882               <h5>{i18n.t('subscribed')}</h5>
883               <ul class="list-unstyled mb-0">
884                 {this.state.follows.map(community => (
885                   <li>
886                     <Link to={`/c/${community.community_name}`}>
887                       {community.community_name}
888                     </Link>
889                   </li>
890                 ))}
891               </ul>
892             </div>
893           </div>
894         )}
895       </div>
896     );
897   }
898
899   paginator() {
900     return (
901       <div class="my-2">
902         {this.state.page > 1 && (
903           <button
904             class="btn btn-sm btn-secondary mr-1"
905             onClick={linkEvent(this, this.prevPage)}
906           >
907             {i18n.t('prev')}
908           </button>
909         )}
910         {this.state.comments.length + this.state.posts.length > 0 && (
911           <button
912             class="btn btn-sm btn-secondary"
913             onClick={linkEvent(this, this.nextPage)}
914           >
915             {i18n.t('next')}
916           </button>
917         )}
918       </div>
919     );
920   }
921
922   updateUrl() {
923     let viewStr = View[this.state.view].toLowerCase();
924     let sortStr = SortType[this.state.sort].toLowerCase();
925     this.props.history.push(
926       `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
927     );
928   }
929
930   nextPage(i: User) {
931     i.state.page++;
932     i.setState(i.state);
933     i.updateUrl();
934     i.refetch();
935   }
936
937   prevPage(i: User) {
938     i.state.page--;
939     i.setState(i.state);
940     i.updateUrl();
941     i.refetch();
942   }
943
944   refetch() {
945     let form: GetUserDetailsForm = {
946       user_id: this.state.user_id,
947       username: this.state.username,
948       sort: SortType[this.state.sort],
949       saved_only: this.state.view == View.Saved,
950       page: this.state.page,
951       limit: fetchLimit,
952     };
953     WebSocketService.Instance.getUserDetails(form);
954   }
955
956   handleSortChange(val: SortType) {
957     this.state.sort = val;
958     this.state.page = 1;
959     this.setState(this.state);
960     this.updateUrl();
961     this.refetch();
962   }
963
964   handleViewChange(i: User, event: any) {
965     i.state.view = Number(event.target.value);
966     i.state.page = 1;
967     i.setState(i.state);
968     i.updateUrl();
969     i.refetch();
970   }
971
972   handleUserSettingsShowNsfwChange(i: User, event: any) {
973     i.state.userSettingsForm.show_nsfw = event.target.checked;
974     i.setState(i.state);
975   }
976
977   handleUserSettingsShowAvatarsChange(i: User, event: any) {
978     i.state.userSettingsForm.show_avatars = event.target.checked;
979     UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
980     i.setState(i.state);
981   }
982
983   handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
984     i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
985     i.setState(i.state);
986   }
987
988   handleUserSettingsThemeChange(i: User, event: any) {
989     i.state.userSettingsForm.theme = event.target.value;
990     setTheme(event.target.value, true);
991     i.setState(i.state);
992   }
993
994   handleUserSettingsLangChange(i: User, event: any) {
995     i.state.userSettingsForm.lang = event.target.value;
996     i18n.changeLanguage(i.state.userSettingsForm.lang);
997     i.setState(i.state);
998   }
999
1000   handleUserSettingsSortTypeChange(val: SortType) {
1001     this.state.userSettingsForm.default_sort_type = val;
1002     this.setState(this.state);
1003   }
1004
1005   handleUserSettingsListingTypeChange(val: ListingType) {
1006     this.state.userSettingsForm.default_listing_type = val;
1007     this.setState(this.state);
1008   }
1009
1010   handleUserSettingsEmailChange(i: User, event: any) {
1011     i.state.userSettingsForm.email = event.target.value;
1012     if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
1013       i.state.userSettingsForm.email = undefined;
1014     }
1015     i.setState(i.state);
1016   }
1017
1018   handleUserSettingsMatrixUserIdChange(i: User, event: any) {
1019     i.state.userSettingsForm.matrix_user_id = event.target.value;
1020     if (
1021       i.state.userSettingsForm.matrix_user_id == '' &&
1022       !i.state.user.matrix_user_id
1023     ) {
1024       i.state.userSettingsForm.matrix_user_id = undefined;
1025     }
1026     i.setState(i.state);
1027   }
1028
1029   handleUserSettingsNewPasswordChange(i: User, event: any) {
1030     i.state.userSettingsForm.new_password = event.target.value;
1031     if (i.state.userSettingsForm.new_password == '') {
1032       i.state.userSettingsForm.new_password = undefined;
1033     }
1034     i.setState(i.state);
1035   }
1036
1037   handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
1038     i.state.userSettingsForm.new_password_verify = event.target.value;
1039     if (i.state.userSettingsForm.new_password_verify == '') {
1040       i.state.userSettingsForm.new_password_verify = undefined;
1041     }
1042     i.setState(i.state);
1043   }
1044
1045   handleUserSettingsOldPasswordChange(i: User, event: any) {
1046     i.state.userSettingsForm.old_password = event.target.value;
1047     if (i.state.userSettingsForm.old_password == '') {
1048       i.state.userSettingsForm.old_password = undefined;
1049     }
1050     i.setState(i.state);
1051   }
1052
1053   handleImageUpload(i: User, event: any) {
1054     event.preventDefault();
1055     let file = event.target.files[0];
1056     const imageUploadUrl = `/pictrs/image`;
1057     const formData = new FormData();
1058     formData.append('images[]', file);
1059
1060     i.state.avatarLoading = true;
1061     i.setState(i.state);
1062
1063     fetch(imageUploadUrl, {
1064       method: 'POST',
1065       body: formData,
1066     })
1067       .then(res => res.json())
1068       .then(res => {
1069         console.log('pictrs upload:');
1070         console.log(res);
1071         if (res.msg == 'ok') {
1072           let hash = res.files[0].file;
1073           let url = `${window.location.origin}/pictrs/image/${hash}`;
1074           i.state.userSettingsForm.avatar = url;
1075           i.state.avatarLoading = false;
1076           i.setState(i.state);
1077         } else {
1078           i.state.avatarLoading = false;
1079           i.setState(i.state);
1080           toast(JSON.stringify(res), 'danger');
1081         }
1082       })
1083       .catch(error => {
1084         i.state.avatarLoading = false;
1085         i.setState(i.state);
1086         toast(error, 'danger');
1087       });
1088   }
1089
1090   removeAvatar(i: User, event: any) {
1091     event.preventDefault();
1092     i.state.userSettingsLoading = true;
1093     i.state.userSettingsForm.avatar = '';
1094     i.setState(i.state);
1095
1096     WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
1097   }
1098
1099   get checkSettingsAvatar(): boolean {
1100     return (
1101       this.state.userSettingsForm.avatar &&
1102       this.state.userSettingsForm.avatar != ''
1103     );
1104   }
1105
1106   handleUserSettingsSubmit(i: User, event: any) {
1107     event.preventDefault();
1108     i.state.userSettingsLoading = true;
1109     i.setState(i.state);
1110
1111     WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
1112   }
1113
1114   handleDeleteAccountShowConfirmToggle(i: User, event: any) {
1115     event.preventDefault();
1116     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
1117     i.setState(i.state);
1118   }
1119
1120   handleDeleteAccountPasswordChange(i: User, event: any) {
1121     i.state.deleteAccountForm.password = event.target.value;
1122     i.setState(i.state);
1123   }
1124
1125   handleLogoutClick(i: User) {
1126     UserService.Instance.logout();
1127     i.context.router.history.push('/');
1128   }
1129
1130   handleDeleteAccount(i: User, event: any) {
1131     event.preventDefault();
1132     i.state.deleteAccountLoading = true;
1133     i.setState(i.state);
1134
1135     WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
1136   }
1137
1138   parseMessage(msg: WebSocketJsonResponse) {
1139     console.log(msg);
1140     let res = wsJsonToRes(msg);
1141     if (msg.error) {
1142       toast(i18n.t(msg.error), 'danger');
1143       this.state.deleteAccountLoading = false;
1144       this.state.avatarLoading = false;
1145       this.state.userSettingsLoading = false;
1146       if (msg.error == 'couldnt_find_that_username_or_email') {
1147         this.context.router.history.push('/');
1148       }
1149       this.setState(this.state);
1150       return;
1151     } else if (msg.reconnect) {
1152       this.refetch();
1153     } else if (res.op == UserOperation.GetUserDetails) {
1154       let data = res.data as UserDetailsResponse;
1155       this.state.user = data.user;
1156       this.state.comments = data.comments;
1157       this.state.follows = data.follows;
1158       this.state.moderates = data.moderates;
1159       this.state.posts = data.posts;
1160       this.state.admins = data.admins;
1161       this.state.loading = false;
1162       if (this.isCurrentUser) {
1163         this.state.userSettingsForm.show_nsfw =
1164           UserService.Instance.user.show_nsfw;
1165         this.state.userSettingsForm.theme = UserService.Instance.user.theme
1166           ? UserService.Instance.user.theme
1167           : 'darkly';
1168         this.state.userSettingsForm.default_sort_type =
1169           UserService.Instance.user.default_sort_type;
1170         this.state.userSettingsForm.default_listing_type =
1171           UserService.Instance.user.default_listing_type;
1172         this.state.userSettingsForm.lang = UserService.Instance.user.lang;
1173         this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
1174         this.state.userSettingsForm.email = this.state.user.email;
1175         this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
1176         this.state.userSettingsForm.show_avatars =
1177           UserService.Instance.user.show_avatars;
1178         this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
1179       }
1180       document.title = `/u/${this.state.user.name} - ${this.state.site.name}`;
1181       window.scrollTo(0, 0);
1182       this.setState(this.state);
1183       setupTippy();
1184     } else if (res.op == UserOperation.EditComment) {
1185       let data = res.data as CommentResponse;
1186       editCommentRes(data, this.state.comments);
1187       this.setState(this.state);
1188     } else if (res.op == UserOperation.CreateComment) {
1189       let data = res.data as CommentResponse;
1190       if (
1191         UserService.Instance.user &&
1192         data.comment.creator_id == UserService.Instance.user.id
1193       ) {
1194         toast(i18n.t('reply_sent'));
1195       }
1196     } else if (res.op == UserOperation.SaveComment) {
1197       let data = res.data as CommentResponse;
1198       saveCommentRes(data, this.state.comments);
1199       this.setState(this.state);
1200     } else if (res.op == UserOperation.CreateCommentLike) {
1201       let data = res.data as CommentResponse;
1202       createCommentLikeRes(data, this.state.comments);
1203       this.setState(this.state);
1204     } else if (res.op == UserOperation.CreatePostLike) {
1205       let data = res.data as PostResponse;
1206       createPostLikeFindRes(data, this.state.posts);
1207       this.setState(this.state);
1208     } else if (res.op == UserOperation.BanUser) {
1209       let data = res.data as BanUserResponse;
1210       this.state.comments
1211         .filter(c => c.creator_id == data.user.id)
1212         .forEach(c => (c.banned = data.banned));
1213       this.state.posts
1214         .filter(c => c.creator_id == data.user.id)
1215         .forEach(c => (c.banned = data.banned));
1216       this.setState(this.state);
1217     } else if (res.op == UserOperation.AddAdmin) {
1218       let data = res.data as AddAdminResponse;
1219       this.state.admins = data.admins;
1220       this.setState(this.state);
1221     } else if (res.op == UserOperation.SaveUserSettings) {
1222       let data = res.data as LoginResponse;
1223       this.state.userSettingsLoading = false;
1224       this.setState(this.state);
1225       UserService.Instance.login(data);
1226     } else if (res.op == UserOperation.DeleteAccount) {
1227       this.state.deleteAccountLoading = false;
1228       this.state.deleteAccountShowConfirm = false;
1229       this.setState(this.state);
1230       this.context.router.history.push('/');
1231     } else if (res.op == UserOperation.GetSite) {
1232       let data = res.data as GetSiteResponse;
1233       this.state.site = data.site;
1234       this.setState(this.state);
1235     }
1236   }
1237 }