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