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:
parent
e264dfbb87
commit
4fad18b2fa
22 changed files with 516 additions and 64 deletions
|
@ -17,6 +17,7 @@ import {logEvent} from '#/lib/statsig/statsig'
|
|||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {listenPostCreated} from '#/state/events'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {
|
||||
FeedDescriptor,
|
||||
|
@ -88,6 +89,7 @@ let Feed = ({
|
|||
const queryClient = useQueryClient()
|
||||
const {currentAccount} = useSession()
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
const feedFeedback = useFeedFeedbackContext()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const checkForNewRef = React.useRef<(() => void) | null>(null)
|
||||
const lastFetchRef = React.useRef<number>(Date.now())
|
||||
|
@ -353,6 +355,7 @@ let Feed = ({
|
|||
}
|
||||
initialNumToRender={initialNumToRender}
|
||||
windowSize={11}
|
||||
onItemSeen={feedFeedback.onItemSeen}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
|
|
@ -16,6 +16,7 @@ import {useLingui} from '@lingui/react'
|
|||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types'
|
||||
import {MAX_POST_LINES} from 'lib/constants'
|
||||
|
@ -45,6 +46,7 @@ export function FeedItem({
|
|||
post,
|
||||
record,
|
||||
reason,
|
||||
feedContext,
|
||||
moderation,
|
||||
isThreadChild,
|
||||
isThreadLastChild,
|
||||
|
@ -53,6 +55,7 @@ export function FeedItem({
|
|||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
|
||||
feedContext: string | undefined
|
||||
moderation: ModerationDecision
|
||||
isThreadChild?: boolean
|
||||
isThreadLastChild?: boolean
|
||||
|
@ -78,6 +81,7 @@ export function FeedItem({
|
|||
post={postShadowed}
|
||||
record={record}
|
||||
reason={reason}
|
||||
feedContext={feedContext}
|
||||
richText={richText}
|
||||
moderation={moderation}
|
||||
isThreadChild={isThreadChild}
|
||||
|
@ -93,6 +97,7 @@ let FeedItemInner = ({
|
|||
post,
|
||||
record,
|
||||
reason,
|
||||
feedContext,
|
||||
richText,
|
||||
moderation,
|
||||
isThreadChild,
|
||||
|
@ -102,6 +107,7 @@ let FeedItemInner = ({
|
|||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
record: AppBskyFeedPost.Record
|
||||
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
|
||||
feedContext: string | undefined
|
||||
richText: RichTextAPI
|
||||
moderation: ModerationDecision
|
||||
isThreadChild?: boolean
|
||||
|
@ -116,6 +122,7 @@ let FeedItemInner = ({
|
|||
const urip = new AtUri(post.uri)
|
||||
return makeProfileLink(post.author, 'post', urip.rkey)
|
||||
}, [post.uri, post.author])
|
||||
const {sendInteraction} = useFeedFeedbackContext()
|
||||
|
||||
const replyAuthorDid = useMemo(() => {
|
||||
if (!record?.reply) {
|
||||
|
@ -126,6 +133,11 @@ let FeedItemInner = ({
|
|||
}, [record?.reply])
|
||||
|
||||
const onPressReply = React.useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionReply',
|
||||
feedContext,
|
||||
})
|
||||
openComposer({
|
||||
replyTo: {
|
||||
uri: post.uri,
|
||||
|
@ -136,11 +148,40 @@ let FeedItemInner = ({
|
|||
moderation,
|
||||
},
|
||||
})
|
||||
}, [post, record, openComposer, moderation])
|
||||
}, [post, record, openComposer, moderation, sendInteraction, feedContext])
|
||||
|
||||
const onOpenAuthor = React.useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughAuthor',
|
||||
feedContext,
|
||||
})
|
||||
}, [sendInteraction, post, feedContext])
|
||||
|
||||
const onOpenReposter = React.useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughReposter',
|
||||
feedContext,
|
||||
})
|
||||
}, [sendInteraction, post, feedContext])
|
||||
|
||||
const onOpenEmbed = React.useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughEmbed',
|
||||
feedContext,
|
||||
})
|
||||
}, [sendInteraction, post, feedContext])
|
||||
|
||||
const onBeforePress = React.useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughItem',
|
||||
feedContext,
|
||||
})
|
||||
precacheProfile(queryClient, post.author)
|
||||
}, [queryClient, post.author])
|
||||
}, [queryClient, post, sendInteraction, feedContext])
|
||||
|
||||
const outerStyles = [
|
||||
styles.outer,
|
||||
|
@ -207,7 +248,8 @@ let FeedItemInner = ({
|
|||
msg`Reposted by ${sanitizeDisplayName(
|
||||
reason.by.displayName || reason.by.handle,
|
||||
)}`,
|
||||
)}>
|
||||
)}
|
||||
onBeforePress={onOpenReposter}>
|
||||
<FontAwesomeIcon
|
||||
icon="retweet"
|
||||
style={{
|
||||
|
@ -235,6 +277,7 @@ let FeedItemInner = ({
|
|||
moderation.ui('displayName'),
|
||||
)}
|
||||
href={makeProfileLink(reason.by)}
|
||||
onBeforePress={onOpenReposter}
|
||||
/>
|
||||
</ProfileHoverCard>
|
||||
</Trans>
|
||||
|
@ -251,6 +294,7 @@ let FeedItemInner = ({
|
|||
profile={post.author}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={post.author.associated?.labeler ? 'labeler' : 'user'}
|
||||
onBeforePress={onOpenAuthor}
|
||||
/>
|
||||
{isThreadParent && (
|
||||
<View
|
||||
|
@ -272,6 +316,7 @@ let FeedItemInner = ({
|
|||
authorHasWarning={!!post.author.labels?.length}
|
||||
timestamp={post.indexedAt}
|
||||
postHref={href}
|
||||
onOpenAuthor={onOpenAuthor}
|
||||
/>
|
||||
{!isThreadChild && replyAuthorDid !== '' && (
|
||||
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
|
||||
|
@ -308,6 +353,7 @@ let FeedItemInner = ({
|
|||
richText={richText}
|
||||
postEmbed={post.embed}
|
||||
postAuthor={post.author}
|
||||
onOpenEmbed={onOpenEmbed}
|
||||
/>
|
||||
<PostCtrls
|
||||
post={post}
|
||||
|
@ -315,6 +361,7 @@ let FeedItemInner = ({
|
|||
richText={richText}
|
||||
onPressReply={onPressReply}
|
||||
logContext="FeedItem"
|
||||
feedContext={feedContext}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -328,11 +375,13 @@ let PostContent = ({
|
|||
richText,
|
||||
postEmbed,
|
||||
postAuthor,
|
||||
onOpenEmbed,
|
||||
}: {
|
||||
moderation: ModerationDecision
|
||||
richText: RichTextAPI
|
||||
postEmbed: AppBskyFeedDefs.PostView['embed']
|
||||
postAuthor: AppBskyFeedDefs.PostView['author']
|
||||
onOpenEmbed: () => void
|
||||
}): React.ReactNode => {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
@ -373,7 +422,11 @@ let PostContent = ({
|
|||
) : undefined}
|
||||
{postEmbed ? (
|
||||
<View style={[a.pb_sm]}>
|
||||
<PostEmbeds embed={postEmbed} moderation={moderation} />
|
||||
<PostEmbeds
|
||||
embed={postEmbed}
|
||||
moderation={moderation}
|
||||
onOpen={onOpenEmbed}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
</ContentHider>
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import React, {memo} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {FeedPostSlice} from '#/state/queries/post-feed'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import Svg, {Circle, Line} from 'react-native-svg'
|
||||
import {FeedItem} from './FeedItem'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {FeedPostSlice} from '#/state/queries/post-feed'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {FeedItem} from './FeedItem'
|
||||
|
||||
let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
|
||||
if (slice.isThread && slice.items.length > 3) {
|
||||
|
@ -20,6 +21,7 @@ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
|
|||
post={slice.items[0].post}
|
||||
record={slice.items[0].record}
|
||||
reason={slice.items[0].reason}
|
||||
feedContext={slice.items[0].feedContext}
|
||||
moderation={slice.items[0].moderation}
|
||||
isThreadParent={isThreadParentAt(slice.items, 0)}
|
||||
isThreadChild={isThreadChildAt(slice.items, 0)}
|
||||
|
@ -29,6 +31,7 @@ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
|
|||
post={slice.items[1].post}
|
||||
record={slice.items[1].record}
|
||||
reason={slice.items[1].reason}
|
||||
feedContext={slice.items[1].feedContext}
|
||||
moderation={slice.items[1].moderation}
|
||||
isThreadParent={isThreadParentAt(slice.items, 1)}
|
||||
isThreadChild={isThreadChildAt(slice.items, 1)}
|
||||
|
@ -39,6 +42,7 @@ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
|
|||
post={slice.items[last].post}
|
||||
record={slice.items[last].record}
|
||||
reason={slice.items[last].reason}
|
||||
feedContext={slice.items[last].feedContext}
|
||||
moderation={slice.items[last].moderation}
|
||||
isThreadParent={isThreadParentAt(slice.items, last)}
|
||||
isThreadChild={isThreadChildAt(slice.items, last)}
|
||||
|
@ -56,6 +60,7 @@ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
|
|||
post={slice.items[i].post}
|
||||
record={slice.items[i].record}
|
||||
reason={slice.items[i].reason}
|
||||
feedContext={slice.items[i].feedContext}
|
||||
moderation={slice.items[i].moderation}
|
||||
isThreadParent={isThreadParentAt(slice.items, i)}
|
||||
isThreadChild={isThreadChildAt(slice.items, i)}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue