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';
21 CreatePostLikeResponse,
22 } from '../interfaces';
23 import { WebSocketService, UserService } from '../services';
28 capitalizeFirstLetter,
34 import { PostListing } from './post-listing';
35 import { SortSelect } from './sort-select';
36 import { ListingTypeSelect } from './listing-type-select';
37 import { CommentNodes } from './comment-nodes';
38 import { MomentTime } from './moment-time';
39 import { i18n } from '../i18next';
40 import { T } from 'inferno-i18next';
53 follows: Array<CommunityUser>;
54 moderates: Array<CommunityUser>;
55 comments: Array<Comment>;
58 admins: Array<UserView>;
63 avatarLoading: boolean;
64 userSettingsForm: UserSettingsForm;
65 userSettingsLoading: boolean;
66 deleteAccountLoading: boolean;
67 deleteAccountShowConfirm: boolean;
68 deleteAccountForm: DeleteAccountForm;
71 export class User extends Component<any, UserState> {
72 private subscription: Subscription;
73 private emptyState: UserState = {
79 number_of_posts: null,
81 number_of_comments: null,
86 send_notifications_to_email: null,
97 view: this.getViewFromProps(this.props),
98 sort: this.getSortTypeFromProps(this.props),
99 page: this.getPageFromProps(this.props),
103 default_sort_type: null,
104 default_listing_type: null,
107 send_notifications_to_email: null,
110 userSettingsLoading: null,
111 deleteAccountLoading: null,
112 deleteAccountShowConfirm: false,
118 constructor(props: any, context: any) {
119 super(props, context);
121 this.state = this.emptyState;
122 this.handleSortChange = this.handleSortChange.bind(this);
123 this.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind(
126 this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
130 this.state.user_id = Number(this.props.match.params.id);
131 this.state.username = this.props.match.params.username;
133 this.subscription = WebSocketService.Instance.subject
134 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
136 msg => this.parseMessage(msg),
137 err => console.error(err),
138 () => console.log('complete')
144 get isCurrentUser() {
146 UserService.Instance.user &&
147 UserService.Instance.user.id == this.state.user.id
151 getViewFromProps(props: any): View {
152 return props.match.params.view
153 ? View[capitalizeFirstLetter(props.match.params.view)]
157 getSortTypeFromProps(props: any): SortType {
158 return props.match.params.sort
159 ? routeSortTypeToEnum(props.match.params.sort)
163 getPageFromProps(props: any): number {
164 return props.match.params.page ? Number(props.match.params.page) : 1;
167 componentWillUnmount() {
168 this.subscription.unsubscribe();
171 // Necessary for back button for some reason
172 componentWillReceiveProps(nextProps: any) {
174 nextProps.history.action == 'POP' ||
175 nextProps.history.action == 'PUSH'
177 this.state.view = this.getViewFromProps(nextProps);
178 this.state.sort = this.getSortTypeFromProps(nextProps);
179 this.state.page = this.getPageFromProps(nextProps);
180 this.setState(this.state);
185 componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
186 // Necessary if you are on a post and you click another post (same route)
188 lastProps.location.pathname.split('/')[2] !==
189 lastProps.history.location.pathname.split('/')[2]
191 // Couldnt get a refresh working. This does for now.
198 <div class="container">
199 {this.state.loading ? (
201 <svg class="icon icon-spinner spin">
202 <use xlinkHref="#icon-spinner"></use>
207 <div class="col-12 col-md-8">
209 {this.state.user.avatar && showAvatars() && (
213 src={this.state.user.avatar}
214 class="rounded-circle mr-2"
217 <span>/u/{this.state.user.name}</span>
220 {this.state.view == View.Overview && this.overview()}
221 {this.state.view == View.Comments && this.comments()}
222 {this.state.view == View.Posts && this.posts()}
223 {this.state.view == View.Saved && this.overview()}
226 <div class="col-12 col-md-4">
228 {this.isCurrentUser && this.userSettings()}
240 <div className="mb-2">
242 value={this.state.view}
243 onChange={linkEvent(this, this.handleViewChange)}
244 class="custom-select custom-select-sm w-auto"
247 <T i18nKey="view">#</T>
249 <option value={View.Overview}>
250 <T i18nKey="overview">#</T>
252 <option value={View.Comments}>
253 <T i18nKey="comments">#</T>
255 <option value={View.Posts}>
256 <T i18nKey="posts">#</T>
258 <option value={View.Saved}>
259 <T i18nKey="saved">#</T>
264 sort={this.state.sort}
265 onChange={this.handleSortChange}
270 href={`/feeds/u/${this.state.username}.xml?sort=${
271 SortType[this.state.sort]
275 <svg class="icon mx-2 text-muted small">
276 <use xlinkHref="#icon-rss">#</use>
284 let combined: Array<{ type_: string; data: Comment | Post }> = [];
285 let comments = this.state.comments.map(e => {
286 return { type_: 'comments', data: e };
288 let posts = this.state.posts.map(e => {
289 return { type_: 'posts', data: e };
292 combined.push(...comments);
293 combined.push(...posts);
296 if (this.state.sort == SortType.New) {
297 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
299 combined.sort((a, b) => b.data.score - a.data.score);
306 {i.type_ == 'posts' ? (
308 post={i.data as Post}
309 admins={this.state.admins}
314 nodes={[{ comment: i.data as Comment }]}
315 admins={this.state.admins}
328 {this.state.comments.map(comment => (
330 nodes={[{ comment: comment }]}
331 admins={this.state.admins}
342 {this.state.posts.map(post => (
343 <PostListing post={post} admins={this.state.admins} showCommunity />
350 let user = this.state.user;
353 <div class="card border-secondary mb-3">
354 <div class="card-body">
356 <ul class="list-inline mb-0">
357 <li className="list-inline-item">{user.name}</li>
359 <li className="list-inline-item badge badge-danger">
360 <T i18nKey="banned">#</T>
366 {i18n.t('joined')} <MomentTime data={user} />
368 <div class="table-responsive">
369 <table class="table table-bordered table-sm mt-2 mb-0">
373 i18nKey="number_of_points"
374 interpolation={{ count: user.post_score }}
381 i18nKey="number_of_posts"
382 interpolation={{ count: user.number_of_posts }}
391 i18nKey="number_of_points"
392 interpolation={{ count: user.comment_score }}
399 i18nKey="number_of_comments"
400 interpolation={{ count: user.number_of_comments }}
408 {this.isCurrentUser ? (
410 class="btn btn-block btn-secondary mt-3"
411 onClick={linkEvent(this, this.handleLogoutClick)}
413 <T i18nKey="logout">#</T>
418 className={`btn btn-block btn-secondary mt-3 ${!this.state
419 .user.matrix_user_id && 'disabled'}`}
421 href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
423 {i18n.t('send_secure_message')}
426 class="btn btn-block btn-secondary mt-3"
427 to={`/create_private_message?recipient_id=${this.state.user.id}`}
429 {i18n.t('send_message')}
442 <div class="card border-secondary mb-3">
443 <div class="card-body">
445 <T i18nKey="settings">#</T>
447 <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
448 <div class="form-group">
450 <T i18nKey="avatar">#</T>
452 <form class="d-inline">
454 htmlFor="file-upload"
455 class="pointer ml-4 text-muted small font-weight-bold"
457 {!this.state.userSettingsForm.avatar ? (
458 <span class="btn btn-sm btn-secondary">
459 <T i18nKey="upload_avatar">#</T>
465 src={this.state.userSettingsForm.avatar}
466 class="rounded-circle"
473 accept="image/*,video/*"
476 disabled={!UserService.Instance.user}
477 onChange={linkEvent(this, this.handleImageUpload)}
481 <div class="form-group">
483 <T i18nKey="language">#</T>
486 value={this.state.userSettingsForm.lang}
487 onChange={linkEvent(this, this.handleUserSettingsLangChange)}
488 class="ml-2 custom-select custom-select-sm w-auto"
491 <T i18nKey="language">#</T>
493 <option value="browser">
494 <T i18nKey="browser_default">#</T>
496 <option disabled>──</option>
497 {languages.map(lang => (
498 <option value={lang.code}>{lang.name}</option>
502 <div class="form-group">
504 <T i18nKey="theme">#</T>
507 value={this.state.userSettingsForm.theme}
508 onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
509 class="ml-2 custom-select custom-select-sm w-auto"
512 <T i18nKey="theme">#</T>
514 {themes.map(theme => (
515 <option value={theme}>{theme}</option>
519 <form className="form-group">
521 <T i18nKey="sort_type" class="mr-2">
526 type_={this.state.userSettingsForm.default_listing_type}
527 onChange={this.handleUserSettingsListingTypeChange}
530 <form className="form-group">
532 <T i18nKey="type" class="mr-2">
537 sort={this.state.userSettingsForm.default_sort_type}
538 onChange={this.handleUserSettingsSortTypeChange}
541 <div class="form-group row">
542 <label class="col-lg-3 col-form-label">
543 <T i18nKey="email">#</T>
545 <div class="col-lg-9">
549 placeholder={i18n.t('optional')}
550 value={this.state.userSettingsForm.email}
553 this.handleUserSettingsEmailChange
559 <div class="form-group row">
560 <label class="col-lg-5 col-form-label">
561 <a href="https://about.riot.im/" target="_blank">
562 {i18n.t('matrix_user_id')}
565 <div class="col-lg-7">
569 placeholder="@user:example.com"
570 value={this.state.userSettingsForm.matrix_user_id}
573 this.handleUserSettingsMatrixUserIdChange
579 <div class="form-group row">
580 <label class="col-lg-5 col-form-label">
581 <T i18nKey="new_password">#</T>
583 <div class="col-lg-7">
587 value={this.state.userSettingsForm.new_password}
590 this.handleUserSettingsNewPasswordChange
595 <div class="form-group row">
596 <label class="col-lg-5 col-form-label">
597 <T i18nKey="verify_password">#</T>
599 <div class="col-lg-7">
603 value={this.state.userSettingsForm.new_password_verify}
606 this.handleUserSettingsNewPasswordVerifyChange
611 <div class="form-group row">
612 <label class="col-lg-5 col-form-label">
613 <T i18nKey="old_password">#</T>
615 <div class="col-lg-7">
619 value={this.state.userSettingsForm.old_password}
622 this.handleUserSettingsOldPasswordChange
627 {WebSocketService.Instance.site.enable_nsfw && (
628 <div class="form-group">
629 <div class="form-check">
631 class="form-check-input"
633 checked={this.state.userSettingsForm.show_nsfw}
636 this.handleUserSettingsShowNsfwChange
639 <label class="form-check-label">
640 <T i18nKey="show_nsfw">#</T>
645 <div class="form-group">
646 <div class="form-check">
648 class="form-check-input"
650 checked={this.state.userSettingsForm.show_avatars}
653 this.handleUserSettingsShowAvatarsChange
656 <label class="form-check-label">
657 <T i18nKey="show_avatars">#</T>
661 <div class="form-group">
662 <div class="form-check">
664 class="form-check-input"
666 disabled={!this.state.user.email}
668 this.state.userSettingsForm.send_notifications_to_email
672 this.handleUserSettingsSendNotificationsToEmailChange
675 <label class="form-check-label">
676 <T i18nKey="send_notifications_to_email">#</T>
680 <div class="form-group">
681 <button type="submit" class="btn btn-block btn-secondary mr-4">
682 {this.state.userSettingsLoading ? (
683 <svg class="icon icon-spinner spin">
684 <use xlinkHref="#icon-spinner"></use>
687 capitalizeFirstLetter(i18n.t('save'))
692 <div class="form-group mb-0">
694 class="btn btn-block btn-danger"
697 this.handleDeleteAccountShowConfirmToggle
700 <T i18nKey="delete_account">#</T>
702 {this.state.deleteAccountShowConfirm && (
704 <div class="my-2 alert alert-danger" role="alert">
705 <T i18nKey="delete_account_confirm">#</T>
709 value={this.state.deleteAccountForm.password}
712 this.handleDeleteAccountPasswordChange
714 class="form-control my-2"
717 class="btn btn-danger mr-4"
718 disabled={!this.state.deleteAccountForm.password}
719 onClick={linkEvent(this, this.handleDeleteAccount)}
721 {this.state.deleteAccountLoading ? (
722 <svg class="icon icon-spinner spin">
723 <use xlinkHref="#icon-spinner"></use>
726 capitalizeFirstLetter(i18n.t('delete'))
730 class="btn btn-secondary"
733 this.handleDeleteAccountShowConfirmToggle
736 <T i18nKey="cancel">#</T>
751 {this.state.moderates.length > 0 && (
752 <div class="card border-secondary mb-3">
753 <div class="card-body">
755 <T i18nKey="moderates">#</T>
757 <ul class="list-unstyled mb-0">
758 {this.state.moderates.map(community => (
760 <Link to={`/c/${community.community_name}`}>
761 {community.community_name}
776 {this.state.follows.length > 0 && (
777 <div class="card border-secondary mb-3">
778 <div class="card-body">
780 <T i18nKey="subscribed">#</T>
782 <ul class="list-unstyled mb-0">
783 {this.state.follows.map(community => (
785 <Link to={`/c/${community.community_name}`}>
786 {community.community_name}
801 {this.state.page > 1 && (
803 class="btn btn-sm btn-secondary mr-1"
804 onClick={linkEvent(this, this.prevPage)}
806 <T i18nKey="prev">#</T>
810 class="btn btn-sm btn-secondary"
811 onClick={linkEvent(this, this.nextPage)}
813 <T i18nKey="next">#</T>
820 let viewStr = View[this.state.view].toLowerCase();
821 let sortStr = SortType[this.state.sort].toLowerCase();
822 this.props.history.push(
823 `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
842 let form: GetUserDetailsForm = {
843 user_id: this.state.user_id,
844 username: this.state.username,
845 sort: SortType[this.state.sort],
846 saved_only: this.state.view == View.Saved,
847 page: this.state.page,
850 WebSocketService.Instance.getUserDetails(form);
853 handleSortChange(val: SortType) {
854 this.state.sort = val;
856 this.setState(this.state);
861 handleViewChange(i: User, event: any) {
862 i.state.view = Number(event.target.value);
869 handleUserSettingsShowNsfwChange(i: User, event: any) {
870 i.state.userSettingsForm.show_nsfw = event.target.checked;
874 handleUserSettingsShowAvatarsChange(i: User, event: any) {
875 i.state.userSettingsForm.show_avatars = event.target.checked;
876 UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
880 handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
881 i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
885 handleUserSettingsThemeChange(i: User, event: any) {
886 i.state.userSettingsForm.theme = event.target.value;
887 setTheme(event.target.value);
891 handleUserSettingsLangChange(i: User, event: any) {
892 i.state.userSettingsForm.lang = event.target.value;
893 i18n.changeLanguage(i.state.userSettingsForm.lang);
897 handleUserSettingsSortTypeChange(val: SortType) {
898 this.state.userSettingsForm.default_sort_type = val;
899 this.setState(this.state);
902 handleUserSettingsListingTypeChange(val: ListingType) {
903 this.state.userSettingsForm.default_listing_type = val;
904 this.setState(this.state);
907 handleUserSettingsEmailChange(i: User, event: any) {
908 i.state.userSettingsForm.email = event.target.value;
909 if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
910 i.state.userSettingsForm.email = undefined;
915 handleUserSettingsMatrixUserIdChange(i: User, event: any) {
916 i.state.userSettingsForm.matrix_user_id = event.target.value;
918 i.state.userSettingsForm.matrix_user_id == '' &&
919 !i.state.user.matrix_user_id
921 i.state.userSettingsForm.matrix_user_id = undefined;
926 handleUserSettingsNewPasswordChange(i: User, event: any) {
927 i.state.userSettingsForm.new_password = event.target.value;
928 if (i.state.userSettingsForm.new_password == '') {
929 i.state.userSettingsForm.new_password = undefined;
934 handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
935 i.state.userSettingsForm.new_password_verify = event.target.value;
936 if (i.state.userSettingsForm.new_password_verify == '') {
937 i.state.userSettingsForm.new_password_verify = undefined;
942 handleUserSettingsOldPasswordChange(i: User, event: any) {
943 i.state.userSettingsForm.old_password = event.target.value;
944 if (i.state.userSettingsForm.old_password == '') {
945 i.state.userSettingsForm.old_password = undefined;
950 handleImageUpload(i: User, event: any) {
951 event.preventDefault();
952 let file = event.target.files[0];
953 const imageUploadUrl = `/pictshare/api/upload.php`;
954 const formData = new FormData();
955 formData.append('file', file);
957 i.state.avatarLoading = true;
960 fetch(imageUploadUrl, {
964 .then(res => res.json())
966 let url = `${window.location.origin}/pictshare/${res.url}`;
967 if (res.filetype == 'mp4') {
970 i.state.userSettingsForm.avatar = url;
972 i.state.avatarLoading = false;
976 i.state.avatarLoading = false;
982 handleUserSettingsSubmit(i: User, event: any) {
983 event.preventDefault();
984 i.state.userSettingsLoading = true;
987 WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
990 handleDeleteAccountShowConfirmToggle(i: User, event: any) {
991 event.preventDefault();
992 i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
996 handleDeleteAccountPasswordChange(i: User, event: any) {
997 i.state.deleteAccountForm.password = event.target.value;
1001 handleLogoutClick(i: User) {
1002 UserService.Instance.logout();
1003 i.context.router.history.push('/');
1006 handleDeleteAccount(i: User, event: any) {
1007 event.preventDefault();
1008 i.state.deleteAccountLoading = true;
1009 i.setState(i.state);
1011 WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
1014 parseMessage(msg: any) {
1016 let op: UserOperation = msgOp(msg);
1018 alert(i18n.t(msg.error));
1019 this.state.deleteAccountLoading = false;
1020 this.state.avatarLoading = false;
1021 this.state.userSettingsLoading = false;
1022 if (msg.error == 'couldnt_find_that_username_or_email') {
1023 this.context.router.history.push('/');
1025 this.setState(this.state);
1027 } else if (op == UserOperation.GetUserDetails) {
1028 let res: UserDetailsResponse = msg;
1029 this.state.user = res.user;
1030 this.state.comments = res.comments;
1031 this.state.follows = res.follows;
1032 this.state.moderates = res.moderates;
1033 this.state.posts = res.posts;
1034 this.state.admins = res.admins;
1035 this.state.loading = false;
1036 if (this.isCurrentUser) {
1037 this.state.userSettingsForm.show_nsfw =
1038 UserService.Instance.user.show_nsfw;
1039 this.state.userSettingsForm.theme = UserService.Instance.user.theme
1040 ? UserService.Instance.user.theme
1042 this.state.userSettingsForm.default_sort_type =
1043 UserService.Instance.user.default_sort_type;
1044 this.state.userSettingsForm.default_listing_type =
1045 UserService.Instance.user.default_listing_type;
1046 this.state.userSettingsForm.lang = UserService.Instance.user.lang;
1047 this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
1048 this.state.userSettingsForm.email = this.state.user.email;
1049 this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
1050 this.state.userSettingsForm.show_avatars =
1051 UserService.Instance.user.show_avatars;
1052 this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
1054 document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
1055 window.scrollTo(0, 0);
1056 this.setState(this.state);
1057 } else if (op == UserOperation.EditComment) {
1058 let res: CommentResponse = msg;
1060 let found = this.state.comments.find(c => c.id == res.comment.id);
1061 found.content = res.comment.content;
1062 found.updated = res.comment.updated;
1063 found.removed = res.comment.removed;
1064 found.deleted = res.comment.deleted;
1065 found.upvotes = res.comment.upvotes;
1066 found.downvotes = res.comment.downvotes;
1067 found.score = res.comment.score;
1069 this.setState(this.state);
1070 } else if (op == UserOperation.CreateComment) {
1071 // let res: CommentResponse = msg;
1072 alert(i18n.t('reply_sent'));
1073 // this.state.comments.unshift(res.comment); // TODO do this right
1074 // this.setState(this.state);
1075 } else if (op == UserOperation.SaveComment) {
1076 let res: CommentResponse = msg;
1077 let found = this.state.comments.find(c => c.id == res.comment.id);
1078 found.saved = res.comment.saved;
1079 this.setState(this.state);
1080 } else if (op == UserOperation.CreateCommentLike) {
1081 let res: CommentResponse = msg;
1082 let found: Comment = this.state.comments.find(
1083 c => c.id === res.comment.id
1085 found.score = res.comment.score;
1086 found.upvotes = res.comment.upvotes;
1087 found.downvotes = res.comment.downvotes;
1088 if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
1089 this.setState(this.state);
1090 } else if (op == UserOperation.CreatePostLike) {
1091 let res: CreatePostLikeResponse = msg;
1092 let found = this.state.posts.find(c => c.id == res.post.id);
1093 found.my_vote = res.post.my_vote;
1094 found.score = res.post.score;
1095 found.upvotes = res.post.upvotes;
1096 found.downvotes = res.post.downvotes;
1097 this.setState(this.state);
1098 } else if (op == UserOperation.BanUser) {
1099 let res: BanUserResponse = msg;
1101 .filter(c => c.creator_id == res.user.id)
1102 .forEach(c => (c.banned = res.banned));
1104 .filter(c => c.creator_id == res.user.id)
1105 .forEach(c => (c.banned = res.banned));
1106 this.setState(this.state);
1107 } else if (op == UserOperation.AddAdmin) {
1108 let res: AddAdminResponse = msg;
1109 this.state.admins = res.admins;
1110 this.setState(this.state);
1111 } else if (op == UserOperation.SaveUserSettings) {
1112 this.state = this.emptyState;
1113 this.state.userSettingsLoading = false;
1114 this.setState(this.state);
1115 let res: LoginResponse = msg;
1116 UserService.Instance.login(res);
1117 } else if (op == UserOperation.DeleteAccount) {
1118 this.state.deleteAccountLoading = false;
1119 this.state.deleteAccountShowConfirm = false;
1120 this.setState(this.state);
1121 this.context.router.history.push('/');