]> Untitled Git - lemmy-ui.git/blob - src/shared/components/user.tsx
Add post, inbox, and user routes.
[lemmy-ui.git] / src / shared / 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 {
6   UserOperation,
7   SortType,
8   ListingType,
9   UserSettingsForm,
10   LoginResponse,
11   DeleteAccountForm,
12   WebSocketJsonResponse,
13   GetSiteResponse,
14   UserDetailsResponse,
15   AddAdminResponse,
16   GetUserDetailsForm,
17   CommentResponse,
18   PostResponse,
19   BanUserResponse,
20 } from 'lemmy-js-client';
21 import { UserDetailsView } from '../interfaces';
22 import { WebSocketService, UserService } from '../services';
23 import {
24   wsJsonToRes,
25   fetchLimit,
26   routeSortTypeToEnum,
27   capitalizeFirstLetter,
28   themes,
29   setTheme,
30   languages,
31   toast,
32   setupTippy,
33   getLanguage,
34   mdToHtml,
35   elementUrl,
36   favIconUrl,
37   setIsoData,
38   getIdFromProps,
39   getUsernameFromProps,
40   wsSubscribe,
41   createCommentLikeRes,
42   editCommentRes,
43   saveCommentRes,
44   createPostLikeFindRes,
45   setAuth,
46   lemmyHttp,
47 } from '../utils';
48 import { UserListing } from './user-listing';
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
59 interface UserState {
60   userRes: UserDetailsResponse;
61   userId: number;
62   userName: string;
63   view: UserDetailsView;
64   sort: SortType;
65   page: number;
66   loading: boolean;
67   userSettingsForm: UserSettingsForm;
68   userSettingsLoading: boolean;
69   deleteAccountLoading: boolean;
70   deleteAccountShowConfirm: boolean;
71   deleteAccountForm: DeleteAccountForm;
72   siteRes: GetSiteResponse;
73 }
74
75 interface UserProps {
76   view: UserDetailsView;
77   sort: SortType;
78   page: number;
79   user_id: number | null;
80   username: string;
81 }
82
83 interface UrlParams {
84   view?: string;
85   sort?: SortType;
86   page?: number;
87 }
88
89 export class User extends Component<any, UserState> {
90   private isoData = setIsoData(this.context);
91   private subscription: Subscription;
92   private emptyState: UserState = {
93     userRes: undefined,
94     userId: getIdFromProps(this.props),
95     userName: getUsernameFromProps(this.props),
96     loading: true,
97     view: User.getViewFromProps(this.props.match.view),
98     sort: User.getSortTypeFromProps(this.props.match.sort),
99     page: User.getPageFromProps(this.props.match.page),
100     userSettingsForm: {
101       show_nsfw: null,
102       theme: null,
103       default_sort_type: null,
104       default_listing_type: null,
105       lang: null,
106       show_avatars: null,
107       send_notifications_to_email: null,
108       auth: null,
109       bio: null,
110       preferred_username: null,
111     },
112     userSettingsLoading: null,
113     deleteAccountLoading: null,
114     deleteAccountShowConfirm: false,
115     deleteAccountForm: {
116       password: null,
117     },
118     siteRes: this.isoData.site,
119   };
120
121   constructor(props: any, context: any) {
122     super(props, context);
123
124     this.state = this.emptyState;
125     this.handleSortChange = this.handleSortChange.bind(this);
126     this.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind(
127       this
128     );
129     this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
130       this
131     );
132     this.handlePageChange = this.handlePageChange.bind(this);
133     this.handleUserSettingsBioChange = this.handleUserSettingsBioChange.bind(
134       this
135     );
136
137     this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
138     this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
139
140     this.handleBannerUpload = this.handleBannerUpload.bind(this);
141     this.handleBannerRemove = this.handleBannerRemove.bind(this);
142
143     this.parseMessage = this.parseMessage.bind(this);
144     this.subscription = wsSubscribe(this.parseMessage);
145
146     // Only fetch the data if coming from another route
147     if (this.isoData.path == this.context.router.route.match.url) {
148       this.state.userRes = this.isoData.routeData[0];
149       this.setUserInfo();
150       this.state.loading = false;
151     } else {
152       this.fetchUserData();
153     }
154
155     setupTippy();
156   }
157
158   fetchUserData() {
159     let form: GetUserDetailsForm = {
160       user_id: this.state.userId,
161       username: this.state.userName,
162       sort: this.state.sort,
163       saved_only: this.state.view === UserDetailsView.Saved,
164       page: this.state.page,
165       limit: fetchLimit,
166     };
167     WebSocketService.Instance.getUserDetails(form);
168   }
169
170   get isCurrentUser() {
171     return (
172       UserService.Instance.user &&
173       UserService.Instance.user.id == this.state.userRes.user.id
174     );
175   }
176
177   static getViewFromProps(view: string): UserDetailsView {
178     return view ? UserDetailsView[view] : UserDetailsView.Overview;
179   }
180
181   static getSortTypeFromProps(sort: string): SortType {
182     return sort ? routeSortTypeToEnum(sort) : SortType.New;
183   }
184
185   static getPageFromProps(page: number): number {
186     return page ? Number(page) : 1;
187   }
188
189   static fetchInitialData(auth: string, path: string): Promise<any>[] {
190     let pathSplit = path.split('/');
191     let promises: Promise<any>[] = [];
192
193     // It can be /u/me, or /username/1
194     let idOrName = pathSplit[2];
195     let user_id: number;
196     let username: string;
197     if (isNaN(Number(idOrName))) {
198       username = idOrName;
199     } else {
200       user_id = Number(idOrName);
201     }
202
203     let view = this.getViewFromProps(pathSplit[4]);
204     let sort = this.getSortTypeFromProps(pathSplit[6]);
205     let page = this.getPageFromProps(Number(pathSplit[8]));
206
207     let form: GetUserDetailsForm = {
208       sort,
209       saved_only: view === UserDetailsView.Saved,
210       page,
211       limit: fetchLimit,
212     };
213     this.setIdOrName(form, user_id, username);
214     setAuth(form, auth);
215     promises.push(lemmyHttp.getUserDetails(form));
216     return promises;
217   }
218
219   static setIdOrName(obj: any, id: number, name_: string) {
220     if (id) {
221       obj.user_id = id;
222     } else {
223       obj.username = name_;
224     }
225   }
226
227   componentWillUnmount() {
228     this.subscription.unsubscribe();
229   }
230
231   static getDerivedStateFromProps(props: any): UserProps {
232     return {
233       view: this.getViewFromProps(props.match.params.view),
234       sort: this.getSortTypeFromProps(props.match.params.sort),
235       page: this.getPageFromProps(props.match.params.page),
236       user_id: Number(props.match.params.id) || null,
237       username: props.match.params.username,
238     };
239   }
240
241   componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
242     // Necessary if you are on a post and you click another post (same route)
243     if (
244       lastProps.location.pathname.split('/')[2] !==
245       lastProps.history.location.pathname.split('/')[2]
246     ) {
247       // Couldnt get a refresh working. This does for now.
248       location.reload();
249     }
250   }
251
252   get documentTitle(): string {
253     if (this.state.siteRes.site.name) {
254       return `@${this.state.userName} - ${this.state.siteRes.site.name}`;
255     } else {
256       return 'Lemmy';
257     }
258   }
259
260   get favIcon(): string {
261     return this.state.siteRes.site.icon
262       ? this.state.siteRes.site.icon
263       : favIconUrl;
264   }
265
266   render() {
267     return (
268       <div class="container">
269         <Helmet title={this.documentTitle}>
270           <link
271             id="favicon"
272             rel="icon"
273             type="image/x-icon"
274             href={this.favIcon}
275           />
276         </Helmet>
277         {this.state.loading ? (
278           <h5>
279             <svg class="icon icon-spinner spin">
280               <use xlinkHref="#icon-spinner"></use>
281             </svg>
282           </h5>
283         ) : (
284           <div class="row">
285             <div class="col-12 col-md-8">
286               <>
287                 {this.userInfo()}
288                 <hr />
289               </>
290               {!this.state.loading && this.selects()}
291               <UserDetails
292                 userRes={this.state.userRes}
293                 sort={this.state.sort}
294                 page={this.state.page}
295                 limit={fetchLimit}
296                 enableDownvotes={this.state.siteRes.site.enable_downvotes}
297                 enableNsfw={this.state.siteRes.site.enable_nsfw}
298                 view={this.state.view}
299                 onPageChange={this.handlePageChange}
300               />
301             </div>
302
303             {!this.state.loading && (
304               <div class="col-12 col-md-4">
305                 {this.isCurrentUser && this.userSettings()}
306                 {this.moderates()}
307                 {this.follows()}
308               </div>
309             )}
310           </div>
311         )}
312       </div>
313     );
314   }
315
316   viewRadios() {
317     return (
318       <div class="btn-group btn-group-toggle flex-wrap mb-2">
319         <label
320           className={`btn btn-outline-secondary pointer
321             ${this.state.view == UserDetailsView.Overview && 'active'}
322           `}
323         >
324           <input
325             type="radio"
326             value={UserDetailsView.Overview}
327             checked={this.state.view === UserDetailsView.Overview}
328             onChange={linkEvent(this, this.handleViewChange)}
329           />
330           {i18n.t('overview')}
331         </label>
332         <label
333           className={`btn btn-outline-secondary pointer
334             ${this.state.view == UserDetailsView.Comments && 'active'}
335           `}
336         >
337           <input
338             type="radio"
339             value={UserDetailsView.Comments}
340             checked={this.state.view == UserDetailsView.Comments}
341             onChange={linkEvent(this, this.handleViewChange)}
342           />
343           {i18n.t('comments')}
344         </label>
345         <label
346           className={`btn btn-outline-secondary pointer
347             ${this.state.view == UserDetailsView.Posts && 'active'}
348           `}
349         >
350           <input
351             type="radio"
352             value={UserDetailsView.Posts}
353             checked={this.state.view == UserDetailsView.Posts}
354             onChange={linkEvent(this, this.handleViewChange)}
355           />
356           {i18n.t('posts')}
357         </label>
358         <label
359           className={`btn btn-outline-secondary pointer
360             ${this.state.view == UserDetailsView.Saved && 'active'}
361           `}
362         >
363           <input
364             type="radio"
365             value={UserDetailsView.Saved}
366             checked={this.state.view == UserDetailsView.Saved}
367             onChange={linkEvent(this, this.handleViewChange)}
368           />
369           {i18n.t('saved')}
370         </label>
371       </div>
372     );
373   }
374
375   selects() {
376     return (
377       <div className="mb-2">
378         <span class="mr-3">{this.viewRadios()}</span>
379         <SortSelect
380           sort={this.state.sort}
381           onChange={this.handleSortChange}
382           hideHot
383         />
384         <a
385           href={`/feeds/u/${this.state.userName}.xml?sort=${this.state.sort}`}
386           target="_blank"
387           rel="noopener"
388           title="RSS"
389         >
390           <svg class="icon mx-2 text-muted small">
391             <use xlinkHref="#icon-rss">#</use>
392           </svg>
393         </a>
394       </div>
395     );
396   }
397
398   userInfo() {
399     let user = this.state.userRes.user;
400
401     return (
402       <div>
403         <BannerIconHeader banner={user.banner} icon={user.avatar} />
404         <div class="mb-3">
405           <div class="">
406             <div class="mb-0 d-flex flex-wrap">
407               <div>
408                 {user.preferred_username && (
409                   <h5 class="mb-0">{user.preferred_username}</h5>
410                 )}
411                 <ul class="list-inline mb-2">
412                   <li className="list-inline-item">
413                     <UserListing
414                       user={user}
415                       realLink
416                       useApubName
417                       muted
418                       hideAvatar
419                     />
420                   </li>
421                   {user.banned && (
422                     <li className="list-inline-item badge badge-danger">
423                       {i18n.t('banned')}
424                     </li>
425                   )}
426                 </ul>
427               </div>
428               <div className="flex-grow-1 unselectable pointer mx-2"></div>
429               {this.isCurrentUser ? (
430                 <button
431                   class="d-flex align-self-start btn btn-secondary ml-2"
432                   onClick={linkEvent(this, this.handleLogoutClick)}
433                 >
434                   {i18n.t('logout')}
435                 </button>
436               ) : (
437                 <>
438                   <a
439                     className={`d-flex align-self-start btn btn-secondary ml-2 ${
440                       !user.matrix_user_id && 'invisible'
441                     }`}
442                     target="_blank"
443                     rel="noopener"
444                     href={`https://matrix.to/#/${user.matrix_user_id}`}
445                   >
446                     {i18n.t('send_secure_message')}
447                   </a>
448                   <Link
449                     class="d-flex align-self-start btn btn-secondary ml-2"
450                     to={`/create_private_message/recipient/${user.id}`}
451                   >
452                     {i18n.t('send_message')}
453                   </Link>
454                 </>
455               )}
456             </div>
457             {user.bio && (
458               <div className="d-flex align-items-center mb-2">
459                 <div
460                   className="md-div"
461                   dangerouslySetInnerHTML={mdToHtml(user.bio)}
462                 />
463               </div>
464             )}
465             <div>
466               <ul class="list-inline mb-2">
467                 <li className="list-inline-item badge badge-light">
468                   {i18n.t('number_of_posts', { count: user.number_of_posts })}
469                 </li>
470                 <li className="list-inline-item badge badge-light">
471                   {i18n.t('number_of_comments', {
472                     count: user.number_of_comments,
473                   })}
474                 </li>
475               </ul>
476             </div>
477             <div class="text-muted">
478               {i18n.t('joined')} <MomentTime data={user} showAgo />
479             </div>
480             <div className="d-flex align-items-center text-muted mb-2">
481               <svg class="icon">
482                 <use xlinkHref="#icon-cake"></use>
483               </svg>
484               <span className="ml-2">
485                 {i18n.t('cake_day_title')}{' '}
486                 {moment.utc(user.published).local().format('MMM DD, YYYY')}
487               </span>
488             </div>
489           </div>
490         </div>
491       </div>
492     );
493   }
494
495   userSettings() {
496     return (
497       <div>
498         <div class="card bg-transparent border-secondary mb-3">
499           <div class="card-body">
500             <h5>{i18n.t('settings')}</h5>
501             <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
502               <div class="form-group">
503                 <label>{i18n.t('avatar')}</label>
504                 <ImageUploadForm
505                   uploadTitle={i18n.t('upload_avatar')}
506                   imageSrc={this.state.userSettingsForm.avatar}
507                   onUpload={this.handleAvatarUpload}
508                   onRemove={this.handleAvatarRemove}
509                   rounded
510                 />
511               </div>
512               <div class="form-group">
513                 <label>{i18n.t('banner')}</label>
514                 <ImageUploadForm
515                   uploadTitle={i18n.t('upload_banner')}
516                   imageSrc={this.state.userSettingsForm.banner}
517                   onUpload={this.handleBannerUpload}
518                   onRemove={this.handleBannerRemove}
519                 />
520               </div>
521               <div class="form-group">
522                 <label>{i18n.t('language')}</label>
523                 <select
524                   value={this.state.userSettingsForm.lang}
525                   onChange={linkEvent(this, this.handleUserSettingsLangChange)}
526                   class="ml-2 custom-select w-auto"
527                 >
528                   <option disabled>{i18n.t('language')}</option>
529                   <option value="browser">{i18n.t('browser_default')}</option>
530                   <option disabled>──</option>
531                   {languages.map(lang => (
532                     <option value={lang.code}>{lang.name}</option>
533                   ))}
534                 </select>
535               </div>
536               <div class="form-group">
537                 <label>{i18n.t('theme')}</label>
538                 <select
539                   value={this.state.userSettingsForm.theme}
540                   onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
541                   class="ml-2 custom-select w-auto"
542                 >
543                   <option disabled>{i18n.t('theme')}</option>
544                   {themes.map(theme => (
545                     <option value={theme}>{theme}</option>
546                   ))}
547                 </select>
548               </div>
549               <form className="form-group">
550                 <label>
551                   <div class="mr-2">{i18n.t('sort_type')}</div>
552                 </label>
553                 <ListingTypeSelect
554                   type_={
555                     Object.values(ListingType)[
556                       this.state.userSettingsForm.default_listing_type
557                     ]
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 bg-transparent 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                     <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   follows() {
859     return (
860       <div>
861         {this.state.userRes.follows.length > 0 && (
862           <div class="card bg-transparent border-secondary mb-3">
863             <div class="card-body">
864               <h5>{i18n.t('subscribed')}</h5>
865               <ul class="list-unstyled mb-0">
866                 {this.state.userRes.follows.map(community => (
867                   <li>
868                     <Link to={`/c/${community.community_name}`}>
869                       {community.community_name}
870                     </Link>
871                   </li>
872                 ))}
873               </ul>
874             </div>
875           </div>
876         )}
877       </div>
878     );
879   }
880
881   updateUrl(paramUpdates: UrlParams) {
882     const page = paramUpdates.page || this.state.page;
883     const viewStr = paramUpdates.view || UserDetailsView[this.state.view];
884     const sortStr = paramUpdates.sort || this.state.sort;
885     this.props.history.push(
886       `/u/${this.state.userName}/view/${viewStr}/sort/${sortStr}/page/${page}`
887     );
888   }
889
890   handlePageChange(page: number) {
891     this.updateUrl({ page });
892   }
893
894   handleSortChange(val: SortType) {
895     this.updateUrl({ sort: val, page: 1 });
896   }
897
898   handleViewChange(i: User, event: any) {
899     i.updateUrl({
900       view: UserDetailsView[Number(event.target.value)],
901       page: 1,
902     });
903   }
904
905   handleUserSettingsShowNsfwChange(i: User, event: any) {
906     i.state.userSettingsForm.show_nsfw = event.target.checked;
907     i.setState(i.state);
908   }
909
910   handleUserSettingsShowAvatarsChange(i: User, event: any) {
911     i.state.userSettingsForm.show_avatars = event.target.checked;
912     UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
913     i.setState(i.state);
914   }
915
916   handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
917     i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
918     i.setState(i.state);
919   }
920
921   handleUserSettingsThemeChange(i: User, event: any) {
922     i.state.userSettingsForm.theme = event.target.value;
923     setTheme(event.target.value, true);
924     i.setState(i.state);
925   }
926
927   handleUserSettingsLangChange(i: User, event: any) {
928     i.state.userSettingsForm.lang = event.target.value;
929     i18n.changeLanguage(getLanguage(i.state.userSettingsForm.lang));
930     i.setState(i.state);
931   }
932
933   handleUserSettingsSortTypeChange(val: SortType) {
934     this.state.userSettingsForm.default_sort_type = Object.keys(
935       SortType
936     ).indexOf(val);
937     this.setState(this.state);
938   }
939
940   handleUserSettingsListingTypeChange(val: ListingType) {
941     this.state.userSettingsForm.default_listing_type = Object.keys(
942       ListingType
943     ).indexOf(val);
944     this.setState(this.state);
945   }
946
947   handleUserSettingsEmailChange(i: User, event: any) {
948     i.state.userSettingsForm.email = event.target.value;
949     i.setState(i.state);
950   }
951
952   handleUserSettingsBioChange(val: string) {
953     this.state.userSettingsForm.bio = val;
954     this.setState(this.state);
955   }
956
957   handleAvatarUpload(url: string) {
958     this.state.userSettingsForm.avatar = url;
959     this.setState(this.state);
960   }
961
962   handleAvatarRemove() {
963     this.state.userSettingsForm.avatar = '';
964     this.setState(this.state);
965   }
966
967   handleBannerUpload(url: string) {
968     this.state.userSettingsForm.banner = url;
969     this.setState(this.state);
970   }
971
972   handleBannerRemove() {
973     this.state.userSettingsForm.banner = '';
974     this.setState(this.state);
975   }
976
977   handleUserSettingsPreferredUsernameChange(i: User, event: any) {
978     i.state.userSettingsForm.preferred_username = event.target.value;
979     i.setState(i.state);
980   }
981
982   handleUserSettingsMatrixUserIdChange(i: User, event: any) {
983     i.state.userSettingsForm.matrix_user_id = event.target.value;
984     if (
985       i.state.userSettingsForm.matrix_user_id == '' &&
986       !i.state.userRes.user.matrix_user_id
987     ) {
988       i.state.userSettingsForm.matrix_user_id = undefined;
989     }
990     i.setState(i.state);
991   }
992
993   handleUserSettingsNewPasswordChange(i: User, event: any) {
994     i.state.userSettingsForm.new_password = event.target.value;
995     if (i.state.userSettingsForm.new_password == '') {
996       i.state.userSettingsForm.new_password = undefined;
997     }
998     i.setState(i.state);
999   }
1000
1001   handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
1002     i.state.userSettingsForm.new_password_verify = event.target.value;
1003     if (i.state.userSettingsForm.new_password_verify == '') {
1004       i.state.userSettingsForm.new_password_verify = undefined;
1005     }
1006     i.setState(i.state);
1007   }
1008
1009   handleUserSettingsOldPasswordChange(i: User, event: any) {
1010     i.state.userSettingsForm.old_password = event.target.value;
1011     if (i.state.userSettingsForm.old_password == '') {
1012       i.state.userSettingsForm.old_password = undefined;
1013     }
1014     i.setState(i.state);
1015   }
1016
1017   handleUserSettingsSubmit(i: User, event: any) {
1018     event.preventDefault();
1019     i.state.userSettingsLoading = true;
1020     i.setState(i.state);
1021
1022     WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
1023   }
1024
1025   handleDeleteAccountShowConfirmToggle(i: User, event: any) {
1026     event.preventDefault();
1027     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
1028     i.setState(i.state);
1029   }
1030
1031   handleDeleteAccountPasswordChange(i: User, event: any) {
1032     i.state.deleteAccountForm.password = event.target.value;
1033     i.setState(i.state);
1034   }
1035
1036   handleLogoutClick(i: User) {
1037     UserService.Instance.logout();
1038     i.context.router.history.push('/');
1039   }
1040
1041   handleDeleteAccount(i: User, event: any) {
1042     event.preventDefault();
1043     i.state.deleteAccountLoading = true;
1044     i.setState(i.state);
1045
1046     WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
1047   }
1048
1049   setUserInfo() {
1050     if (this.isCurrentUser) {
1051       this.state.userSettingsForm.show_nsfw =
1052         UserService.Instance.user.show_nsfw;
1053       this.state.userSettingsForm.theme = UserService.Instance.user.theme
1054         ? UserService.Instance.user.theme
1055         : 'darkly';
1056       this.state.userSettingsForm.default_sort_type =
1057         UserService.Instance.user.default_sort_type;
1058       this.state.userSettingsForm.default_listing_type =
1059         UserService.Instance.user.default_listing_type;
1060       this.state.userSettingsForm.lang = UserService.Instance.user.lang;
1061       this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
1062       this.state.userSettingsForm.banner = UserService.Instance.user.banner;
1063       this.state.userSettingsForm.preferred_username =
1064         UserService.Instance.user.preferred_username;
1065       this.state.userSettingsForm.show_avatars =
1066         UserService.Instance.user.show_avatars;
1067       this.state.userSettingsForm.email = UserService.Instance.user.email;
1068       this.state.userSettingsForm.bio = UserService.Instance.user.bio;
1069       this.state.userSettingsForm.send_notifications_to_email =
1070         UserService.Instance.user.send_notifications_to_email;
1071       this.state.userSettingsForm.matrix_user_id =
1072         UserService.Instance.user.matrix_user_id;
1073     }
1074   }
1075
1076   parseMessage(msg: WebSocketJsonResponse) {
1077     console.log(msg);
1078     const res = wsJsonToRes(msg);
1079     if (msg.error) {
1080       toast(i18n.t(msg.error), 'danger');
1081       if (msg.error == 'couldnt_find_that_username_or_email') {
1082         this.context.router.history.push('/');
1083       }
1084       this.setState({
1085         deleteAccountLoading: false,
1086         userSettingsLoading: false,
1087       });
1088       return;
1089     } else if (msg.reconnect) {
1090       this.fetchUserData();
1091     } else if (res.op == UserOperation.GetUserDetails) {
1092       // Since the UserDetails contains posts/comments as well as some general user info we listen here as well
1093       // and set the parent state if it is not set or differs
1094       // TODO this might need to get abstracted
1095       const data = res.data as UserDetailsResponse;
1096       this.state.userRes = data;
1097       this.setUserInfo();
1098       this.state.loading = false;
1099       this.setState(this.state);
1100     } else if (res.op == UserOperation.SaveUserSettings) {
1101       const data = res.data as LoginResponse;
1102       UserService.Instance.login(data);
1103       this.state.userRes.user.bio = this.state.userSettingsForm.bio;
1104       this.state.userRes.user.preferred_username = this.state.userSettingsForm.preferred_username;
1105       this.state.userRes.user.banner = this.state.userSettingsForm.banner;
1106       this.state.userRes.user.avatar = this.state.userSettingsForm.avatar;
1107       this.state.userSettingsLoading = false;
1108       this.setState(this.state);
1109
1110       window.scrollTo(0, 0);
1111     } else if (res.op == UserOperation.DeleteAccount) {
1112       this.setState({
1113         deleteAccountLoading: false,
1114         deleteAccountShowConfirm: false,
1115       });
1116       this.context.router.history.push('/');
1117     } else if (res.op == UserOperation.AddAdmin) {
1118       const data = res.data as AddAdminResponse;
1119       this.state.siteRes.admins = data.admins;
1120       this.setState(this.state);
1121     } else if (res.op == UserOperation.CreateCommentLike) {
1122       const data = res.data as CommentResponse;
1123       createCommentLikeRes(data, this.state.userRes.comments);
1124       this.setState(this.state);
1125     } else if (
1126       res.op == UserOperation.EditComment ||
1127       res.op == UserOperation.DeleteComment ||
1128       res.op == UserOperation.RemoveComment
1129     ) {
1130       const data = res.data as CommentResponse;
1131       editCommentRes(data, this.state.userRes.comments);
1132       this.setState(this.state);
1133     } else if (res.op == UserOperation.CreateComment) {
1134       const data = res.data as CommentResponse;
1135       if (
1136         UserService.Instance.user &&
1137         data.comment.creator_id == UserService.Instance.user.id
1138       ) {
1139         toast(i18n.t('reply_sent'));
1140       }
1141     } else if (res.op == UserOperation.SaveComment) {
1142       const data = res.data as CommentResponse;
1143       saveCommentRes(data, this.state.userRes.comments);
1144       this.setState(this.state);
1145     } else if (res.op == UserOperation.CreatePostLike) {
1146       const data = res.data as PostResponse;
1147       createPostLikeFindRes(data, this.state.userRes.posts);
1148       this.setState(this.state);
1149     } else if (res.op == UserOperation.BanUser) {
1150       const data = res.data as BanUserResponse;
1151       this.state.userRes.comments
1152         .filter(c => c.creator_id == data.user.id)
1153         .forEach(c => (c.banned = data.banned));
1154       this.state.userRes.posts
1155         .filter(c => c.creator_id == data.user.id)
1156         .forEach(c => (c.banned = data.banned));
1157       this.setState(this.state);
1158     }
1159   }
1160 }