mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-22 06:36:17 +00:00
Render more while reloading only some resources (#2480)
* AdminSettings remove unused currentTab state * Fix amAdmin check in reports fetchInitialData * Make CreatePost render earlier * Include children of auth and anonymous guard in first render. * Convert DidMount to WillMount where things don't depend on the DOM `componentDidMount` is called after the first render. A lot of components used it to call `setState`, which causes a second render. * Keep route components mounted during same route navigation Not sure why this wasn't the case without this change. The only difference here is that the same array is reused in every render. * Disable mounted same route navigation by default * Enable mounted same route navigation for some routes * Render more while loading * Prettier markup * Make Post use query params and reload comments independently * Fix issue with <Prompt /> for forms that remain mounted after "leaving". * Make Search not rerender the results on every keystroke * Discard old requests These used to (mostly) arrive at the old already unmounted components. Now they would render briefly until the latest response is received. * Move non breaking space to modlog * Make show optional for modals
This commit is contained in:
parent
937fd3eb4e
commit
b7fe70d8c1
|
@ -32,6 +32,64 @@ export default class App extends Component<any, any> {
|
|||
destroyTippy();
|
||||
}
|
||||
|
||||
routes = routes.map(
|
||||
({
|
||||
path,
|
||||
component: RouteComponent,
|
||||
fetchInitialData,
|
||||
getQueryParams,
|
||||
mountedSameRouteNavKey,
|
||||
}) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
exact
|
||||
component={routeProps => {
|
||||
if (!fetchInitialData) {
|
||||
FirstLoadService.falsify();
|
||||
}
|
||||
|
||||
let queryProps = routeProps;
|
||||
if (getQueryParams && this.isoData.site_res) {
|
||||
// ErrorGuard will not render its children when
|
||||
// site_res is missing, this guarantees that props
|
||||
// will always contain the query params.
|
||||
queryProps = {
|
||||
...routeProps,
|
||||
...getQueryParams(
|
||||
routeProps.location.search,
|
||||
this.isoData.site_res,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// When key is location.key the component will be recreated when
|
||||
// navigating to itself. This is usesful to e.g. reset forms.
|
||||
const key = mountedSameRouteNavKey ?? routeProps.location.key;
|
||||
|
||||
return (
|
||||
<ErrorGuard>
|
||||
<div tabIndex={-1}>
|
||||
{RouteComponent &&
|
||||
(isAuthPath(path ?? "") ? (
|
||||
<AuthGuard {...routeProps}>
|
||||
<RouteComponent key={key} {...queryProps} />
|
||||
</AuthGuard>
|
||||
) : isAnonymousPath(path ?? "") ? (
|
||||
<AnonymousGuard>
|
||||
<RouteComponent key={key} {...queryProps} />
|
||||
</AnonymousGuard>
|
||||
) : (
|
||||
<RouteComponent key={key} {...queryProps} />
|
||||
))}
|
||||
</div>
|
||||
</ErrorGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
render() {
|
||||
const siteRes = this.isoData.site_res;
|
||||
const siteView = siteRes?.site_view;
|
||||
|
@ -64,58 +122,7 @@ export default class App extends Component<any, any> {
|
|||
<Navbar siteRes={siteRes} />
|
||||
<div className="mt-4 p-0 fl-1">
|
||||
<Switch>
|
||||
{routes.map(
|
||||
({
|
||||
path,
|
||||
component: RouteComponent,
|
||||
fetchInitialData,
|
||||
getQueryParams,
|
||||
}) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
exact
|
||||
component={routeProps => {
|
||||
if (!fetchInitialData) {
|
||||
FirstLoadService.falsify();
|
||||
}
|
||||
|
||||
let queryProps = routeProps;
|
||||
if (getQueryParams && this.isoData.site_res) {
|
||||
// ErrorGuard will not render its children when
|
||||
// site_res is missing, this guarantees that props
|
||||
// will always contain the query params.
|
||||
queryProps = {
|
||||
...routeProps,
|
||||
...getQueryParams(
|
||||
routeProps.location.search,
|
||||
this.isoData.site_res,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorGuard>
|
||||
<div tabIndex={-1}>
|
||||
{RouteComponent &&
|
||||
(isAuthPath(path ?? "") ? (
|
||||
<AuthGuard {...routeProps}>
|
||||
<RouteComponent {...queryProps} />
|
||||
</AuthGuard>
|
||||
) : isAnonymousPath(path ?? "") ? (
|
||||
<AnonymousGuard>
|
||||
<RouteComponent {...queryProps} />
|
||||
</AnonymousGuard>
|
||||
) : (
|
||||
<RouteComponent {...queryProps} />
|
||||
))}
|
||||
</div>
|
||||
</ErrorGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{this.routes}
|
||||
<Route component={ErrorPage} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
|
|
@ -63,7 +63,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
async componentWillMount() {
|
||||
// Subscribe to jwt changes
|
||||
if (isBrowser()) {
|
||||
// On the first load, check the unreads
|
||||
|
|
|
@ -502,7 +502,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
<>
|
||||
<Link
|
||||
className={classnames}
|
||||
to={`/comment/${
|
||||
to={`/post/${cv.post.id}/${
|
||||
(this.props.showContext && getCommentParentId(cv.comment)) ||
|
||||
cv.comment.id
|
||||
}`}
|
||||
|
|
|
@ -1,30 +1,25 @@
|
|||
import { Component } from "inferno";
|
||||
import { UserService } from "../../services";
|
||||
import { Spinner } from "./icon";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
interface AnonymousGuardState {
|
||||
hasRedirected: boolean;
|
||||
}
|
||||
|
||||
class AnonymousGuard extends Component<any, AnonymousGuardState> {
|
||||
state = {
|
||||
hasRedirected: false,
|
||||
} as AnonymousGuardState;
|
||||
|
||||
class AnonymousGuard extends Component<any, any> {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (UserService.Instance.myUserInfo) {
|
||||
hasAuth() {
|
||||
return UserService.Instance.myUserInfo;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.hasAuth() && isBrowser()) {
|
||||
this.context.router.history.replace(`/`);
|
||||
} else {
|
||||
this.setState({ hasRedirected: true });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.hasRedirected ? this.props.children : <Spinner />;
|
||||
return !this.hasAuth() ? this.props.children : <Spinner />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,19 +3,12 @@ import { RouteComponentProps } from "inferno-router/dist/Route";
|
|||
import { UserService } from "../../services";
|
||||
import { Spinner } from "./icon";
|
||||
import { getQueryString } from "@utils/helpers";
|
||||
|
||||
interface AuthGuardState {
|
||||
hasRedirected: boolean;
|
||||
}
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
class AuthGuard extends Component<
|
||||
RouteComponentProps<Record<string, string>>,
|
||||
AuthGuardState
|
||||
any
|
||||
> {
|
||||
state = {
|
||||
hasRedirected: false,
|
||||
} as AuthGuardState;
|
||||
|
||||
constructor(
|
||||
props: RouteComponentProps<Record<string, string>>,
|
||||
context: any,
|
||||
|
@ -23,19 +16,21 @@ class AuthGuard extends Component<
|
|||
super(props, context);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!UserService.Instance.myUserInfo) {
|
||||
hasAuth() {
|
||||
return UserService.Instance.myUserInfo;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (!this.hasAuth() && isBrowser()) {
|
||||
const { pathname, search } = this.props.location;
|
||||
this.context.router.history.replace(
|
||||
`/login${getQueryString({ prev: pathname + search })}`,
|
||||
);
|
||||
} else {
|
||||
this.setState({ hasRedirected: true });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.hasRedirected ? this.props.children : <Spinner />;
|
||||
return this.hasAuth() ? this.props.children : <Spinner />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -643,7 +643,6 @@ export default class ContentActionDropdown extends Component<
|
|||
type,
|
||||
} = this.props;
|
||||
|
||||
// Wait until componentDidMount runs (which only happens on the browser) to prevent sending over a gratuitous amount of markup
|
||||
return (
|
||||
<>
|
||||
{renderRemoveDialog && (
|
||||
|
|
|
@ -39,7 +39,7 @@ function handleSearch(i: SearchableSelect, e: ChangeEvent<HTMLInputElement>) {
|
|||
}
|
||||
|
||||
function focusSearch(i: SearchableSelect) {
|
||||
if (i.toggleButtonRef.current?.ariaExpanded !== "true") {
|
||||
if (i.toggleButtonRef.current?.ariaExpanded === "true") {
|
||||
i.searchInputRef.current?.focus();
|
||||
|
||||
if (i.props.onSearch) {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { getQueryString, validInstanceTLD } from "@utils/helpers";
|
||||
import classNames from "classnames";
|
||||
import { NoOptionI18nKeys } from "i18next";
|
||||
import { Component, MouseEventHandler, linkEvent } from "inferno";
|
||||
import { Component, MouseEventHandler, createRef, linkEvent } from "inferno";
|
||||
import { CommunityView } from "lemmy-js-client";
|
||||
import { I18NextService, UserService } from "../../services";
|
||||
import { VERSION } from "../../version";
|
||||
import { Icon, Spinner } from "./icon";
|
||||
import { toast } from "../../toast";
|
||||
import { modalMixin } from "../mixins/modal-mixin";
|
||||
|
||||
interface SubscribeButtonProps {
|
||||
communityView: CommunityView;
|
||||
|
@ -93,6 +94,7 @@ export function SubscribeButton({
|
|||
|
||||
interface RemoteFetchModalProps {
|
||||
communityActorId: string;
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
interface RemoteFetchModalState {
|
||||
|
@ -103,10 +105,6 @@ function handleInput(i: RemoteFetchModal, event: any) {
|
|||
i.setState({ instanceText: event.target.value });
|
||||
}
|
||||
|
||||
function focusInput() {
|
||||
document.getElementById("remoteFetchInstance")?.focus();
|
||||
}
|
||||
|
||||
function submitRemoteFollow(
|
||||
{ state: { instanceText }, props: { communityActorId } }: RemoteFetchModal,
|
||||
event: Event,
|
||||
|
@ -139,6 +137,7 @@ function submitRemoteFollow(
|
|||
)}`;
|
||||
}
|
||||
|
||||
@modalMixin
|
||||
class RemoteFetchModal extends Component<
|
||||
RemoteFetchModalProps,
|
||||
RemoteFetchModalState
|
||||
|
@ -147,20 +146,15 @@ class RemoteFetchModal extends Component<
|
|||
instanceText: "",
|
||||
};
|
||||
|
||||
modalDivRef = createRef<HTMLDivElement>();
|
||||
inputRef = createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document
|
||||
.getElementById("remoteFetchModal")
|
||||
?.addEventListener("shown.bs.modal", focusInput);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
document
|
||||
.getElementById("remoteFetchModal")
|
||||
?.removeEventListener("shown.bs.modal", focusInput);
|
||||
handleShow() {
|
||||
this.inputRef.current?.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -171,6 +165,7 @@ class RemoteFetchModal extends Component<
|
|||
tabIndex={-1}
|
||||
aria-hidden
|
||||
aria-labelledby="#remoteFetchModalTitle"
|
||||
ref={this.modalDivRef}
|
||||
>
|
||||
<div className="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
|
||||
<div className="modal-content">
|
||||
|
@ -203,6 +198,7 @@ class RemoteFetchModal extends Component<
|
|||
required
|
||||
enterKeyHint="go"
|
||||
inputMode="url"
|
||||
ref={this.inputRef}
|
||||
/>
|
||||
</form>
|
||||
<footer className="modal-footer">
|
||||
|
|
|
@ -24,6 +24,7 @@ import { fetchLimit } from "../../config";
|
|||
import { PersonListing } from "../person/person-listing";
|
||||
import { modalMixin } from "../mixins/modal-mixin";
|
||||
import { UserBadges } from "./user-badges";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
interface ViewVotesModalProps {
|
||||
children?: InfernoNode;
|
||||
|
@ -96,8 +97,8 @@ export default class ViewVotesModal extends Component<
|
|||
this.handlePageChange = this.handlePageChange.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (this.props.show) {
|
||||
async componentWillMount() {
|
||||
if (this.props.show && isBrowser()) {
|
||||
await this.refetch();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ import { getHttpBaseInternal } from "../../utils/env";
|
|||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
type CommunitiesData = RouteDataResponse<{
|
||||
listCommunitiesResponse: ListCommunitiesResponse;
|
||||
|
@ -121,19 +122,23 @@ export class Communities extends Component<
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!this.state.isIsomorphic) {
|
||||
await this.refetch();
|
||||
async componentWillMount() {
|
||||
if (!this.state.isIsomorphic && isBrowser()) {
|
||||
await this.refetch(this.props);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: CommunitiesRouteProps) {
|
||||
this.refetch(nextProps);
|
||||
}
|
||||
|
||||
get documentTitle(): string {
|
||||
return `${I18NextService.i18n.t("communities")} - ${
|
||||
this.state.siteRes.site_view.site.name
|
||||
}`;
|
||||
}
|
||||
|
||||
renderListings() {
|
||||
renderListingsTable() {
|
||||
switch (this.state.listCommunitiesResponse.state) {
|
||||
case "loading":
|
||||
return (
|
||||
|
@ -142,32 +147,8 @@ export class Communities extends Component<
|
|||
</h5>
|
||||
);
|
||||
case "success": {
|
||||
const { listingType, sort, page } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<h1 className="h4 mb-4">
|
||||
{I18NextService.i18n.t("list_of_communities")}
|
||||
</h1>
|
||||
<div className="row g-3 align-items-center mb-2">
|
||||
<div className="col-auto">
|
||||
<ListingTypeSelect
|
||||
type_={listingType}
|
||||
showLocal={showLocal(this.isoData)}
|
||||
showSubscribed
|
||||
onChange={this.handleListingTypeChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-auto me-auto">
|
||||
<SortSelect sort={sort} onChange={this.handleSortChange} />
|
||||
</div>
|
||||
<div className="col-auto">{this.searchForm()}</div>
|
||||
</div>
|
||||
|
||||
<div className="table-responsive">
|
||||
<table
|
||||
id="community_table"
|
||||
className="table table-sm table-hover"
|
||||
>
|
||||
<table id="community_table" className="table table-sm table-hover">
|
||||
<thead className="pointer">
|
||||
<tr>
|
||||
<th>{I18NextService.i18n.t("name")}</th>
|
||||
|
@ -188,8 +169,7 @@ export class Communities extends Component<
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.state.listCommunitiesResponse.data.communities.map(
|
||||
cv => (
|
||||
{this.state.listCommunitiesResponse.data.communities.map(cv => (
|
||||
<tr key={cv.community.id}>
|
||||
<td>
|
||||
<CommunityLink community={cv.community} />
|
||||
|
@ -229,33 +209,52 @@ export class Communities extends Component<
|
|||
/>
|
||||
</td>
|
||||
</tr>
|
||||
),
|
||||
)}
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Paginator
|
||||
page={page}
|
||||
onChange={this.handlePageChange}
|
||||
nextDisabled={
|
||||
communityLimit >
|
||||
this.state.listCommunitiesResponse.data.communities.length
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { listingType, sort, page } = this.props;
|
||||
return (
|
||||
<div className="communities container-lg">
|
||||
<HtmlTags
|
||||
title={this.documentTitle}
|
||||
path={this.context.router.route.match.url}
|
||||
/>
|
||||
{this.renderListings()}
|
||||
<div>
|
||||
<h1 className="h4 mb-4">
|
||||
{I18NextService.i18n.t("list_of_communities")}
|
||||
</h1>
|
||||
<div className="row g-3 align-items-center mb-2">
|
||||
<div className="col-auto">
|
||||
<ListingTypeSelect
|
||||
type_={listingType}
|
||||
showLocal={showLocal(this.isoData)}
|
||||
showSubscribed
|
||||
onChange={this.handleListingTypeChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-auto me-auto">
|
||||
<SortSelect sort={sort} onChange={this.handleSortChange} />
|
||||
</div>
|
||||
<div className="col-auto">{this.searchForm()}</div>
|
||||
</div>
|
||||
|
||||
<div className="table-responsive">{this.renderListingsTable()}</div>
|
||||
<Paginator
|
||||
page={page}
|
||||
onChange={this.handlePageChange}
|
||||
nextDisabled={
|
||||
this.state.listCommunitiesResponse.state !== "success" ||
|
||||
communityLimit >
|
||||
this.state.listCommunitiesResponse.data.communities.length
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -287,22 +286,16 @@ export class Communities extends Component<
|
|||
);
|
||||
}
|
||||
|
||||
async updateUrl({ listingType, sort, page }: Partial<CommunitiesProps>) {
|
||||
const {
|
||||
listingType: urlListingType,
|
||||
sort: urlSort,
|
||||
page: urlPage,
|
||||
} = this.props;
|
||||
async updateUrl(props: Partial<CommunitiesProps>) {
|
||||
const { listingType, sort, page } = { ...this.props, ...props };
|
||||
|
||||
const queryParams: QueryParams<CommunitiesProps> = {
|
||||
listingType: listingType ?? urlListingType,
|
||||
sort: sort ?? urlSort,
|
||||
page: (page ?? urlPage)?.toString(),
|
||||
listingType: listingType,
|
||||
sort: sort,
|
||||
page: page?.toString(),
|
||||
};
|
||||
|
||||
this.props.history.push(`/communities${getQueryString(queryParams)}`);
|
||||
|
||||
await this.refetch();
|
||||
}
|
||||
|
||||
handlePageChange(page: number) {
|
||||
|
@ -368,19 +361,19 @@ export class Communities extends Component<
|
|||
data.i.findAndUpdateCommunity(res);
|
||||
}
|
||||
|
||||
async refetch() {
|
||||
fetchToken?: symbol;
|
||||
async refetch({ listingType, sort, page }: CommunitiesProps) {
|
||||
const token = (this.fetchToken = Symbol());
|
||||
this.setState({ listCommunitiesResponse: LOADING_REQUEST });
|
||||
|
||||
const { listingType, sort, page } = this.props;
|
||||
|
||||
this.setState({
|
||||
listCommunitiesResponse: await HttpService.client.listCommunities({
|
||||
const listCommunitiesResponse = await HttpService.client.listCommunities({
|
||||
type_: listingType,
|
||||
sort: sort,
|
||||
limit: communityLimit,
|
||||
page,
|
||||
}),
|
||||
});
|
||||
if (token === this.fetchToken) {
|
||||
this.setState({ listCommunitiesResponse });
|
||||
}
|
||||
}
|
||||
|
||||
findAndUpdateCommunity(res: RequestState<CommunityResponse>) {
|
||||
|
|
|
@ -19,11 +19,18 @@ import {
|
|||
getQueryParams,
|
||||
getQueryString,
|
||||
resourcesSettled,
|
||||
bareRoutePush,
|
||||
} from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import type { QueryParams, StringBoolean } from "@utils/types";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
import { Component, RefObject, createRef, linkEvent } from "inferno";
|
||||
import {
|
||||
Component,
|
||||
InfernoNode,
|
||||
RefObject,
|
||||
createRef,
|
||||
linkEvent,
|
||||
} from "inferno";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import {
|
||||
AddAdmin,
|
||||
|
@ -100,7 +107,7 @@ import { CommentNodes } from "../comment/comment-nodes";
|
|||
import { BannerIconHeader } from "../common/banner-icon-header";
|
||||
import { DataTypeSelect } from "../common/data-type-select";
|
||||
import { HtmlTags } from "../common/html-tags";
|
||||
import { Icon, Spinner } from "../common/icon";
|
||||
import { Icon } from "../common/icon";
|
||||
import { SortSelect } from "../common/sort-select";
|
||||
import { SiteSidebar } from "../home/site-sidebar";
|
||||
import { PostListings } from "../post/post-listings";
|
||||
|
@ -114,6 +121,8 @@ import {
|
|||
import { Sidebar } from "./sidebar";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import PostHiddenSelect from "../common/post-hidden-select";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
import { LoadingEllipses } from "../common/loading-ellipses";
|
||||
|
||||
type CommunityData = RouteDataResponse<{
|
||||
communityRes: GetCommunityResponse;
|
||||
|
@ -265,21 +274,39 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
}
|
||||
}
|
||||
|
||||
async fetchCommunity() {
|
||||
fetchCommunityToken?: symbol;
|
||||
async fetchCommunity(props: CommunityRouteProps) {
|
||||
const token = (this.fetchCommunityToken = Symbol());
|
||||
this.setState({ communityRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
communityRes: await HttpService.client.getCommunity({
|
||||
name: this.props.match.params.name,
|
||||
}),
|
||||
const communityRes = await HttpService.client.getCommunity({
|
||||
name: props.match.params.name,
|
||||
});
|
||||
if (token === this.fetchCommunityToken) {
|
||||
this.setState({ communityRes });
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!this.state.isIsomorphic) {
|
||||
await Promise.all([this.fetchCommunity(), this.fetchData()]);
|
||||
async componentWillMount() {
|
||||
if (!this.state.isIsomorphic && isBrowser()) {
|
||||
await Promise.all([
|
||||
this.fetchCommunity(this.props),
|
||||
this.fetchData(this.props),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(
|
||||
nextProps: CommunityRouteProps & { children?: InfernoNode },
|
||||
) {
|
||||
if (
|
||||
bareRoutePush(this.props, nextProps) ||
|
||||
this.props.match.params.name !== nextProps.match.params.name
|
||||
) {
|
||||
this.fetchCommunity(nextProps);
|
||||
}
|
||||
this.fetchData(nextProps);
|
||||
}
|
||||
|
||||
static async fetchInitialData({
|
||||
headers,
|
||||
query: { dataType, pageCursor, sort, showHidden },
|
||||
|
@ -356,18 +383,12 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
}
|
||||
|
||||
renderCommunity() {
|
||||
switch (this.state.communityRes.state) {
|
||||
case "loading":
|
||||
return (
|
||||
<h5>
|
||||
<Spinner large />
|
||||
</h5>
|
||||
);
|
||||
case "success": {
|
||||
const res = this.state.communityRes.data;
|
||||
|
||||
const res =
|
||||
this.state.communityRes.state === "success" &&
|
||||
this.state.communityRes.data;
|
||||
return (
|
||||
<>
|
||||
{res && (
|
||||
<HtmlTags
|
||||
title={this.documentTitle}
|
||||
path={this.context.router.route.match.url}
|
||||
|
@ -375,13 +396,9 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
description={res.community_view.community.description}
|
||||
image={res.community_view.community.icon}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="row">
|
||||
<main
|
||||
className="col-12 col-md-8 col-lg-9"
|
||||
ref={this.mainContentRef}
|
||||
>
|
||||
{this.communityInfo(res)}
|
||||
{this.communityInfo()}
|
||||
<div className="d-block d-md-none">
|
||||
<button
|
||||
className="btn btn-secondary d-inline-block mb-2 me-3"
|
||||
|
@ -390,39 +407,43 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
{I18NextService.i18n.t("sidebar")}{" "}
|
||||
<Icon
|
||||
icon={
|
||||
this.state.showSidebarMobile
|
||||
? `minus-square`
|
||||
: `plus-square`
|
||||
this.state.showSidebarMobile ? `minus-square` : `plus-square`
|
||||
}
|
||||
classes="icon-inline"
|
||||
/>
|
||||
</button>
|
||||
{this.state.showSidebarMobile && this.sidebar(res)}
|
||||
{this.state.showSidebarMobile && this.sidebar()}
|
||||
</div>
|
||||
{this.selects(res)}
|
||||
{this.listings(res)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="community container-lg">
|
||||
<div className="row">
|
||||
<main className="col-12 col-md-8 col-lg-9" ref={this.mainContentRef}>
|
||||
{this.renderCommunity()}
|
||||
{this.selects()}
|
||||
{this.listings()}
|
||||
<PaginatorCursor
|
||||
nextPage={this.getNextPage}
|
||||
onNext={this.handlePageNext}
|
||||
/>
|
||||
</main>
|
||||
<aside className="d-none d-md-block col-md-4 col-lg-3">
|
||||
{this.sidebar(res)}
|
||||
{this.sidebar()}
|
||||
</aside>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="community container-lg">{this.renderCommunity()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
sidebar(res: GetCommunityResponse) {
|
||||
sidebar() {
|
||||
if (this.state.communityRes.state !== "success") {
|
||||
return undefined;
|
||||
}
|
||||
const res = this.state.communityRes.data;
|
||||
const siteRes = this.isoData.site_res;
|
||||
// For some reason, this returns an empty vec if it matches the site langs
|
||||
const communityLangs =
|
||||
|
@ -456,7 +477,7 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
);
|
||||
}
|
||||
|
||||
listings(communityRes: GetCommunityResponse) {
|
||||
listings() {
|
||||
const { dataType } = this.props;
|
||||
const siteRes = this.isoData.site_res;
|
||||
|
||||
|
@ -496,6 +517,9 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
);
|
||||
}
|
||||
} else {
|
||||
if (this.state.communityRes.state !== "success") {
|
||||
return;
|
||||
}
|
||||
switch (this.state.commentsRes.state) {
|
||||
case "loading":
|
||||
return <CommentsLoadingSkeleton />;
|
||||
|
@ -509,7 +533,7 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
showContext
|
||||
enableDownvotes={enableDownvotes(siteRes)}
|
||||
voteDisplayMode={voteDisplayMode(siteRes)}
|
||||
moderators={communityRes.moderators}
|
||||
moderators={this.state.communityRes.data.moderators}
|
||||
admins={siteRes.admins}
|
||||
allLanguages={siteRes.all_languages}
|
||||
siteLanguages={siteRes.discussion_languages}
|
||||
|
@ -537,28 +561,40 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
}
|
||||
}
|
||||
|
||||
communityInfo(res: GetCommunityResponse) {
|
||||
const community = res.community_view.community;
|
||||
communityInfo() {
|
||||
const res =
|
||||
(this.state.communityRes.state === "success" &&
|
||||
this.state.communityRes.data) ||
|
||||
undefined;
|
||||
const community = res && res.community_view.community;
|
||||
const urlCommunityName = this.props.match.params.name;
|
||||
|
||||
return (
|
||||
community && (
|
||||
<div className="mb-2">
|
||||
{community && (
|
||||
<BannerIconHeader banner={community.banner} icon={community.icon} />
|
||||
)}
|
||||
<div>
|
||||
<h1
|
||||
className="h4 mb-0 overflow-wrap-anywhere d-inline"
|
||||
data-tippy-content={
|
||||
community.posting_restricted_to_mods
|
||||
community?.posting_restricted_to_mods
|
||||
? I18NextService.i18n.t("community_locked")
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{community.title}
|
||||
{community?.title ?? (
|
||||
<>
|
||||
{urlCommunityName}
|
||||
<LoadingEllipses />
|
||||
</>
|
||||
)}
|
||||
</h1>
|
||||
{community.posting_restricted_to_mods && (
|
||||
{community?.posting_restricted_to_mods && (
|
||||
<Icon icon="lock" inline classes="text-danger fs-4 ms-2" />
|
||||
)}
|
||||
</div>
|
||||
{(community && (
|
||||
<CommunityLink
|
||||
community={community}
|
||||
realLink
|
||||
|
@ -566,12 +602,16 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
muted
|
||||
hideAvatar
|
||||
/>
|
||||
)) ??
|
||||
urlCommunityName}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
selects(res: GetCommunityResponse) {
|
||||
selects() {
|
||||
const res =
|
||||
this.state.communityRes.state === "success" &&
|
||||
this.state.communityRes.data;
|
||||
const { dataType, sort, showHidden } = this.props;
|
||||
const communityRss = res
|
||||
? communityRSSUrl(res.community_view.community.actor_id, sort)
|
||||
|
@ -641,40 +681,39 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
}));
|
||||
}
|
||||
|
||||
async updateUrl({
|
||||
async updateUrl(props: Partial<CommunityProps>) {
|
||||
const {
|
||||
dataType,
|
||||
pageCursor,
|
||||
sort,
|
||||
showHidden,
|
||||
}: Partial<CommunityProps>) {
|
||||
const {
|
||||
dataType: urlDataType,
|
||||
sort: urlSort,
|
||||
showHidden: urlShowHidden,
|
||||
} = this.props;
|
||||
|
||||
const queryParams: QueryParams<CommunityProps> = {
|
||||
dataType: getDataTypeString(dataType ?? urlDataType),
|
||||
pageCursor: pageCursor,
|
||||
sort: sort ?? urlSort,
|
||||
showHidden: showHidden ?? urlShowHidden,
|
||||
match: {
|
||||
params: { name },
|
||||
},
|
||||
} = {
|
||||
...this.props,
|
||||
...props,
|
||||
};
|
||||
|
||||
this.props.history.push(
|
||||
`/c/${this.props.match.params.name}${getQueryString(queryParams)}`,
|
||||
);
|
||||
const queryParams: QueryParams<CommunityProps> = {
|
||||
dataType: getDataTypeString(dataType ?? DataType.Post),
|
||||
pageCursor: pageCursor,
|
||||
sort: sort,
|
||||
showHidden: showHidden,
|
||||
};
|
||||
|
||||
await this.fetchData();
|
||||
this.props.history.push(`/c/${name}${getQueryString(queryParams)}`);
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
const { dataType, pageCursor, sort, showHidden } = this.props;
|
||||
const { name } = this.props.match.params;
|
||||
fetchDataToken?: symbol;
|
||||
async fetchData(props: CommunityRouteProps) {
|
||||
const token = (this.fetchDataToken = Symbol());
|
||||
const { dataType, pageCursor, sort, showHidden } = props;
|
||||
const { name } = props.match.params;
|
||||
|
||||
if (dataType === DataType.Post) {
|
||||
this.setState({ postsRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
postsRes: await HttpService.client.getPosts({
|
||||
this.setState({ postsRes: LOADING_REQUEST, commentsRes: EMPTY_REQUEST });
|
||||
const postsRes = await HttpService.client.getPosts({
|
||||
page_cursor: pageCursor,
|
||||
limit: fetchLimit,
|
||||
sort,
|
||||
|
@ -682,19 +721,22 @@ export class Community extends Component<CommunityRouteProps, State> {
|
|||
community_name: name,
|
||||
saved_only: false,
|
||||
show_hidden: showHidden === "true",
|
||||
}),
|
||||
});
|
||||
if (token === this.fetchDataToken) {
|
||||
this.setState({ postsRes });
|
||||
}
|
||||
} else {
|
||||
this.setState({ commentsRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
commentsRes: await HttpService.client.getComments({
|
||||
this.setState({ commentsRes: LOADING_REQUEST, postsRes: EMPTY_REQUEST });
|
||||
const commentsRes = await HttpService.client.getComments({
|
||||
limit: fetchLimit,
|
||||
sort: postToCommentSortType(sort),
|
||||
type_: "All",
|
||||
community_name: name,
|
||||
saved_only: false,
|
||||
}),
|
||||
});
|
||||
if (token === this.fetchDataToken) {
|
||||
this.setState({ commentsRes });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -80,6 +80,21 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
this.handleEditCancel = this.handleEditCancel.bind(this);
|
||||
}
|
||||
|
||||
unlisten = () => {};
|
||||
|
||||
componentWillMount() {
|
||||
// Leave edit mode on navigation
|
||||
this.unlisten = this.context.router.history.listen(() => {
|
||||
if (this.state.showEdit) {
|
||||
this.setState({ showEdit: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(
|
||||
nextProps: Readonly<{ children?: InfernoNode } & SidebarProps>,
|
||||
): void {
|
||||
|
|
|
@ -41,6 +41,7 @@ import { IRoutePropsWithFetch } from "../../routes";
|
|||
import { MediaUploads } from "../common/media-uploads";
|
||||
import { Paginator } from "../common/paginator";
|
||||
import { snapToTop } from "@utils/browser";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
type AdminSettingsData = RouteDataResponse<{
|
||||
bannedRes: BannedPersonsResponse;
|
||||
|
@ -51,7 +52,6 @@ type AdminSettingsData = RouteDataResponse<{
|
|||
interface AdminSettingsState {
|
||||
siteRes: GetSiteResponse;
|
||||
banned: PersonView[];
|
||||
currentTab: string;
|
||||
instancesRes: RequestState<GetFederatedInstancesResponse>;
|
||||
bannedRes: RequestState<BannedPersonsResponse>;
|
||||
leaveAdminTeamRes: RequestState<GetSiteResponse>;
|
||||
|
@ -79,7 +79,6 @@ export class AdminSettings extends Component<
|
|||
state: AdminSettingsState = {
|
||||
siteRes: this.isoData.site_res,
|
||||
banned: [],
|
||||
currentTab: "site",
|
||||
bannedRes: EMPTY_REQUEST,
|
||||
instancesRes: EMPTY_REQUEST,
|
||||
leaveAdminTeamRes: EMPTY_REQUEST,
|
||||
|
@ -134,7 +133,8 @@ export class AdminSettings extends Component<
|
|||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
async componentWillMount() {
|
||||
if (isBrowser()) {
|
||||
if (!this.state.isIsomorphic) {
|
||||
await this.fetchData();
|
||||
} else {
|
||||
|
@ -142,6 +142,7 @@ export class AdminSettings extends Component<
|
|||
this.setState({ themeList });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get documentTitle(): string {
|
||||
return `${I18NextService.i18n.t("admin_settings")} - ${
|
||||
|
@ -431,10 +432,6 @@ export class AdminSettings extends Component<
|
|||
return editRes;
|
||||
}
|
||||
|
||||
handleSwitchTab(i: { ctx: AdminSettings; tab: string }) {
|
||||
i.ctx.setState({ currentTab: i.tab });
|
||||
}
|
||||
|
||||
async handleLeaveAdminTeam(i: AdminSettings) {
|
||||
i.setState({ leaveAdminTeamRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
|
|
|
@ -19,13 +19,14 @@ import {
|
|||
getQueryString,
|
||||
getRandomFromList,
|
||||
resourcesSettled,
|
||||
bareRoutePush,
|
||||
} from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import { canCreateCommunity } from "@utils/roles";
|
||||
import type { QueryParams, StringBoolean } from "@utils/types";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
import { NoOptionI18nKeys } from "i18next";
|
||||
import { Component, MouseEventHandler, linkEvent } from "inferno";
|
||||
import { Component, InfernoNode, MouseEventHandler, linkEvent } from "inferno";
|
||||
import { T } from "inferno-i18next-dess";
|
||||
import { Link } from "inferno-router";
|
||||
import {
|
||||
|
@ -112,7 +113,7 @@ import {
|
|||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import PostHiddenSelect from "../common/post-hidden-select";
|
||||
import { snapToTop } from "@utils/browser";
|
||||
import { isBrowser, snapToTop } from "@utils/browser";
|
||||
|
||||
interface HomeState {
|
||||
postsRes: RequestState<GetPostsResponse>;
|
||||
|
@ -344,14 +345,28 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
|||
)?.content;
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
async componentWillMount() {
|
||||
if (
|
||||
!this.state.isIsomorphic ||
|
||||
(!this.state.isIsomorphic ||
|
||||
!Object.values(this.isoData.routeData).some(
|
||||
res => res.state === "success" || res.state === "failed",
|
||||
)
|
||||
)) &&
|
||||
isBrowser()
|
||||
) {
|
||||
await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
|
||||
await Promise.all([
|
||||
this.fetchTrendingCommunities(),
|
||||
this.fetchData(this.props),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(
|
||||
nextProps: HomeRouteProps & { children?: InfernoNode },
|
||||
) {
|
||||
this.fetchData(nextProps);
|
||||
|
||||
if (bareRoutePush(this.props, nextProps)) {
|
||||
this.fetchTrendingCommunities();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -661,34 +676,23 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
|||
);
|
||||
}
|
||||
|
||||
async updateUrl({
|
||||
dataType,
|
||||
listingType,
|
||||
pageCursor,
|
||||
sort,
|
||||
showHidden,
|
||||
}: Partial<HomeProps>) {
|
||||
const {
|
||||
dataType: urlDataType,
|
||||
listingType: urlListingType,
|
||||
sort: urlSort,
|
||||
showHidden: urlShowHidden,
|
||||
} = this.props;
|
||||
|
||||
async updateUrl(props: Partial<HomeProps>) {
|
||||
const { dataType, listingType, pageCursor, sort, showHidden } = {
|
||||
...this.props,
|
||||
...props,
|
||||
};
|
||||
const queryParams: QueryParams<HomeProps> = {
|
||||
dataType: getDataTypeString(dataType ?? urlDataType),
|
||||
listingType: listingType ?? urlListingType,
|
||||
dataType: getDataTypeString(dataType ?? DataType.Post),
|
||||
listingType: listingType,
|
||||
pageCursor: pageCursor,
|
||||
sort: sort ?? urlSort,
|
||||
showHidden: showHidden ?? urlShowHidden,
|
||||
sort: sort,
|
||||
showHidden: showHidden,
|
||||
};
|
||||
|
||||
this.props.history.push({
|
||||
pathname: "/",
|
||||
search: getQueryString(queryParams),
|
||||
});
|
||||
|
||||
await this.fetchData();
|
||||
}
|
||||
|
||||
get posts() {
|
||||
|
@ -854,31 +858,39 @@ export class Home extends Component<HomeRouteProps, HomeState> {
|
|||
});
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
const { dataType, pageCursor, listingType, sort, showHidden } = this.props;
|
||||
|
||||
fetchDataToken?: symbol;
|
||||
async fetchData({
|
||||
dataType,
|
||||
pageCursor,
|
||||
listingType,
|
||||
sort,
|
||||
showHidden,
|
||||
}: HomeProps) {
|
||||
const token = (this.fetchDataToken = Symbol());
|
||||
if (dataType === DataType.Post) {
|
||||
this.setState({ postsRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
postsRes: await HttpService.client.getPosts({
|
||||
this.setState({ postsRes: LOADING_REQUEST, commentsRes: EMPTY_REQUEST });
|
||||
const postsRes = await HttpService.client.getPosts({
|
||||
page_cursor: pageCursor,
|
||||
limit: fetchLimit,
|
||||
sort,
|
||||
saved_only: false,
|
||||
type_: listingType,
|
||||
show_hidden: showHidden === "true",
|
||||
}),
|
||||
});
|
||||
if (token === this.fetchDataToken) {
|
||||
this.setState({ postsRes });
|
||||
}
|
||||
} else {
|
||||
this.setState({ commentsRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
commentsRes: await HttpService.client.getComments({
|
||||
this.setState({ commentsRes: LOADING_REQUEST, postsRes: EMPTY_REQUEST });
|
||||
const commentsRes = await HttpService.client.getComments({
|
||||
limit: fetchLimit,
|
||||
sort: postToCommentSortType(sort),
|
||||
saved_only: false,
|
||||
type_: listingType,
|
||||
}),
|
||||
});
|
||||
if (token === this.fetchDataToken) {
|
||||
this.setState({ commentsRes });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import { RouteComponentProps } from "inferno-router/dist/Route";
|
|||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { resourcesSettled } from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
type InstancesData = RouteDataResponse<{
|
||||
federatedInstancesResponse: GetFederatedInstancesResponse;
|
||||
|
@ -71,8 +72,8 @@ export class Instances extends Component<InstancesRouteProps, InstancesState> {
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!this.state.isIsomorphic) {
|
||||
async componentWillMount() {
|
||||
if (!this.state.isIsomorphic && isBrowser()) {
|
||||
await this.fetchInstances();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import PasswordInput from "../common/password-input";
|
|||
import { SiteForm } from "./site-form";
|
||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
interface State {
|
||||
form: {
|
||||
|
@ -61,9 +62,11 @@ export class Setup extends Component<
|
|||
this.handleCreateSite = this.handleCreateSite.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
async componentWillMount() {
|
||||
if (isBrowser()) {
|
||||
this.setState({ themeList: await fetchThemeList() });
|
||||
}
|
||||
}
|
||||
|
||||
get documentTitle(): string {
|
||||
return `${I18NextService.i18n.t("setup")} - Lemmy`;
|
||||
|
|
|
@ -76,8 +76,11 @@ export class Signup extends Component<
|
|||
this.handleAnswerChange = this.handleAnswerChange.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (this.state.siteRes.site_view.local_site.captcha_enabled) {
|
||||
async componentWillMount() {
|
||||
if (
|
||||
this.state.siteRes.site_view.local_site.captcha_enabled &&
|
||||
isBrowser()
|
||||
) {
|
||||
await this.fetchCaptcha();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Modal } from "bootstrap";
|
|||
import { Component, InfernoNode, RefObject } from "inferno";
|
||||
|
||||
export function modalMixin<
|
||||
P extends { show: boolean },
|
||||
P extends { show?: boolean },
|
||||
S,
|
||||
Base extends new (...args: any[]) => Component<P, S> & {
|
||||
readonly modalDivRef: RefObject<HTMLDivElement>;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { isBrowser, nextUserAction, snapToTop } from "../../utils/browser";
|
||||
import { Component, InfernoNode } from "inferno";
|
||||
import { Location } from "history";
|
||||
import { Location, History, Action } from "history";
|
||||
|
||||
function restoreScrollPosition(props: { location: Location }) {
|
||||
const key: string = props.location.key;
|
||||
|
@ -25,7 +25,7 @@ function dropScrollPosition(props: { location: Location }) {
|
|||
}
|
||||
|
||||
export function scrollMixin<
|
||||
P extends { location: Location },
|
||||
P extends { location: Location; history: History },
|
||||
S,
|
||||
Base extends new (
|
||||
...args: any
|
||||
|
@ -68,11 +68,12 @@ export function scrollMixin<
|
|||
nextProps: Readonly<{ children?: InfernoNode } & P>,
|
||||
nextContext: any,
|
||||
) {
|
||||
// Currently this is hypothetical. Components unmount before route changes.
|
||||
if (this.props.location.key !== nextProps.location.key) {
|
||||
if (nextProps.history.action !== Action.Replace) {
|
||||
this.saveFinalPosition();
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
return super.componentWillReceiveProps?.(nextProps, nextContext);
|
||||
}
|
||||
|
||||
|
@ -131,7 +132,7 @@ export function scrollMixin<
|
|||
}
|
||||
|
||||
export function simpleScrollMixin<
|
||||
P extends { location: Location },
|
||||
P extends { location: Location; history: History },
|
||||
S,
|
||||
Base extends new (...args: any) => Component<P, S>,
|
||||
>(base: Base, _context?: ClassDecoratorContext<Base>) {
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
import {
|
||||
fetchUsers,
|
||||
getUpdatedSearchId,
|
||||
personToChoice,
|
||||
setIsoData,
|
||||
} from "@utils/app";
|
||||
import { fetchUsers, personToChoice, setIsoData } from "@utils/app";
|
||||
import {
|
||||
debounce,
|
||||
formatPastDate,
|
||||
|
@ -12,6 +7,7 @@ import {
|
|||
getQueryParams,
|
||||
getQueryString,
|
||||
resourcesSettled,
|
||||
bareRoutePush,
|
||||
} from "@utils/helpers";
|
||||
import { scrollMixin } from "./mixins/scroll-mixin";
|
||||
import { amAdmin, amMod } from "@utils/roles";
|
||||
|
@ -66,6 +62,8 @@ import { CommunityLink } from "./community/community-link";
|
|||
import { PersonListing } from "./person/person-listing";
|
||||
import { getHttpBaseInternal } from "../utils/env";
|
||||
import { IRoutePropsWithFetch } from "../routes";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
import { LoadingEllipses } from "./common/loading-ellipses";
|
||||
|
||||
type FilterType = "mod" | "user";
|
||||
|
||||
|
@ -703,40 +701,68 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!this.state.isIsomorphic) {
|
||||
const { modId, userId } = this.props;
|
||||
const promises = [this.refetch()];
|
||||
async componentWillMount() {
|
||||
if (!this.state.isIsomorphic && isBrowser()) {
|
||||
await Promise.all([
|
||||
this.fetchModlog(this.props),
|
||||
this.fetchCommunity(this.props),
|
||||
this.fetchUser(this.props),
|
||||
this.fetchMod(this.props),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: ModlogRouteProps) {
|
||||
this.fetchModlog(nextProps);
|
||||
|
||||
const reload = bareRoutePush(this.props, nextProps);
|
||||
|
||||
if (nextProps.modId !== this.props.modId || reload) {
|
||||
this.fetchMod(nextProps);
|
||||
}
|
||||
if (nextProps.userId !== this.props.userId || reload) {
|
||||
this.fetchUser(nextProps);
|
||||
}
|
||||
if (
|
||||
nextProps.match.params.communityId !==
|
||||
this.props.match.params.communityId ||
|
||||
reload
|
||||
) {
|
||||
this.fetchCommunity(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
fetchUserToken?: symbol;
|
||||
async fetchUser(props: ModlogRouteProps) {
|
||||
const token = (this.fetchUserToken = Symbol());
|
||||
const { userId } = props;
|
||||
|
||||
if (userId) {
|
||||
promises.push(
|
||||
HttpService.client
|
||||
.getPersonDetails({ person_id: userId })
|
||||
.then(res => {
|
||||
if (res.state === "success") {
|
||||
const res = await HttpService.client.getPersonDetails({
|
||||
person_id: userId,
|
||||
});
|
||||
if (res.state === "success" && token === this.fetchUserToken) {
|
||||
this.setState({
|
||||
userSearchOptions: [personToChoice(res.data.person_view)],
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fetchModToken?: symbol;
|
||||
async fetchMod(props: ModlogRouteProps) {
|
||||
const token = (this.fetchModToken = Symbol());
|
||||
const { modId } = props;
|
||||
|
||||
if (modId) {
|
||||
promises.push(
|
||||
HttpService.client
|
||||
.getPersonDetails({ person_id: modId })
|
||||
.then(res => {
|
||||
if (res.state === "success") {
|
||||
const res = await HttpService.client.getPersonDetails({
|
||||
person_id: modId,
|
||||
});
|
||||
if (res.state === "success" && token === this.fetchModToken) {
|
||||
this.setState({
|
||||
modSearchOptions: [personToChoice(res.data.person_view)],
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -793,6 +819,11 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
|
|||
modSearchOptions,
|
||||
} = this.state;
|
||||
const { actionType, modId, userId } = this.props;
|
||||
const { communityId } = this.props.match.params;
|
||||
|
||||
const communityState = this.state.communityRes.state;
|
||||
const communityResp =
|
||||
communityState === "success" && this.state.communityRes.data;
|
||||
|
||||
return (
|
||||
<div className="modlog container-lg">
|
||||
|
@ -816,15 +847,26 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
|
|||
#<strong>#</strong>#
|
||||
</T>
|
||||
</div>
|
||||
{this.state.communityRes.state === "success" && (
|
||||
{communityId && (
|
||||
<h5>
|
||||
{communityResp ? (
|
||||
<>
|
||||
<Link
|
||||
className="text-body"
|
||||
to={`/c/${this.state.communityRes.data.community_view.community.name}`}
|
||||
to={`/c/${communityResp.community_view.community.name}`}
|
||||
>
|
||||
/c/{this.state.communityRes.data.community_view.community.name}{" "}
|
||||
</Link>
|
||||
/c/{communityResp.community_view.community.name}
|
||||
</Link>{" "}
|
||||
<span>{I18NextService.i18n.t("modlog")}</span>
|
||||
</>
|
||||
) : (
|
||||
communityState === "loading" && (
|
||||
<>
|
||||
<LoadingEllipses />
|
||||
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</h5>
|
||||
)}
|
||||
<div className="row mb-2">
|
||||
|
@ -935,6 +977,10 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
|
|||
}
|
||||
|
||||
handleSearchUsers = debounce(async (text: string) => {
|
||||
if (!text.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { userId } = this.props;
|
||||
const { userSearchOptions } = this.state;
|
||||
this.setState({ loadingUserSearch: true });
|
||||
|
@ -952,6 +998,10 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
|
|||
});
|
||||
|
||||
handleSearchMods = debounce(async (text: string) => {
|
||||
if (!text.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { modId } = this.props;
|
||||
const { modSearchOptions } = this.state;
|
||||
this.setState({ loadingModSearch: true });
|
||||
|
@ -968,40 +1018,40 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
|
|||
});
|
||||
});
|
||||
|
||||
async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
|
||||
async updateUrl(props: Partial<ModlogProps>) {
|
||||
const {
|
||||
page: urlPage,
|
||||
actionType: urlActionType,
|
||||
modId: urlModId,
|
||||
userId: urlUserId,
|
||||
} = this.props;
|
||||
actionType,
|
||||
modId,
|
||||
page,
|
||||
userId,
|
||||
match: {
|
||||
params: { communityId },
|
||||
},
|
||||
} = { ...this.props, ...props };
|
||||
|
||||
const queryParams: QueryParams<ModlogProps> = {
|
||||
page: (page ?? urlPage).toString(),
|
||||
actionType: actionType ?? urlActionType,
|
||||
modId: getUpdatedSearchId(modId, urlModId),
|
||||
userId: getUpdatedSearchId(userId, urlUserId),
|
||||
page: page.toString(),
|
||||
actionType: actionType,
|
||||
modId: modId?.toString(),
|
||||
userId: userId?.toString(),
|
||||
};
|
||||
|
||||
const communityId = this.props.match.params.communityId;
|
||||
|
||||
this.props.history.push(
|
||||
`/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
|
||||
queryParams,
|
||||
)}`,
|
||||
);
|
||||
|
||||
await this.refetch();
|
||||
}
|
||||
|
||||
async refetch() {
|
||||
const { actionType, page, modId, userId, postId, commentId } = this.props;
|
||||
const { communityId: urlCommunityId } = this.props.match.params;
|
||||
fetchModlogToken?: symbol;
|
||||
async fetchModlog(props: ModlogRouteProps) {
|
||||
const token = (this.fetchModlogToken = Symbol());
|
||||
const { actionType, page, modId, userId, postId, commentId } = props;
|
||||
const { communityId: urlCommunityId } = props.match.params;
|
||||
const communityId = getIdFromString(urlCommunityId);
|
||||
|
||||
this.setState({ res: LOADING_REQUEST });
|
||||
this.setState({
|
||||
res: await HttpService.client.getModlog({
|
||||
const res = await HttpService.client.getModlog({
|
||||
community_id: communityId,
|
||||
page,
|
||||
limit: fetchLimit,
|
||||
|
@ -1013,16 +1063,28 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
|
|||
: undefined,
|
||||
comment_id: commentId,
|
||||
post_id: postId,
|
||||
}),
|
||||
});
|
||||
if (token === this.fetchModlogToken) {
|
||||
this.setState({ res });
|
||||
}
|
||||
}
|
||||
|
||||
fetchCommunityToken?: symbol;
|
||||
async fetchCommunity(props: ModlogRouteProps) {
|
||||
const token = (this.fetchCommunityToken = Symbol());
|
||||
const { communityId: urlCommunityId } = props.match.params;
|
||||
const communityId = getIdFromString(urlCommunityId);
|
||||
|
||||
if (communityId) {
|
||||
this.setState({ communityRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
communityRes: await HttpService.client.getCommunity({
|
||||
const communityRes = await HttpService.client.getCommunity({
|
||||
id: communityId,
|
||||
}),
|
||||
});
|
||||
if (token === this.fetchCommunityToken) {
|
||||
this.setState({ communityRes });
|
||||
}
|
||||
} else {
|
||||
this.setState({ communityRes: EMPTY_REQUEST });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -89,6 +89,7 @@ import { getHttpBaseInternal } from "../../utils/env";
|
|||
import { CommentsLoadingSkeleton } from "../common/loading-skeleton";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
enum UnreadOrAll {
|
||||
Unread,
|
||||
|
@ -213,8 +214,8 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!this.state.isIsomorphic) {
|
||||
async componentWillMount() {
|
||||
if (!this.state.isIsomorphic && isBrowser()) {
|
||||
await this.refetch();
|
||||
}
|
||||
}
|
||||
|
@ -784,40 +785,60 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
|
|||
return inboxData;
|
||||
}
|
||||
|
||||
refetchToken?: symbol;
|
||||
async refetch() {
|
||||
const token = (this.refetchToken = Symbol());
|
||||
const sort = this.state.sort;
|
||||
const unread_only = this.state.unreadOrAll === UnreadOrAll.Unread;
|
||||
const page = this.state.page;
|
||||
const limit = fetchLimit;
|
||||
|
||||
this.setState({ repliesRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
repliesRes: await HttpService.client.getReplies({
|
||||
repliesRes: LOADING_REQUEST,
|
||||
mentionsRes: LOADING_REQUEST,
|
||||
messagesRes: LOADING_REQUEST,
|
||||
});
|
||||
const repliesPromise = HttpService.client
|
||||
.getReplies({
|
||||
sort,
|
||||
unread_only,
|
||||
page,
|
||||
limit,
|
||||
}),
|
||||
})
|
||||
.then(repliesRes => {
|
||||
if (token === this.refetchToken) {
|
||||
this.setState({
|
||||
repliesRes,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({ mentionsRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
mentionsRes: await HttpService.client.getPersonMentions({
|
||||
const mentionsPromise = HttpService.client
|
||||
.getPersonMentions({
|
||||
sort,
|
||||
unread_only,
|
||||
page,
|
||||
limit,
|
||||
}),
|
||||
})
|
||||
.then(mentionsRes => {
|
||||
if (token === this.refetchToken) {
|
||||
this.setState({ mentionsRes });
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({ messagesRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
messagesRes: await HttpService.client.getPrivateMessages({
|
||||
const messagesPromise = HttpService.client
|
||||
.getPrivateMessages({
|
||||
unread_only,
|
||||
page,
|
||||
limit,
|
||||
}),
|
||||
})
|
||||
.then(messagesRes => {
|
||||
if (token === this.refetchToken) {
|
||||
this.setState({ messagesRes });
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all([repliesPromise, mentionsPromise, messagesPromise]);
|
||||
UnreadCounterService.Instance.updateInboxCounts();
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
numToSI,
|
||||
randomStr,
|
||||
resourcesSettled,
|
||||
bareRoutePush,
|
||||
} from "@utils/helpers";
|
||||
import { canMod } from "@utils/roles";
|
||||
import type { QueryParams } from "@utils/types";
|
||||
|
@ -100,6 +101,7 @@ import { getHttpBaseInternal } from "../../utils/env";
|
|||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { MediaUploads } from "../common/media-uploads";
|
||||
import { cakeDate } from "@utils/helpers";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
type ProfileData = RouteDataResponse<{
|
||||
personRes: GetPersonDetailsResponse;
|
||||
|
@ -108,6 +110,9 @@ type ProfileData = RouteDataResponse<{
|
|||
|
||||
interface ProfileState {
|
||||
personRes: RequestState<GetPersonDetailsResponse>;
|
||||
// personRes and personDetailsRes point to `===` identical data. This allows
|
||||
// to render the start of the profile while the new details are loading.
|
||||
personDetailsRes: RequestState<GetPersonDetailsResponse>;
|
||||
uploadsRes: RequestState<ListMediaResponse>;
|
||||
personBlocked: boolean;
|
||||
banReason?: string;
|
||||
|
@ -195,6 +200,7 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
private isoData = setIsoData<ProfileData>(this.context);
|
||||
state: ProfileState = {
|
||||
personRes: EMPTY_REQUEST,
|
||||
personDetailsRes: EMPTY_REQUEST,
|
||||
uploadsRes: EMPTY_REQUEST,
|
||||
personBlocked: false,
|
||||
siteRes: this.isoData.site_res,
|
||||
|
@ -205,7 +211,12 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
};
|
||||
|
||||
loadingSettled() {
|
||||
return resourcesSettled([this.state.personRes]);
|
||||
return resourcesSettled([
|
||||
this.state.personRes,
|
||||
this.props.view === PersonDetailsView.Uploads
|
||||
? this.state.uploadsRes
|
||||
: this.state.personDetailsRes,
|
||||
]);
|
||||
}
|
||||
|
||||
constructor(props: ProfileRouteProps, context: any) {
|
||||
|
@ -253,6 +264,7 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
this.state = {
|
||||
...this.state,
|
||||
personRes,
|
||||
personDetailsRes: personRes,
|
||||
uploadsRes,
|
||||
isIsomorphic: true,
|
||||
personBlocked: isPersonBlocked(personRes),
|
||||
|
@ -260,37 +272,96 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!this.state.isIsomorphic) {
|
||||
await this.fetchUserData();
|
||||
async componentWillMount() {
|
||||
if (!this.state.isIsomorphic && isBrowser()) {
|
||||
await this.fetchUserData(this.props, true);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchUserData() {
|
||||
const { page, sort, view } = this.props;
|
||||
componentWillReceiveProps(nextProps: ProfileRouteProps) {
|
||||
// Overview, Posts and Comments views can use the same data.
|
||||
const sharedViewTypes = [nextProps.view, this.props.view].every(
|
||||
v =>
|
||||
v === PersonDetailsView.Overview ||
|
||||
v === PersonDetailsView.Posts ||
|
||||
v === PersonDetailsView.Comments,
|
||||
);
|
||||
|
||||
const reload = bareRoutePush(this.props, nextProps);
|
||||
|
||||
const newUsername =
|
||||
nextProps.match.params.username !== this.props.match.params.username;
|
||||
|
||||
if (
|
||||
(nextProps.view !== this.props.view && !sharedViewTypes) ||
|
||||
nextProps.sort !== this.props.sort ||
|
||||
nextProps.page !== this.props.page ||
|
||||
newUsername ||
|
||||
reload
|
||||
) {
|
||||
this.fetchUserData(nextProps, reload || newUsername);
|
||||
}
|
||||
}
|
||||
|
||||
fetchUploadsToken?: symbol;
|
||||
async fetchUploads(props: ProfileRouteProps) {
|
||||
const token = (this.fetchUploadsToken = Symbol());
|
||||
const { page } = props;
|
||||
this.setState({ uploadsRes: LOADING_REQUEST });
|
||||
const form: ListMedia = {
|
||||
// userId?
|
||||
page,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
const uploadsRes = await HttpService.client.listMedia(form);
|
||||
if (token === this.fetchUploadsToken) {
|
||||
this.setState({ uploadsRes });
|
||||
}
|
||||
}
|
||||
|
||||
fetchUserDataToken?: symbol;
|
||||
async fetchUserData(props: ProfileRouteProps, showBothLoading = false) {
|
||||
const token = (this.fetchUploadsToken = this.fetchUserDataToken = Symbol());
|
||||
const { page, sort, view } = props;
|
||||
|
||||
if (view === PersonDetailsView.Uploads) {
|
||||
this.fetchUploads(props);
|
||||
if (!showBothLoading) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
personRes: LOADING_REQUEST,
|
||||
personDetailsRes: LOADING_REQUEST,
|
||||
});
|
||||
} else {
|
||||
if (showBothLoading) {
|
||||
this.setState({
|
||||
personRes: LOADING_REQUEST,
|
||||
personDetailsRes: LOADING_REQUEST,
|
||||
uploadsRes: EMPTY_REQUEST,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
personDetailsRes: LOADING_REQUEST,
|
||||
uploadsRes: EMPTY_REQUEST,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ personRes: LOADING_REQUEST });
|
||||
const personRes = await HttpService.client.getPersonDetails({
|
||||
username: this.props.match.params.username,
|
||||
username: props.match.params.username,
|
||||
sort,
|
||||
saved_only: view === PersonDetailsView.Saved,
|
||||
page,
|
||||
limit: fetchLimit,
|
||||
});
|
||||
|
||||
if (token === this.fetchUserDataToken) {
|
||||
this.setState({
|
||||
personRes,
|
||||
personDetailsRes: personRes,
|
||||
personBlocked: isPersonBlocked(personRes),
|
||||
});
|
||||
|
||||
if (view === PersonDetailsView.Uploads) {
|
||||
this.setState({ uploadsRes: LOADING_REQUEST });
|
||||
const form: ListMedia = {
|
||||
page,
|
||||
limit: fetchLimit,
|
||||
};
|
||||
const uploadsRes = await HttpService.client.listMedia(form);
|
||||
this.setState({ uploadsRes });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -384,6 +455,10 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
const personRes = this.state.personRes.data;
|
||||
const { page, sort, view } = this.props;
|
||||
|
||||
const personDetailsState = this.state.personDetailsRes.state;
|
||||
const personDetailsRes =
|
||||
personDetailsState === "success" && this.state.personDetailsRes.data;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-8">
|
||||
|
@ -403,8 +478,15 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
|
||||
{this.renderUploadsRes()}
|
||||
|
||||
{personDetailsState === "loading" &&
|
||||
this.props.view !== PersonDetailsView.Uploads ? (
|
||||
<h5>
|
||||
<Spinner large />
|
||||
</h5>
|
||||
) : (
|
||||
personDetailsRes && (
|
||||
<PersonDetails
|
||||
personRes={personRes}
|
||||
personRes={personDetailsRes}
|
||||
admins={siteRes.admins}
|
||||
sort={sort}
|
||||
page={page}
|
||||
|
@ -447,6 +529,8 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
onFeaturePost={this.handleFeaturePost}
|
||||
onMarkPostAsRead={() => {}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-4">
|
||||
|
@ -788,19 +872,23 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
|||
);
|
||||
}
|
||||
|
||||
async updateUrl({ page, sort, view }: Partial<ProfileProps>) {
|
||||
const { page: urlPage, sort: urlSort, view: urlView } = this.props;
|
||||
async updateUrl(props: Partial<ProfileRouteProps>) {
|
||||
const {
|
||||
page,
|
||||
sort,
|
||||
view,
|
||||
match: {
|
||||
params: { username },
|
||||
},
|
||||
} = { ...this.props, ...props };
|
||||
|
||||
const queryParams: QueryParams<ProfileProps> = {
|
||||
page: (page ?? urlPage).toString(),
|
||||
sort: sort ?? urlSort,
|
||||
view: view ?? urlView,
|
||||
page: page?.toString(),
|
||||
sort,
|
||||
view,
|
||||
};
|
||||
|
||||
const { username } = this.props.match.params;
|
||||
|
||||
this.props.history.push(`/u/${username}${getQueryString(queryParams)}`);
|
||||
await this.fetchUserData();
|
||||
}
|
||||
|
||||
handlePageChange(page: number) {
|
||||
|
|
|
@ -29,6 +29,7 @@ import { UnreadCounterService } from "../../services";
|
|||
import { getHttpBaseInternal } from "../../utils/env";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
enum RegistrationState {
|
||||
Unread,
|
||||
|
@ -92,8 +93,8 @@ export class RegistrationApplications extends Component<
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!this.state.isIsomorphic) {
|
||||
async componentWillMount() {
|
||||
if (!this.state.isIsomorphic && isBrowser()) {
|
||||
await this.refetch();
|
||||
}
|
||||
}
|
||||
|
@ -108,15 +109,11 @@ export class RegistrationApplications extends Component<
|
|||
}
|
||||
|
||||
renderApps() {
|
||||
switch (this.state.appsRes.state) {
|
||||
case "loading":
|
||||
return (
|
||||
<h5>
|
||||
<Spinner large />
|
||||
</h5>
|
||||
);
|
||||
case "success": {
|
||||
const apps = this.state.appsRes.data.registration_applications;
|
||||
const appsState = this.state.appsRes.state;
|
||||
const apps =
|
||||
appsState === "success" &&
|
||||
this.state.appsRes.data.registration_applications;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
|
@ -128,18 +125,26 @@ export class RegistrationApplications extends Component<
|
|||
{I18NextService.i18n.t("registration_applications")}
|
||||
</h1>
|
||||
{this.selects()}
|
||||
{apps ? (
|
||||
<>
|
||||
{this.applicationList(apps)}
|
||||
<Paginator
|
||||
page={this.state.page}
|
||||
onChange={this.handlePageChange}
|
||||
nextDisabled={fetchLimit > apps.length}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
appsState === "loading" && (
|
||||
<div className="text-center">
|
||||
<Spinner large />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
@ -263,19 +268,22 @@ export class RegistrationApplications extends Component<
|
|||
};
|
||||
}
|
||||
|
||||
refetchToken?: symbol;
|
||||
async refetch() {
|
||||
const token = (this.refetchToken = Symbol());
|
||||
const unread_only =
|
||||
this.state.registrationState === RegistrationState.Unread;
|
||||
this.setState({
|
||||
appsRes: LOADING_REQUEST,
|
||||
});
|
||||
this.setState({
|
||||
appsRes: await HttpService.client.listRegistrationApplications({
|
||||
const appsRes = await HttpService.client.listRegistrationApplications({
|
||||
unread_only: unread_only,
|
||||
page: this.state.page,
|
||||
limit: fetchLimit,
|
||||
}),
|
||||
});
|
||||
if (token === this.refetchToken) {
|
||||
this.setState({ appsRes });
|
||||
}
|
||||
}
|
||||
|
||||
async handleApproveApplication(form: ApproveRegistrationApplication) {
|
||||
|
|
|
@ -56,6 +56,7 @@ import { UnreadCounterService } from "../../services";
|
|||
import { getHttpBaseInternal } from "../../utils/env";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
enum UnreadOrAll {
|
||||
Unread,
|
||||
|
@ -160,8 +161,8 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!this.state.isIsomorphic) {
|
||||
async componentWillMount() {
|
||||
if (!this.state.isIsomorphic && isBrowser()) {
|
||||
await this.refetch();
|
||||
}
|
||||
}
|
||||
|
@ -452,9 +453,22 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
|
|||
}
|
||||
|
||||
all() {
|
||||
const combined = this.buildCombined;
|
||||
if (
|
||||
combined.length === 0 &&
|
||||
(this.state.commentReportsRes.state === "loading" ||
|
||||
this.state.postReportsRes.state === "loading" ||
|
||||
this.state.messageReportsRes.state === "loading")
|
||||
) {
|
||||
return (
|
||||
<h5>
|
||||
<Spinner large />
|
||||
</h5>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{this.buildCombined.map(i => (
|
||||
{combined.map(i => (
|
||||
<>
|
||||
<hr />
|
||||
{this.renderItemType(i)}
|
||||
|
@ -575,6 +589,7 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
|
|||
|
||||
static async fetchInitialData({
|
||||
headers,
|
||||
site,
|
||||
}: InitialFetchRequest): Promise<ReportsData> {
|
||||
const client = wrapClient(
|
||||
new LemmyHttp(getHttpBaseInternal(), { headers }),
|
||||
|
@ -601,7 +616,7 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
|
|||
messageReportsRes: EMPTY_REQUEST,
|
||||
};
|
||||
|
||||
if (amAdmin()) {
|
||||
if (amAdmin(site.my_user)) {
|
||||
const privateMessageReportsForm: ListPrivateMessageReports = {
|
||||
unresolved_only,
|
||||
page,
|
||||
|
@ -616,7 +631,9 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
|
|||
return data;
|
||||
}
|
||||
|
||||
refetchToken?: symbol;
|
||||
async refetch() {
|
||||
const token = (this.refetchToken = Symbol());
|
||||
const unresolved_only = this.state.unreadOrAll === UnreadOrAll.Unread;
|
||||
const page = this.state.page;
|
||||
const limit = fetchLimit;
|
||||
|
@ -636,19 +653,32 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
|
|||
limit,
|
||||
};
|
||||
|
||||
this.setState({
|
||||
commentReportsRes: await HttpService.client.listCommentReports(form),
|
||||
postReportsRes: await HttpService.client.listPostReports(form),
|
||||
const commentReportPromise = HttpService.client
|
||||
.listCommentReports(form)
|
||||
.then(commentReportsRes => {
|
||||
if (token === this.refetchToken) {
|
||||
this.setState({ commentReportsRes });
|
||||
}
|
||||
});
|
||||
const postReportPromise = HttpService.client
|
||||
.listPostReports(form)
|
||||
.then(postReportsRes => {
|
||||
if (token === this.refetchToken) {
|
||||
this.setState({ postReportsRes });
|
||||
}
|
||||
});
|
||||
|
||||
if (amAdmin()) {
|
||||
this.setState({
|
||||
messageReportsRes:
|
||||
await HttpService.client.listPrivateMessageReports(form),
|
||||
});
|
||||
const messageReportsRes =
|
||||
await HttpService.client.listPrivateMessageReports(form);
|
||||
if (token === this.refetchToken) {
|
||||
this.setState({ messageReportsRes });
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([commentReportPromise, postReportPromise]);
|
||||
}
|
||||
|
||||
async handleResolveCommentReport(form: ResolveCommentReport) {
|
||||
const res = await HttpService.client.resolveCommentReport(form);
|
||||
this.findAndUpdateCommentReport(res);
|
||||
|
|
|
@ -348,7 +348,7 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
async componentWillMount() {
|
||||
this.setState({ themeList: await fetchThemeList() });
|
||||
|
||||
if (!this.state.isIsomorphic) {
|
||||
|
|
|
@ -13,6 +13,7 @@ import { HtmlTags } from "../common/html-tags";
|
|||
import { Spinner } from "../common/icon";
|
||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
interface State {
|
||||
verifyRes: RequestState<SuccessResponse>;
|
||||
|
@ -52,9 +53,11 @@ export class VerifyEmail extends Component<
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
async componentWillMount() {
|
||||
if (isBrowser()) {
|
||||
await this.verify();
|
||||
}
|
||||
}
|
||||
|
||||
get documentTitle(): string {
|
||||
return `${I18NextService.i18n.t("verify_email")} - ${
|
||||
|
|
|
@ -33,6 +33,7 @@ import { getHttpBaseInternal } from "../../utils/env";
|
|||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { simpleScrollMixin } from "../mixins/scroll-mixin";
|
||||
import { toast } from "../../toast";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
export interface CreatePostProps {
|
||||
communityId?: number;
|
||||
|
@ -81,7 +82,7 @@ export class CreatePost extends Component<
|
|||
private isoData = setIsoData<CreatePostData>(this.context);
|
||||
state: CreatePostState = {
|
||||
siteRes: this.isoData.site_res,
|
||||
loading: true,
|
||||
loading: false,
|
||||
initialCommunitiesRes: EMPTY_REQUEST,
|
||||
isIsomorphic: false,
|
||||
};
|
||||
|
@ -132,9 +133,9 @@ export class CreatePost extends Component<
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
async componentWillMount() {
|
||||
// TODO test this
|
||||
if (!this.state.isIsomorphic) {
|
||||
if (!this.state.isIsomorphic && isBrowser()) {
|
||||
const { communityId } = this.props;
|
||||
|
||||
const initialCommunitiesRes = await fetchCommunitiesForOptions(
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { isImage } from "@utils/media";
|
||||
import { Choice } from "@utils/types";
|
||||
import autosize from "autosize";
|
||||
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||
import { Component, InfernoNode, createRef, linkEvent } from "inferno";
|
||||
import { Prompt } from "inferno-router";
|
||||
import {
|
||||
CommunityView,
|
||||
|
@ -132,8 +132,9 @@ function copySuggestedTitle(d: { i: PostForm; suggestedTitle?: string }) {
|
|||
);
|
||||
d.i.setState({ suggestedPostsRes: EMPTY_REQUEST });
|
||||
setTimeout(() => {
|
||||
const textarea: any = document.getElementById("post-title");
|
||||
autosize.update(textarea);
|
||||
if (d.i.postTitleRef.current) {
|
||||
autosize.update(d.i.postTitleRef.current);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
@ -248,6 +249,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
submitted: false,
|
||||
};
|
||||
|
||||
postTitleRef = createRef<HTMLTextAreaElement>();
|
||||
|
||||
constructor(props: PostFormProps, context: any) {
|
||||
super(props, context);
|
||||
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this));
|
||||
|
@ -306,17 +309,19 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
const textarea: any = document.getElementById("post-title");
|
||||
|
||||
if (textarea) {
|
||||
autosize(textarea);
|
||||
if (this.postTitleRef.current) {
|
||||
autosize(this.postTitleRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(
|
||||
nextProps: Readonly<{ children?: InfernoNode } & PostFormProps>,
|
||||
): void {
|
||||
if (this.props !== nextProps) {
|
||||
if (
|
||||
this.props.selectedCommunityChoice?.value !==
|
||||
nextProps.selectedCommunityChoice?.value &&
|
||||
nextProps.selectedCommunityChoice
|
||||
) {
|
||||
this.setState(
|
||||
s => (
|
||||
(s.form.community_id = getIdFromString(
|
||||
|
@ -325,6 +330,22 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
s
|
||||
),
|
||||
);
|
||||
this.setState({
|
||||
communitySearchOptions: [nextProps.selectedCommunityChoice].concat(
|
||||
(nextProps.initialCommunities?.map(communityToChoice) ?? []).filter(
|
||||
option => option.value !== nextProps.selectedCommunityChoice?.value,
|
||||
),
|
||||
),
|
||||
});
|
||||
}
|
||||
if (
|
||||
!this.props.initialCommunities?.length &&
|
||||
nextProps.initialCommunities?.length
|
||||
) {
|
||||
this.setState({
|
||||
communitySearchOptions:
|
||||
nextProps.initialCommunities?.map(communityToChoice) ?? [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -362,6 +383,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
rows={1}
|
||||
minLength={3}
|
||||
maxLength={MAX_POST_TITLE_LENGTH}
|
||||
ref={this.postTitleRef}
|
||||
/>
|
||||
{!validTitle(this.state.form.name) && (
|
||||
<div className="invalid-feedback">
|
||||
|
|
|
@ -137,7 +137,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
this.handleHidePost = this.handleHidePost.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
unlisten = () => {};
|
||||
|
||||
componentWillMount(): void {
|
||||
if (
|
||||
UserService.Instance.myUserInfo &&
|
||||
!this.isoData.showAdultConsentModal
|
||||
|
@ -148,6 +150,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
imageExpanded: auto_expand && !(blur_nsfw && this.postView.post.nsfw),
|
||||
});
|
||||
}
|
||||
|
||||
// Leave edit mode on navigation
|
||||
this.unlisten = this.context.router.history.listen(() => {
|
||||
if (this.state.showEdit) {
|
||||
this.setState({ showEdit: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
get postView(): PostView {
|
||||
|
|
|
@ -18,15 +18,17 @@ import { isBrowser } from "@utils/browser";
|
|||
import {
|
||||
debounce,
|
||||
getApubName,
|
||||
getQueryParams,
|
||||
getQueryString,
|
||||
randomStr,
|
||||
resourcesSettled,
|
||||
bareRoutePush,
|
||||
} from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import { isImage } from "@utils/media";
|
||||
import { RouteDataResponse } from "@utils/types";
|
||||
import autosize from "autosize";
|
||||
import { QueryParams, RouteDataResponse } from "@utils/types";
|
||||
import classNames from "classnames";
|
||||
import { Component, RefObject, createRef, linkEvent } from "inferno";
|
||||
import { Component, createRef, linkEvent } from "inferno";
|
||||
import {
|
||||
AddAdmin,
|
||||
AddModToCommunity,
|
||||
|
@ -103,6 +105,7 @@ import { PostListing } from "./post-listing";
|
|||
import { getHttpBaseInternal } from "../../utils/env";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { compareAsc, compareDesc } from "date-fns";
|
||||
|
||||
const commentsShownInterval = 15;
|
||||
|
||||
|
@ -112,44 +115,107 @@ type PostData = RouteDataResponse<{
|
|||
}>;
|
||||
|
||||
interface PostState {
|
||||
postId?: number;
|
||||
commentId?: number;
|
||||
postRes: RequestState<GetPostResponse>;
|
||||
commentsRes: RequestState<GetCommentsResponse>;
|
||||
commentSort: CommentSortType;
|
||||
commentViewType: CommentViewType;
|
||||
scrolled?: boolean;
|
||||
siteRes: GetSiteResponse;
|
||||
commentSectionRef?: RefObject<HTMLDivElement>;
|
||||
showSidebarMobile: boolean;
|
||||
maxCommentsShown: number;
|
||||
finished: Map<CommentId, boolean | undefined>;
|
||||
isIsomorphic: boolean;
|
||||
}
|
||||
|
||||
type PostPathProps =
|
||||
| { post_id: string; comment_id: never }
|
||||
| { post_id: never; comment_id: string };
|
||||
type PostRouteProps = RouteComponentProps<PostPathProps> &
|
||||
Record<string, never>;
|
||||
const defaultCommentSort: CommentSortType = "Hot";
|
||||
|
||||
function getCommentSortTypeFromQuery(source?: string): CommentSortType {
|
||||
if (!source) {
|
||||
return defaultCommentSort;
|
||||
}
|
||||
switch (source) {
|
||||
case "Hot":
|
||||
case "Top":
|
||||
case "New":
|
||||
case "Old":
|
||||
case "Controversial":
|
||||
return source;
|
||||
default:
|
||||
return defaultCommentSort;
|
||||
}
|
||||
}
|
||||
|
||||
function getQueryStringFromCommentSortType(
|
||||
sort: CommentSortType,
|
||||
): undefined | string {
|
||||
if (sort === defaultCommentSort) {
|
||||
return undefined;
|
||||
}
|
||||
return sort;
|
||||
}
|
||||
|
||||
const defaultCommentView: CommentViewType = CommentViewType.Tree;
|
||||
|
||||
function getCommentViewTypeFromQuery(source?: string): CommentViewType {
|
||||
switch (source) {
|
||||
case "Tree":
|
||||
return CommentViewType.Tree;
|
||||
case "Flat":
|
||||
return CommentViewType.Flat;
|
||||
default:
|
||||
return defaultCommentView;
|
||||
}
|
||||
}
|
||||
|
||||
function getQueryStringFromCommentView(
|
||||
view: CommentViewType,
|
||||
): string | undefined {
|
||||
if (view === defaultCommentView) {
|
||||
return undefined;
|
||||
}
|
||||
switch (view) {
|
||||
case CommentViewType.Tree:
|
||||
return "Tree";
|
||||
case CommentViewType.Flat:
|
||||
return "Flat";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
interface PostProps {
|
||||
sort: CommentSortType;
|
||||
view: CommentViewType;
|
||||
scrollToComments: boolean;
|
||||
}
|
||||
export function getPostQueryParams(source: string | undefined): PostProps {
|
||||
return getQueryParams<PostProps>(
|
||||
{
|
||||
scrollToComments: (s?: string) => !!s,
|
||||
sort: getCommentSortTypeFromQuery,
|
||||
view: getCommentViewTypeFromQuery,
|
||||
},
|
||||
source,
|
||||
);
|
||||
}
|
||||
|
||||
type PostPathProps = { post_id?: string; comment_id?: string };
|
||||
type PostRouteProps = RouteComponentProps<PostPathProps> & PostProps;
|
||||
type PartialPostRouteProps = Partial<
|
||||
PostProps & { match: { params: PostPathProps } }
|
||||
>;
|
||||
export type PostFetchConfig = IRoutePropsWithFetch<
|
||||
PostData,
|
||||
PostPathProps,
|
||||
Record<string, never>
|
||||
PostProps
|
||||
>;
|
||||
|
||||
@scrollMixin
|
||||
export class Post extends Component<PostRouteProps, PostState> {
|
||||
private isoData = setIsoData<PostData>(this.context);
|
||||
private commentScrollDebounced: () => void;
|
||||
private shouldScrollToComments: boolean = false;
|
||||
private commentSectionRef = createRef<HTMLDivElement>();
|
||||
state: PostState = {
|
||||
postRes: EMPTY_REQUEST,
|
||||
commentsRes: EMPTY_REQUEST,
|
||||
postId: getIdFromProps(this.props),
|
||||
commentId: getCommentIdFromProps(this.props),
|
||||
commentSort: "Hot",
|
||||
commentViewType: CommentViewType.Tree,
|
||||
scrolled: false,
|
||||
siteRes: this.isoData.site_res,
|
||||
showSidebarMobile: false,
|
||||
maxCommentsShown: commentsShownInterval,
|
||||
|
@ -201,8 +267,6 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
this.handleScrollIntoCommentsClick =
|
||||
this.handleScrollIntoCommentsClick.bind(this);
|
||||
|
||||
this.state = { ...this.state, commentSectionRef: createRef() };
|
||||
|
||||
// Only fetch the data if coming from another route
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const { commentsRes, postRes } = this.isoData.routeData;
|
||||
|
@ -213,71 +277,104 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
commentsRes,
|
||||
isIsomorphic: true,
|
||||
};
|
||||
|
||||
if (isBrowser()) {
|
||||
if (this.checkScrollIntoCommentsParam) {
|
||||
this.scrollIntoCommentSection();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fetchPost() {
|
||||
this.setState({
|
||||
postRes: LOADING_REQUEST,
|
||||
commentsRes: LOADING_REQUEST,
|
||||
fetchPostToken?: symbol;
|
||||
async fetchPost(props: PostRouteProps) {
|
||||
const token = (this.fetchPostToken = Symbol());
|
||||
this.setState({ postRes: LOADING_REQUEST });
|
||||
const postRes = await HttpService.client.getPost({
|
||||
id: getIdFromProps(props),
|
||||
comment_id: getCommentIdFromProps(props),
|
||||
});
|
||||
if (token === this.fetchPostToken) {
|
||||
this.setState({ postRes });
|
||||
}
|
||||
}
|
||||
|
||||
const [postRes, commentsRes] = await Promise.all([
|
||||
await HttpService.client.getPost({
|
||||
id: this.state.postId,
|
||||
comment_id: this.state.commentId,
|
||||
}),
|
||||
HttpService.client.getComments({
|
||||
post_id: this.state.postId,
|
||||
parent_id: this.state.commentId,
|
||||
fetchCommentsToken?: symbol;
|
||||
async fetchComments(props: PostRouteProps) {
|
||||
const token = (this.fetchCommentsToken = Symbol());
|
||||
const { sort } = props;
|
||||
this.setState({ commentsRes: LOADING_REQUEST });
|
||||
const commentsRes = await HttpService.client.getComments({
|
||||
post_id: getIdFromProps(props),
|
||||
parent_id: getCommentIdFromProps(props),
|
||||
max_depth: commentTreeMaxDepth,
|
||||
sort: this.state.commentSort,
|
||||
sort,
|
||||
type_: "All",
|
||||
saved_only: false,
|
||||
}),
|
||||
]);
|
||||
|
||||
this.setState({
|
||||
postRes,
|
||||
commentsRes,
|
||||
});
|
||||
if (token === this.fetchCommentsToken) {
|
||||
this.setState({ commentsRes });
|
||||
}
|
||||
}
|
||||
|
||||
if (this.checkScrollIntoCommentsParam) {
|
||||
this.scrollIntoCommentSection();
|
||||
updateUrl(props: PartialPostRouteProps, replace = false) {
|
||||
const {
|
||||
view,
|
||||
sort,
|
||||
match: {
|
||||
params: { comment_id, post_id },
|
||||
},
|
||||
} = {
|
||||
...this.props,
|
||||
...props,
|
||||
};
|
||||
|
||||
const query: QueryParams<PostProps> = {
|
||||
sort: getQueryStringFromCommentSortType(sort),
|
||||
view: getQueryStringFromCommentView(view),
|
||||
};
|
||||
|
||||
// Not inheriting old scrollToComments
|
||||
if (props.scrollToComments) {
|
||||
query.scrollToComments = true.toString();
|
||||
}
|
||||
|
||||
let pathname: string | undefined;
|
||||
if (comment_id && post_id) {
|
||||
pathname = `/post/${post_id}/${comment_id}`;
|
||||
} else if (comment_id) {
|
||||
pathname = `/comment/${comment_id}`;
|
||||
} else {
|
||||
pathname = `/post/${post_id}`;
|
||||
}
|
||||
|
||||
const location = { pathname, search: getQueryString(query) };
|
||||
if (replace || this.props.location.pathname === pathname) {
|
||||
this.props.history.replace(location);
|
||||
} else {
|
||||
this.props.history.push(location);
|
||||
}
|
||||
}
|
||||
|
||||
static async fetchInitialData({
|
||||
headers,
|
||||
match,
|
||||
}: InitialFetchRequest<PostPathProps>): Promise<PostData> {
|
||||
query: { sort },
|
||||
}: InitialFetchRequest<PostPathProps, PostProps>): Promise<PostData> {
|
||||
const client = wrapClient(
|
||||
new LemmyHttp(getHttpBaseInternal(), { headers }),
|
||||
);
|
||||
const postId = getIdFromProps({ match });
|
||||
const commentId = getCommentIdFromProps({ match });
|
||||
|
||||
const postForm: GetPost = {};
|
||||
const postForm: GetPost = {
|
||||
id: postId,
|
||||
comment_id: commentId,
|
||||
};
|
||||
|
||||
const commentsForm: GetComments = {
|
||||
post_id: postId,
|
||||
parent_id: commentId,
|
||||
max_depth: commentTreeMaxDepth,
|
||||
sort: "Hot",
|
||||
sort,
|
||||
type_: "All",
|
||||
saved_only: false,
|
||||
};
|
||||
|
||||
postForm.id = postId;
|
||||
postForm.comment_id = commentId;
|
||||
|
||||
commentsForm.post_id = postId;
|
||||
commentsForm.parent_id = commentId;
|
||||
|
||||
const [postRes, commentsRes] = await Promise.all([
|
||||
client.getPost(postForm),
|
||||
client.getComments(commentsForm),
|
||||
|
@ -293,15 +390,79 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
document.removeEventListener("scroll", this.commentScrollDebounced);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
async componentWillMount() {
|
||||
if (isBrowser()) {
|
||||
this.shouldScrollToComments = this.props.scrollToComments;
|
||||
if (!this.state.isIsomorphic) {
|
||||
await this.fetchPost();
|
||||
await Promise.all([
|
||||
this.fetchPost(this.props),
|
||||
this.fetchComments(this.props),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
autosize(document.querySelectorAll("textarea"));
|
||||
|
||||
componentDidMount() {
|
||||
this.commentScrollDebounced = debounce(this.trackCommentsBoxScrolling, 100);
|
||||
document.addEventListener("scroll", this.commentScrollDebounced);
|
||||
|
||||
if (this.state.isIsomorphic) {
|
||||
this.maybeScrollToComments();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: PostRouteProps): void {
|
||||
const { post_id: nextPost, comment_id: nextComment } =
|
||||
nextProps.match.params;
|
||||
const { post_id: prevPost, comment_id: prevComment } =
|
||||
this.props.match.params;
|
||||
|
||||
const newOrder =
|
||||
this.props.sort !== nextProps.sort || this.props.view !== nextProps.view;
|
||||
|
||||
// For comment links restore sort type from current props.
|
||||
if (
|
||||
nextPost === prevPost &&
|
||||
nextComment &&
|
||||
newOrder &&
|
||||
!nextProps.location.search &&
|
||||
nextProps.history.action === "PUSH"
|
||||
) {
|
||||
this.updateUrl({ match: nextProps.match }, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const needPost =
|
||||
prevPost !== nextPost ||
|
||||
(bareRoutePush(this.props, nextProps) && !nextComment);
|
||||
const needComments =
|
||||
needPost ||
|
||||
prevComment !== nextComment ||
|
||||
nextProps.sort !== this.props.sort;
|
||||
|
||||
if (needPost) {
|
||||
this.fetchPost(nextProps);
|
||||
}
|
||||
if (needComments) {
|
||||
this.fetchComments(nextProps);
|
||||
}
|
||||
|
||||
if (
|
||||
nextProps.scrollToComments &&
|
||||
this.props.scrollToComments !== nextProps.scrollToComments
|
||||
) {
|
||||
this.shouldScrollToComments = true;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
if (
|
||||
this.commentSectionRef.current &&
|
||||
this.state.postRes.state === "success" &&
|
||||
this.state.commentsRes.state === "success"
|
||||
) {
|
||||
this.maybeScrollToComments();
|
||||
}
|
||||
}
|
||||
|
||||
handleScrollIntoCommentsClick(e: MouseEvent) {
|
||||
|
@ -309,16 +470,18 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
e.preventDefault();
|
||||
}
|
||||
|
||||
get checkScrollIntoCommentsParam() {
|
||||
return (
|
||||
Boolean(
|
||||
new URLSearchParams(this.props.location.search).get("scrollToComments"),
|
||||
) && this.props.history.action !== "POP"
|
||||
);
|
||||
maybeScrollToComments() {
|
||||
if (this.shouldScrollToComments) {
|
||||
this.shouldScrollToComments = false;
|
||||
if (this.props.history.action !== "POP" || this.state.isIsomorphic) {
|
||||
this.scrollIntoCommentSection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollIntoCommentSection() {
|
||||
this.state.commentSectionRef?.current?.scrollIntoView();
|
||||
// This doesn't work when in a background tab in firefox.
|
||||
this.commentSectionRef.current?.scrollIntoView();
|
||||
}
|
||||
|
||||
isBottom(el: Element): boolean {
|
||||
|
@ -413,13 +576,18 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
onHidePost={this.handleHidePost}
|
||||
onScrollIntoCommentsClick={this.handleScrollIntoCommentsClick}
|
||||
/>
|
||||
<div ref={this.state.commentSectionRef} className="mb-2" />
|
||||
<div ref={this.commentSectionRef} className="mb-2" />
|
||||
|
||||
{/* Only show the top level comment form if its not a context view */}
|
||||
{!(
|
||||
this.state.commentId || res.post_view.banned_from_community
|
||||
getCommentIdFromProps(this.props) ||
|
||||
res.post_view.banned_from_community
|
||||
) && (
|
||||
<CommentForm
|
||||
key={
|
||||
this.context.router.history.location.key
|
||||
// reset on new location, otherwise <Prompt /> stops working
|
||||
}
|
||||
node={res.post_view.post.id}
|
||||
disabled={res.post_view.post.locked}
|
||||
allLanguages={siteRes.all_languages}
|
||||
|
@ -447,10 +615,8 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
{this.state.showSidebarMobile && this.sidebar()}
|
||||
</div>
|
||||
{this.sortRadios()}
|
||||
{this.state.commentViewType === CommentViewType.Tree &&
|
||||
this.commentsTree()}
|
||||
{this.state.commentViewType === CommentViewType.Flat &&
|
||||
this.commentsFlat()}
|
||||
{this.props.view === CommentViewType.Tree && this.commentsTree()}
|
||||
{this.props.view === CommentViewType.Flat && this.commentsFlat()}
|
||||
</main>
|
||||
<aside className="d-none d-md-block col-md-4 col-lg-3">
|
||||
{this.sidebar()}
|
||||
|
@ -482,13 +648,13 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
type="radio"
|
||||
className="btn-check"
|
||||
value={"Hot"}
|
||||
checked={this.state.commentSort === "Hot"}
|
||||
checked={this.props.sort === "Hot"}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${radioId}-hot`}
|
||||
className={classNames("btn btn-outline-secondary pointer", {
|
||||
active: this.state.commentSort === "Hot",
|
||||
active: this.props.sort === "Hot",
|
||||
})}
|
||||
>
|
||||
{I18NextService.i18n.t("hot")}
|
||||
|
@ -498,13 +664,13 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
type="radio"
|
||||
className="btn-check"
|
||||
value={"Top"}
|
||||
checked={this.state.commentSort === "Top"}
|
||||
checked={this.props.sort === "Top"}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${radioId}-top`}
|
||||
className={classNames("btn btn-outline-secondary pointer", {
|
||||
active: this.state.commentSort === "Top",
|
||||
active: this.props.sort === "Top",
|
||||
})}
|
||||
>
|
||||
{I18NextService.i18n.t("top")}
|
||||
|
@ -514,13 +680,13 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
type="radio"
|
||||
className="btn-check"
|
||||
value={"Controversial"}
|
||||
checked={this.state.commentSort === "Controversial"}
|
||||
checked={this.props.sort === "Controversial"}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${radioId}-controversial`}
|
||||
className={classNames("btn btn-outline-secondary pointer", {
|
||||
active: this.state.commentSort === "Controversial",
|
||||
active: this.props.sort === "Controversial",
|
||||
})}
|
||||
>
|
||||
{I18NextService.i18n.t("controversial")}
|
||||
|
@ -530,13 +696,13 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
type="radio"
|
||||
className="btn-check"
|
||||
value={"New"}
|
||||
checked={this.state.commentSort === "New"}
|
||||
checked={this.props.sort === "New"}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${radioId}-new`}
|
||||
className={classNames("btn btn-outline-secondary pointer", {
|
||||
active: this.state.commentSort === "New",
|
||||
active: this.props.sort === "New",
|
||||
})}
|
||||
>
|
||||
{I18NextService.i18n.t("new")}
|
||||
|
@ -546,13 +712,13 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
type="radio"
|
||||
className="btn-check"
|
||||
value={"Old"}
|
||||
checked={this.state.commentSort === "Old"}
|
||||
checked={this.props.sort === "Old"}
|
||||
onChange={linkEvent(this, this.handleCommentSortChange)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${radioId}-old`}
|
||||
className={classNames("btn btn-outline-secondary pointer", {
|
||||
active: this.state.commentSort === "Old",
|
||||
active: this.props.sort === "Old",
|
||||
})}
|
||||
>
|
||||
{I18NextService.i18n.t("old")}
|
||||
|
@ -564,13 +730,13 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
type="radio"
|
||||
className="btn-check"
|
||||
value={CommentViewType.Flat}
|
||||
checked={this.state.commentViewType === CommentViewType.Flat}
|
||||
checked={this.props.view === CommentViewType.Flat}
|
||||
onChange={linkEvent(this, this.handleCommentViewTypeChange)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${radioId}-chat`}
|
||||
className={classNames("btn btn-outline-secondary pointer", {
|
||||
active: this.state.commentViewType === CommentViewType.Flat,
|
||||
active: this.props.view === CommentViewType.Flat,
|
||||
})}
|
||||
>
|
||||
{I18NextService.i18n.t("chat")}
|
||||
|
@ -581,6 +747,14 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
}
|
||||
|
||||
commentsFlat() {
|
||||
if (this.state.commentsRes.state === "loading") {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<Spinner large />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// These are already sorted by new
|
||||
const commentsRes = this.state.commentsRes;
|
||||
const postRes = this.state.postRes;
|
||||
|
@ -590,8 +764,8 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
return (
|
||||
<div>
|
||||
<CommentNodes
|
||||
nodes={commentsToFlatNodes(commentsRes.data.comments)}
|
||||
viewType={this.state.commentViewType}
|
||||
nodes={this.sortedFlatNodes()}
|
||||
viewType={this.props.view}
|
||||
maxCommentsShown={this.state.maxCommentsShown}
|
||||
isTopLevel
|
||||
locked={postRes.data.post_view.post.locked}
|
||||
|
@ -652,7 +826,29 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
}
|
||||
}
|
||||
|
||||
sortedFlatNodes(): CommentNodeI[] {
|
||||
if (this.state.commentsRes.state !== "success") {
|
||||
return [];
|
||||
}
|
||||
const nodeToDate = (node: CommentNodeI) =>
|
||||
node.comment_view.comment.published;
|
||||
const nodes = commentsToFlatNodes(this.state.commentsRes.data.comments);
|
||||
if (this.props.sort === "New") {
|
||||
return nodes.sort((a, b) => compareDesc(nodeToDate(a), nodeToDate(b)));
|
||||
} else {
|
||||
return nodes.sort((a, b) => compareAsc(nodeToDate(a), nodeToDate(b)));
|
||||
}
|
||||
}
|
||||
|
||||
commentsTree() {
|
||||
if (this.state.commentsRes.state === "loading") {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<Spinner large />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const res = this.state.postRes;
|
||||
const firstComment = this.commentTree().at(0)?.comment_view.comment;
|
||||
const depth = getDepthFromComment(firstComment);
|
||||
|
@ -662,11 +858,11 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
return (
|
||||
res.state === "success" && (
|
||||
<div>
|
||||
{!!this.state.commentId && (
|
||||
{!!getCommentIdFromProps(this.props) && (
|
||||
<>
|
||||
<button
|
||||
className="ps-0 d-block btn btn-link text-muted"
|
||||
onClick={linkEvent(this, this.handleViewPost)}
|
||||
onClick={linkEvent(this, this.handleViewAllComments)}
|
||||
>
|
||||
{I18NextService.i18n.t("view_all_comments")} ➔
|
||||
</button>
|
||||
|
@ -682,7 +878,7 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
)}
|
||||
<CommentNodes
|
||||
nodes={this.commentTree()}
|
||||
viewType={this.state.commentViewType}
|
||||
viewType={this.props.view}
|
||||
maxCommentsShown={this.state.maxCommentsShown}
|
||||
locked={res.data.post_view.post.locked}
|
||||
moderators={res.data.moderators}
|
||||
|
@ -719,50 +915,69 @@ export class Post extends Component<PostRouteProps, PostState> {
|
|||
|
||||
commentTree(): CommentNodeI[] {
|
||||
if (this.state.commentsRes.state === "success") {
|
||||
return buildCommentsTree(
|
||||
this.state.commentsRes.data.comments,
|
||||
!!this.state.commentId,
|
||||
);
|
||||
} else {
|
||||
return [];
|
||||
const comments = this.state.commentsRes.data.comments;
|
||||
if (comments.length) {
|
||||
return buildCommentsTree(comments, !!getCommentIdFromProps(this.props));
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async handleCommentSortChange(i: Post, event: any) {
|
||||
i.setState({
|
||||
commentSort: event.target.value as CommentSortType,
|
||||
commentViewType: CommentViewType.Tree,
|
||||
commentsRes: LOADING_REQUEST,
|
||||
postRes: LOADING_REQUEST,
|
||||
});
|
||||
await i.fetchPost();
|
||||
const sort = event.target.value as CommentSortType;
|
||||
const flattenable = sort === "New" || sort === "Old";
|
||||
if (flattenable || i.props.view !== CommentViewType.Flat) {
|
||||
i.updateUrl({ sort });
|
||||
} else {
|
||||
i.updateUrl({ sort, view: CommentViewType.Tree });
|
||||
}
|
||||
}
|
||||
|
||||
handleCommentViewTypeChange(i: Post, event: any) {
|
||||
i.setState({
|
||||
commentViewType: Number(event.target.value),
|
||||
commentSort: "New",
|
||||
});
|
||||
const flattenable = i.props.sort === "New" || i.props.sort === "Old";
|
||||
const view: CommentViewType = Number(event.target.value);
|
||||
if (flattenable || view !== CommentViewType.Flat) {
|
||||
i.updateUrl({ view });
|
||||
} else {
|
||||
i.updateUrl({ view, sort: "New" });
|
||||
}
|
||||
}
|
||||
|
||||
handleShowSidebarMobile(i: Post) {
|
||||
i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
|
||||
}
|
||||
|
||||
handleViewPost(i: Post) {
|
||||
if (i.state.postRes.state === "success") {
|
||||
const id = i.state.postRes.data.post_view.post.id;
|
||||
i.context.router.history.push(`/post/${id}`);
|
||||
handleViewAllComments(i: Post) {
|
||||
const id =
|
||||
getIdFromProps(i.props) ||
|
||||
(i.state.postRes.state === "success" &&
|
||||
i.state.postRes.data.post_view.post.id);
|
||||
if (id) {
|
||||
i.updateUrl({
|
||||
match: { params: { post_id: id.toString() } },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleViewContext(i: Post) {
|
||||
if (i.state.commentsRes.state === "success") {
|
||||
const parentId = getCommentParentId(
|
||||
i.state.commentsRes.data.comments.at(0)?.comment,
|
||||
const commentId = getCommentIdFromProps(i.props);
|
||||
const commentView = i.state.commentsRes.data.comments.find(
|
||||
c => c.comment.id === commentId,
|
||||
);
|
||||
if (parentId) {
|
||||
i.context.router.history.push(`/comment/${parentId}`);
|
||||
|
||||
const parentId = getCommentParentId(commentView?.comment);
|
||||
const postId = commentView?.post.id;
|
||||
|
||||
if (parentId && postId) {
|
||||
i.updateUrl({
|
||||
match: {
|
||||
params: {
|
||||
post_id: postId.toString(),
|
||||
comment_id: parentId.toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import { RouteComponentProps } from "inferno-router/dist/Route";
|
|||
import { IRoutePropsWithFetch } from "../../routes";
|
||||
import { resourcesSettled } from "@utils/helpers";
|
||||
import { scrollMixin } from "../mixins/scroll-mixin";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
type CreatePrivateMessageData = RouteDataResponse<{
|
||||
recipientDetailsResponse: GetPersonDetailsResponse;
|
||||
|
@ -79,8 +80,8 @@ export class CreatePrivateMessage extends Component<
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!this.state.isIsomorphic) {
|
||||
async componentWillMount() {
|
||||
if (!this.state.isIsomorphic && isBrowser()) {
|
||||
await this.fetchPersonDetails();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import { CommunityLink } from "./community/community-link";
|
|||
import { getHttpBaseInternal } from "../utils/env";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { IRoutePropsWithFetch } from "../routes";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
interface RemoteFetchProps {
|
||||
uri?: string;
|
||||
|
@ -128,8 +129,8 @@ export class RemoteFetch extends Component<
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!this.state.isIsomorphic) {
|
||||
async componentWillMount() {
|
||||
if (!this.state.isIsomorphic && isBrowser()) {
|
||||
const { uri } = this.props;
|
||||
|
||||
if (uri) {
|
||||
|
|
|
@ -5,7 +5,6 @@ import {
|
|||
enableNsfw,
|
||||
fetchCommunities,
|
||||
fetchUsers,
|
||||
getUpdatedSearchId,
|
||||
myAuth,
|
||||
personToChoice,
|
||||
setIsoData,
|
||||
|
@ -71,6 +70,7 @@ import { PostListing } from "./post/post-listing";
|
|||
import { getHttpBaseInternal } from "../utils/env";
|
||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
import { IRoutePropsWithFetch } from "../routes";
|
||||
import { isBrowser } from "@utils/browser";
|
||||
|
||||
interface SearchProps {
|
||||
q?: string;
|
||||
|
@ -96,7 +96,6 @@ interface SearchState {
|
|||
searchRes: RequestState<SearchResponse>;
|
||||
resolveObjectRes: RequestState<ResolveObjectResponse>;
|
||||
siteRes: GetSiteResponse;
|
||||
searchText?: string;
|
||||
communitySearchOptions: Choice[];
|
||||
creatorSearchOptions: Choice[];
|
||||
searchCreatorLoading: boolean;
|
||||
|
@ -285,10 +284,6 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
this.handleCommunityFilterChange.bind(this);
|
||||
this.handleCreatorFilterChange = this.handleCreatorFilterChange.bind(this);
|
||||
|
||||
const { q } = this.props;
|
||||
|
||||
this.state.searchText = q;
|
||||
|
||||
// Only fetch the data if coming from another route
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const {
|
||||
|
@ -329,81 +324,142 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
componentWillMount() {
|
||||
if (!this.state.isIsomorphic && isBrowser()) {
|
||||
this.fetchAll(this.props);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.history.action !== "POP" || this.state.isIsomorphic) {
|
||||
this.searchInput.current?.select();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: SearchRouteProps) {
|
||||
if (nextProps.communityId !== this.props.communityId) {
|
||||
this.fetchSelectedCommunity(nextProps);
|
||||
}
|
||||
if (nextProps.creatorId !== this.props.creatorId) {
|
||||
this.fetchSelectedCreator(nextProps);
|
||||
}
|
||||
this.search(nextProps);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: SearchRouteProps) {
|
||||
if (this.props.location.key !== prevProps.location.key) {
|
||||
if (this.props.history.action !== "POP") {
|
||||
this.searchInput.current?.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.state.isIsomorphic) {
|
||||
fetchDefaultCommunitiesToken?: symbol;
|
||||
async fetchDefaultCommunities({
|
||||
communityId,
|
||||
}: Pick<SearchRouteProps, "communityId">) {
|
||||
const token = (this.fetchDefaultCommunitiesToken = Symbol());
|
||||
this.setState({
|
||||
searchCommunitiesLoading: true,
|
||||
searchCreatorLoading: true,
|
||||
});
|
||||
|
||||
const promises = [
|
||||
HttpService.client
|
||||
.listCommunities({
|
||||
const res = await HttpService.client.listCommunities({
|
||||
type_: defaultListingType,
|
||||
sort: defaultSortType,
|
||||
limit: fetchLimit,
|
||||
})
|
||||
.then(res => {
|
||||
});
|
||||
|
||||
if (token !== this.fetchDefaultCommunitiesToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.state === "success") {
|
||||
const retainSelected: false | undefined | Choice =
|
||||
!res.data.communities.some(cv => cv.community.id === communityId) &&
|
||||
this.state.communitySearchOptions.find(
|
||||
choice => choice.value === communityId?.toString(),
|
||||
);
|
||||
const choices = res.data.communities.map(communityToChoice);
|
||||
this.setState({
|
||||
communitySearchOptions:
|
||||
res.data.communities.map(communityToChoice),
|
||||
communitySearchOptions: retainSelected
|
||||
? [retainSelected, ...choices]
|
||||
: choices,
|
||||
});
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
const { communityId, creatorId } = this.props;
|
||||
this.setState({
|
||||
searchCommunitiesLoading: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (communityId) {
|
||||
promises.push(
|
||||
HttpService.client.getCommunity({ id: communityId }).then(res => {
|
||||
if (res.state === "success") {
|
||||
fetchSelectedCommunityToken?: symbol;
|
||||
async fetchSelectedCommunity({
|
||||
communityId,
|
||||
}: Pick<SearchRouteProps, "communityId">) {
|
||||
const token = (this.fetchSelectedCommunityToken = Symbol());
|
||||
const needsSelectedCommunity = () => {
|
||||
return !this.state.communitySearchOptions.some(
|
||||
choice => choice.value === communityId?.toString(),
|
||||
);
|
||||
};
|
||||
if (communityId && needsSelectedCommunity()) {
|
||||
const res = await HttpService.client.getCommunity({ id: communityId });
|
||||
if (
|
||||
res.state === "success" &&
|
||||
needsSelectedCommunity() &&
|
||||
token === this.fetchSelectedCommunityToken
|
||||
) {
|
||||
this.setState(prev => {
|
||||
prev.communitySearchOptions.unshift(
|
||||
communityToChoice(res.data.community_view),
|
||||
);
|
||||
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (creatorId) {
|
||||
promises.push(
|
||||
HttpService.client
|
||||
.getPersonDetails({
|
||||
fetchSelectedCreatorToken?: symbol;
|
||||
async fetchSelectedCreator({
|
||||
creatorId,
|
||||
}: Pick<SearchRouteProps, "creatorId">) {
|
||||
const token = (this.fetchSelectedCreatorToken = Symbol());
|
||||
const needsSelectedCreator = () => {
|
||||
return !this.state.creatorSearchOptions.some(
|
||||
choice => choice.value === creatorId?.toString(),
|
||||
);
|
||||
};
|
||||
|
||||
if (!creatorId || !needsSelectedCreator()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ searchCreatorLoading: true });
|
||||
|
||||
const res = await HttpService.client.getPersonDetails({
|
||||
person_id: creatorId,
|
||||
})
|
||||
.then(res => {
|
||||
if (res.state === "success") {
|
||||
});
|
||||
|
||||
if (token !== this.fetchSelectedCreatorToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.state === "success" && needsSelectedCreator()) {
|
||||
this.setState(prev => {
|
||||
prev.creatorSearchOptions.push(
|
||||
personToChoice(res.data.person_view),
|
||||
);
|
||||
prev.creatorSearchOptions.push(personToChoice(res.data.person_view));
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.setState({ searchCreatorLoading: false });
|
||||
}
|
||||
|
||||
if (this.state.searchText) {
|
||||
promises.push(this.search());
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
this.setState({
|
||||
searchCommunitiesLoading: false,
|
||||
searchCreatorLoading: false,
|
||||
});
|
||||
}
|
||||
async fetchAll(props: SearchRouteProps) {
|
||||
await Promise.all([
|
||||
this.fetchDefaultCommunities(props),
|
||||
this.fetchSelectedCommunity(props),
|
||||
this.fetchSelectedCreator(props),
|
||||
this.search(props),
|
||||
]);
|
||||
}
|
||||
|
||||
static async fetchInitialData({
|
||||
|
@ -551,13 +607,15 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
onSubmit={linkEvent(this, this.handleSearchSubmit)}
|
||||
>
|
||||
<div className="col-auto flex-grow-1 flex-sm-grow-0">
|
||||
{/* key is necessary for defaultValue to update when props.q changes,
|
||||
e.g. back button. */}
|
||||
<input
|
||||
key={this.context.router.history.location.key}
|
||||
type="text"
|
||||
className="form-control me-2 mb-2 col-sm-8"
|
||||
value={this.state.searchText}
|
||||
defaultValue={this.props.q ?? ""}
|
||||
placeholder={`${I18NextService.i18n.t("search")}...`}
|
||||
aria-label={I18NextService.i18n.t("search")}
|
||||
onInput={linkEvent(this, this.handleQChange)}
|
||||
required
|
||||
minLength={1}
|
||||
ref={this.searchInput}
|
||||
|
@ -984,15 +1042,14 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
return resObjCount + searchCount;
|
||||
}
|
||||
|
||||
async search() {
|
||||
const { searchText: q } = this.state;
|
||||
const { communityId, creatorId, type, sort, listingType, page } =
|
||||
this.props;
|
||||
searchToken?: symbol;
|
||||
async search(props: SearchRouteProps) {
|
||||
const token = (this.searchToken = Symbol());
|
||||
const { q, communityId, creatorId, type, sort, listingType, page } = props;
|
||||
|
||||
if (q) {
|
||||
this.setState({ searchRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
searchRes: await HttpService.client.search({
|
||||
const searchRes = await HttpService.client.search({
|
||||
q,
|
||||
community_id: communityId ?? undefined,
|
||||
creator_id: creatorId ?? undefined,
|
||||
|
@ -1001,18 +1058,24 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
listing_type: listingType,
|
||||
page,
|
||||
limit: fetchLimit,
|
||||
}),
|
||||
});
|
||||
if (token !== this.searchToken) {
|
||||
return;
|
||||
}
|
||||
this.setState({ searchRes });
|
||||
|
||||
if (myAuth()) {
|
||||
this.setState({ resolveObjectRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
resolveObjectRes: await HttpService.client.resolveObject({
|
||||
const resolveObjectRes = await HttpService.client.resolveObject({
|
||||
q,
|
||||
}),
|
||||
});
|
||||
if (token === this.searchToken) {
|
||||
this.setState({ resolveObjectRes });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.setState({ searchRes: EMPTY_REQUEST });
|
||||
}
|
||||
}
|
||||
|
||||
handleCreatorSearch = debounce(async (text: string) => {
|
||||
|
@ -1053,8 +1116,12 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
}
|
||||
});
|
||||
|
||||
getQ(): string | undefined {
|
||||
return this.searchInput.current?.value ?? this.props.q;
|
||||
}
|
||||
|
||||
handleSortChange(sort: SortType) {
|
||||
this.updateUrl({ sort, page: 1 });
|
||||
this.updateUrl({ sort, page: 1, q: this.getQ() });
|
||||
}
|
||||
|
||||
handleTypeChange(i: Search, event: any) {
|
||||
|
@ -1063,6 +1130,7 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
i.updateUrl({
|
||||
type,
|
||||
page: 1,
|
||||
q: i.getQ(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1074,20 +1142,23 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
this.updateUrl({
|
||||
listingType,
|
||||
page: 1,
|
||||
q: this.getQ(),
|
||||
});
|
||||
}
|
||||
|
||||
handleCommunityFilterChange({ value }: Choice) {
|
||||
this.updateUrl({
|
||||
communityId: getIdFromString(value) ?? 0,
|
||||
communityId: getIdFromString(value),
|
||||
page: 1,
|
||||
q: this.getQ(),
|
||||
});
|
||||
}
|
||||
|
||||
handleCreatorFilterChange({ value }: Choice) {
|
||||
this.updateUrl({
|
||||
creatorId: getIdFromString(value) ?? 0,
|
||||
creatorId: getIdFromString(value),
|
||||
page: 1,
|
||||
q: this.getQ(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1095,44 +1166,25 @@ export class Search extends Component<SearchRouteProps, SearchState> {
|
|||
event.preventDefault();
|
||||
|
||||
i.updateUrl({
|
||||
q: i.state.searchText,
|
||||
q: i.getQ(),
|
||||
page: 1,
|
||||
});
|
||||
}
|
||||
|
||||
handleQChange(i: Search, event: any) {
|
||||
i.setState({ searchText: event.target.value });
|
||||
}
|
||||
|
||||
async updateUrl({
|
||||
q,
|
||||
type,
|
||||
listingType,
|
||||
sort,
|
||||
communityId,
|
||||
creatorId,
|
||||
page,
|
||||
}: Partial<SearchProps>) {
|
||||
const {
|
||||
q: urlQ,
|
||||
type: urlType,
|
||||
listingType: urlListingType,
|
||||
communityId: urlCommunityId,
|
||||
sort: urlSort,
|
||||
creatorId: urlCreatorId,
|
||||
page: urlPage,
|
||||
} = this.props;
|
||||
|
||||
const query = q ?? this.state.searchText ?? urlQ;
|
||||
async updateUrl(props: Partial<SearchProps>) {
|
||||
const { q, type, listingType, sort, communityId, creatorId, page } = {
|
||||
...this.props,
|
||||
...props,
|
||||
};
|
||||
|
||||
const queryParams: QueryParams<SearchProps> = {
|
||||
q: query,
|
||||
type: type ?? urlType,
|
||||
listingType: listingType ?? urlListingType,
|
||||
communityId: getUpdatedSearchId(communityId, urlCommunityId),
|
||||
creatorId: getUpdatedSearchId(creatorId, urlCreatorId),
|
||||
page: (page ?? urlPage).toString(),
|
||||
sort: sort ?? urlSort,
|
||||
q,
|
||||
type: type,
|
||||
listingType: listingType,
|
||||
communityId: communityId?.toString(),
|
||||
creatorId: creatorId?.toString(),
|
||||
page: page?.toString(),
|
||||
sort: sort,
|
||||
};
|
||||
|
||||
this.props.history.push(`/search${getQueryString(queryParams)}`);
|
||||
|
|
|
@ -53,7 +53,11 @@ import {
|
|||
CreatePost,
|
||||
getCreatePostQueryParams,
|
||||
} from "./components/post/create-post";
|
||||
import { Post, PostFetchConfig } from "./components/post/post";
|
||||
import {
|
||||
Post,
|
||||
PostFetchConfig,
|
||||
getPostQueryParams,
|
||||
} from "./components/post/post";
|
||||
import {
|
||||
CreatePrivateMessage,
|
||||
CreatePrivateMessageFetchConfig,
|
||||
|
@ -87,6 +91,7 @@ export interface IRoutePropsWithFetch<
|
|||
component: Inferno.ComponentClass<
|
||||
RouteComponentProps<PathPropsT> & QueryPropsT
|
||||
>;
|
||||
mountedSameRouteNavKey?: string;
|
||||
}
|
||||
|
||||
export const routes: IRoutePropsWithFetch<RouteData, any, any>[] = [
|
||||
|
@ -96,6 +101,7 @@ export const routes: IRoutePropsWithFetch<RouteData, any, any>[] = [
|
|||
fetchInitialData: Home.fetchInitialData,
|
||||
exact: true,
|
||||
getQueryParams: getHomeQueryParams,
|
||||
mountedSameRouteNavKey: "home",
|
||||
} as HomeFetchConfig,
|
||||
{
|
||||
path: `/login`,
|
||||
|
@ -130,28 +136,38 @@ export const routes: IRoutePropsWithFetch<RouteData, any, any>[] = [
|
|||
component: Communities,
|
||||
fetchInitialData: Communities.fetchInitialData,
|
||||
getQueryParams: getCommunitiesQueryParams,
|
||||
mountedSameRouteNavKey: "communities",
|
||||
} as CommunitiesFetchConfig,
|
||||
{
|
||||
path: `/post/:post_id`,
|
||||
// "/comment/:post_id?/:comment_id" would be preferable as direct comment
|
||||
// link, but it looks like a Route can't match multiple paths and a
|
||||
// component can't stay mounted across routes.
|
||||
path: `/post/:post_id/:comment_id?`,
|
||||
component: Post,
|
||||
fetchInitialData: Post.fetchInitialData,
|
||||
getQueryParams: getPostQueryParams,
|
||||
mountedSameRouteNavKey: "post",
|
||||
} as PostFetchConfig,
|
||||
{
|
||||
path: `/comment/:comment_id`,
|
||||
component: Post,
|
||||
fetchInitialData: Post.fetchInitialData,
|
||||
getQueryParams: getPostQueryParams,
|
||||
mountedSameRouteNavKey: "post",
|
||||
} as PostFetchConfig,
|
||||
{
|
||||
path: `/c/:name`,
|
||||
component: Community,
|
||||
fetchInitialData: Community.fetchInitialData,
|
||||
getQueryParams: getCommunityQueryParams,
|
||||
mountedSameRouteNavKey: "community",
|
||||
} as CommunityFetchConfig,
|
||||
{
|
||||
path: `/u/:username`,
|
||||
component: Profile,
|
||||
fetchInitialData: Profile.fetchInitialData,
|
||||
getQueryParams: getProfileQueryParams,
|
||||
mountedSameRouteNavKey: "profile",
|
||||
} as ProfileFetchConfig,
|
||||
{
|
||||
path: `/inbox`,
|
||||
|
@ -164,16 +180,11 @@ export const routes: IRoutePropsWithFetch<RouteData, any, any>[] = [
|
|||
fetchInitialData: Settings.fetchInitialData,
|
||||
} as SettingsFetchConfig,
|
||||
{
|
||||
path: `/modlog/:communityId`,
|
||||
component: Modlog,
|
||||
fetchInitialData: Modlog.fetchInitialData,
|
||||
getQueryParams: getModlogQueryParams,
|
||||
} as ModlogFetchConfig,
|
||||
{
|
||||
path: `/modlog`,
|
||||
path: `/modlog/:communityId?`,
|
||||
component: Modlog,
|
||||
fetchInitialData: Modlog.fetchInitialData,
|
||||
getQueryParams: getModlogQueryParams,
|
||||
mountedSameRouteNavKey: "modlog",
|
||||
} as ModlogFetchConfig,
|
||||
{ path: `/setup`, component: Setup },
|
||||
{
|
||||
|
@ -196,6 +207,7 @@ export const routes: IRoutePropsWithFetch<RouteData, any, any>[] = [
|
|||
component: Search,
|
||||
fetchInitialData: Search.fetchInitialData,
|
||||
getQueryParams: getSearchQueryParams,
|
||||
mountedSameRouteNavKey: "search",
|
||||
} as SearchFetchConfig,
|
||||
{
|
||||
path: `/password_change/:token`,
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
export default function getUpdatedSearchId(
|
||||
id?: number | null,
|
||||
urlId?: number | null,
|
||||
) {
|
||||
return id === null
|
||||
? undefined
|
||||
: ((id ?? urlId) === 0 ? undefined : id ?? urlId)?.toString();
|
||||
}
|
|
@ -31,7 +31,6 @@ import getDataTypeString from "./get-data-type-string";
|
|||
import getDepthFromComment from "./get-depth-from-comment";
|
||||
import getIdFromProps from "./get-id-from-props";
|
||||
import getRecipientIdFromProps from "./get-recipient-id-from-props";
|
||||
import getUpdatedSearchId from "./get-updated-search-id";
|
||||
import initializeSite from "./initialize-site";
|
||||
import insertCommentIntoTree from "./insert-comment-into-tree";
|
||||
import isAuthPath from "./is-auth-path";
|
||||
|
@ -89,7 +88,6 @@ export {
|
|||
getDepthFromComment,
|
||||
getIdFromProps,
|
||||
getRecipientIdFromProps,
|
||||
getUpdatedSearchId,
|
||||
initializeSite,
|
||||
insertCommentIntoTree,
|
||||
isAuthPath,
|
||||
|
|
14
src/shared/utils/helpers/bare-route-push.ts
Normal file
14
src/shared/utils/helpers/bare-route-push.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||
|
||||
// Intended to allow reloading all the data of the current page by clicking the
|
||||
// navigation link of the current page.
|
||||
export default function bareRoutePush<P extends RouteComponentProps<any>>(
|
||||
prevProps: P,
|
||||
nextProps: P,
|
||||
) {
|
||||
return (
|
||||
prevProps.location.pathname === nextProps.location.pathname &&
|
||||
!nextProps.location.search &&
|
||||
nextProps.history.action === "PUSH"
|
||||
);
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import bareRoutePush from "./bare-route-push";
|
||||
import capitalizeFirstLetter from "./capitalize-first-letter";
|
||||
import debounce from "./debounce";
|
||||
import editListImmutable from "./edit-list-immutable";
|
||||
|
@ -27,6 +28,7 @@ import dedupByProperty from "./dedup-by-property";
|
|||
import getApubName from "./apub-name";
|
||||
|
||||
export {
|
||||
bareRoutePush,
|
||||
cakeDate,
|
||||
capitalizeFirstLetter,
|
||||
debounce,
|
||||
|
|
Loading…
Reference in a new issue