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, icon?: string,
extra?: any, extra?: any,
pinnedUrls?: Array<string>, pinnedUrls?: Array<string>,
pinnedClaimIds?: Array<string>, // pinnedUrls takes precedence
options?: { options?: {
channelIds?: Array<string>, channelIds?: Array<string>,
limitClaimsPerChannel?: number, limitClaimsPerChannel?: number,

View file

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

View file

@ -18,13 +18,14 @@ import I18nMessage from 'component/i18nMessage';
import LangFilterIndicator from 'component/langFilterIndicator'; import LangFilterIndicator from 'component/langFilterIndicator';
import ClaimListHeader from 'component/claimListHeader'; import ClaimListHeader from 'component/claimListHeader';
import useFetchViewCount from 'effects/use-fetch-view-count'; import useFetchViewCount from 'effects/use-fetch-view-count';
import useResolvePins from 'effects/use-resolve-pins';
import { useIsLargeScreen } from 'effects/use-screensize'; import { useIsLargeScreen } from 'effects/use-screensize';
import useGetUserMemberships from 'effects/use-get-user-memberships'; import useGetUserMemberships from 'effects/use-get-user-memberships';
type Props = { type Props = {
uris: Array<string>, uris: Array<string>,
prefixUris?: Array<string>, prefixUris?: Array<string>,
pins?: { urls: Array<string>, onlyPinForOrder?: string }, pins?: { urls?: Array<string>, claimIds?: Array<string>, onlyPinForOrder?: string },
name?: string, name?: string,
type: string, type: string,
pageSize?: number, pageSize?: number,
@ -88,6 +89,7 @@ type Props = {
claimSearchByQuery: { [string]: Array<string> }, claimSearchByQuery: { [string]: Array<string> },
claimSearchByQueryLastPageReached: { [string]: boolean }, claimSearchByQueryLastPageReached: { [string]: boolean },
claimsByUri: { [string]: any }, claimsByUri: { [string]: any },
claimsById: { [string]: any },
loading: boolean, loading: boolean,
showNsfw: boolean, showNsfw: boolean,
hideReposts: boolean, hideReposts: boolean,
@ -100,6 +102,8 @@ type Props = {
doClaimSearch: ({}) => void, doClaimSearch: ({}) => void,
doFetchViewCount: (claimIdCsv: string) => void, doFetchViewCount: (claimIdCsv: string) => void,
doFetchUserMemberships: (claimIdCsv: string) => void, doFetchUserMemberships: (claimIdCsv: string) => void,
doResolveClaimIds: (Array<string>) => Promise<any>,
doResolveUris: (Array<string>, boolean) => Promise<any>,
hideLayoutButton?: boolean, hideLayoutButton?: boolean,
loadedCallback?: (number) => void, loadedCallback?: (number) => void,
@ -173,6 +177,7 @@ function ClaimListDiscover(props: Props) {
showNoSourceClaims, showNoSourceClaims,
empty, empty,
claimsByUri, claimsByUri,
claimsById,
doFetchViewCount, doFetchViewCount,
hideLayoutButton = false, hideLayoutButton = false,
loadedCallback, loadedCallback,
@ -181,7 +186,11 @@ function ClaimListDiscover(props: Props) {
excludeUris = [], excludeUris = [],
doFetchUserMemberships, doFetchUserMemberships,
swipeLayout = false, swipeLayout = false,
doResolveUris,
doResolveClaimIds,
} = props; } = props;
const resolvedPinUris = useResolvePins({ pins, claimsById, doResolveClaimIds, doResolveUris });
const didNavigateForward = history.action === 'PUSH'; const didNavigateForward = history.action === 'PUSH';
const { search } = location; const { search } = location;
const prevUris = React.useRef(); const prevUris = React.useRef();
@ -525,7 +534,7 @@ function ClaimListDiscover(props: Props) {
if (uris) { if (uris) {
// --- direct uris // --- direct uris
finalUris = uris; finalUris = uris;
injectPinUrls(finalUris, orderParam, pins); injectPinUrls(finalUris, orderParam, pins, resolvedPinUris);
finalUris = filterExcludedUris(finalUris, excludeUris); finalUris = filterExcludedUris(finalUris, excludeUris);
} else { } else {
// --- searched uris // --- searched uris
@ -533,7 +542,7 @@ function ClaimListDiscover(props: Props) {
finalUris = prevUris.current; finalUris = prevUris.current;
} else { } else {
finalUris = claimSearchResult; finalUris = claimSearchResult;
injectPinUrls(finalUris, orderParam, pins); injectPinUrls(finalUris, orderParam, pins, resolvedPinUris);
finalUris = filterExcludedUris(finalUris, excludeUris); finalUris = filterExcludedUris(finalUris, excludeUris);
prevUris.current = finalUris; prevUris.current = finalUris;
} }
@ -611,14 +620,13 @@ function ClaimListDiscover(props: Props) {
return order_by; return order_by;
} }
function injectPinUrls(uris, order, pins) { function injectPinUrls(uris, order, pins, resolvedPinUris) {
if (!pins || !pins.urls || (pins.onlyPinForOrder && pins.onlyPinForOrder !== order)) { if (!pins || !uris || uris.length <= 2 || (pins.onlyPinForOrder && pins.onlyPinForOrder !== order)) {
return; return;
} }
const pinUrls = pins.urls; if (resolvedPinUris) {
if (pinUrls && uris && uris.length > 2) { resolvedPinUris.forEach((pin) => {
pinUrls.forEach((pin) => {
if (uris.includes(pin)) { if (uris.includes(pin)) {
uris.splice(uris.indexOf(pin), 1); uris.splice(uris.indexOf(pin), 1);
} else { } 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 // @flow
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, selectClaimsByUri } from 'redux/selectors/claims'; import {
import { doClaimSearch } from 'redux/actions/claims'; selectClaimSearchByQuery,
selectFetchingClaimSearchByQuery,
selectClaimsByUri,
selectById,
} from 'redux/selectors/claims';
import { doClaimSearch, doResolveClaimIds, doResolveUris } from 'redux/actions/claims';
import { doFetchUserMemberships } from 'redux/actions/user'; import { doFetchUserMemberships } from 'redux/actions/user';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { MATURE_TAGS } from 'constants/tags'; import { MATURE_TAGS } from 'constants/tags';
@ -35,6 +40,7 @@ const select = (state, props) => {
return { return {
claimSearchResults: selectClaimSearchByQuery(state)[searchKey], claimSearchResults: selectClaimSearchByQuery(state)[searchKey],
claimsByUri: selectClaimsByUri(state), claimsByUri: selectClaimsByUri(state),
claimsById: selectById(state),
fetchingClaimSearch: selectFetchingClaimSearchByQuery(state)[searchKey], fetchingClaimSearch: selectFetchingClaimSearchByQuery(state)[searchKey],
showNsfw, showNsfw,
hideReposts, hideReposts,
@ -47,6 +53,8 @@ const perform = {
doClaimSearch, doClaimSearch,
doFetchViewCount, doFetchViewCount,
doFetchUserMemberships, doFetchUserMemberships,
doResolveClaimIds,
doResolveUris,
}; };
export default withRouter(connect(select, perform)(ClaimListDiscover)); export default withRouter(connect(select, perform)(ClaimListDiscover));

View file

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

View file

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

View file

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