From f1d01f4fa091ffd506518fa0af049fb385684c75 Mon Sep 17 00:00:00 2001 From: derek Date: Sun, 12 Jul 2020 04:00:10 -0400 Subject: [PATCH 01/68] ui.components: split user component up to fix duplicate requests Replace componentWillReceiveProps with getDerivedState and pass state as props to new component --- ui/src/components/user-details.tsx | 270 +++++++++++++++++++++ ui/src/components/user.tsx | 369 +++++++++-------------------- ui/src/interfaces.ts | 7 + 3 files changed, 385 insertions(+), 261 deletions(-) create mode 100644 ui/src/components/user-details.tsx diff --git a/ui/src/components/user-details.tsx b/ui/src/components/user-details.tsx new file mode 100644 index 000000000..a622985ff --- /dev/null +++ b/ui/src/components/user-details.tsx @@ -0,0 +1,270 @@ +import { Component } from 'inferno'; +import { WebSocketService, UserService } from '../services'; +import { Subscription } from 'rxjs'; +import { retryWhen, delay, take, last } from 'rxjs/operators'; +import { i18n } from '../i18next'; +import { + UserOperation, + Post, + Comment, + CommunityUser, + SortType, + UserDetailsResponse, + UserView, + WebSocketJsonResponse, + UserDetailsView, + CommentResponse, + BanUserResponse, + PostResponse, + AddAdminResponse, +} from '../interfaces'; +import { + wsJsonToRes, + toast, + commentsToFlatNodes, + setupTippy, + editCommentRes, + saveCommentRes, + createCommentLikeRes, + createPostLikeFindRes, +} from '../utils'; +import { PostListing } from './post-listing'; +import { CommentNodes } from './comment-nodes'; + +interface UserDetailsProps { + username?: string; + user_id?: number; + page: number; + limit: number; + sort: string; + enableDownvotes: boolean; + enableNsfw: boolean; + view: UserDetailsView; +} + +interface UserDetailsState { + follows: Array; + moderates: Array; + comments: Array; + posts: Array; + saved?: Array; + admins: Array; +} + +export class UserDetails extends Component { + private subscription: Subscription; + constructor(props: any, context: any) { + super(props, context); + + this.state = { + follows: [], + moderates: [], + comments: [], + posts: [], + saved: [], + admins: [], + }; + + this.subscription = WebSocketService.Instance.subject + .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) + .subscribe( + msg => this.parseMessage(msg), + err => console.error(err), + () => console.log('complete') + ); + } + + componentWillUnmount() { + this.subscription.unsubscribe(); + } + + componentDidMount() { + this.fetchUserData(); + } + + componentDidUpdate(lastProps: UserDetailsProps) { + for (const key of Object.keys(lastProps)) { + if (lastProps[key] !== this.props[key]) { + this.fetchUserData(); + break; + } + } + setupTippy(); + } + + fetchUserData() { + WebSocketService.Instance.getUserDetails({ + user_id: this.props.user_id, + username: this.props.username, + sort: this.props.sort, + saved_only: this.props.view === UserDetailsView.Saved, + page: this.props.page, + limit: this.props.limit, + }); + } + + render() { + return this.viewSelector(this.props.view); + } + + viewSelector(view: UserDetailsView) { + if (view === UserDetailsView.Overview || view === UserDetailsView.Saved) { + return this.overview(); + } + if (view === UserDetailsView.Comments) { + return this.comments(); + } + if (view === UserDetailsView.Posts) { + return this.posts(); + } + } + + overview() { + const comments = this.state.comments.map((c: Comment) => { + return { type: 'comments', data: c }; + }); + const posts = this.state.posts.map((p: Post) => { + return { type: 'posts', data: p }; + }); + + const combined: Array<{ type: string; data: Comment | Post }> = [ + ...comments, + ...posts, + ]; + + // Sort it + if (SortType[this.props.sort] === SortType.New) { + combined.sort((a, b) => b.data.published.localeCompare(a.data.published)); + } else { + combined.sort((a, b) => b.data.score - a.data.score); + } + + return ( +
+ {combined.map(i => ( +
+ {i.type === 'posts' ? ( + + ) : ( + + )} +
+ ))} +
+ ); + } + + comments() { + return ( +
+ +
+ ); + } + + posts() { + return ( +
+ {this.state.posts.map(post => ( + + ))} +
+ ); + } + + parseMessage(msg: WebSocketJsonResponse) { + const res = wsJsonToRes(msg); + + if (msg.error) { + toast(i18n.t(msg.error), 'danger'); + if (msg.error == 'couldnt_find_that_username_or_email') { + this.context.router.history.push('/'); + } + return; + } else if (msg.reconnect) { + this.fetchUserData(); + } else if (res.op == UserOperation.GetUserDetails) { + const data = res.data as UserDetailsResponse; + this.setState({ + comments: data.comments, + follows: data.follows, + moderates: data.moderates, + posts: data.posts, + admins: data.admins, + }); + } else if (res.op == UserOperation.CreateCommentLike) { + const data = res.data as CommentResponse; + createCommentLikeRes(data, this.state.comments); + this.setState({ + comments: this.state.comments, + }); + } else if (res.op == UserOperation.EditComment) { + const data = res.data as CommentResponse; + editCommentRes(data, this.state.comments); + this.setState({ + comments: this.state.comments, + }); + } else if (res.op == UserOperation.CreateComment) { + const data = res.data as CommentResponse; + if ( + UserService.Instance.user && + data.comment.creator_id == UserService.Instance.user.id + ) { + toast(i18n.t('reply_sent')); + } + } else if (res.op == UserOperation.SaveComment) { + const data = res.data as CommentResponse; + saveCommentRes(data, this.state.comments); + this.setState({ + comments: this.state.comments, + }); + } else if (res.op == UserOperation.CreatePostLike) { + const data = res.data as PostResponse; + createPostLikeFindRes(data, this.state.posts); + this.setState({ + posts: this.state.posts, + }); + } else if (res.op == UserOperation.BanUser) { + const data = res.data as BanUserResponse; + this.state.comments + .filter(c => c.creator_id == data.user.id) + .forEach(c => (c.banned = data.banned)); + this.state.posts + .filter(c => c.creator_id == data.user.id) + .forEach(c => (c.banned = data.banned)); + this.setState({ + posts: this.state.posts, + comments: this.state.comments, + }); + } else if (res.op == UserOperation.AddAdmin) { + const data = res.data as AddAdminResponse; + this.setState({ + admins: data.admins, + }); + } + } +} diff --git a/ui/src/components/user.tsx b/ui/src/components/user.tsx index 854dd6efd..25aaf2208 100644 --- a/ui/src/components/user.tsx +++ b/ui/src/components/user.tsx @@ -4,24 +4,18 @@ import { Subscription } from 'rxjs'; import { retryWhen, delay, take } from 'rxjs/operators'; import { UserOperation, - Post, - Comment, CommunityUser, - GetUserDetailsForm, SortType, ListingType, - UserDetailsResponse, UserView, - CommentResponse, UserSettingsForm, LoginResponse, - BanUserResponse, - AddAdminResponse, DeleteAccountForm, - PostResponse, WebSocketJsonResponse, GetSiteResponse, Site, + UserDetailsView, + UserDetailsResponse, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { @@ -34,28 +28,15 @@ import { languages, showAvatars, toast, - editCommentRes, - saveCommentRes, - createCommentLikeRes, - createPostLikeFindRes, - commentsToFlatNodes, setupTippy, } from '../utils'; -import { PostListing } from './post-listing'; import { UserListing } from './user-listing'; import { SortSelect } from './sort-select'; import { ListingTypeSelect } from './listing-type-select'; -import { CommentNodes } from './comment-nodes'; import { MomentTime } from './moment-time'; import { i18n } from '../i18next'; import moment from 'moment'; - -enum View { - Overview, - Comments, - Posts, - Saved, -} +import { UserDetails } from './user-details'; interface UserState { user: UserView; @@ -63,11 +44,7 @@ interface UserState { username: string; follows: Array; moderates: Array; - comments: Array; - posts: Array; - saved?: Array; - admins: Array; - view: View; + view: UserDetailsView; sort: SortType; page: number; loading: boolean; @@ -102,14 +79,11 @@ export class User extends Component { username: null, follows: [], moderates: [], - comments: [], - posts: [], - admins: [], - loading: true, + loading: false, avatarLoading: false, - view: this.getViewFromProps(this.props), - sort: this.getSortTypeFromProps(this.props), - page: this.getPageFromProps(this.props), + view: User.getViewFromProps(this.props.match.view), + sort: User.getSortTypeFromProps(this.props.match.sort), + page: User.getPageFromProps(this.props.match.page), userSettingsForm: { show_nsfw: null, theme: null, @@ -145,7 +119,7 @@ export class User extends Component { constructor(props: any, context: any) { super(props, context); - this.state = this.emptyState; + this.state = Object.assign({}, this.emptyState); this.handleSortChange = this.handleSortChange.bind(this); this.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind( this @@ -154,7 +128,7 @@ export class User extends Component { this ); - this.state.user_id = Number(this.props.match.params.id); + this.state.user_id = Number(this.props.match.params.id) || null; this.state.username = this.props.match.params.username; this.subscription = WebSocketService.Instance.subject @@ -165,7 +139,6 @@ export class User extends Component { () => console.log('complete') ); - this.refetch(); WebSocketService.Instance.getSite(); } @@ -176,38 +149,32 @@ export class User extends Component { ); } - getViewFromProps(props: any): View { - return props.match.params.view - ? View[capitalizeFirstLetter(props.match.params.view)] - : View.Overview; + static getViewFromProps(view: any): UserDetailsView { + return view + ? UserDetailsView[capitalizeFirstLetter(view)] + : UserDetailsView.Overview; } - getSortTypeFromProps(props: any): SortType { - return props.match.params.sort - ? routeSortTypeToEnum(props.match.params.sort) - : SortType.New; + static getSortTypeFromProps(sort: any): SortType { + return sort ? routeSortTypeToEnum(sort) : SortType.New; } - getPageFromProps(props: any): number { - return props.match.params.page ? Number(props.match.params.page) : 1; + static getPageFromProps(page: any): number { + return page ? Number(page) : 1; } componentWillUnmount() { this.subscription.unsubscribe(); } - // Necessary for back button for some reason - componentWillReceiveProps(nextProps: any) { - if ( - nextProps.history.action == 'POP' || - nextProps.history.action == 'PUSH' - ) { - this.state.view = this.getViewFromProps(nextProps); - this.state.sort = this.getSortTypeFromProps(nextProps); - this.state.page = this.getPageFromProps(nextProps); - this.setState(this.state); - this.refetch(); - } + static getDerivedStateFromProps(props) { + return { + view: this.getViewFromProps(props.match.params.view), + sort: this.getSortTypeFromProps(props.match.params.sort), + page: this.getPageFromProps(props.match.params.page), + user_id: Number(props.match.params.id) || null, + username: props.match.params.username, + }; } componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) { @@ -219,6 +186,8 @@ export class User extends Component { // Couldnt get a refresh working. This does for now. location.reload(); } + document.title = `/u/${this.state.username} - ${this.state.site.name}`; + setupTippy(); } render() { @@ -242,13 +211,20 @@ export class User extends Component { class="rounded-circle mr-2" /> )} - /u/{this.state.user.name} + /u/{this.state.username} {this.selects()} - {this.state.view == View.Overview && this.overview()} - {this.state.view == View.Comments && this.comments()} - {this.state.view == View.Posts && this.posts()} - {this.state.view == View.Saved && this.overview()} + {this.paginator()}
@@ -268,52 +244,52 @@ export class User extends Component {
@@ -186,6 +187,14 @@ export class Login extends Component { onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} /> + {!validEmail(this.state.registerForm.email) && ( + + )}
diff --git a/ui/translations/en.json b/ui/translations/en.json index cb4347f1c..90c4a9959 100644 --- a/ui/translations/en.json +++ b/ui/translations/en.json @@ -253,6 +253,7 @@ "Couldn't find that username or email.", "password_incorrect": "Password incorrect.", "passwords_dont_match": "Passwords do not match.", + "no_password_reset": "You will not be able to reset your password without an email.", "invalid_username": "Invalid username.", "admin_already_created": "Sorry, there's already an admin.", "user_already_exists": "User already exists.", From 2c0928efe07ffa352f690a8bd3d436ad2a8bbfbf Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 14 Jul 2020 13:21:31 -0400 Subject: [PATCH 15/68] Change styling on comment-form no login-alert. --- ui/src/components/comment-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx index b9f1e815b..72a4f398b 100644 --- a/ui/src/components/comment-form.tsx +++ b/ui/src/components/comment-form.tsx @@ -253,7 +253,7 @@ export class CommentForm extends Component { ) : ( -