diff options
author | Paul Frazee <pfrazee@gmail.com> | 2024-05-06 19:08:33 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-07 03:08:33 +0100 |
commit | 4fad18b2fa3c12ffdf1d49afac5228f7df658bc2 (patch) | |
tree | 8fa8df48dcf544c288bc0618127fcead58014962 /src/state/feed-feedback.tsx | |
parent | e264dfbb875118036d5b155f46f2b0b71261e1ff (diff) | |
download | voidsky-4fad18b2fa3c12ffdf1d49afac5228f7df658bc2.tar.zst |
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>
Diffstat (limited to 'src/state/feed-feedback.tsx')
-rw-r--r-- | src/state/feed-feedback.tsx | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx new file mode 100644 index 000000000..5bfc77d0a --- /dev/null +++ b/src/state/feed-feedback.tsx @@ -0,0 +1,151 @@ +import React from 'react' +import {AppState, AppStateStatus} from 'react-native' +import {AppBskyFeedDefs, BskyAgent} from '@atproto/api' +import throttle from 'lodash.throttle' + +import {PROD_DEFAULT_FEED} from '#/lib/constants' +import {logger} from '#/logger' +import { + FeedDescriptor, + FeedPostSliceItem, + isFeedPostSlice, +} from '#/state/queries/post-feed' +import {useAgent} from './session' + +type StateContext = { + enabled: boolean + onItemSeen: (item: any) => void + sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void +} + +const stateContext = React.createContext<StateContext>({ + enabled: false, + onItemSeen: (_item: any) => {}, + sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {}, +}) + +export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { + const {getAgent} = useAgent() + const enabled = isDiscoverFeed(feed) && hasSession + const queue = React.useRef<Set<string>>(new Set()) + const history = React.useRef< + // Use a WeakSet so that we don't need to clear it. + // This assumes that referential identity of slice items maps 1:1 to feed (re)fetches. + WeakSet<FeedPostSliceItem | AppBskyFeedDefs.Interaction> + >(new WeakSet()) + + const sendToFeedNoDelay = React.useCallback(() => { + const proxyAgent = getAgent().withProxy( + // @ts-ignore TODO need to update withProxy() to support this key -prf + 'bsky_fg', + // TODO when we start sending to other feeds, we need to grab their DID -prf + 'did:web:discover.bsky.app', + ) as BskyAgent + + const interactions = Array.from(queue.current).map(toInteraction) + queue.current.clear() + + proxyAgent.app.bsky.feed + .sendInteractions({interactions}) + .catch((e: any) => { + logger.warn('Failed to send feed interactions', {error: e}) + }) + }, [getAgent]) + + const sendToFeed = React.useMemo( + () => + throttle(sendToFeedNoDelay, 15e3, { + leading: false, + trailing: true, + }), + [sendToFeedNoDelay], + ) + + React.useEffect(() => { + if (!enabled) { + return + } + const sub = AppState.addEventListener('change', (state: AppStateStatus) => { + if (state === 'background') { + sendToFeed.flush() + } + }) + return () => sub.remove() + }, [enabled, sendToFeed]) + + const onItemSeen = React.useCallback( + (slice: any) => { + if (!enabled) { + return + } + if (!isFeedPostSlice(slice)) { + return + } + for (const postItem of slice.items) { + if (!history.current.has(postItem)) { + history.current.add(postItem) + queue.current.add( + toString({ + item: postItem.uri, + event: 'app.bsky.feed.defs#interactionSeen', + feedContext: postItem.feedContext, + }), + ) + sendToFeed() + } + } + }, + [enabled, sendToFeed], + ) + + const sendInteraction = React.useCallback( + (interaction: AppBskyFeedDefs.Interaction) => { + if (!enabled) { + return + } + if (!history.current.has(interaction)) { + history.current.add(interaction) + queue.current.add(toString(interaction)) + sendToFeed() + } + }, + [enabled, sendToFeed], + ) + + return React.useMemo(() => { + return { + enabled, + // pass this method to the <List> onItemSeen + onItemSeen, + // call on various events + // queues the event to be sent with the throttled sendToFeed call + sendInteraction, + } + }, [enabled, onItemSeen, sendInteraction]) +} + +export const FeedFeedbackProvider = stateContext.Provider + +export function useFeedFeedbackContext() { + return React.useContext(stateContext) +} + +// TODO +// We will introduce a permissions framework for 3p feeds to +// take advantage of the feed feedback API. Until that's in +// place, we're hardcoding it to the discover feed. +// -prf +function isDiscoverFeed(feed: FeedDescriptor) { + return feed === `feedgen|${PROD_DEFAULT_FEED('whats-hot')}` +} + +function toString(interaction: AppBskyFeedDefs.Interaction): string { + return `${interaction.item}|${interaction.event}|${ + interaction.feedContext || '' + }` +} + +function toInteraction(str: string): AppBskyFeedDefs.Interaction { + const [item, event, feedContext] = str.split('|') + return {item, event, feedContext} +} |