Rework query parsing (#2396)

* Pass parsed query params as props to components

* Pass parsed query params to fetchInitialData

* Pass router Match to fetchInitialData

* Cast individual routes to their concrete types

Adds an IRoutePropsWithFetch definition for routes with getQueryParams
or fetchInitialData to cause compiler errors when the types no longer
match.

* Don't double decode query parameters.

Problem: A search for "%ab" produces a url with "%25ab". Refreshing
the page results in URLSearchParams turning "%25ab" back into "%ab".
decodeURIComponent() then complains about "%ab" being malformed.

This removes decodeURIComponent() calls for query parameters and
composes all query strings with getQueryString(), which now uses
URLSearchParams. Query parsing already goes through getQueryParams()
which also uses URLSearchParams.

* Fix for PictrsImage when src also has query params

* Small getQueryParams cleanup
This commit is contained in:
matc-pub 2024-03-27 14:25:59 +01:00 committed by GitHub
parent 579aea40d0
commit 70e382b3d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 695 additions and 368 deletions

View file

@ -3,6 +3,7 @@ import { getHttpBaseInternal } from "@utils/env";
import { ErrorPageData } from "@utils/types"; import { ErrorPageData } from "@utils/types";
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import { StaticRouter, matchPath } from "inferno-router"; import { StaticRouter, matchPath } from "inferno-router";
import { Match } from "inferno-router/dist/Route";
import { renderToString } from "inferno-server"; import { renderToString } from "inferno-server";
import { GetSiteResponse, LemmyHttp } from "lemmy-js-client"; import { GetSiteResponse, LemmyHttp } from "lemmy-js-client";
import { App } from "../../shared/components/app/app"; import { App } from "../../shared/components/app/app";
@ -25,6 +26,8 @@ import {
LanguageService, LanguageService,
UserService, UserService,
} from "../../shared/services/"; } from "../../shared/services/";
import { parsePath } from "history";
import { getQueryString } from "@utils/helpers";
export default async (req: Request, res: Response) => { export default async (req: Request, res: Response) => {
try { try {
@ -40,7 +43,10 @@ export default async (req: Request, res: Response) => {
.sort((a, b) => b.q - a.q) .sort((a, b) => b.q - a.q)
.map(x => (x.lang === "*" ? "en" : x.lang)) ?? []; .map(x => (x.lang === "*" ? "en" : x.lang)) ?? [];
const activeRoute = routes.find(route => matchPath(req.path, route)); let match: Match<any> | null | undefined;
const activeRoute = routes.find(
route => (match = matchPath(req.path, route)),
);
const headers = setForwardedHeaders(req.headers); const headers = setForwardedHeaders(req.headers);
const auth = getJwtCookie(req.headers); const auth = getJwtCookie(req.headers);
@ -49,7 +55,7 @@ export default async (req: Request, res: Response) => {
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),
); );
const { path, url, query } = req; const { path, url } = req;
// Get site data first // Get site data first
// This bypasses errors, so that the client can hit the error on its own, // This bypasses errors, so that the client can hit the error on its own,
@ -71,7 +77,7 @@ export default async (req: Request, res: Response) => {
} }
if (!auth && isAuthPath(path)) { if (!auth && isAuthPath(path)) {
return res.redirect(`/login?prev=${encodeURIComponent(url)}`); return res.redirect(`/login${getQueryString({ prev: url })}`);
} }
if (try_site.state === "success") { if (try_site.state === "success") {
@ -83,10 +89,12 @@ export default async (req: Request, res: Response) => {
return res.redirect("/setup"); return res.redirect("/setup");
} }
if (site && activeRoute?.fetchInitialData) { if (site && activeRoute?.fetchInitialData && match) {
const initialFetchReq: InitialFetchRequest = { const { search } = parsePath(url);
const initialFetchReq: InitialFetchRequest<Record<string, any>> = {
path, path,
query, query: activeRoute.getQueryParams?.(search, site) ?? {},
match,
site, site,
headers, headers,
}; };

View file

@ -49,7 +49,12 @@ export class App extends Component<any, any> {
<div className="mt-4 p-0 fl-1"> <div className="mt-4 p-0 fl-1">
<Switch> <Switch>
{routes.map( {routes.map(
({ path, component: RouteComponent, fetchInitialData }) => ( ({
path,
component: RouteComponent,
fetchInitialData,
getQueryParams,
}) => (
<Route <Route
key={path} key={path}
path={path} path={path}
@ -59,20 +64,34 @@ export class App extends Component<any, any> {
FirstLoadService.falsify(); 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 ( return (
<ErrorGuard> <ErrorGuard>
<div tabIndex={-1}> <div tabIndex={-1}>
{RouteComponent && {RouteComponent &&
(isAuthPath(path ?? "") ? ( (isAuthPath(path ?? "") ? (
<AuthGuard {...routeProps}> <AuthGuard {...routeProps}>
<RouteComponent {...routeProps} /> <RouteComponent {...queryProps} />
</AuthGuard> </AuthGuard>
) : isAnonymousPath(path ?? "") ? ( ) : isAnonymousPath(path ?? "") ? (
<AnonymousGuard> <AnonymousGuard>
<RouteComponent {...routeProps} /> <RouteComponent {...queryProps} />
</AnonymousGuard> </AnonymousGuard>
) : ( ) : (
<RouteComponent {...routeProps} /> <RouteComponent {...queryProps} />
))} ))}
</div> </div>
</ErrorGuard> </ErrorGuard>

View file

@ -2,6 +2,7 @@ import { Component } from "inferno";
import { RouteComponentProps } from "inferno-router/dist/Route"; 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";
interface AuthGuardState { interface AuthGuardState {
hasRedirected: boolean; hasRedirected: boolean;
@ -26,7 +27,7 @@ class AuthGuard extends Component<
if (!UserService.Instance.myUserInfo) { if (!UserService.Instance.myUserInfo) {
const { pathname, search } = this.props.location; const { pathname, search } = this.props.location;
this.context.router.history.replace( this.context.router.history.replace(
`/login?prev=${encodeURIComponent(pathname + search)}`, `/login${getQueryString({ prev: pathname + search })}`,
); );
} else { } else {
this.setState({ hasRedirected: true }); this.setState({ hasRedirected: true });

View file

@ -68,28 +68,31 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
// sample url: // sample url:
// http://localhost:8535/pictrs/image/file.png?thumbnail=256&format=jpg // http://localhost:8535/pictrs/image/file.png?thumbnail=256&format=jpg
const split = this.props.src.split("/pictrs/image/"); let url: URL | undefined;
try {
// If theres not multiple, then its not a pictrs image url = new URL(this.props.src);
if (split.length === 1) { } catch {
return this.props.src; return this.props.src;
} }
const host = split[0]; // If theres no match, then its not a pictrs image
const path = split[1]; if (!url.pathname.includes("/pictrs/image/")) {
return this.props.src;
const params = { format };
if (this.props.thumbnail) {
params["thumbnail"] = thumbnailSize;
} else if (this.props.icon) {
params["thumbnail"] = iconThumbnailSize;
} }
const paramsStr = new URLSearchParams(params).toString(); // Keeps original search params. Could probably do `url.search = ""` here.
const out = `${host}/pictrs/image/${path}?${paramsStr}`;
return out; url.searchParams.set("format", format);
if (this.props.thumbnail) {
url.searchParams.set("thumbnail", thumbnailSize.toString());
} else if (this.props.icon) {
url.searchParams.set("thumbnail", iconThumbnailSize.toString());
} else {
url.searchParams.delete("thumbnail");
}
return url.href;
} }
alt(): string { alt(): string {

View file

@ -1,4 +1,4 @@
import { 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, linkEvent } from "inferno";
@ -134,8 +134,8 @@ function submitRemoteFollow(
instanceText = `http${VERSION !== "dev" ? "s" : ""}://${instanceText}`; instanceText = `http${VERSION !== "dev" ? "s" : ""}://${instanceText}`;
} }
window.location.href = `${instanceText}/activitypub/externalInteraction?uri=${encodeURIComponent( window.location.href = `${instanceText}/activitypub/externalInteraction${getQueryString(
communityActorId, { uri: communityActorId },
)}`; )}`;
} }

View file

@ -36,6 +36,8 @@ import { CommunityLink } from "./community-link";
import { communityLimit } from "../../config"; import { communityLimit } from "../../config";
import { SubscribeButton } from "../common/subscribe-button"; import { SubscribeButton } from "../common/subscribe-button";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes";
type CommunitiesData = RouteDataResponse<{ type CommunitiesData = RouteDataResponse<{
listCommunitiesResponse: ListCommunitiesResponse; listCommunitiesResponse: ListCommunitiesResponse;
@ -62,15 +64,30 @@ function getSortTypeFromQuery(type?: string): SortType {
return type ? (type as SortType) : "TopMonth"; return type ? (type as SortType) : "TopMonth";
} }
function getCommunitiesQueryParams() { export function getCommunitiesQueryParams(source?: string): CommunitiesProps {
return getQueryParams<CommunitiesProps>({ return getQueryParams<CommunitiesProps>(
{
listingType: getListingTypeFromQuery, listingType: getListingTypeFromQuery,
sort: getSortTypeFromQuery, sort: getSortTypeFromQuery,
page: getPageFromString, page: getPageFromString,
}); },
source,
);
} }
export class Communities extends Component<any, CommunitiesState> { type CommunitiesPathProps = Record<string, never>;
type CommunitiesRouteProps = RouteComponentProps<CommunitiesPathProps> &
CommunitiesProps;
export type CommunitiesFetchConfig = IRoutePropsWithFetch<
CommunitiesData,
CommunitiesPathProps,
CommunitiesProps
>;
export class Communities extends Component<
CommunitiesRouteProps,
CommunitiesState
> {
private isoData = setIsoData<CommunitiesData>(this.context); private isoData = setIsoData<CommunitiesData>(this.context);
state: CommunitiesState = { state: CommunitiesState = {
listCommunitiesResponse: EMPTY_REQUEST, listCommunitiesResponse: EMPTY_REQUEST,
@ -79,7 +96,7 @@ export class Communities extends Component<any, CommunitiesState> {
isIsomorphic: false, isIsomorphic: false,
}; };
constructor(props: any, context: any) { constructor(props: CommunitiesRouteProps, context: any) {
super(props, context); super(props, context);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.handleSortChange = this.handleSortChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this);
@ -118,7 +135,7 @@ export class Communities extends Component<any, CommunitiesState> {
</h5> </h5>
); );
case "success": { case "success": {
const { listingType, sort, page } = getCommunitiesQueryParams(); const { listingType, sort, page } = this.props;
return ( return (
<div> <div>
<h1 className="h4 mb-4"> <h1 className="h4 mb-4">
@ -268,7 +285,7 @@ export class Communities extends Component<any, CommunitiesState> {
listingType: urlListingType, listingType: urlListingType,
sort: urlSort, sort: urlSort,
page: urlPage, page: urlPage,
} = getCommunitiesQueryParams(); } = this.props;
const queryParams: QueryParams<CommunitiesProps> = { const queryParams: QueryParams<CommunitiesProps> = {
listingType: listingType ?? urlListingType, listingType: listingType ?? urlListingType,
@ -302,10 +319,10 @@ export class Communities extends Component<any, CommunitiesState> {
handleSearchSubmit(i: Communities, event: any) { handleSearchSubmit(i: Communities, event: any) {
event.preventDefault(); event.preventDefault();
const searchParamEncoded = encodeURIComponent(i.state.searchText); const searchParamEncoded = i.state.searchText;
const { listingType } = getCommunitiesQueryParams(); const { listingType } = i.props;
i.context.router.history.push( i.context.router.history.push(
`/search?q=${searchParamEncoded}&type=Communities&listingType=${listingType}`, `/search${getQueryString({ q: searchParamEncoded, type: "Communities", listingType })}`,
); );
} }
@ -313,16 +330,17 @@ export class Communities extends Component<any, CommunitiesState> {
headers, headers,
query: { listingType, sort, page }, query: { listingType, sort, page },
}: InitialFetchRequest< }: InitialFetchRequest<
QueryParams<CommunitiesProps> CommunitiesPathProps,
CommunitiesProps
>): Promise<CommunitiesData> { >): Promise<CommunitiesData> {
const client = wrapClient( const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),
); );
const listCommunitiesForm: ListCommunities = { const listCommunitiesForm: ListCommunities = {
type_: getListingTypeFromQuery(listingType), type_: listingType,
sort: getSortTypeFromQuery(sort), sort,
limit: communityLimit, limit: communityLimit,
page: getPageFromString(page), page,
}; };
return { return {
@ -346,7 +364,7 @@ export class Communities extends Component<any, CommunitiesState> {
async refetch() { async refetch() {
this.setState({ listCommunitiesResponse: LOADING_REQUEST }); this.setState({ listCommunitiesResponse: LOADING_REQUEST });
const { listingType, sort, page } = getCommunitiesQueryParams(); const { listingType, sort, page } = this.props;
this.setState({ this.setState({
listCommunitiesResponse: await HttpService.client.listCommunities({ listCommunitiesResponse: await HttpService.client.listCommunities({

View file

@ -101,6 +101,7 @@ import { CommunityLink } from "./community-link";
import { PaginatorCursor } from "../common/paginator-cursor"; import { PaginatorCursor } from "../common/paginator-cursor";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { Sidebar } from "./sidebar"; import { Sidebar } from "./sidebar";
import { IRoutePropsWithFetch } from "../../routes";
type CommunityData = RouteDataResponse<{ type CommunityData = RouteDataResponse<{
communityRes: GetCommunityResponse; communityRes: GetCommunityResponse;
@ -124,12 +125,26 @@ interface CommunityProps {
pageCursor?: PaginationCursor; pageCursor?: PaginationCursor;
} }
function getCommunityQueryParams() { type Fallbacks = { sort: SortType };
return getQueryParams<CommunityProps>({
export function getCommunityQueryParams(
source: string | undefined,
siteRes: GetSiteResponse,
) {
const myUserInfo = siteRes.my_user ?? UserService.Instance.myUserInfo;
const local_user = myUserInfo?.local_user_view.local_user;
const local_site = siteRes.site_view.local_site;
return getQueryParams<CommunityProps, Fallbacks>(
{
dataType: getDataTypeFromQuery, dataType: getDataTypeFromQuery,
pageCursor: cursor => cursor, pageCursor: (cursor?: string) => cursor,
sort: getSortTypeFromQuery, sort: getSortTypeFromQuery,
}); },
source,
{
sort: local_user?.default_sort_type ?? local_site.default_sort_type,
},
);
} }
function getDataTypeFromQuery(type?: string): DataType { function getDataTypeFromQuery(type?: string): DataType {
@ -144,10 +159,16 @@ function getSortTypeFromQuery(type?: string): SortType {
return type ? (type as SortType) : mySortType ?? "Active"; return type ? (type as SortType) : mySortType ?? "Active";
} }
export class Community extends Component< type CommunityPathProps = { name: string };
RouteComponentProps<{ name: string }>, type CommunityRouteProps = RouteComponentProps<CommunityPathProps> &
State CommunityProps;
> { export type CommunityFetchConfig = IRoutePropsWithFetch<
CommunityData,
CommunityPathProps,
CommunityProps
>;
export class Community extends Component<CommunityRouteProps, State> {
private isoData = setIsoData<CommunityData>(this.context); private isoData = setIsoData<CommunityData>(this.context);
state: State = { state: State = {
communityRes: EMPTY_REQUEST, communityRes: EMPTY_REQUEST,
@ -159,7 +180,7 @@ export class Community extends Component<
isIsomorphic: false, isIsomorphic: false,
}; };
private readonly mainContentRef: RefObject<HTMLElement>; private readonly mainContentRef: RefObject<HTMLElement>;
constructor(props: RouteComponentProps<{ name: string }>, context: any) { constructor(props: CommunityRouteProps, context: any) {
super(props, context); super(props, context);
this.handleSortChange = this.handleSortChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this);
@ -234,25 +255,22 @@ export class Community extends Component<
static async fetchInitialData({ static async fetchInitialData({
headers, headers,
path, query: { dataType, pageCursor, sort },
query: { dataType: urlDataType, pageCursor, sort: urlSort }, match: {
}: InitialFetchRequest<QueryParams<CommunityProps>>): Promise< params: { name: communityName },
Promise<CommunityData> },
> { }: InitialFetchRequest<
CommunityPathProps,
CommunityProps
>): Promise<CommunityData> {
const client = wrapClient( const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),
); );
const pathSplit = path.split("/");
const communityName = pathSplit[2];
const communityForm: GetCommunity = { const communityForm: GetCommunity = {
name: communityName, name: communityName,
}; };
const dataType = getDataTypeFromQuery(urlDataType);
const sort = getSortTypeFromQuery(urlSort);
let postsFetch: Promise<RequestState<GetPostsResponse>> = let postsFetch: Promise<RequestState<GetPostsResponse>> =
Promise.resolve(EMPTY_REQUEST); Promise.resolve(EMPTY_REQUEST);
let commentsFetch: Promise<RequestState<GetCommentsResponse>> = let commentsFetch: Promise<RequestState<GetCommentsResponse>> =
@ -411,7 +429,7 @@ export class Community extends Component<
} }
listings(communityRes: GetCommunityResponse) { listings(communityRes: GetCommunityResponse) {
const { dataType } = getCommunityQueryParams(); const { dataType } = this.props;
const { site_res } = this.isoData; const { site_res } = this.isoData;
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
@ -534,7 +552,7 @@ export class Community extends Component<
// let communityRss = this.state.communityRes.map(r => // let communityRss = this.state.communityRes.map(r =>
// communityRSSUrl(r.community_view.community.actor_id, this.state.sort) // communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
// ); // );
const { dataType, sort } = getCommunityQueryParams(); const { dataType, sort } = this.props;
const communityRss = res const communityRss = res
? communityRSSUrl(res.community_view.community.actor_id, sort) ? communityRSSUrl(res.community_view.community.actor_id, sort)
: undefined; : undefined;
@ -592,7 +610,7 @@ export class Community extends Component<
} }
async updateUrl({ dataType, pageCursor, sort }: Partial<CommunityProps>) { async updateUrl({ dataType, pageCursor, sort }: Partial<CommunityProps>) {
const { dataType: urlDataType, sort: urlSort } = getCommunityQueryParams(); const { dataType: urlDataType, sort: urlSort } = this.props;
const queryParams: QueryParams<CommunityProps> = { const queryParams: QueryParams<CommunityProps> = {
dataType: getDataTypeString(dataType ?? urlDataType), dataType: getDataTypeString(dataType ?? urlDataType),
@ -608,7 +626,7 @@ export class Community extends Component<
} }
async fetchData() { async fetchData() {
const { dataType, pageCursor, sort } = getCommunityQueryParams(); const { dataType, pageCursor, sort } = this.props;
const { name } = this.props.match.params; const { name } = this.props.match.params;
if (dataType === DataType.Post) { if (dataType === DataType.Post) {

View file

@ -1,4 +1,4 @@
import { hostname } from "@utils/helpers"; import { getQueryString, hostname } from "@utils/helpers";
import { amAdmin, amMod, amTopMod } from "@utils/roles"; import { amAdmin, amMod, amTopMod } from "@utils/roles";
import { Component, InfernoNode, linkEvent } from "inferno"; import { Component, InfernoNode, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
@ -287,7 +287,10 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
className={`btn btn-secondary d-block mb-2 w-100 ${ className={`btn btn-secondary d-block mb-2 w-100 ${
cv.community.deleted || cv.community.removed ? "no-click" : "" cv.community.deleted || cv.community.removed ? "no-click" : ""
}`} }`}
to={`/create_post?communityId=${cv.community.id}`} to={
"/create_post" +
getQueryString({ communityId: cv.community.id.toString() })
}
> >
{I18NextService.i18n.t("create_a_post")} {I18NextService.i18n.t("create_a_post")}
</Link> </Link>

View file

@ -34,6 +34,8 @@ import RateLimitForm from "./rate-limit-form";
import { SiteForm } from "./site-form"; import { SiteForm } from "./site-form";
import { TaglineForm } from "./tagline-form"; import { TaglineForm } from "./tagline-form";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes";
type AdminSettingsData = RouteDataResponse<{ type AdminSettingsData = RouteDataResponse<{
bannedRes: BannedPersonsResponse; bannedRes: BannedPersonsResponse;
@ -52,7 +54,18 @@ interface AdminSettingsState {
isIsomorphic: boolean; isIsomorphic: boolean;
} }
export class AdminSettings extends Component<any, AdminSettingsState> { type AdminSettingsRouteProps = RouteComponentProps<Record<string, never>> &
Record<string, never>;
export type AdminSettingsFetchConfig = IRoutePropsWithFetch<
AdminSettingsData,
Record<string, never>,
Record<string, never>
>;
export class AdminSettings extends Component<
AdminSettingsRouteProps,
AdminSettingsState
> {
private isoData = setIsoData<AdminSettingsData>(this.context); private isoData = setIsoData<AdminSettingsData>(this.context);
state: AdminSettingsState = { state: AdminSettingsState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,

View file

@ -100,6 +100,8 @@ import { PostListings } from "../post/post-listings";
import { SiteSidebar } from "./site-sidebar"; import { SiteSidebar } from "./site-sidebar";
import { PaginatorCursor } from "../common/paginator-cursor"; import { PaginatorCursor } from "../common/paginator-cursor";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes";
interface HomeState { interface HomeState {
postsRes: RequestState<GetPostsResponse>; postsRes: RequestState<GetPostsResponse>;
@ -129,23 +131,22 @@ type HomeData = RouteDataResponse<{
trendingCommunitiesRes: ListCommunitiesResponse; trendingCommunitiesRes: ListCommunitiesResponse;
}>; }>;
function getRss(listingType: ListingType) { function getRss(listingType: ListingType, sort: SortType) {
const { sort } = getHomeQueryParams();
let rss: string | undefined = undefined; let rss: string | undefined = undefined;
const queryString = getQueryString({ sort });
switch (listingType) { switch (listingType) {
case "All": { case "All": {
rss = `/feeds/all.xml?sort=${sort}`; rss = "/feeds/all.xml" + queryString;
break; break;
} }
case "Local": { case "Local": {
rss = `/feeds/local.xml?sort=${sort}`; rss = "/feeds/local.xml" + queryString;
break; break;
} }
case "Subscribed": { case "Subscribed": {
const auth = myAuth(); const auth = myAuth();
rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined; rss = auth ? `/feeds/front/${auth}.xml${queryString}` : undefined;
break; break;
} }
} }
@ -167,31 +168,46 @@ function getDataTypeFromQuery(type?: string): DataType {
} }
function getListingTypeFromQuery( function getListingTypeFromQuery(
type?: string, type: string | undefined,
myUserInfo = UserService.Instance.myUserInfo, fallback: ListingType,
): ListingType | undefined { ): ListingType {
const myListingType = return type ? (type as ListingType) : fallback;
myUserInfo?.local_user_view?.local_user?.default_listing_type;
return type ? (type as ListingType) : myListingType;
} }
function getSortTypeFromQuery( function getSortTypeFromQuery(
type?: string, type: string | undefined,
myUserInfo = UserService.Instance.myUserInfo, fallback: SortType,
): SortType { ): SortType {
const mySortType = myUserInfo?.local_user_view?.local_user?.default_sort_type; return type ? (type as SortType) : fallback;
return (type ? (type as SortType) : mySortType) ?? "Active";
} }
function getHomeQueryParams() { type Fallbacks = {
return getQueryParams<HomeProps>({ sort: SortType;
listingType: ListingType;
};
export function getHomeQueryParams(
source: string | undefined,
siteRes: GetSiteResponse,
): HomeProps {
const myUserInfo = siteRes.my_user ?? UserService.Instance.myUserInfo;
const local_user = myUserInfo?.local_user_view.local_user;
const local_site = siteRes.site_view.local_site;
return getQueryParams<HomeProps, Fallbacks>(
{
sort: getSortTypeFromQuery, sort: getSortTypeFromQuery,
listingType: getListingTypeFromQuery, listingType: getListingTypeFromQuery,
pageCursor: cursor => cursor, pageCursor: (cursor?: string) => cursor,
dataType: getDataTypeFromQuery, dataType: getDataTypeFromQuery,
}); },
source,
{
sort: local_user?.default_sort_type ?? local_site.default_sort_type,
listingType:
local_user?.default_listing_type ??
local_site.default_post_listing_type,
},
);
} }
const MobileButton = ({ const MobileButton = ({
@ -224,7 +240,15 @@ const LinkButton = ({
</Link> </Link>
); );
export class Home extends Component<any, HomeState> { type HomePathProps = Record<string, never>;
type HomeRouteProps = RouteComponentProps<HomePathProps> & HomeProps;
export type HomeFetchConfig = IRoutePropsWithFetch<
HomeData,
HomePathProps,
HomeProps
>;
export class Home extends Component<HomeRouteProps, HomeState> {
private isoData = setIsoData<HomeData>(this.context); private isoData = setIsoData<HomeData>(this.context);
state: HomeState = { state: HomeState = {
postsRes: EMPTY_REQUEST, postsRes: EMPTY_REQUEST,
@ -310,20 +334,13 @@ export class Home extends Component<any, HomeState> {
} }
static async fetchInitialData({ static async fetchInitialData({
query: { dataType: urlDataType, listingType, pageCursor, sort: urlSort }, query: { listingType, dataType, sort, pageCursor },
site,
headers, headers,
}: InitialFetchRequest<QueryParams<HomeProps>>): Promise<HomeData> { }: InitialFetchRequest<HomePathProps, HomeProps>): Promise<HomeData> {
const client = wrapClient( const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),
); );
const dataType = getDataTypeFromQuery(urlDataType);
const type_ =
getListingTypeFromQuery(listingType, site.my_user) ??
site.site_view.local_site.default_post_listing_type;
const sort = getSortTypeFromQuery(urlSort, site.my_user);
let postsFetch: Promise<RequestState<GetPostsResponse>> = let postsFetch: Promise<RequestState<GetPostsResponse>> =
Promise.resolve(EMPTY_REQUEST); Promise.resolve(EMPTY_REQUEST);
let commentsFetch: Promise<RequestState<GetCommentsResponse>> = let commentsFetch: Promise<RequestState<GetCommentsResponse>> =
@ -331,7 +348,7 @@ export class Home extends Component<any, HomeState> {
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
const getPostsForm: GetPosts = { const getPostsForm: GetPosts = {
type_, type_: listingType,
page_cursor: pageCursor, page_cursor: pageCursor,
limit: fetchLimit, limit: fetchLimit,
sort, sort,
@ -343,7 +360,7 @@ export class Home extends Component<any, HomeState> {
const getCommentsForm: GetComments = { const getCommentsForm: GetComments = {
limit: fetchLimit, limit: fetchLimit,
sort: postToCommentSortType(sort), sort: postToCommentSortType(sort),
type_, type_: listingType,
saved_only: false, saved_only: false,
}; };
@ -635,7 +652,7 @@ export class Home extends Component<any, HomeState> {
dataType: urlDataType, dataType: urlDataType,
listingType: urlListingType, listingType: urlListingType,
sort: urlSort, sort: urlSort,
} = getHomeQueryParams(); } = this.props;
const queryParams: QueryParams<HomeProps> = { const queryParams: QueryParams<HomeProps> = {
dataType: getDataTypeString(dataType ?? urlDataType), dataType: getDataTypeString(dataType ?? urlDataType),
@ -679,7 +696,7 @@ export class Home extends Component<any, HomeState> {
} }
get listings() { get listings() {
const { dataType } = getHomeQueryParams(); const { dataType } = this.props;
const siteRes = this.state.siteRes; const siteRes = this.state.siteRes;
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
@ -771,7 +788,7 @@ export class Home extends Component<any, HomeState> {
} }
get selects() { get selects() {
const { listingType, dataType, sort } = getHomeQueryParams(); const { listingType, dataType, sort } = this.props;
return ( return (
<div className="row align-items-center mb-3 g-3"> <div className="row align-items-center mb-3 g-3">
@ -799,6 +816,7 @@ export class Home extends Component<any, HomeState> {
{getRss( {getRss(
listingType ?? listingType ??
this.state.siteRes.site_view.local_site.default_post_listing_type, this.state.siteRes.site_view.local_site.default_post_listing_type,
sort,
)} )}
</div> </div>
</div> </div>
@ -817,7 +835,7 @@ export class Home extends Component<any, HomeState> {
} }
async fetchData() { async fetchData() {
const { dataType, pageCursor, listingType, sort } = getHomeQueryParams(); const { dataType, pageCursor, listingType, sort } = this.props;
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
this.setState({ postsRes: LOADING_REQUEST }); this.setState({ postsRes: LOADING_REQUEST });

View file

@ -22,6 +22,8 @@ import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import Tabs from "../common/tabs"; import Tabs from "../common/tabs";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes";
type InstancesData = RouteDataResponse<{ type InstancesData = RouteDataResponse<{
federatedInstancesResponse: GetFederatedInstancesResponse; federatedInstancesResponse: GetFederatedInstancesResponse;
@ -33,7 +35,15 @@ interface InstancesState {
isIsomorphic: boolean; isIsomorphic: boolean;
} }
export class Instances extends Component<any, InstancesState> { type InstancesRouteProps = RouteComponentProps<Record<string, never>> &
Record<string, never>;
export type InstancesFetchConfig = IRoutePropsWithFetch<
InstancesData,
Record<string, never>,
Record<string, never>
>;
export class Instances extends Component<InstancesRouteProps, InstancesState> {
private isoData = setIsoData<InstancesData>(this.context); private isoData = setIsoData<InstancesData>(this.context);
state: InstancesState = { state: InstancesState = {
instancesRes: EMPTY_REQUEST, instancesRes: EMPTY_REQUEST,

View file

@ -17,17 +17,21 @@ import { Spinner } from "../common/icon";
import PasswordInput from "../common/password-input"; import PasswordInput from "../common/password-input";
import TotpModal from "../common/totp-modal"; import TotpModal from "../common/totp-modal";
import { UnreadCounterService } from "../../services"; import { UnreadCounterService } from "../../services";
import { RouteData } from "../../interfaces";
import { IRoutePropsWithFetch } from "../../routes";
interface LoginProps { interface LoginProps {
prev?: string; prev?: string;
} }
const getLoginQueryParams = () => export function getLoginQueryParams(source?: string): LoginProps {
getQueryParams<LoginProps>({ return getQueryParams<LoginProps>(
prev(param) { {
return param ? decodeURIComponent(param) : undefined; prev: (param?: string) => param,
}, },
}); source,
);
}
interface State { interface State {
loginRes: RequestState<LoginResponse>; loginRes: RequestState<LoginResponse>;
@ -50,7 +54,7 @@ async function handleLoginSuccess(i: Login, loginRes: LoginResponse) {
refreshTheme(); refreshTheme();
} }
const { prev } = getLoginQueryParams(); const { prev } = i.props;
prev prev
? i.props.history.replace(prev) ? i.props.history.replace(prev)
@ -114,10 +118,14 @@ function handleClose2faModal(i: Login) {
i.setState({ show2faModal: false }); i.setState({ show2faModal: false });
} }
export class Login extends Component< type LoginRouteProps = RouteComponentProps<Record<string, never>> & LoginProps;
RouteComponentProps<Record<string, never>>, export type LoginFetchConfig = IRoutePropsWithFetch<
State RouteData,
> { Record<string, never>,
LoginProps
>;
export class Login extends Component<LoginRouteProps, State> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
state: State = { state: State = {

View file

@ -63,6 +63,7 @@ import { SearchableSelect } from "./common/searchable-select";
import { CommunityLink } from "./community/community-link"; 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";
type FilterType = "mod" | "user"; type FilterType = "mod" | "user";
@ -97,13 +98,17 @@ interface ModlogType {
when_: string; when_: string;
} }
const getModlogQueryParams = () => export function getModlogQueryParams(source?: string): ModlogProps {
getQueryParams<ModlogProps>({ return getQueryParams<ModlogProps>(
{
actionType: getActionFromString, actionType: getActionFromString,
modId: getIdFromString, modId: getIdFromString,
userId: getIdFromString, userId: getIdFromString,
page: getPageFromString, page: getPageFromString,
}); },
source,
);
}
interface ModlogState { interface ModlogState {
res: RequestState<GetModlogResponse>; res: RequestState<GetModlogResponse>;
@ -117,8 +122,8 @@ interface ModlogState {
interface ModlogProps { interface ModlogProps {
page: number; page: number;
userId?: number | null; userId?: number;
modId?: number | null; modId?: number;
actionType: ModlogActionType; actionType: ModlogActionType;
} }
@ -632,10 +637,15 @@ async function createNewOptions({
} }
} }
export class Modlog extends Component< type ModlogPathProps = { communityId?: string };
RouteComponentProps<{ communityId?: string }>, type ModlogRouteProps = RouteComponentProps<ModlogPathProps> & ModlogProps;
ModlogState export type ModlogFetchConfig = IRoutePropsWithFetch<
> { ModlogData,
ModlogPathProps,
ModlogProps
>;
export class Modlog extends Component<ModlogRouteProps, ModlogState> {
private isoData = setIsoData<ModlogData>(this.context); private isoData = setIsoData<ModlogData>(this.context);
state: ModlogState = { state: ModlogState = {
@ -648,10 +658,7 @@ export class Modlog extends Component<
isIsomorphic: false, isIsomorphic: false,
}; };
constructor( constructor(props: ModlogRouteProps, context: any) {
props: RouteComponentProps<{ communityId?: string }>,
context: any,
) {
super(props, context); super(props, context);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.handleUserChange = this.handleUserChange.bind(this); this.handleUserChange = this.handleUserChange.bind(this);
@ -687,7 +694,7 @@ export class Modlog extends Component<
async componentDidMount() { async componentDidMount() {
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic) {
const { modId, userId } = getModlogQueryParams(); const { modId, userId } = this.props;
const promises = [this.refetch()]; const promises = [this.refetch()];
if (userId) { if (userId) {
@ -774,7 +781,7 @@ export class Modlog extends Component<
userSearchOptions, userSearchOptions,
modSearchOptions, modSearchOptions,
} = this.state; } = this.state;
const { actionType, modId, userId } = getModlogQueryParams(); const { actionType, modId, userId } = this.props;
return ( return (
<div className="modlog container-lg"> <div className="modlog container-lg">
@ -873,7 +880,7 @@ export class Modlog extends Component<
</h5> </h5>
); );
case "success": { case "success": {
const page = getModlogQueryParams().page; const page = this.props.page;
return ( return (
<div className="table-responsive"> <div className="table-responsive">
<table id="modlog_table" className="table table-sm table-hover"> <table id="modlog_table" className="table table-sm table-hover">
@ -909,15 +916,15 @@ export class Modlog extends Component<
} }
handleUserChange(option: Choice) { handleUserChange(option: Choice) {
this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 }); this.updateUrl({ userId: getIdFromString(option.value), page: 1 });
} }
handleModChange(option: Choice) { handleModChange(option: Choice) {
this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 }); this.updateUrl({ modId: getIdFromString(option.value), page: 1 });
} }
handleSearchUsers = debounce(async (text: string) => { handleSearchUsers = debounce(async (text: string) => {
const { userId } = getModlogQueryParams(); const { userId } = this.props;
const { userSearchOptions } = this.state; const { userSearchOptions } = this.state;
this.setState({ loadingUserSearch: true }); this.setState({ loadingUserSearch: true });
@ -934,7 +941,7 @@ export class Modlog extends Component<
}); });
handleSearchMods = debounce(async (text: string) => { handleSearchMods = debounce(async (text: string) => {
const { modId } = getModlogQueryParams(); const { modId } = this.props;
const { modSearchOptions } = this.state; const { modSearchOptions } = this.state;
this.setState({ loadingModSearch: true }); this.setState({ loadingModSearch: true });
@ -956,7 +963,7 @@ export class Modlog extends Component<
actionType: urlActionType, actionType: urlActionType,
modId: urlModId, modId: urlModId,
userId: urlUserId, userId: urlUserId,
} = getModlogQueryParams(); } = this.props;
const queryParams: QueryParams<ModlogProps> = { const queryParams: QueryParams<ModlogProps> = {
page: (page ?? urlPage).toString(), page: (page ?? urlPage).toString(),
@ -977,7 +984,7 @@ export class Modlog extends Component<
} }
async refetch() { async refetch() {
const { actionType, page, modId, userId } = getModlogQueryParams(); const { actionType, page, modId, userId } = this.props;
const { communityId: urlCommunityId } = this.props.match.params; const { communityId: urlCommunityId } = this.props.match.params;
const communityId = getIdFromString(urlCommunityId); const communityId = getIdFromString(urlCommunityId);
@ -988,10 +995,10 @@ export class Modlog extends Component<
page, page,
limit: fetchLimit, limit: fetchLimit,
type_: actionType, type_: actionType,
other_person_id: userId ?? undefined, 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 ?? undefined ? modId
: undefined, : undefined,
}), }),
}); });
@ -1008,25 +1015,25 @@ export class Modlog extends Component<
static async fetchInitialData({ static async fetchInitialData({
headers, headers,
path, query: { page, userId, modId: modId_, actionType },
query: { modId: urlModId, page, userId: urlUserId, actionType }, match: {
params: { communityId: urlCommunityId },
},
site, site,
}: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> { }: InitialFetchRequest<ModlogPathProps, ModlogProps>): Promise<ModlogData> {
const client = wrapClient( const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),
); );
const pathSplit = path.split("/"); const communityId = getIdFromString(urlCommunityId);
const communityId = getIdFromString(pathSplit[2]);
const modId = !site.site_view.local_site.hide_modlog_mod_names const modId = !site.site_view.local_site.hide_modlog_mod_names
? getIdFromString(urlModId) ? modId_
: undefined; : undefined;
const userId = getIdFromString(urlUserId);
const modlogForm: GetModlog = { const modlogForm: GetModlog = {
page: getPageFromString(page), page,
limit: fetchLimit, limit: fetchLimit,
community_id: communityId, community_id: communityId,
type_: getActionFromString(actionType), type_: actionType,
mod_person_id: modId, mod_person_id: modId,
other_person_id: userId, other_person_id: userId,
}; };

View file

@ -80,6 +80,8 @@ import { Icon, Spinner } from "../common/icon";
import { Paginator } from "../common/paginator"; import { Paginator } from "../common/paginator";
import { PrivateMessage } from "../private_message/private-message"; import { PrivateMessage } from "../private_message/private-message";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes";
enum UnreadOrAll { enum UnreadOrAll {
Unread, Unread,
@ -126,7 +128,15 @@ interface InboxState {
isIsomorphic: boolean; isIsomorphic: boolean;
} }
export class Inbox extends Component<any, InboxState> { type InboxRouteProps = RouteComponentProps<Record<string, never>> &
Record<string, never>;
export type InboxFetchConfig = IRoutePropsWithFetch<
InboxData,
Record<string, never>,
Record<string, never>
>;
export class Inbox extends Component<InboxRouteProps, InboxState> {
private isoData = setIsoData<InboxData>(this.context); private isoData = setIsoData<InboxData>(this.context);
state: InboxState = { state: InboxState = {
unreadOrAll: UnreadOrAll.Unread, unreadOrAll: UnreadOrAll.Unread,

View file

@ -94,6 +94,7 @@ import { CommunityLink } from "../community/community-link";
import { PersonDetails } from "./person-details"; import { PersonDetails } from "./person-details";
import { PersonListing } from "./person-listing"; import { PersonListing } from "./person-listing";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { IRoutePropsWithFetch } from "../../routes";
type ProfileData = RouteDataResponse<{ type ProfileData = RouteDataResponse<{
personResponse: GetPersonDetailsResponse; personResponse: GetPersonDetailsResponse;
@ -117,12 +118,15 @@ interface ProfileProps {
page: number; page: number;
} }
function getProfileQueryParams() { export function getProfileQueryParams(source?: string): ProfileProps {
return getQueryParams<ProfileProps>({ return getQueryParams<ProfileProps>(
{
view: getViewFromProps, view: getViewFromProps,
page: getPageFromString, page: getPageFromString,
sort: getSortTypeFromQuery, sort: getSortTypeFromQuery,
}); },
source,
);
} }
function getSortTypeFromQuery(sort?: string): SortType { function getSortTypeFromQuery(sort?: string): SortType {
@ -171,10 +175,15 @@ function isPersonBlocked(personRes: RequestState<GetPersonDetailsResponse>) {
); );
} }
export class Profile extends Component< type ProfilePathProps = { username: string };
RouteComponentProps<{ username: string }>, type ProfileRouteProps = RouteComponentProps<ProfilePathProps> & ProfileProps;
ProfileState export type ProfileFetchConfig = IRoutePropsWithFetch<
> { ProfileData,
ProfilePathProps,
ProfileProps
>;
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,
@ -186,7 +195,7 @@ export class Profile extends Component<
isIsomorphic: false, isIsomorphic: false,
}; };
constructor(props: RouteComponentProps<{ username: string }>, context: any) { constructor(props: ProfileRouteProps, context: any) {
super(props, context); super(props, context);
this.handleSortChange = this.handleSortChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this);
@ -248,7 +257,7 @@ export class Profile extends Component<
} }
async fetchUserData() { async fetchUserData() {
const { page, sort, view } = getProfileQueryParams(); const { page, sort, view } = this.props;
this.setState({ personRes: LOADING_REQUEST }); this.setState({ personRes: LOADING_REQUEST });
const personRes = await HttpService.client.getPersonDetails({ const personRes = await HttpService.client.getPersonDetails({
@ -278,22 +287,23 @@ export class Profile extends Component<
static async fetchInitialData({ static async fetchInitialData({
headers, headers,
path, query: { view, sort, page },
query: { page, sort, view: urlView }, match: {
}: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<ProfileData> { params: { username },
},
}: InitialFetchRequest<
ProfilePathProps,
ProfileProps
>): Promise<ProfileData> {
const client = wrapClient( const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),
); );
const pathSplit = path.split("/");
const username = pathSplit[2];
const view = getViewFromProps(urlView);
const form: GetPersonDetails = { const form: GetPersonDetails = {
username: username, username: username,
sort: getSortTypeFromQuery(sort), sort,
saved_only: view === PersonDetailsView.Saved, saved_only: view === PersonDetailsView.Saved,
page: getPageFromString(page), page,
limit: fetchLimit, limit: fetchLimit,
}; };
@ -321,7 +331,7 @@ export class Profile extends Component<
case "success": { case "success": {
const siteRes = this.state.siteRes; const siteRes = this.state.siteRes;
const personRes = this.state.personRes.data; const personRes = this.state.personRes.data;
const { page, sort, view } = getProfileQueryParams(); const { page, sort, view } = this.props;
return ( return (
<div className="row"> <div className="row">
@ -415,7 +425,7 @@ export class Profile extends Component<
} }
getRadio(view: PersonDetailsView) { getRadio(view: PersonDetailsView) {
const { view: urlView } = getProfileQueryParams(); const { view: urlView } = this.props;
const active = view === urlView; const active = view === urlView;
const radioId = randomStr(); const radioId = randomStr();
@ -442,10 +452,10 @@ export class Profile extends Component<
} }
get selects() { get selects() {
const { sort } = getProfileQueryParams(); const { sort } = this.props;
const { username } = this.props.match.params; const { username } = this.props.match.params;
const profileRss = `/feeds/u/${username}.xml?sort=${sort}`; const profileRss = `/feeds/u/${username}.xml${getQueryString({ sort })}`;
return ( return (
<div className="mb-2"> <div className="mb-2">
@ -713,11 +723,7 @@ export class Profile extends Component<
} }
async updateUrl({ page, sort, view }: Partial<ProfileProps>) { async updateUrl({ page, sort, view }: Partial<ProfileProps>) {
const { const { page: urlPage, sort: urlSort, view: urlView } = this.props;
page: urlPage,
sort: urlSort,
view: urlView,
} = getProfileQueryParams();
const queryParams: QueryParams<ProfileProps> = { const queryParams: QueryParams<ProfileProps> = {
page: (page ?? urlPage).toString(), page: (page ?? urlPage).toString(),

View file

@ -27,6 +27,8 @@ import { Paginator } from "../common/paginator";
import { RegistrationApplication } from "../common/registration-application"; import { RegistrationApplication } from "../common/registration-application";
import { UnreadCounterService } from "../../services"; import { UnreadCounterService } from "../../services";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes";
enum RegistrationState { enum RegistrationState {
Unread, Unread,
@ -46,8 +48,18 @@ interface RegistrationApplicationsState {
isIsomorphic: boolean; isIsomorphic: boolean;
} }
type RegistrationApplicationsRouteProps = RouteComponentProps<
Record<string, never>
> &
Record<string, never>;
export type RegistrationApplicationsFetchConfig = IRoutePropsWithFetch<
RegistrationApplicationsData,
Record<string, never>,
Record<string, never>
>;
export class RegistrationApplications extends Component< export class RegistrationApplications extends Component<
any, RegistrationApplicationsRouteProps,
RegistrationApplicationsState RegistrationApplicationsState
> { > {
private isoData = setIsoData<RegistrationApplicationsData>(this.context); private isoData = setIsoData<RegistrationApplicationsData>(this.context);

View file

@ -50,6 +50,8 @@ import { PostReport } from "../post/post-report";
import { PrivateMessageReport } from "../private_message/private-message-report"; import { PrivateMessageReport } from "../private_message/private-message-report";
import { UnreadCounterService } from "../../services"; import { UnreadCounterService } from "../../services";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes";
enum UnreadOrAll { enum UnreadOrAll {
Unread, Unread,
@ -93,7 +95,15 @@ interface ReportsState {
isIsomorphic: boolean; isIsomorphic: boolean;
} }
export class Reports extends Component<any, ReportsState> { type ReportsRouteProps = RouteComponentProps<Record<string, never>> &
Record<string, never>;
export type ReportsFetchConfig = IRoutePropsWithFetch<
ReportsData,
Record<string, never>,
Record<string, never>
>;
export class Reports extends Component<ReportsRouteProps, ReportsState> {
private isoData = setIsoData<ReportsData>(this.context); private isoData = setIsoData<ReportsData>(this.context);
state: ReportsState = { state: ReportsState = {
commentReportsRes: EMPTY_REQUEST, commentReportsRes: EMPTY_REQUEST,

View file

@ -68,6 +68,8 @@ import TotpModal from "../common/totp-modal";
import { LoadingEllipses } from "../common/loading-ellipses"; import { LoadingEllipses } from "../common/loading-ellipses";
import { refreshTheme, setThemeOverride } from "../../utils/browser"; import { refreshTheme, setThemeOverride } from "../../utils/browser";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { IRoutePropsWithFetch } from "../../routes";
import { RouteComponentProps } from "inferno-router/dist/Route";
type SettingsData = RouteDataResponse<{ type SettingsData = RouteDataResponse<{
instancesRes: GetFederatedInstancesResponse; instancesRes: GetFederatedInstancesResponse;
@ -193,7 +195,15 @@ function handleClose2faModal(i: Settings) {
i.setState({ show2faModal: false }); i.setState({ show2faModal: false });
} }
export class Settings extends Component<any, SettingsState> { type SettingsRouteProps = RouteComponentProps<Record<string, never>> &
Record<string, never>;
export type SettingsFetchConfig = IRoutePropsWithFetch<
SettingsData,
Record<string, never>,
Record<string, never>
>;
export class Settings extends Component<SettingsRouteProps, SettingsState> {
private isoData = setIsoData<SettingsData>(this.context); private isoData = setIsoData<SettingsData>(this.context);
exportSettingsLink = createRef<HTMLAnchorElement>(); exportSettingsLink = createRef<HTMLAnchorElement>();

View file

@ -5,7 +5,6 @@ import {
setIsoData, setIsoData,
} from "@utils/app"; } from "@utils/app";
import { getIdFromString, getQueryParams } from "@utils/helpers"; import { getIdFromString, getQueryParams } from "@utils/helpers";
import type { QueryParams } from "@utils/types";
import { Choice, RouteDataResponse } from "@utils/types"; import { Choice, RouteDataResponse } from "@utils/types";
import { Component } from "inferno"; import { Component } from "inferno";
import { RouteComponentProps } from "inferno-router/dist/Route"; import { RouteComponentProps } from "inferno-router/dist/Route";
@ -30,6 +29,7 @@ import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import { PostForm } from "./post-form"; import { PostForm } from "./post-form";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { IRoutePropsWithFetch } from "../../routes";
export interface CreatePostProps { export interface CreatePostProps {
communityId?: number; communityId?: number;
@ -40,10 +40,13 @@ type CreatePostData = RouteDataResponse<{
initialCommunitiesRes: ListCommunitiesResponse; initialCommunitiesRes: ListCommunitiesResponse;
}>; }>;
function getCreatePostQueryParams() { export function getCreatePostQueryParams(source?: string): CreatePostProps {
return getQueryParams<CreatePostProps>({ return getQueryParams<CreatePostProps>(
{
communityId: getIdFromString, communityId: getIdFromString,
}); },
source,
);
} }
function fetchCommunitiesForOptions(client: WrappedLemmyHttp) { function fetchCommunitiesForOptions(client: WrappedLemmyHttp) {
@ -58,8 +61,17 @@ interface CreatePostState {
isIsomorphic: boolean; isIsomorphic: boolean;
} }
type CreatePostPathProps = Record<string, never>;
type CreatePostRouteProps = RouteComponentProps<CreatePostPathProps> &
CreatePostProps;
export type CreatePostFetchConfig = IRoutePropsWithFetch<
CreatePostData,
CreatePostPathProps,
CreatePostProps
>;
export class CreatePost extends Component< export class CreatePost extends Component<
RouteComponentProps<Record<string, never>>, CreatePostRouteProps,
CreatePostState CreatePostState
> { > {
private isoData = setIsoData<CreatePostData>(this.context); private isoData = setIsoData<CreatePostData>(this.context);
@ -70,7 +82,7 @@ export class CreatePost extends Component<
isIsomorphic: false, isIsomorphic: false,
}; };
constructor(props: RouteComponentProps<Record<string, never>>, context: any) { constructor(props: CreatePostRouteProps, context: any) {
super(props, context); super(props, context);
this.handlePostCreate = this.handlePostCreate.bind(this); this.handlePostCreate = this.handlePostCreate.bind(this);
@ -102,9 +114,7 @@ export class CreatePost extends Component<
} }
} }
async fetchCommunity() { async fetchCommunity({ communityId }: CreatePostProps) {
const { communityId } = getCreatePostQueryParams();
if (communityId) { if (communityId) {
const res = await HttpService.client.getCommunity({ const res = await HttpService.client.getCommunity({
id: communityId, id: communityId,
@ -121,7 +131,7 @@ export class CreatePost extends Component<
async componentDidMount() { async componentDidMount() {
// TODO test this // TODO test this
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic) {
const { communityId } = getCreatePostQueryParams(); const { communityId } = this.props;
const initialCommunitiesRes = await fetchCommunitiesForOptions( const initialCommunitiesRes = await fetchCommunitiesForOptions(
HttpService.client, HttpService.client,
@ -134,7 +144,7 @@ export class CreatePost extends Component<
if ( if (
communityId?.toString() !== this.state.selectedCommunityChoice?.value communityId?.toString() !== this.state.selectedCommunityChoice?.value
) { ) {
await this.fetchCommunity(); await this.fetchCommunity({ communityId });
} else if (!communityId) { } else if (!communityId) {
this.setState({ this.setState({
selectedCommunityChoice: undefined, selectedCommunityChoice: undefined,
@ -199,15 +209,13 @@ export class CreatePost extends Component<
} }
async updateUrl({ communityId }: Partial<CreatePostProps>) { async updateUrl({ communityId }: Partial<CreatePostProps>) {
const { communityId: urlCommunityId } = getCreatePostQueryParams();
const locationState = this.props.history.location.state as const locationState = this.props.history.location.state as
| PostFormParams | PostFormParams
| undefined; | undefined;
const url = new URL(location.href); const url = new URL(location.href);
const newId = (communityId ?? urlCommunityId)?.toString(); const newId = communityId?.toString();
if (newId !== undefined) { if (newId !== undefined) {
url.searchParams.set("communityId", newId); url.searchParams.set("communityId", newId);
@ -215,9 +223,10 @@ export class CreatePost extends Component<
url.searchParams.delete("communityId"); url.searchParams.delete("communityId");
} }
history.replaceState(locationState, "", url); // This bypasses the router and doesn't update the query props.
window.history.replaceState(locationState, "", url);
await this.fetchCommunity(); await this.fetchCommunity({ communityId });
} }
handleSelectedCommunityChange(choice: Choice) { handleSelectedCommunityChange(choice: Choice) {
@ -243,7 +252,8 @@ export class CreatePost extends Component<
headers, headers,
query: { communityId }, query: { communityId },
}: InitialFetchRequest< }: InitialFetchRequest<
QueryParams<CreatePostProps> CreatePostPathProps,
CreatePostProps
>): Promise<CreatePostData> { >): Promise<CreatePostData> {
const client = wrapClient( const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),
@ -255,7 +265,7 @@ export class CreatePost extends Component<
if (communityId) { if (communityId) {
const form: GetCommunity = { const form: GetCommunity = {
id: getIdFromString(communityId), id: communityId,
}; };
data.communityResponse = await client.getCommunity(form); data.communityResponse = await client.getCommunity(form);

View file

@ -3,6 +3,7 @@ import {
capitalizeFirstLetter, capitalizeFirstLetter,
debounce, debounce,
getIdFromString, getIdFromString,
getQueryString,
validTitle, validTitle,
validURL, validURL,
} from "@utils/helpers"; } from "@utils/helpers";
@ -380,18 +381,14 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
archive.org {I18NextService.i18n.t("archive_link")} archive.org {I18NextService.i18n.t("archive_link")}
</a> </a>
<a <a
href={`${ghostArchiveUrl}/search?term=${encodeURIComponent( href={`${ghostArchiveUrl}/search${getQueryString({ term: url })}`}
url,
)}`}
className="me-2 d-inline-block float-right text-muted small fw-bold" className="me-2 d-inline-block float-right text-muted small fw-bold"
rel={relTags} rel={relTags}
> >
ghostarchive.org {I18NextService.i18n.t("archive_link")} ghostarchive.org {I18NextService.i18n.t("archive_link")}
</a> </a>
<a <a
href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent( href={`${archiveTodayUrl}/${getQueryString({ run: "1", url })}`}
url,
)}`}
className="me-2 d-inline-block float-right text-muted small fw-bold" className="me-2 d-inline-block float-right text-muted small fw-bold"
rel={relTags} rel={relTags}
> >

View file

@ -98,6 +98,8 @@ import { Icon, Spinner } from "../common/icon";
import { Sidebar } from "../community/sidebar"; import { Sidebar } from "../community/sidebar";
import { PostListing } from "./post-listing"; import { PostListing } from "./post-listing";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes";
const commentsShownInterval = 15; const commentsShownInterval = 15;
@ -122,7 +124,18 @@ interface PostState {
isIsomorphic: boolean; isIsomorphic: boolean;
} }
export class Post extends Component<any, PostState> { type PostPathProps =
| { post_id: string; comment_id: never }
| { post_id: never; comment_id: string };
type PostRouteProps = RouteComponentProps<PostPathProps> &
Record<string, never>;
export type PostFetchConfig = IRoutePropsWithFetch<
PostData,
PostPathProps,
Record<string, never>
>;
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;
state: PostState = { state: PostState = {
@ -235,15 +248,13 @@ export class Post extends Component<any, PostState> {
static async fetchInitialData({ static async fetchInitialData({
headers, headers,
path, match,
}: InitialFetchRequest): Promise<PostData> { }: InitialFetchRequest<PostPathProps>): Promise<PostData> {
const client = wrapClient( const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),
); );
const pathSplit = path.split("/"); const postId = getIdFromProps({ match });
const commentId = getCommentIdFromProps({ match });
const pathType = pathSplit.at(1);
const id = pathSplit.at(2) ? Number(pathSplit.at(2)) : undefined;
const postForm: GetPost = {}; const postForm: GetPost = {};
@ -254,14 +265,11 @@ export class Post extends Component<any, PostState> {
saved_only: false, saved_only: false,
}; };
// Set the correct id based on the path type postForm.id = postId;
if (pathType === "post") { postForm.comment_id = commentId;
postForm.id = id;
commentsForm.post_id = id; commentsForm.post_id = postId;
} else { commentsForm.parent_id = commentId;
postForm.comment_id = id;
commentsForm.parent_id = id;
}
const [postRes, commentsRes] = await Promise.all([ const [postRes, commentsRes] = await Promise.all([
client.getPost(postForm), client.getPost(postForm),

View file

@ -22,6 +22,8 @@ import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import { PrivateMessageForm } from "./private-message-form"; import { PrivateMessageForm } from "./private-message-form";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../../routes";
type CreatePrivateMessageData = RouteDataResponse<{ type CreatePrivateMessageData = RouteDataResponse<{
recipientDetailsResponse: GetPersonDetailsResponse; recipientDetailsResponse: GetPersonDetailsResponse;
@ -34,8 +36,17 @@ interface CreatePrivateMessageState {
isIsomorphic: boolean; isIsomorphic: boolean;
} }
type CreatePrivateMessagePathProps = { recipient_id: string };
type CreatePrivateMessageRouteProps =
RouteComponentProps<CreatePrivateMessagePathProps> & Record<string, never>;
export type CreatePrivateMessageFetchConfig = IRoutePropsWithFetch<
CreatePrivateMessageData,
CreatePrivateMessagePathProps,
Record<string, never>
>;
export class CreatePrivateMessage extends Component< export class CreatePrivateMessage extends Component<
any, CreatePrivateMessageRouteProps,
CreatePrivateMessageState CreatePrivateMessageState
> { > {
private isoData = setIsoData<CreatePrivateMessageData>(this.context); private isoData = setIsoData<CreatePrivateMessageData>(this.context);
@ -69,12 +80,12 @@ export class CreatePrivateMessage extends Component<
static async fetchInitialData({ static async fetchInitialData({
headers, headers,
path, match,
}: InitialFetchRequest): Promise<CreatePrivateMessageData> { }: InitialFetchRequest<CreatePrivateMessagePathProps>): Promise<CreatePrivateMessageData> {
const client = wrapClient( const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),
); );
const person_id = Number(path.split("/").pop()); const person_id = getRecipientIdFromProps({ match });
const form: GetPersonDetails = { const form: GetPersonDetails = {
person_id, person_id,

View file

@ -1,6 +1,6 @@
import { setIsoData } from "@utils/app"; import { setIsoData } from "@utils/app";
import { getQueryParams } from "@utils/helpers"; import { getQueryParams } from "@utils/helpers";
import { QueryParams, RouteDataResponse } from "@utils/types"; import { RouteDataResponse } from "@utils/types";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
CommunityView, CommunityView,
@ -22,6 +22,8 @@ import { PictrsImage } from "./common/pictrs-image";
import { SubscribeButton } from "./common/subscribe-button"; import { SubscribeButton } from "./common/subscribe-button";
import { CommunityLink } from "./community/community-link"; import { CommunityLink } from "./community/community-link";
import { getHttpBaseInternal } from "../utils/env"; import { getHttpBaseInternal } from "../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../routes";
interface RemoteFetchProps { interface RemoteFetchProps {
uri?: string; uri?: string;
@ -37,16 +39,19 @@ interface RemoteFetchState {
followCommunityLoading: boolean; followCommunityLoading: boolean;
} }
const getUriFromQuery = (uri?: string): string | undefined => const getUriFromQuery = (uri?: string): string | undefined => uri;
uri ? decodeURIComponent(uri) : undefined;
const getRemoteFetchQueryParams = () => export function getRemoteFetchQueryParams(source?: string): RemoteFetchProps {
getQueryParams<RemoteFetchProps>({ return getQueryParams<RemoteFetchProps>(
{
uri: getUriFromQuery, uri: getUriFromQuery,
}); },
source,
);
}
function uriToQuery(uri: string) { function uriToQuery(uri: string) {
const match = decodeURIComponent(uri).match(/https?:\/\/(.+)\/c\/(.+)/); const match = uri.match(/https?:\/\/(.+)\/c\/(.+)/);
return match ? `!${match[2]}@${match[1]}` : ""; return match ? `!${match[2]}@${match[1]}` : "";
} }
@ -83,7 +88,19 @@ async function handleToggleFollow(i: RemoteFetch, follow: boolean) {
const handleFollow = (i: RemoteFetch) => handleToggleFollow(i, true); const handleFollow = (i: RemoteFetch) => handleToggleFollow(i, true);
const handleUnfollow = (i: RemoteFetch) => handleToggleFollow(i, false); const handleUnfollow = (i: RemoteFetch) => handleToggleFollow(i, false);
export class RemoteFetch extends Component<any, RemoteFetchState> { type RemoteFetchPathProps = Record<string, never>;
type RemoteFetchRouteProps = RouteComponentProps<RemoteFetchPathProps> &
RemoteFetchProps;
export type RemoteFetchFetchConfig = IRoutePropsWithFetch<
RemoteFetchData,
RemoteFetchPathProps,
RemoteFetchProps
>;
export class RemoteFetch extends Component<
RemoteFetchRouteProps,
RemoteFetchState
> {
private isoData = setIsoData<RemoteFetchData>(this.context); private isoData = setIsoData<RemoteFetchData>(this.context);
state: RemoteFetchState = { state: RemoteFetchState = {
resolveObjectRes: EMPTY_REQUEST, resolveObjectRes: EMPTY_REQUEST,
@ -91,7 +108,7 @@ export class RemoteFetch extends Component<any, RemoteFetchState> {
followCommunityLoading: false, followCommunityLoading: false,
}; };
constructor(props: any, context: any) { constructor(props: RemoteFetchRouteProps, context: any) {
super(props, context); super(props, context);
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
@ -107,7 +124,7 @@ export class RemoteFetch extends Component<any, RemoteFetchState> {
async componentDidMount() { async componentDidMount() {
if (!this.state.isIsomorphic) { if (!this.state.isIsomorphic) {
const { uri } = getRemoteFetchQueryParams(); const { uri } = this.props;
if (uri) { if (uri) {
this.setState({ resolveObjectRes: LOADING_REQUEST }); this.setState({ resolveObjectRes: LOADING_REQUEST });
@ -139,7 +156,7 @@ export class RemoteFetch extends Component<any, RemoteFetchState> {
get content() { get content() {
const res = this.state.resolveObjectRes; const res = this.state.resolveObjectRes;
const { uri } = getRemoteFetchQueryParams(); const { uri } = this.props;
const remoteCommunityName = uri ? uriToQuery(uri) : "remote community"; const remoteCommunityName = uri ? uriToQuery(uri) : "remote community";
switch (res.state) { switch (res.state) {
@ -204,7 +221,7 @@ export class RemoteFetch extends Component<any, RemoteFetchState> {
} }
get documentTitle(): string { get documentTitle(): string {
const { uri } = getRemoteFetchQueryParams(); const { uri } = this.props;
const name = this.isoData.site_res.site_view.site.name; const name = this.isoData.site_res.site_view.site.name;
return `${I18NextService.i18n.t("remote_follow")} - ${ return `${I18NextService.i18n.t("remote_follow")} - ${
uri ? `${uri} - ` : "" uri ? `${uri} - ` : ""
@ -215,7 +232,8 @@ export class RemoteFetch extends Component<any, RemoteFetchState> {
headers, headers,
query: { uri }, query: { uri },
}: InitialFetchRequest< }: InitialFetchRequest<
QueryParams<RemoteFetchProps> RemoteFetchPathProps,
RemoteFetchProps
>): Promise<RemoteFetchData> { >): Promise<RemoteFetchData> {
const client = wrapClient( const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),

View file

@ -67,14 +67,16 @@ import { CommunityLink } from "./community/community-link";
import { PersonListing } from "./person/person-listing"; import { PersonListing } from "./person/person-listing";
import { PostListing } from "./post/post-listing"; import { PostListing } from "./post/post-listing";
import { getHttpBaseInternal } from "../utils/env"; import { getHttpBaseInternal } from "../utils/env";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { IRoutePropsWithFetch } from "../routes";
interface SearchProps { interface SearchProps {
q?: string; q?: string;
type: SearchType; type: SearchType;
sort: SortType; sort: SortType;
listingType: ListingType; listingType: ListingType;
communityId?: number | null; communityId?: number;
creatorId?: number | null; creatorId?: number;
page: number; page: number;
} }
@ -112,8 +114,9 @@ const defaultListingType = "All";
const searchTypes = ["All", "Comments", "Posts", "Communities", "Users", "Url"]; const searchTypes = ["All", "Comments", "Posts", "Communities", "Users", "Url"];
const getSearchQueryParams = () => export function getSearchQueryParams(source?: string): SearchProps {
getQueryParams<SearchProps>({ return getQueryParams<SearchProps>(
{
q: getSearchQueryFromQuery, q: getSearchQueryFromQuery,
type: getSearchTypeFromQuery, type: getSearchTypeFromQuery,
sort: getSortTypeFromQuery, sort: getSortTypeFromQuery,
@ -121,10 +124,12 @@ const getSearchQueryParams = () =>
communityId: getIdFromString, communityId: getIdFromString,
creatorId: getIdFromString, creatorId: getIdFromString,
page: getPageFromString, page: getPageFromString,
}); },
source,
);
}
const getSearchQueryFromQuery = (q?: string): string | undefined => const getSearchQueryFromQuery = (q?: string): string | undefined => q;
q ? decodeURIComponent(q) : undefined;
function getSearchTypeFromQuery(type_?: string): SearchType { function getSearchTypeFromQuery(type_?: string): SearchType {
return type_ ? (type_ as SearchType) : defaultSearchType; return type_ ? (type_ as SearchType) : defaultSearchType;
@ -240,7 +245,15 @@ function getListing(
); );
} }
export class Search extends Component<any, SearchState> { type SearchPathProps = Record<string, never>;
type SearchRouteProps = RouteComponentProps<SearchPathProps> & SearchProps;
export type SearchFetchConfig = IRoutePropsWithFetch<
SearchData,
SearchPathProps,
SearchProps
>;
export class Search extends Component<SearchRouteProps, SearchState> {
private isoData = setIsoData<SearchData>(this.context); private isoData = setIsoData<SearchData>(this.context);
searchInput = createRef<HTMLInputElement>(); searchInput = createRef<HTMLInputElement>();
@ -255,7 +268,7 @@ export class Search extends Component<any, SearchState> {
isIsomorphic: false, isIsomorphic: false,
}; };
constructor(props: any, context: any) { constructor(props: SearchRouteProps, context: any) {
super(props, context); super(props, context);
this.handleSortChange = this.handleSortChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this);
@ -265,7 +278,7 @@ export class Search extends Component<any, SearchState> {
this.handleCommunityFilterChange.bind(this); this.handleCommunityFilterChange.bind(this);
this.handleCreatorFilterChange = this.handleCreatorFilterChange.bind(this); this.handleCreatorFilterChange = this.handleCreatorFilterChange.bind(this);
const { q } = getSearchQueryParams(); const { q } = this.props;
this.state.searchText = q; this.state.searchText = q;
@ -335,7 +348,7 @@ export class Search extends Component<any, SearchState> {
}), }),
]; ];
const { communityId, creatorId } = getSearchQueryParams(); const { communityId, creatorId } = this.props;
if (communityId) { if (communityId) {
promises.push( promises.push(
@ -390,12 +403,19 @@ export class Search extends Component<any, SearchState> {
static async fetchInitialData({ static async fetchInitialData({
headers, headers,
query: { communityId, creatorId, q, type, sort, listingType, page }, query: {
}: InitialFetchRequest<QueryParams<SearchProps>>): Promise<SearchData> { q: query,
type: searchType,
sort,
listingType: listing_type,
communityId: community_id,
creatorId: creator_id,
page,
},
}: InitialFetchRequest<SearchPathProps, SearchProps>): Promise<SearchData> {
const client = wrapClient( const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { headers }), new LemmyHttp(getHttpBaseInternal(), { headers }),
); );
const community_id = getIdFromString(communityId);
let communityResponse: RequestState<GetCommunityResponse> = EMPTY_REQUEST; let communityResponse: RequestState<GetCommunityResponse> = EMPTY_REQUEST;
if (community_id) { if (community_id) {
const getCommunityForm: GetCommunity = { const getCommunityForm: GetCommunity = {
@ -411,7 +431,6 @@ export class Search extends Component<any, SearchState> {
limit: fetchLimit, limit: fetchLimit,
}); });
const creator_id = getIdFromString(creatorId);
let creatorDetailsResponse: RequestState<GetPersonDetailsResponse> = let creatorDetailsResponse: RequestState<GetPersonDetailsResponse> =
EMPTY_REQUEST; EMPTY_REQUEST;
if (creator_id) { if (creator_id) {
@ -422,8 +441,6 @@ export class Search extends Component<any, SearchState> {
creatorDetailsResponse = await client.getPersonDetails(getCreatorForm); creatorDetailsResponse = await client.getPersonDetails(getCreatorForm);
} }
const query = getSearchQueryFromQuery(q);
let searchResponse: RequestState<SearchResponse> = EMPTY_REQUEST; let searchResponse: RequestState<SearchResponse> = EMPTY_REQUEST;
let resolveObjectResponse: RequestState<ResolveObjectResponse> = let resolveObjectResponse: RequestState<ResolveObjectResponse> =
EMPTY_REQUEST; EMPTY_REQUEST;
@ -433,10 +450,10 @@ export class Search extends Component<any, SearchState> {
q: query, q: query,
community_id, community_id,
creator_id, creator_id,
type_: getSearchTypeFromQuery(type), type_: searchType,
sort: getSortTypeFromQuery(sort), sort,
listing_type: getListingTypeFromQuery(listingType), listing_type,
page: getPageFromString(page), page,
limit: fetchLimit, limit: fetchLimit,
}; };
@ -466,13 +483,13 @@ export class Search extends Component<any, SearchState> {
} }
get documentTitle(): string { get documentTitle(): string {
const { q } = getSearchQueryParams(); const { q } = this.props;
const name = this.state.siteRes.site_view.site.name; const name = this.state.siteRes.site_view.site.name;
return `${I18NextService.i18n.t("search")} - ${q ? `${q} - ` : ""}${name}`; return `${I18NextService.i18n.t("search")} - ${q ? `${q} - ` : ""}${name}`;
} }
render() { render() {
const { type, page } = getSearchQueryParams(); const { type, page } = this.props;
return ( return (
<div className="search container-lg"> <div className="search container-lg">
@ -555,8 +572,7 @@ export class Search extends Component<any, SearchState> {
} }
get selects() { get selects() {
const { type, listingType, sort, communityId, creatorId } = const { type, listingType, sort, communityId, creatorId } = this.props;
getSearchQueryParams();
const { const {
communitySearchOptions, communitySearchOptions,
creatorSearchOptions, creatorSearchOptions,
@ -664,7 +680,7 @@ export class Search extends Component<any, SearchState> {
); );
} }
const { sort } = getSearchQueryParams(); const { sort } = this.props;
// Sort it // Sort it
if (sort === "New") { if (sort === "New") {
@ -959,7 +975,7 @@ export class Search extends Component<any, SearchState> {
async search() { async search() {
const { searchText: q } = this.state; const { searchText: q } = this.state;
const { communityId, creatorId, type, sort, listingType, page } = const { communityId, creatorId, type, sort, listingType, page } =
getSearchQueryParams(); this.props;
if (q) { if (q) {
this.setState({ searchRes: LOADING_REQUEST }); this.setState({ searchRes: LOADING_REQUEST });
@ -991,7 +1007,7 @@ export class Search extends Component<any, SearchState> {
handleCreatorSearch = debounce(async (text: string) => { handleCreatorSearch = debounce(async (text: string) => {
if (text.length > 0) { if (text.length > 0) {
const { creatorId } = getSearchQueryParams(); const { creatorId } = this.props;
const { creatorSearchOptions } = this.state; const { creatorSearchOptions } = this.state;
this.setState({ searchCreatorLoading: true }); this.setState({ searchCreatorLoading: true });
@ -1009,7 +1025,7 @@ export class Search extends Component<any, SearchState> {
handleCommunitySearch = debounce(async (text: string) => { handleCommunitySearch = debounce(async (text: string) => {
if (text.length > 0) { if (text.length > 0) {
const { communityId } = getSearchQueryParams(); const { communityId } = this.props;
const { communitySearchOptions } = this.state; const { communitySearchOptions } = this.state;
this.setState({ this.setState({
@ -1053,14 +1069,14 @@ export class Search extends Component<any, SearchState> {
handleCommunityFilterChange({ value }: Choice) { handleCommunityFilterChange({ value }: Choice) {
this.updateUrl({ this.updateUrl({
communityId: getIdFromString(value) ?? null, communityId: getIdFromString(value),
page: 1, page: 1,
}); });
} }
handleCreatorFilterChange({ value }: Choice) { handleCreatorFilterChange({ value }: Choice) {
this.updateUrl({ this.updateUrl({
creatorId: getIdFromString(value) ?? null, creatorId: getIdFromString(value),
page: 1, page: 1,
}); });
} }
@ -1095,13 +1111,9 @@ export class Search extends Component<any, SearchState> {
sort: urlSort, sort: urlSort,
creatorId: urlCreatorId, creatorId: urlCreatorId,
page: urlPage, page: urlPage,
} = getSearchQueryParams(); } = this.props;
let query = q ?? this.state.searchText ?? urlQ; const query = q ?? this.state.searchText ?? urlQ;
if (query && query.length > 0) {
query = encodeURIComponent(query);
}
const queryParams: QueryParams<SearchProps> = { const queryParams: QueryParams<SearchProps> = {
q: query, q: query,

View file

@ -5,8 +5,8 @@ import {
GetSiteResponse, GetSiteResponse,
PersonMention, PersonMention,
} from "lemmy-js-client"; } from "lemmy-js-client";
import type { ParsedQs } from "qs";
import { RequestState } from "./services/HttpService"; import { RequestState } from "./services/HttpService";
import { Match } from "inferno-router/dist/Route";
/** /**
* This contains serialized data, it needs to be deserialized before use. * This contains serialized data, it needs to be deserialized before use.
@ -30,9 +30,13 @@ declare global {
} }
} }
export interface InitialFetchRequest<T extends ParsedQs = ParsedQs> { export interface InitialFetchRequest<
P extends Record<string, string> = Record<string, never>,
T extends Record<string, any> = Record<string, never>,
> {
path: string; path: string;
query: T; query: T;
match: Match<P>;
site: GetSiteResponse; site: GetSiteResponse;
headers: { [key: string]: string }; headers: { [key: string]: string };
} }

View file

@ -1,45 +1,107 @@
import { IRouteProps } from "inferno-router/dist/Route"; import { IRouteProps, RouteComponentProps } from "inferno-router/dist/Route";
import { Communities } from "./components/community/communities"; import {
import { Community } from "./components/community/community"; Communities,
CommunitiesFetchConfig,
getCommunitiesQueryParams,
} from "./components/community/communities";
import {
Community,
CommunityFetchConfig,
getCommunityQueryParams,
} from "./components/community/community";
import { CreateCommunity } from "./components/community/create-community"; import { CreateCommunity } from "./components/community/create-community";
import { AdminSettings } from "./components/home/admin-settings"; import {
import { Home } from "./components/home/home"; AdminSettings,
import { Instances } from "./components/home/instances"; AdminSettingsFetchConfig,
} from "./components/home/admin-settings";
import {
Home,
HomeFetchConfig,
getHomeQueryParams,
} from "./components/home/home";
import { Instances, InstancesFetchConfig } from "./components/home/instances";
import { Legal } from "./components/home/legal"; import { Legal } from "./components/home/legal";
import { Login } from "./components/home/login"; import {
Login,
LoginFetchConfig,
getLoginQueryParams,
} from "./components/home/login";
import { LoginReset } from "./components/home/login-reset"; import { LoginReset } from "./components/home/login-reset";
import { Setup } from "./components/home/setup"; import { Setup } from "./components/home/setup";
import { Signup } from "./components/home/signup"; import { Signup } from "./components/home/signup";
import { Modlog } from "./components/modlog"; import {
import { Inbox } from "./components/person/inbox"; Modlog,
ModlogFetchConfig,
getModlogQueryParams,
} from "./components/modlog";
import { Inbox, InboxFetchConfig } from "./components/person/inbox";
import { PasswordChange } from "./components/person/password-change"; import { PasswordChange } from "./components/person/password-change";
import { Profile } from "./components/person/profile"; import {
import { RegistrationApplications } from "./components/person/registration-applications"; Profile,
import { Reports } from "./components/person/reports"; ProfileFetchConfig,
import { Settings } from "./components/person/settings"; getProfileQueryParams,
} from "./components/person/profile";
import {
RegistrationApplications,
RegistrationApplicationsFetchConfig,
} from "./components/person/registration-applications";
import { Reports, ReportsFetchConfig } from "./components/person/reports";
import { Settings, SettingsFetchConfig } from "./components/person/settings";
import { VerifyEmail } from "./components/person/verify-email"; import { VerifyEmail } from "./components/person/verify-email";
import { CreatePost } from "./components/post/create-post"; import {
import { Post } from "./components/post/post"; CreatePostFetchConfig,
import { CreatePrivateMessage } from "./components/private_message/create-private-message"; CreatePost,
import { RemoteFetch } from "./components/remote-fetch"; getCreatePostQueryParams,
import { Search } from "./components/search"; } from "./components/post/create-post";
import { Post, PostFetchConfig } from "./components/post/post";
import {
CreatePrivateMessage,
CreatePrivateMessageFetchConfig,
} from "./components/private_message/create-private-message";
import {
RemoteFetch,
RemoteFetchFetchConfig,
getRemoteFetchQueryParams,
} from "./components/remote-fetch";
import {
Search,
SearchFetchConfig,
getSearchQueryParams,
} from "./components/search";
import { InitialFetchRequest, RouteData } from "./interfaces"; import { InitialFetchRequest, RouteData } from "./interfaces";
import { GetSiteResponse } from "lemmy-js-client";
import { Inferno } from "inferno";
interface IRoutePropsWithFetch<T extends RouteData> extends IRouteProps { export interface IRoutePropsWithFetch<
fetchInitialData?(req: InitialFetchRequest): Promise<T>; DataT extends RouteData,
PathPropsT extends Record<string, string>,
QueryPropsT extends Record<string, any>,
> extends IRouteProps {
fetchInitialData?(
req: InitialFetchRequest<PathPropsT, QueryPropsT>,
): Promise<DataT>;
getQueryParams?(
source: string | undefined,
siteRes: GetSiteResponse,
): QueryPropsT;
component: Inferno.ComponentClass<
RouteComponentProps<PathPropsT> & QueryPropsT
>;
} }
export const routes: IRoutePropsWithFetch<Record<string, any>>[] = [ export const routes: IRoutePropsWithFetch<RouteData, any, any>[] = [
{ {
path: `/`, path: `/`,
component: Home, component: Home,
fetchInitialData: Home.fetchInitialData, fetchInitialData: Home.fetchInitialData,
exact: true, exact: true,
}, getQueryParams: getHomeQueryParams,
} as HomeFetchConfig,
{ {
path: `/login`, path: `/login`,
component: Login, component: Login,
}, getQueryParams: getLoginQueryParams,
} as LoginFetchConfig,
{ {
path: `/login_reset`, path: `/login_reset`,
component: LoginReset, component: LoginReset,
@ -52,7 +114,8 @@ export const routes: IRoutePropsWithFetch<Record<string, any>>[] = [
path: `/create_post`, path: `/create_post`,
component: CreatePost, component: CreatePost,
fetchInitialData: CreatePost.fetchInitialData, fetchInitialData: CreatePost.fetchInitialData,
}, getQueryParams: getCreatePostQueryParams,
} as CreatePostFetchConfig,
{ {
path: `/create_community`, path: `/create_community`,
component: CreateCommunity, component: CreateCommunity,
@ -61,73 +124,79 @@ export const routes: IRoutePropsWithFetch<Record<string, any>>[] = [
path: `/create_private_message/:recipient_id`, path: `/create_private_message/:recipient_id`,
component: CreatePrivateMessage, component: CreatePrivateMessage,
fetchInitialData: CreatePrivateMessage.fetchInitialData, fetchInitialData: CreatePrivateMessage.fetchInitialData,
}, } as CreatePrivateMessageFetchConfig,
{ {
path: `/communities`, path: `/communities`,
component: Communities, component: Communities,
fetchInitialData: Communities.fetchInitialData, fetchInitialData: Communities.fetchInitialData,
}, getQueryParams: getCommunitiesQueryParams,
} as CommunitiesFetchConfig,
{ {
path: `/post/:post_id`, path: `/post/:post_id`,
component: Post, component: Post,
fetchInitialData: Post.fetchInitialData, fetchInitialData: Post.fetchInitialData,
}, } as PostFetchConfig,
{ {
path: `/comment/:comment_id`, path: `/comment/:comment_id`,
component: Post, component: Post,
fetchInitialData: Post.fetchInitialData, fetchInitialData: Post.fetchInitialData,
}, } as PostFetchConfig,
{ {
path: `/c/:name`, path: `/c/:name`,
component: Community, component: Community,
fetchInitialData: Community.fetchInitialData, fetchInitialData: Community.fetchInitialData,
}, getQueryParams: getCommunityQueryParams,
} as CommunityFetchConfig,
{ {
path: `/u/:username`, path: `/u/:username`,
component: Profile, component: Profile,
fetchInitialData: Profile.fetchInitialData, fetchInitialData: Profile.fetchInitialData,
}, getQueryParams: getProfileQueryParams,
} as ProfileFetchConfig,
{ {
path: `/inbox`, path: `/inbox`,
component: Inbox, component: Inbox,
fetchInitialData: Inbox.fetchInitialData, fetchInitialData: Inbox.fetchInitialData,
}, } as InboxFetchConfig,
{ {
path: `/settings`, path: `/settings`,
component: Settings, component: Settings,
fetchInitialData: Settings.fetchInitialData, fetchInitialData: Settings.fetchInitialData,
}, } as SettingsFetchConfig,
{ {
path: `/modlog/:communityId`, path: `/modlog/:communityId`,
component: Modlog, component: Modlog,
fetchInitialData: Modlog.fetchInitialData, fetchInitialData: Modlog.fetchInitialData,
}, getQueryParams: getModlogQueryParams,
} as ModlogFetchConfig,
{ {
path: `/modlog`, path: `/modlog`,
component: Modlog, component: Modlog,
fetchInitialData: Modlog.fetchInitialData, fetchInitialData: Modlog.fetchInitialData,
}, getQueryParams: getModlogQueryParams,
} as ModlogFetchConfig,
{ path: `/setup`, component: Setup }, { path: `/setup`, component: Setup },
{ {
path: `/admin`, path: `/admin`,
component: AdminSettings, component: AdminSettings,
fetchInitialData: AdminSettings.fetchInitialData, fetchInitialData: AdminSettings.fetchInitialData,
}, } as AdminSettingsFetchConfig,
{ {
path: `/reports`, path: `/reports`,
component: Reports, component: Reports,
fetchInitialData: Reports.fetchInitialData, fetchInitialData: Reports.fetchInitialData,
}, } as ReportsFetchConfig,
{ {
path: `/registration_applications`, path: `/registration_applications`,
component: RegistrationApplications, component: RegistrationApplications,
fetchInitialData: RegistrationApplications.fetchInitialData, fetchInitialData: RegistrationApplications.fetchInitialData,
}, } as RegistrationApplicationsFetchConfig,
{ {
path: `/search`, path: `/search`,
component: Search, component: Search,
fetchInitialData: Search.fetchInitialData, fetchInitialData: Search.fetchInitialData,
}, getQueryParams: getSearchQueryParams,
} as SearchFetchConfig,
{ {
path: `/password_change/:token`, path: `/password_change/:token`,
component: PasswordChange, component: PasswordChange,
@ -140,11 +209,12 @@ export const routes: IRoutePropsWithFetch<Record<string, any>>[] = [
path: `/instances`, path: `/instances`,
component: Instances, component: Instances,
fetchInitialData: Instances.fetchInitialData, fetchInitialData: Instances.fetchInitialData,
}, } as InstancesFetchConfig,
{ path: `/legal`, component: Legal }, { path: `/legal`, component: Legal },
{ {
path: "/activitypub/externalInteraction", path: "/activitypub/externalInteraction",
component: RemoteFetch, component: RemoteFetch,
fetchInitialData: RemoteFetch.fetchInitialData, fetchInitialData: RemoteFetch.fetchInitialData,
}, getQueryParams: getRemoteFetchQueryParams,
} as RemoteFetchFetchConfig,
]; ];

View file

@ -1,4 +1,6 @@
import { getQueryString } from "@utils/helpers";
export default function communityRSSUrl(actorId: string, sort: string): string { export default function communityRSSUrl(actorId: string, sort: string): string {
const url = new URL(actorId); const url = new URL(actorId);
return `${url.origin}/feeds${url.pathname}.xml?sort=${sort}`; return `${url.origin}/feeds${url.pathname}.xml${getQueryString({ sort })}`;
} }

View file

@ -1,4 +1,8 @@
export default function getCommentIdFromProps(props: any): number | undefined { import { RouteComponentProps } from "inferno-router/dist/Route";
export default function getCommentIdFromProps(
props: Pick<RouteComponentProps<{ comment_id?: string }>, "match">,
): number | undefined {
const id = props.match.params.comment_id; const id = props.match.params.comment_id;
return id ? Number(id) : undefined; return id ? Number(id) : undefined;
} }

View file

@ -1,4 +1,8 @@
export default function getIdFromProps(props: any): number | undefined { import { RouteComponentProps } from "inferno-router/dist/Route";
export default function getIdFromProps(
props: Pick<RouteComponentProps<{ post_id?: string }>, "match">,
): number | undefined {
const id = props.match.params.post_id; const id = props.match.params.post_id;
return id ? Number(id) : undefined; return id ? Number(id) : undefined;
} }

View file

@ -1,4 +1,8 @@
export default function getRecipientIdFromProps(props: any): number { import { RouteComponentProps } from "inferno-router/dist/Route";
export default function getRecipientIdFromProps(
props: Pick<RouteComponentProps<{ recipient_id: string }>, "match">,
): number {
return props.match.params.recipient_id return props.match.params.recipient_id
? Number(props.match.params.recipient_id) ? Number(props.match.params.recipient_id)
: 1; : 1;

View file

@ -1,21 +1,28 @@
import { isBrowser } from "@utils/browser"; type Empty = NonNullable<unknown>;
type QueryMapping<PropsT, FallbacksT extends Empty> = {
[K in keyof PropsT]-?: (
input: string | undefined,
fallback: K extends keyof FallbacksT ? FallbacksT[K] : undefined,
) => PropsT[K];
};
export default function getQueryParams< export default function getQueryParams<
T extends Record<string, any>, PropsT,
>(processors: { FallbacksT extends Empty = Empty,
[K in keyof T]: (param: string) => T[K]; >(
}): T { processors: QueryMapping<PropsT, FallbacksT>,
if (isBrowser()) { source?: string,
const searchParams = new URLSearchParams(window.location.search); fallbacks: FallbacksT = {} as FallbacksT,
): PropsT {
const searchParams = new URLSearchParams(source);
return Array.from(Object.entries(processors)).reduce( const ret: Partial<PropsT> = {};
(acc, [key, process]) => ({ for (const key in processors) {
...acc, ret[key as string] = processors[key](
[key]: process(searchParams.get(key)), searchParams.get(key) ?? undefined,
}), fallbacks[key as string],
{} as T,
); );
} }
return ret as PropsT;
return {} as T;
} }

View file

@ -1,10 +1,12 @@
export default function getQueryString< export default function getQueryString<
T extends Record<string, string | undefined>, T extends Record<string, string | undefined>,
>(obj: T) { >(obj: T) {
return Object.entries(obj) const searchParams = new URLSearchParams();
Object.entries(obj)
.filter(([, val]) => val !== undefined && val !== null) .filter(([, val]) => val !== undefined && val !== null)
.reduce( .forEach(([key, val]) => searchParams.set(key, val ?? ""));
(acc, [key, val], index) => `${acc}${index > 0 ? "&" : ""}${key}=${val}`, if (searchParams.size) {
"?", return "?" + searchParams.toString();
); }
return "";
} }