Batch resolve pin urls in 2 ways

Expanded homepage pins to support 2 types of input (if both are passed in, pinnedUrls take precedence):
  (1) pinnedUrls     --> uses doResolveUris (resolve)
  (2) pinnedClaimIds --> uses doResolveClaimIds (claim_search)

Instead of injecting the pinned URLs directly, we inject from `resolvedPinUris`, which will be blank until the uris are resolved, thus preventing it from being resolved individually from the tiles.

There's additional complexity with the `claim_search` method, as the rest of the code deals with uris instead of IDs. Fortunately, it's all contained with `useResolvePins`, so removal would be easier when the batch `resolve` issue is fixed.
This commit is contained in:
infinite-persistence 2022-03-25 15:53:04 +08:00 committed by Thomas Zarebczan
parent dd5f4872fc
commit f5034f74ca
10 changed files with 134 additions and 31 deletions

View file

@ -19,6 +19,7 @@ declare type RowDataItem = {
icon?: string,
extra?: any,
pinnedUrls?: Array<string>,
pinnedClaimIds?: Array<string>, // pinnedUrls takes precedence
options?: {
channelIds?: Array<string>,
limitClaimsPerChannel?: number,

View file

@ -1,11 +1,12 @@
import { connect } from 'react-redux';
import {
selectById,
selectClaimsByUri,
selectClaimSearchByQuery,
selectClaimSearchByQueryLastPageReached,
selectFetchingClaimSearch,
} from 'redux/selectors/claims';
import { doClaimSearch } from 'redux/actions/claims';
import { doClaimSearch, doResolveClaimIds, doResolveUris } from 'redux/actions/claims';
import * as SETTINGS from 'constants/settings';
import { selectFollowedTags } from 'redux/selectors/tags';
import { selectMutedChannels } from 'redux/selectors/blocked';
@ -20,6 +21,7 @@ const select = (state, props) => ({
claimSearchByQuery: selectClaimSearchByQuery(state),
claimSearchByQueryLastPageReached: selectClaimSearchByQueryLastPageReached(state),
claimsByUri: selectClaimsByUri(state),
claimsById: selectById(state),
loading: props.loading !== undefined ? props.loading : selectFetchingClaimSearch(state),
showNsfw: selectShowMatureContent(state),
hideReposts: selectClientSetting(state, SETTINGS.HIDE_REPOSTS),
@ -33,6 +35,8 @@ const perform = {
doClaimSearch,
doFetchViewCount,
doFetchUserMemberships,
doResolveClaimIds,
doResolveUris,
};
export default connect(select, perform)(ClaimListDiscover);

View file

@ -18,13 +18,14 @@ import I18nMessage from 'component/i18nMessage';
import LangFilterIndicator from 'component/langFilterIndicator';
import ClaimListHeader from 'component/claimListHeader';
import useFetchViewCount from 'effects/use-fetch-view-count';
import useResolvePins from 'effects/use-resolve-pins';
import { useIsLargeScreen } from 'effects/use-screensize';
import useGetUserMemberships from 'effects/use-get-user-memberships';
type Props = {
uris: Array<string>,
prefixUris?: Array<string>,
pins?: { urls: Array<string>, onlyPinForOrder?: string },
pins?: { urls?: Array<string>, claimIds?: Array<string>, onlyPinForOrder?: string },
name?: string,
type: string,
pageSize?: number,
@ -88,6 +89,7 @@ type Props = {
claimSearchByQuery: { [string]: Array<string> },
claimSearchByQueryLastPageReached: { [string]: boolean },
claimsByUri: { [string]: any },
claimsById: { [string]: any },
loading: boolean,
showNsfw: boolean,
hideReposts: boolean,
@ -100,6 +102,8 @@ type Props = {
doClaimSearch: ({}) => void,
doFetchViewCount: (claimIdCsv: string) => void,
doFetchUserMemberships: (claimIdCsv: string) => void,
doResolveClaimIds: (Array<string>) => Promise<any>,
doResolveUris: (Array<string>, boolean) => Promise<any>,
hideLayoutButton?: boolean,
loadedCallback?: (number) => void,
@ -173,6 +177,7 @@ function ClaimListDiscover(props: Props) {
showNoSourceClaims,
empty,
claimsByUri,
claimsById,
doFetchViewCount,
hideLayoutButton = false,
loadedCallback,
@ -181,7 +186,11 @@ function ClaimListDiscover(props: Props) {
excludeUris = [],
doFetchUserMemberships,
swipeLayout = false,
doResolveUris,
doResolveClaimIds,
} = props;
const resolvedPinUris = useResolvePins({ pins, claimsById, doResolveClaimIds, doResolveUris });
const didNavigateForward = history.action === 'PUSH';
const { search } = location;
const prevUris = React.useRef();
@ -525,7 +534,7 @@ function ClaimListDiscover(props: Props) {
if (uris) {
// --- direct uris
finalUris = uris;
injectPinUrls(finalUris, orderParam, pins);
injectPinUrls(finalUris, orderParam, pins, resolvedPinUris);
finalUris = filterExcludedUris(finalUris, excludeUris);
} else {
// --- searched uris
@ -533,7 +542,7 @@ function ClaimListDiscover(props: Props) {
finalUris = prevUris.current;
} else {
finalUris = claimSearchResult;
injectPinUrls(finalUris, orderParam, pins);
injectPinUrls(finalUris, orderParam, pins, resolvedPinUris);
finalUris = filterExcludedUris(finalUris, excludeUris);
prevUris.current = finalUris;
}
@ -611,14 +620,13 @@ function ClaimListDiscover(props: Props) {
return order_by;
}
function injectPinUrls(uris, order, pins) {
if (!pins || !pins.urls || (pins.onlyPinForOrder && pins.onlyPinForOrder !== order)) {
function injectPinUrls(uris, order, pins, resolvedPinUris) {
if (!pins || !uris || uris.length <= 2 || (pins.onlyPinForOrder && pins.onlyPinForOrder !== order)) {
return;
}
const pinUrls = pins.urls;
if (pinUrls && uris && uris.length > 2) {
pinUrls.forEach((pin) => {
if (resolvedPinUris) {
resolvedPinUris.forEach((pin) => {
if (uris.includes(pin)) {
uris.splice(uris.indexOf(pin), 1);
} else {
@ -626,7 +634,7 @@ function ClaimListDiscover(props: Props) {
}
});
uris.splice(2, 0, ...pinUrls);
uris.splice(2, 0, ...resolvedPinUris);
}
}

View file

@ -1,8 +1,13 @@
// @flow
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, selectClaimsByUri } from 'redux/selectors/claims';
import { doClaimSearch } from 'redux/actions/claims';
import {
selectClaimSearchByQuery,
selectFetchingClaimSearchByQuery,
selectClaimsByUri,
selectById,
} from 'redux/selectors/claims';
import { doClaimSearch, doResolveClaimIds, doResolveUris } from 'redux/actions/claims';
import { doFetchUserMemberships } from 'redux/actions/user';
import * as SETTINGS from 'constants/settings';
import { MATURE_TAGS } from 'constants/tags';
@ -35,6 +40,7 @@ const select = (state, props) => {
return {
claimSearchResults: selectClaimSearchByQuery(state)[searchKey],
claimsByUri: selectClaimsByUri(state),
claimsById: selectById(state),
fetchingClaimSearch: selectFetchingClaimSearchByQuery(state)[searchKey],
showNsfw,
hideReposts,
@ -47,6 +53,8 @@ const perform = {
doClaimSearch,
doFetchViewCount,
doFetchUserMemberships,
doResolveClaimIds,
doResolveUris,
};
export default withRouter(connect(select, perform)(ClaimListDiscover));

View file

@ -4,6 +4,7 @@ import React from 'react';
import ClaimPreviewTile from 'component/claimPreviewTile';
import useFetchViewCount from 'effects/use-fetch-view-count';
import useLastVisibleItem from 'effects/use-last-visible-item';
import useResolvePins from 'effects/use-resolve-pins';
import useGetUserMemberships from 'effects/use-get-user-memberships';
function urisEqual(prev: ?Array<string>, next: ?Array<string>) {
@ -25,7 +26,7 @@ function urisEqual(prev: ?Array<string>, next: ?Array<string>) {
type Props = {
prefixUris?: Array<string>,
pinUrls?: Array<string>,
pins?: { urls?: Array<string>, claimIds?: Array<string>, onlyPinForOrder?: string },
uris: Array<string>,
injectedItem?: { node: Node, index?: number, replace?: boolean },
showNoSourceClaims?: boolean,
@ -51,6 +52,7 @@ type Props = {
location: { search: string },
claimSearchResults: Array<string>,
claimsByUri: { [string]: any },
claimsById: { [string]: any },
fetchingClaimSearch: boolean,
showNsfw: boolean,
hideReposts: boolean,
@ -59,6 +61,8 @@ type Props = {
doClaimSearch: ({}) => void,
doFetchViewCount: (claimIdCsv: string) => void,
doFetchUserMemberships: (claimIdCsv: string) => void,
doResolveClaimIds: (Array<string>) => Promise<any>,
doResolveUris: (Array<string>, boolean) => Promise<any>,
};
function ClaimTilesDiscover(props: Props) {
@ -66,12 +70,13 @@ function ClaimTilesDiscover(props: Props) {
doClaimSearch,
claimSearchResults,
claimsByUri,
claimsById,
fetchViewCount,
fetchingClaimSearch,
hasNoSource,
// forceShowReposts = false,
renderProperties,
pinUrls,
pins,
prefixUris,
injectedItem,
showNoSourceClaims,
@ -79,6 +84,8 @@ function ClaimTilesDiscover(props: Props) {
pageSize = 8,
optionsStringified,
doFetchUserMemberships,
doResolveClaimIds,
doResolveUris,
} = props;
const listRef = React.useRef();
@ -87,21 +94,15 @@ function ClaimTilesDiscover(props: Props) {
const prevUris = React.useRef();
const claimSearchUris = claimSearchResults || [];
const isUnfetchedClaimSearch = claimSearchResults === undefined;
const resolvedPinUris = useResolvePins({ pins, claimsById, doResolveClaimIds, doResolveUris });
const shouldPerformSearch = !fetchingClaimSearch && claimSearchUris.length === 0;
const uris = (prefixUris || []).concat(claimSearchUris);
if (prefixUris && prefixUris.length) uris.splice(prefixUris.length * -1, prefixUris.length);
if (pinUrls && uris && uris.length > 2 && window.location.pathname === '/') {
pinUrls.forEach((pin) => {
if (uris.indexOf(pin) !== -1) {
uris.splice(uris.indexOf(pin), 1);
} else {
uris.pop();
}
});
uris.splice(2, 0, ...pinUrls);
if (window.location.pathname === '/') {
injectPinUrls(uris, pins, resolvedPinUris);
}
if (uris.length > 0 && uris.length < pageSize && shouldPerformSearch) {
@ -117,11 +118,31 @@ function ClaimTilesDiscover(props: Props) {
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
function injectPinUrls(uris, pins, resolvedPinUris) {
if (!pins || !uris || uris.length <= 2) {
return;
}
if (resolvedPinUris) {
resolvedPinUris.forEach((pin) => {
if (uris.includes(pin)) {
uris.splice(uris.indexOf(pin), 1);
} else {
uris.pop();
}
});
uris.splice(2, 0, ...resolvedPinUris);
}
}
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
useFetchViewCount(fetchViewCount, uris, claimsByUri, doFetchViewCount);
useGetUserMemberships(true, uris, claimsByUri, doFetchUserMemberships);
// Run `doClaimSearch`
React.useEffect(() => {
if (shouldPerformSearch) {
const searchOptions = JSON.parse(optionsStringified);
@ -129,6 +150,9 @@ function ClaimTilesDiscover(props: Props) {
}
}, [doClaimSearch, shouldPerformSearch, optionsStringified]);
// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
return (
<ul ref={listRef} className="claim-grid">
{finalUris && finalUris.length

View file

@ -174,7 +174,9 @@ function SideNavigation(props: Props) {
const isLargeScreen = useIsLargeScreen();
const EXTRA_SIDEBAR_LINKS = GetLinksData(homepageData, isLargeScreen).map(({ pinnedUrls, ...theRest }) => theRest);
const EXTRA_SIDEBAR_LINKS = GetLinksData(homepageData, isLargeScreen).map(
({ pinnedUrls, pinnedClaimIds, ...theRest }) => theRest
);
const MOBILE_LINKS: Array<SideNavLink> = [
{

View file

@ -0,0 +1,51 @@
// @flow
import React from 'react';
import useFetched from 'effects/use-fetched';
type Props = {
pins?: { urls?: Array<string>, claimIds?: Array<string>, onlyPinForOrder?: string },
claimsById: { [string]: Claim },
doResolveClaimIds: (Array<string>) => Promise<any>,
doResolveUris: (Array<string>, boolean) => Promise<any>,
};
export default function useResolvePins(props: Props) {
const { pins, claimsById, doResolveClaimIds, doResolveUris } = props;
const [resolvedPinUris, setResolvedPinUris] = React.useState(pins ? undefined : null);
const [resolvingPinUris, setResolvingPinUris] = React.useState(false);
const hasResolvedPinUris = useFetched(resolvingPinUris);
React.useEffect(() => {
if (resolvedPinUris === undefined && pins && !resolvingPinUris) {
if (pins.urls) {
doResolveUris(pins.urls, true).finally(() => setResolvedPinUris(pins.urls));
} else if (pins.claimIds) {
// setResolvingPinUris is only needed for claim_search.
// doResolveUris uses selectResolvingUris internally to prevent double call.
setResolvingPinUris(true);
// We can't use .then() here to grab the `claim_search` uris directly,
// because we skip those that are already resolved. Instead, we mark a
// flag here, then populate the array in the other effect below in the
// next render cycle (redux would be updated by then). Pretty dumb.
// $FlowFixMe: already checked for null `pins`, but flow can't see it when there's code above it? Wow.
doResolveClaimIds(pins.claimIds).finally(() => setResolvingPinUris(false));
} else {
setResolvedPinUris(null);
}
}
}, [resolvedPinUris, pins, doResolveUris, doResolveClaimIds, resolvingPinUris]);
React.useEffect(() => {
if (hasResolvedPinUris) {
if (pins && pins.claimIds) {
setResolvedPinUris(pins.claimIds.map<string>((id) => claimsById[id]?.canonical_url));
}
}
// Only do this over a false->true->false transition for hasResolvedPinUris.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasResolvedPinUris]);
return resolvedPinUris;
}

View file

@ -128,9 +128,10 @@ function DiscoverPage(props: Props) {
}
function getPins(routeProps) {
if (routeProps && routeProps.pinnedUrls) {
if (routeProps && (routeProps.pinnedUrls || routeProps.pinnedClaimIds)) {
return {
urls: routeProps.pinnedUrls,
claimIds: routeProps.pinnedClaimIds,
onlyPinForOrder: CS.ORDER_BY_TRENDING,
};
}

View file

@ -146,7 +146,7 @@ function HomePage(props: Props) {
);
};
function getRowElements(id, title, route, link, icon, help, options, index, pinUrls) {
function getRowElements(id, title, route, link, icon, help, options, index, pinUrls, pinnedClaimIds) {
const tilePlaceholder = (
<ul className="claim-grid">
{new Array(options.pageSize || 8).fill(1).map((x, i) => (
@ -161,7 +161,7 @@ function HomePage(props: Props) {
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
hasSource
prefixUris={getLivestreamUris(activeLivestreams, options.channelIds)}
pinUrls={pinUrls}
pins={{ urls: pinUrls, claimIds: pinnedClaimIds }}
injectedItem={
index === 0 && {
node: <Ads small type="video" tileLayout />,
@ -249,9 +249,11 @@ function HomePage(props: Props) {
</div>
)}
{sortedRowData.map(({ id, title, route, link, icon, help, pinnedUrls: pinUrls, options = {} }, index) => {
return getRowElements(id, title, route, link, icon, help, options, index, pinUrls);
})}
{sortedRowData.map(
({ id, title, route, link, icon, help, pinnedUrls: pinUrls, pinnedClaimIds, options = {} }, index) => {
return getRowElements(id, title, route, link, icon, help, options, index, pinUrls, pinnedClaimIds);
}
)}
</Page>
);
}

View file

@ -19,6 +19,7 @@ export type HomepageCat = {
order?: string,
tags?: Array<string>,
pinnedUrls?: Array<string>,
pinnedClaimIds?: Array<string>, // pinnedUrls takes precedence
mixIn?: Array<string>,
};
@ -89,6 +90,7 @@ export const getHomepageRowForCat = (key: string, cat: HomepageCat) => {
icon: cat.icon || '', // some default
title: cat.label,
pinnedUrls: cat.pinnedUrls,
pinnedClaimIds: cat.pinnedClaimIds,
options: {
claimType: cat.claimType || ['stream', 'repost'],
channelIds: cat.channelIds,