1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { Subscription } from 'rxjs';
4 import { retryWhen, delay, take } from 'rxjs/operators';
14 WebSocketJsonResponse,
19 } from '../interfaces';
20 import { WebSocketService, UserService } from '../services';
25 capitalizeFirstLetter,
33 import { UserListing } from './user-listing';
34 import { SortSelect } from './sort-select';
35 import { ListingTypeSelect } from './listing-type-select';
36 import { MomentTime } from './moment-time';
37 import { i18n } from '../i18next';
38 import moment from 'moment';
39 import { UserDetails } from './user-details';
45 follows: Array<CommunityUser>;
46 moderates: Array<CommunityUser>;
47 view: UserDetailsView;
51 avatarLoading: boolean;
52 userSettingsForm: UserSettingsForm;
53 userSettingsLoading: boolean;
54 deleteAccountLoading: boolean;
55 deleteAccountShowConfirm: boolean;
56 deleteAccountForm: DeleteAccountForm;
61 view: UserDetailsView;
64 user_id: number | null;
74 export class User extends Component<any, UserState> {
75 private subscription: Subscription;
76 private emptyState: UserState = {
81 number_of_posts: null,
83 number_of_comments: null,
88 send_notifications_to_email: null,
98 view: User.getViewFromProps(this.props.match.view),
99 sort: User.getSortTypeFromProps(this.props.match.sort),
100 page: User.getPageFromProps(this.props.match.page),
104 default_sort_type: null,
105 default_listing_type: null,
108 send_notifications_to_email: null,
111 userSettingsLoading: null,
112 deleteAccountLoading: null,
113 deleteAccountShowConfirm: false,
120 creator_id: undefined,
121 published: undefined,
122 creator_name: undefined,
123 number_of_users: undefined,
124 number_of_posts: undefined,
125 number_of_comments: undefined,
126 number_of_communities: undefined,
127 enable_downvotes: undefined,
128 open_registration: undefined,
129 enable_nsfw: undefined,
133 constructor(props: any, context: any) {
134 super(props, context);
136 this.state = this.emptyState;
137 this.handleSortChange = this.handleSortChange.bind(this);
138 this.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind(
141 this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
144 this.handlePageChange = this.handlePageChange.bind(this);
146 this.state.user_id = Number(this.props.match.params.id) || null;
147 this.state.username = this.props.match.params.username;
149 this.subscription = WebSocketService.Instance.subject
150 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
152 msg => this.parseMessage(msg),
153 err => console.error(err),
154 () => console.log('complete')
157 WebSocketService.Instance.getSite();
160 get isCurrentUser() {
162 UserService.Instance.user &&
163 UserService.Instance.user.id == this.state.user.id
167 static getViewFromProps(view: any): UserDetailsView {
169 ? UserDetailsView[capitalizeFirstLetter(view)]
170 : UserDetailsView.Overview;
173 static getSortTypeFromProps(sort: any): SortType {
174 return sort ? routeSortTypeToEnum(sort) : SortType.New;
177 static getPageFromProps(page: any): number {
178 return page ? Number(page) : 1;
181 componentWillUnmount() {
182 this.subscription.unsubscribe();
185 static getDerivedStateFromProps(props: any): UserProps {
187 view: this.getViewFromProps(props.match.params.view),
188 sort: this.getSortTypeFromProps(props.match.params.sort),
189 page: this.getPageFromProps(props.match.params.page),
190 user_id: Number(props.match.params.id) || null,
191 username: props.match.params.username,
195 componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
196 // Necessary if you are on a post and you click another post (same route)
198 lastProps.location.pathname.split('/')[2] !==
199 lastProps.history.location.pathname.split('/')[2]
201 // Couldnt get a refresh working. This does for now.
204 document.title = `/u/${this.state.username} - ${this.state.site.name}`;
210 <div class="container">
212 <div class="col-12 col-md-8">
214 {this.state.user.avatar && showAvatars() && (
218 src={this.state.user.avatar}
219 class="rounded-circle mr-2"
222 <span>/u/{this.state.username}</span>
224 {this.state.loading ? (
226 <svg class="icon icon-spinner spin">
227 <use xlinkHref="#icon-spinner"></use>
234 user_id={this.state.user_id}
235 username={this.state.username}
236 sort={SortType[this.state.sort]}
237 page={this.state.page}
239 enableDownvotes={this.state.site.enable_downvotes}
240 enableNsfw={this.state.site.enable_nsfw}
241 view={this.state.view}
242 onPageChange={this.handlePageChange}
246 {!this.state.loading && (
247 <div class="col-12 col-md-4">
249 {this.isCurrentUser && this.userSettings()}
261 <div class="btn-group btn-group-toggle">
263 className={`btn btn-sm btn-secondary pointer btn-outline-light
264 ${this.state.view == UserDetailsView.Overview && 'active'}
269 value={UserDetailsView.Overview}
270 checked={this.state.view === UserDetailsView.Overview}
271 onChange={linkEvent(this, this.handleViewChange)}
276 className={`btn btn-sm btn-secondary pointer btn-outline-light
277 ${this.state.view == UserDetailsView.Comments && 'active'}
282 value={UserDetailsView.Comments}
283 checked={this.state.view == UserDetailsView.Comments}
284 onChange={linkEvent(this, this.handleViewChange)}
289 className={`btn btn-sm btn-secondary pointer btn-outline-light
290 ${this.state.view == UserDetailsView.Posts && 'active'}
295 value={UserDetailsView.Posts}
296 checked={this.state.view == UserDetailsView.Posts}
297 onChange={linkEvent(this, this.handleViewChange)}
302 className={`btn btn-sm btn-secondary pointer btn-outline-light
303 ${this.state.view == UserDetailsView.Saved && 'active'}
308 value={UserDetailsView.Saved}
309 checked={this.state.view == UserDetailsView.Saved}
310 onChange={linkEvent(this, this.handleViewChange)}
320 <div className="mb-2">
321 <span class="mr-3">{this.viewRadios()}</span>
323 sort={this.state.sort}
324 onChange={this.handleSortChange}
328 href={`/feeds/u/${this.state.username}.xml?sort=${
329 SortType[this.state.sort]
335 <svg class="icon mx-2 text-muted small">
336 <use xlinkHref="#icon-rss">#</use>
344 let user = this.state.user;
347 <div class="card border-secondary mb-3">
348 <div class="card-body">
350 <ul class="list-inline mb-0">
351 <li className="list-inline-item">
352 <UserListing user={user} realLink />
355 <li className="list-inline-item badge badge-danger">
361 <div className="d-flex align-items-center mb-2">
363 <use xlinkHref="#icon-cake"></use>
365 <span className="ml-2">
366 {i18n.t('cake_day_title')}{' '}
367 {moment.utc(user.published).local().format('MMM DD, YYYY')}
371 {i18n.t('joined')} <MomentTime data={user} showAgo />
373 <div class="table-responsive mt-1">
374 <table class="table table-bordered table-sm mt-2 mb-0">
377 <td class="text-center" colSpan={2}>
378 {i18n.t('number_of_points', {
379 count: user.post_score + user.comment_score,
387 {i18n.t('number_of_points', { count: user.post_score })}
391 {i18n.t('number_of_posts', { count: user.number_of_posts })}
397 {i18n.t('number_of_points', { count: user.comment_score })}
401 {i18n.t('number_of_comments', {
402 count: user.number_of_comments,
408 {this.isCurrentUser ? (
410 class="btn btn-block btn-secondary mt-3"
411 onClick={linkEvent(this, this.handleLogoutClick)}
418 className={`btn btn-block btn-secondary mt-3 ${
419 !this.state.user.matrix_user_id && 'disabled'
423 href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
425 {i18n.t('send_secure_message')}
428 class="btn btn-block btn-secondary mt-3"
429 to={`/create_private_message?recipient_id=${this.state.user.id}`}
431 {i18n.t('send_message')}
444 <div class="card border-secondary mb-3">
445 <div class="card-body">
446 <h5>{i18n.t('settings')}</h5>
447 <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
448 <div class="form-group">
449 <label>{i18n.t('avatar')}</label>
450 <form class="d-inline">
452 htmlFor="file-upload"
453 class="pointer ml-4 text-muted small font-weight-bold"
455 {!this.checkSettingsAvatar ? (
456 <span class="btn btn-sm btn-secondary">
457 {i18n.t('upload_avatar')}
463 src={this.state.userSettingsForm.avatar}
464 class="rounded-circle"
471 accept="image/*,video/*"
474 disabled={!UserService.Instance.user}
475 onChange={linkEvent(this, this.handleImageUpload)}
479 {this.checkSettingsAvatar && (
480 <div class="form-group">
482 class="btn btn-secondary btn-block"
483 onClick={linkEvent(this, this.removeAvatar)}
485 {`${capitalizeFirstLetter(i18n.t('remove'))} ${i18n.t(
491 <div class="form-group">
492 <label>{i18n.t('language')}</label>
494 value={this.state.userSettingsForm.lang}
495 onChange={linkEvent(this, this.handleUserSettingsLangChange)}
496 class="ml-2 custom-select custom-select-sm w-auto"
498 <option disabled>{i18n.t('language')}</option>
499 <option value="browser">{i18n.t('browser_default')}</option>
500 <option disabled>──</option>
501 {languages.map(lang => (
502 <option value={lang.code}>{lang.name}</option>
506 <div class="form-group">
507 <label>{i18n.t('theme')}</label>
509 value={this.state.userSettingsForm.theme}
510 onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
511 class="ml-2 custom-select custom-select-sm w-auto"
513 <option disabled>{i18n.t('theme')}</option>
514 {themes.map(theme => (
515 <option value={theme}>{theme}</option>
519 <form className="form-group">
521 <div class="mr-2">{i18n.t('sort_type')}</div>
524 type_={this.state.userSettingsForm.default_listing_type}
525 onChange={this.handleUserSettingsListingTypeChange}
528 <form className="form-group">
530 <div class="mr-2">{i18n.t('type')}</div>
533 sort={this.state.userSettingsForm.default_sort_type}
534 onChange={this.handleUserSettingsSortTypeChange}
537 <div class="form-group row">
538 <label class="col-lg-3 col-form-label" htmlFor="user-email">
541 <div class="col-lg-9">
546 placeholder={i18n.t('optional')}
547 value={this.state.userSettingsForm.email}
550 this.handleUserSettingsEmailChange
556 <div class="form-group row">
557 <label class="col-lg-5 col-form-label">
559 href="https://about.riot.im/"
563 {i18n.t('matrix_user_id')}
566 <div class="col-lg-7">
570 placeholder="@user:example.com"
571 value={this.state.userSettingsForm.matrix_user_id}
574 this.handleUserSettingsMatrixUserIdChange
580 <div class="form-group row">
581 <label class="col-lg-5 col-form-label" htmlFor="user-password">
582 {i18n.t('new_password')}
584 <div class="col-lg-7">
589 value={this.state.userSettingsForm.new_password}
590 autoComplete="new-password"
593 this.handleUserSettingsNewPasswordChange
598 <div class="form-group row">
600 class="col-lg-5 col-form-label"
601 htmlFor="user-verify-password"
603 {i18n.t('verify_password')}
605 <div class="col-lg-7">
608 id="user-verify-password"
610 value={this.state.userSettingsForm.new_password_verify}
611 autoComplete="new-password"
614 this.handleUserSettingsNewPasswordVerifyChange
619 <div class="form-group row">
621 class="col-lg-5 col-form-label"
622 htmlFor="user-old-password"
624 {i18n.t('old_password')}
626 <div class="col-lg-7">
629 id="user-old-password"
631 value={this.state.userSettingsForm.old_password}
632 autoComplete="new-password"
635 this.handleUserSettingsOldPasswordChange
640 {this.state.site.enable_nsfw && (
641 <div class="form-group">
642 <div class="form-check">
644 class="form-check-input"
647 checked={this.state.userSettingsForm.show_nsfw}
650 this.handleUserSettingsShowNsfwChange
653 <label class="form-check-label" htmlFor="user-show-nsfw">
654 {i18n.t('show_nsfw')}
659 <div class="form-group">
660 <div class="form-check">
662 class="form-check-input"
663 id="user-show-avatars"
665 checked={this.state.userSettingsForm.show_avatars}
668 this.handleUserSettingsShowAvatarsChange
671 <label class="form-check-label" htmlFor="user-show-avatars">
672 {i18n.t('show_avatars')}
676 <div class="form-group">
677 <div class="form-check">
679 class="form-check-input"
680 id="user-send-notifications-to-email"
682 disabled={!this.state.user.email}
684 this.state.userSettingsForm.send_notifications_to_email
688 this.handleUserSettingsSendNotificationsToEmailChange
692 class="form-check-label"
693 htmlFor="user-send-notifications-to-email"
695 {i18n.t('send_notifications_to_email')}
699 <div class="form-group">
700 <button type="submit" class="btn btn-block btn-secondary mr-4">
701 {this.state.userSettingsLoading ? (
702 <svg class="icon icon-spinner spin">
703 <use xlinkHref="#icon-spinner"></use>
706 capitalizeFirstLetter(i18n.t('save'))
711 <div class="form-group mb-0">
713 class="btn btn-block btn-danger"
716 this.handleDeleteAccountShowConfirmToggle
719 {i18n.t('delete_account')}
721 {this.state.deleteAccountShowConfirm && (
723 <div class="my-2 alert alert-danger" role="alert">
724 {i18n.t('delete_account_confirm')}
728 value={this.state.deleteAccountForm.password}
729 autoComplete="new-password"
732 this.handleDeleteAccountPasswordChange
734 class="form-control my-2"
737 class="btn btn-danger mr-4"
738 disabled={!this.state.deleteAccountForm.password}
739 onClick={linkEvent(this, this.handleDeleteAccount)}
741 {this.state.deleteAccountLoading ? (
742 <svg class="icon icon-spinner spin">
743 <use xlinkHref="#icon-spinner"></use>
746 capitalizeFirstLetter(i18n.t('delete'))
750 class="btn btn-secondary"
753 this.handleDeleteAccountShowConfirmToggle
771 {this.state.moderates.length > 0 && (
772 <div class="card border-secondary mb-3">
773 <div class="card-body">
774 <h5>{i18n.t('moderates')}</h5>
775 <ul class="list-unstyled mb-0">
776 {this.state.moderates.map(community => (
778 <Link to={`/c/${community.community_name}`}>
779 {community.community_name}
794 {this.state.follows.length > 0 && (
795 <div class="card border-secondary mb-3">
796 <div class="card-body">
797 <h5>{i18n.t('subscribed')}</h5>
798 <ul class="list-unstyled mb-0">
799 {this.state.follows.map(community => (
801 <Link to={`/c/${community.community_name}`}>
802 {community.community_name}
814 updateUrl(paramUpdates: UrlParams) {
815 const page = paramUpdates.page || this.state.page;
817 paramUpdates.view || UserDetailsView[this.state.view].toLowerCase();
819 paramUpdates.sort || SortType[this.state.sort].toLowerCase();
820 this.props.history.push(
821 `/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
825 handlePageChange(page: number) {
826 this.updateUrl({ page });
829 handleSortChange(val: SortType) {
830 this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
833 handleViewChange(i: User, event: any) {
835 view: UserDetailsView[Number(event.target.value)].toLowerCase(),
840 handleUserSettingsShowNsfwChange(i: User, event: any) {
841 i.state.userSettingsForm.show_nsfw = event.target.checked;
845 handleUserSettingsShowAvatarsChange(i: User, event: any) {
846 i.state.userSettingsForm.show_avatars = event.target.checked;
847 UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
851 handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
852 i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
856 handleUserSettingsThemeChange(i: User, event: any) {
857 i.state.userSettingsForm.theme = event.target.value;
858 setTheme(event.target.value, true);
862 handleUserSettingsLangChange(i: User, event: any) {
863 i.state.userSettingsForm.lang = event.target.value;
864 i18n.changeLanguage(i.state.userSettingsForm.lang);
868 handleUserSettingsSortTypeChange(val: SortType) {
869 this.state.userSettingsForm.default_sort_type = val;
870 this.setState(this.state);
873 handleUserSettingsListingTypeChange(val: ListingType) {
874 this.state.userSettingsForm.default_listing_type = val;
875 this.setState(this.state);
878 handleUserSettingsEmailChange(i: User, event: any) {
879 i.state.userSettingsForm.email = event.target.value;
880 if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
881 i.state.userSettingsForm.email = undefined;
886 handleUserSettingsMatrixUserIdChange(i: User, event: any) {
887 i.state.userSettingsForm.matrix_user_id = event.target.value;
889 i.state.userSettingsForm.matrix_user_id == '' &&
890 !i.state.user.matrix_user_id
892 i.state.userSettingsForm.matrix_user_id = undefined;
897 handleUserSettingsNewPasswordChange(i: User, event: any) {
898 i.state.userSettingsForm.new_password = event.target.value;
899 if (i.state.userSettingsForm.new_password == '') {
900 i.state.userSettingsForm.new_password = undefined;
905 handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
906 i.state.userSettingsForm.new_password_verify = event.target.value;
907 if (i.state.userSettingsForm.new_password_verify == '') {
908 i.state.userSettingsForm.new_password_verify = undefined;
913 handleUserSettingsOldPasswordChange(i: User, event: any) {
914 i.state.userSettingsForm.old_password = event.target.value;
915 if (i.state.userSettingsForm.old_password == '') {
916 i.state.userSettingsForm.old_password = undefined;
921 handleImageUpload(i: User, event: any) {
922 event.preventDefault();
923 let file = event.target.files[0];
924 const imageUploadUrl = `/pictrs/image`;
925 const formData = new FormData();
926 formData.append('images[]', file);
928 i.state.avatarLoading = true;
931 fetch(imageUploadUrl, {
935 .then(res => res.json())
937 console.log('pictrs upload:');
939 if (res.msg == 'ok') {
940 let hash = res.files[0].file;
941 let url = `${window.location.origin}/pictrs/image/${hash}`;
942 i.state.userSettingsForm.avatar = url;
943 i.state.avatarLoading = false;
946 i.state.avatarLoading = false;
948 toast(JSON.stringify(res), 'danger');
952 i.state.avatarLoading = false;
954 toast(error, 'danger');
958 removeAvatar(i: User, event: any) {
959 event.preventDefault();
960 i.state.userSettingsLoading = true;
961 i.state.userSettingsForm.avatar = '';
964 WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
967 get checkSettingsAvatar(): boolean {
969 this.state.userSettingsForm.avatar &&
970 this.state.userSettingsForm.avatar != ''
974 handleUserSettingsSubmit(i: User, event: any) {
975 event.preventDefault();
976 i.state.userSettingsLoading = true;
979 WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
982 handleDeleteAccountShowConfirmToggle(i: User, event: any) {
983 event.preventDefault();
984 i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
988 handleDeleteAccountPasswordChange(i: User, event: any) {
989 i.state.deleteAccountForm.password = event.target.value;
993 handleLogoutClick(i: User) {
994 UserService.Instance.logout();
995 i.context.router.history.push('/');
998 handleDeleteAccount(i: User, event: any) {
999 event.preventDefault();
1000 i.state.deleteAccountLoading = true;
1001 i.setState(i.state);
1003 WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
1006 parseMessage(msg: WebSocketJsonResponse) {
1008 const res = wsJsonToRes(msg);
1010 toast(i18n.t(msg.error), 'danger');
1011 if (msg.error == 'couldnt_find_that_username_or_email') {
1012 this.context.router.history.push('/');
1015 deleteAccountLoading: false,
1016 avatarLoading: false,
1017 userSettingsLoading: false,
1020 } else if (res.op == UserOperation.GetUserDetails) {
1021 // Since the UserDetails contains posts/comments as well as some general user info we listen here as well
1022 // and set the parent state if it is not set or differs
1023 const data = res.data as UserDetailsResponse;
1025 if (this.state.user.id !== data.user.id) {
1026 this.state.user = data.user;
1027 this.state.follows = data.follows;
1028 this.state.moderates = data.moderates;
1030 if (this.isCurrentUser) {
1031 this.state.userSettingsForm.show_nsfw =
1032 UserService.Instance.user.show_nsfw;
1033 this.state.userSettingsForm.theme = UserService.Instance.user.theme
1034 ? UserService.Instance.user.theme
1036 this.state.userSettingsForm.default_sort_type =
1037 UserService.Instance.user.default_sort_type;
1038 this.state.userSettingsForm.default_listing_type =
1039 UserService.Instance.user.default_listing_type;
1040 this.state.userSettingsForm.lang = UserService.Instance.user.lang;
1041 this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
1042 this.state.userSettingsForm.email = this.state.user.email;
1043 this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
1044 this.state.userSettingsForm.show_avatars =
1045 UserService.Instance.user.show_avatars;
1046 this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
1048 this.state.loading = false;
1049 this.setState(this.state);
1051 } else if (res.op == UserOperation.SaveUserSettings) {
1052 const data = res.data as LoginResponse;
1053 UserService.Instance.login(data);
1055 userSettingsLoading: false,
1057 window.scrollTo(0, 0);
1058 } else if (res.op == UserOperation.DeleteAccount) {
1060 deleteAccountLoading: false,
1061 deleteAccountShowConfirm: false,
1063 this.context.router.history.push('/');
1064 } else if (res.op == UserOperation.GetSite) {
1065 const data = res.data as GetSiteResponse;