From cf63c2ca07c9a77bb92449ea4f3d78b8dd54fb8f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 28 May 2025 22:09:28 +0300 Subject: Send FeedFeedback interactions in thread view (#8414) --- src/App.native.tsx | 19 ++-- src/App.web.tsx | 11 ++- src/components/PostControls/index.tsx | 9 +- src/screens/VideoFeed/index.tsx | 7 +- src/state/feed-feedback.tsx | 51 ++++++---- src/state/queries/notifications/types.ts | 2 + src/state/queries/notifications/util.ts | 11 ++- src/state/queries/post.ts | 22 +++-- src/state/unstable-post-source.tsx | 73 ++++++++++++++ src/view/com/notifications/NotificationFeed.tsx | 4 +- .../com/notifications/NotificationFeedItem.tsx | 54 ++++++++++- src/view/com/post-thread/PostThreadItem.tsx | 107 +++++++++++++++------ src/view/com/posts/PostFeedItem.tsx | 27 +++++- 13 files changed, 319 insertions(+), 78 deletions(-) create mode 100644 src/state/unstable-post-source.tsx (limited to 'src') diff --git a/src/App.native.tsx b/src/App.native.tsx index baab8c838..e3f85c0fe 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -58,6 +58,7 @@ 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' @@ -150,14 +151,16 @@ function InnerApp() { - - - - - - - + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index c5ec0473c..97ada6148 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -48,6 +48,7 @@ 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' @@ -131,10 +132,12 @@ function InnerApp() { - - - - + + + + + + diff --git a/src/components/PostControls/index.tsx b/src/components/PostControls/index.tsx index 7739da56b..f024928ee 100644 --- a/src/components/PostControls/index.tsx +++ b/src/components/PostControls/index.tsx @@ -50,6 +50,7 @@ let PostControls = ({ logContext, threadgateRecord, onShowLess, + viaRepost, }: { big?: boolean post: Shadow @@ -63,13 +64,19 @@ let PostControls = ({ logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' threadgateRecord?: AppBskyFeedThreadgate.Record onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void + viaRepost?: {uri: string; cid: string} }): React.ReactNode => { const {_, i18n} = useLingui() const {gtMobile} = useBreakpoints() const {openComposer} = useOpenComposer() - const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext) + const [queueLike, queueUnlike] = usePostLikeMutationQueue( + post, + viaRepost, + logContext, + ) const [queueRepost, queueUnrepost] = usePostRepostMutationQueue( post, + viaRepost, logContext, ) const requireAuth = useRequireAuth() diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx index 2a61db715..21eb53baf 100644 --- a/src/screens/VideoFeed/index.tsx +++ b/src/screens/VideoFeed/index.tsx @@ -1023,7 +1023,12 @@ function PlayPauseTapArea({ const {_} = useLingui() const doubleTapRef = useRef | null>(null) const playHaptic = useHaptics() - const [queueLike] = usePostLikeMutationQueue(post, 'ImmersiveVideo') + // TODO: implement viaRepost -sfn + const [queueLike] = usePostLikeMutationQueue( + post, + undefined, + 'ImmersiveVideo', + ) const {sendInteraction} = useFeedFeedbackContext() const {isPlaying} = useEvent(player, 'playingChange', { isPlaying: player.playing, 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({ +const stateContext = createContext({ 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>(new Set()) - const history = React.useRef< + const queue = useRef>(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 >(new WeakSet()) - const aggregatedStats = React.useRef(null) - const throttledFlushAggregatedStats = React.useMemo( + const aggregatedStats = useRef(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 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( 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, + 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, + 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>(() => 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 ( + + + {children} + + + ) +} + +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 +} diff --git a/src/view/com/notifications/NotificationFeed.tsx b/src/view/com/notifications/NotificationFeed.tsx index 73cebf868..1f87b3186 100644 --- a/src/view/com/notifications/NotificationFeed.tsx +++ b/src/view/com/notifications/NotificationFeed.tsx @@ -1,7 +1,7 @@ import React from 'react' import { ActivityIndicator, - ListRenderItemInfo, + type ListRenderItemInfo, StyleSheet, View, } from 'react-native' @@ -16,7 +16,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' import {EmptyState} from '#/view/com/util/EmptyState' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import {List, ListRef} from '#/view/com/util/List' +import {List, type ListRef} from '#/view/com/util/List' import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {NotificationFeedItem} from './NotificationFeedItem' diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index a30aba7d8..1f99a3c34 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -446,6 +446,55 @@ let NotificationFeedItem = ({ ) icon = + } else if (item.type === 'like-via-repost') { + a11yLabel = hasMultipleAuthors + ? _( + msg`${firstAuthorName} and ${plural(additionalAuthorsCount, { + one: `${formattedAuthorsCount} other`, + other: `${formattedAuthorsCount} others`, + })} liked your repost`, + ) + : _(msg`${firstAuthorName} liked your repost`) + notificationContent = hasMultipleAuthors ? ( + + {firstAuthorLink} and{' '} + + + {' '} + liked your repost + + ) : ( + {firstAuthorLink} liked your repost + ) + } else if (item.type === 'repost-via-repost') { + a11yLabel = hasMultipleAuthors + ? _( + msg`${firstAuthorName} and ${plural(additionalAuthorsCount, { + one: `${formattedAuthorsCount} other`, + other: `${formattedAuthorsCount} others`, + })} reposted your repost`, + ) + : _(msg`${firstAuthorName} reposted your repost`) + notificationContent = hasMultipleAuthors ? ( + + {firstAuthorLink} and{' '} + + + {' '} + reposted your repost + + ) : ( + {firstAuthorLink} reposted your repost + ) + icon = } else { return null } @@ -553,7 +602,10 @@ let NotificationFeedItem = ({ - {item.type === 'post-like' || item.type === 'repost' ? ( + {item.type === 'post-like' || + item.type === 'repost' || + item.type === 'like-via-repost' || + item.type === 'repost-via-repost' ? ( diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 82852aa62..77adebac9 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -1,4 +1,4 @@ -import React, {memo, useMemo} from 'react' +import {memo, useCallback, useMemo, useState} from 'react' import { type GestureResponderEvent, StyleSheet, @@ -6,7 +6,7 @@ import { View, } from 'react-native' import { - type AppBskyFeedDefs, + AppBskyFeedDefs, AppBskyFeedPost, type AppBskyFeedThreadgate, AtUri, @@ -35,10 +35,12 @@ import { usePostShadow, } from '#/state/cache/post-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow' +import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' 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 {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {Link, TextLink} from '#/view/com/util/Link' @@ -201,18 +203,21 @@ let PostThreadItemLoaded = ({ hideTopBorder?: boolean threadgateRecord?: AppBskyFeedThreadgate.Record }): React.ReactNode => { + const {currentAccount, hasSession} = useSession() + const source = useUnstablePostSource(post.uri) + const feedFeedback = useFeedFeedback(source?.feed, hasSession) + const t = useTheme() const pal = usePalette('default') const {_, i18n} = useLingui() const langPrefs = useLanguagePrefs() const {openComposer} = useOpenComposer() - const [limitLines, setLimitLines] = React.useState( + const [limitLines, setLimitLines] = useState( () => countLines(richText?.text) >= MAX_POST_LINES, ) - const {currentAccount} = useSession() const shadowedPostAuthor = useProfileShadow(post.author) const rootUri = record.reply?.root?.uri || post.uri - const postHref = React.useMemo(() => { + const postHref = useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey) }, [post.uri, post.author]) @@ -220,12 +225,12 @@ let PostThreadItemLoaded = ({ const authorHref = makeProfileLink(post.author) const authorTitle = post.author.handle const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did - const likesHref = React.useMemo(() => { + const likesHref = useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') }, [post.uri, post.author]) const likesTitle = _(msg`Likes on this post`) - const repostsHref = React.useMemo(() => { + const repostsHref = useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by') }, [post.uri, post.author]) @@ -233,7 +238,7 @@ let PostThreadItemLoaded = ({ const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ threadgateRecord, }) - const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => { + const additionalPostAlerts: AppModerationCause[] = useMemo(() => { const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) const isControlledByViewer = new AtUri(rootUri).host === currentAccount?.did return isControlledByViewer && isPostHiddenByThreadgate @@ -246,7 +251,7 @@ let PostThreadItemLoaded = ({ ] : [] }, [post, currentAccount?.did, threadgateHiddenReplies, rootUri]) - const quotesHref = React.useMemo(() => { + const quotesHref = useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey, 'quotes') }, [post.uri, post.author]) @@ -270,7 +275,15 @@ let PostThreadItemLoaded = ({ [post, langPrefs.primaryLanguage], ) - const onPressReply = React.useCallback(() => { + const onPressReply = () => { + if (source) { + feedFeedback.sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionReply', + feedContext: source.post.feedContext, + reqId: source.post.reqId, + }) + } openComposer({ replyTo: { uri: post.uri, @@ -282,14 +295,46 @@ let PostThreadItemLoaded = ({ }, onPost: onPostReply, }) - }, [openComposer, post, record, onPostReply, moderation]) + } - const onPressShowMore = React.useCallback(() => { + const onOpenAuthor = () => { + if (source) { + feedFeedback.sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#clickthroughAuthor', + feedContext: source.post.feedContext, + reqId: source.post.reqId, + }) + } + } + + const onOpenEmbed = () => { + if (source) { + feedFeedback.sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#clickthroughEmbed', + feedContext: source.post.feedContext, + reqId: source.post.reqId, + }) + } + } + + const onPressShowMore = useCallback(() => { setLimitLines(false) }, [setLimitLines]) const {isActive: live} = useActorStatus(post.author) + const reason = source?.post.reason + const viaRepost = useMemo(() => { + if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) { + return { + uri: reason.uri, + cid: reason.cid, + } + } + }, [reason]) + if (!record) { return } @@ -309,10 +354,8 @@ let PostThreadItemLoaded = ({ @@ -334,13 +377,15 @@ let PostThreadItemLoaded = ({ moderation={moderation.ui('avatar')} type={post.author.associated?.labeler ? 'labeler' : 'user'} live={live} + onBeforePress={onOpenAuthor} /> + title={authorTitle} + onBeforePress={onOpenAuthor}> )} @@ -494,16 +540,21 @@ let PostThreadItemLoaded = ({ marginLeft: -5, }, ]}> - + + + @@ -779,7 +830,7 @@ function ExpandedPostDetails({ const isRootPost = !('reply' in post.record) const langPrefs = useLanguagePrefs() - const onTranslatePress = React.useCallback( + const onTranslatePress = useCallback( (e: GestureResponderEvent) => { e.preventDefault() openLink(translatorUrl, true) diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index 3735bbb5a..b9aa67673 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -33,9 +33,10 @@ import { usePostShadow, } from '#/state/cache/post-shadow' import {useFeedFeedbackContext} from '#/state/feed-feedback' -import {precacheProfile} from '#/state/queries/profile' +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 {FeedNameText} from '#/view/com/util/FeedInfoText' import {Link, TextLink, TextLinkOnWebOnly} from '#/view/com/util/Link' import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' @@ -174,7 +175,8 @@ let FeedItemInner = ({ const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey) }, [post.uri, post.author]) - const {sendInteraction} = useFeedFeedbackContext() + const {sendInteraction, feedDescriptor} = useFeedFeedbackContext() + const unstableSetPostSource = useSetUnstablePostSource() const onPressReply = () => { sendInteraction({ @@ -229,7 +231,16 @@ let FeedItemInner = ({ feedContext, reqId, }) - precacheProfile(queryClient, post.author) + unstableCacheProfileView(queryClient, post.author) + unstableSetPostSource(post.uri, { + feed: feedDescriptor, + post: { + post, + reason: AppBskyFeedDefs.isReasonRepost(reason) ? reason : undefined, + feedContext, + reqId, + }, + }) } const outerStyles = [ @@ -263,6 +274,15 @@ let FeedItemInner = ({ const {isActive: live} = useActorStatus(post.author) + const viaRepost = useMemo(() => { + if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) { + return { + uri: reason.uri, + cid: reason.cid, + } + } + }, [reason]) + return ( -- cgit 1.4.1