diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 40a7ca6cf4..a718a761d5 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -141,12 +141,12 @@ export function useFeedFeedback( const sendInteraction = useCallback( (interaction: AppBskyFeedDefs.Interaction) => { - logger.debug('sendInteraction', { - ...interaction, - }) 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 2a741e615d..a999b53b54 100644 --- a/src/state/unstable-post-source.tsx +++ b/src/state/unstable-post-source.tsx @@ -1,5 +1,12 @@ -import {createContext, useCallback, useContext, useRef, useState} from 'react' -import {type AppBskyFeedDefs,AtUri} from '@atproto/api' +import { + createContext, + useCallback, + useContext, + useId, + useRef, + useState, +} from 'react' +import {type AppBskyFeedDefs, AtUri} from '@atproto/api' import {Logger} from '#/logger' import {type FeedDescriptor} from '#/state/queries/post-feed' @@ -12,7 +19,7 @@ const logger = Logger.create(Logger.Context.PostSource) * and other ephemeral non-critical systems. */ -type Source = { +export type Source = { post: AppBskyFeedDefs.FeedViewPost feed?: FeedDescriptor } @@ -21,9 +28,11 @@ const SetUnstablePostSourceContext = createContext< (uri: string, source: Source) => void >(() => {}) const ConsumeUnstablePostSourceContext = createContext< - (uri: string) => Source | undefined + (uri: string, id: string) => Source | undefined >(() => undefined) +const persistentSourcesRef = new Map(new Map()) + export function Provider({children}: {children: React.ReactNode}) { const sourcesRef = useRef>(new Map()) @@ -41,7 +50,7 @@ export function Provider({children}: {children: React.ReactNode}) { sourcesRef.current.set(uri, source) }, []) - const consumeUnstablePostSource = useCallback((uri: string) => { + const consumeUnstablePostSource = useCallback((uri: string, id: string) => { if (__DEV__) { const urip = new AtUri(uri) if (urip.host.startsWith('did:')) { @@ -51,11 +60,12 @@ export function Provider({children}: {children: React.ReactNode}) { } } - const source = sourcesRef.current.get(uri) + const source = persistentSourcesRef.get(id) || sourcesRef.current.get(uri) if (source) { logger.debug('consume', {uri, source}) sourcesRef.current.delete(uri) + persistentSourcesRef.set(id, source) } return source @@ -80,9 +90,9 @@ export function useSetUnstablePostSource() { * and other ephemeral non-critical systems. Does not change when the URI changes. */ export function useUnstablePostSource(uri: string) { + const id = useId() const consume = useContext(ConsumeUnstablePostSourceContext) - - const [source] = useState(() => consume(uri)) + const [source] = useState(() => consume(uri, id)) return source } diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index d974ce6b53..5bec9ced1a 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(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 ( {!isMobile && ( - + )} ) @@ -511,6 +530,7 @@ export function PostThread({uri}: {uri: string | undefined}) { } onPostReply={onPostReply} hideTopBorder={index === 0 && !item.ctx.isParentLoading} + anchorPostSource={anchorPostSource} /> ) @@ -586,7 +606,7 @@ export function PostThread({uri}: {uri: string | undefined}) { /> {isMobile && canReply && hasSession && ( - + )} ) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 1c1046247e..aaa04ed092 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -40,10 +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 { - buildPostSourceUri, - useUnstablePostSource, -} from '#/state/unstable-post-source' +import {type Source} 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' @@ -90,6 +87,7 @@ export function PostThreadItem({ onPostReply, hideTopBorder, threadgateRecord, + anchorPostSource, }: { post: AppBskyFeedDefs.PostView record: AppBskyFeedPost.Record @@ -107,6 +105,7 @@ export function PostThreadItem({ onPostReply: (postUri: string | undefined) => void hideTopBorder?: boolean threadgateRecord?: AppBskyFeedThreadgate.Record + anchorPostSource?: Source }) { const postShadowed = usePostShadow(post) const richText = useMemo( @@ -142,6 +141,7 @@ export function PostThreadItem({ onPostReply={onPostReply} hideTopBorder={hideTopBorder} threadgateRecord={threadgateRecord} + anchorPostSource={anchorPostSource} /> ) } @@ -187,6 +187,7 @@ let PostThreadItemLoaded = ({ onPostReply, hideTopBorder, threadgateRecord, + anchorPostSource, }: { post: Shadow record: AppBskyFeedPost.Record @@ -205,12 +206,10 @@ let PostThreadItemLoaded = ({ onPostReply: (postUri: string | undefined) => void hideTopBorder?: boolean threadgateRecord?: AppBskyFeedThreadgate.Record + anchorPostSource?: Source }): React.ReactNode => { const {currentAccount, hasSession} = useSession() - const source = useUnstablePostSource( - buildPostSourceUri(post.uri, post.author.handle), - ) - const feedFeedback = useFeedFeedback(source?.feed, hasSession) + const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession) const t = useTheme() const pal = usePalette('default') @@ -281,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({ @@ -303,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, }) } } @@ -330,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 { @@ -555,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} />