Support callback method to inject items (1351)

This commit is contained in:
infinite-persistence 2022-04-18 19:15:35 +08:00
commit 805b607e73
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
6 changed files with 96 additions and 74 deletions

6
flow-typed/gui.js vendored Normal file
View file

@ -0,0 +1,6 @@
declare type ListInjectedItem = {
node: Node | (index: number, lastVisibleIndex: ?number, pageSize: ?number) => Node,
index?: number,
replace?: boolean
};

View file

@ -7,7 +7,7 @@ import ClaimPreview from 'component/claimPreview';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import useLastVisibleItem from 'effects/use-last-visible-item'; import useGetLastVisibleSlot from 'effects/use-get-last-visible-slot';
import debounce from 'util/debounce'; import debounce from 'util/debounce';
import ClaimPreviewTile from 'component/claimPreviewTile'; import ClaimPreviewTile from 'component/claimPreviewTile';
@ -41,7 +41,7 @@ type Props = {
renderActions?: (Claim) => ?Node, renderActions?: (Claim) => ?Node,
renderProperties?: (Claim) => ?Node, renderProperties?: (Claim) => ?Node,
includeSupportAction?: boolean, includeSupportAction?: boolean,
injectedItem?: { node: Node, index?: number, replace?: boolean }, injectedItem?: ListInjectedItem,
timedOutMessage?: Node, timedOutMessage?: Node,
tileLayout?: boolean, tileLayout?: boolean,
searchInLanguage: boolean, searchInLanguage: boolean,
@ -106,7 +106,8 @@ export default function ClaimList(props: Props) {
// Resolve the index for injectedItem, if provided; else injectedIndex will be 'undefined'. // Resolve the index for injectedItem, if provided; else injectedIndex will be 'undefined'.
const listRef = React.useRef(); const listRef = React.useRef();
const injectedIndex = useLastVisibleItem(injectedItem, listRef); const findLastVisibleSlot = injectedItem && injectedItem.node && injectedItem.index === undefined;
const lastVisibleIndex = useGetLastVisibleSlot(listRef, !findLastVisibleSlot);
// Exclude prefix uris in these results variables. We don't want to show // Exclude prefix uris in these results variables. We don't want to show
// anything if the search failed or timed out. // anything if the search failed or timed out.
@ -205,9 +206,18 @@ export default function ClaimList(props: Props) {
); );
const getInjectedItem = (index) => { const getInjectedItem = (index) => {
if (injectedItem && injectedItem.node && injectedIndex === index) { if (injectedItem && injectedItem.node) {
return injectedItem.node; if (typeof injectedItem.node === 'function') {
return injectedItem.node(index, lastVisibleIndex, pageSize);
} else {
if (injectedItem.index === undefined || injectedItem.index === null) {
return index === lastVisibleIndex ? injectedItem.node : null;
} else {
return index === injectedItem.index ? injectedItem.node : null;
}
}
} }
return null; return null;
}; };

View file

@ -76,7 +76,7 @@ type Props = {
header?: Node, header?: Node,
headerLabel?: string | Node, headerLabel?: string | Node,
hiddenNsfwMessage?: Node, hiddenNsfwMessage?: Node,
injectedItem?: { node: Node, index?: number, replace?: boolean }, injectedItem?: ListInjectedItem,
meta?: Node, meta?: Node,
subSection?: Node, // Additional section below [Header|Meta] subSection?: Node, // Additional section below [Header|Meta]
renderProperties?: (Claim) => Node, renderProperties?: (Claim) => Node,

View file

@ -5,7 +5,7 @@ import Button from 'component/button';
import ClaimPreviewTile from 'component/claimPreviewTile'; import ClaimPreviewTile from 'component/claimPreviewTile';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
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 useGetLastVisibleSlot from 'effects/use-get-last-visible-slot';
import useResolvePins from 'effects/use-resolve-pins'; import useResolvePins from 'effects/use-resolve-pins';
import useGetUserMemberships from 'effects/use-get-user-memberships'; import useGetUserMemberships from 'effects/use-get-user-memberships';
@ -32,7 +32,7 @@ type Props = {
prefixUris?: Array<string>, prefixUris?: Array<string>,
pins?: { urls?: Array<string>, claimIds?: Array<string>, onlyPinForOrder?: string }, pins?: { urls?: Array<string>, claimIds?: Array<string>, onlyPinForOrder?: string },
uris: Array<string>, uris: Array<string>,
injectedItem?: { node: Node, index?: number, replace?: boolean }, injectedItem?: ListInjectedItem,
showNoSourceClaims?: boolean, showNoSourceClaims?: boolean,
renderProperties?: (Claim) => ?Node, renderProperties?: (Claim) => ?Node,
fetchViewCount?: boolean, fetchViewCount?: boolean,
@ -93,7 +93,8 @@ function ClaimTilesDiscover(props: Props) {
} = props; } = props;
const listRef = React.useRef(); const listRef = React.useRef();
const injectedIndex = useLastVisibleItem(injectedItem, listRef); const findLastVisibleSlot = injectedItem && injectedItem.node && injectedItem.index === undefined;
const lastVisibleIndex = useGetLastVisibleSlot(listRef, !findLastVisibleSlot);
const prevUris = React.useRef(); const prevUris = React.useRef();
const claimSearchUris = claimSearchResults || []; const claimSearchUris = claimSearchResults || [];
@ -141,6 +142,22 @@ function ClaimTilesDiscover(props: Props) {
} }
} }
const getInjectedItem = (index) => {
if (injectedItem && injectedItem.node) {
if (typeof injectedItem.node === 'function') {
return injectedItem.node(index, lastVisibleIndex, pageSize);
} else {
if (injectedItem.index === undefined || injectedItem.index === null) {
return index === lastVisibleIndex ? injectedItem.node : null;
} else {
return index === injectedItem.index ? injectedItem.node : null;
}
}
}
return null;
};
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
@ -186,18 +203,17 @@ function ClaimTilesDiscover(props: Props) {
{finalUris && finalUris.length {finalUris && finalUris.length
? finalUris.map((uri, i) => { ? finalUris.map((uri, i) => {
if (uri) { if (uri) {
if (injectedIndex === i && injectedItem && injectedItem.replace) { const inj = getInjectedItem(i);
return <React.Fragment key={uri}>{injectedItem.node}</React.Fragment>;
}
return ( return (
<React.Fragment key={uri}> <React.Fragment key={uri}>
{injectedIndex === i && injectedItem && injectedItem.node} {inj && inj}
<ClaimPreviewTile {(!inj || !injectedItem || !injectedItem.replace) && (
showNoSourceClaims={hasNoSource || showNoSourceClaims} <ClaimPreviewTile
uri={uri} showNoSourceClaims={hasNoSource || showNoSourceClaims}
properties={renderProperties} uri={uri}
/> properties={renderProperties}
/>
)}
</React.Fragment> </React.Fragment>
); );
} else { } else {

View file

@ -0,0 +1,45 @@
// @flow
import React from 'react';
/**
* Returns the last visible slot in a list.
*
* @param listRef A reference to the list.
* @param skipEval Skip the evaluation/effect. Useful if the client knows the effect is not needed in certain scenarios.
* @param checkDelayMs Delay before running the effect. Useful if the list is known to not be populated until later.
* @returns {number | undefined} Zero-indexed number representing the last visible slot.
*/
export default function useGetLastVisibleSlot(listRef: any, skipEval: boolean, checkDelayMs: number = 1500) {
const [lastVisibleIndex, setLastVisibleIndex] = React.useState(undefined);
React.useEffect(() => {
if (!skipEval) {
const timer = setTimeout(() => {
if (listRef.current) {
const screenBottom = window.innerHeight;
const items = listRef.current.children;
if (items.length) {
let i = 2; // Start from 2, so that the min possible is index-1
for (; i < items.length; ++i) {
const rect = items[i].getBoundingClientRect();
if (rect.top > screenBottom || rect.bottom > screenBottom) {
break;
}
}
setLastVisibleIndex(i - 1);
return;
}
}
// Fallback to index-1 (2nd item) for failures. No retries.
setLastVisibleIndex(1);
}, checkDelayMs);
return () => clearTimeout(timer);
}
}, []);
return lastVisibleIndex;
}

View file

@ -1,55 +0,0 @@
// @flow
import React from 'react';
import type { Node } from 'react';
type InjectedItem = { node: Node, index?: number, replace?: boolean };
/**
* Returns the index indicating where in the claim-grid to inject the ad element
* @param injectedItem
* @param listRef - A reference to the claim-grid
* @returns {number}
*/
export default function useLastVisibleItem(injectedItem: ?InjectedItem, listRef: any) {
const [injectedIndex, setInjectedIndex] = React.useState(injectedItem?.index);
React.useEffect(() => {
// Move to default injection index (last visible item)
if (injectedItem && injectedItem.index === undefined) {
// AD_INJECTION_DELAY_MS = average total-blocking-time incurred for
// loading ads. Delay to let higher priority tasks run first. Ideally,
// should use 'requestIdleCallback/requestAnimationFrame'.
const AD_INJECTION_DELAY_MS = 1500;
const timer = setTimeout(() => {
if (listRef.current) {
const screenBottom = window.innerHeight;
// claim preview tiles
const items = listRef.current.children;
// algo to return index of item, where ad will be injected before it
if (items.length) {
let i = 2; // Start from 2, so that the min possible is index-1
for (; i < items.length; ++i) {
const rect = items[i].getBoundingClientRect();
if (rect.top > screenBottom || rect.bottom > screenBottom) {
break;
}
}
setInjectedIndex(i - 1);
return;
}
}
// Fallback to index-1 (2nd item) for failures. No retries.
setInjectedIndex(1);
}, AD_INJECTION_DELAY_MS);
return () => clearTimeout(timer);
}
}, []);
return injectedIndex;
}