Implement FeedFeedback API (#3498)

* Implement onViewableItemsChanged on List.web.tsx

* Introduce onItemSeen to List API

* Add FeedFeedback tracker

* Add clickthrough interaction tracking

* Add engagement interaction tracking

* Reduce duplicate sends, introduce a flushAndReset to be triggered on refreshes, and modify the api design a bit

* Wire up SDK types and feedContext

* Avoid needless function allocations

* Fix schema usage

* Add show more / show less buttons

* Fix minor rendering issue on mobile menu

* Wire up sendInteractions()

* Fix logic error

* Fix: it's item not uri

* Update 'seen' to mean 3 seconds on-screen with some significant portion visible

* Fix non-reactive debounce

* Move methods out

* Use a WeakSet for deduping

* Reset timeout

* 3 -> 2 seconds

* Oopsie

* Throttle instead

* Fix divider

* Remove explicit flush calls

* Rm unused

---------

Co-authored-by: dan <dan.abramov@gmail.com>
This commit is contained in:
Paul Frazee 2024-05-06 19:08:33 -07:00 committed by GitHub
parent e264dfbb87
commit 4fad18b2fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 516 additions and 64 deletions

View file

@ -20,11 +20,17 @@ export type ListProps<ItemT> = Omit<
headerOffset?: number
refreshing?: boolean
onRefresh?: () => void
onItemSeen?: (item: ItemT) => void
desktopFixedHeight: any // TODO: Better types.
containWeb?: boolean
}
export type ListRef = React.MutableRefObject<any | null> // TODO: Better types.
const ON_ITEM_SEEN_WAIT_DURATION = 2e3 // post must be "seen" 2 seconds before capturing
const ON_ITEM_SEEN_INTERSECTION_OPTS = {
rootMargin: '-200px 0px -200px 0px',
} // post must be 200px visible to be "seen"
function ListImpl<ItemT>(
{
ListHeaderComponent,
@ -43,6 +49,7 @@ function ListImpl<ItemT>(
onRefresh: _unsupportedOnRefresh,
onScrolledDownChange,
onContentSizeChange,
onItemSeen,
renderItem,
extraData,
style,
@ -319,15 +326,19 @@ function ListImpl<ItemT>(
/>
)}
{header}
{(data as Array<ItemT>).map((item, index) => (
<Row<ItemT>
key={keyExtractor!(item, index)}
item={item}
index={index}
renderItem={renderItem}
extraData={extraData}
/>
))}
{(data as Array<ItemT>).map((item, index) => {
const key = keyExtractor!(item, index)
return (
<Row<ItemT>
key={key}
item={item}
index={index}
renderItem={renderItem}
extraData={extraData}
onItemSeen={onItemSeen}
/>
)
})}
{onEndReached && (
<Visibility
root={containWeb ? nativeRef : null}
@ -372,6 +383,7 @@ let Row = function RowImpl<ItemT>({
index,
renderItem,
extraData: _unused,
onItemSeen,
}: {
item: ItemT
index: number
@ -380,12 +392,57 @@ let Row = function RowImpl<ItemT>({
| undefined
| ((data: {index: number; item: any; separators: any}) => React.ReactNode)
extraData: any
onItemSeen: ((item: any) => void) | undefined
}): React.ReactNode {
const rowRef = React.useRef(null)
const intersectionTimeout = React.useRef<NodeJS.Timer | undefined>(undefined)
const handleIntersection = useNonReactiveCallback(
(entries: IntersectionObserverEntry[]) => {
batchedUpdates(() => {
if (!onItemSeen) {
return
}
entries.forEach(entry => {
if (entry.isIntersecting) {
if (!intersectionTimeout.current) {
intersectionTimeout.current = setTimeout(() => {
intersectionTimeout.current = undefined
onItemSeen!(item)
}, ON_ITEM_SEEN_WAIT_DURATION)
}
} else {
if (intersectionTimeout.current) {
clearTimeout(intersectionTimeout.current)
intersectionTimeout.current = undefined
}
}
})
})
},
)
React.useEffect(() => {
if (!onItemSeen) {
return
}
const observer = new IntersectionObserver(
handleIntersection,
ON_ITEM_SEEN_INTERSECTION_OPTS,
)
const row: Element | null = rowRef.current!
observer.observe(row)
return () => {
observer.unobserve(row)
}
}, [handleIntersection, onItemSeen])
if (!renderItem) {
return null
}
return (
<View style={styles.row}>
<View style={styles.row} ref={rowRef}>
{renderItem({item, index, separators: null as any})}
</View>
)