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:
matc-pub 2024-05-22 21:46:13 +02:00 committed by GitHub
parent 937fd3eb4e
commit b7fe70d8c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1529 additions and 934 deletions

View file

@ -32,6 +32,64 @@ export default class App extends Component<any, any> {
destroyTippy(); 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() { render() {
const siteRes = this.isoData.site_res; const siteRes = this.isoData.site_res;
const siteView = siteRes?.site_view; const siteView = siteRes?.site_view;
@ -64,58 +122,7 @@ export default class App extends Component<any, any> {
<Navbar siteRes={siteRes} /> <Navbar siteRes={siteRes} />
<div className="mt-4 p-0 fl-1"> <div className="mt-4 p-0 fl-1">
<Switch> <Switch>
{routes.map( {this.routes}
({
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>
);
}}
/>
),
)}
<Route component={ErrorPage} /> <Route component={ErrorPage} />
</Switch> </Switch>
</div> </div>

View file

@ -63,7 +63,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this); this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this);
} }
async componentDidMount() { async componentWillMount() {
// Subscribe to jwt changes // Subscribe to jwt changes
if (isBrowser()) { if (isBrowser()) {
// On the first load, check the unreads // On the first load, check the unreads

View file

@ -502,7 +502,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<> <>
<Link <Link
className={classnames} className={classnames}
to={`/comment/${ to={`/post/${cv.post.id}/${
(this.props.showContext && getCommentParentId(cv.comment)) || (this.props.showContext && getCommentParentId(cv.comment)) ||
cv.comment.id cv.comment.id
}`} }`}

View file

@ -1,30 +1,25 @@
import { Component } from "inferno"; import { Component } from "inferno";
import { UserService } from "../../services"; import { UserService } from "../../services";
import { Spinner } from "./icon"; import { Spinner } from "./icon";
import { isBrowser } from "@utils/browser";
interface AnonymousGuardState { class AnonymousGuard extends Component<any, any> {
hasRedirected: boolean;
}
class AnonymousGuard extends Component<any, AnonymousGuardState> {
state = {
hasRedirected: false,
} as AnonymousGuardState;
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
} }
componentDidMount() { hasAuth() {
if (UserService.Instance.myUserInfo) { return UserService.Instance.myUserInfo;
}
componentWillMount() {
if (this.hasAuth() && isBrowser()) {
this.context.router.history.replace(`/`); this.context.router.history.replace(`/`);
} else {
this.setState({ hasRedirected: true });
} }
} }
render() { render() {
return this.state.hasRedirected ? this.props.children : <Spinner />; return !this.hasAuth() ? this.props.children : <Spinner />;
} }
} }

View file

@ -3,19 +3,12 @@ import { RouteComponentProps } from "inferno-router/dist/Route";
import { UserService } from "../../services"; import { UserService } from "../../services";
import { Spinner } from "./icon"; import { Spinner } from "./icon";
import { getQueryString } from "@utils/helpers"; import { getQueryString } from "@utils/helpers";
import { isBrowser } from "@utils/browser";
interface AuthGuardState {
hasRedirected: boolean;
}
class AuthGuard extends Component< class AuthGuard extends Component<
RouteComponentProps<Record<string, string>>, RouteComponentProps<Record<string, string>>,
AuthGuardState any
> { > {
state = {
hasRedirected: false,
} as AuthGuardState;
constructor( constructor(
props: RouteComponentProps<Record<string, string>>, props: RouteComponentProps<Record<string, string>>,
context: any, context: any,
@ -23,19 +16,21 @@ class AuthGuard extends Component<
super(props, context); super(props, context);
} }
componentDidMount() { hasAuth() {
if (!UserService.Instance.myUserInfo) { return UserService.Instance.myUserInfo;
}
componentWillMount() {
if (!this.hasAuth() && isBrowser()) {
const { pathname, search } = this.props.location; const { pathname, search } = this.props.location;
this.context.router.history.replace( this.context.router.history.replace(
`/login${getQueryString({ prev: pathname + search })}`, `/login${getQueryString({ prev: pathname + search })}`,
); );
} else {
this.setState({ hasRedirected: true });
} }
} }
render() { render() {
return this.state.hasRedirected ? this.props.children : <Spinner />; return this.hasAuth() ? this.props.children : <Spinner />;
} }
} }

View file

@ -643,7 +643,6 @@ export default class ContentActionDropdown extends Component<
type, type,
} = this.props; } = this.props;
// Wait until componentDidMount runs (which only happens on the browser) to prevent sending over a gratuitous amount of markup
return ( return (
<> <>
{renderRemoveDialog && ( {renderRemoveDialog && (

View file

@ -39,7 +39,7 @@ function handleSearch(i: SearchableSelect, e: ChangeEvent<HTMLInputElement>) {
} }
function focusSearch(i: SearchableSelect) { function focusSearch(i: SearchableSelect) {
if (i.toggleButtonRef.current?.ariaExpanded !== "true") { if (i.toggleButtonRef.current?.ariaExpanded === "true") {
i.searchInputRef.current?.focus(); i.searchInputRef.current?.focus();
if (i.props.onSearch) { if (i.props.onSearch) {

View file

@ -1,12 +1,13 @@
import { getQueryString, validInstanceTLD } from "@utils/helpers"; import { getQueryString, validInstanceTLD } from "@utils/helpers";
import classNames from "classnames"; import classNames from "classnames";
import { NoOptionI18nKeys } from "i18next"; import { NoOptionI18nKeys } from "i18next";
import { Component, MouseEventHandler, linkEvent } from "inferno"; import { Component, MouseEventHandler, createRef, linkEvent } from "inferno";
import { CommunityView } from "lemmy-js-client"; import { CommunityView } from "lemmy-js-client";
import { I18NextService, UserService } from "../../services"; import { I18NextService, UserService } from "../../services";
import { VERSION } from "../../version"; import { VERSION } from "../../version";
import { Icon, Spinner } from "./icon"; import { Icon, Spinner } from "./icon";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { modalMixin } from "../mixins/modal-mixin";
interface SubscribeButtonProps { interface SubscribeButtonProps {
communityView: CommunityView; communityView: CommunityView;
@ -93,6 +94,7 @@ export function SubscribeButton({
interface RemoteFetchModalProps { interface RemoteFetchModalProps {
communityActorId: string; communityActorId: string;
show?: boolean;
} }
interface RemoteFetchModalState { interface RemoteFetchModalState {
@ -103,10 +105,6 @@ function handleInput(i: RemoteFetchModal, event: any) {
i.setState({ instanceText: event.target.value }); i.setState({ instanceText: event.target.value });
} }
function focusInput() {
document.getElementById("remoteFetchInstance")?.focus();
}
function submitRemoteFollow( function submitRemoteFollow(
{ state: { instanceText }, props: { communityActorId } }: RemoteFetchModal, { state: { instanceText }, props: { communityActorId } }: RemoteFetchModal,
event: Event, event: Event,
@ -139,6 +137,7 @@ function submitRemoteFollow(
)}`; )}`;
} }
@modalMixin
class RemoteFetchModal extends Component< class RemoteFetchModal extends Component<
RemoteFetchModalProps, RemoteFetchModalProps,
RemoteFetchModalState RemoteFetchModalState
@ -147,20 +146,15 @@ class RemoteFetchModal extends Component<
instanceText: "", instanceText: "",
}; };
modalDivRef = createRef<HTMLDivElement>();
inputRef = createRef<HTMLInputElement>();
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
} }
componentDidMount() { handleShow() {
document this.inputRef.current?.focus();
.getElementById("remoteFetchModal")
?.addEventListener("shown.bs.modal", focusInput);
}
componentWillUnmount(): void {
document
.getElementById("remoteFetchModal")
?.removeEventListener("shown.bs.modal", focusInput);
} }
render() { render() {
@ -171,6 +165,7 @@ class RemoteFetchModal extends Component<
tabIndex={-1} tabIndex={-1}
aria-hidden aria-hidden
aria-labelledby="#remoteFetchModalTitle" aria-labelledby="#remoteFetchModalTitle"
ref={this.modalDivRef}
> >
<div className="modal-dialog modal-dialog-centered modal-fullscreen-sm-down"> <div className="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
<div className="modal-content"> <div className="modal-content">
@ -203,6 +198,7 @@ class RemoteFetchModal extends Component<
required required
enterKeyHint="go" enterKeyHint="go"
inputMode="url" inputMode="url"
ref={this.inputRef}
/> />
</form> </form>
<footer className="modal-footer"> <footer className="modal-footer">

View file

@ -24,6 +24,7 @@ import { fetchLimit } from "../../config";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
import { modalMixin } from "../mixins/modal-mixin"; import { modalMixin } from "../mixins/modal-mixin";
import { UserBadges } from "./user-badges"; import { UserBadges } from "./user-badges";
import { isBrowser } from "@utils/browser";
interface ViewVotesModalProps { interface ViewVotesModalProps {
children?: InfernoNode; children?: InfernoNode;
@ -96,8 +97,8 @@ export default class ViewVotesModal extends Component<
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
} }
async componentDidMount() { async componentWillMount() {
if (this.props.show) { if (this.props.show && isBrowser()) {
await this.refetch(); await this.refetch();
} }
} }

View file

@ -40,6 +40,7 @@ import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import { scrollMixin } from "../mixins/scroll-mixin"; import { scrollMixin } from "../mixins/scroll-mixin";
import { isBrowser } from "@utils/browser";
type CommunitiesData = RouteDataResponse<{ type CommunitiesData = RouteDataResponse<{
listCommunitiesResponse: ListCommunitiesResponse; listCommunitiesResponse: ListCommunitiesResponse;
@ -121,19 +122,23 @@ export class Communities extends Component<
} }
} }
async componentDidMount() { async componentWillMount() {
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic && isBrowser()) {
await this.refetch(); await this.refetch(this.props);
} }
} }
componentWillReceiveProps(nextProps: CommunitiesRouteProps) {
this.refetch(nextProps);
}
get documentTitle(): string { get documentTitle(): string {
return `${I18NextService.i18n.t("communities")} - ${ return `${I18NextService.i18n.t("communities")} - ${
this.state.siteRes.site_view.site.name this.state.siteRes.site_view.site.name
}`; }`;
} }
renderListings() { renderListingsTable() {
switch (this.state.listCommunitiesResponse.state) { switch (this.state.listCommunitiesResponse.state) {
case "loading": case "loading":
return ( return (
@ -142,120 +147,114 @@ export class Communities extends Component<
</h5> </h5>
); );
case "success": { case "success": {
const { listingType, sort, page } = this.props;
return ( return (
<div> <table id="community_table" className="table table-sm table-hover">
<h1 className="h4 mb-4"> <thead className="pointer">
{I18NextService.i18n.t("list_of_communities")} <tr>
</h1> <th>{I18NextService.i18n.t("name")}</th>
<div className="row g-3 align-items-center mb-2"> <th className="text-right">
<div className="col-auto"> {I18NextService.i18n.t("subscribers")}
<ListingTypeSelect </th>
type_={listingType} <th className="text-right">
showLocal={showLocal(this.isoData)} {I18NextService.i18n.t("users")} /{" "}
showSubscribed {I18NextService.i18n.t("month")}
onChange={this.handleListingTypeChange} </th>
/> <th className="text-right d-none d-lg-table-cell">
</div> {I18NextService.i18n.t("posts")}
<div className="col-auto me-auto"> </th>
<SortSelect sort={sort} onChange={this.handleSortChange} /> <th className="text-right d-none d-lg-table-cell">
</div> {I18NextService.i18n.t("comments")}
<div className="col-auto">{this.searchForm()}</div> </th>
</div> <th></th>
</tr>
<div className="table-responsive"> </thead>
<table <tbody>
id="community_table" {this.state.listCommunitiesResponse.data.communities.map(cv => (
className="table table-sm table-hover" <tr key={cv.community.id}>
> <td>
<thead className="pointer"> <CommunityLink community={cv.community} />
<tr> </td>
<th>{I18NextService.i18n.t("name")}</th> <td className="text-right">
<th className="text-right"> {numToSI(cv.counts.subscribers)}
{I18NextService.i18n.t("subscribers")} </td>
</th> <td className="text-right">
<th className="text-right"> {numToSI(cv.counts.users_active_month)}
{I18NextService.i18n.t("users")} /{" "} </td>
{I18NextService.i18n.t("month")} <td className="text-right d-none d-lg-table-cell">
</th> {numToSI(cv.counts.posts)}
<th className="text-right d-none d-lg-table-cell"> </td>
{I18NextService.i18n.t("posts")} <td className="text-right d-none d-lg-table-cell">
</th> {numToSI(cv.counts.comments)}
<th className="text-right d-none d-lg-table-cell"> </td>
{I18NextService.i18n.t("comments")} <td className="text-right">
</th> <SubscribeButton
<th></th> communityView={cv}
</tr> onFollow={linkEvent(
</thead> {
<tbody> i: this,
{this.state.listCommunitiesResponse.data.communities.map( communityId: cv.community.id,
cv => ( follow: true,
<tr key={cv.community.id}> },
<td> this.handleFollow,
<CommunityLink community={cv.community} /> )}
</td> onUnFollow={linkEvent(
<td className="text-right"> {
{numToSI(cv.counts.subscribers)} i: this,
</td> communityId: cv.community.id,
<td className="text-right"> follow: false,
{numToSI(cv.counts.users_active_month)} },
</td> this.handleFollow,
<td className="text-right d-none d-lg-table-cell"> )}
{numToSI(cv.counts.posts)} isLink
</td> />
<td className="text-right d-none d-lg-table-cell"> </td>
{numToSI(cv.counts.comments)} </tr>
</td> ))}
<td className="text-right"> </tbody>
<SubscribeButton </table>
communityView={cv}
onFollow={linkEvent(
{
i: this,
communityId: cv.community.id,
follow: true,
},
this.handleFollow,
)}
onUnFollow={linkEvent(
{
i: this,
communityId: cv.community.id,
follow: false,
},
this.handleFollow,
)}
isLink
/>
</td>
</tr>
),
)}
</tbody>
</table>
</div>
<Paginator
page={page}
onChange={this.handlePageChange}
nextDisabled={
communityLimit >
this.state.listCommunitiesResponse.data.communities.length
}
/>
</div>
); );
} }
} }
} }
render() { render() {
const { listingType, sort, page } = this.props;
return ( return (
<div className="communities container-lg"> <div className="communities container-lg">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} 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> </div>
); );
} }
@ -287,22 +286,16 @@ export class Communities extends Component<
); );
} }
async updateUrl({ listingType, sort, page }: Partial<CommunitiesProps>) { async updateUrl(props: Partial<CommunitiesProps>) {
const { const { listingType, sort, page } = { ...this.props, ...props };
listingType: urlListingType,
sort: urlSort,
page: urlPage,
} = this.props;
const queryParams: QueryParams<CommunitiesProps> = { const queryParams: QueryParams<CommunitiesProps> = {
listingType: listingType ?? urlListingType, listingType: listingType,
sort: sort ?? urlSort, sort: sort,
page: (page ?? urlPage)?.toString(), page: page?.toString(),
}; };
this.props.history.push(`/communities${getQueryString(queryParams)}`); this.props.history.push(`/communities${getQueryString(queryParams)}`);
await this.refetch();
} }
handlePageChange(page: number) { handlePageChange(page: number) {
@ -368,19 +361,19 @@ export class Communities extends Component<
data.i.findAndUpdateCommunity(res); data.i.findAndUpdateCommunity(res);
} }
async refetch() { fetchToken?: symbol;
async refetch({ listingType, sort, page }: CommunitiesProps) {
const token = (this.fetchToken = Symbol());
this.setState({ listCommunitiesResponse: LOADING_REQUEST }); this.setState({ listCommunitiesResponse: LOADING_REQUEST });
const listCommunitiesResponse = await HttpService.client.listCommunities({
const { listingType, sort, page } = this.props; type_: listingType,
sort: sort,
this.setState({ limit: communityLimit,
listCommunitiesResponse: await HttpService.client.listCommunities({ page,
type_: listingType,
sort: sort,
limit: communityLimit,
page,
}),
}); });
if (token === this.fetchToken) {
this.setState({ listCommunitiesResponse });
}
} }
findAndUpdateCommunity(res: RequestState<CommunityResponse>) { findAndUpdateCommunity(res: RequestState<CommunityResponse>) {

View file

@ -19,11 +19,18 @@ import {
getQueryParams, getQueryParams,
getQueryString, getQueryString,
resourcesSettled, resourcesSettled,
bareRoutePush,
} from "@utils/helpers"; } from "@utils/helpers";
import { scrollMixin } from "../mixins/scroll-mixin"; import { scrollMixin } from "../mixins/scroll-mixin";
import type { QueryParams, StringBoolean } from "@utils/types"; import type { QueryParams, StringBoolean } from "@utils/types";
import { RouteDataResponse } 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 { RouteComponentProps } from "inferno-router/dist/Route";
import { import {
AddAdmin, AddAdmin,
@ -100,7 +107,7 @@ import { CommentNodes } from "../comment/comment-nodes";
import { BannerIconHeader } from "../common/banner-icon-header"; import { BannerIconHeader } from "../common/banner-icon-header";
import { DataTypeSelect } from "../common/data-type-select"; import { DataTypeSelect } from "../common/data-type-select";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon"; import { Icon } from "../common/icon";
import { SortSelect } from "../common/sort-select"; import { SortSelect } from "../common/sort-select";
import { SiteSidebar } from "../home/site-sidebar"; import { SiteSidebar } from "../home/site-sidebar";
import { PostListings } from "../post/post-listings"; import { PostListings } from "../post/post-listings";
@ -114,6 +121,8 @@ import {
import { Sidebar } from "./sidebar"; import { Sidebar } from "./sidebar";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import PostHiddenSelect from "../common/post-hidden-select"; import PostHiddenSelect from "../common/post-hidden-select";
import { isBrowser } from "@utils/browser";
import { LoadingEllipses } from "../common/loading-ellipses";
type CommunityData = RouteDataResponse<{ type CommunityData = RouteDataResponse<{
communityRes: GetCommunityResponse; 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: LOADING_REQUEST });
this.setState({ const communityRes = await HttpService.client.getCommunity({
communityRes: await HttpService.client.getCommunity({ name: props.match.params.name,
name: this.props.match.params.name,
}),
}); });
if (token === this.fetchCommunityToken) {
this.setState({ communityRes });
}
} }
async componentDidMount() { async componentWillMount() {
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic && isBrowser()) {
await Promise.all([this.fetchCommunity(), this.fetchData()]); 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({ static async fetchInitialData({
headers, headers,
query: { dataType, pageCursor, sort, showHidden }, query: { dataType, pageCursor, sort, showHidden },
@ -356,73 +383,67 @@ export class Community extends Component<CommunityRouteProps, State> {
} }
renderCommunity() { renderCommunity() {
switch (this.state.communityRes.state) { const res =
case "loading": this.state.communityRes.state === "success" &&
return ( this.state.communityRes.data;
<h5> return (
<Spinner large /> <>
</h5> {res && (
); <HtmlTags
case "success": { title={this.documentTitle}
const res = this.state.communityRes.data; path={this.context.router.route.match.url}
canonicalPath={res.community_view.community.actor_id}
description={res.community_view.community.description}
image={res.community_view.community.icon}
/>
)}
return ( {this.communityInfo()}
<> <div className="d-block d-md-none">
<HtmlTags <button
title={this.documentTitle} className="btn btn-secondary d-inline-block mb-2 me-3"
path={this.context.router.route.match.url} onClick={linkEvent(this, this.handleShowSidebarMobile)}
canonicalPath={res.community_view.community.actor_id} >
description={res.community_view.community.description} {I18NextService.i18n.t("sidebar")}{" "}
image={res.community_view.community.icon} <Icon
icon={
this.state.showSidebarMobile ? `minus-square` : `plus-square`
}
classes="icon-inline"
/> />
</button>
<div className="row"> {this.state.showSidebarMobile && this.sidebar()}
<main </div>
className="col-12 col-md-8 col-lg-9" </>
ref={this.mainContentRef} );
>
{this.communityInfo(res)}
<div className="d-block d-md-none">
<button
className="btn btn-secondary d-inline-block mb-2 me-3"
onClick={linkEvent(this, this.handleShowSidebarMobile)}
>
{I18NextService.i18n.t("sidebar")}{" "}
<Icon
icon={
this.state.showSidebarMobile
? `minus-square`
: `plus-square`
}
classes="icon-inline"
/>
</button>
{this.state.showSidebarMobile && this.sidebar(res)}
</div>
{this.selects(res)}
{this.listings(res)}
<PaginatorCursor
nextPage={this.getNextPage}
onNext={this.handlePageNext}
/>
</main>
<aside className="d-none d-md-block col-md-4 col-lg-3">
{this.sidebar(res)}
</aside>
</div>
</>
);
}
}
} }
render() { render() {
return ( return (
<div className="community container-lg">{this.renderCommunity()}</div> <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()}
</aside>
</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; const siteRes = this.isoData.site_res;
// For some reason, this returns an empty vec if it matches the site langs // For some reason, this returns an empty vec if it matches the site langs
const communityLangs = const communityLangs =
@ -456,7 +477,7 @@ export class Community extends Component<CommunityRouteProps, State> {
); );
} }
listings(communityRes: GetCommunityResponse) { listings() {
const { dataType } = this.props; const { dataType } = this.props;
const siteRes = this.isoData.site_res; const siteRes = this.isoData.site_res;
@ -496,6 +517,9 @@ export class Community extends Component<CommunityRouteProps, State> {
); );
} }
} else { } else {
if (this.state.communityRes.state !== "success") {
return;
}
switch (this.state.commentsRes.state) { switch (this.state.commentsRes.state) {
case "loading": case "loading":
return <CommentsLoadingSkeleton />; return <CommentsLoadingSkeleton />;
@ -509,7 +533,7 @@ export class Community extends Component<CommunityRouteProps, State> {
showContext showContext
enableDownvotes={enableDownvotes(siteRes)} enableDownvotes={enableDownvotes(siteRes)}
voteDisplayMode={voteDisplayMode(siteRes)} voteDisplayMode={voteDisplayMode(siteRes)}
moderators={communityRes.moderators} moderators={this.state.communityRes.data.moderators}
admins={siteRes.admins} admins={siteRes.admins}
allLanguages={siteRes.all_languages} allLanguages={siteRes.all_languages}
siteLanguages={siteRes.discussion_languages} siteLanguages={siteRes.discussion_languages}
@ -537,28 +561,40 @@ export class Community extends Component<CommunityRouteProps, State> {
} }
} }
communityInfo(res: GetCommunityResponse) { communityInfo() {
const community = res.community_view.community; 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 ( return (
community && ( <div className="mb-2">
<div className="mb-2"> {community && (
<BannerIconHeader banner={community.banner} icon={community.icon} /> <BannerIconHeader banner={community.banner} icon={community.icon} />
<div> )}
<h1 <div>
className="h4 mb-0 overflow-wrap-anywhere d-inline" <h1
data-tippy-content={ className="h4 mb-0 overflow-wrap-anywhere d-inline"
community.posting_restricted_to_mods data-tippy-content={
? I18NextService.i18n.t("community_locked") community?.posting_restricted_to_mods
: "" ? I18NextService.i18n.t("community_locked")
} : ""
> }
{community.title} >
</h1> {community?.title ?? (
{community.posting_restricted_to_mods && ( <>
<Icon icon="lock" inline classes="text-danger fs-4 ms-2" /> {urlCommunityName}
<LoadingEllipses />
</>
)} )}
</div> </h1>
{community?.posting_restricted_to_mods && (
<Icon icon="lock" inline classes="text-danger fs-4 ms-2" />
)}
</div>
{(community && (
<CommunityLink <CommunityLink
community={community} community={community}
realLink realLink
@ -566,12 +602,16 @@ export class Community extends Component<CommunityRouteProps, State> {
muted muted
hideAvatar hideAvatar
/> />
</div> )) ??
) urlCommunityName}
</div>
); );
} }
selects(res: GetCommunityResponse) { selects() {
const res =
this.state.communityRes.state === "success" &&
this.state.communityRes.data;
const { dataType, sort, showHidden } = this.props; const { dataType, sort, showHidden } = this.props;
const communityRss = res const communityRss = res
? communityRSSUrl(res.community_view.community.actor_id, sort) ? communityRSSUrl(res.community_view.community.actor_id, sort)
@ -641,60 +681,62 @@ export class Community extends Component<CommunityRouteProps, State> {
})); }));
} }
async updateUrl({ async updateUrl(props: Partial<CommunityProps>) {
dataType,
pageCursor,
sort,
showHidden,
}: Partial<CommunityProps>) {
const { const {
dataType: urlDataType, dataType,
sort: urlSort, pageCursor,
showHidden: urlShowHidden, sort,
} = this.props; showHidden,
match: {
const queryParams: QueryParams<CommunityProps> = { params: { name },
dataType: getDataTypeString(dataType ?? urlDataType), },
pageCursor: pageCursor, } = {
sort: sort ?? urlSort, ...this.props,
showHidden: showHidden ?? urlShowHidden, ...props,
}; };
this.props.history.push( const queryParams: QueryParams<CommunityProps> = {
`/c/${this.props.match.params.name}${getQueryString(queryParams)}`, dataType: getDataTypeString(dataType ?? DataType.Post),
); pageCursor: pageCursor,
sort: sort,
showHidden: showHidden,
};
await this.fetchData(); this.props.history.push(`/c/${name}${getQueryString(queryParams)}`);
} }
async fetchData() { fetchDataToken?: symbol;
const { dataType, pageCursor, sort, showHidden } = this.props; async fetchData(props: CommunityRouteProps) {
const { name } = this.props.match.params; const token = (this.fetchDataToken = Symbol());
const { dataType, pageCursor, sort, showHidden } = props;
const { name } = props.match.params;
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
this.setState({ postsRes: LOADING_REQUEST }); this.setState({ postsRes: LOADING_REQUEST, commentsRes: EMPTY_REQUEST });
this.setState({ const postsRes = await HttpService.client.getPosts({
postsRes: await HttpService.client.getPosts({ page_cursor: pageCursor,
page_cursor: pageCursor, limit: fetchLimit,
limit: fetchLimit, sort,
sort, type_: "All",
type_: "All", community_name: name,
community_name: name, saved_only: false,
saved_only: false, show_hidden: showHidden === "true",
show_hidden: showHidden === "true",
}),
}); });
if (token === this.fetchDataToken) {
this.setState({ postsRes });
}
} else { } else {
this.setState({ commentsRes: LOADING_REQUEST }); this.setState({ commentsRes: LOADING_REQUEST, postsRes: EMPTY_REQUEST });
this.setState({ const commentsRes = await HttpService.client.getComments({
commentsRes: await HttpService.client.getComments({ limit: fetchLimit,
limit: fetchLimit, sort: postToCommentSortType(sort),
sort: postToCommentSortType(sort), type_: "All",
type_: "All", community_name: name,
community_name: name, saved_only: false,
saved_only: false,
}),
}); });
if (token === this.fetchDataToken) {
this.setState({ commentsRes });
}
} }
} }

View file

@ -80,6 +80,21 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
this.handleEditCancel = this.handleEditCancel.bind(this); 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( componentWillReceiveProps(
nextProps: Readonly<{ children?: InfernoNode } & SidebarProps>, nextProps: Readonly<{ children?: InfernoNode } & SidebarProps>,
): void { ): void {

View file

@ -41,6 +41,7 @@ import { IRoutePropsWithFetch } from "../../routes";
import { MediaUploads } from "../common/media-uploads"; import { MediaUploads } from "../common/media-uploads";
import { Paginator } from "../common/paginator"; import { Paginator } from "../common/paginator";
import { snapToTop } from "@utils/browser"; import { snapToTop } from "@utils/browser";
import { isBrowser } from "@utils/browser";
type AdminSettingsData = RouteDataResponse<{ type AdminSettingsData = RouteDataResponse<{
bannedRes: BannedPersonsResponse; bannedRes: BannedPersonsResponse;
@ -51,7 +52,6 @@ type AdminSettingsData = RouteDataResponse<{
interface AdminSettingsState { interface AdminSettingsState {
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
banned: PersonView[]; banned: PersonView[];
currentTab: string;
instancesRes: RequestState<GetFederatedInstancesResponse>; instancesRes: RequestState<GetFederatedInstancesResponse>;
bannedRes: RequestState<BannedPersonsResponse>; bannedRes: RequestState<BannedPersonsResponse>;
leaveAdminTeamRes: RequestState<GetSiteResponse>; leaveAdminTeamRes: RequestState<GetSiteResponse>;
@ -79,7 +79,6 @@ export class AdminSettings extends Component<
state: AdminSettingsState = { state: AdminSettingsState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
banned: [], banned: [],
currentTab: "site",
bannedRes: EMPTY_REQUEST, bannedRes: EMPTY_REQUEST,
instancesRes: EMPTY_REQUEST, instancesRes: EMPTY_REQUEST,
leaveAdminTeamRes: EMPTY_REQUEST, leaveAdminTeamRes: EMPTY_REQUEST,
@ -134,12 +133,14 @@ export class AdminSettings extends Component<
}; };
} }
async componentDidMount() { async componentWillMount() {
if (!this.state.isIsomorphic) { if (isBrowser()) {
await this.fetchData(); if (!this.state.isIsomorphic) {
} else { await this.fetchData();
const themeList = await fetchThemeList(); } else {
this.setState({ themeList }); const themeList = await fetchThemeList();
this.setState({ themeList });
}
} }
} }
@ -431,10 +432,6 @@ export class AdminSettings extends Component<
return editRes; return editRes;
} }
handleSwitchTab(i: { ctx: AdminSettings; tab: string }) {
i.ctx.setState({ currentTab: i.tab });
}
async handleLeaveAdminTeam(i: AdminSettings) { async handleLeaveAdminTeam(i: AdminSettings) {
i.setState({ leaveAdminTeamRes: LOADING_REQUEST }); i.setState({ leaveAdminTeamRes: LOADING_REQUEST });
this.setState({ this.setState({

View file

@ -19,13 +19,14 @@ import {
getQueryString, getQueryString,
getRandomFromList, getRandomFromList,
resourcesSettled, resourcesSettled,
bareRoutePush,
} from "@utils/helpers"; } from "@utils/helpers";
import { scrollMixin } from "../mixins/scroll-mixin"; import { scrollMixin } from "../mixins/scroll-mixin";
import { canCreateCommunity } from "@utils/roles"; import { canCreateCommunity } from "@utils/roles";
import type { QueryParams, StringBoolean } from "@utils/types"; import type { QueryParams, StringBoolean } from "@utils/types";
import { RouteDataResponse } from "@utils/types"; import { RouteDataResponse } from "@utils/types";
import { NoOptionI18nKeys } from "i18next"; import { NoOptionI18nKeys } from "i18next";
import { Component, MouseEventHandler, linkEvent } from "inferno"; import { Component, InfernoNode, MouseEventHandler, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { import {
@ -112,7 +113,7 @@ import {
import { RouteComponentProps } from "inferno-router/dist/Route"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import PostHiddenSelect from "../common/post-hidden-select"; import PostHiddenSelect from "../common/post-hidden-select";
import { snapToTop } from "@utils/browser"; import { isBrowser, snapToTop } from "@utils/browser";
interface HomeState { interface HomeState {
postsRes: RequestState<GetPostsResponse>; postsRes: RequestState<GetPostsResponse>;
@ -344,14 +345,28 @@ export class Home extends Component<HomeRouteProps, HomeState> {
)?.content; )?.content;
} }
async componentDidMount() { async componentWillMount() {
if ( if (
!this.state.isIsomorphic || (!this.state.isIsomorphic ||
!Object.values(this.isoData.routeData).some( !Object.values(this.isoData.routeData).some(
res => res.state === "success" || res.state === "failed", 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({ async updateUrl(props: Partial<HomeProps>) {
dataType, const { dataType, listingType, pageCursor, sort, showHidden } = {
listingType, ...this.props,
pageCursor, ...props,
sort, };
showHidden,
}: Partial<HomeProps>) {
const {
dataType: urlDataType,
listingType: urlListingType,
sort: urlSort,
showHidden: urlShowHidden,
} = this.props;
const queryParams: QueryParams<HomeProps> = { const queryParams: QueryParams<HomeProps> = {
dataType: getDataTypeString(dataType ?? urlDataType), dataType: getDataTypeString(dataType ?? DataType.Post),
listingType: listingType ?? urlListingType, listingType: listingType,
pageCursor: pageCursor, pageCursor: pageCursor,
sort: sort ?? urlSort, sort: sort,
showHidden: showHidden ?? urlShowHidden, showHidden: showHidden,
}; };
this.props.history.push({ this.props.history.push({
pathname: "/", pathname: "/",
search: getQueryString(queryParams), search: getQueryString(queryParams),
}); });
await this.fetchData();
} }
get posts() { get posts() {
@ -854,31 +858,39 @@ export class Home extends Component<HomeRouteProps, HomeState> {
}); });
} }
async fetchData() { fetchDataToken?: symbol;
const { dataType, pageCursor, listingType, sort, showHidden } = this.props; async fetchData({
dataType,
pageCursor,
listingType,
sort,
showHidden,
}: HomeProps) {
const token = (this.fetchDataToken = Symbol());
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
this.setState({ postsRes: LOADING_REQUEST }); this.setState({ postsRes: LOADING_REQUEST, commentsRes: EMPTY_REQUEST });
this.setState({ const postsRes = await HttpService.client.getPosts({
postsRes: await HttpService.client.getPosts({ page_cursor: pageCursor,
page_cursor: pageCursor, limit: fetchLimit,
limit: fetchLimit, sort,
sort, saved_only: false,
saved_only: false, type_: listingType,
type_: listingType, show_hidden: showHidden === "true",
show_hidden: showHidden === "true",
}),
}); });
if (token === this.fetchDataToken) {
this.setState({ postsRes });
}
} else { } else {
this.setState({ commentsRes: LOADING_REQUEST }); this.setState({ commentsRes: LOADING_REQUEST, postsRes: EMPTY_REQUEST });
this.setState({ const commentsRes = await HttpService.client.getComments({
commentsRes: await HttpService.client.getComments({ limit: fetchLimit,
limit: fetchLimit, sort: postToCommentSortType(sort),
sort: postToCommentSortType(sort), saved_only: false,
saved_only: false, type_: listingType,
type_: listingType,
}),
}); });
if (token === this.fetchDataToken) {
this.setState({ commentsRes });
}
} }
} }

View file

@ -26,6 +26,7 @@ import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import { resourcesSettled } from "@utils/helpers"; import { resourcesSettled } from "@utils/helpers";
import { scrollMixin } from "../mixins/scroll-mixin"; import { scrollMixin } from "../mixins/scroll-mixin";
import { isBrowser } from "@utils/browser";
type InstancesData = RouteDataResponse<{ type InstancesData = RouteDataResponse<{
federatedInstancesResponse: GetFederatedInstancesResponse; federatedInstancesResponse: GetFederatedInstancesResponse;
@ -71,8 +72,8 @@ export class Instances extends Component<InstancesRouteProps, InstancesState> {
} }
} }
async componentDidMount() { async componentWillMount() {
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic && isBrowser()) {
await this.fetchInstances(); await this.fetchInstances();
} }
} }

View file

@ -19,6 +19,7 @@ import PasswordInput from "../common/password-input";
import { SiteForm } from "./site-form"; import { SiteForm } from "./site-form";
import { simpleScrollMixin } from "../mixins/scroll-mixin"; import { simpleScrollMixin } from "../mixins/scroll-mixin";
import { RouteComponentProps } from "inferno-router/dist/Route"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { isBrowser } from "@utils/browser";
interface State { interface State {
form: { form: {
@ -61,8 +62,10 @@ export class Setup extends Component<
this.handleCreateSite = this.handleCreateSite.bind(this); this.handleCreateSite = this.handleCreateSite.bind(this);
} }
async componentDidMount() { async componentWillMount() {
this.setState({ themeList: await fetchThemeList() }); if (isBrowser()) {
this.setState({ themeList: await fetchThemeList() });
}
} }
get documentTitle(): string { get documentTitle(): string {

View file

@ -76,8 +76,11 @@ export class Signup extends Component<
this.handleAnswerChange = this.handleAnswerChange.bind(this); this.handleAnswerChange = this.handleAnswerChange.bind(this);
} }
async componentDidMount() { async componentWillMount() {
if (this.state.siteRes.site_view.local_site.captcha_enabled) { if (
this.state.siteRes.site_view.local_site.captcha_enabled &&
isBrowser()
) {
await this.fetchCaptcha(); await this.fetchCaptcha();
} }
} }

View file

@ -2,7 +2,7 @@ import { Modal } from "bootstrap";
import { Component, InfernoNode, RefObject } from "inferno"; import { Component, InfernoNode, RefObject } from "inferno";
export function modalMixin< export function modalMixin<
P extends { show: boolean }, P extends { show?: boolean },
S, S,
Base extends new (...args: any[]) => Component<P, S> & { Base extends new (...args: any[]) => Component<P, S> & {
readonly modalDivRef: RefObject<HTMLDivElement>; readonly modalDivRef: RefObject<HTMLDivElement>;

View file

@ -1,6 +1,6 @@
import { isBrowser, nextUserAction, snapToTop } from "../../utils/browser"; import { isBrowser, nextUserAction, snapToTop } from "../../utils/browser";
import { Component, InfernoNode } from "inferno"; import { Component, InfernoNode } from "inferno";
import { Location } from "history"; import { Location, History, Action } from "history";
function restoreScrollPosition(props: { location: Location }) { function restoreScrollPosition(props: { location: Location }) {
const key: string = props.location.key; const key: string = props.location.key;
@ -25,7 +25,7 @@ function dropScrollPosition(props: { location: Location }) {
} }
export function scrollMixin< export function scrollMixin<
P extends { location: Location }, P extends { location: Location; history: History },
S, S,
Base extends new ( Base extends new (
...args: any ...args: any
@ -68,10 +68,11 @@ export function scrollMixin<
nextProps: Readonly<{ children?: InfernoNode } & P>, nextProps: Readonly<{ children?: InfernoNode } & P>,
nextContext: any, nextContext: any,
) { ) {
// Currently this is hypothetical. Components unmount before route changes.
if (this.props.location.key !== nextProps.location.key) { if (this.props.location.key !== nextProps.location.key) {
this.saveFinalPosition(); if (nextProps.history.action !== Action.Replace) {
this.reset(); this.saveFinalPosition();
this.reset();
}
} }
return super.componentWillReceiveProps?.(nextProps, nextContext); return super.componentWillReceiveProps?.(nextProps, nextContext);
} }
@ -131,7 +132,7 @@ export function scrollMixin<
} }
export function simpleScrollMixin< export function simpleScrollMixin<
P extends { location: Location }, P extends { location: Location; history: History },
S, S,
Base extends new (...args: any) => Component<P, S>, Base extends new (...args: any) => Component<P, S>,
>(base: Base, _context?: ClassDecoratorContext<Base>) { >(base: Base, _context?: ClassDecoratorContext<Base>) {

View file

@ -1,9 +1,4 @@
import { import { fetchUsers, personToChoice, setIsoData } from "@utils/app";
fetchUsers,
getUpdatedSearchId,
personToChoice,
setIsoData,
} from "@utils/app";
import { import {
debounce, debounce,
formatPastDate, formatPastDate,
@ -12,6 +7,7 @@ import {
getQueryParams, getQueryParams,
getQueryString, getQueryString,
resourcesSettled, resourcesSettled,
bareRoutePush,
} from "@utils/helpers"; } from "@utils/helpers";
import { scrollMixin } from "./mixins/scroll-mixin"; import { scrollMixin } from "./mixins/scroll-mixin";
import { amAdmin, amMod } from "@utils/roles"; import { amAdmin, amMod } from "@utils/roles";
@ -66,6 +62,8 @@ import { CommunityLink } from "./community/community-link";
import { PersonListing } from "./person/person-listing"; import { PersonListing } from "./person/person-listing";
import { getHttpBaseInternal } from "../utils/env"; import { getHttpBaseInternal } from "../utils/env";
import { IRoutePropsWithFetch } from "../routes"; import { IRoutePropsWithFetch } from "../routes";
import { isBrowser } from "@utils/browser";
import { LoadingEllipses } from "./common/loading-ellipses";
type FilterType = "mod" | "user"; type FilterType = "mod" | "user";
@ -703,40 +701,68 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
} }
} }
async componentDidMount() { async componentWillMount() {
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic && isBrowser()) {
const { modId, userId } = this.props; await Promise.all([
const promises = [this.refetch()]; this.fetchModlog(this.props),
this.fetchCommunity(this.props),
this.fetchUser(this.props),
this.fetchMod(this.props),
]);
}
}
if (userId) { componentWillReceiveProps(nextProps: ModlogRouteProps) {
promises.push( this.fetchModlog(nextProps);
HttpService.client
.getPersonDetails({ person_id: userId }) const reload = bareRoutePush(this.props, nextProps);
.then(res => {
if (res.state === "success") { if (nextProps.modId !== this.props.modId || reload) {
this.setState({ this.fetchMod(nextProps);
userSearchOptions: [personToChoice(res.data.person_view)], }
}); 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) {
const res = await HttpService.client.getPersonDetails({
person_id: userId,
});
if (res.state === "success" && token === this.fetchUserToken) {
this.setState({
userSearchOptions: [personToChoice(res.data.person_view)],
});
} }
}
}
if (modId) { fetchModToken?: symbol;
promises.push( async fetchMod(props: ModlogRouteProps) {
HttpService.client const token = (this.fetchModToken = Symbol());
.getPersonDetails({ person_id: modId }) const { modId } = props;
.then(res => {
if (res.state === "success") { if (modId) {
this.setState({ const res = await HttpService.client.getPersonDetails({
modSearchOptions: [personToChoice(res.data.person_view)], 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, modSearchOptions,
} = this.state; } = this.state;
const { actionType, modId, userId } = this.props; 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 ( return (
<div className="modlog container-lg"> <div className="modlog container-lg">
@ -816,15 +847,26 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
#<strong>#</strong># #<strong>#</strong>#
</T> </T>
</div> </div>
{this.state.communityRes.state === "success" && ( {communityId && (
<h5> <h5>
<Link {communityResp ? (
className="text-body" <>
to={`/c/${this.state.communityRes.data.community_view.community.name}`} <Link
> className="text-body"
/c/{this.state.communityRes.data.community_view.community.name}{" "} to={`/c/${communityResp.community_view.community.name}`}
</Link> >
<span>{I18NextService.i18n.t("modlog")}</span> /c/{communityResp.community_view.community.name}
</Link>{" "}
<span>{I18NextService.i18n.t("modlog")}</span>
</>
) : (
communityState === "loading" && (
<>
<LoadingEllipses />
&nbsp;
</>
)
)}
</h5> </h5>
)} )}
<div className="row mb-2"> <div className="row mb-2">
@ -935,6 +977,10 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
} }
handleSearchUsers = debounce(async (text: string) => { handleSearchUsers = debounce(async (text: string) => {
if (!text.length) {
return;
}
const { userId } = this.props; const { userId } = this.props;
const { userSearchOptions } = this.state; const { userSearchOptions } = this.state;
this.setState({ loadingUserSearch: true }); this.setState({ loadingUserSearch: true });
@ -952,6 +998,10 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
}); });
handleSearchMods = debounce(async (text: string) => { handleSearchMods = debounce(async (text: string) => {
if (!text.length) {
return;
}
const { modId } = this.props; const { modId } = this.props;
const { modSearchOptions } = this.state; const { modSearchOptions } = this.state;
this.setState({ loadingModSearch: true }); this.setState({ loadingModSearch: true });
@ -968,61 +1018,73 @@ export class Modlog extends Component<ModlogRouteProps, ModlogState> {
}); });
}); });
async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) { async updateUrl(props: Partial<ModlogProps>) {
const { const {
page: urlPage, actionType,
actionType: urlActionType, modId,
modId: urlModId, page,
userId: urlUserId, userId,
} = this.props; match: {
params: { communityId },
},
} = { ...this.props, ...props };
const queryParams: QueryParams<ModlogProps> = { const queryParams: QueryParams<ModlogProps> = {
page: (page ?? urlPage).toString(), page: page.toString(),
actionType: actionType ?? urlActionType, actionType: actionType,
modId: getUpdatedSearchId(modId, urlModId), modId: modId?.toString(),
userId: getUpdatedSearchId(userId, urlUserId), userId: userId?.toString(),
}; };
const communityId = this.props.match.params.communityId;
this.props.history.push( this.props.history.push(
`/modlog${communityId ? `/${communityId}` : ""}${getQueryString( `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
queryParams, queryParams,
)}`, )}`,
); );
await this.refetch();
} }
async refetch() { fetchModlogToken?: symbol;
const { actionType, page, modId, userId, postId, commentId } = this.props; async fetchModlog(props: ModlogRouteProps) {
const { communityId: urlCommunityId } = this.props.match.params; const token = (this.fetchModlogToken = Symbol());
const { actionType, page, modId, userId, postId, commentId } = props;
const { communityId: urlCommunityId } = props.match.params;
const communityId = getIdFromString(urlCommunityId); const communityId = getIdFromString(urlCommunityId);
this.setState({ res: LOADING_REQUEST }); this.setState({ res: LOADING_REQUEST });
this.setState({ const res = await HttpService.client.getModlog({
res: await HttpService.client.getModlog({ community_id: communityId,
community_id: communityId, page,
page, limit: fetchLimit,
limit: fetchLimit, type_: actionType,
type_: actionType, other_person_id: userId,
other_person_id: userId, mod_person_id: !this.isoData.site_res.site_view.local_site
mod_person_id: !this.isoData.site_res.site_view.local_site .hide_modlog_mod_names
.hide_modlog_mod_names ? modId
? modId : undefined,
: undefined, comment_id: commentId,
comment_id: commentId, post_id: postId,
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) { if (communityId) {
this.setState({ communityRes: LOADING_REQUEST }); this.setState({ communityRes: LOADING_REQUEST });
this.setState({ const communityRes = await HttpService.client.getCommunity({
communityRes: await HttpService.client.getCommunity({ id: communityId,
id: communityId,
}),
}); });
if (token === this.fetchCommunityToken) {
this.setState({ communityRes });
}
} else {
this.setState({ communityRes: EMPTY_REQUEST });
} }
} }

View file

@ -89,6 +89,7 @@ import { getHttpBaseInternal } from "../../utils/env";
import { CommentsLoadingSkeleton } from "../common/loading-skeleton"; import { CommentsLoadingSkeleton } from "../common/loading-skeleton";
import { RouteComponentProps } from "inferno-router/dist/Route"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import { isBrowser } from "@utils/browser";
enum UnreadOrAll { enum UnreadOrAll {
Unread, Unread,
@ -213,8 +214,8 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
} }
} }
async componentDidMount() { async componentWillMount() {
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic && isBrowser()) {
await this.refetch(); await this.refetch();
} }
} }
@ -784,40 +785,60 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
return inboxData; return inboxData;
} }
refetchToken?: symbol;
async refetch() { async refetch() {
const token = (this.refetchToken = Symbol());
const sort = this.state.sort; const sort = this.state.sort;
const unread_only = this.state.unreadOrAll === UnreadOrAll.Unread; const unread_only = this.state.unreadOrAll === UnreadOrAll.Unread;
const page = this.state.page; const page = this.state.page;
const limit = fetchLimit; const limit = fetchLimit;
this.setState({ repliesRes: LOADING_REQUEST });
this.setState({ this.setState({
repliesRes: await HttpService.client.getReplies({ repliesRes: LOADING_REQUEST,
mentionsRes: LOADING_REQUEST,
messagesRes: LOADING_REQUEST,
});
const repliesPromise = HttpService.client
.getReplies({
sort, sort,
unread_only, unread_only,
page, page,
limit, limit,
}), })
}); .then(repliesRes => {
if (token === this.refetchToken) {
this.setState({
repliesRes,
});
}
});
this.setState({ mentionsRes: LOADING_REQUEST }); const mentionsPromise = HttpService.client
this.setState({ .getPersonMentions({
mentionsRes: await HttpService.client.getPersonMentions({
sort, sort,
unread_only, unread_only,
page, page,
limit, limit,
}), })
}); .then(mentionsRes => {
if (token === this.refetchToken) {
this.setState({ mentionsRes });
}
});
this.setState({ messagesRes: LOADING_REQUEST }); const messagesPromise = HttpService.client
this.setState({ .getPrivateMessages({
messagesRes: await HttpService.client.getPrivateMessages({
unread_only, unread_only,
page, page,
limit, limit,
}), })
}); .then(messagesRes => {
if (token === this.refetchToken) {
this.setState({ messagesRes });
}
});
await Promise.all([repliesPromise, mentionsPromise, messagesPromise]);
UnreadCounterService.Instance.updateInboxCounts(); UnreadCounterService.Instance.updateInboxCounts();
} }

View file

@ -19,6 +19,7 @@ import {
numToSI, numToSI,
randomStr, randomStr,
resourcesSettled, resourcesSettled,
bareRoutePush,
} from "@utils/helpers"; } from "@utils/helpers";
import { canMod } from "@utils/roles"; import { canMod } from "@utils/roles";
import type { QueryParams } from "@utils/types"; import type { QueryParams } from "@utils/types";
@ -100,6 +101,7 @@ import { getHttpBaseInternal } from "../../utils/env";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import { MediaUploads } from "../common/media-uploads"; import { MediaUploads } from "../common/media-uploads";
import { cakeDate } from "@utils/helpers"; import { cakeDate } from "@utils/helpers";
import { isBrowser } from "@utils/browser";
type ProfileData = RouteDataResponse<{ type ProfileData = RouteDataResponse<{
personRes: GetPersonDetailsResponse; personRes: GetPersonDetailsResponse;
@ -108,6 +110,9 @@ type ProfileData = RouteDataResponse<{
interface ProfileState { interface ProfileState {
personRes: RequestState<GetPersonDetailsResponse>; 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>; uploadsRes: RequestState<ListMediaResponse>;
personBlocked: boolean; personBlocked: boolean;
banReason?: string; banReason?: string;
@ -195,6 +200,7 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
private isoData = setIsoData<ProfileData>(this.context); private isoData = setIsoData<ProfileData>(this.context);
state: ProfileState = { state: ProfileState = {
personRes: EMPTY_REQUEST, personRes: EMPTY_REQUEST,
personDetailsRes: EMPTY_REQUEST,
uploadsRes: EMPTY_REQUEST, uploadsRes: EMPTY_REQUEST,
personBlocked: false, personBlocked: false,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
@ -205,7 +211,12 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
}; };
loadingSettled() { 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) { constructor(props: ProfileRouteProps, context: any) {
@ -253,6 +264,7 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
this.state = { this.state = {
...this.state, ...this.state,
personRes, personRes,
personDetailsRes: personRes,
uploadsRes, uploadsRes,
isIsomorphic: true, isIsomorphic: true,
personBlocked: isPersonBlocked(personRes), personBlocked: isPersonBlocked(personRes),
@ -260,37 +272,96 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
} }
} }
async componentDidMount() { async componentWillMount() {
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic && isBrowser()) {
await this.fetchUserData(); await this.fetchUserData(this.props, true);
} }
} }
async fetchUserData() { componentWillReceiveProps(nextProps: ProfileRouteProps) {
const { page, sort, view } = this.props; // 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({ const personRes = await HttpService.client.getPersonDetails({
username: this.props.match.params.username, username: props.match.params.username,
sort, sort,
saved_only: view === PersonDetailsView.Saved, saved_only: view === PersonDetailsView.Saved,
page, page,
limit: fetchLimit, limit: fetchLimit,
}); });
this.setState({ if (token === this.fetchUserDataToken) {
personRes, this.setState({
personBlocked: isPersonBlocked(personRes), 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 personRes = this.state.personRes.data;
const { page, sort, view } = this.props; const { page, sort, view } = this.props;
const personDetailsState = this.state.personDetailsRes.state;
const personDetailsRes =
personDetailsState === "success" && this.state.personDetailsRes.data;
return ( return (
<div className="row"> <div className="row">
<div className="col-12 col-md-8"> <div className="col-12 col-md-8">
@ -403,50 +478,59 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
{this.renderUploadsRes()} {this.renderUploadsRes()}
<PersonDetails {personDetailsState === "loading" &&
personRes={personRes} this.props.view !== PersonDetailsView.Uploads ? (
admins={siteRes.admins} <h5>
sort={sort} <Spinner large />
page={page} </h5>
limit={fetchLimit} ) : (
finished={this.state.finished} personDetailsRes && (
enableDownvotes={enableDownvotes(siteRes)} <PersonDetails
voteDisplayMode={voteDisplayMode(siteRes)} personRes={personDetailsRes}
enableNsfw={enableNsfw(siteRes)} admins={siteRes.admins}
view={view} sort={sort}
onPageChange={this.handlePageChange} page={page}
allLanguages={siteRes.all_languages} limit={fetchLimit}
siteLanguages={siteRes.discussion_languages} finished={this.state.finished}
// TODO all the forms here enableDownvotes={enableDownvotes(siteRes)}
onSaveComment={this.handleSaveComment} voteDisplayMode={voteDisplayMode(siteRes)}
onBlockPerson={this.handleBlockPersonAlt} enableNsfw={enableNsfw(siteRes)}
onDeleteComment={this.handleDeleteComment} view={view}
onRemoveComment={this.handleRemoveComment} onPageChange={this.handlePageChange}
onCommentVote={this.handleCommentVote} allLanguages={siteRes.all_languages}
onCommentReport={this.handleCommentReport} siteLanguages={siteRes.discussion_languages}
onDistinguishComment={this.handleDistinguishComment} // TODO all the forms here
onAddModToCommunity={this.handleAddModToCommunity} onSaveComment={this.handleSaveComment}
onAddAdmin={this.handleAddAdmin} onBlockPerson={this.handleBlockPersonAlt}
onTransferCommunity={this.handleTransferCommunity} onDeleteComment={this.handleDeleteComment}
onPurgeComment={this.handlePurgeComment} onRemoveComment={this.handleRemoveComment}
onPurgePerson={this.handlePurgePerson} onCommentVote={this.handleCommentVote}
onCommentReplyRead={this.handleCommentReplyRead} onCommentReport={this.handleCommentReport}
onPersonMentionRead={this.handlePersonMentionRead} onDistinguishComment={this.handleDistinguishComment}
onBanPersonFromCommunity={this.handleBanFromCommunity} onAddModToCommunity={this.handleAddModToCommunity}
onBanPerson={this.handleBanPerson} onAddAdmin={this.handleAddAdmin}
onCreateComment={this.handleCreateComment} onTransferCommunity={this.handleTransferCommunity}
onEditComment={this.handleEditComment} onPurgeComment={this.handlePurgeComment}
onPostEdit={this.handlePostEdit} onPurgePerson={this.handlePurgePerson}
onPostVote={this.handlePostVote} onCommentReplyRead={this.handleCommentReplyRead}
onPostReport={this.handlePostReport} onPersonMentionRead={this.handlePersonMentionRead}
onLockPost={this.handleLockPost} onBanPersonFromCommunity={this.handleBanFromCommunity}
onDeletePost={this.handleDeletePost} onBanPerson={this.handleBanPerson}
onRemovePost={this.handleRemovePost} onCreateComment={this.handleCreateComment}
onSavePost={this.handleSavePost} onEditComment={this.handleEditComment}
onPurgePost={this.handlePurgePost} onPostEdit={this.handlePostEdit}
onFeaturePost={this.handleFeaturePost} onPostVote={this.handlePostVote}
onMarkPostAsRead={() => {}} onPostReport={this.handlePostReport}
/> onLockPost={this.handleLockPost}
onDeletePost={this.handleDeletePost}
onRemovePost={this.handleRemovePost}
onSavePost={this.handleSavePost}
onPurgePost={this.handlePurgePost}
onFeaturePost={this.handleFeaturePost}
onMarkPostAsRead={() => {}}
/>
)
)}
</div> </div>
<div className="col-12 col-md-4"> <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>) { async updateUrl(props: Partial<ProfileRouteProps>) {
const { page: urlPage, sort: urlSort, view: urlView } = this.props; const {
page,
sort,
view,
match: {
params: { username },
},
} = { ...this.props, ...props };
const queryParams: QueryParams<ProfileProps> = { const queryParams: QueryParams<ProfileProps> = {
page: (page ?? urlPage).toString(), page: page?.toString(),
sort: sort ?? urlSort, sort,
view: view ?? urlView, view,
}; };
const { username } = this.props.match.params;
this.props.history.push(`/u/${username}${getQueryString(queryParams)}`); this.props.history.push(`/u/${username}${getQueryString(queryParams)}`);
await this.fetchUserData();
} }
handlePageChange(page: number) { handlePageChange(page: number) {

View file

@ -29,6 +29,7 @@ import { UnreadCounterService } from "../../services";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import { isBrowser } from "@utils/browser";
enum RegistrationState { enum RegistrationState {
Unread, Unread,
@ -92,8 +93,8 @@ export class RegistrationApplications extends Component<
} }
} }
async componentDidMount() { async componentWillMount() {
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic && isBrowser()) {
await this.refetch(); await this.refetch();
} }
} }
@ -108,37 +109,41 @@ export class RegistrationApplications extends Component<
} }
renderApps() { renderApps() {
switch (this.state.appsRes.state) { const appsState = this.state.appsRes.state;
case "loading": const apps =
return ( appsState === "success" &&
<h5> this.state.appsRes.data.registration_applications;
<Spinner large />
</h5> return (
); <div className="row">
case "success": { <div className="col-12">
const apps = this.state.appsRes.data.registration_applications; <HtmlTags
return ( title={this.documentTitle}
<div className="row"> path={this.context.router.route.match.url}
<div className="col-12"> />
<HtmlTags <h1 className="h4 mb-4">
title={this.documentTitle} {I18NextService.i18n.t("registration_applications")}
path={this.context.router.route.match.url} </h1>
/> {this.selects()}
<h1 className="h4 mb-4"> {apps ? (
{I18NextService.i18n.t("registration_applications")} <>
</h1>
{this.selects()}
{this.applicationList(apps)} {this.applicationList(apps)}
<Paginator <Paginator
page={this.state.page} page={this.state.page}
onChange={this.handlePageChange} onChange={this.handlePageChange}
nextDisabled={fetchLimit > apps.length} nextDisabled={fetchLimit > apps.length}
/> />
</div> </>
</div> ) : (
); appsState === "loading" && (
} <div className="text-center">
} <Spinner large />
</div>
)
)}
</div>
</div>
);
} }
render() { render() {
@ -263,19 +268,22 @@ export class RegistrationApplications extends Component<
}; };
} }
refetchToken?: symbol;
async refetch() { async refetch() {
const token = (this.refetchToken = Symbol());
const unread_only = const unread_only =
this.state.registrationState === RegistrationState.Unread; this.state.registrationState === RegistrationState.Unread;
this.setState({ this.setState({
appsRes: LOADING_REQUEST, appsRes: LOADING_REQUEST,
}); });
this.setState({ const appsRes = await HttpService.client.listRegistrationApplications({
appsRes: await HttpService.client.listRegistrationApplications({ unread_only: unread_only,
unread_only: unread_only, page: this.state.page,
page: this.state.page, limit: fetchLimit,
limit: fetchLimit,
}),
}); });
if (token === this.refetchToken) {
this.setState({ appsRes });
}
} }
async handleApproveApplication(form: ApproveRegistrationApplication) { async handleApproveApplication(form: ApproveRegistrationApplication) {

View file

@ -56,6 +56,7 @@ import { UnreadCounterService } from "../../services";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import { isBrowser } from "@utils/browser";
enum UnreadOrAll { enum UnreadOrAll {
Unread, Unread,
@ -160,8 +161,8 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
} }
} }
async componentDidMount() { async componentWillMount() {
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic && isBrowser()) {
await this.refetch(); await this.refetch();
} }
} }
@ -452,9 +453,22 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
} }
all() { 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 ( return (
<div> <div>
{this.buildCombined.map(i => ( {combined.map(i => (
<> <>
<hr /> <hr />
{this.renderItemType(i)} {this.renderItemType(i)}
@ -575,6 +589,7 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
static async fetchInitialData({ static async fetchInitialData({
headers, headers,
site,
}: InitialFetchRequest): Promise<ReportsData> { }: InitialFetchRequest): Promise<ReportsData> {
const client = wrapClient( const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),
@ -601,7 +616,7 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
messageReportsRes: EMPTY_REQUEST, messageReportsRes: EMPTY_REQUEST,
}; };
if (amAdmin()) { if (amAdmin(site.my_user)) {
const privateMessageReportsForm: ListPrivateMessageReports = { const privateMessageReportsForm: ListPrivateMessageReports = {
unresolved_only, unresolved_only,
page, page,
@ -616,7 +631,9 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
return data; return data;
} }
refetchToken?: symbol;
async refetch() { async refetch() {
const token = (this.refetchToken = Symbol());
const unresolved_only = this.state.unreadOrAll === UnreadOrAll.Unread; const unresolved_only = this.state.unreadOrAll === UnreadOrAll.Unread;
const page = this.state.page; const page = this.state.page;
const limit = fetchLimit; const limit = fetchLimit;
@ -636,17 +653,30 @@ export class Reports extends Component<ReportsRouteProps, ReportsState> {
limit, limit,
}; };
this.setState({ const commentReportPromise = HttpService.client
commentReportsRes: await HttpService.client.listCommentReports(form), .listCommentReports(form)
postReportsRes: await HttpService.client.listPostReports(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()) { if (amAdmin()) {
this.setState({ const messageReportsRes =
messageReportsRes: await HttpService.client.listPrivateMessageReports(form);
await HttpService.client.listPrivateMessageReports(form), if (token === this.refetchToken) {
}); this.setState({ messageReportsRes });
}
} }
await Promise.all([commentReportPromise, postReportPromise]);
} }
async handleResolveCommentReport(form: ResolveCommentReport) { async handleResolveCommentReport(form: ResolveCommentReport) {

View file

@ -348,7 +348,7 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
} }
} }
async componentDidMount() { async componentWillMount() {
this.setState({ themeList: await fetchThemeList() }); this.setState({ themeList: await fetchThemeList() });
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic) {

View file

@ -13,6 +13,7 @@ import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import { simpleScrollMixin } from "../mixins/scroll-mixin"; import { simpleScrollMixin } from "../mixins/scroll-mixin";
import { RouteComponentProps } from "inferno-router/dist/Route"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { isBrowser } from "@utils/browser";
interface State { interface State {
verifyRes: RequestState<SuccessResponse>; verifyRes: RequestState<SuccessResponse>;
@ -52,8 +53,10 @@ export class VerifyEmail extends Component<
} }
} }
async componentDidMount() { async componentWillMount() {
await this.verify(); if (isBrowser()) {
await this.verify();
}
} }
get documentTitle(): string { get documentTitle(): string {

View file

@ -33,6 +33,7 @@ import { getHttpBaseInternal } from "../../utils/env";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import { simpleScrollMixin } from "../mixins/scroll-mixin"; import { simpleScrollMixin } from "../mixins/scroll-mixin";
import { toast } from "../../toast"; import { toast } from "../../toast";
import { isBrowser } from "@utils/browser";
export interface CreatePostProps { export interface CreatePostProps {
communityId?: number; communityId?: number;
@ -81,7 +82,7 @@ export class CreatePost extends Component<
private isoData = setIsoData<CreatePostData>(this.context); private isoData = setIsoData<CreatePostData>(this.context);
state: CreatePostState = { state: CreatePostState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
loading: true, loading: false,
initialCommunitiesRes: EMPTY_REQUEST, initialCommunitiesRes: EMPTY_REQUEST,
isIsomorphic: false, isIsomorphic: false,
}; };
@ -132,9 +133,9 @@ export class CreatePost extends Component<
} }
} }
async componentDidMount() { async componentWillMount() {
// TODO test this // TODO test this
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic && isBrowser()) {
const { communityId } = this.props; const { communityId } = this.props;
const initialCommunitiesRes = await fetchCommunitiesForOptions( const initialCommunitiesRes = await fetchCommunitiesForOptions(

View file

@ -10,7 +10,7 @@ import {
import { isImage } from "@utils/media"; import { isImage } from "@utils/media";
import { Choice } from "@utils/types"; import { Choice } from "@utils/types";
import autosize from "autosize"; import autosize from "autosize";
import { Component, InfernoNode, linkEvent } from "inferno"; import { Component, InfernoNode, createRef, linkEvent } from "inferno";
import { Prompt } from "inferno-router"; import { Prompt } from "inferno-router";
import { import {
CommunityView, CommunityView,
@ -132,8 +132,9 @@ function copySuggestedTitle(d: { i: PostForm; suggestedTitle?: string }) {
); );
d.i.setState({ suggestedPostsRes: EMPTY_REQUEST }); d.i.setState({ suggestedPostsRes: EMPTY_REQUEST });
setTimeout(() => { setTimeout(() => {
const textarea: any = document.getElementById("post-title"); if (d.i.postTitleRef.current) {
autosize.update(textarea); autosize.update(d.i.postTitleRef.current);
}
}, 10); }, 10);
} }
} }
@ -248,6 +249,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
submitted: false, submitted: false,
}; };
postTitleRef = createRef<HTMLTextAreaElement>();
constructor(props: PostFormProps, context: any) { constructor(props: PostFormProps, context: any) {
super(props, context); super(props, context);
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this)); this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this));
@ -306,17 +309,19 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
componentDidMount() { componentDidMount() {
const textarea: any = document.getElementById("post-title"); if (this.postTitleRef.current) {
autosize(this.postTitleRef.current);
if (textarea) {
autosize(textarea);
} }
} }
componentWillReceiveProps( componentWillReceiveProps(
nextProps: Readonly<{ children?: InfernoNode } & PostFormProps>, nextProps: Readonly<{ children?: InfernoNode } & PostFormProps>,
): void { ): void {
if (this.props !== nextProps) { if (
this.props.selectedCommunityChoice?.value !==
nextProps.selectedCommunityChoice?.value &&
nextProps.selectedCommunityChoice
) {
this.setState( this.setState(
s => ( s => (
(s.form.community_id = getIdFromString( (s.form.community_id = getIdFromString(
@ -325,6 +330,22 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
s 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} rows={1}
minLength={3} minLength={3}
maxLength={MAX_POST_TITLE_LENGTH} maxLength={MAX_POST_TITLE_LENGTH}
ref={this.postTitleRef}
/> />
{!validTitle(this.state.form.name) && ( {!validTitle(this.state.form.name) && (
<div className="invalid-feedback"> <div className="invalid-feedback">

View file

@ -137,7 +137,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
this.handleHidePost = this.handleHidePost.bind(this); this.handleHidePost = this.handleHidePost.bind(this);
} }
componentDidMount(): void { unlisten = () => {};
componentWillMount(): void {
if ( if (
UserService.Instance.myUserInfo && UserService.Instance.myUserInfo &&
!this.isoData.showAdultConsentModal !this.isoData.showAdultConsentModal
@ -148,6 +150,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
imageExpanded: auto_expand && !(blur_nsfw && this.postView.post.nsfw), 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 { get postView(): PostView {

View file

@ -18,15 +18,17 @@ import { isBrowser } from "@utils/browser";
import { import {
debounce, debounce,
getApubName, getApubName,
getQueryParams,
getQueryString,
randomStr, randomStr,
resourcesSettled, resourcesSettled,
bareRoutePush,
} from "@utils/helpers"; } from "@utils/helpers";
import { scrollMixin } from "../mixins/scroll-mixin"; import { scrollMixin } from "../mixins/scroll-mixin";
import { isImage } from "@utils/media"; import { isImage } from "@utils/media";
import { RouteDataResponse } from "@utils/types"; import { QueryParams, RouteDataResponse } from "@utils/types";
import autosize from "autosize";
import classNames from "classnames"; import classNames from "classnames";
import { Component, RefObject, createRef, linkEvent } from "inferno"; import { Component, createRef, linkEvent } from "inferno";
import { import {
AddAdmin, AddAdmin,
AddModToCommunity, AddModToCommunity,
@ -103,6 +105,7 @@ import { PostListing } from "./post-listing";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import { compareAsc, compareDesc } from "date-fns";
const commentsShownInterval = 15; const commentsShownInterval = 15;
@ -112,44 +115,107 @@ type PostData = RouteDataResponse<{
}>; }>;
interface PostState { interface PostState {
postId?: number;
commentId?: number;
postRes: RequestState<GetPostResponse>; postRes: RequestState<GetPostResponse>;
commentsRes: RequestState<GetCommentsResponse>; commentsRes: RequestState<GetCommentsResponse>;
commentSort: CommentSortType;
commentViewType: CommentViewType;
scrolled?: boolean;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
commentSectionRef?: RefObject<HTMLDivElement>;
showSidebarMobile: boolean; showSidebarMobile: boolean;
maxCommentsShown: number; maxCommentsShown: number;
finished: Map<CommentId, boolean | undefined>; finished: Map<CommentId, boolean | undefined>;
isIsomorphic: boolean; isIsomorphic: boolean;
} }
type PostPathProps = const defaultCommentSort: CommentSortType = "Hot";
| { post_id: string; comment_id: never }
| { post_id: never; comment_id: string }; function getCommentSortTypeFromQuery(source?: string): CommentSortType {
type PostRouteProps = RouteComponentProps<PostPathProps> & if (!source) {
Record<string, never>; 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< export type PostFetchConfig = IRoutePropsWithFetch<
PostData, PostData,
PostPathProps, PostPathProps,
Record<string, never> PostProps
>; >;
@scrollMixin @scrollMixin
export class Post extends Component<PostRouteProps, PostState> { export class Post extends Component<PostRouteProps, PostState> {
private isoData = setIsoData<PostData>(this.context); private isoData = setIsoData<PostData>(this.context);
private commentScrollDebounced: () => void; private commentScrollDebounced: () => void;
private shouldScrollToComments: boolean = false;
private commentSectionRef = createRef<HTMLDivElement>();
state: PostState = { state: PostState = {
postRes: EMPTY_REQUEST, postRes: EMPTY_REQUEST,
commentsRes: 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, siteRes: this.isoData.site_res,
showSidebarMobile: false, showSidebarMobile: false,
maxCommentsShown: commentsShownInterval, maxCommentsShown: commentsShownInterval,
@ -201,8 +267,6 @@ export class Post extends Component<PostRouteProps, PostState> {
this.handleScrollIntoCommentsClick = this.handleScrollIntoCommentsClick =
this.handleScrollIntoCommentsClick.bind(this); this.handleScrollIntoCommentsClick.bind(this);
this.state = { ...this.state, commentSectionRef: createRef() };
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const { commentsRes, postRes } = this.isoData.routeData; const { commentsRes, postRes } = this.isoData.routeData;
@ -213,71 +277,104 @@ export class Post extends Component<PostRouteProps, PostState> {
commentsRes, commentsRes,
isIsomorphic: true, isIsomorphic: true,
}; };
if (isBrowser()) {
if (this.checkScrollIntoCommentsParam) {
this.scrollIntoCommentSection();
}
}
} }
} }
async fetchPost() { fetchPostToken?: symbol;
this.setState({ async fetchPost(props: PostRouteProps) {
postRes: LOADING_REQUEST, const token = (this.fetchPostToken = Symbol());
commentsRes: LOADING_REQUEST, 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([ fetchCommentsToken?: symbol;
await HttpService.client.getPost({ async fetchComments(props: PostRouteProps) {
id: this.state.postId, const token = (this.fetchCommentsToken = Symbol());
comment_id: this.state.commentId, const { sort } = props;
}), this.setState({ commentsRes: LOADING_REQUEST });
HttpService.client.getComments({ const commentsRes = await HttpService.client.getComments({
post_id: this.state.postId, post_id: getIdFromProps(props),
parent_id: this.state.commentId, parent_id: getCommentIdFromProps(props),
max_depth: commentTreeMaxDepth, max_depth: commentTreeMaxDepth,
sort: this.state.commentSort, sort,
type_: "All", type_: "All",
saved_only: false, saved_only: false,
}),
]);
this.setState({
postRes,
commentsRes,
}); });
if (token === this.fetchCommentsToken) {
this.setState({ commentsRes });
}
}
if (this.checkScrollIntoCommentsParam) { updateUrl(props: PartialPostRouteProps, replace = false) {
this.scrollIntoCommentSection(); 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({ static async fetchInitialData({
headers, headers,
match, match,
}: InitialFetchRequest<PostPathProps>): Promise<PostData> { query: { sort },
}: InitialFetchRequest<PostPathProps, PostProps>): Promise<PostData> {
const client = wrapClient( const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),
); );
const postId = getIdFromProps({ match }); const postId = getIdFromProps({ match });
const commentId = getCommentIdFromProps({ match }); const commentId = getCommentIdFromProps({ match });
const postForm: GetPost = {}; const postForm: GetPost = {
id: postId,
comment_id: commentId,
};
const commentsForm: GetComments = { const commentsForm: GetComments = {
post_id: postId,
parent_id: commentId,
max_depth: commentTreeMaxDepth, max_depth: commentTreeMaxDepth,
sort: "Hot", sort,
type_: "All", type_: "All",
saved_only: false, saved_only: false,
}; };
postForm.id = postId;
postForm.comment_id = commentId;
commentsForm.post_id = postId;
commentsForm.parent_id = commentId;
const [postRes, commentsRes] = await Promise.all([ const [postRes, commentsRes] = await Promise.all([
client.getPost(postForm), client.getPost(postForm),
client.getComments(commentsForm), client.getComments(commentsForm),
@ -293,15 +390,79 @@ export class Post extends Component<PostRouteProps, PostState> {
document.removeEventListener("scroll", this.commentScrollDebounced); document.removeEventListener("scroll", this.commentScrollDebounced);
} }
async componentDidMount() { async componentWillMount() {
if (!this.state.isIsomorphic) { if (isBrowser()) {
await this.fetchPost(); this.shouldScrollToComments = this.props.scrollToComments;
if (!this.state.isIsomorphic) {
await Promise.all([
this.fetchPost(this.props),
this.fetchComments(this.props),
]);
}
} }
}
autosize(document.querySelectorAll("textarea")); componentDidMount() {
this.commentScrollDebounced = debounce(this.trackCommentsBoxScrolling, 100); this.commentScrollDebounced = debounce(this.trackCommentsBoxScrolling, 100);
document.addEventListener("scroll", this.commentScrollDebounced); 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) { handleScrollIntoCommentsClick(e: MouseEvent) {
@ -309,16 +470,18 @@ export class Post extends Component<PostRouteProps, PostState> {
e.preventDefault(); e.preventDefault();
} }
get checkScrollIntoCommentsParam() { maybeScrollToComments() {
return ( if (this.shouldScrollToComments) {
Boolean( this.shouldScrollToComments = false;
new URLSearchParams(this.props.location.search).get("scrollToComments"), if (this.props.history.action !== "POP" || this.state.isIsomorphic) {
) && this.props.history.action !== "POP" this.scrollIntoCommentSection();
); }
}
} }
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 { isBottom(el: Element): boolean {
@ -413,13 +576,18 @@ export class Post extends Component<PostRouteProps, PostState> {
onHidePost={this.handleHidePost} onHidePost={this.handleHidePost}
onScrollIntoCommentsClick={this.handleScrollIntoCommentsClick} 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 */} {/* 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 <CommentForm
key={
this.context.router.history.location.key
// reset on new location, otherwise <Prompt /> stops working
}
node={res.post_view.post.id} node={res.post_view.post.id}
disabled={res.post_view.post.locked} disabled={res.post_view.post.locked}
allLanguages={siteRes.all_languages} allLanguages={siteRes.all_languages}
@ -447,10 +615,8 @@ export class Post extends Component<PostRouteProps, PostState> {
{this.state.showSidebarMobile && this.sidebar()} {this.state.showSidebarMobile && this.sidebar()}
</div> </div>
{this.sortRadios()} {this.sortRadios()}
{this.state.commentViewType === CommentViewType.Tree && {this.props.view === CommentViewType.Tree && this.commentsTree()}
this.commentsTree()} {this.props.view === CommentViewType.Flat && this.commentsFlat()}
{this.state.commentViewType === CommentViewType.Flat &&
this.commentsFlat()}
</main> </main>
<aside className="d-none d-md-block col-md-4 col-lg-3"> <aside className="d-none d-md-block col-md-4 col-lg-3">
{this.sidebar()} {this.sidebar()}
@ -482,13 +648,13 @@ export class Post extends Component<PostRouteProps, PostState> {
type="radio" type="radio"
className="btn-check" className="btn-check"
value={"Hot"} value={"Hot"}
checked={this.state.commentSort === "Hot"} checked={this.props.sort === "Hot"}
onChange={linkEvent(this, this.handleCommentSortChange)} onChange={linkEvent(this, this.handleCommentSortChange)}
/> />
<label <label
htmlFor={`${radioId}-hot`} htmlFor={`${radioId}-hot`}
className={classNames("btn btn-outline-secondary pointer", { className={classNames("btn btn-outline-secondary pointer", {
active: this.state.commentSort === "Hot", active: this.props.sort === "Hot",
})} })}
> >
{I18NextService.i18n.t("hot")} {I18NextService.i18n.t("hot")}
@ -498,13 +664,13 @@ export class Post extends Component<PostRouteProps, PostState> {
type="radio" type="radio"
className="btn-check" className="btn-check"
value={"Top"} value={"Top"}
checked={this.state.commentSort === "Top"} checked={this.props.sort === "Top"}
onChange={linkEvent(this, this.handleCommentSortChange)} onChange={linkEvent(this, this.handleCommentSortChange)}
/> />
<label <label
htmlFor={`${radioId}-top`} htmlFor={`${radioId}-top`}
className={classNames("btn btn-outline-secondary pointer", { className={classNames("btn btn-outline-secondary pointer", {
active: this.state.commentSort === "Top", active: this.props.sort === "Top",
})} })}
> >
{I18NextService.i18n.t("top")} {I18NextService.i18n.t("top")}
@ -514,13 +680,13 @@ export class Post extends Component<PostRouteProps, PostState> {
type="radio" type="radio"
className="btn-check" className="btn-check"
value={"Controversial"} value={"Controversial"}
checked={this.state.commentSort === "Controversial"} checked={this.props.sort === "Controversial"}
onChange={linkEvent(this, this.handleCommentSortChange)} onChange={linkEvent(this, this.handleCommentSortChange)}
/> />
<label <label
htmlFor={`${radioId}-controversial`} htmlFor={`${radioId}-controversial`}
className={classNames("btn btn-outline-secondary pointer", { className={classNames("btn btn-outline-secondary pointer", {
active: this.state.commentSort === "Controversial", active: this.props.sort === "Controversial",
})} })}
> >
{I18NextService.i18n.t("controversial")} {I18NextService.i18n.t("controversial")}
@ -530,13 +696,13 @@ export class Post extends Component<PostRouteProps, PostState> {
type="radio" type="radio"
className="btn-check" className="btn-check"
value={"New"} value={"New"}
checked={this.state.commentSort === "New"} checked={this.props.sort === "New"}
onChange={linkEvent(this, this.handleCommentSortChange)} onChange={linkEvent(this, this.handleCommentSortChange)}
/> />
<label <label
htmlFor={`${radioId}-new`} htmlFor={`${radioId}-new`}
className={classNames("btn btn-outline-secondary pointer", { className={classNames("btn btn-outline-secondary pointer", {
active: this.state.commentSort === "New", active: this.props.sort === "New",
})} })}
> >
{I18NextService.i18n.t("new")} {I18NextService.i18n.t("new")}
@ -546,13 +712,13 @@ export class Post extends Component<PostRouteProps, PostState> {
type="radio" type="radio"
className="btn-check" className="btn-check"
value={"Old"} value={"Old"}
checked={this.state.commentSort === "Old"} checked={this.props.sort === "Old"}
onChange={linkEvent(this, this.handleCommentSortChange)} onChange={linkEvent(this, this.handleCommentSortChange)}
/> />
<label <label
htmlFor={`${radioId}-old`} htmlFor={`${radioId}-old`}
className={classNames("btn btn-outline-secondary pointer", { className={classNames("btn btn-outline-secondary pointer", {
active: this.state.commentSort === "Old", active: this.props.sort === "Old",
})} })}
> >
{I18NextService.i18n.t("old")} {I18NextService.i18n.t("old")}
@ -564,13 +730,13 @@ export class Post extends Component<PostRouteProps, PostState> {
type="radio" type="radio"
className="btn-check" className="btn-check"
value={CommentViewType.Flat} value={CommentViewType.Flat}
checked={this.state.commentViewType === CommentViewType.Flat} checked={this.props.view === CommentViewType.Flat}
onChange={linkEvent(this, this.handleCommentViewTypeChange)} onChange={linkEvent(this, this.handleCommentViewTypeChange)}
/> />
<label <label
htmlFor={`${radioId}-chat`} htmlFor={`${radioId}-chat`}
className={classNames("btn btn-outline-secondary pointer", { className={classNames("btn btn-outline-secondary pointer", {
active: this.state.commentViewType === CommentViewType.Flat, active: this.props.view === CommentViewType.Flat,
})} })}
> >
{I18NextService.i18n.t("chat")} {I18NextService.i18n.t("chat")}
@ -581,6 +747,14 @@ export class Post extends Component<PostRouteProps, PostState> {
} }
commentsFlat() { commentsFlat() {
if (this.state.commentsRes.state === "loading") {
return (
<div className="text-center">
<Spinner large />
</div>
);
}
// These are already sorted by new // These are already sorted by new
const commentsRes = this.state.commentsRes; const commentsRes = this.state.commentsRes;
const postRes = this.state.postRes; const postRes = this.state.postRes;
@ -590,8 +764,8 @@ export class Post extends Component<PostRouteProps, PostState> {
return ( return (
<div> <div>
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(commentsRes.data.comments)} nodes={this.sortedFlatNodes()}
viewType={this.state.commentViewType} viewType={this.props.view}
maxCommentsShown={this.state.maxCommentsShown} maxCommentsShown={this.state.maxCommentsShown}
isTopLevel isTopLevel
locked={postRes.data.post_view.post.locked} 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() { commentsTree() {
if (this.state.commentsRes.state === "loading") {
return (
<div className="text-center">
<Spinner large />
</div>
);
}
const res = this.state.postRes; const res = this.state.postRes;
const firstComment = this.commentTree().at(0)?.comment_view.comment; const firstComment = this.commentTree().at(0)?.comment_view.comment;
const depth = getDepthFromComment(firstComment); const depth = getDepthFromComment(firstComment);
@ -662,11 +858,11 @@ export class Post extends Component<PostRouteProps, PostState> {
return ( return (
res.state === "success" && ( res.state === "success" && (
<div> <div>
{!!this.state.commentId && ( {!!getCommentIdFromProps(this.props) && (
<> <>
<button <button
className="ps-0 d-block btn btn-link text-muted" 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")} {I18NextService.i18n.t("view_all_comments")}
</button> </button>
@ -682,7 +878,7 @@ export class Post extends Component<PostRouteProps, PostState> {
)} )}
<CommentNodes <CommentNodes
nodes={this.commentTree()} nodes={this.commentTree()}
viewType={this.state.commentViewType} viewType={this.props.view}
maxCommentsShown={this.state.maxCommentsShown} maxCommentsShown={this.state.maxCommentsShown}
locked={res.data.post_view.post.locked} locked={res.data.post_view.post.locked}
moderators={res.data.moderators} moderators={res.data.moderators}
@ -719,50 +915,69 @@ export class Post extends Component<PostRouteProps, PostState> {
commentTree(): CommentNodeI[] { commentTree(): CommentNodeI[] {
if (this.state.commentsRes.state === "success") { if (this.state.commentsRes.state === "success") {
return buildCommentsTree( const comments = this.state.commentsRes.data.comments;
this.state.commentsRes.data.comments, if (comments.length) {
!!this.state.commentId, return buildCommentsTree(comments, !!getCommentIdFromProps(this.props));
); }
} else {
return [];
} }
return [];
} }
async handleCommentSortChange(i: Post, event: any) { async handleCommentSortChange(i: Post, event: any) {
i.setState({ const sort = event.target.value as CommentSortType;
commentSort: event.target.value as CommentSortType, const flattenable = sort === "New" || sort === "Old";
commentViewType: CommentViewType.Tree, if (flattenable || i.props.view !== CommentViewType.Flat) {
commentsRes: LOADING_REQUEST, i.updateUrl({ sort });
postRes: LOADING_REQUEST, } else {
}); i.updateUrl({ sort, view: CommentViewType.Tree });
await i.fetchPost(); }
} }
handleCommentViewTypeChange(i: Post, event: any) { handleCommentViewTypeChange(i: Post, event: any) {
i.setState({ const flattenable = i.props.sort === "New" || i.props.sort === "Old";
commentViewType: Number(event.target.value), const view: CommentViewType = Number(event.target.value);
commentSort: "New", if (flattenable || view !== CommentViewType.Flat) {
}); i.updateUrl({ view });
} else {
i.updateUrl({ view, sort: "New" });
}
} }
handleShowSidebarMobile(i: Post) { handleShowSidebarMobile(i: Post) {
i.setState({ showSidebarMobile: !i.state.showSidebarMobile }); i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
} }
handleViewPost(i: Post) { handleViewAllComments(i: Post) {
if (i.state.postRes.state === "success") { const id =
const id = i.state.postRes.data.post_view.post.id; getIdFromProps(i.props) ||
i.context.router.history.push(`/post/${id}`); (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) { handleViewContext(i: Post) {
if (i.state.commentsRes.state === "success") { if (i.state.commentsRes.state === "success") {
const parentId = getCommentParentId( const commentId = getCommentIdFromProps(i.props);
i.state.commentsRes.data.comments.at(0)?.comment, 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(),
},
},
});
} }
} }
} }

View file

@ -26,6 +26,7 @@ import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
import { resourcesSettled } from "@utils/helpers"; import { resourcesSettled } from "@utils/helpers";
import { scrollMixin } from "../mixins/scroll-mixin"; import { scrollMixin } from "../mixins/scroll-mixin";
import { isBrowser } from "@utils/browser";
type CreatePrivateMessageData = RouteDataResponse<{ type CreatePrivateMessageData = RouteDataResponse<{
recipientDetailsResponse: GetPersonDetailsResponse; recipientDetailsResponse: GetPersonDetailsResponse;
@ -79,8 +80,8 @@ export class CreatePrivateMessage extends Component<
} }
} }
async componentDidMount() { async componentWillMount() {
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic && isBrowser()) {
await this.fetchPersonDetails(); await this.fetchPersonDetails();
} }
} }

View file

@ -25,6 +25,7 @@ import { CommunityLink } from "./community/community-link";
import { getHttpBaseInternal } from "../utils/env"; import { getHttpBaseInternal } from "../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../routes"; import { IRoutePropsWithFetch } from "../routes";
import { isBrowser } from "@utils/browser";
interface RemoteFetchProps { interface RemoteFetchProps {
uri?: string; uri?: string;
@ -128,8 +129,8 @@ export class RemoteFetch extends Component<
} }
} }
async componentDidMount() { async componentWillMount() {
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic && isBrowser()) {
const { uri } = this.props; const { uri } = this.props;
if (uri) { if (uri) {

View file

@ -5,7 +5,6 @@ import {
enableNsfw, enableNsfw,
fetchCommunities, fetchCommunities,
fetchUsers, fetchUsers,
getUpdatedSearchId,
myAuth, myAuth,
personToChoice, personToChoice,
setIsoData, setIsoData,
@ -71,6 +70,7 @@ import { PostListing } from "./post/post-listing";
import { getHttpBaseInternal } from "../utils/env"; import { getHttpBaseInternal } from "../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route"; import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../routes"; import { IRoutePropsWithFetch } from "../routes";
import { isBrowser } from "@utils/browser";
interface SearchProps { interface SearchProps {
q?: string; q?: string;
@ -96,7 +96,6 @@ interface SearchState {
searchRes: RequestState<SearchResponse>; searchRes: RequestState<SearchResponse>;
resolveObjectRes: RequestState<ResolveObjectResponse>; resolveObjectRes: RequestState<ResolveObjectResponse>;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
searchText?: string;
communitySearchOptions: Choice[]; communitySearchOptions: Choice[];
creatorSearchOptions: Choice[]; creatorSearchOptions: Choice[];
searchCreatorLoading: boolean; searchCreatorLoading: boolean;
@ -285,10 +284,6 @@ export class Search extends Component<SearchRouteProps, SearchState> {
this.handleCommunityFilterChange.bind(this); this.handleCommunityFilterChange.bind(this);
this.handleCreatorFilterChange = this.handleCreatorFilterChange.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 // Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const { const {
@ -329,81 +324,142 @@ export class Search extends Component<SearchRouteProps, SearchState> {
} }
} }
async componentDidMount() { componentWillMount() {
if (this.props.history.action !== "POP") { if (!this.state.isIsomorphic && isBrowser()) {
this.fetchAll(this.props);
}
}
componentDidMount() {
if (this.props.history.action !== "POP" || this.state.isIsomorphic) {
this.searchInput.current?.select(); this.searchInput.current?.select();
} }
}
if (!this.state.isIsomorphic) { componentWillReceiveProps(nextProps: SearchRouteProps) {
this.setState({ if (nextProps.communityId !== this.props.communityId) {
searchCommunitiesLoading: true, this.fetchSelectedCommunity(nextProps);
searchCreatorLoading: true, }
}); if (nextProps.creatorId !== this.props.creatorId) {
this.fetchSelectedCreator(nextProps);
}
this.search(nextProps);
}
const promises = [ componentDidUpdate(prevProps: SearchRouteProps) {
HttpService.client if (this.props.location.key !== prevProps.location.key) {
.listCommunities({ if (this.props.history.action !== "POP") {
type_: defaultListingType, this.searchInput.current?.select();
sort: defaultSortType, }
limit: fetchLimit, }
}) }
.then(res => {
if (res.state === "success") {
this.setState({
communitySearchOptions:
res.data.communities.map(communityToChoice),
});
}
}),
];
const { communityId, creatorId } = this.props; fetchDefaultCommunitiesToken?: symbol;
async fetchDefaultCommunities({
communityId,
}: Pick<SearchRouteProps, "communityId">) {
const token = (this.fetchDefaultCommunitiesToken = Symbol());
this.setState({
searchCommunitiesLoading: true,
});
if (communityId) { const res = await HttpService.client.listCommunities({
promises.push( type_: defaultListingType,
HttpService.client.getCommunity({ id: communityId }).then(res => { sort: defaultSortType,
if (res.state === "success") { limit: fetchLimit,
this.setState(prev => { });
prev.communitySearchOptions.unshift(
communityToChoice(res.data.community_view),
);
return prev; 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);
if (creatorId) {
promises.push(
HttpService.client
.getPersonDetails({
person_id: creatorId,
})
.then(res => {
if (res.state === "success") {
this.setState(prev => {
prev.creatorSearchOptions.push(
personToChoice(res.data.person_view),
);
});
}
}),
);
}
if (this.state.searchText) {
promises.push(this.search());
}
await Promise.all(promises);
this.setState({ this.setState({
searchCommunitiesLoading: false, communitySearchOptions: retainSelected
searchCreatorLoading: false, ? [retainSelected, ...choices]
: choices,
}); });
} }
this.setState({
searchCommunitiesLoading: false,
});
}
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;
});
}
}
}
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,
});
if (token !== this.fetchSelectedCreatorToken) {
return;
}
if (res.state === "success" && needsSelectedCreator()) {
this.setState(prev => {
prev.creatorSearchOptions.push(personToChoice(res.data.person_view));
});
}
this.setState({ searchCreatorLoading: false });
}
async fetchAll(props: SearchRouteProps) {
await Promise.all([
this.fetchDefaultCommunities(props),
this.fetchSelectedCommunity(props),
this.fetchSelectedCreator(props),
this.search(props),
]);
} }
static async fetchInitialData({ static async fetchInitialData({
@ -551,13 +607,15 @@ export class Search extends Component<SearchRouteProps, SearchState> {
onSubmit={linkEvent(this, this.handleSearchSubmit)} onSubmit={linkEvent(this, this.handleSearchSubmit)}
> >
<div className="col-auto flex-grow-1 flex-sm-grow-0"> <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 <input
key={this.context.router.history.location.key}
type="text" type="text"
className="form-control me-2 mb-2 col-sm-8" className="form-control me-2 mb-2 col-sm-8"
value={this.state.searchText} defaultValue={this.props.q ?? ""}
placeholder={`${I18NextService.i18n.t("search")}...`} placeholder={`${I18NextService.i18n.t("search")}...`}
aria-label={I18NextService.i18n.t("search")} aria-label={I18NextService.i18n.t("search")}
onInput={linkEvent(this, this.handleQChange)}
required required
minLength={1} minLength={1}
ref={this.searchInput} ref={this.searchInput}
@ -984,34 +1042,39 @@ export class Search extends Component<SearchRouteProps, SearchState> {
return resObjCount + searchCount; return resObjCount + searchCount;
} }
async search() { searchToken?: symbol;
const { searchText: q } = this.state; async search(props: SearchRouteProps) {
const { communityId, creatorId, type, sort, listingType, page } = const token = (this.searchToken = Symbol());
this.props; const { q, communityId, creatorId, type, sort, listingType, page } = props;
if (q) { if (q) {
this.setState({ searchRes: LOADING_REQUEST }); this.setState({ searchRes: LOADING_REQUEST });
this.setState({ const searchRes = await HttpService.client.search({
searchRes: await HttpService.client.search({ q,
q, community_id: communityId ?? undefined,
community_id: communityId ?? undefined, creator_id: creatorId ?? undefined,
creator_id: creatorId ?? undefined, type_: type,
type_: type, sort,
sort, listing_type: listingType,
listing_type: listingType, page,
page, limit: fetchLimit,
limit: fetchLimit,
}),
}); });
if (token !== this.searchToken) {
return;
}
this.setState({ searchRes });
if (myAuth()) { if (myAuth()) {
this.setState({ resolveObjectRes: LOADING_REQUEST }); this.setState({ resolveObjectRes: LOADING_REQUEST });
this.setState({ const resolveObjectRes = await HttpService.client.resolveObject({
resolveObjectRes: await HttpService.client.resolveObject({ q,
q,
}),
}); });
if (token === this.searchToken) {
this.setState({ resolveObjectRes });
}
} }
} else {
this.setState({ searchRes: EMPTY_REQUEST });
} }
} }
@ -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) { handleSortChange(sort: SortType) {
this.updateUrl({ sort, page: 1 }); this.updateUrl({ sort, page: 1, q: this.getQ() });
} }
handleTypeChange(i: Search, event: any) { handleTypeChange(i: Search, event: any) {
@ -1063,6 +1130,7 @@ export class Search extends Component<SearchRouteProps, SearchState> {
i.updateUrl({ i.updateUrl({
type, type,
page: 1, page: 1,
q: i.getQ(),
}); });
} }
@ -1074,20 +1142,23 @@ export class Search extends Component<SearchRouteProps, SearchState> {
this.updateUrl({ this.updateUrl({
listingType, listingType,
page: 1, page: 1,
q: this.getQ(),
}); });
} }
handleCommunityFilterChange({ value }: Choice) { handleCommunityFilterChange({ value }: Choice) {
this.updateUrl({ this.updateUrl({
communityId: getIdFromString(value) ?? 0, communityId: getIdFromString(value),
page: 1, page: 1,
q: this.getQ(),
}); });
} }
handleCreatorFilterChange({ value }: Choice) { handleCreatorFilterChange({ value }: Choice) {
this.updateUrl({ this.updateUrl({
creatorId: getIdFromString(value) ?? 0, creatorId: getIdFromString(value),
page: 1, page: 1,
q: this.getQ(),
}); });
} }
@ -1095,44 +1166,25 @@ export class Search extends Component<SearchRouteProps, SearchState> {
event.preventDefault(); event.preventDefault();
i.updateUrl({ i.updateUrl({
q: i.state.searchText, q: i.getQ(),
page: 1, page: 1,
}); });
} }
handleQChange(i: Search, event: any) { async updateUrl(props: Partial<SearchProps>) {
i.setState({ searchText: event.target.value }); const { q, type, listingType, sort, communityId, creatorId, page } = {
} ...this.props,
...props,
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;
const queryParams: QueryParams<SearchProps> = { const queryParams: QueryParams<SearchProps> = {
q: query, q,
type: type ?? urlType, type: type,
listingType: listingType ?? urlListingType, listingType: listingType,
communityId: getUpdatedSearchId(communityId, urlCommunityId), communityId: communityId?.toString(),
creatorId: getUpdatedSearchId(creatorId, urlCreatorId), creatorId: creatorId?.toString(),
page: (page ?? urlPage).toString(), page: page?.toString(),
sort: sort ?? urlSort, sort: sort,
}; };
this.props.history.push(`/search${getQueryString(queryParams)}`); this.props.history.push(`/search${getQueryString(queryParams)}`);

View file

@ -53,7 +53,11 @@ import {
CreatePost, CreatePost,
getCreatePostQueryParams, getCreatePostQueryParams,
} from "./components/post/create-post"; } from "./components/post/create-post";
import { Post, PostFetchConfig } from "./components/post/post"; import {
Post,
PostFetchConfig,
getPostQueryParams,
} from "./components/post/post";
import { import {
CreatePrivateMessage, CreatePrivateMessage,
CreatePrivateMessageFetchConfig, CreatePrivateMessageFetchConfig,
@ -87,6 +91,7 @@ export interface IRoutePropsWithFetch<
component: Inferno.ComponentClass< component: Inferno.ComponentClass<
RouteComponentProps<PathPropsT> & QueryPropsT RouteComponentProps<PathPropsT> & QueryPropsT
>; >;
mountedSameRouteNavKey?: string;
} }
export const routes: IRoutePropsWithFetch<RouteData, any, any>[] = [ export const routes: IRoutePropsWithFetch<RouteData, any, any>[] = [
@ -96,6 +101,7 @@ export const routes: IRoutePropsWithFetch<RouteData, any, any>[] = [
fetchInitialData: Home.fetchInitialData, fetchInitialData: Home.fetchInitialData,
exact: true, exact: true,
getQueryParams: getHomeQueryParams, getQueryParams: getHomeQueryParams,
mountedSameRouteNavKey: "home",
} as HomeFetchConfig, } as HomeFetchConfig,
{ {
path: `/login`, path: `/login`,
@ -130,28 +136,38 @@ export const routes: IRoutePropsWithFetch<RouteData, any, any>[] = [
component: Communities, component: Communities,
fetchInitialData: Communities.fetchInitialData, fetchInitialData: Communities.fetchInitialData,
getQueryParams: getCommunitiesQueryParams, getQueryParams: getCommunitiesQueryParams,
mountedSameRouteNavKey: "communities",
} as CommunitiesFetchConfig, } 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, component: Post,
fetchInitialData: Post.fetchInitialData, fetchInitialData: Post.fetchInitialData,
getQueryParams: getPostQueryParams,
mountedSameRouteNavKey: "post",
} as PostFetchConfig, } as PostFetchConfig,
{ {
path: `/comment/:comment_id`, path: `/comment/:comment_id`,
component: Post, component: Post,
fetchInitialData: Post.fetchInitialData, fetchInitialData: Post.fetchInitialData,
getQueryParams: getPostQueryParams,
mountedSameRouteNavKey: "post",
} as PostFetchConfig, } as PostFetchConfig,
{ {
path: `/c/:name`, path: `/c/:name`,
component: Community, component: Community,
fetchInitialData: Community.fetchInitialData, fetchInitialData: Community.fetchInitialData,
getQueryParams: getCommunityQueryParams, getQueryParams: getCommunityQueryParams,
mountedSameRouteNavKey: "community",
} as CommunityFetchConfig, } as CommunityFetchConfig,
{ {
path: `/u/:username`, path: `/u/:username`,
component: Profile, component: Profile,
fetchInitialData: Profile.fetchInitialData, fetchInitialData: Profile.fetchInitialData,
getQueryParams: getProfileQueryParams, getQueryParams: getProfileQueryParams,
mountedSameRouteNavKey: "profile",
} as ProfileFetchConfig, } as ProfileFetchConfig,
{ {
path: `/inbox`, path: `/inbox`,
@ -164,16 +180,11 @@ export const routes: IRoutePropsWithFetch<RouteData, any, any>[] = [
fetchInitialData: Settings.fetchInitialData, fetchInitialData: Settings.fetchInitialData,
} as SettingsFetchConfig, } as SettingsFetchConfig,
{ {
path: `/modlog/:communityId`, path: `/modlog/:communityId?`,
component: Modlog,
fetchInitialData: Modlog.fetchInitialData,
getQueryParams: getModlogQueryParams,
} as ModlogFetchConfig,
{
path: `/modlog`,
component: Modlog, component: Modlog,
fetchInitialData: Modlog.fetchInitialData, fetchInitialData: Modlog.fetchInitialData,
getQueryParams: getModlogQueryParams, getQueryParams: getModlogQueryParams,
mountedSameRouteNavKey: "modlog",
} as ModlogFetchConfig, } as ModlogFetchConfig,
{ path: `/setup`, component: Setup }, { path: `/setup`, component: Setup },
{ {
@ -196,6 +207,7 @@ export const routes: IRoutePropsWithFetch<RouteData, any, any>[] = [
component: Search, component: Search,
fetchInitialData: Search.fetchInitialData, fetchInitialData: Search.fetchInitialData,
getQueryParams: getSearchQueryParams, getQueryParams: getSearchQueryParams,
mountedSameRouteNavKey: "search",
} as SearchFetchConfig, } as SearchFetchConfig,
{ {
path: `/password_change/:token`, path: `/password_change/:token`,

View file

@ -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();
}

View file

@ -31,7 +31,6 @@ import getDataTypeString from "./get-data-type-string";
import getDepthFromComment from "./get-depth-from-comment"; import getDepthFromComment from "./get-depth-from-comment";
import getIdFromProps from "./get-id-from-props"; import getIdFromProps from "./get-id-from-props";
import getRecipientIdFromProps from "./get-recipient-id-from-props"; import getRecipientIdFromProps from "./get-recipient-id-from-props";
import getUpdatedSearchId from "./get-updated-search-id";
import initializeSite from "./initialize-site"; import initializeSite from "./initialize-site";
import insertCommentIntoTree from "./insert-comment-into-tree"; import insertCommentIntoTree from "./insert-comment-into-tree";
import isAuthPath from "./is-auth-path"; import isAuthPath from "./is-auth-path";
@ -89,7 +88,6 @@ export {
getDepthFromComment, getDepthFromComment,
getIdFromProps, getIdFromProps,
getRecipientIdFromProps, getRecipientIdFromProps,
getUpdatedSearchId,
initializeSite, initializeSite,
insertCommentIntoTree, insertCommentIntoTree,
isAuthPath, isAuthPath,

View 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"
);
}

View file

@ -1,3 +1,4 @@
import bareRoutePush from "./bare-route-push";
import capitalizeFirstLetter from "./capitalize-first-letter"; import capitalizeFirstLetter from "./capitalize-first-letter";
import debounce from "./debounce"; import debounce from "./debounce";
import editListImmutable from "./edit-list-immutable"; import editListImmutable from "./edit-list-immutable";
@ -27,6 +28,7 @@ import dedupByProperty from "./dedup-by-property";
import getApubName from "./apub-name"; import getApubName from "./apub-name";
export { export {
bareRoutePush,
cakeDate, cakeDate,
capitalizeFirstLetter, capitalizeFirstLetter,
debounce, debounce,