diff --git a/package.json b/package.json
index fdf6680457..5675681cae 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 baab8c8385..e3f85c0fe5 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 c5ec0473ce..97ada61485 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 7739da56be..f024928eef 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 2a61db7158..21eb53baf1 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 8880cb6b38..225b495d3f 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 b3a972394f..e05715f77d 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 6bbf9b250e..569fbbd0fd 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 7052590ca3..4700a7fdc9 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 0000000000..1fb4af2872
--- /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