Merge branch 'fix_frontend_duplicate_requests' of https://github.com/masterstur/lemmy into masterstur-fix_frontend_duplicate_requests

This commit is contained in:
Dessalines 2020-07-14 11:26:16 -04:00
commit d6fdfe0b6d
10 changed files with 630 additions and 453 deletions

View file

@ -13,7 +13,7 @@ import {
GetSiteResponse, GetSiteResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { wsJsonToRes, toast } from '../utils'; import { wsJsonToRes, toast, getPageFromProps } from '../utils';
import { CommunityLink } from './community-link'; import { CommunityLink } from './community-link';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
@ -27,12 +27,16 @@ interface CommunitiesState {
loading: boolean; loading: boolean;
} }
interface CommunitiesProps {
page: number;
}
export class Communities extends Component<any, CommunitiesState> { export class Communities extends Component<any, CommunitiesState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: CommunitiesState = { private emptyState: CommunitiesState = {
communities: [], communities: [],
loading: true, loading: true,
page: this.getPageFromProps(this.props), page: getPageFromProps(this.props),
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -50,19 +54,19 @@ export class Communities extends Component<any, CommunitiesState> {
WebSocketService.Instance.getSite(); WebSocketService.Instance.getSite();
} }
getPageFromProps(props: any): number {
return props.match.params.page ? Number(props.match.params.page) : 1;
}
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
// Necessary for back button for some reason static getDerivedStateFromProps(props: any): CommunitiesProps {
componentWillReceiveProps(nextProps: any) { return {
if (nextProps.history.action == 'POP') { page: getPageFromProps(props),
this.state = this.emptyState; };
this.state.page = this.getPageFromProps(nextProps); }
componentDidUpdate(_: any, lastState: CommunitiesState) {
if (lastState.page !== this.state.page) {
this.setState({ loading: true });
this.refetch(); this.refetch();
} }
} }
@ -172,22 +176,17 @@ export class Communities extends Component<any, CommunitiesState> {
); );
} }
updateUrl() { updateUrl(paramUpdates: CommunitiesProps) {
this.props.history.push(`/communities/page/${this.state.page}`); const page = paramUpdates.page || this.state.page;
this.props.history.push(`/communities/page/${page}`);
} }
nextPage(i: Communities) { nextPage(i: Communities) {
i.state.page++; i.updateUrl({ page: i.state.page + 1 });
i.setState(i.state);
i.updateUrl();
i.refetch();
} }
prevPage(i: Communities) { prevPage(i: Communities) {
i.state.page--; i.updateUrl({ page: i.state.page - 1 });
i.setState(i.state);
i.updateUrl();
i.refetch();
} }
handleUnsubscribe(communityId: number) { handleUnsubscribe(communityId: number) {

View file

@ -65,6 +65,18 @@ interface State {
site: Site; site: Site;
} }
interface CommunityProps {
dataType: DataType;
sort: SortType;
page: number;
}
interface UrlParams {
dataType?: string;
sort?: string;
page?: number;
}
export class Community extends Component<any, State> { export class Community extends Component<any, State> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: State = { private emptyState: State = {
@ -143,16 +155,21 @@ export class Community extends Component<any, State> {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
// Necessary for back button for some reason static getDerivedStateFromProps(props: any): CommunityProps {
componentWillReceiveProps(nextProps: any) { return {
dataType: getDataTypeFromProps(props),
sort: getSortTypeFromProps(props),
page: getPageFromProps(props),
};
}
componentDidUpdate(_: any, lastState: State) {
if ( if (
nextProps.history.action == 'POP' || lastState.dataType !== this.state.dataType ||
nextProps.history.action == 'PUSH' lastState.sort !== this.state.sort ||
lastState.page !== this.state.page
) { ) {
this.state.dataType = getDataTypeFromProps(nextProps); this.setState({ loading: true });
this.state.sort = getSortTypeFromProps(nextProps);
this.state.page = getPageFromProps(nextProps);
this.setState(this.state);
this.fetchData(); this.fetchData();
} }
} }
@ -273,46 +290,33 @@ export class Community extends Component<any, State> {
} }
nextPage(i: Community) { nextPage(i: Community) {
i.state.page++; i.updateUrl({ page: i.state.page + 1 });
i.setState(i.state);
i.updateUrl();
i.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
prevPage(i: Community) { prevPage(i: Community) {
i.state.page--; i.updateUrl({ page: i.state.page - 1 });
i.setState(i.state);
i.updateUrl();
i.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
handleSortChange(val: SortType) { handleSortChange(val: SortType) {
this.state.sort = val; this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.state.loading = true;
this.setState(this.state);
this.updateUrl();
this.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
handleDataTypeChange(val: DataType) { handleDataTypeChange(val: DataType) {
this.state.dataType = val; this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.state.loading = true;
this.setState(this.state);
this.updateUrl();
this.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
updateUrl() { updateUrl(paramUpdates: UrlParams) {
let dataTypeStr = DataType[this.state.dataType].toLowerCase(); const dataTypeStr =
let sortStr = SortType[this.state.sort].toLowerCase(); paramUpdates.dataType || DataType[this.state.dataType].toLowerCase();
const sortStr =
paramUpdates.sort || SortType[this.state.sort].toLowerCase();
const page = paramUpdates.page || this.state.page;
this.props.history.push( this.props.history.push(
`/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${this.state.page}` `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
); );
} }

View file

@ -25,6 +25,12 @@ export class DataTypeSelect extends Component<
this.state = this.emptyState; this.state = this.emptyState;
} }
static getDerivedStateFromProps(props) {
return {
type_: props.type_,
};
}
render() { render() {
return ( return (
<div class="btn-group btn-group-toggle"> <div class="btn-group btn-group-toggle">
@ -42,8 +48,9 @@ export class DataTypeSelect extends Component<
{i18n.t('posts')} {i18n.t('posts')}
</label> </label>
<label <label
className={`pointer btn btn-sm btn-secondary ${this.state.type_ == className={`pointer btn btn-sm btn-secondary ${
DataType.Comment && 'active'}`} this.state.type_ == DataType.Comment && 'active'
}`}
> >
<input <input
type="radio" type="radio"
@ -58,8 +65,6 @@ export class DataTypeSelect extends Component<
} }
handleTypeChange(i: DataTypeSelect, event: any) { handleTypeChange(i: DataTypeSelect, event: any) {
i.state.type_ = Number(event.target.value); i.props.onChange(Number(event.target.value));
i.setState(i.state);
i.props.onChange(i.state.type_);
} }
} }

View file

@ -26,6 +26,12 @@ export class ListingTypeSelect extends Component<
this.state = this.emptyState; this.state = this.emptyState;
} }
static getDerivedStateFromProps(props) {
return {
type_: props.type_,
};
}
render() { render() {
return ( return (
<div class="btn-group btn-group-toggle"> <div class="btn-group btn-group-toggle">
@ -45,8 +51,9 @@ export class ListingTypeSelect extends Component<
{i18n.t('subscribed')} {i18n.t('subscribed')}
</label> </label>
<label <label
className={`pointer btn btn-sm btn-secondary ${this.state.type_ == className={`pointer btn btn-sm btn-secondary ${
ListingType.All && 'active'}`} this.state.type_ == ListingType.All && 'active'
}`}
> >
<input <input
type="radio" type="radio"
@ -61,8 +68,6 @@ export class ListingTypeSelect extends Component<
} }
handleTypeChange(i: ListingTypeSelect, event: any) { handleTypeChange(i: ListingTypeSelect, event: any) {
i.state.type_ = Number(event.target.value); i.props.onChange(Number(event.target.value));
i.setState(i.state);
i.props.onChange(i.state.type_);
} }
} }

View file

@ -70,6 +70,20 @@ interface MainState {
page: number; page: number;
} }
interface MainProps {
listingType: ListingType;
dataType: DataType;
sort: SortType;
page: number;
}
interface UrlParams {
listingType?: string;
dataType?: string;
sort?: string;
page?: number;
}
export class Main extends Component<any, MainState> { export class Main extends Component<any, MainState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: MainState = { private emptyState: MainState = {
@ -141,17 +155,23 @@ export class Main extends Component<any, MainState> {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
// Necessary for back button for some reason static getDerivedStateFromProps(props: any): MainProps {
componentWillReceiveProps(nextProps: any) { return {
listingType: getListingTypeFromProps(props),
dataType: getDataTypeFromProps(props),
sort: getSortTypeFromProps(props),
page: getPageFromProps(props),
};
}
componentDidUpdate(_: any, lastState: MainState) {
if ( if (
nextProps.history.action == 'POP' || lastState.listingType !== this.state.listingType ||
nextProps.history.action == 'PUSH' lastState.dataType !== this.state.dataType ||
lastState.sort !== this.state.sort ||
lastState.page !== this.state.page
) { ) {
this.state.listingType = getListingTypeFromProps(nextProps); this.setState({ loading: true });
this.state.dataType = getDataTypeFromProps(nextProps);
this.state.sort = getSortTypeFromProps(nextProps);
this.state.page = getPageFromProps(nextProps);
this.setState(this.state);
this.fetchData(); this.fetchData();
} }
} }
@ -257,12 +277,17 @@ export class Main extends Component<any, MainState> {
); );
} }
updateUrl() { updateUrl(paramUpdates: UrlParams) {
let listingTypeStr = ListingType[this.state.listingType].toLowerCase(); const listingTypeStr =
let dataTypeStr = DataType[this.state.dataType].toLowerCase(); paramUpdates.listingType ||
let sortStr = SortType[this.state.sort].toLowerCase(); ListingType[this.state.listingType].toLowerCase();
const dataTypeStr =
paramUpdates.dataType || DataType[this.state.dataType].toLowerCase();
const sortStr =
paramUpdates.sort || SortType[this.state.sort].toLowerCase();
const page = paramUpdates.page || this.state.page;
this.props.history.push( this.props.history.push(
`/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${this.state.page}` `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
); );
} }
@ -529,50 +554,27 @@ export class Main extends Component<any, MainState> {
} }
nextPage(i: Main) { nextPage(i: Main) {
i.state.page++; i.updateUrl({ page: this.state.page + 1 });
i.state.loading = true;
i.setState(i.state);
i.updateUrl();
i.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
prevPage(i: Main) { prevPage(i: Main) {
i.state.page--; i.updateUrl({ page: this.state.page - 1 });
i.state.loading = true;
i.setState(i.state);
i.updateUrl();
i.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
handleSortChange(val: SortType) { handleSortChange(val: SortType) {
this.state.sort = val; this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.state.loading = true;
this.setState(this.state);
this.updateUrl();
this.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
handleListingTypeChange(val: ListingType) { handleListingTypeChange(val: ListingType) {
this.state.listingType = val; this.updateUrl({ listingType: ListingType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.state.loading = true;
this.setState(this.state);
this.updateUrl();
this.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
handleDataTypeChange(val: DataType) { handleDataTypeChange(val: DataType) {
this.state.dataType = val; this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.state.loading = true;
this.setState(this.state);
this.updateUrl();
this.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }

View file

@ -28,6 +28,7 @@ import {
createCommentLikeRes, createCommentLikeRes,
createPostLikeFindRes, createPostLikeFindRes,
commentsToFlatNodes, commentsToFlatNodes,
getPageFromProps,
} from '../utils'; } from '../utils';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
import { UserListing } from './user-listing'; import { UserListing } from './user-listing';
@ -44,15 +45,31 @@ interface SearchState {
searchResponse: SearchResponse; searchResponse: SearchResponse;
loading: boolean; loading: boolean;
site: Site; site: Site;
searchText: string;
}
interface SearchProps {
q: string;
type_: SearchType;
sort: SortType;
page: number;
}
interface UrlParams {
q?: string;
type_?: string;
sort?: string;
page?: number;
} }
export class Search extends Component<any, SearchState> { export class Search extends Component<any, SearchState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: SearchState = { private emptyState: SearchState = {
q: this.getSearchQueryFromProps(this.props), q: Search.getSearchQueryFromProps(this.props),
type_: this.getSearchTypeFromProps(this.props), type_: Search.getSearchTypeFromProps(this.props),
sort: this.getSortTypeFromProps(this.props), sort: Search.getSortTypeFromProps(this.props),
page: this.getPageFromProps(this.props), page: getPageFromProps(this.props),
searchText: Search.getSearchQueryFromProps(this.props),
searchResponse: { searchResponse: {
type_: null, type_: null,
posts: [], posts: [],
@ -77,26 +94,22 @@ export class Search extends Component<any, SearchState> {
}, },
}; };
getSearchQueryFromProps(props: any): string { static getSearchQueryFromProps(props: any): string {
return props.match.params.q ? props.match.params.q : ''; return props.match.params.q ? props.match.params.q : '';
} }
getSearchTypeFromProps(props: any): SearchType { static getSearchTypeFromProps(props: any): SearchType {
return props.match.params.type return props.match.params.type
? routeSearchTypeToEnum(props.match.params.type) ? routeSearchTypeToEnum(props.match.params.type)
: SearchType.All; : SearchType.All;
} }
getSortTypeFromProps(props: any): SortType { static getSortTypeFromProps(props: any): SortType {
return props.match.params.sort return props.match.params.sort
? routeSortTypeToEnum(props.match.params.sort) ? routeSortTypeToEnum(props.match.params.sort)
: SortType.TopAll; : SortType.TopAll;
} }
getPageFromProps(props: any): number {
return props.match.params.page ? Number(props.match.params.page) : 1;
}
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -122,17 +135,23 @@ export class Search extends Component<any, SearchState> {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
// Necessary for back button for some reason static getDerivedStateFromProps(props: any): SearchProps {
componentWillReceiveProps(nextProps: any) { return {
q: Search.getSearchQueryFromProps(props),
type_: Search.getSearchTypeFromProps(props),
sort: Search.getSortTypeFromProps(props),
page: getPageFromProps(props),
};
}
componentDidUpdate(_: any, lastState: SearchState) {
if ( if (
nextProps.history.action == 'POP' || lastState.q !== this.state.q ||
nextProps.history.action == 'PUSH' lastState.type_ !== this.state.type_ ||
lastState.sort !== this.state.sort ||
lastState.page !== this.state.page
) { ) {
this.state.q = this.getSearchQueryFromProps(nextProps); this.setState({ loading: true, searchText: this.state.q });
this.state.type_ = this.getSearchTypeFromProps(nextProps);
this.state.sort = this.getSortTypeFromProps(nextProps);
this.state.page = this.getPageFromProps(nextProps);
this.setState(this.state);
this.search(); this.search();
} }
} }
@ -163,7 +182,7 @@ export class Search extends Component<any, SearchState> {
<input <input
type="text" type="text"
class="form-control mr-2" class="form-control mr-2"
value={this.state.q} value={this.state.searchText}
placeholder={`${i18n.t('search')}...`} placeholder={`${i18n.t('search')}...`}
onInput={linkEvent(this, this.handleQChange)} onInput={linkEvent(this, this.handleQChange)}
required required
@ -413,17 +432,11 @@ export class Search extends Component<any, SearchState> {
} }
nextPage(i: Search) { nextPage(i: Search) {
i.state.page++; i.updateUrl({ page: i.state.page + 1 });
i.setState(i.state);
i.updateUrl();
i.search();
} }
prevPage(i: Search) { prevPage(i: Search) {
i.state.page--; i.updateUrl({ page: i.state.page - 1 });
i.setState(i.state);
i.updateUrl();
i.search();
} }
search() { search() {
@ -441,37 +454,39 @@ export class Search extends Component<any, SearchState> {
} }
handleSortChange(val: SortType) { handleSortChange(val: SortType) {
this.state.sort = val; this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.setState(this.state);
this.updateUrl();
} }
handleTypeChange(i: Search, event: any) { handleTypeChange(i: Search, event: any) {
i.state.type_ = Number(event.target.value); i.updateUrl({
i.state.page = 1; type_: SearchType[Number(event.target.value)].toLowerCase(),
i.setState(i.state); page: 1,
i.updateUrl(); });
} }
handleSearchSubmit(i: Search, event: any) { handleSearchSubmit(i: Search, event: any) {
event.preventDefault(); event.preventDefault();
i.state.loading = true; i.updateUrl({
i.search(); q: i.state.searchText,
i.setState(i.state); type_: SearchType[i.state.type_].toLowerCase(),
i.updateUrl(); sort: SortType[i.state.sort].toLowerCase(),
page: i.state.page,
});
} }
handleQChange(i: Search, event: any) { handleQChange(i: Search, event: any) {
i.state.q = event.target.value; i.setState({ searchText: event.target.value });
i.setState(i.state);
} }
updateUrl() { updateUrl(paramUpdates: UrlParams) {
let typeStr = SearchType[this.state.type_].toLowerCase(); const qStr = paramUpdates.q || this.state.q;
let sortStr = SortType[this.state.sort].toLowerCase(); const typeStr =
paramUpdates.type_ || SearchType[this.state.type_].toLowerCase();
const sortStr =
paramUpdates.sort || SortType[this.state.sort].toLowerCase();
const page = paramUpdates.page || this.state.page;
this.props.history.push( this.props.history.push(
`/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}` `/search/q/${qStr}/type/${typeStr}/sort/${sortStr}/page/${page}`
); );
} }

View file

@ -23,6 +23,12 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
this.state = this.emptyState; this.state = this.emptyState;
} }
static getDerivedStateFromProps(props: any): SortSelectState {
return {
sort: props.sort,
};
}
render() { render() {
return ( return (
<> <>
@ -59,8 +65,6 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
} }
handleSortChange(i: SortSelect, event: any) { handleSortChange(i: SortSelect, event: any) {
i.state.sort = Number(event.target.value); i.props.onChange(event.target.value);
i.setState(i.state);
i.props.onChange(i.state.sort);
} }
} }

307
ui/src/components/user-details.tsx vendored Normal file
View file

@ -0,0 +1,307 @@
import { Component, linkEvent } 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;
onPageChange(page: number): number | any;
}
interface UserDetailsState {
follows: Array<CommunityUser>;
moderates: Array<CommunityUser>;
comments: Array<Comment>;
posts: Array<Post>;
saved?: Array<Post>;
admins: Array<UserView>;
}
export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
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 (
<div>
{this.viewSelector(this.props.view)}
{this.paginator()}
</div>
);
}
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 (
<div>
{combined.map(i => (
<div>
{i.type === 'posts' ? (
<PostListing
post={i.data as Post}
admins={this.state.admins}
showCommunity
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
) : (
<CommentNodes
nodes={[{ comment: i.data as Comment }]}
admins={this.state.admins}
noIndent
showContext
enableDownvotes={this.props.enableDownvotes}
/>
)}
</div>
))}
</div>
);
}
comments() {
return (
<div>
<CommentNodes
nodes={commentsToFlatNodes(this.state.comments)}
admins={this.state.admins}
noIndent
showContext
enableDownvotes={this.props.enableDownvotes}
/>
</div>
);
}
posts() {
return (
<div>
{this.state.posts.map(post => (
<PostListing
post={post}
admins={this.state.admins}
showCommunity
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
))}
</div>
);
}
paginator() {
return (
<div class="my-2">
{this.props.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
</button>
)}
{this.state.comments.length + this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
</button>
)}
</div>
);
}
nextPage(i: UserDetails) {
i.props.onPageChange(i.props.page + 1);
}
prevPage(i: UserDetails) {
i.props.onPageChange(i.props.page - 1);
}
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,
});
}
}
}

View file

@ -4,24 +4,18 @@ import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { import {
UserOperation, UserOperation,
Post,
Comment,
CommunityUser, CommunityUser,
GetUserDetailsForm,
SortType, SortType,
ListingType, ListingType,
UserDetailsResponse,
UserView, UserView,
CommentResponse,
UserSettingsForm, UserSettingsForm,
LoginResponse, LoginResponse,
BanUserResponse,
AddAdminResponse,
DeleteAccountForm, DeleteAccountForm,
PostResponse,
WebSocketJsonResponse, WebSocketJsonResponse,
GetSiteResponse, GetSiteResponse,
Site, Site,
UserDetailsView,
UserDetailsResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { import {
@ -34,28 +28,15 @@ import {
languages, languages,
showAvatars, showAvatars,
toast, toast,
editCommentRes,
saveCommentRes,
createCommentLikeRes,
createPostLikeFindRes,
commentsToFlatNodes,
setupTippy, setupTippy,
} from '../utils'; } from '../utils';
import { PostListing } from './post-listing';
import { UserListing } from './user-listing'; import { UserListing } from './user-listing';
import { SortSelect } from './sort-select'; import { SortSelect } from './sort-select';
import { ListingTypeSelect } from './listing-type-select'; import { ListingTypeSelect } from './listing-type-select';
import { CommentNodes } from './comment-nodes';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import moment from 'moment'; import moment from 'moment';
import { UserDetails } from './user-details';
enum View {
Overview,
Comments,
Posts,
Saved,
}
interface UserState { interface UserState {
user: UserView; user: UserView;
@ -63,11 +44,7 @@ interface UserState {
username: string; username: string;
follows: Array<CommunityUser>; follows: Array<CommunityUser>;
moderates: Array<CommunityUser>; moderates: Array<CommunityUser>;
comments: Array<Comment>; view: UserDetailsView;
posts: Array<Post>;
saved?: Array<Post>;
admins: Array<UserView>;
view: View;
sort: SortType; sort: SortType;
page: number; page: number;
loading: boolean; loading: boolean;
@ -80,6 +57,20 @@ interface UserState {
site: Site; site: Site;
} }
interface UserProps {
view: UserDetailsView;
sort: SortType;
page: number;
user_id: number | null;
username: string;
}
interface UrlParams {
view?: string;
sort?: string;
page?: number;
}
export class User extends Component<any, UserState> { export class User extends Component<any, UserState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: UserState = { private emptyState: UserState = {
@ -102,14 +93,11 @@ export class User extends Component<any, UserState> {
username: null, username: null,
follows: [], follows: [],
moderates: [], moderates: [],
comments: [], loading: false,
posts: [],
admins: [],
loading: true,
avatarLoading: false, avatarLoading: false,
view: this.getViewFromProps(this.props), view: User.getViewFromProps(this.props.match.view),
sort: this.getSortTypeFromProps(this.props), sort: User.getSortTypeFromProps(this.props.match.sort),
page: this.getPageFromProps(this.props), page: User.getPageFromProps(this.props.match.page),
userSettingsForm: { userSettingsForm: {
show_nsfw: null, show_nsfw: null,
theme: null, theme: null,
@ -153,8 +141,9 @@ export class User extends Component<any, UserState> {
this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind( this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
this this
); );
this.handlePageChange = this.handlePageChange.bind(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.state.username = this.props.match.params.username;
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
@ -165,7 +154,6 @@ export class User extends Component<any, UserState> {
() => console.log('complete') () => console.log('complete')
); );
this.refetch();
WebSocketService.Instance.getSite(); WebSocketService.Instance.getSite();
} }
@ -176,38 +164,32 @@ export class User extends Component<any, UserState> {
); );
} }
getViewFromProps(props: any): View { static getViewFromProps(view: any): UserDetailsView {
return props.match.params.view return view
? View[capitalizeFirstLetter(props.match.params.view)] ? UserDetailsView[capitalizeFirstLetter(view)]
: View.Overview; : UserDetailsView.Overview;
} }
getSortTypeFromProps(props: any): SortType { static getSortTypeFromProps(sort: any): SortType {
return props.match.params.sort return sort ? routeSortTypeToEnum(sort) : SortType.New;
? routeSortTypeToEnum(props.match.params.sort)
: SortType.New;
} }
getPageFromProps(props: any): number { static getPageFromProps(page: any): number {
return props.match.params.page ? Number(props.match.params.page) : 1; return page ? Number(page) : 1;
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
// Necessary for back button for some reason static getDerivedStateFromProps(props: any): UserProps {
componentWillReceiveProps(nextProps: any) { return {
if ( view: this.getViewFromProps(props.match.params.view),
nextProps.history.action == 'POP' || sort: this.getSortTypeFromProps(props.match.params.sort),
nextProps.history.action == 'PUSH' page: this.getPageFromProps(props.match.params.page),
) { user_id: Number(props.match.params.id) || null,
this.state.view = this.getViewFromProps(nextProps); username: props.match.params.username,
this.state.sort = this.getSortTypeFromProps(nextProps); };
this.state.page = this.getPageFromProps(nextProps);
this.setState(this.state);
this.refetch();
}
} }
componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) { componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
@ -219,6 +201,8 @@ export class User extends Component<any, UserState> {
// Couldnt get a refresh working. This does for now. // Couldnt get a refresh working. This does for now.
location.reload(); location.reload();
} }
document.title = `/u/${this.state.username} - ${this.state.site.name}`;
setupTippy();
} }
render() { render() {
@ -242,14 +226,20 @@ export class User extends Component<any, UserState> {
class="rounded-circle mr-2" class="rounded-circle mr-2"
/> />
)} )}
<span>/u/{this.state.user.name}</span> <span>/u/{this.state.username}</span>
</h5> </h5>
{this.selects()} {this.selects()}
{this.state.view == View.Overview && this.overview()} <UserDetails
{this.state.view == View.Comments && this.comments()} user_id={this.state.user_id}
{this.state.view == View.Posts && this.posts()} username={this.state.username}
{this.state.view == View.Saved && this.overview()} sort={SortType[this.state.sort]}
{this.paginator()} page={this.state.page}
limit={fetchLimit}
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
view={this.state.view}
onPageChange={this.handlePageChange}
/>
</div> </div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
{this.userInfo()} {this.userInfo()}
@ -268,52 +258,52 @@ export class User extends Component<any, UserState> {
<div class="btn-group btn-group-toggle"> <div class="btn-group btn-group-toggle">
<label <label
className={`btn btn-sm btn-secondary pointer btn-outline-light className={`btn btn-sm btn-secondary pointer btn-outline-light
${this.state.view == View.Overview && 'active'} ${this.state.view == UserDetailsView.Overview && 'active'}
`} `}
> >
<input <input
type="radio" type="radio"
value={View.Overview} value={UserDetailsView.Overview}
checked={this.state.view == View.Overview} checked={this.state.view === UserDetailsView.Overview}
onChange={linkEvent(this, this.handleViewChange)} onChange={linkEvent(this, this.handleViewChange)}
/> />
{i18n.t('overview')} {i18n.t('overview')}
</label> </label>
<label <label
className={`btn btn-sm btn-secondary pointer btn-outline-light className={`btn btn-sm btn-secondary pointer btn-outline-light
${this.state.view == View.Comments && 'active'} ${this.state.view == UserDetailsView.Comments && 'active'}
`} `}
> >
<input <input
type="radio" type="radio"
value={View.Comments} value={UserDetailsView.Comments}
checked={this.state.view == View.Comments} checked={this.state.view == UserDetailsView.Comments}
onChange={linkEvent(this, this.handleViewChange)} onChange={linkEvent(this, this.handleViewChange)}
/> />
{i18n.t('comments')} {i18n.t('comments')}
</label> </label>
<label <label
className={`btn btn-sm btn-secondary pointer btn-outline-light className={`btn btn-sm btn-secondary pointer btn-outline-light
${this.state.view == View.Posts && 'active'} ${this.state.view == UserDetailsView.Posts && 'active'}
`} `}
> >
<input <input
type="radio" type="radio"
value={View.Posts} value={UserDetailsView.Posts}
checked={this.state.view == View.Posts} checked={this.state.view == UserDetailsView.Posts}
onChange={linkEvent(this, this.handleViewChange)} onChange={linkEvent(this, this.handleViewChange)}
/> />
{i18n.t('posts')} {i18n.t('posts')}
</label> </label>
<label <label
className={`btn btn-sm btn-secondary pointer btn-outline-light className={`btn btn-sm btn-secondary pointer btn-outline-light
${this.state.view == View.Saved && 'active'} ${this.state.view == UserDetailsView.Saved && 'active'}
`} `}
> >
<input <input
type="radio" type="radio"
value={View.Saved} value={UserDetailsView.Saved}
checked={this.state.view == View.Saved} checked={this.state.view == UserDetailsView.Saved}
onChange={linkEvent(this, this.handleViewChange)} onChange={linkEvent(this, this.handleViewChange)}
/> />
{i18n.t('saved')} {i18n.t('saved')}
@ -347,84 +337,6 @@ export class User extends Component<any, UserState> {
); );
} }
overview() {
let combined: Array<{ type_: string; data: Comment | Post }> = [];
let comments = this.state.comments.map(e => {
return { type_: 'comments', data: e };
});
let posts = this.state.posts.map(e => {
return { type_: 'posts', data: e };
});
combined.push(...comments);
combined.push(...posts);
// Sort it
if (this.state.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 (
<div>
{combined.map(i => (
<div>
{i.type_ == 'posts' ? (
<PostListing
post={i.data as Post}
admins={this.state.admins}
showCommunity
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/>
) : (
<CommentNodes
nodes={[{ comment: i.data as Comment }]}
admins={this.state.admins}
noIndent
showCommunity
showContext
enableDownvotes={this.state.site.enable_downvotes}
/>
)}
</div>
))}
</div>
);
}
comments() {
return (
<div>
<CommentNodes
nodes={commentsToFlatNodes(this.state.comments)}
admins={this.state.admins}
noIndent
showCommunity
showContext
enableDownvotes={this.state.site.enable_downvotes}
/>
</div>
);
}
posts() {
return (
<div>
{this.state.posts.map(post => (
<PostListing
post={post}
admins={this.state.admins}
showCommunity
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/>
))}
</div>
);
}
userInfo() { userInfo() {
let user = this.state.user; let user = this.state.user;
return ( return (
@ -896,77 +808,30 @@ export class User extends Component<any, UserState> {
); );
} }
paginator() { updateUrl(paramUpdates: UrlParams) {
return ( const page = paramUpdates.page || this.state.page;
<div class="my-2"> const viewStr =
{this.state.page > 1 && ( paramUpdates.view || UserDetailsView[this.state.view].toLowerCase();
<button const sortStr =
class="btn btn-sm btn-secondary mr-1" paramUpdates.sort || SortType[this.state.sort].toLowerCase();
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
</button>
)}
{this.state.comments.length + this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
</button>
)}
</div>
);
}
updateUrl() {
let viewStr = View[this.state.view].toLowerCase();
let sortStr = SortType[this.state.sort].toLowerCase();
this.props.history.push( this.props.history.push(
`/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}` `/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
); );
} }
nextPage(i: User) { handlePageChange(page: number) {
i.state.page++; this.updateUrl({ page });
i.setState(i.state);
i.updateUrl();
i.refetch();
}
prevPage(i: User) {
i.state.page--;
i.setState(i.state);
i.updateUrl();
i.refetch();
}
refetch() {
let form: GetUserDetailsForm = {
user_id: this.state.user_id,
username: this.state.username,
sort: SortType[this.state.sort],
saved_only: this.state.view == View.Saved,
page: this.state.page,
limit: fetchLimit,
};
WebSocketService.Instance.getUserDetails(form);
} }
handleSortChange(val: SortType) { handleSortChange(val: SortType) {
this.state.sort = val; this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.setState(this.state);
this.updateUrl();
this.refetch();
} }
handleViewChange(i: User, event: any) { handleViewChange(i: User, event: any) {
i.state.view = Number(event.target.value); i.updateUrl({
i.state.page = 1; view: UserDetailsView[Number(event.target.value)].toLowerCase(),
i.setState(i.state); page: 1,
i.updateUrl(); });
i.refetch();
} }
handleUserSettingsShowNsfwChange(i: User, event: any) { handleUserSettingsShowNsfwChange(i: User, event: any) {
@ -1136,102 +1001,66 @@ export class User extends Component<any, UserState> {
} }
parseMessage(msg: WebSocketJsonResponse) { parseMessage(msg: WebSocketJsonResponse) {
console.log(msg); const res = wsJsonToRes(msg);
let res = wsJsonToRes(msg);
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), 'danger'); toast(i18n.t(msg.error), 'danger');
this.state.deleteAccountLoading = false;
this.state.avatarLoading = false;
this.state.userSettingsLoading = false;
if (msg.error == 'couldnt_find_that_username_or_email') { if (msg.error == 'couldnt_find_that_username_or_email') {
this.context.router.history.push('/'); this.context.router.history.push('/');
} }
this.setState(this.state); this.setState({
deleteAccountLoading: false,
avatarLoading: false,
userSettingsLoading: false,
});
return; return;
} else if (msg.reconnect) {
this.refetch();
} else if (res.op == UserOperation.GetUserDetails) { } else if (res.op == UserOperation.GetUserDetails) {
let data = res.data as UserDetailsResponse; // Since the UserDetails contains posts/comments as well as some general user info we listen here as well
this.state.user = data.user; // and set the parent state if it is not set or differs
this.state.comments = data.comments; const data = res.data as UserDetailsResponse;
this.state.follows = data.follows;
this.state.moderates = data.moderates; if (this.state.user.id !== data.user.id) {
this.state.posts = data.posts; this.state.user = data.user;
this.state.admins = data.admins; this.state.follows = data.follows;
this.state.loading = false; this.state.moderates = data.moderates;
if (this.isCurrentUser) {
this.state.userSettingsForm.show_nsfw = if (this.isCurrentUser) {
UserService.Instance.user.show_nsfw; this.state.userSettingsForm.show_nsfw =
this.state.userSettingsForm.theme = UserService.Instance.user.theme UserService.Instance.user.show_nsfw;
? UserService.Instance.user.theme this.state.userSettingsForm.theme = UserService.Instance.user.theme
: 'darkly'; ? UserService.Instance.user.theme
this.state.userSettingsForm.default_sort_type = : 'darkly';
UserService.Instance.user.default_sort_type; this.state.userSettingsForm.default_sort_type =
this.state.userSettingsForm.default_listing_type = UserService.Instance.user.default_sort_type;
UserService.Instance.user.default_listing_type; this.state.userSettingsForm.default_listing_type =
this.state.userSettingsForm.lang = UserService.Instance.user.lang; UserService.Instance.user.default_listing_type;
this.state.userSettingsForm.avatar = UserService.Instance.user.avatar; this.state.userSettingsForm.lang = UserService.Instance.user.lang;
this.state.userSettingsForm.email = this.state.user.email; this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email; this.state.userSettingsForm.email = this.state.user.email;
this.state.userSettingsForm.show_avatars = this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
UserService.Instance.user.show_avatars; this.state.userSettingsForm.show_avatars =
this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id; UserService.Instance.user.show_avatars;
this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
}
this.setState(this.state);
} }
document.title = `/u/${this.state.user.name} - ${this.state.site.name}`;
window.scrollTo(0, 0);
this.setState(this.state);
setupTippy();
} else if (res.op == UserOperation.EditComment) {
let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments);
this.setState(this.state);
} else if (res.op == UserOperation.CreateComment) {
let 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) {
let data = res.data as CommentResponse;
saveCommentRes(data, this.state.comments);
this.setState(this.state);
} else if (res.op == UserOperation.CreateCommentLike) {
let data = res.data as CommentResponse;
createCommentLikeRes(data, this.state.comments);
this.setState(this.state);
} else if (res.op == UserOperation.CreatePostLike) {
let data = res.data as PostResponse;
createPostLikeFindRes(data, this.state.posts);
this.setState(this.state);
} else if (res.op == UserOperation.BanUser) {
let 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(this.state);
} else if (res.op == UserOperation.AddAdmin) {
let data = res.data as AddAdminResponse;
this.state.admins = data.admins;
this.setState(this.state);
} else if (res.op == UserOperation.SaveUserSettings) { } else if (res.op == UserOperation.SaveUserSettings) {
let data = res.data as LoginResponse; const data = res.data as LoginResponse;
this.state.userSettingsLoading = false;
this.setState(this.state);
UserService.Instance.login(data); UserService.Instance.login(data);
this.setState({
userSettingsLoading: false,
});
window.scrollTo(0, 0);
} else if (res.op == UserOperation.DeleteAccount) { } else if (res.op == UserOperation.DeleteAccount) {
this.state.deleteAccountLoading = false; this.setState({
this.state.deleteAccountShowConfirm = false; deleteAccountLoading: false,
this.setState(this.state); deleteAccountShowConfirm: false,
});
this.context.router.history.push('/'); this.context.router.history.push('/');
} else if (res.op == UserOperation.GetSite) { } else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse; const data = res.data as GetSiteResponse;
this.state.site = data.site; this.setState({
this.setState(this.state); site: data.site,
});
} }
} }
} }

View file

@ -937,3 +937,10 @@ export interface WebSocketJsonResponse {
error?: string; error?: string;
reconnect?: boolean; reconnect?: boolean;
} }
export enum UserDetailsView {
Overview,
Comments,
Posts,
Saved,
}