Send FeedFeedback interactions in thread view (#8414)
This commit is contained in:
+32
-19
@@ -1,4 +1,11 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import {AppState, type AppStateStatus} from 'react-native'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
import throttle from 'lodash.throttle'
|
||||
@@ -13,31 +20,36 @@ import {
|
||||
import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
|
||||
import {useAgent} from './session'
|
||||
|
||||
type StateContext = {
|
||||
export type StateContext = {
|
||||
enabled: boolean
|
||||
onItemSeen: (item: any) => void
|
||||
sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
feedDescriptor: FeedDescriptor | undefined
|
||||
}
|
||||
|
||||
const stateContext = React.createContext<StateContext>({
|
||||
const stateContext = createContext<StateContext>({
|
||||
enabled: false,
|
||||
onItemSeen: (_item: any) => {},
|
||||
sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {},
|
||||
feedDescriptor: undefined,
|
||||
})
|
||||
|
||||
export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
export function useFeedFeedback(
|
||||
feed: FeedDescriptor | undefined,
|
||||
hasSession: boolean,
|
||||
) {
|
||||
const agent = useAgent()
|
||||
const enabled = isDiscoverFeed(feed) && hasSession
|
||||
|
||||
const queue = React.useRef<Set<string>>(new Set())
|
||||
const history = React.useRef<
|
||||
const queue = useRef<Set<string>>(new Set())
|
||||
const history = 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 aggregatedStats = React.useRef<AggregatedStats | null>(null)
|
||||
const throttledFlushAggregatedStats = React.useMemo(
|
||||
const aggregatedStats = useRef<AggregatedStats | null>(null)
|
||||
const throttledFlushAggregatedStats = useMemo(
|
||||
() =>
|
||||
throttle(() => flushToStatsig(aggregatedStats.current), 45e3, {
|
||||
leading: true, // The outer call is already throttled somewhat.
|
||||
@@ -46,12 +58,12 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
[],
|
||||
)
|
||||
|
||||
const sendToFeedNoDelay = React.useCallback(() => {
|
||||
const sendToFeedNoDelay = useCallback(() => {
|
||||
const interactions = Array.from(queue.current).map(toInteraction)
|
||||
queue.current.clear()
|
||||
|
||||
let proxyDid = 'did:web:discover.bsky.app'
|
||||
if (STAGING_FEEDS.includes(feed)) {
|
||||
if (STAGING_FEEDS.includes(feed ?? '')) {
|
||||
proxyDid = 'did:web:algo.pop2.bsky.app'
|
||||
}
|
||||
|
||||
@@ -79,7 +91,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
throttledFlushAggregatedStats()
|
||||
}, [agent, throttledFlushAggregatedStats, feed])
|
||||
|
||||
const sendToFeed = React.useMemo(
|
||||
const sendToFeed = useMemo(
|
||||
() =>
|
||||
throttle(sendToFeedNoDelay, 10e3, {
|
||||
leading: false,
|
||||
@@ -88,7 +100,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
[sendToFeedNoDelay],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
@@ -100,7 +112,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
return () => sub.remove()
|
||||
}, [enabled, sendToFeed])
|
||||
|
||||
const onItemSeen = React.useCallback(
|
||||
const onItemSeen = useCallback(
|
||||
(feedItem: any) => {
|
||||
if (!enabled) {
|
||||
return
|
||||
@@ -124,7 +136,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
[enabled, sendToFeed],
|
||||
)
|
||||
|
||||
const sendInteraction = React.useCallback(
|
||||
const sendInteraction = useCallback(
|
||||
(interaction: AppBskyFeedDefs.Interaction) => {
|
||||
if (!enabled) {
|
||||
return
|
||||
@@ -138,7 +150,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
[enabled, sendToFeed],
|
||||
)
|
||||
|
||||
return React.useMemo(() => {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
enabled,
|
||||
// pass this method to the <List> onItemSeen
|
||||
@@ -146,14 +158,15 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
// call on various events
|
||||
// queues the event to be sent with the throttled sendToFeed call
|
||||
sendInteraction,
|
||||
feedDescriptor: feed,
|
||||
}
|
||||
}, [enabled, onItemSeen, sendInteraction])
|
||||
}, [enabled, onItemSeen, sendInteraction, feed])
|
||||
}
|
||||
|
||||
export const FeedFeedbackProvider = stateContext.Provider
|
||||
|
||||
export function useFeedFeedbackContext() {
|
||||
return React.useContext(stateContext)
|
||||
return useContext(stateContext)
|
||||
}
|
||||
|
||||
// TODO
|
||||
@@ -161,8 +174,8 @@ export function useFeedFeedbackContext() {
|
||||
// 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 FEEDBACK_FEEDS.includes(feed)
|
||||
function isDiscoverFeed(feed?: FeedDescriptor) {
|
||||
return !!feed && FEEDBACK_FEEDS.includes(feed)
|
||||
}
|
||||
|
||||
function toString(interaction: AppBskyFeedDefs.Interaction): string {
|
||||
|
||||
@@ -46,6 +46,8 @@ type OtherNotificationType =
|
||||
| 'feedgen-like'
|
||||
| 'verified'
|
||||
| 'unverified'
|
||||
| 'like-via-repost'
|
||||
| 'repost-via-repost'
|
||||
| 'unknown'
|
||||
|
||||
type FeedNotificationBase = {
|
||||
|
||||
@@ -244,7 +244,9 @@ function toKnownType(
|
||||
notif.reason === 'follow' ||
|
||||
notif.reason === 'starterpack-joined' ||
|
||||
notif.reason === 'verified' ||
|
||||
notif.reason === 'unverified'
|
||||
notif.reason === 'unverified' ||
|
||||
notif.reason === 'like-via-repost' ||
|
||||
notif.reason === 'repost-via-repost'
|
||||
) {
|
||||
return notif.reason as NotificationType
|
||||
}
|
||||
@@ -257,7 +259,12 @@ function getSubjectUri(
|
||||
): string | undefined {
|
||||
if (type === 'reply' || type === 'quote' || type === 'mention') {
|
||||
return notif.uri
|
||||
} else if (type === 'post-like' || type === 'repost') {
|
||||
} else if (
|
||||
type === 'post-like' ||
|
||||
type === 'repost' ||
|
||||
type === 'like-via-repost' ||
|
||||
type === 'repost-via-repost'
|
||||
) {
|
||||
if (
|
||||
bsky.dangerousIsType<AppBskyFeedRepost.Record>(
|
||||
notif.record,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {useCallback} from 'react'
|
||||
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {type AppBskyActorDefs, type AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
|
||||
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
|
||||
import {logEvent, type LogEvents, toClout} from '#/lib/statsig/statsig'
|
||||
import {updatePostShadow} from '#/state/cache/post-shadow'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {type Shadow} from '#/state/cache/types'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes'
|
||||
@@ -98,6 +98,7 @@ export function useGetPosts() {
|
||||
|
||||
export function usePostLikeMutationQueue(
|
||||
post: Shadow<AppBskyFeedDefs.PostView>,
|
||||
viaRepost: {uri: string; cid: string} | undefined,
|
||||
logContext: LogEvents['post:like']['logContext'] &
|
||||
LogEvents['post:unlike']['logContext'],
|
||||
) {
|
||||
@@ -115,6 +116,7 @@ export function usePostLikeMutationQueue(
|
||||
const {uri: likeUri} = await likeMutation.mutateAsync({
|
||||
uri: postUri,
|
||||
cid: postCid,
|
||||
via: viaRepost,
|
||||
})
|
||||
userActionHistory.like([postUri])
|
||||
return likeUri
|
||||
@@ -167,9 +169,9 @@ function usePostLikeMutation(
|
||||
return useMutation<
|
||||
{uri: string}, // responds with the uri of the like
|
||||
Error,
|
||||
{uri: string; cid: string} // the post's uri and cid
|
||||
{uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present
|
||||
>({
|
||||
mutationFn: ({uri, cid}) => {
|
||||
mutationFn: ({uri, cid, via}) => {
|
||||
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
|
||||
if (currentAccount) {
|
||||
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
|
||||
@@ -190,7 +192,7 @@ function usePostLikeMutation(
|
||||
? toClout(post.likeCount + post.repostCount + post.replyCount)
|
||||
: undefined,
|
||||
})
|
||||
return agent.like(uri, cid)
|
||||
return agent.like(uri, cid, via)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -209,6 +211,7 @@ function usePostUnlikeMutation(
|
||||
|
||||
export function usePostRepostMutationQueue(
|
||||
post: Shadow<AppBskyFeedDefs.PostView>,
|
||||
viaRepost: {uri: string; cid: string} | undefined,
|
||||
logContext: LogEvents['post:repost']['logContext'] &
|
||||
LogEvents['post:unrepost']['logContext'],
|
||||
) {
|
||||
@@ -226,6 +229,7 @@ export function usePostRepostMutationQueue(
|
||||
const {uri: repostUri} = await repostMutation.mutateAsync({
|
||||
uri: postUri,
|
||||
cid: postCid,
|
||||
via: viaRepost,
|
||||
})
|
||||
return repostUri
|
||||
} else {
|
||||
@@ -272,11 +276,11 @@ function usePostRepostMutation(
|
||||
return useMutation<
|
||||
{uri: string}, // responds with the uri of the repost
|
||||
Error,
|
||||
{uri: string; cid: string} // the post's uri and cid
|
||||
{uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present
|
||||
>({
|
||||
mutationFn: post => {
|
||||
mutationFn: ({uri, cid, via}) => {
|
||||
logEvent('post:repost', {logContext})
|
||||
return agent.repost(post.uri, post.cid)
|
||||
return agent.repost(uri, cid, via)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import {createContext, useCallback, useContext, useState} from 'react'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
|
||||
import {type FeedDescriptor} from './queries/post-feed'
|
||||
|
||||
/**
|
||||
* For passing the source of the post (i.e. the original post, from the feed) to the threadview,
|
||||
* without using query params. Deliberately unstable to avoid using query params, use for FeedFeedback
|
||||
* and other ephemeral non-critical systems.
|
||||
*/
|
||||
|
||||
type Source = {
|
||||
post: AppBskyFeedDefs.FeedViewPost
|
||||
feed?: FeedDescriptor
|
||||
}
|
||||
|
||||
const SetUnstablePostSourceContext = createContext<
|
||||
(key: string, source: Source) => void
|
||||
>(() => {})
|
||||
const ConsumeUnstablePostSourceContext = createContext<
|
||||
(uri: string) => Source | undefined
|
||||
>(() => undefined)
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
const [sources, setSources] = useState<Map<string, Source>>(() => new Map())
|
||||
|
||||
const setUnstablePostSource = useCallback((key: string, source: Source) => {
|
||||
setSources(prev => {
|
||||
const newMap = new Map(prev)
|
||||
newMap.set(key, source)
|
||||
return newMap
|
||||
})
|
||||
}, [])
|
||||
|
||||
const consumeUnstablePostSource = useCallback(
|
||||
(uri: string) => {
|
||||
const source = sources.get(uri)
|
||||
if (source) {
|
||||
setSources(prev => {
|
||||
const newMap = new Map(prev)
|
||||
newMap.delete(uri)
|
||||
return newMap
|
||||
})
|
||||
}
|
||||
return source
|
||||
},
|
||||
[sources],
|
||||
)
|
||||
|
||||
return (
|
||||
<SetUnstablePostSourceContext.Provider value={setUnstablePostSource}>
|
||||
<ConsumeUnstablePostSourceContext.Provider
|
||||
value={consumeUnstablePostSource}>
|
||||
{children}
|
||||
</ConsumeUnstablePostSourceContext.Provider>
|
||||
</SetUnstablePostSourceContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useSetUnstablePostSource() {
|
||||
return useContext(SetUnstablePostSourceContext)
|
||||
}
|
||||
|
||||
/**
|
||||
* DANGER - This hook is unstable and should only be used for FeedFeedback
|
||||
* and other ephemeral non-critical systems. Does not change when the URI changes.
|
||||
*/
|
||||
export function useUnstablePostSource(uri: string) {
|
||||
const consume = useContext(ConsumeUnstablePostSourceContext)
|
||||
|
||||
const [source] = useState(() => consume(uri))
|
||||
return source
|
||||
}
|
||||
Reference in New Issue
Block a user