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