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';
22 WebSocketJsonResponse,
23 } from '../interfaces';
24 import { WebSocketService, UserService } from '../services';
29 capitalizeFirstLetter,
38 createPostLikeFindRes,
42 import { PostListing } from './post-listing';
43 import { UserListing } from './user-listing';
44 import { SortSelect } from './sort-select';
45 import { ListingTypeSelect } from './listing-type-select';
46 import { CommentNodes } from './comment-nodes';
47 import { MomentTime } from './moment-time';
48 import { i18n } from '../i18next';
49 import moment from 'moment';
62 follows: Array<CommunityUser>;
63 moderates: Array<CommunityUser>;
64 comments: Array<Comment>;
67 admins: Array<UserView>;
72 avatarLoading: boolean;
73 userSettingsForm: UserSettingsForm;
74 userSettingsLoading: boolean;
75 deleteAccountLoading: boolean;
76 deleteAccountShowConfirm: boolean;
77 deleteAccountForm: DeleteAccountForm;
80 export class User extends Component<any, UserState> {
81 private subscription: Subscription;
82 private emptyState: UserState = {
87 number_of_posts: null,
89 number_of_comments: null,
94 send_notifications_to_email: null,
106 avatarLoading: false,
107 view: this.getViewFromProps(this.props),
108 sort: this.getSortTypeFromProps(this.props),
109 page: this.getPageFromProps(this.props),
113 default_sort_type: null,
114 default_listing_type: null,
117 send_notifications_to_email: null,
120 userSettingsLoading: null,
121 deleteAccountLoading: null,
122 deleteAccountShowConfirm: false,
128 constructor(props: any, context: any) {
129 super(props, context);
131 this.state = this.emptyState;
132 this.handleSortChange = this.handleSortChange.bind(this);
133 this.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind(
136 this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
140 this.state.user_id = Number(this.props.match.params.id);
141 this.state.username = this.props.match.params.username;
143 this.subscription = WebSocketService.Instance.subject
144 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
146 msg => this.parseMessage(msg),
147 err => console.error(err),
148 () => console.log('complete')
154 get isCurrentUser() {
156 UserService.Instance.user &&
157 UserService.Instance.user.id == this.state.user.id
161 getViewFromProps(props: any): View {
162 return props.match.params.view
163 ? View[capitalizeFirstLetter(props.match.params.view)]
167 getSortTypeFromProps(props: any): SortType {
168 return props.match.params.sort
169 ? routeSortTypeToEnum(props.match.params.sort)
173 getPageFromProps(props: any): number {
174 return props.match.params.page ? Number(props.match.params.page) : 1;
177 componentWillUnmount() {
178 this.subscription.unsubscribe();
181 // Necessary for back button for some reason
182 componentWillReceiveProps(nextProps: any) {
184 nextProps.history.action == 'POP' ||
185 nextProps.history.action == 'PUSH'
187 this.state.view = this.getViewFromProps(nextProps);
188 this.state.sort = this.getSortTypeFromProps(nextProps);
189 this.state.page = this.getPageFromProps(nextProps);
190 this.setState(this.state);
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.
208 <div class="container">
209 {this.state.loading ? (
211 <svg class="icon icon-spinner spin">
212 <use xlinkHref="#icon-spinner"></use>
217 <div class="col-12 col-md-8">
219 {this.state.user.avatar && showAvatars() && (
223 src={this.state.user.avatar}
224 class="rounded-circle mr-2"
227 <span>/u/{this.state.user.name}</span>
230 {this.state.view == View.Overview && this.overview()}
231 {this.state.view == View.Comments && this.comments()}
232 {this.state.view == View.Posts && this.posts()}
233 {this.state.view == View.Saved && this.overview()}
236 <div class="col-12 col-md-4">
238 {this.isCurrentUser && this.userSettings()}
250 <div class="btn-group btn-group-toggle">
252 className={`btn btn-sm btn-secondary pointer btn-outline-light
253 ${this.state.view == View.Overview && 'active'}
258 value={View.Overview}
259 checked={this.state.view == View.Overview}
260 onChange={linkEvent(this, this.handleViewChange)}
265 className={`btn btn-sm btn-secondary pointer btn-outline-light
266 ${this.state.view == View.Comments && 'active'}
271 value={View.Comments}
272 checked={this.state.view == View.Comments}
273 onChange={linkEvent(this, this.handleViewChange)}
278 className={`btn btn-sm btn-secondary pointer btn-outline-light
279 ${this.state.view == View.Posts && 'active'}
285 checked={this.state.view == View.Posts}
286 onChange={linkEvent(this, this.handleViewChange)}
291 className={`btn btn-sm btn-secondary pointer btn-outline-light
292 ${this.state.view == View.Saved && 'active'}
298 checked={this.state.view == View.Saved}
299 onChange={linkEvent(this, this.handleViewChange)}
309 <div className="mb-2">
310 <span class="mr-3">{this.viewRadios()}</span>
312 sort={this.state.sort}
313 onChange={this.handleSortChange}
317 href={`/feeds/u/${this.state.username}.xml?sort=${
318 SortType[this.state.sort]
324 <svg class="icon mx-2 text-muted small">
325 <use xlinkHref="#icon-rss">#</use>
333 let combined: Array<{ type_: string; data: Comment | Post }> = [];
334 let comments = this.state.comments.map(e => {
335 return { type_: 'comments', data: e };
337 let posts = this.state.posts.map(e => {
338 return { type_: 'posts', data: e };
341 combined.push(...comments);
342 combined.push(...posts);
345 if (this.state.sort == SortType.New) {
346 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
348 combined.sort((a, b) => b.data.score - a.data.score);
355 {i.type_ == 'posts' ? (
357 post={i.data as Post}
358 admins={this.state.admins}
363 nodes={[{ comment: i.data as Comment }]}
364 admins={this.state.admins}
379 nodes={commentsToFlatNodes(this.state.comments)}
380 admins={this.state.admins}
391 {this.state.posts.map(post => (
392 <PostListing post={post} admins={this.state.admins} showCommunity />
399 let user = this.state.user;
402 <div class="card border-secondary mb-3">
403 <div class="card-body">
405 <ul class="list-inline mb-0">
406 <li className="list-inline-item">
407 <UserListing user={user} realLink />
410 <li className="list-inline-item badge badge-danger">
416 <div className="d-flex align-items-center mb-2">
418 <use xlinkHref="#icon-cake"></use>
420 <span className="ml-2">
421 {i18n.t('cake_day_title')}{' '}
422 {moment.utc(user.published).local().format('MMM DD, YYYY')}
426 {i18n.t('joined')} <MomentTime data={user} showAgo />
428 <div class="table-responsive mt-1">
429 <table class="table table-bordered table-sm mt-2 mb-0">
432 <td class="text-center" colSpan={2}>
433 {i18n.t('number_of_points', {
434 count: user.post_score + user.comment_score,
442 {i18n.t('number_of_points', { count: user.post_score })}
446 {i18n.t('number_of_posts', { count: user.number_of_posts })}
452 {i18n.t('number_of_points', { count: user.comment_score })}
456 {i18n.t('number_of_comments', {
457 count: user.number_of_comments,
463 {this.isCurrentUser ? (
465 class="btn btn-block btn-secondary mt-3"
466 onClick={linkEvent(this, this.handleLogoutClick)}
473 className={`btn btn-block btn-secondary mt-3 ${
474 !this.state.user.matrix_user_id && 'disabled'
478 href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
480 {i18n.t('send_secure_message')}
483 class="btn btn-block btn-secondary mt-3"
484 to={`/create_private_message?recipient_id=${this.state.user.id}`}
486 {i18n.t('send_message')}
499 <div class="card border-secondary mb-3">
500 <div class="card-body">
501 <h5>{i18n.t('settings')}</h5>
502 <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
503 <div class="form-group">
504 <label>{i18n.t('avatar')}</label>
505 <form class="d-inline">
507 htmlFor="file-upload"
508 class="pointer ml-4 text-muted small font-weight-bold"
510 {!this.state.userSettingsForm.avatar ? (
511 <span class="btn btn-sm btn-secondary">
512 {i18n.t('upload_avatar')}
518 src={this.state.userSettingsForm.avatar}
519 class="rounded-circle"
526 accept="image/*,video/*"
529 disabled={!UserService.Instance.user}
530 onChange={linkEvent(this, this.handleImageUpload)}
534 <div class="form-group">
535 <label>{i18n.t('language')}</label>
537 value={this.state.userSettingsForm.lang}
538 onChange={linkEvent(this, this.handleUserSettingsLangChange)}
539 class="ml-2 custom-select custom-select-sm w-auto"
541 <option disabled>{i18n.t('language')}</option>
542 <option value="browser">{i18n.t('browser_default')}</option>
543 <option disabled>──</option>
544 {languages.map(lang => (
545 <option value={lang.code}>{lang.name}</option>
549 <div class="form-group">
550 <label>{i18n.t('theme')}</label>
552 value={this.state.userSettingsForm.theme}
553 onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
554 class="ml-2 custom-select custom-select-sm w-auto"
556 <option disabled>{i18n.t('theme')}</option>
557 {themes.map(theme => (
558 <option value={theme}>{theme}</option>
562 <form className="form-group">
564 <div class="mr-2">{i18n.t('sort_type')}</div>
567 type_={this.state.userSettingsForm.default_listing_type}
568 onChange={this.handleUserSettingsListingTypeChange}
571 <form className="form-group">
573 <div class="mr-2">{i18n.t('type')}</div>
576 sort={this.state.userSettingsForm.default_sort_type}
577 onChange={this.handleUserSettingsSortTypeChange}
580 <div class="form-group row">
581 <label class="col-lg-3 col-form-label" htmlFor="user-email">
584 <div class="col-lg-9">
589 placeholder={i18n.t('optional')}
590 value={this.state.userSettingsForm.email}
593 this.handleUserSettingsEmailChange
599 <div class="form-group row">
600 <label class="col-lg-5 col-form-label">
602 href="https://about.riot.im/"
606 {i18n.t('matrix_user_id')}
609 <div class="col-lg-7">
613 placeholder="@user:example.com"
614 value={this.state.userSettingsForm.matrix_user_id}
617 this.handleUserSettingsMatrixUserIdChange
623 <div class="form-group row">
624 <label class="col-lg-5 col-form-label" htmlFor="user-password">
625 {i18n.t('new_password')}
627 <div class="col-lg-7">
632 value={this.state.userSettingsForm.new_password}
633 autoComplete="new-password"
636 this.handleUserSettingsNewPasswordChange
641 <div class="form-group row">
643 class="col-lg-5 col-form-label"
644 htmlFor="user-verify-password"
646 {i18n.t('verify_password')}
648 <div class="col-lg-7">
651 id="user-verify-password"
653 value={this.state.userSettingsForm.new_password_verify}
654 autoComplete="new-password"
657 this.handleUserSettingsNewPasswordVerifyChange
662 <div class="form-group row">
664 class="col-lg-5 col-form-label"
665 htmlFor="user-old-password"
667 {i18n.t('old_password')}
669 <div class="col-lg-7">
672 id="user-old-password"
674 value={this.state.userSettingsForm.old_password}
675 autoComplete="new-password"
678 this.handleUserSettingsOldPasswordChange
683 {WebSocketService.Instance.site.enable_nsfw && (
684 <div class="form-group">
685 <div class="form-check">
687 class="form-check-input"
690 checked={this.state.userSettingsForm.show_nsfw}
693 this.handleUserSettingsShowNsfwChange
696 <label class="form-check-label" htmlFor="user-show-nsfw">
697 {i18n.t('show_nsfw')}
702 <div class="form-group">
703 <div class="form-check">
705 class="form-check-input"
706 id="user-show-avatars"
708 checked={this.state.userSettingsForm.show_avatars}
711 this.handleUserSettingsShowAvatarsChange
714 <label class="form-check-label" htmlFor="user-show-avatars">
715 {i18n.t('show_avatars')}
719 <div class="form-group">
720 <div class="form-check">
722 class="form-check-input"
723 id="user-send-notifications-to-email"
725 disabled={!this.state.user.email}
727 this.state.userSettingsForm.send_notifications_to_email
731 this.handleUserSettingsSendNotificationsToEmailChange
735 class="form-check-label"
736 htmlFor="user-send-notifications-to-email"
738 {i18n.t('send_notifications_to_email')}
742 <div class="form-group">
743 <button type="submit" class="btn btn-block btn-secondary mr-4">
744 {this.state.userSettingsLoading ? (
745 <svg class="icon icon-spinner spin">
746 <use xlinkHref="#icon-spinner"></use>
749 capitalizeFirstLetter(i18n.t('save'))
754 <div class="form-group mb-0">
756 class="btn btn-block btn-danger"
759 this.handleDeleteAccountShowConfirmToggle
762 {i18n.t('delete_account')}
764 {this.state.deleteAccountShowConfirm && (
766 <div class="my-2 alert alert-danger" role="alert">
767 {i18n.t('delete_account_confirm')}
771 value={this.state.deleteAccountForm.password}
772 autoComplete="new-password"
775 this.handleDeleteAccountPasswordChange
777 class="form-control my-2"
780 class="btn btn-danger mr-4"
781 disabled={!this.state.deleteAccountForm.password}
782 onClick={linkEvent(this, this.handleDeleteAccount)}
784 {this.state.deleteAccountLoading ? (
785 <svg class="icon icon-spinner spin">
786 <use xlinkHref="#icon-spinner"></use>
789 capitalizeFirstLetter(i18n.t('delete'))
793 class="btn btn-secondary"
796 this.handleDeleteAccountShowConfirmToggle
814 {this.state.moderates.length > 0 && (
815 <div class="card border-secondary mb-3">
816 <div class="card-body">
817 <h5>{i18n.t('moderates')}</h5>
818 <ul class="list-unstyled mb-0">
819 {this.state.moderates.map(community => (
821 <Link to={`/c/${community.community_name}`}>
822 {community.community_name}
837 {this.state.follows.length > 0 && (
838 <div class="card border-secondary mb-3">
839 <div class="card-body">
840 <h5>{i18n.t('subscribed')}</h5>
841 <ul class="list-unstyled mb-0">
842 {this.state.follows.map(community => (
844 <Link to={`/c/${community.community_name}`}>
845 {community.community_name}
860 {this.state.page > 1 && (
862 class="btn btn-sm btn-secondary mr-1"
863 onClick={linkEvent(this, this.prevPage)}
869 class="btn btn-sm btn-secondary"
870 onClick={linkEvent(this, this.nextPage)}
879 let viewStr = View[this.state.view].toLowerCase();
880 let sortStr = SortType[this.state.sort].toLowerCase();
881 this.props.history.push(
882 `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
901 let form: GetUserDetailsForm = {
902 user_id: this.state.user_id,
903 username: this.state.username,
904 sort: SortType[this.state.sort],
905 saved_only: this.state.view == View.Saved,
906 page: this.state.page,
909 WebSocketService.Instance.getUserDetails(form);
912 handleSortChange(val: SortType) {
913 this.state.sort = val;
915 this.setState(this.state);
920 handleViewChange(i: User, event: any) {
921 i.state.view = Number(event.target.value);
928 handleUserSettingsShowNsfwChange(i: User, event: any) {
929 i.state.userSettingsForm.show_nsfw = event.target.checked;
933 handleUserSettingsShowAvatarsChange(i: User, event: any) {
934 i.state.userSettingsForm.show_avatars = event.target.checked;
935 UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
939 handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
940 i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
944 handleUserSettingsThemeChange(i: User, event: any) {
945 i.state.userSettingsForm.theme = event.target.value;
946 setTheme(event.target.value, true);
950 handleUserSettingsLangChange(i: User, event: any) {
951 i.state.userSettingsForm.lang = event.target.value;
952 i18n.changeLanguage(i.state.userSettingsForm.lang);
956 handleUserSettingsSortTypeChange(val: SortType) {
957 this.state.userSettingsForm.default_sort_type = val;
958 this.setState(this.state);
961 handleUserSettingsListingTypeChange(val: ListingType) {
962 this.state.userSettingsForm.default_listing_type = val;
963 this.setState(this.state);
966 handleUserSettingsEmailChange(i: User, event: any) {
967 i.state.userSettingsForm.email = event.target.value;
968 if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
969 i.state.userSettingsForm.email = undefined;
974 handleUserSettingsMatrixUserIdChange(i: User, event: any) {
975 i.state.userSettingsForm.matrix_user_id = event.target.value;
977 i.state.userSettingsForm.matrix_user_id == '' &&
978 !i.state.user.matrix_user_id
980 i.state.userSettingsForm.matrix_user_id = undefined;
985 handleUserSettingsNewPasswordChange(i: User, event: any) {
986 i.state.userSettingsForm.new_password = event.target.value;
987 if (i.state.userSettingsForm.new_password == '') {
988 i.state.userSettingsForm.new_password = undefined;
993 handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
994 i.state.userSettingsForm.new_password_verify = event.target.value;
995 if (i.state.userSettingsForm.new_password_verify == '') {
996 i.state.userSettingsForm.new_password_verify = undefined;
1001 handleUserSettingsOldPasswordChange(i: User, event: any) {
1002 i.state.userSettingsForm.old_password = event.target.value;
1003 if (i.state.userSettingsForm.old_password == '') {
1004 i.state.userSettingsForm.old_password = undefined;
1006 i.setState(i.state);
1009 handleImageUpload(i: User, event: any) {
1010 event.preventDefault();
1011 let file = event.target.files[0];
1012 const imageUploadUrl = `/pictrs/image`;
1013 const formData = new FormData();
1014 formData.append('images[]', file);
1016 i.state.avatarLoading = true;
1017 i.setState(i.state);
1019 fetch(imageUploadUrl, {
1023 .then(res => res.json())
1025 console.log('pictrs upload:');
1027 if (res.msg == 'ok') {
1028 let hash = res.files[0].file;
1029 let url = `${window.location.origin}/pictrs/image/${hash}`;
1030 i.state.userSettingsForm.avatar = url;
1031 i.state.avatarLoading = false;
1032 i.setState(i.state);
1034 i.state.avatarLoading = false;
1035 i.setState(i.state);
1036 toast(JSON.stringify(res), 'danger');
1040 i.state.avatarLoading = false;
1041 i.setState(i.state);
1042 toast(error, 'danger');
1046 handleUserSettingsSubmit(i: User, event: any) {
1047 event.preventDefault();
1048 i.state.userSettingsLoading = true;
1049 i.setState(i.state);
1051 WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
1054 handleDeleteAccountShowConfirmToggle(i: User, event: any) {
1055 event.preventDefault();
1056 i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
1057 i.setState(i.state);
1060 handleDeleteAccountPasswordChange(i: User, event: any) {
1061 i.state.deleteAccountForm.password = event.target.value;
1062 i.setState(i.state);
1065 handleLogoutClick(i: User) {
1066 UserService.Instance.logout();
1067 i.context.router.history.push('/');
1070 handleDeleteAccount(i: User, event: any) {
1071 event.preventDefault();
1072 i.state.deleteAccountLoading = true;
1073 i.setState(i.state);
1075 WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
1078 parseMessage(msg: WebSocketJsonResponse) {
1080 let res = wsJsonToRes(msg);
1082 toast(i18n.t(msg.error), 'danger');
1083 this.state.deleteAccountLoading = false;
1084 this.state.avatarLoading = false;
1085 this.state.userSettingsLoading = false;
1086 if (msg.error == 'couldnt_find_that_username_or_email') {
1087 this.context.router.history.push('/');
1089 this.setState(this.state);
1091 } else if (msg.reconnect) {
1093 } else if (res.op == UserOperation.GetUserDetails) {
1094 let data = res.data as UserDetailsResponse;
1095 this.state.user = data.user;
1096 this.state.comments = data.comments;
1097 this.state.follows = data.follows;
1098 this.state.moderates = data.moderates;
1099 this.state.posts = data.posts;
1100 this.state.admins = data.admins;
1101 this.state.loading = false;
1102 if (this.isCurrentUser) {
1103 this.state.userSettingsForm.show_nsfw =
1104 UserService.Instance.user.show_nsfw;
1105 this.state.userSettingsForm.theme = UserService.Instance.user.theme
1106 ? UserService.Instance.user.theme
1108 this.state.userSettingsForm.default_sort_type =
1109 UserService.Instance.user.default_sort_type;
1110 this.state.userSettingsForm.default_listing_type =
1111 UserService.Instance.user.default_listing_type;
1112 this.state.userSettingsForm.lang = UserService.Instance.user.lang;
1113 this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
1114 this.state.userSettingsForm.email = this.state.user.email;
1115 this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
1116 this.state.userSettingsForm.show_avatars =
1117 UserService.Instance.user.show_avatars;
1118 this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
1120 document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
1121 window.scrollTo(0, 0);
1122 this.setState(this.state);
1124 } else if (res.op == UserOperation.EditComment) {
1125 let data = res.data as CommentResponse;
1126 editCommentRes(data, this.state.comments);
1127 this.setState(this.state);
1128 } else if (res.op == UserOperation.CreateComment) {
1129 let data = res.data as CommentResponse;
1131 UserService.Instance.user &&
1132 data.comment.creator_id == UserService.Instance.user.id
1134 toast(i18n.t('reply_sent'));
1136 } else if (res.op == UserOperation.SaveComment) {
1137 let data = res.data as CommentResponse;
1138 saveCommentRes(data, this.state.comments);
1139 this.setState(this.state);
1140 } else if (res.op == UserOperation.CreateCommentLike) {
1141 let data = res.data as CommentResponse;
1142 createCommentLikeRes(data, this.state.comments);
1143 this.setState(this.state);
1144 } else if (res.op == UserOperation.CreatePostLike) {
1145 let data = res.data as PostResponse;
1146 createPostLikeFindRes(data, this.state.posts);
1147 this.setState(this.state);
1148 } else if (res.op == UserOperation.BanUser) {
1149 let data = res.data as BanUserResponse;
1151 .filter(c => c.creator_id == data.user.id)
1152 .forEach(c => (c.banned = data.banned));
1154 .filter(c => c.creator_id == data.user.id)
1155 .forEach(c => (c.banned = data.banned));
1156 this.setState(this.state);
1157 } else if (res.op == UserOperation.AddAdmin) {
1158 let data = res.data as AddAdminResponse;
1159 this.state.admins = data.admins;
1160 this.setState(this.state);
1161 } else if (res.op == UserOperation.SaveUserSettings) {
1162 let data = res.data as LoginResponse;
1163 this.state = this.emptyState;
1164 this.state.userSettingsLoading = false;
1165 this.setState(this.state);
1166 UserService.Instance.login(data);
1167 } else if (res.op == UserOperation.DeleteAccount) {
1168 this.state.deleteAccountLoading = false;
1169 this.state.deleteAccountShowConfirm = false;
1170 this.setState(this.state);
1171 this.context.router.history.push('/');