diff --git a/package.json b/package.json index 5eae485c..3f6c4ed0 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,16 @@ "themes:watch": "sass --watch src/assets/css/themes/:src/assets/css/themes" }, "lint-staged": { - "*.{ts,tsx,js}": ["prettier --write", "eslint --fix"], - "*.{css, scss}": ["prettier --write"], - "package.json": ["sortpack"] + "*.{ts,tsx,js}": [ + "prettier --write", + "eslint --fix" + ], + "*.{css, scss}": [ + "prettier --write" + ], + "package.json": [ + "sortpack" + ] }, "dependencies": { "@babel/plugin-proposal-decorators": "^7.21.0", diff --git a/src/server/index.tsx b/src/server/index.tsx index 3a12ad7e..4cff98b5 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -17,9 +17,10 @@ import { ILemmyConfig, InitialFetchRequest, IsoDataOptionalSite, + RouteData, } from "../shared/interfaces"; import { routes } from "../shared/routes"; -import { RequestState, wrapClient } from "../shared/services/HttpService"; +import { FailedRequestState, wrapClient } from "../shared/services/HttpService"; import { ErrorPageData, favIconPngUrl, @@ -136,7 +137,7 @@ server.get("/*", async (req, res) => { // This bypasses errors, so that the client can hit the error on its own, // in order to remove the jwt on the browser. Necessary for wrong jwts let site: GetSiteResponse | undefined = undefined; - const routeData: RequestState[] = []; + let routeData: RouteData = {}; let errorPageData: ErrorPageData | undefined = undefined; let try_site = await client.getSite(getSiteForm); if (try_site.state === "failed" && try_site.msg == "not_logged_in") { @@ -160,7 +161,7 @@ server.get("/*", async (req, res) => { return res.redirect("/setup"); } - if (site) { + if (site && activeRoute?.fetchInitialData) { const initialFetchReq: InitialFetchRequest = { client, auth, @@ -169,26 +170,23 @@ server.get("/*", async (req, res) => { site, }; - if (activeRoute?.fetchInitialData) { - routeData.push( - ...(await Promise.all([ - ...activeRoute.fetchInitialData(initialFetchReq), - ])) - ); - } + routeData = await activeRoute.fetchInitialData(initialFetchReq); } } else if (try_site.state === "failed") { errorPageData = getErrorPageData(new Error(try_site.msg), site); } + const error = Object.values(routeData).find( + res => res.state === "failed" + ) as FailedRequestState | undefined; + // Redirect to the 404 if there's an API error - if (routeData[0] && routeData[0].state === "failed") { - const error = routeData[0].msg; - console.error(error); - if (error === "instance_is_private") { + if (error) { + console.error(error.msg); + if (error.msg === "instance_is_private") { return res.redirect(`/signup`); } else { - errorPageData = getErrorPageData(new Error(error), site); + errorPageData = getErrorPageData(new Error(error.msg), site); } } diff --git a/src/shared/components/common/pictrs-image.tsx b/src/shared/components/common/pictrs-image.tsx index 27d1cc5f..f975e4ac 100644 --- a/src/shared/components/common/pictrs-image.tsx +++ b/src/shared/components/common/pictrs-image.tsx @@ -22,7 +22,7 @@ export class PictrsImage extends Component { render() { return ( - + diff --git a/src/shared/components/community/communities.tsx b/src/shared/components/community/communities.tsx index 62326943..3eb7bd3a 100644 --- a/src/shared/components/community/communities.tsx +++ b/src/shared/components/community/communities.tsx @@ -12,6 +12,7 @@ import { FirstLoadService } from "../../services/FirstLoadService"; import { HttpService, RequestState } from "../../services/HttpService"; import { QueryParams, + RouteDataResponse, editCommunity, getPageFromString, getQueryParams, @@ -30,6 +31,10 @@ import { CommunityLink } from "./community-link"; const communityLimit = 50; +type CommunitiesData = RouteDataResponse<{ + listCommunitiesResponse: ListCommunitiesResponse; +}>; + interface CommunitiesState { listCommunitiesResponse: RequestState; siteRes: GetSiteResponse; @@ -47,7 +52,7 @@ function getListingTypeFromQuery(listingType?: string): ListingType { } export class Communities extends Component { - private isoData = setIsoData(this.context); + private isoData = setIsoData(this.context); state: CommunitiesState = { listCommunitiesResponse: { state: "empty" }, siteRes: this.isoData.site_res, @@ -62,9 +67,11 @@ export class Communities extends Component { // Only fetch the data if coming from another route if (FirstLoadService.isFirstLoad) { + const { listCommunitiesResponse } = this.isoData.routeData; + this.state = { ...this.state, - listCommunitiesResponse: this.isoData.routeData[0], + listCommunitiesResponse, isIsomorphic: true, }; } @@ -274,13 +281,13 @@ export class Communities extends Component { i.context.router.history.push(`/search?q=${searchParamEncoded}`); } - static fetchInitialData({ + static async fetchInitialData({ query: { listingType, page }, client, auth, - }: InitialFetchRequest>): Promise< - RequestState - >[] { + }: InitialFetchRequest< + QueryParams + >): Promise { const listCommunitiesForm: ListCommunities = { type_: getListingTypeFromQuery(listingType), sort: "TopMonth", @@ -289,7 +296,11 @@ export class Communities extends Component { auth: auth, }; - return [client.listCommunities(listCommunitiesForm)]; + return { + listCommunitiesResponse: await client.listCommunities( + listCommunitiesForm + ), + }; } getCommunitiesQueryParams() { diff --git a/src/shared/components/community/community.tsx b/src/shared/components/community/community.tsx index f2d7ad72..6f3c9112 100644 --- a/src/shared/components/community/community.tsx +++ b/src/shared/components/community/community.tsx @@ -63,6 +63,7 @@ import { FirstLoadService } from "../../services/FirstLoadService"; import { HttpService, RequestState } from "../../services/HttpService"; import { QueryParams, + RouteDataResponse, commentsToFlatNodes, communityRSSUrl, editComment, @@ -100,6 +101,12 @@ import { SiteSidebar } from "../home/site-sidebar"; import { PostListings } from "../post/post-listings"; import { CommunityLink } from "./community-link"; +type CommunityData = RouteDataResponse<{ + communityRes: GetCommunityResponse; + postsRes: GetPostsResponse; + commentsRes: GetCommentsResponse; +}>; + interface State { communityRes: RequestState; postsRes: RequestState; @@ -140,7 +147,7 @@ export class Community extends Component< RouteComponentProps<{ name: string }>, State > { - private isoData = setIsoData(this.context); + private isoData = setIsoData(this.context); state: State = { communityRes: { state: "empty" }, postsRes: { state: "empty" }, @@ -194,13 +201,14 @@ export class Community extends Component< // Only fetch the data if coming from another route if (FirstLoadService.isFirstLoad) { - const [communityRes, postsRes, commentsRes] = this.isoData.routeData; + const { communityRes, commentsRes, postsRes } = this.isoData.routeData; + this.state = { ...this.state, + isIsomorphic: true, + commentsRes, communityRes, postsRes, - commentsRes, - isIsomorphic: true, }; } } @@ -227,23 +235,21 @@ export class Community extends Component< saveScrollPosition(this.context); } - static fetchInitialData({ + static async fetchInitialData({ client, path, query: { dataType: urlDataType, page: urlPage, sort: urlSort }, auth, }: InitialFetchRequest>): Promise< - RequestState - >[] { + Promise + > { const pathSplit = path.split("/"); - const promises: Promise>[] = []; const communityName = pathSplit[2]; const communityForm: GetCommunity = { name: communityName, auth, }; - promises.push(client.getCommunity(communityForm)); const dataType = getDataTypeFromQuery(urlDataType); @@ -251,6 +257,11 @@ export class Community extends Component< const page = getPageFromString(urlPage); + let postsResponse: RequestState = { state: "empty" }; + let commentsResponse: RequestState = { + state: "empty", + }; + if (dataType === DataType.Post) { const getPostsForm: GetPosts = { community_name: communityName, @@ -261,8 +272,8 @@ export class Community extends Component< saved_only: false, auth, }; - promises.push(client.getPosts(getPostsForm)); - promises.push(Promise.resolve({ state: "empty" })); + + postsResponse = await client.getPosts(getPostsForm); } else { const getCommentsForm: GetComments = { community_name: communityName, @@ -273,11 +284,15 @@ export class Community extends Component< saved_only: false, auth, }; - promises.push(Promise.resolve({ state: "empty" })); - promises.push(client.getComments(getCommentsForm)); + + commentsResponse = await client.getComments(getCommentsForm); } - return promises; + return { + communityRes: await client.getCommunity(communityForm), + commentsRes: commentsResponse, + postsRes: postsResponse, + }; } get documentTitle(): string { diff --git a/src/shared/components/home/admin-settings.tsx b/src/shared/components/home/admin-settings.tsx index 302e96bd..ad7ac931 100644 --- a/src/shared/components/home/admin-settings.tsx +++ b/src/shared/components/home/admin-settings.tsx @@ -14,6 +14,7 @@ import { InitialFetchRequest } from "../../interfaces"; import { FirstLoadService } from "../../services/FirstLoadService"; import { HttpService, RequestState } from "../../services/HttpService"; import { + RouteDataResponse, capitalizeFirstLetter, fetchThemeList, myAuthRequired, @@ -32,6 +33,11 @@ import RateLimitForm from "./rate-limit-form"; import { SiteForm } from "./site-form"; import { TaglineForm } from "./tagline-form"; +type AdminSettingsData = RouteDataResponse<{ + bannedRes: BannedPersonsResponse; + instancesRes: GetFederatedInstancesResponse; +}>; + interface AdminSettingsState { siteRes: GetSiteResponse; banned: PersonView[]; @@ -46,7 +52,7 @@ interface AdminSettingsState { } export class AdminSettings extends Component { - private isoData = setIsoData(this.context); + private isoData = setIsoData(this.context); state: AdminSettingsState = { siteRes: this.isoData.site_res, banned: [], @@ -70,7 +76,8 @@ export class AdminSettings extends Component { // Only fetch the data if coming from another route if (FirstLoadService.isFirstLoad) { - const [bannedRes, instancesRes] = this.isoData.routeData; + const { bannedRes, instancesRes } = this.isoData.routeData; + this.state = { ...this.state, bannedRes, @@ -80,47 +87,18 @@ export class AdminSettings extends Component { } } - async fetchData() { - this.setState({ - bannedRes: { state: "loading" }, - instancesRes: { state: "loading" }, - themeList: [], - loading: true, - }); - - const auth = myAuthRequired(); - - const [bannedRes, instancesRes, themeList] = await Promise.all([ - HttpService.client.getBannedPersons({ auth }), - HttpService.client.getFederatedInstances({ auth }), - fetchThemeList(), - ]); - - this.setState({ - bannedRes, - instancesRes, - themeList, - loading: false, - }); - } - - static fetchInitialData({ + static async fetchInitialData({ auth, client, - }: InitialFetchRequest): Promise[] { - const promises: Promise>[] = []; - - if (auth) { - promises.push(client.getBannedPersons({ auth })); - promises.push(client.getFederatedInstances({ auth })); - } else { - promises.push( - Promise.resolve({ state: "empty" }), - Promise.resolve({ state: "empty" }) - ); - } - - return promises; + }: InitialFetchRequest): Promise { + return { + bannedRes: await client.getBannedPersons({ + auth: auth as string, + }), + instancesRes: await client.getFederatedInstances({ + auth: auth as string, + }), + }; } async componentDidMount() { @@ -218,6 +196,28 @@ export class AdminSettings extends Component { ); } + async fetchData() { + this.setState({ + bannedRes: { state: "loading" }, + instancesRes: { state: "loading" }, + themeList: [], + }); + + const auth = myAuthRequired(); + + const [bannedRes, instancesRes, themeList] = await Promise.all([ + HttpService.client.getBannedPersons({ auth }), + HttpService.client.getFederatedInstances({ auth }), + fetchThemeList(), + ]); + + this.setState({ + bannedRes, + instancesRes, + themeList, + }); + } + admins() { return ( <> diff --git a/src/shared/components/home/home.tsx b/src/shared/components/home/home.tsx index 870eb76c..4d79bc63 100644 --- a/src/shared/components/home/home.tsx +++ b/src/shared/components/home/home.tsx @@ -77,6 +77,7 @@ import { QueryParams, relTags, restoreScrollPosition, + RouteDataResponse, saveScrollPosition, setIsoData, setupTippy, @@ -117,6 +118,45 @@ interface HomeProps { page: number; } +type HomeData = RouteDataResponse<{ + postsRes: GetPostsResponse; + commentsRes: GetCommentsResponse; + trendingCommunitiesRes: ListCommunitiesResponse; +}>; + +function getRss(listingType: ListingType) { + const { sort } = getHomeQueryParams(); + const auth = myAuth(); + + let rss: string | undefined = undefined; + + switch (listingType) { + case "All": { + rss = `/feeds/all.xml?sort=${sort}`; + break; + } + case "Local": { + rss = `/feeds/local.xml?sort=${sort}`; + break; + } + case "Subscribed": { + rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined; + break; + } + } + + return ( + rss && ( + <> + + + + + + ) + ); +} + function getDataTypeFromQuery(type?: string): DataType { return type ? DataType[type] : DataType.Post; } @@ -176,7 +216,7 @@ const LinkButton = ({ ); export class Home extends Component { - private isoData = setIsoData(this.context); + private isoData = setIsoData(this.context); state: HomeState = { postsRes: { state: "empty" }, commentsRes: { state: "empty" }, @@ -228,14 +268,14 @@ export class Home extends Component { // Only fetch the data if coming from another route if (FirstLoadService.isFirstLoad) { - const [postsRes, commentsRes, trendingCommunitiesRes] = + const { trendingCommunitiesRes, commentsRes, postsRes } = this.isoData.routeData; this.state = { ...this.state, - postsRes, - commentsRes, trendingCommunitiesRes, + commentsRes, + postsRes, tagline: getRandomFromList(this.state?.siteRes?.taglines ?? []) ?.content, isIsomorphic: true, @@ -244,7 +284,12 @@ export class Home extends Component { } async componentDidMount() { - if (!this.state.isIsomorphic || !this.isoData.routeData.length) { + if ( + !this.state.isIsomorphic || + !Object.values(this.isoData.routeData).some( + res => res.state === "success" || res.state === "failed" + ) + ) { await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]); } @@ -255,13 +300,11 @@ export class Home extends Component { saveScrollPosition(this.context); } - static fetchInitialData({ + static async fetchInitialData({ client, auth, query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort }, - }: InitialFetchRequest>): Promise< - RequestState - >[] { + }: InitialFetchRequest>): Promise { const dataType = getDataTypeFromQuery(urlDataType); // TODO figure out auth default_listingType, default_sort_type @@ -270,7 +313,10 @@ export class Home extends Component { const page = urlPage ? Number(urlPage) : 1; - const promises: Promise>[] = []; + let postsRes: RequestState = { state: "empty" }; + let commentsRes: RequestState = { + state: "empty", + }; if (dataType === DataType.Post) { const getPostsForm: GetPosts = { @@ -282,8 +328,7 @@ export class Home extends Component { auth, }; - promises.push(client.getPosts(getPostsForm)); - promises.push(Promise.resolve({ state: "empty" })); + postsRes = await client.getPosts(getPostsForm); } else { const getCommentsForm: GetComments = { page, @@ -293,8 +338,8 @@ export class Home extends Component { saved_only: false, auth, }; - promises.push(Promise.resolve({ state: "empty" })); - promises.push(client.getComments(getCommentsForm)); + + commentsRes = await client.getComments(getCommentsForm); } const trendingCommunitiesForm: ListCommunities = { @@ -303,9 +348,14 @@ export class Home extends Component { limit: trendingFetchLimit, auth, }; - promises.push(client.listCommunities(trendingCommunitiesForm)); - return promises; + return { + trendingCommunitiesRes: await client.listCommunities( + trendingCommunitiesForm + ), + commentsRes, + postsRes, + }; } get documentTitle(): string { @@ -340,7 +390,7 @@ export class Home extends Component { > )}
{this.mobileView}
- {this.posts()} + {this.posts}