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>
425 <div class="card border-secondary mb-3">
426 <div class="card-body">
428 <T i18nKey="settings">#</T>
430 <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
431 <div class="form-group">
433 <T i18nKey="avatar">#</T>
435 <form class="d-inline">
437 htmlFor="file-upload"
438 class="pointer ml-4 text-muted small font-weight-bold"
440 {!this.state.userSettingsForm.avatar ? (
441 <span class="btn btn-sm btn-secondary">
442 <T i18nKey="upload_avatar">#</T>
448 src={this.state.userSettingsForm.avatar}
449 class="rounded-circle"
456 accept="image/*,video/*"
459 disabled={!UserService.Instance.user}
460 onChange={linkEvent(this, this.handleImageUpload)}
464 <div class="form-group">
466 <T i18nKey="language">#</T>
469 value={this.state.userSettingsForm.lang}
470 onChange={linkEvent(this, this.handleUserSettingsLangChange)}
471 class="ml-2 custom-select custom-select-sm w-auto"
474 <T i18nKey="language">#</T>
476 <option value="browser">
477 <T i18nKey="browser_default">#</T>
479 <option disabled>──</option>
480 {languages.map(lang => (
481 <option value={lang.code}>{lang.name}</option>
485 <div class="form-group">
487 <T i18nKey="theme">#</T>
490 value={this.state.userSettingsForm.theme}
491 onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
492 class="ml-2 custom-select custom-select-sm w-auto"
495 <T i18nKey="theme">#</T>
497 {themes.map(theme => (
498 <option value={theme}>{theme}</option>
502 <form className="form-group">
504 <T i18nKey="sort_type" class="mr-2">
509 type_={this.state.userSettingsForm.default_listing_type}
510 onChange={this.handleUserSettingsListingTypeChange}
513 <form className="form-group">
515 <T i18nKey="type" class="mr-2">
520 sort={this.state.userSettingsForm.default_sort_type}
521 onChange={this.handleUserSettingsSortTypeChange}
524 <div class="form-group row">
525 <label class="col-lg-3 col-form-label">
526 <T i18nKey="email">#</T>
528 <div class="col-lg-9">
532 placeholder={i18n.t('optional')}
533 value={this.state.userSettingsForm.email}
536 this.handleUserSettingsEmailChange
542 <div class="form-group row">
543 <label class="col-lg-5 col-form-label">
544 <T i18nKey="new_password">#</T>
546 <div class="col-lg-7">
550 value={this.state.userSettingsForm.new_password}
553 this.handleUserSettingsNewPasswordChange
558 <div class="form-group row">
559 <label class="col-lg-5 col-form-label">
560 <T i18nKey="verify_password">#</T>
562 <div class="col-lg-7">
566 value={this.state.userSettingsForm.new_password_verify}
569 this.handleUserSettingsNewPasswordVerifyChange
574 <div class="form-group row">
575 <label class="col-lg-5 col-form-label">
576 <T i18nKey="old_password">#</T>
578 <div class="col-lg-7">
582 value={this.state.userSettingsForm.old_password}
585 this.handleUserSettingsOldPasswordChange
590 {WebSocketService.Instance.site.enable_nsfw && (
591 <div class="form-group">
592 <div class="form-check">
594 class="form-check-input"
596 checked={this.state.userSettingsForm.show_nsfw}
599 this.handleUserSettingsShowNsfwChange
602 <label class="form-check-label">
603 <T i18nKey="show_nsfw">#</T>
608 <div class="form-group">
609 <div class="form-check">
611 class="form-check-input"
613 checked={this.state.userSettingsForm.show_avatars}
616 this.handleUserSettingsShowAvatarsChange
619 <label class="form-check-label">
620 <T i18nKey="show_avatars">#</T>
624 <div class="form-group">
625 <div class="form-check">
627 class="form-check-input"
629 disabled={!this.state.user.email}
631 this.state.userSettingsForm.send_notifications_to_email
635 this.handleUserSettingsSendNotificationsToEmailChange
638 <label class="form-check-label">
639 <T i18nKey="send_notifications_to_email">#</T>
643 <div class="form-group">
644 <button type="submit" class="btn btn-block btn-secondary mr-4">
645 {this.state.userSettingsLoading ? (
646 <svg class="icon icon-spinner spin">
647 <use xlinkHref="#icon-spinner"></use>
650 capitalizeFirstLetter(i18n.t('save'))
655 <div class="form-group mb-0">
657 class="btn btn-block btn-danger"
660 this.handleDeleteAccountShowConfirmToggle
663 <T i18nKey="delete_account">#</T>
665 {this.state.deleteAccountShowConfirm && (
667 <div class="my-2 alert alert-danger" role="alert">
668 <T i18nKey="delete_account_confirm">#</T>
672 value={this.state.deleteAccountForm.password}
675 this.handleDeleteAccountPasswordChange
677 class="form-control my-2"
680 class="btn btn-danger mr-4"
681 disabled={!this.state.deleteAccountForm.password}
682 onClick={linkEvent(this, this.handleDeleteAccount)}
684 {this.state.deleteAccountLoading ? (
685 <svg class="icon icon-spinner spin">
686 <use xlinkHref="#icon-spinner"></use>
689 capitalizeFirstLetter(i18n.t('delete'))
693 class="btn btn-secondary"
696 this.handleDeleteAccountShowConfirmToggle
699 <T i18nKey="cancel">#</T>
714 {this.state.moderates.length > 0 && (
715 <div class="card border-secondary mb-3">
716 <div class="card-body">
718 <T i18nKey="moderates">#</T>
720 <ul class="list-unstyled mb-0">
721 {this.state.moderates.map(community => (
723 <Link to={`/c/${community.community_name}`}>
724 {community.community_name}
739 {this.state.follows.length > 0 && (
740 <div class="card border-secondary mb-3">
741 <div class="card-body">
743 <T i18nKey="subscribed">#</T>
745 <ul class="list-unstyled mb-0">
746 {this.state.follows.map(community => (
748 <Link to={`/c/${community.community_name}`}>
749 {community.community_name}
764 {this.state.page > 1 && (
766 class="btn btn-sm btn-secondary mr-1"
767 onClick={linkEvent(this, this.prevPage)}
769 <T i18nKey="prev">#</T>
773 class="btn btn-sm btn-secondary"
774 onClick={linkEvent(this, this.nextPage)}
776 <T i18nKey="next">#</T>
783 let viewStr = View[this.state.view].toLowerCase();
784 let sortStr = SortType[this.state.sort].toLowerCase();
785 this.props.history.push(
786 `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
805 let form: GetUserDetailsForm = {
806 user_id: this.state.user_id,
807 username: this.state.username,
808 sort: SortType[this.state.sort],
809 saved_only: this.state.view == View.Saved,
810 page: this.state.page,
813 WebSocketService.Instance.getUserDetails(form);
816 handleSortChange(val: SortType) {
817 this.state.sort = val;
819 this.setState(this.state);
824 handleViewChange(i: User, event: any) {
825 i.state.view = Number(event.target.value);
832 handleUserSettingsShowNsfwChange(i: User, event: any) {
833 i.state.userSettingsForm.show_nsfw = event.target.checked;
837 handleUserSettingsShowAvatarsChange(i: User, event: any) {
838 i.state.userSettingsForm.show_avatars = event.target.checked;
839 UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates
843 handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) {
844 i.state.userSettingsForm.send_notifications_to_email = event.target.checked;
848 handleUserSettingsThemeChange(i: User, event: any) {
849 i.state.userSettingsForm.theme = event.target.value;
850 setTheme(event.target.value);
854 handleUserSettingsLangChange(i: User, event: any) {
855 i.state.userSettingsForm.lang = event.target.value;
856 i18n.changeLanguage(i.state.userSettingsForm.lang);
860 handleUserSettingsSortTypeChange(val: SortType) {
861 this.state.userSettingsForm.default_sort_type = val;
862 this.setState(this.state);
865 handleUserSettingsListingTypeChange(val: ListingType) {
866 this.state.userSettingsForm.default_listing_type = val;
867 this.setState(this.state);
870 handleUserSettingsEmailChange(i: User, event: any) {
871 i.state.userSettingsForm.email = event.target.value;
872 if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
873 i.state.userSettingsForm.email = undefined;
878 handleUserSettingsNewPasswordChange(i: User, event: any) {
879 i.state.userSettingsForm.new_password = event.target.value;
880 if (i.state.userSettingsForm.new_password == '') {
881 i.state.userSettingsForm.new_password = undefined;
886 handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
887 i.state.userSettingsForm.new_password_verify = event.target.value;
888 if (i.state.userSettingsForm.new_password_verify == '') {
889 i.state.userSettingsForm.new_password_verify = undefined;
894 handleUserSettingsOldPasswordChange(i: User, event: any) {
895 i.state.userSettingsForm.old_password = event.target.value;
896 if (i.state.userSettingsForm.old_password == '') {
897 i.state.userSettingsForm.old_password = undefined;
902 handleImageUpload(i: User, event: any) {
903 event.preventDefault();
904 let file = event.target.files[0];
905 const imageUploadUrl = `/pictshare/api/upload.php`;
906 const formData = new FormData();
907 formData.append('file', file);
909 i.state.avatarLoading = true;
912 fetch(imageUploadUrl, {
916 .then(res => res.json())
918 let url = `${window.location.origin}/pictshare/${res.url}`;
919 if (res.filetype == 'mp4') {
922 i.state.userSettingsForm.avatar = url;
924 i.state.avatarLoading = false;
928 i.state.avatarLoading = false;
934 handleUserSettingsSubmit(i: User, event: any) {
935 event.preventDefault();
936 i.state.userSettingsLoading = true;
939 WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
942 handleDeleteAccountShowConfirmToggle(i: User, event: any) {
943 event.preventDefault();
944 i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
948 handleDeleteAccountPasswordChange(i: User, event: any) {
949 i.state.deleteAccountForm.password = event.target.value;
953 handleLogoutClick(i: User) {
954 UserService.Instance.logout();
955 i.context.router.history.push('/');
958 handleDeleteAccount(i: User, event: any) {
959 event.preventDefault();
960 i.state.deleteAccountLoading = true;
963 WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
966 parseMessage(msg: any) {
968 let op: UserOperation = msgOp(msg);
970 alert(i18n.t(msg.error));
971 this.state.deleteAccountLoading = false;
972 this.state.avatarLoading = false;
973 this.state.userSettingsLoading = false;
974 if (msg.error == 'couldnt_find_that_username_or_email') {
975 this.context.router.history.push('/');
977 this.setState(this.state);
979 } else if (op == UserOperation.GetUserDetails) {
980 let res: UserDetailsResponse = msg;
981 this.state.user = res.user;
982 this.state.comments = res.comments;
983 this.state.follows = res.follows;
984 this.state.moderates = res.moderates;
985 this.state.posts = res.posts;
986 this.state.admins = res.admins;
987 this.state.loading = false;
988 if (this.isCurrentUser) {
989 this.state.userSettingsForm.show_nsfw =
990 UserService.Instance.user.show_nsfw;
991 this.state.userSettingsForm.theme = UserService.Instance.user.theme
992 ? UserService.Instance.user.theme
994 this.state.userSettingsForm.default_sort_type =
995 UserService.Instance.user.default_sort_type;
996 this.state.userSettingsForm.default_listing_type =
997 UserService.Instance.user.default_listing_type;
998 this.state.userSettingsForm.lang = UserService.Instance.user.lang;
999 this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
1000 this.state.userSettingsForm.email = this.state.user.email;
1001 this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
1002 this.state.userSettingsForm.show_avatars =
1003 UserService.Instance.user.show_avatars;
1005 document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
1006 window.scrollTo(0, 0);
1007 this.setState(this.state);
1008 } else if (op == UserOperation.EditComment) {
1009 let res: CommentResponse = msg;
1011 let found = this.state.comments.find(c => c.id == res.comment.id);
1012 found.content = res.comment.content;
1013 found.updated = res.comment.updated;
1014 found.removed = res.comment.removed;
1015 found.deleted = res.comment.deleted;
1016 found.upvotes = res.comment.upvotes;
1017 found.downvotes = res.comment.downvotes;
1018 found.score = res.comment.score;
1020 this.setState(this.state);
1021 } else if (op == UserOperation.CreateComment) {
1022 // let res: CommentResponse = msg;
1023 alert(i18n.t('reply_sent'));
1024 // this.state.comments.unshift(res.comment); // TODO do this right
1025 // this.setState(this.state);
1026 } else if (op == UserOperation.SaveComment) {
1027 let res: CommentResponse = msg;
1028 let found = this.state.comments.find(c => c.id == res.comment.id);
1029 found.saved = res.comment.saved;
1030 this.setState(this.state);
1031 } else if (op == UserOperation.CreateCommentLike) {
1032 let res: CommentResponse = msg;
1033 let found: Comment = this.state.comments.find(
1034 c => c.id === res.comment.id
1036 found.score = res.comment.score;
1037 found.upvotes = res.comment.upvotes;
1038 found.downvotes = res.comment.downvotes;
1039 if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
1040 this.setState(this.state);
1041 } else if (op == UserOperation.CreatePostLike) {
1042 let res: CreatePostLikeResponse = msg;
1043 let found = this.state.posts.find(c => c.id == res.post.id);
1044 found.my_vote = res.post.my_vote;
1045 found.score = res.post.score;
1046 found.upvotes = res.post.upvotes;
1047 found.downvotes = res.post.downvotes;
1048 this.setState(this.state);
1049 } else if (op == UserOperation.BanUser) {
1050 let res: BanUserResponse = msg;
1052 .filter(c => c.creator_id == res.user.id)
1053 .forEach(c => (c.banned = res.banned));
1055 .filter(c => c.creator_id == res.user.id)
1056 .forEach(c => (c.banned = res.banned));
1057 this.setState(this.state);
1058 } else if (op == UserOperation.AddAdmin) {
1059 let res: AddAdminResponse = msg;
1060 this.state.admins = res.admins;
1061 this.setState(this.state);
1062 } else if (op == UserOperation.SaveUserSettings) {
1063 this.state = this.emptyState;
1064 this.state.userSettingsLoading = false;
1065 this.setState(this.state);
1066 let res: LoginResponse = msg;
1067 UserService.Instance.login(res);
1068 } else if (op == UserOperation.DeleteAccount) {
1069 this.state.deleteAccountLoading = false;
1070 this.state.deleteAccountShowConfirm = false;
1071 this.setState(this.state);
1072 this.context.router.history.push('/');