diff options
-rw-r--r-- | src/App.native.tsx | 19 | ||||
-rw-r--r-- | src/App.web.tsx | 11 | ||||
-rw-r--r-- | src/logger/types.ts | 2 | ||||
-rw-r--r-- | src/state/feed-feedback.tsx | 8 | ||||
-rw-r--r-- | src/state/unstable-post-source.tsx | 115 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 30 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 34 | ||||
-rw-r--r-- | src/view/com/posts/PostFeedItem.tsx | 8 |
8 files changed, 145 insertions, 82 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx index e3f85c0fe..baab8c838 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -58,7 +58,6 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' -import {Provider as UnstablePostSourceProvider} from '#/state/unstable-post-source' import {TestCtrls} from '#/view/com/testing/TestCtrls' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' @@ -151,16 +150,14 @@ function InnerApp() { <MutedThreadsProvider> <ProgressGuideProvider> <ServiceAccountManager> - <UnstablePostSourceProvider> - <GestureHandlerRootView - style={s.h100pct}> - <IntentDialogProvider> - <TestCtrls /> - <Shell /> - <NuxDialogs /> - </IntentDialogProvider> - </GestureHandlerRootView> - </UnstablePostSourceProvider> + <GestureHandlerRootView + style={s.h100pct}> + <IntentDialogProvider> + <TestCtrls /> + <Shell /> + <NuxDialogs /> + </IntentDialogProvider> + </GestureHandlerRootView> </ServiceAccountManager> </ProgressGuideProvider> </MutedThreadsProvider> diff --git a/src/App.web.tsx b/src/App.web.tsx index 97ada6148..c5ec0473c 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -48,7 +48,6 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' -import {Provider as UnstablePostSourceProvider} from '#/state/unstable-post-source' import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' @@ -132,12 +131,10 @@ function InnerApp() { <SafeAreaProvider> <ProgressGuideProvider> <ServiceConfigProvider> - <UnstablePostSourceProvider> - <IntentDialogProvider> - <Shell /> - <NuxDialogs /> - </IntentDialogProvider> - </UnstablePostSourceProvider> + <IntentDialogProvider> + <Shell /> + <NuxDialogs /> + </IntentDialogProvider> </ServiceConfigProvider> </ProgressGuideProvider> </SafeAreaProvider> diff --git a/src/logger/types.ts b/src/logger/types.ts index d14e21a9d..88d8d9d93 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -10,6 +10,8 @@ export enum LogContext { ConversationAgent = 'conversation-agent', DMsAgent = 'dms-agent', ReportDialog = 'report-dialog', + FeedFeedback = 'feed-feedback', + PostSource = 'post-source', /** * METRIC IS FOR INTERNAL USE ONLY, don't create any other loggers using this diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 225b495d3..a718a761d 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -12,7 +12,7 @@ import throttle from 'lodash.throttle' import {FEEDBACK_FEEDS, STAGING_FEEDS} from '#/lib/constants' import {logEvent} from '#/lib/statsig/statsig' -import {logger} from '#/logger' +import {Logger} from '#/logger' import { type FeedDescriptor, type FeedPostSliceItem, @@ -20,6 +20,8 @@ import { import {getItemsForFeedback} from '#/view/com/posts/PostFeed' import {useAgent} from './session' +const logger = Logger.create(Logger.Context.FeedFeedback) + export type StateContext = { enabled: boolean onItemSeen: (item: any) => void @@ -89,6 +91,7 @@ export function useFeedFeedback( } sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions) throttledFlushAggregatedStats() + logger.debug('flushed') }, [agent, throttledFlushAggregatedStats, feed]) const sendToFeed = useMemo( @@ -141,6 +144,9 @@ export function useFeedFeedback( if (!enabled) { return } + logger.debug('sendInteraction', { + ...interaction, + }) if (!history.current.has(interaction)) { history.current.add(interaction) queue.current.add(toString(interaction)) diff --git a/src/state/unstable-post-source.tsx b/src/state/unstable-post-source.tsx index 43aac6f4d..ac126d79c 100644 --- a/src/state/unstable-post-source.tsx +++ b/src/state/unstable-post-source.tsx @@ -1,62 +1,97 @@ -import {createContext, useCallback, useContext, useRef, useState} from 'react' -import {type AppBskyFeedDefs} from '@atproto/api' +import {useEffect, useId, useState} from 'react' +import {type AppBskyFeedDefs, AtUri} from '@atproto/api' -import {type FeedDescriptor} from './queries/post-feed' +import {Logger} from '#/logger' +import {type FeedDescriptor} from '#/state/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. + * Separate logger for better debugging */ +const logger = Logger.create(Logger.Context.PostSource) -type Source = { +export type PostSource = { post: AppBskyFeedDefs.FeedViewPost feed?: FeedDescriptor } -const SetUnstablePostSourceContext = createContext< - (key: string, source: Source) => void ->(() => {}) -const ConsumeUnstablePostSourceContext = createContext< - (uri: string) => Source | undefined ->(() => undefined) +/** + * A cache of sources that will be consumed by the post thread view. This is + * cleaned up any time a source is consumed. + */ +const transientSources = new Map<string, PostSource>() -export function Provider({children}: {children: React.ReactNode}) { - const sourcesRef = useRef<Map<string, Source>>(new Map()) +/** + * A cache of sources that have been consumed by the post thread view. This is + * not cleaned up, but because we use a new ID for each post thread view that + * consumes a source, this is never reused unless a user navigates back to a + * post thread view that has not been dropped from memory. + */ +const consumedSources = new Map<string, PostSource>() - const setUnstablePostSource = useCallback((key: string, source: Source) => { - sourcesRef.current.set(key, source) - }, []) +/** + * For stashing the feed that the user was browsing when they clicked on a post. + * + * Used for FeedFeedback and other ephemeral non-critical systems. + */ +export function setUnstablePostSource(key: string, source: PostSource) { + assertValid( + key, + `setUnstablePostSource key should be a URI containing a handle, received ${key} — use buildPostSourceKey`, + ) + logger.debug('set', {key, source}) + transientSources.set(key, source) +} - const consumeUnstablePostSource = useCallback((uri: string) => { - const source = sourcesRef.current.get(uri) +/** + * This hook is unstable and should only be used for FeedFeedback and other + * ephemeral non-critical systems. Views that use this hook will continue to + * return a reference to the same source until those views are dropped from + * memory. + */ +export function useUnstablePostSource(key: string) { + const id = useId() + const [source] = useState(() => { + assertValid( + key, + `consumeUnstablePostSource key should be a URI containing a handle, received ${key} — use buildPostSourceKey`, + ) + const source = consumedSources.get(id) || transientSources.get(key) if (source) { - sourcesRef.current.delete(uri) + logger.debug('consume', {id, key, source}) + transientSources.delete(key) + consumedSources.set(id, source) } return source - }, []) - - return ( - <SetUnstablePostSourceContext.Provider value={setUnstablePostSource}> - <ConsumeUnstablePostSourceContext.Provider - value={consumeUnstablePostSource}> - {children} - </ConsumeUnstablePostSourceContext.Provider> - </SetUnstablePostSourceContext.Provider> - ) -} + }) -export function useSetUnstablePostSource() { - return useContext(SetUnstablePostSourceContext) + useEffect(() => { + return () => { + consumedSources.delete(id) + logger.debug('cleanup', {id}) + } + }, [id]) + + return source } /** - * 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. + * Builds a post source key. This (atm) is a URI where the `host` is the post + * author's handle, not DID. */ -export function useUnstablePostSource(uri: string) { - const consume = useContext(ConsumeUnstablePostSourceContext) +export function buildPostSourceKey(key: string, handle: string) { + const urip = new AtUri(key) + urip.host = handle + return urip.toString() +} - const [source] = useState(() => consume(uri)) - return source +/** + * Just a lil dev helper + */ +function assertValid(key: string, message: string) { + if (__DEV__) { + const urip = new AtUri(key) + if (urip.host.startsWith('did:')) { + throw new Error(message) + } + } } diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index d974ce6b5..5bec9ced1 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -22,6 +22,7 @@ import {ScrollProvider} from '#/lib/ScrollContext' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {cleanError} from '#/lib/strings/errors' import {isAndroid, isNative, isWeb} from '#/platform/detection' +import {useFeedFeedback} from '#/state/feed-feedback' import {useModerationOpts} from '#/state/preferences/moderation-opts' import { fillThreadModerationCache, @@ -37,6 +38,7 @@ import {useSetThreadViewPreferencesMutation} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences' import {useSession} from '#/state/session' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' +import {useUnstablePostSource} from '#/state/unstable-post-source' import {List, type ListMethods} from '#/view/com/util/List' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' @@ -93,7 +95,7 @@ const keyExtractor = (item: RowItem) => { return item._reactKey } -export function PostThread({uri}: {uri: string | undefined}) { +export function PostThread({uri}: {uri: string}) { const {hasSession, currentAccount} = useSession() const {_} = useLingui() const t = useTheme() @@ -104,6 +106,8 @@ export function PostThread({uri}: {uri: string | undefined}) { HiddenRepliesState.Hide, ) const headerRef = React.useRef<View | null>(null) + const anchorPostSource = useUnstablePostSource(uri) + const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession) const {data: preferences} = usePreferencesQuery() const { @@ -395,10 +399,18 @@ export function PostThread({uri}: {uri: string | undefined}) { ) const {openComposer} = useOpenComposer() - const onPressReply = React.useCallback(() => { + const onReplyToAnchor = React.useCallback(() => { if (thread?.type !== 'post') { return } + if (anchorPostSource) { + feedFeedback.sendInteraction({ + item: thread.post.uri, + event: 'app.bsky.feed.defs#interactionReply', + feedContext: anchorPostSource.post.feedContext, + reqId: anchorPostSource.post.reqId, + }) + } openComposer({ replyTo: { uri: thread.post.uri, @@ -410,7 +422,14 @@ export function PostThread({uri}: {uri: string | undefined}) { }, onPost: onPostReply, }) - }, [openComposer, thread, onPostReply, threadModerationCache]) + }, [ + openComposer, + thread, + onPostReply, + threadModerationCache, + anchorPostSource, + feedFeedback, + ]) const canReply = !error && rootPost && !rootPost.viewer?.replyDisabled const hasParents = @@ -423,7 +442,7 @@ export function PostThread({uri}: {uri: string | undefined}) { return ( <View> {!isMobile && ( - <PostThreadComposePrompt onPressCompose={onPressReply} /> + <PostThreadComposePrompt onPressCompose={onReplyToAnchor} /> )} </View> ) @@ -511,6 +530,7 @@ export function PostThread({uri}: {uri: string | undefined}) { } onPostReply={onPostReply} hideTopBorder={index === 0 && !item.ctx.isParentLoading} + anchorPostSource={anchorPostSource} /> </View> ) @@ -586,7 +606,7 @@ export function PostThread({uri}: {uri: string | undefined}) { /> </ScrollProvider> {isMobile && canReply && hasSession && ( - <MobileComposePrompt onPressReply={onPressReply} /> + <MobileComposePrompt onPressReply={onReplyToAnchor} /> )} </> ) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 8b39072ba..576b195a0 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -40,7 +40,7 @@ import {useLanguagePrefs} from '#/state/preferences' import {type ThreadPost} from '#/state/queries/post-thread' import {useSession} from '#/state/session' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' -import {useUnstablePostSource} from '#/state/unstable-post-source' +import {type PostSource} from '#/state/unstable-post-source' import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {Link, TextLink} from '#/view/com/util/Link' @@ -87,6 +87,7 @@ export function PostThreadItem({ onPostReply, hideTopBorder, threadgateRecord, + anchorPostSource, }: { post: AppBskyFeedDefs.PostView record: AppBskyFeedPost.Record @@ -104,6 +105,7 @@ export function PostThreadItem({ onPostReply: (postUri: string | undefined) => void hideTopBorder?: boolean threadgateRecord?: AppBskyFeedThreadgate.Record + anchorPostSource?: PostSource }) { const postShadowed = usePostShadow(post) const richText = useMemo( @@ -139,6 +141,7 @@ export function PostThreadItem({ onPostReply={onPostReply} hideTopBorder={hideTopBorder} threadgateRecord={threadgateRecord} + anchorPostSource={anchorPostSource} /> ) } @@ -184,6 +187,7 @@ let PostThreadItemLoaded = ({ onPostReply, hideTopBorder, threadgateRecord, + anchorPostSource, }: { post: Shadow<AppBskyFeedDefs.PostView> record: AppBskyFeedPost.Record @@ -202,10 +206,10 @@ let PostThreadItemLoaded = ({ onPostReply: (postUri: string | undefined) => void hideTopBorder?: boolean threadgateRecord?: AppBskyFeedThreadgate.Record + anchorPostSource?: PostSource }): React.ReactNode => { const {currentAccount, hasSession} = useSession() - const source = useUnstablePostSource(post.uri) - const feedFeedback = useFeedFeedback(source?.feed, hasSession) + const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession) const t = useTheme() const pal = usePalette('default') @@ -276,12 +280,12 @@ let PostThreadItemLoaded = ({ ) const onPressReply = () => { - if (source) { + if (anchorPostSource && isHighlightedPost) { feedFeedback.sendInteraction({ item: post.uri, event: 'app.bsky.feed.defs#interactionReply', - feedContext: source.post.feedContext, - reqId: source.post.reqId, + feedContext: anchorPostSource.post.feedContext, + reqId: anchorPostSource.post.reqId, }) } openComposer({ @@ -298,23 +302,23 @@ let PostThreadItemLoaded = ({ } const onOpenAuthor = () => { - if (source) { + if (anchorPostSource) { feedFeedback.sendInteraction({ item: post.uri, event: 'app.bsky.feed.defs#clickthroughAuthor', - feedContext: source.post.feedContext, - reqId: source.post.reqId, + feedContext: anchorPostSource.post.feedContext, + reqId: anchorPostSource.post.reqId, }) } } const onOpenEmbed = () => { - if (source) { + if (anchorPostSource) { feedFeedback.sendInteraction({ item: post.uri, event: 'app.bsky.feed.defs#clickthroughEmbed', - feedContext: source.post.feedContext, - reqId: source.post.reqId, + feedContext: anchorPostSource.post.feedContext, + reqId: anchorPostSource.post.reqId, }) } } @@ -325,7 +329,7 @@ let PostThreadItemLoaded = ({ const {isActive: live} = useActorStatus(post.author) - const reason = source?.post.reason + const reason = anchorPostSource?.post.reason const viaRepost = useMemo(() => { if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) { return { @@ -550,8 +554,8 @@ let PostThreadItemLoaded = ({ onPostReply={onPostReply} logContext="PostThreadItem" threadgateRecord={threadgateRecord} - feedContext={source?.post?.feedContext} - reqId={source?.post?.reqId} + feedContext={anchorPostSource?.post?.feedContext} + reqId={anchorPostSource?.post?.reqId} viaRepost={viaRepost} /> </FeedFeedbackProvider> diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index b9aa67673..fd0d1c707 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -36,7 +36,10 @@ import {useFeedFeedbackContext} from '#/state/feed-feedback' import {unstableCacheProfileView} from '#/state/queries/profile' import {useSession} from '#/state/session' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' -import {useSetUnstablePostSource} from '#/state/unstable-post-source' +import { + buildPostSourceKey, + setUnstablePostSource, +} from '#/state/unstable-post-source' import {FeedNameText} from '#/view/com/util/FeedInfoText' import {Link, TextLink, TextLinkOnWebOnly} from '#/view/com/util/Link' import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' @@ -176,7 +179,6 @@ let FeedItemInner = ({ return makeProfileLink(post.author, 'post', urip.rkey) }, [post.uri, post.author]) const {sendInteraction, feedDescriptor} = useFeedFeedbackContext() - const unstableSetPostSource = useSetUnstablePostSource() const onPressReply = () => { sendInteraction({ @@ -232,7 +234,7 @@ let FeedItemInner = ({ reqId, }) unstableCacheProfileView(queryClient, post.author) - unstableSetPostSource(post.uri, { + setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), { feed: feedDescriptor, post: { post, |