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