mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at rm-patch-drawer 256 lines 7.2 kB view raw
1import React from 'react' 2import {AppState, AppStateStatus} from 'react-native' 3import {AppBskyFeedDefs} from '@atproto/api' 4import throttle from 'lodash.throttle' 5 6import {PROD_DEFAULT_FEED} from '#/lib/constants' 7import {logEvent} from '#/lib/statsig/statsig' 8import {logger} from '#/logger' 9import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed' 10import {getFeedPostSlice} from '#/view/com/posts/PostFeed' 11import {useAgent} from './session' 12 13type StateContext = { 14 enabled: boolean 15 onItemSeen: (item: any) => void 16 sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void 17} 18 19const stateContext = React.createContext<StateContext>({ 20 enabled: false, 21 onItemSeen: (_item: any) => {}, 22 sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {}, 23}) 24 25export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { 26 const agent = useAgent() 27 const enabled = isDiscoverFeed(feed) && hasSession 28 const queue = React.useRef<Set<string>>(new Set()) 29 const history = React.useRef< 30 // Use a WeakSet so that we don't need to clear it. 31 // This assumes that referential identity of slice items maps 1:1 to feed (re)fetches. 32 WeakSet<FeedPostSliceItem | AppBskyFeedDefs.Interaction> 33 >(new WeakSet()) 34 35 const aggregatedStats = React.useRef<AggregatedStats | null>(null) 36 const throttledFlushAggregatedStats = React.useMemo( 37 () => 38 throttle(() => flushToStatsig(aggregatedStats.current), 45e3, { 39 leading: true, // The outer call is already throttled somewhat. 40 trailing: true, 41 }), 42 [], 43 ) 44 45 const sendToFeedNoDelay = React.useCallback(() => { 46 const interactions = Array.from(queue.current).map(toInteraction) 47 queue.current.clear() 48 49 // Send to the feed 50 agent.app.bsky.feed 51 .sendInteractions( 52 {interactions}, 53 { 54 encoding: 'application/json', 55 headers: { 56 // TODO when we start sending to other feeds, we need to grab their DID -prf 57 'atproto-proxy': 'did:web:discover.bsky.app#bsky_fg', 58 }, 59 }, 60 ) 61 .catch((e: any) => { 62 logger.warn('Failed to send feed interactions', {error: e}) 63 }) 64 65 // Send to Statsig 66 if (aggregatedStats.current === null) { 67 aggregatedStats.current = createAggregatedStats() 68 } 69 sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions) 70 throttledFlushAggregatedStats() 71 }, [agent, throttledFlushAggregatedStats]) 72 73 const sendToFeed = React.useMemo( 74 () => 75 throttle(sendToFeedNoDelay, 10e3, { 76 leading: false, 77 trailing: true, 78 }), 79 [sendToFeedNoDelay], 80 ) 81 82 React.useEffect(() => { 83 if (!enabled) { 84 return 85 } 86 const sub = AppState.addEventListener('change', (state: AppStateStatus) => { 87 if (state === 'background') { 88 sendToFeed.flush() 89 } 90 }) 91 return () => sub.remove() 92 }, [enabled, sendToFeed]) 93 94 const onItemSeen = React.useCallback( 95 (feedItem: any) => { 96 if (!enabled) { 97 return 98 } 99 const slice = getFeedPostSlice(feedItem) 100 if (slice === null) { 101 return 102 } 103 for (const postItem of slice.items) { 104 if (!history.current.has(postItem)) { 105 history.current.add(postItem) 106 queue.current.add( 107 toString({ 108 item: postItem.uri, 109 event: 'app.bsky.feed.defs#interactionSeen', 110 feedContext: slice.feedContext, 111 }), 112 ) 113 sendToFeed() 114 } 115 } 116 }, 117 [enabled, sendToFeed], 118 ) 119 120 const sendInteraction = React.useCallback( 121 (interaction: AppBskyFeedDefs.Interaction) => { 122 if (!enabled) { 123 return 124 } 125 if (!history.current.has(interaction)) { 126 history.current.add(interaction) 127 queue.current.add(toString(interaction)) 128 sendToFeed() 129 } 130 }, 131 [enabled, sendToFeed], 132 ) 133 134 return React.useMemo(() => { 135 return { 136 enabled, 137 // pass this method to the <List> onItemSeen 138 onItemSeen, 139 // call on various events 140 // queues the event to be sent with the throttled sendToFeed call 141 sendInteraction, 142 } 143 }, [enabled, onItemSeen, sendInteraction]) 144} 145 146export const FeedFeedbackProvider = stateContext.Provider 147 148export function useFeedFeedbackContext() { 149 return React.useContext(stateContext) 150} 151 152// TODO 153// We will introduce a permissions framework for 3p feeds to 154// take advantage of the feed feedback API. Until that's in 155// place, we're hardcoding it to the discover feed. 156// -prf 157function isDiscoverFeed(feed: FeedDescriptor) { 158 return feed === `feedgen|${PROD_DEFAULT_FEED('whats-hot')}` 159} 160 161function toString(interaction: AppBskyFeedDefs.Interaction): string { 162 return `${interaction.item}|${interaction.event}|${ 163 interaction.feedContext || '' 164 }` 165} 166 167function toInteraction(str: string): AppBskyFeedDefs.Interaction { 168 const [item, event, feedContext] = str.split('|') 169 return {item, event, feedContext} 170} 171 172type AggregatedStats = { 173 clickthroughCount: number 174 engagedCount: number 175 seenCount: number 176} 177 178function createAggregatedStats(): AggregatedStats { 179 return { 180 clickthroughCount: 0, 181 engagedCount: 0, 182 seenCount: 0, 183 } 184} 185 186function sendOrAggregateInteractionsForStats( 187 stats: AggregatedStats, 188 interactions: AppBskyFeedDefs.Interaction[], 189) { 190 for (let interaction of interactions) { 191 switch (interaction.event) { 192 // Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them. 193 // This lets us send the feed context together with them. 194 case 'app.bsky.feed.defs#requestLess': { 195 logEvent('discover:showLess', { 196 feedContext: interaction.feedContext ?? '', 197 }) 198 break 199 } 200 case 'app.bsky.feed.defs#requestMore': { 201 logEvent('discover:showMore', { 202 feedContext: interaction.feedContext ?? '', 203 }) 204 break 205 } 206 207 // The rest of the events are aggregated and sent later in batches. 208 case 'app.bsky.feed.defs#clickthroughAuthor': 209 case 'app.bsky.feed.defs#clickthroughEmbed': 210 case 'app.bsky.feed.defs#clickthroughItem': 211 case 'app.bsky.feed.defs#clickthroughReposter': { 212 stats.clickthroughCount++ 213 break 214 } 215 case 'app.bsky.feed.defs#interactionLike': 216 case 'app.bsky.feed.defs#interactionQuote': 217 case 'app.bsky.feed.defs#interactionReply': 218 case 'app.bsky.feed.defs#interactionRepost': 219 case 'app.bsky.feed.defs#interactionShare': { 220 stats.engagedCount++ 221 break 222 } 223 case 'app.bsky.feed.defs#interactionSeen': { 224 stats.seenCount++ 225 break 226 } 227 } 228 } 229} 230 231function flushToStatsig(stats: AggregatedStats | null) { 232 if (stats === null) { 233 return 234 } 235 236 if (stats.clickthroughCount > 0) { 237 logEvent('discover:clickthrough', { 238 count: stats.clickthroughCount, 239 }) 240 stats.clickthroughCount = 0 241 } 242 243 if (stats.engagedCount > 0) { 244 logEvent('discover:engaged', { 245 count: stats.engagedCount, 246 }) 247 stats.engagedCount = 0 248 } 249 250 if (stats.seenCount > 0) { 251 logEvent('discover:seen', { 252 count: stats.seenCount, 253 }) 254 stats.seenCount = 0 255 } 256}