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