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