From 70e382b3d943b7a683ca657a9fe69c5f682abd4c Mon Sep 17 00:00:00 2001 From: matc-pub <161147791+matc-pub@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:25:59 +0100 Subject: [PATCH] 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 --- src/server/handlers/catch-all-handler.tsx | 20 ++- src/shared/components/app/app.tsx | 27 +++- src/shared/components/common/auth-guard.tsx | 3 +- src/shared/components/common/pictrs-image.tsx | 35 ++-- .../components/common/subscribe-button.tsx | 6 +- .../components/community/communities.tsx | 54 ++++--- src/shared/components/community/community.tsx | 70 +++++--- src/shared/components/community/sidebar.tsx | 7 +- src/shared/components/home/admin-settings.tsx | 15 +- src/shared/components/home/home.tsx | 100 +++++++----- src/shared/components/home/instances.tsx | 12 +- src/shared/components/home/login.tsx | 28 ++-- src/shared/components/modlog.tsx | 81 +++++----- src/shared/components/person/inbox.tsx | 12 +- src/shared/components/person/profile.tsx | 66 ++++---- .../person/registration-applications.tsx | 14 +- src/shared/components/person/reports.tsx | 12 +- src/shared/components/person/settings.tsx | 12 +- src/shared/components/post/create-post.tsx | 48 +++--- src/shared/components/post/post-form.tsx | 9 +- src/shared/components/post/post.tsx | 38 +++-- .../create-private-message.tsx | 19 ++- src/shared/components/remote-fetch.tsx | 46 ++++-- src/shared/components/search.tsx | 100 +++++++----- src/shared/interfaces.ts | 8 +- src/shared/routes.ts | 150 +++++++++++++----- src/shared/utils/app/community-rss-url.ts | 4 +- .../utils/app/get-comment-id-from-props.ts | 6 +- src/shared/utils/app/get-id-from-props.ts | 6 +- .../utils/app/get-recipient-id-from-props.ts | 6 +- src/shared/utils/helpers/get-query-params.ts | 37 +++-- src/shared/utils/helpers/get-query-string.ts | 12 +- 32 files changed, 695 insertions(+), 368 deletions(-) diff --git a/src/server/handlers/catch-all-handler.tsx b/src/server/handlers/catch-all-handler.tsx index 2672f716..33d4d7a6 100644 --- a/src/server/handlers/catch-all-handler.tsx +++ b/src/server/handlers/catch-all-handler.tsx @@ -3,6 +3,7 @@ import { getHttpBaseInternal } from "@utils/env"; import { ErrorPageData } from "@utils/types"; import type { Request, Response } from "express"; import { StaticRouter, matchPath } from "inferno-router"; +import { Match } from "inferno-router/dist/Route"; import { renderToString } from "inferno-server"; import { GetSiteResponse, LemmyHttp } from "lemmy-js-client"; import { App } from "../../shared/components/app/app"; @@ -25,6 +26,8 @@ import { LanguageService, UserService, } from "../../shared/services/"; +import { parsePath } from "history"; +import { getQueryString } from "@utils/helpers"; export default async (req: Request, res: Response) => { try { @@ -40,7 +43,10 @@ export default async (req: Request, res: Response) => { .sort((a, b) => b.q - a.q) .map(x => (x.lang === "*" ? "en" : x.lang)) ?? []; - const activeRoute = routes.find(route => matchPath(req.path, route)); + let match: Match | null | undefined; + const activeRoute = routes.find( + route => (match = matchPath(req.path, route)), + ); const headers = setForwardedHeaders(req.headers); const auth = getJwtCookie(req.headers); @@ -49,7 +55,7 @@ export default async (req: Request, res: Response) => { new LemmyHttp(getHttpBaseInternal(), { headers }), ); - const { path, url, query } = req; + const { path, url } = req; // Get site data first // 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)) { - return res.redirect(`/login?prev=${encodeURIComponent(url)}`); + return res.redirect(`/login${getQueryString({ prev: url })}`); } if (try_site.state === "success") { @@ -83,10 +89,12 @@ export default async (req: Request, res: Response) => { return res.redirect("/setup"); } - if (site && activeRoute?.fetchInitialData) { - const initialFetchReq: InitialFetchRequest = { + if (site && activeRoute?.fetchInitialData && match) { + const { search } = parsePath(url); + const initialFetchReq: InitialFetchRequest> = { path, - query, + query: activeRoute.getQueryParams?.(search, site) ?? {}, + match, site, headers, }; diff --git a/src/shared/components/app/app.tsx b/src/shared/components/app/app.tsx index 970c3a4d..30f445f7 100644 --- a/src/shared/components/app/app.tsx +++ b/src/shared/components/app/app.tsx @@ -49,7 +49,12 @@ export class App extends Component {
{routes.map( - ({ path, component: RouteComponent, fetchInitialData }) => ( + ({ + path, + component: RouteComponent, + fetchInitialData, + getQueryParams, + }) => ( { 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 (
{RouteComponent && (isAuthPath(path ?? "") ? ( - + ) : isAnonymousPath(path ?? "") ? ( - + ) : ( - + ))}
diff --git a/src/shared/components/common/auth-guard.tsx b/src/shared/components/common/auth-guard.tsx index 03352901..410e41c8 100644 --- a/src/shared/components/common/auth-guard.tsx +++ b/src/shared/components/common/auth-guard.tsx @@ -2,6 +2,7 @@ import { Component } from "inferno"; import { RouteComponentProps } from "inferno-router/dist/Route"; import { UserService } from "../../services"; import { Spinner } from "./icon"; +import { getQueryString } from "@utils/helpers"; interface AuthGuardState { hasRedirected: boolean; @@ -26,7 +27,7 @@ class AuthGuard extends Component< if (!UserService.Instance.myUserInfo) { const { pathname, search } = this.props.location; this.context.router.history.replace( - `/login?prev=${encodeURIComponent(pathname + search)}`, + `/login${getQueryString({ prev: pathname + search })}`, ); } else { this.setState({ hasRedirected: true }); diff --git a/src/shared/components/common/pictrs-image.tsx b/src/shared/components/common/pictrs-image.tsx index 56167967..66fa4724 100644 --- a/src/shared/components/common/pictrs-image.tsx +++ b/src/shared/components/common/pictrs-image.tsx @@ -68,28 +68,31 @@ export class PictrsImage extends Component { // sample url: // http://localhost:8535/pictrs/image/file.png?thumbnail=256&format=jpg - const split = this.props.src.split("/pictrs/image/"); - - // If theres not multiple, then its not a pictrs image - if (split.length === 1) { + let url: URL | undefined; + try { + url = new URL(this.props.src); + } catch { return this.props.src; } - const host = split[0]; - const path = split[1]; - - const params = { format }; - - if (this.props.thumbnail) { - params["thumbnail"] = thumbnailSize; - } else if (this.props.icon) { - params["thumbnail"] = iconThumbnailSize; + // If theres no match, then its not a pictrs image + if (!url.pathname.includes("/pictrs/image/")) { + return this.props.src; } - const paramsStr = new URLSearchParams(params).toString(); - const out = `${host}/pictrs/image/${path}?${paramsStr}`; + // Keeps original search params. Could probably do `url.search = ""` here. - 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 { diff --git a/src/shared/components/common/subscribe-button.tsx b/src/shared/components/common/subscribe-button.tsx index 67337323..a663eae1 100644 --- a/src/shared/components/common/subscribe-button.tsx +++ b/src/shared/components/common/subscribe-button.tsx @@ -1,4 +1,4 @@ -import { validInstanceTLD } from "@utils/helpers"; +import { getQueryString, validInstanceTLD } from "@utils/helpers"; import classNames from "classnames"; import { NoOptionI18nKeys } from "i18next"; import { Component, MouseEventHandler, linkEvent } from "inferno"; @@ -134,8 +134,8 @@ function submitRemoteFollow( instanceText = `http${VERSION !== "dev" ? "s" : ""}://${instanceText}`; } - window.location.href = `${instanceText}/activitypub/externalInteraction?uri=${encodeURIComponent( - communityActorId, + window.location.href = `${instanceText}/activitypub/externalInteraction${getQueryString( + { uri: communityActorId }, )}`; } diff --git a/src/shared/components/community/communities.tsx b/src/shared/components/community/communities.tsx index 58639b88..82474f30 100644 --- a/src/shared/components/community/communities.tsx +++ b/src/shared/components/community/communities.tsx @@ -36,6 +36,8 @@ import { CommunityLink } from "./community-link"; import { communityLimit } from "../../config"; import { SubscribeButton } from "../common/subscribe-button"; import { getHttpBaseInternal } from "../../utils/env"; +import { RouteComponentProps } from "inferno-router/dist/Route"; +import { IRoutePropsWithFetch } from "../../routes"; type CommunitiesData = RouteDataResponse<{ listCommunitiesResponse: ListCommunitiesResponse; @@ -62,15 +64,30 @@ function getSortTypeFromQuery(type?: string): SortType { return type ? (type as SortType) : "TopMonth"; } -function getCommunitiesQueryParams() { - return getQueryParams({ - listingType: getListingTypeFromQuery, - sort: getSortTypeFromQuery, - page: getPageFromString, - }); +export function getCommunitiesQueryParams(source?: string): CommunitiesProps { + return getQueryParams( + { + listingType: getListingTypeFromQuery, + sort: getSortTypeFromQuery, + page: getPageFromString, + }, + source, + ); } -export class Communities extends Component { +type CommunitiesPathProps = Record; +type CommunitiesRouteProps = RouteComponentProps & + CommunitiesProps; +export type CommunitiesFetchConfig = IRoutePropsWithFetch< + CommunitiesData, + CommunitiesPathProps, + CommunitiesProps +>; + +export class Communities extends Component< + CommunitiesRouteProps, + CommunitiesState +> { private isoData = setIsoData(this.context); state: CommunitiesState = { listCommunitiesResponse: EMPTY_REQUEST, @@ -79,7 +96,7 @@ export class Communities extends Component { isIsomorphic: false, }; - constructor(props: any, context: any) { + constructor(props: CommunitiesRouteProps, context: any) { super(props, context); this.handlePageChange = this.handlePageChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this); @@ -118,7 +135,7 @@ export class Communities extends Component { ); case "success": { - const { listingType, sort, page } = getCommunitiesQueryParams(); + const { listingType, sort, page } = this.props; return (

@@ -268,7 +285,7 @@ export class Communities extends Component { listingType: urlListingType, sort: urlSort, page: urlPage, - } = getCommunitiesQueryParams(); + } = this.props; const queryParams: QueryParams = { listingType: listingType ?? urlListingType, @@ -302,10 +319,10 @@ export class Communities extends Component { handleSearchSubmit(i: Communities, event: any) { event.preventDefault(); - const searchParamEncoded = encodeURIComponent(i.state.searchText); - const { listingType } = getCommunitiesQueryParams(); + const searchParamEncoded = i.state.searchText; + const { listingType } = i.props; 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 { headers, query: { listingType, sort, page }, }: InitialFetchRequest< - QueryParams + CommunitiesPathProps, + CommunitiesProps >): Promise { const client = wrapClient( new LemmyHttp(getHttpBaseInternal(), { headers }), ); const listCommunitiesForm: ListCommunities = { - type_: getListingTypeFromQuery(listingType), - sort: getSortTypeFromQuery(sort), + type_: listingType, + sort, limit: communityLimit, - page: getPageFromString(page), + page, }; return { @@ -346,7 +364,7 @@ export class Communities extends Component { async refetch() { this.setState({ listCommunitiesResponse: LOADING_REQUEST }); - const { listingType, sort, page } = getCommunitiesQueryParams(); + const { listingType, sort, page } = this.props; this.setState({ listCommunitiesResponse: await HttpService.client.listCommunities({ diff --git a/src/shared/components/community/community.tsx b/src/shared/components/community/community.tsx index 3addf2f1..195de1fe 100644 --- a/src/shared/components/community/community.tsx +++ b/src/shared/components/community/community.tsx @@ -101,6 +101,7 @@ import { CommunityLink } from "./community-link"; import { PaginatorCursor } from "../common/paginator-cursor"; import { getHttpBaseInternal } from "../../utils/env"; import { Sidebar } from "./sidebar"; +import { IRoutePropsWithFetch } from "../../routes"; type CommunityData = RouteDataResponse<{ communityRes: GetCommunityResponse; @@ -124,12 +125,26 @@ interface CommunityProps { pageCursor?: PaginationCursor; } -function getCommunityQueryParams() { - return getQueryParams({ - dataType: getDataTypeFromQuery, - pageCursor: cursor => cursor, - sort: getSortTypeFromQuery, - }); +type Fallbacks = { sort: SortType }; + +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( + { + dataType: getDataTypeFromQuery, + pageCursor: (cursor?: string) => cursor, + sort: getSortTypeFromQuery, + }, + source, + { + sort: local_user?.default_sort_type ?? local_site.default_sort_type, + }, + ); } function getDataTypeFromQuery(type?: string): DataType { @@ -144,10 +159,16 @@ function getSortTypeFromQuery(type?: string): SortType { return type ? (type as SortType) : mySortType ?? "Active"; } -export class Community extends Component< - RouteComponentProps<{ name: string }>, - State -> { +type CommunityPathProps = { name: string }; +type CommunityRouteProps = RouteComponentProps & + CommunityProps; +export type CommunityFetchConfig = IRoutePropsWithFetch< + CommunityData, + CommunityPathProps, + CommunityProps +>; + +export class Community extends Component { private isoData = setIsoData(this.context); state: State = { communityRes: EMPTY_REQUEST, @@ -159,7 +180,7 @@ export class Community extends Component< isIsomorphic: false, }; private readonly mainContentRef: RefObject; - constructor(props: RouteComponentProps<{ name: string }>, context: any) { + constructor(props: CommunityRouteProps, context: any) { super(props, context); this.handleSortChange = this.handleSortChange.bind(this); @@ -234,25 +255,22 @@ export class Community extends Component< static async fetchInitialData({ headers, - path, - query: { dataType: urlDataType, pageCursor, sort: urlSort }, - }: InitialFetchRequest>): Promise< - Promise - > { + query: { dataType, pageCursor, sort }, + match: { + params: { name: communityName }, + }, + }: InitialFetchRequest< + CommunityPathProps, + CommunityProps + >): Promise { const client = wrapClient( new LemmyHttp(getHttpBaseInternal(), { headers }), ); - const pathSplit = path.split("/"); - const communityName = pathSplit[2]; const communityForm: GetCommunity = { name: communityName, }; - const dataType = getDataTypeFromQuery(urlDataType); - - const sort = getSortTypeFromQuery(urlSort); - let postsFetch: Promise> = Promise.resolve(EMPTY_REQUEST); let commentsFetch: Promise> = @@ -411,7 +429,7 @@ export class Community extends Component< } listings(communityRes: GetCommunityResponse) { - const { dataType } = getCommunityQueryParams(); + const { dataType } = this.props; const { site_res } = this.isoData; if (dataType === DataType.Post) { @@ -534,7 +552,7 @@ export class Community extends Component< // let communityRss = this.state.communityRes.map(r => // communityRSSUrl(r.community_view.community.actor_id, this.state.sort) // ); - const { dataType, sort } = getCommunityQueryParams(); + const { dataType, sort } = this.props; const communityRss = res ? communityRSSUrl(res.community_view.community.actor_id, sort) : undefined; @@ -592,7 +610,7 @@ export class Community extends Component< } async updateUrl({ dataType, pageCursor, sort }: Partial) { - const { dataType: urlDataType, sort: urlSort } = getCommunityQueryParams(); + const { dataType: urlDataType, sort: urlSort } = this.props; const queryParams: QueryParams = { dataType: getDataTypeString(dataType ?? urlDataType), @@ -608,7 +626,7 @@ export class Community extends Component< } async fetchData() { - const { dataType, pageCursor, sort } = getCommunityQueryParams(); + const { dataType, pageCursor, sort } = this.props; const { name } = this.props.match.params; if (dataType === DataType.Post) { diff --git a/src/shared/components/community/sidebar.tsx b/src/shared/components/community/sidebar.tsx index 4fc7f733..79c4b6b9 100644 --- a/src/shared/components/community/sidebar.tsx +++ b/src/shared/components/community/sidebar.tsx @@ -1,4 +1,4 @@ -import { hostname } from "@utils/helpers"; +import { getQueryString, hostname } from "@utils/helpers"; import { amAdmin, amMod, amTopMod } from "@utils/roles"; import { Component, InfernoNode, linkEvent } from "inferno"; import { T } from "inferno-i18next-dess"; @@ -287,7 +287,10 @@ export class Sidebar extends Component { className={`btn btn-secondary d-block mb-2 w-100 ${ 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")} diff --git a/src/shared/components/home/admin-settings.tsx b/src/shared/components/home/admin-settings.tsx index 48de62a2..7e42acd0 100644 --- a/src/shared/components/home/admin-settings.tsx +++ b/src/shared/components/home/admin-settings.tsx @@ -34,6 +34,8 @@ import RateLimitForm from "./rate-limit-form"; import { SiteForm } from "./site-form"; import { TaglineForm } from "./tagline-form"; import { getHttpBaseInternal } from "../../utils/env"; +import { RouteComponentProps } from "inferno-router/dist/Route"; +import { IRoutePropsWithFetch } from "../../routes"; type AdminSettingsData = RouteDataResponse<{ bannedRes: BannedPersonsResponse; @@ -52,7 +54,18 @@ interface AdminSettingsState { isIsomorphic: boolean; } -export class AdminSettings extends Component { +type AdminSettingsRouteProps = RouteComponentProps> & + Record; +export type AdminSettingsFetchConfig = IRoutePropsWithFetch< + AdminSettingsData, + Record, + Record +>; + +export class AdminSettings extends Component< + AdminSettingsRouteProps, + AdminSettingsState +> { private isoData = setIsoData(this.context); state: AdminSettingsState = { siteRes: this.isoData.site_res, diff --git a/src/shared/components/home/home.tsx b/src/shared/components/home/home.tsx index cc01c0f5..3843a280 100644 --- a/src/shared/components/home/home.tsx +++ b/src/shared/components/home/home.tsx @@ -100,6 +100,8 @@ import { PostListings } from "../post/post-listings"; import { SiteSidebar } from "./site-sidebar"; import { PaginatorCursor } from "../common/paginator-cursor"; import { getHttpBaseInternal } from "../../utils/env"; +import { RouteComponentProps } from "inferno-router/dist/Route"; +import { IRoutePropsWithFetch } from "../../routes"; interface HomeState { postsRes: RequestState; @@ -129,23 +131,22 @@ type HomeData = RouteDataResponse<{ trendingCommunitiesRes: ListCommunitiesResponse; }>; -function getRss(listingType: ListingType) { - const { sort } = getHomeQueryParams(); - +function getRss(listingType: ListingType, sort: SortType) { let rss: string | undefined = undefined; + const queryString = getQueryString({ sort }); switch (listingType) { case "All": { - rss = `/feeds/all.xml?sort=${sort}`; + rss = "/feeds/all.xml" + queryString; break; } case "Local": { - rss = `/feeds/local.xml?sort=${sort}`; + rss = "/feeds/local.xml" + queryString; break; } case "Subscribed": { const auth = myAuth(); - rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined; + rss = auth ? `/feeds/front/${auth}.xml${queryString}` : undefined; break; } } @@ -167,31 +168,46 @@ function getDataTypeFromQuery(type?: string): DataType { } function getListingTypeFromQuery( - type?: string, - myUserInfo = UserService.Instance.myUserInfo, -): ListingType | undefined { - const myListingType = - myUserInfo?.local_user_view?.local_user?.default_listing_type; - - return type ? (type as ListingType) : myListingType; + type: string | undefined, + fallback: ListingType, +): ListingType { + return type ? (type as ListingType) : fallback; } function getSortTypeFromQuery( - type?: string, - myUserInfo = UserService.Instance.myUserInfo, + type: string | undefined, + fallback: SortType, ): SortType { - const mySortType = myUserInfo?.local_user_view?.local_user?.default_sort_type; - - return (type ? (type as SortType) : mySortType) ?? "Active"; + return type ? (type as SortType) : fallback; } -function getHomeQueryParams() { - return getQueryParams({ - sort: getSortTypeFromQuery, - listingType: getListingTypeFromQuery, - pageCursor: cursor => cursor, - dataType: getDataTypeFromQuery, - }); +type Fallbacks = { + 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( + { + sort: getSortTypeFromQuery, + listingType: getListingTypeFromQuery, + pageCursor: (cursor?: string) => cursor, + 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 = ({ @@ -224,7 +240,15 @@ const LinkButton = ({ ); -export class Home extends Component { +type HomePathProps = Record; +type HomeRouteProps = RouteComponentProps & HomeProps; +export type HomeFetchConfig = IRoutePropsWithFetch< + HomeData, + HomePathProps, + HomeProps +>; + +export class Home extends Component { private isoData = setIsoData(this.context); state: HomeState = { postsRes: EMPTY_REQUEST, @@ -310,20 +334,13 @@ export class Home extends Component { } static async fetchInitialData({ - query: { dataType: urlDataType, listingType, pageCursor, sort: urlSort }, - site, + query: { listingType, dataType, sort, pageCursor }, headers, - }: InitialFetchRequest>): Promise { + }: InitialFetchRequest): Promise { const client = wrapClient( 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> = Promise.resolve(EMPTY_REQUEST); let commentsFetch: Promise> = @@ -331,7 +348,7 @@ export class Home extends Component { if (dataType === DataType.Post) { const getPostsForm: GetPosts = { - type_, + type_: listingType, page_cursor: pageCursor, limit: fetchLimit, sort, @@ -343,7 +360,7 @@ export class Home extends Component { const getCommentsForm: GetComments = { limit: fetchLimit, sort: postToCommentSortType(sort), - type_, + type_: listingType, saved_only: false, }; @@ -635,7 +652,7 @@ export class Home extends Component { dataType: urlDataType, listingType: urlListingType, sort: urlSort, - } = getHomeQueryParams(); + } = this.props; const queryParams: QueryParams = { dataType: getDataTypeString(dataType ?? urlDataType), @@ -679,7 +696,7 @@ export class Home extends Component { } get listings() { - const { dataType } = getHomeQueryParams(); + const { dataType } = this.props; const siteRes = this.state.siteRes; if (dataType === DataType.Post) { @@ -771,7 +788,7 @@ export class Home extends Component { } get selects() { - const { listingType, dataType, sort } = getHomeQueryParams(); + const { listingType, dataType, sort } = this.props; return (
@@ -799,6 +816,7 @@ export class Home extends Component { {getRss( listingType ?? this.state.siteRes.site_view.local_site.default_post_listing_type, + sort, )}

@@ -817,7 +835,7 @@ export class Home extends Component { } async fetchData() { - const { dataType, pageCursor, listingType, sort } = getHomeQueryParams(); + const { dataType, pageCursor, listingType, sort } = this.props; if (dataType === DataType.Post) { this.setState({ postsRes: LOADING_REQUEST }); diff --git a/src/shared/components/home/instances.tsx b/src/shared/components/home/instances.tsx index e437a4b6..81d213d6 100644 --- a/src/shared/components/home/instances.tsx +++ b/src/shared/components/home/instances.tsx @@ -22,6 +22,8 @@ import { HtmlTags } from "../common/html-tags"; import { Spinner } from "../common/icon"; import Tabs from "../common/tabs"; import { getHttpBaseInternal } from "../../utils/env"; +import { RouteComponentProps } from "inferno-router/dist/Route"; +import { IRoutePropsWithFetch } from "../../routes"; type InstancesData = RouteDataResponse<{ federatedInstancesResponse: GetFederatedInstancesResponse; @@ -33,7 +35,15 @@ interface InstancesState { isIsomorphic: boolean; } -export class Instances extends Component { +type InstancesRouteProps = RouteComponentProps> & + Record; +export type InstancesFetchConfig = IRoutePropsWithFetch< + InstancesData, + Record, + Record +>; + +export class Instances extends Component { private isoData = setIsoData(this.context); state: InstancesState = { instancesRes: EMPTY_REQUEST, diff --git a/src/shared/components/home/login.tsx b/src/shared/components/home/login.tsx index 0927d10e..c8705fed 100644 --- a/src/shared/components/home/login.tsx +++ b/src/shared/components/home/login.tsx @@ -17,17 +17,21 @@ import { Spinner } from "../common/icon"; import PasswordInput from "../common/password-input"; import TotpModal from "../common/totp-modal"; import { UnreadCounterService } from "../../services"; +import { RouteData } from "../../interfaces"; +import { IRoutePropsWithFetch } from "../../routes"; interface LoginProps { prev?: string; } -const getLoginQueryParams = () => - getQueryParams({ - prev(param) { - return param ? decodeURIComponent(param) : undefined; +export function getLoginQueryParams(source?: string): LoginProps { + return getQueryParams( + { + prev: (param?: string) => param, }, - }); + source, + ); +} interface State { loginRes: RequestState; @@ -50,7 +54,7 @@ async function handleLoginSuccess(i: Login, loginRes: LoginResponse) { refreshTheme(); } - const { prev } = getLoginQueryParams(); + const { prev } = i.props; prev ? i.props.history.replace(prev) @@ -114,10 +118,14 @@ function handleClose2faModal(i: Login) { i.setState({ show2faModal: false }); } -export class Login extends Component< - RouteComponentProps>, - State -> { +type LoginRouteProps = RouteComponentProps> & LoginProps; +export type LoginFetchConfig = IRoutePropsWithFetch< + RouteData, + Record, + LoginProps +>; + +export class Login extends Component { private isoData = setIsoData(this.context); state: State = { diff --git a/src/shared/components/modlog.tsx b/src/shared/components/modlog.tsx index 729e814b..fdd90f30 100644 --- a/src/shared/components/modlog.tsx +++ b/src/shared/components/modlog.tsx @@ -63,6 +63,7 @@ import { SearchableSelect } from "./common/searchable-select"; import { CommunityLink } from "./community/community-link"; import { PersonListing } from "./person/person-listing"; import { getHttpBaseInternal } from "../utils/env"; +import { IRoutePropsWithFetch } from "../routes"; type FilterType = "mod" | "user"; @@ -97,13 +98,17 @@ interface ModlogType { when_: string; } -const getModlogQueryParams = () => - getQueryParams({ - actionType: getActionFromString, - modId: getIdFromString, - userId: getIdFromString, - page: getPageFromString, - }); +export function getModlogQueryParams(source?: string): ModlogProps { + return getQueryParams( + { + actionType: getActionFromString, + modId: getIdFromString, + userId: getIdFromString, + page: getPageFromString, + }, + source, + ); +} interface ModlogState { res: RequestState; @@ -117,8 +122,8 @@ interface ModlogState { interface ModlogProps { page: number; - userId?: number | null; - modId?: number | null; + userId?: number; + modId?: number; actionType: ModlogActionType; } @@ -632,10 +637,15 @@ async function createNewOptions({ } } -export class Modlog extends Component< - RouteComponentProps<{ communityId?: string }>, - ModlogState -> { +type ModlogPathProps = { communityId?: string }; +type ModlogRouteProps = RouteComponentProps & ModlogProps; +export type ModlogFetchConfig = IRoutePropsWithFetch< + ModlogData, + ModlogPathProps, + ModlogProps +>; + +export class Modlog extends Component { private isoData = setIsoData(this.context); state: ModlogState = { @@ -648,10 +658,7 @@ export class Modlog extends Component< isIsomorphic: false, }; - constructor( - props: RouteComponentProps<{ communityId?: string }>, - context: any, - ) { + constructor(props: ModlogRouteProps, context: any) { super(props, context); this.handlePageChange = this.handlePageChange.bind(this); this.handleUserChange = this.handleUserChange.bind(this); @@ -687,7 +694,7 @@ export class Modlog extends Component< async componentDidMount() { if (!this.state.isIsomorphic) { - const { modId, userId } = getModlogQueryParams(); + const { modId, userId } = this.props; const promises = [this.refetch()]; if (userId) { @@ -774,7 +781,7 @@ export class Modlog extends Component< userSearchOptions, modSearchOptions, } = this.state; - const { actionType, modId, userId } = getModlogQueryParams(); + const { actionType, modId, userId } = this.props; return (
@@ -873,7 +880,7 @@ export class Modlog extends Component< ); case "success": { - const page = getModlogQueryParams().page; + const page = this.props.page; return (
@@ -909,15 +916,15 @@ export class Modlog extends Component< } handleUserChange(option: Choice) { - this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 }); + this.updateUrl({ userId: getIdFromString(option.value), page: 1 }); } 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) => { - const { userId } = getModlogQueryParams(); + const { userId } = this.props; const { userSearchOptions } = this.state; this.setState({ loadingUserSearch: true }); @@ -934,7 +941,7 @@ export class Modlog extends Component< }); handleSearchMods = debounce(async (text: string) => { - const { modId } = getModlogQueryParams(); + const { modId } = this.props; const { modSearchOptions } = this.state; this.setState({ loadingModSearch: true }); @@ -956,7 +963,7 @@ export class Modlog extends Component< actionType: urlActionType, modId: urlModId, userId: urlUserId, - } = getModlogQueryParams(); + } = this.props; const queryParams: QueryParams = { page: (page ?? urlPage).toString(), @@ -977,7 +984,7 @@ export class Modlog extends Component< } async refetch() { - const { actionType, page, modId, userId } = getModlogQueryParams(); + const { actionType, page, modId, userId } = this.props; const { communityId: urlCommunityId } = this.props.match.params; const communityId = getIdFromString(urlCommunityId); @@ -988,10 +995,10 @@ export class Modlog extends Component< page, limit: fetchLimit, type_: actionType, - other_person_id: userId ?? undefined, + other_person_id: userId, mod_person_id: !this.isoData.site_res.site_view.local_site .hide_modlog_mod_names - ? modId ?? undefined + ? modId : undefined, }), }); @@ -1008,25 +1015,25 @@ export class Modlog extends Component< static async fetchInitialData({ headers, - path, - query: { modId: urlModId, page, userId: urlUserId, actionType }, + query: { page, userId, modId: modId_, actionType }, + match: { + params: { communityId: urlCommunityId }, + }, site, - }: InitialFetchRequest>): Promise { + }: InitialFetchRequest): Promise { const client = wrapClient( new LemmyHttp(getHttpBaseInternal(), { headers }), ); - const pathSplit = path.split("/"); - const communityId = getIdFromString(pathSplit[2]); + const communityId = getIdFromString(urlCommunityId); const modId = !site.site_view.local_site.hide_modlog_mod_names - ? getIdFromString(urlModId) + ? modId_ : undefined; - const userId = getIdFromString(urlUserId); const modlogForm: GetModlog = { - page: getPageFromString(page), + page, limit: fetchLimit, community_id: communityId, - type_: getActionFromString(actionType), + type_: actionType, mod_person_id: modId, other_person_id: userId, }; diff --git a/src/shared/components/person/inbox.tsx b/src/shared/components/person/inbox.tsx index 3af065c5..26b8a7e0 100644 --- a/src/shared/components/person/inbox.tsx +++ b/src/shared/components/person/inbox.tsx @@ -80,6 +80,8 @@ import { Icon, Spinner } from "../common/icon"; import { Paginator } from "../common/paginator"; import { PrivateMessage } from "../private_message/private-message"; import { getHttpBaseInternal } from "../../utils/env"; +import { RouteComponentProps } from "inferno-router/dist/Route"; +import { IRoutePropsWithFetch } from "../../routes"; enum UnreadOrAll { Unread, @@ -126,7 +128,15 @@ interface InboxState { isIsomorphic: boolean; } -export class Inbox extends Component { +type InboxRouteProps = RouteComponentProps> & + Record; +export type InboxFetchConfig = IRoutePropsWithFetch< + InboxData, + Record, + Record +>; + +export class Inbox extends Component { private isoData = setIsoData(this.context); state: InboxState = { unreadOrAll: UnreadOrAll.Unread, diff --git a/src/shared/components/person/profile.tsx b/src/shared/components/person/profile.tsx index 2367f40b..e74a324d 100644 --- a/src/shared/components/person/profile.tsx +++ b/src/shared/components/person/profile.tsx @@ -94,6 +94,7 @@ import { CommunityLink } from "../community/community-link"; import { PersonDetails } from "./person-details"; import { PersonListing } from "./person-listing"; import { getHttpBaseInternal } from "../../utils/env"; +import { IRoutePropsWithFetch } from "../../routes"; type ProfileData = RouteDataResponse<{ personResponse: GetPersonDetailsResponse; @@ -117,12 +118,15 @@ interface ProfileProps { page: number; } -function getProfileQueryParams() { - return getQueryParams({ - view: getViewFromProps, - page: getPageFromString, - sort: getSortTypeFromQuery, - }); +export function getProfileQueryParams(source?: string): ProfileProps { + return getQueryParams( + { + view: getViewFromProps, + page: getPageFromString, + sort: getSortTypeFromQuery, + }, + source, + ); } function getSortTypeFromQuery(sort?: string): SortType { @@ -171,10 +175,15 @@ function isPersonBlocked(personRes: RequestState) { ); } -export class Profile extends Component< - RouteComponentProps<{ username: string }>, - ProfileState -> { +type ProfilePathProps = { username: string }; +type ProfileRouteProps = RouteComponentProps & ProfileProps; +export type ProfileFetchConfig = IRoutePropsWithFetch< + ProfileData, + ProfilePathProps, + ProfileProps +>; + +export class Profile extends Component { private isoData = setIsoData(this.context); state: ProfileState = { personRes: EMPTY_REQUEST, @@ -186,7 +195,7 @@ export class Profile extends Component< isIsomorphic: false, }; - constructor(props: RouteComponentProps<{ username: string }>, context: any) { + constructor(props: ProfileRouteProps, context: any) { super(props, context); this.handleSortChange = this.handleSortChange.bind(this); @@ -248,7 +257,7 @@ export class Profile extends Component< } async fetchUserData() { - const { page, sort, view } = getProfileQueryParams(); + const { page, sort, view } = this.props; this.setState({ personRes: LOADING_REQUEST }); const personRes = await HttpService.client.getPersonDetails({ @@ -278,22 +287,23 @@ export class Profile extends Component< static async fetchInitialData({ headers, - path, - query: { page, sort, view: urlView }, - }: InitialFetchRequest>): Promise { + query: { view, sort, page }, + match: { + params: { username }, + }, + }: InitialFetchRequest< + ProfilePathProps, + ProfileProps + >): Promise { const client = wrapClient( new LemmyHttp(getHttpBaseInternal(), { headers }), ); - const pathSplit = path.split("/"); - - const username = pathSplit[2]; - const view = getViewFromProps(urlView); const form: GetPersonDetails = { username: username, - sort: getSortTypeFromQuery(sort), + sort, saved_only: view === PersonDetailsView.Saved, - page: getPageFromString(page), + page, limit: fetchLimit, }; @@ -321,7 +331,7 @@ export class Profile extends Component< case "success": { const siteRes = this.state.siteRes; const personRes = this.state.personRes.data; - const { page, sort, view } = getProfileQueryParams(); + const { page, sort, view } = this.props; return (
@@ -415,7 +425,7 @@ export class Profile extends Component< } getRadio(view: PersonDetailsView) { - const { view: urlView } = getProfileQueryParams(); + const { view: urlView } = this.props; const active = view === urlView; const radioId = randomStr(); @@ -442,10 +452,10 @@ export class Profile extends Component< } get selects() { - const { sort } = getProfileQueryParams(); + const { sort } = this.props; const { username } = this.props.match.params; - const profileRss = `/feeds/u/${username}.xml?sort=${sort}`; + const profileRss = `/feeds/u/${username}.xml${getQueryString({ sort })}`; return (
@@ -713,11 +723,7 @@ export class Profile extends Component< } async updateUrl({ page, sort, view }: Partial) { - const { - page: urlPage, - sort: urlSort, - view: urlView, - } = getProfileQueryParams(); + const { page: urlPage, sort: urlSort, view: urlView } = this.props; const queryParams: QueryParams = { page: (page ?? urlPage).toString(), diff --git a/src/shared/components/person/registration-applications.tsx b/src/shared/components/person/registration-applications.tsx index 8a5efbda..8b20c2b8 100644 --- a/src/shared/components/person/registration-applications.tsx +++ b/src/shared/components/person/registration-applications.tsx @@ -27,6 +27,8 @@ import { Paginator } from "../common/paginator"; import { RegistrationApplication } from "../common/registration-application"; import { UnreadCounterService } from "../../services"; import { getHttpBaseInternal } from "../../utils/env"; +import { RouteComponentProps } from "inferno-router/dist/Route"; +import { IRoutePropsWithFetch } from "../../routes"; enum RegistrationState { Unread, @@ -46,8 +48,18 @@ interface RegistrationApplicationsState { isIsomorphic: boolean; } +type RegistrationApplicationsRouteProps = RouteComponentProps< + Record +> & + Record; +export type RegistrationApplicationsFetchConfig = IRoutePropsWithFetch< + RegistrationApplicationsData, + Record, + Record +>; + export class RegistrationApplications extends Component< - any, + RegistrationApplicationsRouteProps, RegistrationApplicationsState > { private isoData = setIsoData(this.context); diff --git a/src/shared/components/person/reports.tsx b/src/shared/components/person/reports.tsx index c3c59161..fd6e4bab 100644 --- a/src/shared/components/person/reports.tsx +++ b/src/shared/components/person/reports.tsx @@ -50,6 +50,8 @@ import { PostReport } from "../post/post-report"; import { PrivateMessageReport } from "../private_message/private-message-report"; import { UnreadCounterService } from "../../services"; import { getHttpBaseInternal } from "../../utils/env"; +import { RouteComponentProps } from "inferno-router/dist/Route"; +import { IRoutePropsWithFetch } from "../../routes"; enum UnreadOrAll { Unread, @@ -93,7 +95,15 @@ interface ReportsState { isIsomorphic: boolean; } -export class Reports extends Component { +type ReportsRouteProps = RouteComponentProps> & + Record; +export type ReportsFetchConfig = IRoutePropsWithFetch< + ReportsData, + Record, + Record +>; + +export class Reports extends Component { private isoData = setIsoData(this.context); state: ReportsState = { commentReportsRes: EMPTY_REQUEST, diff --git a/src/shared/components/person/settings.tsx b/src/shared/components/person/settings.tsx index 2760958a..a3f9d1f2 100644 --- a/src/shared/components/person/settings.tsx +++ b/src/shared/components/person/settings.tsx @@ -68,6 +68,8 @@ import TotpModal from "../common/totp-modal"; import { LoadingEllipses } from "../common/loading-ellipses"; import { refreshTheme, setThemeOverride } from "../../utils/browser"; import { getHttpBaseInternal } from "../../utils/env"; +import { IRoutePropsWithFetch } from "../../routes"; +import { RouteComponentProps } from "inferno-router/dist/Route"; type SettingsData = RouteDataResponse<{ instancesRes: GetFederatedInstancesResponse; @@ -193,7 +195,15 @@ function handleClose2faModal(i: Settings) { i.setState({ show2faModal: false }); } -export class Settings extends Component { +type SettingsRouteProps = RouteComponentProps> & + Record; +export type SettingsFetchConfig = IRoutePropsWithFetch< + SettingsData, + Record, + Record +>; + +export class Settings extends Component { private isoData = setIsoData(this.context); exportSettingsLink = createRef(); diff --git a/src/shared/components/post/create-post.tsx b/src/shared/components/post/create-post.tsx index e9fcb61e..8adf5080 100644 --- a/src/shared/components/post/create-post.tsx +++ b/src/shared/components/post/create-post.tsx @@ -5,7 +5,6 @@ import { setIsoData, } from "@utils/app"; import { getIdFromString, getQueryParams } from "@utils/helpers"; -import type { QueryParams } from "@utils/types"; import { Choice, RouteDataResponse } from "@utils/types"; import { Component } from "inferno"; import { RouteComponentProps } from "inferno-router/dist/Route"; @@ -30,6 +29,7 @@ import { HtmlTags } from "../common/html-tags"; import { Spinner } from "../common/icon"; import { PostForm } from "./post-form"; import { getHttpBaseInternal } from "../../utils/env"; +import { IRoutePropsWithFetch } from "../../routes"; export interface CreatePostProps { communityId?: number; @@ -40,10 +40,13 @@ type CreatePostData = RouteDataResponse<{ initialCommunitiesRes: ListCommunitiesResponse; }>; -function getCreatePostQueryParams() { - return getQueryParams({ - communityId: getIdFromString, - }); +export function getCreatePostQueryParams(source?: string): CreatePostProps { + return getQueryParams( + { + communityId: getIdFromString, + }, + source, + ); } function fetchCommunitiesForOptions(client: WrappedLemmyHttp) { @@ -58,8 +61,17 @@ interface CreatePostState { isIsomorphic: boolean; } +type CreatePostPathProps = Record; +type CreatePostRouteProps = RouteComponentProps & + CreatePostProps; +export type CreatePostFetchConfig = IRoutePropsWithFetch< + CreatePostData, + CreatePostPathProps, + CreatePostProps +>; + export class CreatePost extends Component< - RouteComponentProps>, + CreatePostRouteProps, CreatePostState > { private isoData = setIsoData(this.context); @@ -70,7 +82,7 @@ export class CreatePost extends Component< isIsomorphic: false, }; - constructor(props: RouteComponentProps>, context: any) { + constructor(props: CreatePostRouteProps, context: any) { super(props, context); this.handlePostCreate = this.handlePostCreate.bind(this); @@ -102,9 +114,7 @@ export class CreatePost extends Component< } } - async fetchCommunity() { - const { communityId } = getCreatePostQueryParams(); - + async fetchCommunity({ communityId }: CreatePostProps) { if (communityId) { const res = await HttpService.client.getCommunity({ id: communityId, @@ -121,7 +131,7 @@ export class CreatePost extends Component< async componentDidMount() { // TODO test this if (!this.state.isIsomorphic) { - const { communityId } = getCreatePostQueryParams(); + const { communityId } = this.props; const initialCommunitiesRes = await fetchCommunitiesForOptions( HttpService.client, @@ -134,7 +144,7 @@ export class CreatePost extends Component< if ( communityId?.toString() !== this.state.selectedCommunityChoice?.value ) { - await this.fetchCommunity(); + await this.fetchCommunity({ communityId }); } else if (!communityId) { this.setState({ selectedCommunityChoice: undefined, @@ -199,15 +209,13 @@ export class CreatePost extends Component< } async updateUrl({ communityId }: Partial) { - const { communityId: urlCommunityId } = getCreatePostQueryParams(); - const locationState = this.props.history.location.state as | PostFormParams | undefined; const url = new URL(location.href); - const newId = (communityId ?? urlCommunityId)?.toString(); + const newId = communityId?.toString(); if (newId !== undefined) { url.searchParams.set("communityId", newId); @@ -215,9 +223,10 @@ export class CreatePost extends Component< 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) { @@ -243,7 +252,8 @@ export class CreatePost extends Component< headers, query: { communityId }, }: InitialFetchRequest< - QueryParams + CreatePostPathProps, + CreatePostProps >): Promise { const client = wrapClient( new LemmyHttp(getHttpBaseInternal(), { headers }), @@ -255,7 +265,7 @@ export class CreatePost extends Component< if (communityId) { const form: GetCommunity = { - id: getIdFromString(communityId), + id: communityId, }; data.communityResponse = await client.getCommunity(form); diff --git a/src/shared/components/post/post-form.tsx b/src/shared/components/post/post-form.tsx index 90bd57ef..2e633911 100644 --- a/src/shared/components/post/post-form.tsx +++ b/src/shared/components/post/post-form.tsx @@ -3,6 +3,7 @@ import { capitalizeFirstLetter, debounce, getIdFromString, + getQueryString, validTitle, validURL, } from "@utils/helpers"; @@ -380,18 +381,14 @@ export class PostForm extends Component { archive.org {I18NextService.i18n.t("archive_link")} ghostarchive.org {I18NextService.i18n.t("archive_link")} diff --git a/src/shared/components/post/post.tsx b/src/shared/components/post/post.tsx index 41bb5559..f7562943 100644 --- a/src/shared/components/post/post.tsx +++ b/src/shared/components/post/post.tsx @@ -98,6 +98,8 @@ import { Icon, Spinner } from "../common/icon"; import { Sidebar } from "../community/sidebar"; import { PostListing } from "./post-listing"; import { getHttpBaseInternal } from "../../utils/env"; +import { RouteComponentProps } from "inferno-router/dist/Route"; +import { IRoutePropsWithFetch } from "../../routes"; const commentsShownInterval = 15; @@ -122,7 +124,18 @@ interface PostState { isIsomorphic: boolean; } -export class Post extends Component { +type PostPathProps = + | { post_id: string; comment_id: never } + | { post_id: never; comment_id: string }; +type PostRouteProps = RouteComponentProps & + Record; +export type PostFetchConfig = IRoutePropsWithFetch< + PostData, + PostPathProps, + Record +>; + +export class Post extends Component { private isoData = setIsoData(this.context); private commentScrollDebounced: () => void; state: PostState = { @@ -235,15 +248,13 @@ export class Post extends Component { static async fetchInitialData({ headers, - path, - }: InitialFetchRequest): Promise { + match, + }: InitialFetchRequest): Promise { const client = wrapClient( new LemmyHttp(getHttpBaseInternal(), { headers }), ); - const pathSplit = path.split("/"); - - const pathType = pathSplit.at(1); - const id = pathSplit.at(2) ? Number(pathSplit.at(2)) : undefined; + const postId = getIdFromProps({ match }); + const commentId = getCommentIdFromProps({ match }); const postForm: GetPost = {}; @@ -254,14 +265,11 @@ export class Post extends Component { saved_only: false, }; - // Set the correct id based on the path type - if (pathType === "post") { - postForm.id = id; - commentsForm.post_id = id; - } else { - postForm.comment_id = id; - commentsForm.parent_id = id; - } + postForm.id = postId; + postForm.comment_id = commentId; + + commentsForm.post_id = postId; + commentsForm.parent_id = commentId; const [postRes, commentsRes] = await Promise.all([ client.getPost(postForm), diff --git a/src/shared/components/private_message/create-private-message.tsx b/src/shared/components/private_message/create-private-message.tsx index fab9f112..ea8d5e3a 100644 --- a/src/shared/components/private_message/create-private-message.tsx +++ b/src/shared/components/private_message/create-private-message.tsx @@ -22,6 +22,8 @@ import { HtmlTags } from "../common/html-tags"; import { Spinner } from "../common/icon"; import { PrivateMessageForm } from "./private-message-form"; import { getHttpBaseInternal } from "../../utils/env"; +import { RouteComponentProps } from "inferno-router/dist/Route"; +import { IRoutePropsWithFetch } from "../../routes"; type CreatePrivateMessageData = RouteDataResponse<{ recipientDetailsResponse: GetPersonDetailsResponse; @@ -34,8 +36,17 @@ interface CreatePrivateMessageState { isIsomorphic: boolean; } +type CreatePrivateMessagePathProps = { recipient_id: string }; +type CreatePrivateMessageRouteProps = + RouteComponentProps & Record; +export type CreatePrivateMessageFetchConfig = IRoutePropsWithFetch< + CreatePrivateMessageData, + CreatePrivateMessagePathProps, + Record +>; + export class CreatePrivateMessage extends Component< - any, + CreatePrivateMessageRouteProps, CreatePrivateMessageState > { private isoData = setIsoData(this.context); @@ -69,12 +80,12 @@ export class CreatePrivateMessage extends Component< static async fetchInitialData({ headers, - path, - }: InitialFetchRequest): Promise { + match, + }: InitialFetchRequest): Promise { const client = wrapClient( new LemmyHttp(getHttpBaseInternal(), { headers }), ); - const person_id = Number(path.split("/").pop()); + const person_id = getRecipientIdFromProps({ match }); const form: GetPersonDetails = { person_id, diff --git a/src/shared/components/remote-fetch.tsx b/src/shared/components/remote-fetch.tsx index bd1ec716..f4b6699f 100644 --- a/src/shared/components/remote-fetch.tsx +++ b/src/shared/components/remote-fetch.tsx @@ -1,6 +1,6 @@ import { setIsoData } from "@utils/app"; import { getQueryParams } from "@utils/helpers"; -import { QueryParams, RouteDataResponse } from "@utils/types"; +import { RouteDataResponse } from "@utils/types"; import { Component, linkEvent } from "inferno"; import { CommunityView, @@ -22,6 +22,8 @@ import { PictrsImage } from "./common/pictrs-image"; import { SubscribeButton } from "./common/subscribe-button"; import { CommunityLink } from "./community/community-link"; import { getHttpBaseInternal } from "../utils/env"; +import { RouteComponentProps } from "inferno-router/dist/Route"; +import { IRoutePropsWithFetch } from "../routes"; interface RemoteFetchProps { uri?: string; @@ -37,16 +39,19 @@ interface RemoteFetchState { followCommunityLoading: boolean; } -const getUriFromQuery = (uri?: string): string | undefined => - uri ? decodeURIComponent(uri) : undefined; +const getUriFromQuery = (uri?: string): string | undefined => uri; -const getRemoteFetchQueryParams = () => - getQueryParams({ - uri: getUriFromQuery, - }); +export function getRemoteFetchQueryParams(source?: string): RemoteFetchProps { + return getQueryParams( + { + uri: getUriFromQuery, + }, + source, + ); +} function uriToQuery(uri: string) { - const match = decodeURIComponent(uri).match(/https?:\/\/(.+)\/c\/(.+)/); + const match = uri.match(/https?:\/\/(.+)\/c\/(.+)/); 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 handleUnfollow = (i: RemoteFetch) => handleToggleFollow(i, false); -export class RemoteFetch extends Component { +type RemoteFetchPathProps = Record; +type RemoteFetchRouteProps = RouteComponentProps & + RemoteFetchProps; +export type RemoteFetchFetchConfig = IRoutePropsWithFetch< + RemoteFetchData, + RemoteFetchPathProps, + RemoteFetchProps +>; + +export class RemoteFetch extends Component< + RemoteFetchRouteProps, + RemoteFetchState +> { private isoData = setIsoData(this.context); state: RemoteFetchState = { resolveObjectRes: EMPTY_REQUEST, @@ -91,7 +108,7 @@ export class RemoteFetch extends Component { followCommunityLoading: false, }; - constructor(props: any, context: any) { + constructor(props: RemoteFetchRouteProps, context: any) { super(props, context); if (FirstLoadService.isFirstLoad) { @@ -107,7 +124,7 @@ export class RemoteFetch extends Component { async componentDidMount() { if (!this.state.isIsomorphic) { - const { uri } = getRemoteFetchQueryParams(); + const { uri } = this.props; if (uri) { this.setState({ resolveObjectRes: LOADING_REQUEST }); @@ -139,7 +156,7 @@ export class RemoteFetch extends Component { get content() { const res = this.state.resolveObjectRes; - const { uri } = getRemoteFetchQueryParams(); + const { uri } = this.props; const remoteCommunityName = uri ? uriToQuery(uri) : "remote community"; switch (res.state) { @@ -204,7 +221,7 @@ export class RemoteFetch extends Component { } get documentTitle(): string { - const { uri } = getRemoteFetchQueryParams(); + const { uri } = this.props; const name = this.isoData.site_res.site_view.site.name; return `${I18NextService.i18n.t("remote_follow")} - ${ uri ? `${uri} - ` : "" @@ -215,7 +232,8 @@ export class RemoteFetch extends Component { headers, query: { uri }, }: InitialFetchRequest< - QueryParams + RemoteFetchPathProps, + RemoteFetchProps >): Promise { const client = wrapClient( new LemmyHttp(getHttpBaseInternal(), { headers }), diff --git a/src/shared/components/search.tsx b/src/shared/components/search.tsx index a6051b52..c1e85ff5 100644 --- a/src/shared/components/search.tsx +++ b/src/shared/components/search.tsx @@ -67,14 +67,16 @@ import { CommunityLink } from "./community/community-link"; import { PersonListing } from "./person/person-listing"; import { PostListing } from "./post/post-listing"; import { getHttpBaseInternal } from "../utils/env"; +import { RouteComponentProps } from "inferno-router/dist/Route"; +import { IRoutePropsWithFetch } from "../routes"; interface SearchProps { q?: string; type: SearchType; sort: SortType; listingType: ListingType; - communityId?: number | null; - creatorId?: number | null; + communityId?: number; + creatorId?: number; page: number; } @@ -112,19 +114,22 @@ const defaultListingType = "All"; const searchTypes = ["All", "Comments", "Posts", "Communities", "Users", "Url"]; -const getSearchQueryParams = () => - getQueryParams({ - q: getSearchQueryFromQuery, - type: getSearchTypeFromQuery, - sort: getSortTypeFromQuery, - listingType: getListingTypeFromQuery, - communityId: getIdFromString, - creatorId: getIdFromString, - page: getPageFromString, - }); +export function getSearchQueryParams(source?: string): SearchProps { + return getQueryParams( + { + q: getSearchQueryFromQuery, + type: getSearchTypeFromQuery, + sort: getSortTypeFromQuery, + listingType: getListingTypeFromQuery, + communityId: getIdFromString, + creatorId: getIdFromString, + page: getPageFromString, + }, + source, + ); +} -const getSearchQueryFromQuery = (q?: string): string | undefined => - q ? decodeURIComponent(q) : undefined; +const getSearchQueryFromQuery = (q?: string): string | undefined => q; function getSearchTypeFromQuery(type_?: string): SearchType { return type_ ? (type_ as SearchType) : defaultSearchType; @@ -240,7 +245,15 @@ function getListing( ); } -export class Search extends Component { +type SearchPathProps = Record; +type SearchRouteProps = RouteComponentProps & SearchProps; +export type SearchFetchConfig = IRoutePropsWithFetch< + SearchData, + SearchPathProps, + SearchProps +>; + +export class Search extends Component { private isoData = setIsoData(this.context); searchInput = createRef(); @@ -255,7 +268,7 @@ export class Search extends Component { isIsomorphic: false, }; - constructor(props: any, context: any) { + constructor(props: SearchRouteProps, context: any) { super(props, context); this.handleSortChange = this.handleSortChange.bind(this); @@ -265,7 +278,7 @@ export class Search extends Component { this.handleCommunityFilterChange.bind(this); this.handleCreatorFilterChange = this.handleCreatorFilterChange.bind(this); - const { q } = getSearchQueryParams(); + const { q } = this.props; this.state.searchText = q; @@ -335,7 +348,7 @@ export class Search extends Component { }), ]; - const { communityId, creatorId } = getSearchQueryParams(); + const { communityId, creatorId } = this.props; if (communityId) { promises.push( @@ -390,12 +403,19 @@ export class Search extends Component { static async fetchInitialData({ headers, - query: { communityId, creatorId, q, type, sort, listingType, page }, - }: InitialFetchRequest>): Promise { + query: { + q: query, + type: searchType, + sort, + listingType: listing_type, + communityId: community_id, + creatorId: creator_id, + page, + }, + }: InitialFetchRequest): Promise { const client = wrapClient( new LemmyHttp(getHttpBaseInternal(), { headers }), ); - const community_id = getIdFromString(communityId); let communityResponse: RequestState = EMPTY_REQUEST; if (community_id) { const getCommunityForm: GetCommunity = { @@ -411,7 +431,6 @@ export class Search extends Component { limit: fetchLimit, }); - const creator_id = getIdFromString(creatorId); let creatorDetailsResponse: RequestState = EMPTY_REQUEST; if (creator_id) { @@ -422,8 +441,6 @@ export class Search extends Component { creatorDetailsResponse = await client.getPersonDetails(getCreatorForm); } - const query = getSearchQueryFromQuery(q); - let searchResponse: RequestState = EMPTY_REQUEST; let resolveObjectResponse: RequestState = EMPTY_REQUEST; @@ -433,10 +450,10 @@ export class Search extends Component { q: query, community_id, creator_id, - type_: getSearchTypeFromQuery(type), - sort: getSortTypeFromQuery(sort), - listing_type: getListingTypeFromQuery(listingType), - page: getPageFromString(page), + type_: searchType, + sort, + listing_type, + page, limit: fetchLimit, }; @@ -466,13 +483,13 @@ export class Search extends Component { } get documentTitle(): string { - const { q } = getSearchQueryParams(); + const { q } = this.props; const name = this.state.siteRes.site_view.site.name; return `${I18NextService.i18n.t("search")} - ${q ? `${q} - ` : ""}${name}`; } render() { - const { type, page } = getSearchQueryParams(); + const { type, page } = this.props; return (
@@ -555,8 +572,7 @@ export class Search extends Component { } get selects() { - const { type, listingType, sort, communityId, creatorId } = - getSearchQueryParams(); + const { type, listingType, sort, communityId, creatorId } = this.props; const { communitySearchOptions, creatorSearchOptions, @@ -664,7 +680,7 @@ export class Search extends Component { ); } - const { sort } = getSearchQueryParams(); + const { sort } = this.props; // Sort it if (sort === "New") { @@ -959,7 +975,7 @@ export class Search extends Component { async search() { const { searchText: q } = this.state; const { communityId, creatorId, type, sort, listingType, page } = - getSearchQueryParams(); + this.props; if (q) { this.setState({ searchRes: LOADING_REQUEST }); @@ -991,7 +1007,7 @@ export class Search extends Component { handleCreatorSearch = debounce(async (text: string) => { if (text.length > 0) { - const { creatorId } = getSearchQueryParams(); + const { creatorId } = this.props; const { creatorSearchOptions } = this.state; this.setState({ searchCreatorLoading: true }); @@ -1009,7 +1025,7 @@ export class Search extends Component { handleCommunitySearch = debounce(async (text: string) => { if (text.length > 0) { - const { communityId } = getSearchQueryParams(); + const { communityId } = this.props; const { communitySearchOptions } = this.state; this.setState({ @@ -1053,14 +1069,14 @@ export class Search extends Component { handleCommunityFilterChange({ value }: Choice) { this.updateUrl({ - communityId: getIdFromString(value) ?? null, + communityId: getIdFromString(value), page: 1, }); } handleCreatorFilterChange({ value }: Choice) { this.updateUrl({ - creatorId: getIdFromString(value) ?? null, + creatorId: getIdFromString(value), page: 1, }); } @@ -1095,13 +1111,9 @@ export class Search extends Component { sort: urlSort, creatorId: urlCreatorId, page: urlPage, - } = getSearchQueryParams(); + } = this.props; - let query = q ?? this.state.searchText ?? urlQ; - - if (query && query.length > 0) { - query = encodeURIComponent(query); - } + const query = q ?? this.state.searchText ?? urlQ; const queryParams: QueryParams = { q: query, diff --git a/src/shared/interfaces.ts b/src/shared/interfaces.ts index 62f23b2e..1fd823a4 100644 --- a/src/shared/interfaces.ts +++ b/src/shared/interfaces.ts @@ -5,8 +5,8 @@ import { GetSiteResponse, PersonMention, } from "lemmy-js-client"; -import type { ParsedQs } from "qs"; import { RequestState } from "./services/HttpService"; +import { Match } from "inferno-router/dist/Route"; /** * This contains serialized data, it needs to be deserialized before use. @@ -30,9 +30,13 @@ declare global { } } -export interface InitialFetchRequest { +export interface InitialFetchRequest< + P extends Record = Record, + T extends Record = Record, +> { path: string; query: T; + match: Match

; site: GetSiteResponse; headers: { [key: string]: string }; } diff --git a/src/shared/routes.ts b/src/shared/routes.ts index 3cfdee28..7ed5a022 100644 --- a/src/shared/routes.ts +++ b/src/shared/routes.ts @@ -1,45 +1,107 @@ -import { IRouteProps } from "inferno-router/dist/Route"; -import { Communities } from "./components/community/communities"; -import { Community } from "./components/community/community"; +import { IRouteProps, RouteComponentProps } from "inferno-router/dist/Route"; +import { + Communities, + CommunitiesFetchConfig, + getCommunitiesQueryParams, +} from "./components/community/communities"; +import { + Community, + CommunityFetchConfig, + getCommunityQueryParams, +} from "./components/community/community"; import { CreateCommunity } from "./components/community/create-community"; -import { AdminSettings } from "./components/home/admin-settings"; -import { Home } from "./components/home/home"; -import { Instances } from "./components/home/instances"; +import { + AdminSettings, + 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 { Login } from "./components/home/login"; +import { + Login, + LoginFetchConfig, + getLoginQueryParams, +} from "./components/home/login"; import { LoginReset } from "./components/home/login-reset"; import { Setup } from "./components/home/setup"; import { Signup } from "./components/home/signup"; -import { Modlog } from "./components/modlog"; -import { Inbox } from "./components/person/inbox"; +import { + Modlog, + ModlogFetchConfig, + getModlogQueryParams, +} from "./components/modlog"; +import { Inbox, InboxFetchConfig } from "./components/person/inbox"; import { PasswordChange } from "./components/person/password-change"; -import { Profile } from "./components/person/profile"; -import { RegistrationApplications } from "./components/person/registration-applications"; -import { Reports } from "./components/person/reports"; -import { Settings } from "./components/person/settings"; +import { + Profile, + ProfileFetchConfig, + 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 { CreatePost } from "./components/post/create-post"; -import { Post } from "./components/post/post"; -import { CreatePrivateMessage } from "./components/private_message/create-private-message"; -import { RemoteFetch } from "./components/remote-fetch"; -import { Search } from "./components/search"; +import { + CreatePostFetchConfig, + CreatePost, + getCreatePostQueryParams, +} 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 { GetSiteResponse } from "lemmy-js-client"; +import { Inferno } from "inferno"; -interface IRoutePropsWithFetch extends IRouteProps { - fetchInitialData?(req: InitialFetchRequest): Promise; +export interface IRoutePropsWithFetch< + DataT extends RouteData, + PathPropsT extends Record, + QueryPropsT extends Record, +> extends IRouteProps { + fetchInitialData?( + req: InitialFetchRequest, + ): Promise; + getQueryParams?( + source: string | undefined, + siteRes: GetSiteResponse, + ): QueryPropsT; + component: Inferno.ComponentClass< + RouteComponentProps & QueryPropsT + >; } -export const routes: IRoutePropsWithFetch>[] = [ +export const routes: IRoutePropsWithFetch[] = [ { path: `/`, component: Home, fetchInitialData: Home.fetchInitialData, exact: true, - }, + getQueryParams: getHomeQueryParams, + } as HomeFetchConfig, { path: `/login`, component: Login, - }, + getQueryParams: getLoginQueryParams, + } as LoginFetchConfig, { path: `/login_reset`, component: LoginReset, @@ -52,7 +114,8 @@ export const routes: IRoutePropsWithFetch>[] = [ path: `/create_post`, component: CreatePost, fetchInitialData: CreatePost.fetchInitialData, - }, + getQueryParams: getCreatePostQueryParams, + } as CreatePostFetchConfig, { path: `/create_community`, component: CreateCommunity, @@ -61,73 +124,79 @@ export const routes: IRoutePropsWithFetch>[] = [ path: `/create_private_message/:recipient_id`, component: CreatePrivateMessage, fetchInitialData: CreatePrivateMessage.fetchInitialData, - }, + } as CreatePrivateMessageFetchConfig, { path: `/communities`, component: Communities, fetchInitialData: Communities.fetchInitialData, - }, + getQueryParams: getCommunitiesQueryParams, + } as CommunitiesFetchConfig, { path: `/post/:post_id`, component: Post, fetchInitialData: Post.fetchInitialData, - }, + } as PostFetchConfig, { path: `/comment/:comment_id`, component: Post, fetchInitialData: Post.fetchInitialData, - }, + } as PostFetchConfig, { path: `/c/:name`, component: Community, fetchInitialData: Community.fetchInitialData, - }, + getQueryParams: getCommunityQueryParams, + } as CommunityFetchConfig, { path: `/u/:username`, component: Profile, fetchInitialData: Profile.fetchInitialData, - }, + getQueryParams: getProfileQueryParams, + } as ProfileFetchConfig, { path: `/inbox`, component: Inbox, fetchInitialData: Inbox.fetchInitialData, - }, + } as InboxFetchConfig, { path: `/settings`, component: Settings, fetchInitialData: Settings.fetchInitialData, - }, + } as SettingsFetchConfig, { path: `/modlog/:communityId`, component: Modlog, fetchInitialData: Modlog.fetchInitialData, - }, + getQueryParams: getModlogQueryParams, + } as ModlogFetchConfig, { path: `/modlog`, component: Modlog, fetchInitialData: Modlog.fetchInitialData, - }, + getQueryParams: getModlogQueryParams, + } as ModlogFetchConfig, { path: `/setup`, component: Setup }, { path: `/admin`, component: AdminSettings, fetchInitialData: AdminSettings.fetchInitialData, - }, + } as AdminSettingsFetchConfig, { path: `/reports`, component: Reports, fetchInitialData: Reports.fetchInitialData, - }, + } as ReportsFetchConfig, { path: `/registration_applications`, component: RegistrationApplications, fetchInitialData: RegistrationApplications.fetchInitialData, - }, + } as RegistrationApplicationsFetchConfig, { path: `/search`, component: Search, fetchInitialData: Search.fetchInitialData, - }, + getQueryParams: getSearchQueryParams, + } as SearchFetchConfig, { path: `/password_change/:token`, component: PasswordChange, @@ -140,11 +209,12 @@ export const routes: IRoutePropsWithFetch>[] = [ path: `/instances`, component: Instances, fetchInitialData: Instances.fetchInitialData, - }, + } as InstancesFetchConfig, { path: `/legal`, component: Legal }, { path: "/activitypub/externalInteraction", component: RemoteFetch, fetchInitialData: RemoteFetch.fetchInitialData, - }, + getQueryParams: getRemoteFetchQueryParams, + } as RemoteFetchFetchConfig, ]; diff --git a/src/shared/utils/app/community-rss-url.ts b/src/shared/utils/app/community-rss-url.ts index 2c930c3a..e4840206 100644 --- a/src/shared/utils/app/community-rss-url.ts +++ b/src/shared/utils/app/community-rss-url.ts @@ -1,4 +1,6 @@ +import { getQueryString } from "@utils/helpers"; + export default function communityRSSUrl(actorId: string, sort: string): string { const url = new URL(actorId); - return `${url.origin}/feeds${url.pathname}.xml?sort=${sort}`; + return `${url.origin}/feeds${url.pathname}.xml${getQueryString({ sort })}`; } diff --git a/src/shared/utils/app/get-comment-id-from-props.ts b/src/shared/utils/app/get-comment-id-from-props.ts index 548cd294..c3f8eca7 100644 --- a/src/shared/utils/app/get-comment-id-from-props.ts +++ b/src/shared/utils/app/get-comment-id-from-props.ts @@ -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, "match">, +): number | undefined { const id = props.match.params.comment_id; return id ? Number(id) : undefined; } diff --git a/src/shared/utils/app/get-id-from-props.ts b/src/shared/utils/app/get-id-from-props.ts index 345a25e6..97badb9e 100644 --- a/src/shared/utils/app/get-id-from-props.ts +++ b/src/shared/utils/app/get-id-from-props.ts @@ -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, "match">, +): number | undefined { const id = props.match.params.post_id; return id ? Number(id) : undefined; } diff --git a/src/shared/utils/app/get-recipient-id-from-props.ts b/src/shared/utils/app/get-recipient-id-from-props.ts index 5dae458c..73475c5e 100644 --- a/src/shared/utils/app/get-recipient-id-from-props.ts +++ b/src/shared/utils/app/get-recipient-id-from-props.ts @@ -1,4 +1,8 @@ -export default function getRecipientIdFromProps(props: any): number { +import { RouteComponentProps } from "inferno-router/dist/Route"; + +export default function getRecipientIdFromProps( + props: Pick, "match">, +): number { return props.match.params.recipient_id ? Number(props.match.params.recipient_id) : 1; diff --git a/src/shared/utils/helpers/get-query-params.ts b/src/shared/utils/helpers/get-query-params.ts index 2ba22d45..a00e3340 100644 --- a/src/shared/utils/helpers/get-query-params.ts +++ b/src/shared/utils/helpers/get-query-params.ts @@ -1,21 +1,28 @@ -import { isBrowser } from "@utils/browser"; +type Empty = NonNullable; + +type QueryMapping = { + [K in keyof PropsT]-?: ( + input: string | undefined, + fallback: K extends keyof FallbacksT ? FallbacksT[K] : undefined, + ) => PropsT[K]; +}; export default function getQueryParams< - T extends Record, ->(processors: { - [K in keyof T]: (param: string) => T[K]; -}): T { - if (isBrowser()) { - const searchParams = new URLSearchParams(window.location.search); + PropsT, + FallbacksT extends Empty = Empty, +>( + processors: QueryMapping, + source?: string, + fallbacks: FallbacksT = {} as FallbacksT, +): PropsT { + const searchParams = new URLSearchParams(source); - return Array.from(Object.entries(processors)).reduce( - (acc, [key, process]) => ({ - ...acc, - [key]: process(searchParams.get(key)), - }), - {} as T, + const ret: Partial = {}; + for (const key in processors) { + ret[key as string] = processors[key]( + searchParams.get(key) ?? undefined, + fallbacks[key as string], ); } - - return {} as T; + return ret as PropsT; } diff --git a/src/shared/utils/helpers/get-query-string.ts b/src/shared/utils/helpers/get-query-string.ts index d74e2a69..6b9b6992 100644 --- a/src/shared/utils/helpers/get-query-string.ts +++ b/src/shared/utils/helpers/get-query-string.ts @@ -1,10 +1,12 @@ export default function getQueryString< T extends Record, >(obj: T) { - return Object.entries(obj) + const searchParams = new URLSearchParams(); + Object.entries(obj) .filter(([, val]) => val !== undefined && val !== null) - .reduce( - (acc, [key, val], index) => `${acc}${index > 0 ? "&" : ""}${key}=${val}`, - "?", - ); + .forEach(([key, val]) => searchParams.set(key, val ?? "")); + if (searchParams.size) { + return "?" + searchParams.toString(); + } + return ""; }