Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement FeedFeedback API #3498

Merged
merged 27 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ee19718
Implement onViewableItemsChanged on List.web.tsx
pfrazee Apr 12, 2024
18c8247
Introduce onItemSeen to List API
pfrazee Apr 17, 2024
e1e2bbf
Add FeedFeedback tracker
pfrazee Apr 17, 2024
9398479
Add clickthrough interaction tracking
pfrazee Apr 17, 2024
7574c8f
Add engagement interaction tracking
pfrazee Apr 17, 2024
b3e68f6
Reduce duplicate sends, introduce a flushAndReset to be triggered on …
pfrazee Apr 17, 2024
f190992
Wire up SDK types and feedContext
pfrazee Apr 17, 2024
0803a4d
Avoid needless function allocations
pfrazee Apr 18, 2024
6e66a4a
Fix schema usage
pfrazee Apr 19, 2024
b308bb6
Add show more / show less buttons
pfrazee Apr 19, 2024
4ec18c7
Fix minor rendering issue on mobile menu
pfrazee Apr 19, 2024
41af9a0
Merge branch 'main' into paul/feedfeedback
pfrazee Apr 30, 2024
5f0bdd3
Wire up sendInteractions()
pfrazee Apr 30, 2024
7551a45
Fix logic error
pfrazee Apr 30, 2024
b392f68
Fix: it's item not uri
pfrazee Apr 30, 2024
cb798f5
Update 'seen' to mean 3 seconds on-screen with some significant porti…
pfrazee May 3, 2024
de44efa
Merge branch 'main' into paul/feedfeedback
gaearon May 6, 2024
94779cb
Fix non-reactive debounce
gaearon May 6, 2024
03a0235
Move methods out
gaearon May 6, 2024
0b65b91
Use a WeakSet for deduping
gaearon May 6, 2024
87c53e0
Reset timeout
gaearon May 6, 2024
20ce866
3 -> 2 seconds
gaearon May 6, 2024
2ae15d5
Oopsie
gaearon May 6, 2024
fd1b663
Throttle instead
gaearon May 6, 2024
05f6056
Fix divider
gaearon May 7, 2024
e4f3c28
Remove explicit flush calls
gaearon May 7, 2024
a0a6057
Rm unused
gaearon May 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/icons/emojiSmile_stroke2_corner0_rounded.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"@tiptap/react": "^2.0.0-beta.220",
"@tiptap/suggestion": "^2.0.0-beta.220",
"@types/invariant": "^2.2.37",
"@types/lodash.throttle": "^4.1.9",
"@types/node": "^18.16.2",
"@zxing/text-encoding": "^0.9.0",
"array.prototype.findlast": "^1.2.3",
Expand Down Expand Up @@ -149,6 +150,7 @@
"lodash.samplesize": "^4.2.0",
"lodash.set": "^4.3.2",
"lodash.shuffle": "^4.2.0",
"lodash.throttle": "^4.1.1",
"mobx": "^6.6.1",
"mobx-react-lite": "^3.4.0",
"mobx-utils": "^6.0.6",
Expand Down
4 changes: 4 additions & 0 deletions src/components/icons/Emoji.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ export const EmojiSad_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M6.343 6.343a8 8 0 1 1 11.314 11.314A8 8 0 0 1 6.343 6.343ZM19.071 4.93c-3.905-3.905-10.237-3.905-14.142 0-3.905 3.905-3.905 10.237 0 14.142 3.905 3.905 10.237 3.905 14.142 0 3.905-3.905 3.905-10.237 0-14.142Zm-3.537 9.535a5 5 0 0 0-7.07 0 1 1 0 1 0 1.413 1.415 3 3 0 0 1 4.243 0 1 1 0 0 0 1.414-1.415ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5ZM9.25 11c.69 0 1.25-.672 1.25-1.5S9.94 8 9.25 8 8 8.672 8 9.5 8.56 11 9.25 11Z',
})

