about summary refs log tree commit diff
path: root/src/view
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/view
parent665a0430a3c04a3ad689954c5f930b4434daef79 (diff)
downloadvoidsky-cf63c2ca07c9a77bb92449ea4f3d78b8dd54fb8f.tar.zst
Send FeedFeedback interactions in thread view (#8414)
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/notifications/NotificationFeed.tsx4
-rw-r--r--src/view/com/notifications/NotificationFeedItem.tsx54
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx107
-rw-r--r--src/view/com/posts/PostFeedItem.tsx27
4 files changed, 158 insertions, 34 deletions
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>