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;
60 export class User extends Component<any, UserState> {
61 private subscription: Subscription;
62 private emptyState: UserState = {
67 number_of_posts: null,
69 number_of_comments: null,
74 send_notifications_to_email: null,
84 view: User.getViewFromProps(this.props.match.view),
85 sort: User.getSortTypeFromProps(this.props.match.sort),
86 page: User.getPageFromProps(this.props.match.page),
90 default_sort_type: null,
91 default_listing_type: null,
94 send_notifications_to_email: null,
97 userSettingsLoading: null,
98 deleteAccountLoading: null,
99 deleteAccountShowConfirm: false,
106 creator_id: undefined,
107 published: undefined,
108 creator_name: undefined,
109 number_of_users: undefined,
110 number_of_posts: undefined,
111 number_of_comments: undefined,
112 number_of_communities: undefined,
113 enable_downvotes: undefined,
114 open_registration: undefined,
115 enable_nsfw: undefined,
119 constructor(props: any, context: any) {
120 super(props, context);
122 this.state = Object.assign({}, this.emptyState);
123 this.handleSortChange = this.handleSortChange.bind(this);
124 this.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind(
127 this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
131 this.state.user_id = Number(this.props.match.params.id) || null;
132 this.state.username = this.props.match.params.username;
134 this.subscription = WebSocketService.Instance.subject
135 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
137 msg => this.parseMessage(msg),
138 err => console.error(err),
139 () => console.log('complete')
142 WebSocketService.Instance.getSite();
145 get isCurrentUser() {
147 UserService.Instance.user &&
148 UserService.Instance.user.id == this.state.user.id
152 static getViewFromProps(view: any): UserDetailsView {
154 ? UserDetailsView[capitalizeFirstLetter(view)]
155 : UserDetailsView.Overview;
158 static getSortTypeFromProps(sort: any): SortType {
159 return sort ? routeSortTypeToEnum(sort) : SortType.New;
162 static getPageFromProps(page: any): number {
163 return page ? Number(page) : 1;
166 componentWillUnmount() {
167 this.subscription.unsubscribe();
170 static getDerivedStateFromProps(props) {
172 view: this.getViewFromProps(props.match.params.view),
173 sort: this.getSortTypeFromProps(props.match.params.sort),
174 page: this.getPageFromProps(props.match.params.page),
175 user_id: Number(props.match.params.id) || null,
176 username: props.match.params.username,
180 componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
181 // Necessary if you are on a post and you click another post (same route)
183 lastProps.location.pathname.split('/')[2] !==
184 lastProps.history.location.pathname.split('/')[2]
186 // Couldnt get a refresh working. This does for now.
189 document.title = `/u/${this.state.username} - ${this.state.site.name}`;
195 <div class="container">
196 {this.state.loading ? (
198 <svg class="icon icon-spinner spin">
199 <use xlinkHref="#icon-spinner"></use>
204 <div class="col-12 col-md-8">
206 {this.state.user.avatar && showAvatars() && (
210 src={this.state.user.avatar}
211 class="rounded-circle mr-2"
214 <span>/u/{this.state.username}</span>
218 user_id={this.state.user_id}
219 username={this.state.username}
220 sort={SortType[this.state.sort]}
221 page={this.state.page}
223 enableDownvotes={this.state.site.enable_downvotes}
224 enableNsfw={this.state.site.enable_nsfw}
225 view={this.state.view}
226 key={this.state.user_id || this.state.username}
230 <div class="col-12 col-md-4">
232 {this.isCurrentUser && this.userSettings()}
244 <div class="btn-group btn-group-toggle">
246 className={`btn btn-sm btn-secondary pointer btn-outline-light
247 ${this.state.view == UserDetailsView.Overview && 'active'}
252 value={UserDetailsView.Overview}
253 checked={this.state.view === UserDetailsView.Overview}
254 onChange={linkEvent(this, this.handleViewChange)}
259 className={`btn btn-sm btn-secondary pointer btn-outline-light
260 ${this.state.view == UserDetailsView.Comments && 'active'}
265 value={UserDetailsView.Comments}
266 checked={this.state.view == UserDetailsView.Comments}
267 onChange={linkEvent(this, this.handleViewChange)}
272 className={`btn btn-sm btn-secondary pointer btn-outline-light
273 ${this.state.view == UserDetailsView.Posts && 'active'}
278 value={UserDetailsView.Posts}
279 checked={this.state.view == UserDetailsView.Posts}
280 onChange={linkEvent(this, this.handleViewChange)}
285 className={`btn btn-sm btn-secondary pointer btn-outline-light
286 ${this.state.view == UserDetailsView.Saved && 'active'}
291 value={UserDetailsView.Saved}
292 checked={this.state.view == UserDetailsView.Saved}
293 onChange={linkEvent(this, this.handleViewChange)}
303 <div className="mb-2">
304 <span class="mr-3">{this.viewRadios()}</span>
306 sort={this.state.sort}
307 onChange={this.handleSortChange}
311 href={`/feeds/u/${this.state.username}.xml?sort=${
312 SortType[this.state.sort]
318 <svg class="icon mx-2 text-muted small">
319 <use xlinkHref="#icon-rss">#</use>
327 let user = this.state.user;
330 <div class="card border-secondary mb-3">
331 <div class="card-body">
333 <ul class="list-inline mb-0">
334 <li className="list-inline-item">
335 <UserListing user={user} realLink />
338 <li className="list-inline-item badge badge-danger">
344 <div className="d-flex align-items-center mb-2">
346 <use xlinkHref="#icon-cake"></use>
348 <span className="ml-2">
349 {i18n.t('cake_day_title')}{' '}
350 {moment.utc(user.published).local().format('MMM DD, YYYY')}
354 {i18n.t('joined')} <MomentTime data={user} showAgo />
356 <div class="table-responsive mt-1">
357 <table class="table table-bordered table-sm mt-2 mb-0">
360 <td class="text-center" colSpan={2}>
361 {i18n.t('number_of_points', {
362 count: user.post_score + user.comment_score,
370 {i18n.t('number_of_points', { count: user.post_score })}
374 {i18n.t('number_of_posts', { count: user.number_of_posts })}
380 {i18n.t('number_of_points', { count: user.comment_score })}
384 {i18n.t('number_of_comments', {
385 count: user.number_of_comments,
391 {this.isCurrentUser ? (
393 class="btn btn-block btn-secondary mt-3"
394 onClick={linkEvent(this, this.handleLogoutClick)}
401 className={`btn btn-block btn-secondary mt-3 ${
402 !this.state.user.matrix_user_id && 'disabled'
406 href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
408 {i18n.t('send_secure_message')}
411 class="btn btn-block btn-secondary mt-3"
412 to={`/create_private_message?recipient_id=${this.state.user.id}`}
414 {i18n.t('send_message')}
427 <div class="card border-secondary mb-3">
428 <div class="card-body">
429 <h5>{i18n.t('settings')}</h5>
430 <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
431 <div class="form-group">
432 <label>{i18n.t('avatar')}</label>
433 <form class="d-inline">
435 htmlFor="file-upload"
436 class="pointer ml-4 text-muted small font-weight-bold"
438 {!this.checkSettingsAvatar ? (
439 <span class="btn btn-sm btn-secondary">
440 {i18n.t('upload_avatar')}
446 src={this.state.userSettingsForm.avatar}
447 class="rounded-circle"
454 accept="image/*,video/*"
457 disabled={!UserService.Instance.user}
458 onChange={linkEvent(this, this.handleImageUpload)}
462 {this.checkSettingsAvatar && (
463 <div class="form-group">
465 class="btn btn-secondary btn-block"
466 onClick={linkEvent(this, this.removeAvatar)}
468 {`${capitalizeFirstLetter(i18n.t('remove'))} ${i18n.t(
474 <div class="form-group">
475 <label>{i18n.t('language')}</label>
477 value={this.state.userSettingsForm.lang}
478 onChange={linkEvent(this, this.handleUserSettingsLangChange)}
479 class="ml-2 custom-select custom-select-sm w-auto"
481 <option disabled>{i18n.t('language')}</option>
482 <option value="browser">{i18n.t('browser_default')}</option>
483 <option disabled>──</option>
484 {languages.map(lang => (
485 <option value={lang.code}>{lang.name}</option>
489 <div class="form-group">
490 <label>{i18n.t('theme')}</label>
492 value={this.state.userSettingsForm.theme}
493 onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
494 class="ml-2 custom-select custom-select-sm w-auto"
496 <option disabled>{i18n.t('theme')}</option>
497 {themes.map(theme => (
498 <option value={theme}>{theme}</option>
502 <form className="form-group">
504 <div class="mr-2">{i18n.t('sort_type')}</div>
507 type_={this.state.userSettingsForm.default_listing_type}
508 onChange={this.handleUserSettingsListingTypeChange}
511 <form className="form-group">
513 <div class="mr-2">{i18n.t('type')}</div>
516 sort={this.state.userSettingsForm.default_sort_type}
517 onChange={this.handleUserSettingsSortTypeChange}
520 <div class="form-group row">
521 <label class="col-lg-3 col-form-label" htmlFor="user-email">
524 <div class="col-lg-9">
529 placeholder={i18n.t('optional')}
530 value={this.state.userSettingsForm.email}
533 this.handleUserSettingsEmailChange
539 <div class="form-group row">
540 <label class="col-lg-5 col-form-label">
542 href="https://about.riot.im/"
546 {i18n.t('matrix_user_id')}
549 <div class="col-lg-7">
553 placeholder="@user:example.com"
554 value={this.state.userSettingsForm.matrix_user_id}
557 this.handleUserSettingsMatrixUserIdChange
563 <div class="form-group row">
564 <label class="col-lg-5 col-form-label" htmlFor="user-password">
565 {i18n.t('new_password')}
567 <div class="col-lg-7">
572 value={this.state.userSettingsForm.new_password}
573 autoComplete="new-password"
576 this.handleUserSettingsNewPasswordChange
581 <div class="form-group row">
583 class="col-lg-5 col-form-label"
584 htmlFor="user-verify-password"
586 {i18n.t('verify_password')}
588 <div class="col-lg-7">
591 id="user-verify-password"
593 value={this.state.userSettingsForm.new_password_verify}
594 autoComplete="new-password"
597 this.handleUserSettingsNewPasswordVerifyChange
602 <div class="form-group row">
604 class="col-lg-5 col-form-label"
605 htmlFor="user-old-password"
607 {i18n.t('old_password')}
609 <div class="col-lg-7">
612 id="user-old-password"
614 value={this.state.userSettingsForm.old_password}
615 autoComplete="new-password"
618 this.handleUserSettingsOldPasswordChange
623 {this.state.site.enable_nsfw && (
624 <div class="form-group">
625 <div class="form-check">
627 class="form-check-input"
630 checked={this.state.userSettingsForm.show_nsfw}
633 this.handleUserSettingsShowNsfwChange
636 <label class="form-check-label" htmlFor="user-show-nsfw">
637 {i18n.t('show_nsfw')}
642 <div class="form-group">
643 <div class="form-check">
645 class="form-check-input"
646 id="user-show-avatars"
648 checked={this.state.userSettingsForm.show_avatars}
651 this.handleUserSettingsShowAvatarsChange
654 <label class="form-check-label" htmlFor="user-show-avatars">
655 {i18n.t('show_avatars')}
659 <div class="form-group">
660 <div class="form-check">
662 class="form-check-input"
663 id="user-send-notifications-to-email"
665 disabled={!this.state.user.email}
667 this.state.userSettingsForm.send_notifications_to_email
671 this.handleUserSettingsSendNotificationsToEmailChange
675 class="form-check-label"
676 htmlFor="user-send-notifications-to-email"
678 {i18n.t('send_notifications_to_email')}
682 <div class="form-group">
683 <button type="submit" class="btn btn-block btn-secondary mr-4">
684 {this.state.userSettingsLoading ? (
685 <svg class="icon icon-spinner spin">
686 <use xlinkHref="#icon-spinner"></use>
689 capitalizeFirstLetter(i18n.t('save'))
694 <div class="form-group mb-0">
696 class="btn btn-block btn-danger"
699 this.handleDeleteAccountShowConfirmToggle
702 {i18n.t('delete_account')}
704 {this.state.deleteAccountShowConfirm && (
706 <div class="my-2 alert alert-danger" role="alert">
707 {i18n.t('delete_account_confirm')}
711 value={this.state.deleteAccountForm.password}
712 autoComplete="new-password"
715 this.handleDeleteAccountPasswordChange
717 class="form-control my-2"
720 class="btn btn-danger mr-4"
721 disabled={!this.state.deleteAccountForm.password}
722 onClick={linkEvent(this, this.handleDeleteAccount)}
724 {this.state.deleteAccountLoading ? (
725 <svg class="icon icon-spinner spin">
726 <use xlinkHref="#icon-spinner"></use>
729 capitalizeFirstLetter(i18n.t('delete'))
733 class="btn btn-secondary"
736 this.handleDeleteAccountShowConfirmToggle
754 {this.state.moderates.length > 0 && (
755 <div class="card border-secondary mb-3">
756 <div class="card-body">
757 <h5>{i18n.t('moderates')}</h5>
758 <ul class="list-unstyled mb-0">
759 {this.state.moderates.map(community => (
761 <Link to={`/c/${community.community_name}`}>
762 {community.community_name}
777 {this.state.follows.length > 0 && (
778 <div class="card border-secondary mb-3">
779 <div class="card-body">
780 <h5>{i18n.t('subscribed')}</h5>
781 <ul class="list-unstyled mb-0">
782 {this.state.follows.map(community => (
784 <Link to={`/c/${community.community_name}`}>
785 {community.community_name}
800 {this.state.page > 1 && (
802 class="btn btn-sm btn-secondary mr-1"
803 onClick={linkEvent(this, this.prevPage)}
808 {this.state.comments.length + this.state.posts.length > 0 && (
810 class="btn btn-sm btn-secondary"
811 onClick={linkEvent(this, this.nextPage)}
821 let viewStr = UserDetailsView[this.state.view].toLowerCase();
822 let sortStr = SortType[this.state.sort].toLowerCase();
823 this.props.history.push(
824 `/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
826 this.setState(this.state);
839 handleSortChange(val: SortType) {
840 this.state.sort = val;
845 handleViewChange(i: User, event: any) {
846 i.state.view = Number(event.target.value);
851 handleUserSettingsShowNsfwChange(i: User, event: any) {
854 ...i.state.userSettingsForm,
855 show_nsfw: event.target.checked,
860 handleUserSettingsShowAvatarsChange(i: User, event: any) {
861 i.state.userSettingsForm.show_avatars = event.target.checked;
862 UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
866 handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
867 i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
871 handleUserSettingsThemeChange(i: User, event: any) {
872 i.state.userSettingsForm.theme = event.target.value;
873 setTheme(event.target.value, true);
877 handleUserSettingsLangChange(i: User, event: any) {
878 i.state.userSettingsForm.lang = event.target.value;
879 i18n.changeLanguage(i.state.userSettingsForm.lang);
883 handleUserSettingsSortTypeChange(val: SortType) {
884 this.state.userSettingsForm.default_sort_type = val;
885 this.setState(this.state);
888 handleUserSettingsListingTypeChange(val: ListingType) {
889 this.state.userSettingsForm.default_listing_type = val;
890 this.setState(this.state);
893 handleUserSettingsEmailChange(i: User, event: any) {
894 i.state.userSettingsForm.email = event.target.value;
895 if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
896 i.state.userSettingsForm.email = undefined;
901 handleUserSettingsMatrixUserIdChange(i: User, event: any) {
902 i.state.userSettingsForm.matrix_user_id = event.target.value;
904 i.state.userSettingsForm.matrix_user_id == '' &&
905 !i.state.user.matrix_user_id
907 i.state.userSettingsForm.matrix_user_id = undefined;
912 handleUserSettingsNewPasswordChange(i: User, event: any) {
913 i.state.userSettingsForm.new_password = event.target.value;
914 if (i.state.userSettingsForm.new_password == '') {
915 i.state.userSettingsForm.new_password = undefined;
920 handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
921 i.state.userSettingsForm.new_password_verify = event.target.value;
922 if (i.state.userSettingsForm.new_password_verify == '') {
923 i.state.userSettingsForm.new_password_verify = undefined;
928 handleUserSettingsOldPasswordChange(i: User, event: any) {
929 i.state.userSettingsForm.old_password = event.target.value;
930 if (i.state.userSettingsForm.old_password == '') {
931 i.state.userSettingsForm.old_password = undefined;
936 handleImageUpload(i: User, event: any) {
937 event.preventDefault();
938 let file = event.target.files[0];
939 const imageUploadUrl = `/pictrs/image`;
940 const formData = new FormData();
941 formData.append('images[]', file);
943 i.state.avatarLoading = true;
946 fetch(imageUploadUrl, {
950 .then(res => res.json())
952 console.log('pictrs upload:');
954 if (res.msg == 'ok') {
955 let hash = res.files[0].file;
956 let url = `${window.location.origin}/pictrs/image/${hash}`;
957 i.state.userSettingsForm.avatar = url;
958 i.state.avatarLoading = false;
961 i.state.avatarLoading = false;
963 toast(JSON.stringify(res), 'danger');
967 i.state.avatarLoading = false;
969 toast(error, 'danger');
973 removeAvatar(i: User, event: any) {
974 event.preventDefault();
975 i.state.userSettingsLoading = true;
976 i.state.userSettingsForm.avatar = '';
979 WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
982 get checkSettingsAvatar(): boolean {
984 this.state.userSettingsForm.avatar &&
985 this.state.userSettingsForm.avatar != ''
989 handleUserSettingsSubmit(i: User, event: any) {
990 event.preventDefault();
991 i.state.userSettingsLoading = true;
994 WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
997 handleDeleteAccountShowConfirmToggle(i: User, event: any) {
998 event.preventDefault();
999 i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
1000 i.setState(i.state);
1003 handleDeleteAccountPasswordChange(i: User, event: any) {
1004 i.state.deleteAccountForm.password = event.target.value;
1005 i.setState(i.state);
1008 handleLogoutClick(i: User) {
1009 UserService.Instance.logout();
1010 i.context.router.history.push('/');
1013 handleDeleteAccount(i: User, event: any) {
1014 event.preventDefault();
1015 i.state.deleteAccountLoading = true;
1016 i.setState(i.state);
1018 WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
1021 parseMessage(msg: WebSocketJsonResponse) {
1022 const res = wsJsonToRes(msg);
1024 toast(i18n.t(msg.error), 'danger');
1025 if (msg.error == 'couldnt_find_that_username_or_email') {
1026 this.context.router.history.push('/');
1029 deleteAccountLoading: false,
1030 avatarLoading: false,
1031 userSettingsLoading: false,
1034 } else if (res.op == UserOperation.GetUserDetails) {
1035 // Since the UserDetails contains posts/comments as well as some general user info we listen here as well
1036 // and set the parent state if it is not set or differs
1037 const data = res.data as UserDetailsResponse;
1039 if (this.state.user.id !== data.user.id) {
1040 this.state.user = data.user;
1041 this.state.follows = data.follows;
1042 this.state.moderates = data.moderates;
1044 if (this.isCurrentUser) {
1045 this.state.userSettingsForm.show_nsfw =
1046 UserService.Instance.user.show_nsfw;
1047 this.state.userSettingsForm.theme = UserService.Instance.user.theme
1048 ? UserService.Instance.user.theme
1050 this.state.userSettingsForm.default_sort_type =
1051 UserService.Instance.user.default_sort_type;
1052 this.state.userSettingsForm.default_listing_type =
1053 UserService.Instance.user.default_listing_type;
1054 this.state.userSettingsForm.lang = UserService.Instance.user.lang;
1055 this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
1056 this.state.userSettingsForm.email = this.state.user.email;
1057 this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
1058 this.state.userSettingsForm.show_avatars =
1059 UserService.Instance.user.show_avatars;
1060 this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
1062 this.setState(this.state);
1064 } else if (res.op == UserOperation.SaveUserSettings) {
1065 const data = res.data as LoginResponse;
1066 UserService.Instance.login(data);
1068 userSettingsLoading: false,
1070 window.scrollTo(0, 0);
1071 } else if (res.op == UserOperation.DeleteAccount) {
1073 deleteAccountLoading: false,
1074 deleteAccountShowConfirm: false,
1076 this.context.router.history.push('/');
1077 } else if (res.op == UserOperation.GetSite) {
1078 const data = res.data as GetSiteResponse;