export const EmojiSmile_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M17.657 6.343A8 8 0 1 0 6.343 17.657 8 8 0 0 0 17.657 6.343ZM4.929 4.93c3.905-3.905 10.237-3.905 14.142 0 3.905 3.905 3.905 10.237 0 14.142-3.905 3.905-10.237 3.905-14.142 0-3.905-3.905-3.905-10.237 0-14.142Zm3.536 9.192a1 1 0 0 1 1.414 0 3 3 0 0 0 4.243 0 1 1 0 0 1 1.414 1.415 5 5 0 0 1-7.071 0 1 1 0 0 1 0-1.415ZM10.5 9.5c0 .828-.56 1.5-1.25 1.5S8 10.328 8 9.5 8.56 8 9.25 8s1.25.672 1.25 1.5ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5Z',
})

export const EmojiArc_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-5a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V8a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V8a1 1 0 0 1 1-1Zm-5.894 7.803a1 1 0 0 1 1.341-.447c1.719.859 3.387.859 5.106 0a1 1 0 1 1 .894 1.788c-2.281 1.141-4.613 1.141-6.894 0a1 1 0 0 1-.447-1.341Z',
})
Expand Down
151 changes: 151 additions & 0 deletions src/state/feed-feedback.tsx
Original file line number Diff line number Diff line change
@@ -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}
}
10 changes: 10 additions & 0 deletions src/state/queries/post-feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ export interface FeedPostSliceItem {
post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record
reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource
feedContext: string | undefined
moderation: ModerationDecision
}

export interface FeedPostSlice {
_isFeedPostSlice: boolean
_reactKey: string
rootUri: string
isThread: boolean
Expand Down Expand Up @@ -276,6 +278,7 @@ export function usePostFeedQuery(

return {
_reactKey: slice._reactKey,
_isFeedPostSlice: true,
rootUri: slice.rootItem.post.uri,
isThread:
slice.items.length > 1 &&
Expand All @@ -300,6 +303,7 @@ export function usePostFeedQuery(
i === 0 && slice.source
? slice.source
: item.reason,
feedContext: item.feedContext,
moderation: moderations[i],
}
}
Expand Down Expand Up @@ -507,3 +511,9 @@ export function resetProfilePostsQueries(
})
}, timeout)
}

export function isFeedPostSlice(v: any): v is FeedPostSlice {
return (
v && typeof v === 'object' && '_isFeedPostSlice' in v && v._isFeedPostSlice
)
}
32 changes: 18 additions & 14 deletions src/view/com/feeds/FeedPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
import {logEvent, useGate} from '#/lib/statsig/statsig'
import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events'
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util'
Expand Down Expand Up @@ -51,6 +52,7 @@ export function FeedPage({
const setMinimalShellMode = useSetMinimalShellMode()
const {screen, track} = useAnalytics()
const headerOffset = useHeaderOffset()
const feedFeedback = useFeedFeedback(feed, hasSession)
const scrollElRef = React.useRef<ListMethods>(null)
const [hasNew, setHasNew] = React.useState(false)
const gate = useGate()
Expand Down Expand Up @@ -113,20 +115,22 @@ export function FeedPage({
return (
<View testID={testID} style={s.h100pct}>
<MainScrollProvider>
<Feed
testID={testID ? `${testID}-feed` : undefined}
enabled={isPageFocused}
feed={feed}
feedParams={feedParams}
pollInterval={POLL_FREQ}
disablePoll={hasNew}
scrollElRef={scrollElRef}
onScrolledDownChange={setIsScrolledDown}
onHasNew={setHasNew}
renderEmptyState={renderEmptyState}
renderEndOfFeed={renderEndOfFeed}
headerOffset={headerOffset}
/>
<FeedFeedbackProvider value={feedFeedback}>
<Feed
testID={testID ? `${testID}-feed` : undefined}
enabled={isPageFocused}
feed={feed}
feedParams={feedParams}
pollInterval={POLL_FREQ}
disablePoll={hasNew}
scrollElRef={scrollElRef}
onScrolledDownChange={setIsScrolledDown}
onHasNew={setHasNew}
renderEmptyState={renderEmptyState}
renderEndOfFeed={renderEndOfFeed}
headerOffset={headerOffset}
/>
</FeedFeedbackProvider>
</MainScrollProvider>
{(isScrolledDown || adjustedHasNew) && (
<LoadLatestBtn
Expand Down
3 changes: 3 additions & 0 deletions src/view/com/posts/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -353,6 +355,7 @@ let Feed = ({
}
initialNumToRender={initialNumToRender}
windowSize={11}
onItemSeen={feedFeedback.onItemSeen}
/>
</View>
)
Expand Down
Loading
Loading