+16
-9
src/components/PostControls/PostMenu/PostMenuItems.tsx
+16
-9
src/components/PostControls/PostMenu/PostMenuItems.tsx
···
266
266
feedContext: postFeedContext,
267
267
reqId: postReqId,
268
268
})
269
-
Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
269
+
Toast.show(
270
+
_(msg({message: 'Feedback sent to feed operator', context: 'toast'})),
271
+
)
270
272
}
271
273
272
274
const onPressShowLess = () => {
···
282
284
feedContext: postFeedContext,
283
285
})
284
286
} else {
285
-
Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
287
+
Toast.show(
288
+
_(msg({message: 'Feedback sent to feed operator', context: 'toast'})),
289
+
)
286
290
}
287
291
}
288
292
···
486
490
)}
487
491
488
492
{isDiscoverDebugUser && (
489
-
<Menu.Item
490
-
testID="postDropdownReportMisclassificationBtn"
491
-
label={_(msg`Assign topic for algo`)}
492
-
onPress={onReportMisclassification}>
493
-
<Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText>
494
-
<Menu.ItemIcon icon={AtomIcon} position="right" />
495
-
</Menu.Item>
493
+
<>
494
+
<Menu.Divider />
495
+
<Menu.Item
496
+
testID="postDropdownReportMisclassificationBtn"
497
+
label={_(msg`Assign topic for algo`)}
498
+
onPress={onReportMisclassification}>
499
+
<Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText>
500
+
<Menu.ItemIcon icon={AtomIcon} position="right" />
501
+
</Menu.Item>
502
+
</>
496
503
)}
497
504
498
505
{hasSession && (
-2
src/lib/constants.ts
-2
src/lib/constants.ts
+1
-1
src/screens/PostThread/components/ThreadItemAnchor.tsx
+1
-1
src/screens/PostThread/components/ThreadItemAnchor.tsx
···
180
180
const {openComposer} = useOpenComposer()
181
181
const {currentAccount, hasSession} = useSession()
182
182
const {gtTablet} = useBreakpoints()
183
-
const feedFeedback = useFeedFeedback(postSource?.feed, hasSession)
183
+
const feedFeedback = useFeedFeedback(postSource?.feedSourceInfo, hasSession)
184
184
185
185
const post = postShadow
186
186
const record = item.value.post.record
+4
-1
src/screens/PostThread/index.tsx
+4
-1
src/screens/PostThread/index.tsx
···
49
49
const initialNumToRender = useInitialNumToRender()
50
50
const {height: windowHeight} = useWindowDimensions()
51
51
const anchorPostSource = useUnstablePostSource(uri)
52
-
const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession)
52
+
const feedFeedback = useFeedFeedback(
53
+
anchorPostSource?.feedSourceInfo,
54
+
hasSession,
55
+
)
53
56
54
57
/*
55
58
* One query to rule them all
+1
-1
src/screens/Profile/ProfileFeed/index.tsx
+1
-1
src/screens/Profile/ProfileFeed/index.tsx
···
169
169
const [hasNew, setHasNew] = React.useState(false)
170
170
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
171
171
const queryClient = useQueryClient()
172
-
const feedFeedback = useFeedFeedback(feed, hasSession)
172
+
const feedFeedback = useFeedFeedback(feedInfo, hasSession)
173
173
const scrollElRef = useAnimatedRef() as ListRef
174
174
175
175
const onScrollToTop = useCallback(() => {
+4
-1
src/screens/VideoFeed/index.tsx
+4
-1
src/screens/VideoFeed/index.tsx
···
70
70
useFeedFeedbackContext,
71
71
} from '#/state/feed-feedback'
72
72
import {useFeedFeedback} from '#/state/feed-feedback'
73
+
import {useFeedInfo} from '#/state/queries/feed'
73
74
import {usePostLikeMutationQueue} from '#/state/queries/post'
74
75
import {
75
76
type AuthorFilter,
···
199
200
throw new Error(`Invalid video feed params ${JSON.stringify(params)}`)
200
201
}
201
202
}, [params])
202
-
const feedFeedback = useFeedFeedback(feedDesc, hasSession)
203
+
const feedUri = params.type === 'feedgen' ? params.uri : undefined
204
+
const {data: feedInfo} = useFeedInfo(feedUri)
205
+
const feedFeedback = useFeedFeedback(feedInfo, hasSession)
203
206
const {data, error, hasNextPage, isFetchingNextPage, fetchNextPage} =
204
207
usePostFeedQuery(
205
208
feedDesc,
+86
-12
src/state/feed-feedback.tsx
+86
-12
src/state/feed-feedback.tsx
···
10
10
import {type AppBskyFeedDefs} from '@atproto/api'
11
11
import throttle from 'lodash.throttle'
12
12
13
-
import {FEEDBACK_FEEDS, STAGING_FEEDS} from '#/lib/constants'
13
+
import {PROD_FEEDS, STAGING_FEEDS} from '#/lib/constants'
14
14
import {isNetworkError} from '#/lib/hooks/useCleanError'
15
15
import {logEvent} from '#/lib/statsig/statsig'
16
16
import {Logger} from '#/logger'
17
+
import {
18
+
type FeedSourceFeedInfo,
19
+
type FeedSourceInfo,
20
+
isFeedSourceFeedInfo,
21
+
} from '#/state/queries/feed'
17
22
import {
18
23
type FeedDescriptor,
19
24
type FeedPostSliceItem,
···
21
26
import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
22
27
import {useAgent} from './session'
23
28
29
+
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
30
+
31
+
export const PASSIVE_FEEDBACK_INTERACTIONS = [
32
+
'app.bsky.feed.defs#clickthroughItem',
33
+
'app.bsky.feed.defs#clickthroughAuthor',
34
+
'app.bsky.feed.defs#clickthroughReposter',
35
+
'app.bsky.feed.defs#clickthroughEmbed',
36
+
'app.bsky.feed.defs#interactionSeen',
37
+
] as const
38
+
39
+
export type PassiveFeedbackInteraction =
40
+
(typeof PASSIVE_FEEDBACK_INTERACTIONS)[number]
41
+
42
+
export const DIRECT_FEEDBACK_INTERACTIONS = [
43
+
'app.bsky.feed.defs#requestLess',
44
+
'app.bsky.feed.defs#requestMore',
45
+
] as const
46
+
47
+
export type DirectFeedbackInteraction =
48
+
(typeof DIRECT_FEEDBACK_INTERACTIONS)[number]
49
+
50
+
export const ALL_FEEDBACK_INTERACTIONS = [
51
+
...PASSIVE_FEEDBACK_INTERACTIONS,
52
+
...DIRECT_FEEDBACK_INTERACTIONS,
53
+
] as const
54
+
55
+
export type FeedbackInteraction = (typeof ALL_FEEDBACK_INTERACTIONS)[number]
56
+
57
+
export function isFeedbackInteraction(
58
+
interactionEvent: string,
59
+
): interactionEvent is FeedbackInteraction {
60
+
return ALL_FEEDBACK_INTERACTIONS.includes(
61
+
interactionEvent as FeedbackInteraction,
62
+
)
63
+
}
64
+
24
65
const logger = Logger.create(Logger.Context.FeedFeedback)
25
66
26
67
export type StateContext = {
···
28
69
onItemSeen: (item: any) => void
29
70
sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void
30
71
feedDescriptor: FeedDescriptor | undefined
72
+
feedSourceInfo: FeedSourceInfo | undefined
31
73
}
32
74
33
75
const stateContext = createContext<StateContext>({
···
35
77
onItemSeen: (_item: any) => {},
36
78
sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {},
37
79
feedDescriptor: undefined,
80
+
feedSourceInfo: undefined,
38
81
})
39
82
stateContext.displayName = 'FeedFeedbackContext'
40
83
41
84
export function useFeedFeedback(
42
-
feed: FeedDescriptor | undefined,
85
+
feedSourceInfo: FeedSourceInfo | undefined,
43
86
hasSession: boolean,
44
87
) {
45
88
const agent = useAgent()
46
-
const enabled = isDiscoverFeed(feed) && hasSession
89
+
90
+
const feed =
91
+
!!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo)
92
+
? feedSourceInfo
93
+
: undefined
94
+
95
+
const isDiscover = isDiscoverFeed(feed?.feedDescriptor)
96
+
const acceptsInteractions = Boolean(isDiscover || feed?.acceptsInteractions)
97
+
const proxyDid = feed?.view?.did
98
+
const enabled =
99
+
Boolean(feed) && Boolean(proxyDid) && acceptsInteractions && hasSession
100
+
const enabledInteractions = getEnabledInteractions(enabled, feed, isDiscover)
47
101
48
102
const queue = useRef<Set<string>>(new Set())
49
103
const history = useRef<
···
66
120
const interactions = Array.from(queue.current).map(toInteraction)
67
121
queue.current.clear()
68
122
69
-
let proxyDid = 'did:web:discover.bsky.app'
70
-
if (STAGING_FEEDS.includes(feed ?? '')) {
71
-
proxyDid = 'did:web:algo.pop2.bsky.app'
123
+
const interactionsToSend = interactions.filter(
124
+
interaction =>
125
+
interaction.event &&
126
+
isFeedbackInteraction(interaction.event) &&
127
+
enabledInteractions.includes(interaction.event),
128
+
)
129
+
130
+
if (interactionsToSend.length === 0) {
131
+
return
72
132
}
73
133
74
134
// Send to the feed
75
135
agent.app.bsky.feed
76
136
.sendInteractions(
77
-
{interactions},
137
+
{interactions: interactionsToSend},
78
138
{
79
139
encoding: 'application/json',
80
140
headers: {
81
-
// TODO when we start sending to other feeds, we need to grab their DID -prf
82
141
'atproto-proxy': `${proxyDid}#bsky_fg`,
83
142
},
84
143
},
···
93
152
if (aggregatedStats.current === null) {
94
153
aggregatedStats.current = createAggregatedStats()
95
154
}
96
-
sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions)
155
+
sendOrAggregateInteractionsForStats(
156
+
aggregatedStats.current,
157
+
interactionsToSend,
158
+
)
97
159
throttledFlushAggregatedStats()
98
160
logger.debug('flushed')
99
-
}, [agent, throttledFlushAggregatedStats, feed])
161
+
}, [agent, throttledFlushAggregatedStats, proxyDid, enabledInteractions])
100
162
101
163
const sendToFeed = useMemo(
102
164
() =>
···
168
230
// call on various events
169
231
// queues the event to be sent with the throttled sendToFeed call
170
232
sendInteraction,
171
-
feedDescriptor: feed,
233
+
feedDescriptor: feed?.feedDescriptor,
234
+
feedSourceInfo: typeof feed === 'object' ? feed : undefined,
172
235
}
173
236
}, [enabled, onItemSeen, sendInteraction, feed])
174
237
}
···
184
247
// take advantage of the feed feedback API. Until that's in
185
248
// place, we're hardcoding it to the discover feed.
186
249
// -prf
187
-
function isDiscoverFeed(feed?: FeedDescriptor) {
250
+
export function isDiscoverFeed(feed?: FeedDescriptor) {
188
251
return !!feed && FEEDBACK_FEEDS.includes(feed)
252
+
}
253
+
254
+
function getEnabledInteractions(
255
+
enabled: boolean,
256
+
feed: FeedSourceFeedInfo | undefined,
257
+
isDiscover: boolean,
258
+
): readonly FeedbackInteraction[] {
259
+
if (!enabled || !feed) {
260
+
return []
261
+
}
262
+
return isDiscover ? ALL_FEEDBACK_INTERACTIONS : DIRECT_FEEDBACK_INTERACTIONS
189
263
}
190
264
191
265
function toString(interaction: AppBskyFeedDefs.Interaction): string {
+31
src/state/queries/feed.ts
+31
src/state/queries/feed.ts
···
48
48
creatorDid: string
49
49
creatorHandle: string
50
50
likeCount: number | undefined
51
+
acceptsInteractions?: boolean
51
52
likeUri: string | undefined
52
53
contentMode: AppBskyFeedDefs.GeneratorView['contentMode']
53
54
}
···
72
73
}
73
74
74
75
export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo
76
+
77
+
export function isFeedSourceFeedInfo(
78
+
feed: FeedSourceInfo,
79
+
): feed is FeedSourceFeedInfo {
80
+
return feed.type === 'feed'
81
+
}
75
82
76
83
const feedSourceInfoQueryKeyRoot = 'getFeedSourceInfo'
77
84
export const feedSourceInfoQueryKey = ({uri}: {uri: string}) => [
···
115
122
creatorDid: view.creator.did,
116
123
creatorHandle: view.creator.handle,
117
124
likeCount: view.likeCount,
125
+
acceptsInteractions: view.acceptsInteractions,
118
126
likeUri: view.viewer?.like,
119
127
contentMode: view.contentMode,
120
128
}
···
615
623
count: result.length,
616
624
feeds: result,
617
625
}
626
+
},
627
+
})
628
+
}
629
+
630
+
const feedInfoQueryKeyRoot = 'feedInfo'
631
+
632
+
export function useFeedInfo(feedUri: string | undefined) {
633
+
const agent = useAgent()
634
+
635
+
return useQuery({
636
+
staleTime: STALE.INFINITY,
637
+
queryKey: [feedInfoQueryKeyRoot, feedUri],
638
+
queryFn: async () => {
639
+
if (!feedUri) {
640
+
return undefined
641
+
}
642
+
643
+
const res = await agent.app.bsky.feed.getFeedGenerator({
644
+
feed: feedUri,
645
+
})
646
+
647
+
const feedSourceInfo = hydrateFeedGenerator(res.data.view)
648
+
return feedSourceInfo
618
649
},
619
650
})
620
651
}
+2
-2
src/state/unstable-post-source.tsx
+2
-2
src/state/unstable-post-source.tsx
···
2
2
import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
3
3
4
4
import {Logger} from '#/logger'
5
-
import {type FeedDescriptor} from '#/state/queries/post-feed'
5
+
import {type FeedSourceInfo} from '#/state/queries/feed'
6
6
7
7
/**
8
8
* Separate logger for better debugging
···
11
11
12
12
export type PostSource = {
13
13
post: AppBskyFeedDefs.FeedViewPost
14
-
feed?: FeedDescriptor
14
+
feedSourceInfo?: FeedSourceInfo
15
15
}
16
16
17
17
/**
+3
-3
src/view/com/feeds/FeedPage.tsx
+3
-3
src/view/com/feeds/FeedPage.tsx
···
17
17
import {listenSoftReset} from '#/state/events'
18
18
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
19
19
import {useSetHomeBadge} from '#/state/home-badge'
20
-
import {type SavedFeedSourceInfo} from '#/state/queries/feed'
20
+
import {type FeedSourceInfo} from '#/state/queries/feed'
21
21
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
22
22
import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed'
23
23
import {truncateAndInvalidate} from '#/state/queries/util'
···
51
51
renderEmptyState: () => JSX.Element
52
52
renderEndOfFeed?: () => JSX.Element
53
53
savedFeedConfig?: AppBskyActorDefs.SavedFeed
54
-
feedInfo: SavedFeedSourceInfo
54
+
feedInfo: FeedSourceInfo
55
55
}) {
56
56
const {hasSession} = useSession()
57
57
const {_} = useLingui()
···
61
61
const [isScrolledDown, setIsScrolledDown] = useState(false)
62
62
const setMinimalShellMode = useSetMinimalShellMode()
63
63
const headerOffset = useHeaderOffset()
64
-
const feedFeedback = useFeedFeedback(feed, hasSession)
64
+
const feedFeedback = useFeedFeedback(feedInfo, hasSession)
65
65
const scrollElRef = useRef<ListMethods>(null)
66
66
const [hasNew, setHasNew] = useState(false)
67
67
const setHomeBadge = useSetHomeBadge()
+2
-2
src/view/com/posts/PostFeedItem.tsx
+2
-2
src/view/com/posts/PostFeedItem.tsx
···
176
176
const urip = new AtUri(post.uri)
177
177
return makeProfileLink(post.author, 'post', urip.rkey)
178
178
}, [post.uri, post.author])
179
-
const {sendInteraction, feedDescriptor} = useFeedFeedbackContext()
179
+
const {sendInteraction, feedSourceInfo} = useFeedFeedbackContext()
180
180
181
181
const onPressReply = () => {
182
182
sendInteraction({
···
234
234
})
235
235
unstableCacheProfileView(queryClient, post.author)
236
236
setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), {
237
-
feed: feedDescriptor,
237
+
feedSourceInfo,
238
238
post: {
239
239
post,
240
240
reason: AppBskyFeedDefs.isReasonRepost(reason) ? reason : undefined,