Files
bsky-social-app/src/view/com/util/List.tsx
T
Paul Frazee 4fad18b2fa 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>
2024-05-07 03:08:33 +01:00

137 lines
3.9 KiB
TypeScript

import React, {memo} from 'react'
import {FlatListProps, RefreshControl, ViewToken} from 'react-native'
import {runOnJS, useSharedValue} from 'react-native-reanimated'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {usePalette} from '#/lib/hooks/usePalette'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {addStyle} from 'lib/styles'
import {FlatList_INTERNAL} from './Views'
export type ListMethods = FlatList_INTERNAL
export type ListProps<ItemT> = Omit<
FlatListProps<ItemT>,
| 'onMomentumScrollBegin' // Use ScrollContext instead.
| 'onMomentumScrollEnd' // Use ScrollContext instead.
| 'onScroll' // Use ScrollContext instead.
| 'onScrollBeginDrag' // Use ScrollContext instead.
| 'onScrollEndDrag' // Use ScrollContext instead.
| 'refreshControl' // Pass refreshing and/or onRefresh instead.
| 'contentOffset' // Pass headerOffset instead.
> & {
onScrolledDownChange?: (isScrolledDown: boolean) => void
headerOffset?: number
refreshing?: boolean
onRefresh?: () => void
onItemSeen?: (item: ItemT) => void
containWeb?: boolean
}
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
const SCROLLED_DOWN_LIMIT = 200
function ListImpl<ItemT>(
{
onScrolledDownChange,
refreshing,
onRefresh,
onItemSeen,
headerOffset,
style,
...props
}: ListProps<ItemT>,
ref: React.Ref<ListMethods>,
) {
const isScrolledDown = useSharedValue(false)
const contextScrollHandlers = useScrollHandlers()
const pal = usePalette('default')
function handleScrolledDownChange(didScrollDown: boolean) {
onScrolledDownChange?.(didScrollDown)
}
const scrollHandler = useAnimatedScrollHandler({
onBeginDrag(e, ctx) {
contextScrollHandlers.onBeginDrag?.(e, ctx)
},
onEndDrag(e, ctx) {
contextScrollHandlers.onEndDrag?.(e, ctx)
},
onScroll(e, ctx) {
contextScrollHandlers.onScroll?.(e, ctx)
const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
if (isScrolledDown.value !== didScrollDown) {
isScrolledDown.value = didScrollDown
if (onScrolledDownChange != null) {
runOnJS(handleScrolledDownChange)(didScrollDown)
}
}
},
// Note: adding onMomentumBegin here makes simulator scroll
// lag on Android. So either don't add it, or figure out why.
onMomentumEnd(e, ctx) {
contextScrollHandlers.onMomentumEnd?.(e, ctx)
},
})
const [onViewableItemsChanged, viewabilityConfig] = React.useMemo(() => {
if (!onItemSeen) {
return [undefined, undefined]
}
return [
(info: {viewableItems: Array<ViewToken>; changed: Array<ViewToken>}) => {
for (const item of info.changed) {
if (item.isViewable) {
onItemSeen(item.item)
}
}
},
{
itemVisiblePercentThreshold: 40,
minimumViewTime: 2e3,
},
]
}, [onItemSeen])
let refreshControl
if (refreshing !== undefined || onRefresh !== undefined) {
refreshControl = (
<RefreshControl
refreshing={refreshing ?? false}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
)
}
let contentOffset
if (headerOffset != null) {
style = addStyle(style, {
paddingTop: headerOffset,
})
contentOffset = {x: 0, y: headerOffset * -1}
}
return (
<FlatList_INTERNAL
{...props}
scrollIndicatorInsets={{right: 1}}
contentOffset={contentOffset}
refreshControl={refreshControl}
onScroll={scrollHandler}
scrollEventThrottle={1}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
style={style}
ref={ref}
/>
)
}
export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>},
) => React.ReactElement