about summary refs log tree commit diff
path: root/src/state
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-05-28 22:09:28 +0300
committerGitHub <noreply@github.com>2025-05-28 22:09:28 +0300
commitcf63c2ca07c9a77bb92449ea4f3d78b8dd54fb8f (patch)
tree6136c729a77ef8daf3cbece566f4221c1b6a8d47 /src/state
parent665a0430a3c04a3ad689954c5f930b4434daef79 (diff)
downloadvoidsky-cf63c2ca07c9a77bb92449ea4f3d78b8dd54fb8f.tar.zst
Send FeedFeedback interactions in thread view (#8414)
Diffstat (limited to 'src/state')
-rw-r--r--src/state/feed-feedback.tsx51
-rw-r--r--src/state/queries/notifications/types.ts2
-rw-r--r--src/state/queries/notifications/util.ts11
-rw-r--r--src/state/queries/post.ts22
-rw-r--r--src/state/unstable-post-source.tsx73
5 files changed, 129 insertions, 30 deletions
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
+}