diff options
-rw-r--r-- | package.json | 4 | ||||
-rw-r--r-- | src/App.native.tsx | 19 | ||||
-rw-r--r-- | src/App.web.tsx | 11 | ||||
-rw-r--r-- | src/components/PostControls/index.tsx | 9 | ||||
-rw-r--r-- | src/screens/VideoFeed/index.tsx | 7 | ||||
-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 | ||||
-rw-r--r-- | src/view/com/notifications/NotificationFeed.tsx | 4 | ||||
-rw-r--r-- | src/view/com/notifications/NotificationFeedItem.tsx | 54 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 107 | ||||
-rw-r--r-- | src/view/com/posts/PostFeedItem.tsx | 27 | ||||
-rw-r--r-- | yarn.lock | 84 |
15 files changed, 363 insertions, 122 deletions
diff --git a/package.json b/package.json index fdf668045..5675681ca 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.15.8", + "@atproto/api": "^0.15.9", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", @@ -219,7 +219,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/dev-env": "^0.3.132", + "@atproto/dev-env": "^0.3.133", "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/runtime": "^7.26.0", 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() { <MutedThreadsProvider> <ProgressGuideProvider> <ServiceAccountManager> - <GestureHandlerRootView - style={s.h100pct}> - <IntentDialogProvider> - <TestCtrls /> - <Shell /> - <NuxDialogs /> - </IntentDialogProvider> - </GestureHandlerRootView> + <UnstablePostSourceProvider> + <GestureHandlerRootView + style={s.h100pct}> + <IntentDialogProvider> + <TestCtrls /> + <Shell /> + <NuxDialogs /> + </IntentDialogProvider> + </GestureHandlerRootView> + </UnstablePostSourceProvider> </ServiceAccountManager> </ProgressGuideProvider> </MutedThreadsProvider> 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() { <SafeAreaProvider> <ProgressGuideProvider> <ServiceConfigProvider> - <IntentDialogProvider> - <Shell /> - <NuxDialogs /> - </IntentDialogProvider> + <UnstablePostSourceProvider> + <IntentDialogProvider> + <Shell /> + <NuxDialogs /> + </IntentDialogProvider> + </UnstablePostSourceProvider> </ServiceConfigProvider> </ProgressGuideProvider> </SafeAreaProvider> 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<AppBskyFeedDefs.PostView> @@ -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<ReturnType<typeof setTimeout> | 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<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 +} 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 = ({ </Trans> ) icon = <VerifiedCheck size="xl" fill={t.palette.contrast_500} /> + } 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 ? ( + <Trans> + {firstAuthorLink} and{' '} + <Text style={[a.text_md, a.font_bold, a.leading_snug]}> + <Plural + value={additionalAuthorsCount} + one={`${formattedAuthorsCount} other`} + other={`${formattedAuthorsCount} others`} + /> + </Text>{' '} + liked your repost + </Trans> + ) : ( + <Trans>{firstAuthorLink} liked your repost</Trans> + ) + } 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 ? ( + <Trans> + {firstAuthorLink} and{' '} + <Text style={[a.text_md, a.font_bold, a.leading_snug]}> + <Plural + value={additionalAuthorsCount} + one={`${formattedAuthorsCount} other`} + other={`${formattedAuthorsCount} others`} + /> + </Text>{' '} + reposted your repost + </Trans> + ) : ( + <Trans>{firstAuthorLink} reposted your repost</Trans> + ) + icon = <RepostIcon size="xl" style={{color: t.palette.positive_600}} /> } else { return null } @@ -553,7 +602,10 @@ let NotificationFeedItem = ({ </TimeElapsed> </Text> </ExpandListPressable> - {item.type === 'post-like' || item.type === 'repost' ? ( + {item.type === 'post-like' || + item.type === 'repost' || + item.type === 'like-via-repost' || + item.type === 'repost-via-repost' ? ( <View style={[a.pt_2xs]}> <AdditionalPostText post={item.subject} /> </View> 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 <ErrorMessage message={_(msg`Invalid or unsupported post record`)} /> } @@ -309,10 +354,8 @@ let PostThreadItemLoaded = ({ <View style={[ styles.replyLine, - { - flexGrow: 1, - backgroundColor: pal.colors.replyLine, - }, + a.flex_grow, + {backgroundColor: pal.colors.replyLine}, ]} /> </View> @@ -334,13 +377,15 @@ let PostThreadItemLoaded = ({ moderation={moderation.ui('avatar')} type={post.author.associated?.labeler ? 'labeler' : 'user'} live={live} + onBeforePress={onOpenAuthor} /> <View style={[a.flex_1]}> <View style={[a.flex_row, a.align_center]}> <Link style={[a.flex_shrink]} href={authorHref} - title={authorTitle}> + title={authorTitle} + onBeforePress={onOpenAuthor}> <Text emoji style={[ @@ -413,6 +458,7 @@ let PostThreadItemLoaded = ({ embed={post.embed} moderation={moderation} viewContext={PostEmbedViewContext.ThreadHighlighted} + onOpen={onOpenEmbed} /> </View> )} @@ -494,16 +540,21 @@ let PostThreadItemLoaded = ({ marginLeft: -5, }, ]}> - <PostControls - big - post={post} - record={record} - richText={richText} - onPressReply={onPressReply} - onPostReply={onPostReply} - logContext="PostThreadItem" - threadgateRecord={threadgateRecord} - /> + <FeedFeedbackProvider value={feedFeedback}> + <PostControls + big + post={post} + record={record} + richText={richText} + onPressReply={onPressReply} + onPostReply={onPostReply} + logContext="PostThreadItem" + threadgateRecord={threadgateRecord} + feedContext={source?.post?.feedContext} + reqId={source?.post?.reqId} + viaRepost={viaRepost} + /> + </FeedFeedbackProvider> </View> </View> </View> @@ -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 ( <Link testID={`feedItem-by-${post.author.handle}`} @@ -450,6 +470,7 @@ let FeedItemInner = ({ reqId={reqId} threadgateRecord={threadgateRecord} onShowLess={onShowLess} + viaRepost={viaRepost} /> </View> diff --git a/yarn.lock b/yarn.lock index 776f5ef7e..a9901c42c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,10 +63,10 @@ "@atproto/xrpc" "^0.7.0" "@atproto/xrpc-server" "^0.7.18" -"@atproto/api@^0.15.8": - version "0.15.8" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.8.tgz#f284a9c225191ebd35b46f5695932ab649c04a61" - integrity sha512-PsCgmV4zPjN8VuJMruxqauhn88PuS0b8t2Xsjl4617+bCPpY513jVlxgNH/XExxO7TSVvJM7EzdLY4o3fqh/xQ== +"@atproto/api@^0.15.9": + version "0.15.9" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.9.tgz#f8c40afd6e414ab107d63d6f08d9e264bf9a149a" + integrity sha512-CyAILiIcbN+V5CFAI6MDb247epm25RGkP7HSan5LUaOHiyg1NCAmflWCN/bbMdJX9kLqjAPAG3eN4BUUbYe//Q== dependencies: "@atproto/common-web" "^0.4.2" "@atproto/lexicon" "^0.4.11" @@ -94,14 +94,14 @@ multiformats "^9.9.0" uint8arrays "3.0.0" -"@atproto/bsky@^0.0.150": - version "0.0.150" - resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.150.tgz#6626095875d805d0d3f38fa4e184b9f7d274c80f" - integrity sha512-dn1jzP1EId842+g78Q6EMdOmgEZxa9bSq20HMdd5/R8uu559mPs8zigFuddqCoT1fRaJXFC8ZP7Jk5asvBQhrA== +"@atproto/bsky@^0.0.151": + version "0.0.151" + resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.151.tgz#a0e5b59e163a3b74379fb547601be4fc66b7a133" + integrity sha512-42pvUsyGw0nR6Sxlda824maY4gBxUni1cXPG+7uGe6Ixm6XAaPhfTgT1rAg++1rDXH9tT1EXAVnMxg38S6osLg== dependencies: "@atproto-labs/fetch-node" "0.1.9" "@atproto-labs/xrpc-utils" "0.0.14" - "@atproto/api" "^0.15.8" + "@atproto/api" "^0.15.9" "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" "@atproto/did" "^0.1.5" @@ -218,20 +218,20 @@ "@noble/hashes" "^1.6.1" uint8arrays "3.0.0" -"@atproto/dev-env@^0.3.132": - version "0.3.132" - resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.132.tgz#78d55ef08a368a752c55b1ee7b7c08a41f27b5ac" - integrity sha512-RFd/9kgvmbP859N6NLu/FxCzLsj01iq22P9jNpL+dQNXbWXHYwGMUa6edf/ZrljNi3dFBNxabdDZJ2q+8uvBJQ== +"@atproto/dev-env@^0.3.133": + version "0.3.133" + resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.133.tgz#4ca58c9c4c99f001f26ce50629214f81d6acd3ab" + integrity sha512-GtKDa+q0Fx2tJZL44cDAINMCxNmt1aKkGVpW/6PTnuSSjdA7ErBUEL3opbwgaAcPRGZfscB0mQmGfWR0BUmvUw== dependencies: - "@atproto/api" "^0.15.8" - "@atproto/bsky" "^0.0.150" + "@atproto/api" "^0.15.9" + "@atproto/bsky" "^0.0.151" "@atproto/bsync" "^0.0.19" "@atproto/common-web" "^0.4.2" "@atproto/crypto" "^0.4.4" "@atproto/identity" "^0.4.8" "@atproto/lexicon" "^0.4.11" - "@atproto/ozone" "^0.1.111" - "@atproto/pds" "^0.4.138" + "@atproto/ozone" "^0.1.112" + "@atproto/pds" "^0.4.139" "@atproto/sync" "^0.1.23" "@atproto/syntax" "^0.4.0" "@atproto/xrpc-server" "^0.7.18" @@ -294,24 +294,24 @@ "@atproto/jwk" "0.1.5" "@atproto/oauth-types" "0.2.7" -"@atproto/oauth-provider-frontend@0.1.4": - version "0.1.4" - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.1.4.tgz#240a2e58c29d32fa7d4ea9d142c00c23d2469452" - integrity sha512-TLKL5lTmSieHx7+3RVIx7rIxRPP1SNCwzzdTvYB46yd1XrGHdPU//M6CP5OZ1BvcxF6H4JXIkOSWvFseol+gOw== +"@atproto/oauth-provider-frontend@0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.1.5.tgz#66fd8760fade2ac94111ad5389f33f4d8ce5bba2" + integrity sha512-FdDBuwy827+etjIcRwZU7dtxa8Ltso3ufVLMEi8A2V91v21XDysZjLANC6cvmNNSUcS4E/J6ZAwTrQDo7O5axw== optionalDependencies: "@atproto/oauth-provider-api" "0.1.2" -"@atproto/oauth-provider-ui@0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.5.tgz#b080c5e814975821689c5976c27ac1081211106f" - integrity sha512-pW0Vx3kvIWH1Mu3SOImNHP9JbmhSj2e3ChDvtfXCWI1oC03fiaMlJfdxrx9Plq5Z+DajnCzPzrf1Lvbopjf94Q== +"@atproto/oauth-provider-ui@0.1.6": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.6.tgz#4bae995ff57671ac3915f58fdb2cf6a76a0fe42d" + integrity sha512-pJzV9ouNj1/TDUCl3CWEZrHoUese4lcKx5F59t2OiLFm2K7T7QrszKUIMyU5QdiQHv551B0ZJOkJ8+4b/fVGPA== optionalDependencies: "@atproto/oauth-provider-api" "0.1.2" -"@atproto/oauth-provider@^0.7.7": - version "0.7.7" - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.7.7.tgz#dbbdeb405ab1d239fd926340f83fb41e13455011" - integrity sha512-ElphzmOjw1hr42HN4dD6sMAQFtpTkaJ8bBDAsbL9YBVJDEGhmHsF3Ye8mDUO4nhEdg7PUTWiCzXyqnaorAjiTA== +"@atproto/oauth-provider@^0.7.8": + version "0.7.8" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.7.8.tgz#287b15eb6b0bc0bb4b2da2339150253db006c6e0" + integrity sha512-+dEU9dTyfWKeZ/Nu7ocR6fO73RcG0vwDjT45vgcnM9L7jtuPk9zfpmiR4ODYBk9QUu2DURo9yBhtXNJI3Yz8aQ== dependencies: "@atproto-labs/fetch" "0.2.3" "@atproto-labs/fetch-node" "0.1.9" @@ -322,8 +322,8 @@ "@atproto/jwk" "0.1.5" "@atproto/jwk-jose" "0.1.6" "@atproto/oauth-provider-api" "0.1.2" - "@atproto/oauth-provider-frontend" "0.1.4" - "@atproto/oauth-provider-ui" "0.1.5" + "@atproto/oauth-provider-frontend" "0.1.5" + "@atproto/oauth-provider-ui" "0.1.6" "@atproto/oauth-types" "0.2.7" "@atproto/syntax" "0.4.0" "@hapi/accept" "^6.0.3" @@ -346,12 +346,12 @@ "@atproto/jwk" "0.1.5" zod "^3.23.8" -"@atproto/ozone@^0.1.111": - version "0.1.111" - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.111.tgz#7ef4a02f1af045ab44254fb9d44ab0e50fd94ba9" - integrity sha512-NY+Cn/3dY4tPFkMUoJR1KMZN/v9ZIxjx6EQBMwn/nqTiHk0E3rtGEbyL2jLQ7x+FxpPTjDgpnn3K725+8XUaAg== +"@atproto/ozone@^0.1.112": + version "0.1.112" + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.112.tgz#6b6b5ac052dd4e6dfec3c88f83c9b53f4902fcbe" + integrity sha512-Euut64N/4UyRXyV6m1ATE9K6o6EpCf46ozD4GG8HJ9AC5zEgBYMSkH4l6SLrhKrYYIGXkvglk1WYuuDQKYb3LA== dependencies: - "@atproto/api" "^0.15.8" + "@atproto/api" "^0.15.9" "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" "@atproto/identity" "^0.4.8" @@ -376,20 +376,20 @@ undici "^6.14.1" ws "^8.12.0" -"@atproto/pds@^0.4.138": - version "0.4.138" - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.138.tgz#437d785c83f710bf37bef8baf687b0a46ce9dc68" - integrity sha512-WLzDhmguTgs2wQNKoGxCbpKNegDnRiemSslenMbPrB7kSiXYj+XZobLyoIXHv1EnAd2pbThwNEL8z8EfkM0mDg== +"@atproto/pds@^0.4.139": + version "0.4.139" + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.139.tgz#70ae5afd7d90eab214c652d57a5e6478af454fbe" + integrity sha512-VD1VTSAnbAme4D4Xk/Wdl05qs8YbCe39/i960EyXzw2fYNvL9jMpKm3z0lwhrYN9q7phFhr2ubU2QjfRFDbDAQ== dependencies: "@atproto-labs/fetch-node" "0.1.9" "@atproto-labs/xrpc-utils" "0.0.14" - "@atproto/api" "^0.15.8" + "@atproto/api" "^0.15.9" "@atproto/aws" "^0.2.21" "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" "@atproto/identity" "^0.4.8" "@atproto/lexicon" "^0.4.11" - "@atproto/oauth-provider" "^0.7.7" + "@atproto/oauth-provider" "^0.7.8" "@atproto/repo" "^0.8.1" "@atproto/syntax" "^0.4.0" "@atproto/xrpc" "^0.7.0" |