diff options
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/feed-feedback.tsx | 51 | ||||
-rw-r--r-- | src/state/queries/notifications/types.ts | 2 | ||||
-rw-r--r-- | src/state/queries/notifications/util.ts | 11 | ||||
-rw-r--r-- | src/state/queries/post.ts | 22 | ||||
-rw-r--r-- | src/state/unstable-post-source.tsx | 73 |
5 files changed, 129 insertions, 30 deletions
diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 8880cb6b3..225b495d3 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -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 { diff --git a/src/state/queries/notifications/types.ts b/src/state/queries/notifications/types.ts index b3a972394..e05715f77 100644 --- a/src/state/queries/notifications/types.ts +++ b/src/state/queries/notifications/types.ts @@ -46,6 +46,8 @@ type OtherNotificationType = | 'feedgen-like' | 'verified' | 'unverified' + | 'like-via-repost' + | 'repost-via-repost' | 'unknown' type FeedNotificationBase = { diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index 6bbf9b250..569fbbd0f 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -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, diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 7052590ca..4700a7fdc 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -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) }, }) } diff --git a/src/state/unstable-post-source.tsx b/src/state/unstable-post-source.tsx new file mode 100644 index 000000000..1fb4af287 --- /dev/null +++ b/src/state/unstable-post-source.tsx @@ -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 +} |