about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/composer/Composer.tsx18
-rw-r--r--src/view/com/post-thread/PostLikedBy.tsx4
-rw-r--r--src/view/com/post-thread/PostQuotes.tsx141
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx4
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx30
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx12
-rw-r--r--src/view/screens/PostLikedBy.tsx32
-rw-r--r--src/view/screens/PostRepostedBy.tsx32
-rw-r--r--src/view/shell/Composer.ios.tsx1
-rw-r--r--src/view/shell/Composer.tsx1
-rw-r--r--src/view/shell/Composer.web.tsx1
11 files changed, 203 insertions, 73 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index dba37d82b..0efbe70e6 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -116,6 +116,7 @@ export const ComposePost = observer(function ComposePost({
   replyTo,
   onPost,
   quote: initQuote,
+  quoteCount,
   mention: initMention,
   openPicker,
   text: initText,
@@ -392,7 +393,22 @@ export const ComposePost = observer(function ComposePost({
       emitPostCreated()
     }
     setLangPrefs.savePostLanguageToHistory()
-    onPost?.(postUri)
+    if (quote) {
+      // We want to wait for the quote count to update before we call `onPost`, which will refetch data
+      whenAppViewReady(agent, quote.uri, res => {
+        const thread = res.data.thread
+        if (
+          AppBskyFeedDefs.isThreadViewPost(thread) &&
+          thread.post.quoteCount !== quoteCount
+        ) {
+          onPost?.(postUri)
+          return true
+        }
+        return false
+      })
+    } else {
+      onPost?.(postUri)
+    }
     onClose()
     Toast.show(
       replyTo
diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx
index da230aade..c3e3f9e17 100644
--- a/src/view/com/post-thread/PostLikedBy.tsx
+++ b/src/view/com/post-thread/PostLikedBy.tsx
@@ -8,13 +8,13 @@ import {logger} from '#/logger'
 import {useLikedByQuery} from '#/state/queries/post-liked-by'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
 import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
+import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
+import {List} from '#/view/com/util/List'
 import {
   ListFooter,
   ListHeaderDesktop,
   ListMaybePlaceholder,
 } from '#/components/Lists'
-import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
-import {List} from '../util/List'
 
 function renderItem({item}: {item: GetLikes.Like}) {
   return <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
diff --git a/src/view/com/post-thread/PostQuotes.tsx b/src/view/com/post-thread/PostQuotes.tsx
new file mode 100644
index 000000000..d573d27a1
--- /dev/null
+++ b/src/view/com/post-thread/PostQuotes.tsx
@@ -0,0 +1,141 @@
+import React, {useCallback, useState} from 'react'
+import {
+  AppBskyFeedDefs,
+  AppBskyFeedPost,
+  ModerationDecision,
+} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
+import {cleanError} from '#/lib/strings/errors'
+import {logger} from '#/logger'
+import {isWeb} from '#/platform/detection'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {usePostQuotesQuery} from '#/state/queries/post-quotes'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
+import {Post} from 'view/com/post/Post'
+import {
+  ListFooter,
+  ListHeaderDesktop,
+  ListMaybePlaceholder,
+} from '#/components/Lists'
+import {List} from '../util/List'
+
+function renderItem({
+  item,
+  index,
+}: {
+  item: {
+    post: AppBskyFeedDefs.PostView
+    moderation: ModerationDecision
+    record: AppBskyFeedPost.Record
+  }
+  index: number
+}) {
+  return <Post post={item.post} hideTopBorder={index === 0 && !isWeb} />
+}
+
+function keyExtractor(item: {
+  post: AppBskyFeedDefs.PostView
+  moderation: ModerationDecision
+  record: AppBskyFeedPost.Record
+}) {
+  return item.post.uri
+}
+
+export function PostQuotes({uri}: {uri: string}) {
+  const {_} = useLingui()
+  const initialNumToRender = useInitialNumToRender()
+
+  const [isPTRing, setIsPTRing] = useState(false)
+
+  const {
+    data: resolvedUri,
+    error: resolveError,
+    isLoading: isLoadingUri,
+  } = useResolveUriQuery(uri)
+  const {
+    data,
+    isLoading: isLoadingQuotes,
+    isFetchingNextPage,
+    hasNextPage,
+    fetchNextPage,
+    error,
+    refetch,
+  } = usePostQuotesQuery(resolvedUri?.uri)
+
+  const moderationOpts = useModerationOpts()
+
+  const isError = Boolean(resolveError || error)
+
+  const quotes =
+    data?.pages
+      .flatMap(page =>
+        page.posts.map(post => {
+          if (!AppBskyFeedPost.isRecord(post.record) || !moderationOpts) {
+            return null
+          }
+          const moderation = moderatePost(post, moderationOpts)
+          return {post, record: post.record, moderation}
+        }),
+      )
+      .filter(item => item !== null) ?? []
+
+  const onRefresh = useCallback(async () => {
+    setIsPTRing(true)
+    try {
+      await refetch()
+    } catch (err) {
+      logger.error('Failed to refresh quotes', {message: err})
+    }
+    setIsPTRing(false)
+  }, [refetch, setIsPTRing])
+
+  const onEndReached = useCallback(async () => {
+    if (isFetchingNextPage || !hasNextPage || isError) return
+    try {
+      await fetchNextPage()
+    } catch (err) {
+      logger.error('Failed to load more quotes', {message: err})
+    }
+  }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
+
+  if (isLoadingUri || isLoadingQuotes || isError) {
+    return (
+      <ListMaybePlaceholder
+        isLoading={isLoadingUri || isLoadingQuotes}
+        isError={isError}
+      />
+    )
+  }
+
+  // loaded
+  // =
+  return (
+    <List
+      data={quotes}
+      renderItem={renderItem}
+      keyExtractor={keyExtractor}
+      refreshing={isPTRing}
+      onRefresh={onRefresh}
+      onEndReached={onEndReached}
+      onEndReachedThreshold={4}
+      ListHeaderComponent={<ListHeaderDesktop title={_(msg`Quotes`)} />}
+      ListFooterComponent={
+        <ListFooter
+          isFetchingNextPage={isFetchingNextPage}
+          error={cleanError(error)}
+          onRetry={fetchNextPage}
+          showEndMessage
+          endMessageText={_(msg`That's all, folks!`)}
+        />
+      }
+      // @ts-ignore our .web version only -prf
+      desktopFixedHeight
+      initialNumToRender={initialNumToRender}
+      windowSize={11}
+    />
+  )
+}
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index 9038549a5..0d1e86aec 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -8,13 +8,13 @@ import {logger} from '#/logger'
 import {usePostRepostedByQuery} from '#/state/queries/post-reposted-by'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
 import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
+import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
+import {List} from '#/view/com/util/List'
 import {
   ListFooter,
   ListHeaderDesktop,
   ListMaybePlaceholder,
 } from '#/components/Lists'
-import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
-import {List} from '../util/List'
 
 function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) {
   return <ProfileCardWithFollowBtn key={item.did} profile={item} />
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 0ff360143..26a5f2f03 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -199,6 +199,11 @@ let PostThreadItemLoaded = ({
     return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
   }, [post.uri, post.author])
   const repostsTitle = _(msg`Reposts of this post`)
+  const quotesHref = React.useMemo(() => {
+    const urip = new AtUri(post.uri)
+    return makeProfileLink(post.author, 'post', urip.rkey, 'quotes')
+  }, [post.uri, post.author])
+  const quotesTitle = _(msg`Quotes of this post`)
 
   const translatorUrl = getTranslatorLink(
     record?.text || '',
@@ -343,7 +348,9 @@ let PostThreadItemLoaded = ({
               translatorUrl={translatorUrl}
               needsTranslation={needsTranslation}
             />
-            {post.repostCount !== 0 || post.likeCount !== 0 ? (
+            {post.repostCount !== 0 ||
+            post.likeCount !== 0 ||
+            post.quoteCount !== 0 ? (
               // Show this section unless we're *sure* it has no engagement.
               <View style={[styles.expandedInfo, pal.border]}>
                 {post.repostCount != null && post.repostCount !== 0 ? (
@@ -382,6 +389,26 @@ let PostThreadItemLoaded = ({
                     </Text>
                   </Link>
                 ) : null}
+                {post.quoteCount != null && post.quoteCount !== 0 ? (
+                  <Link
+                    style={styles.expandedInfoItem}
+                    href={quotesHref}
+                    title={quotesTitle}>
+                    <Text
+                      testID="quoteCount-expanded"
+                      type="lg"
+                      style={pal.textLight}>
+                      <Text type="xl-bold" style={pal.text}>
+                        {formatCount(post.quoteCount)}
+                      </Text>{' '}
+                      <Plural
+                        value={post.quoteCount}
+                        one="quote"
+                        other="quotes"
+                      />
+                    </Text>
+                  </Link>
+                ) : null}
               </View>
             ) : null}
             <View style={[s.pl10, s.pr10]}>
@@ -391,6 +418,7 @@ let PostThreadItemLoaded = ({
                 record={record}
                 richText={richText}
                 onPressReply={onPressReply}
+                onPostReply={onPostReply}
                 logContext="PostThreadItem"
               />
             </View>
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index 478b8f0f8..ad5863846 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -58,6 +58,7 @@ let PostCtrls = ({
   feedContext,
   style,
   onPressReply,
+  onPostReply,
   logContext,
 }: {
   big?: boolean
@@ -67,6 +68,7 @@ let PostCtrls = ({
   feedContext?: string | undefined
   style?: StyleProp<ViewStyle>
   onPressReply: () => void
+  onPostReply?: (postUri: string | undefined) => void
   logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
 }): React.ReactNode => {
   const t = useTheme()
@@ -169,16 +171,20 @@ let PostCtrls = ({
         author: post.author,
         indexedAt: post.indexedAt,
       },
+      quoteCount: post.quoteCount,
+      onPost: onPostReply,
     })
   }, [
-    openComposer,
+    sendInteraction,
     post.uri,
     post.cid,
     post.author,
     post.indexedAt,
-    record.text,
-    sendInteraction,
+    post.quoteCount,
     feedContext,
+    openComposer,
+    record.text,
+    onPostReply,
   ])
 
   const onShare = useCallback(() => {
diff --git a/src/view/screens/PostLikedBy.tsx b/src/view/screens/PostLikedBy.tsx
deleted file mode 100644
index 5ff5a1932..000000000
--- a/src/view/screens/PostLikedBy.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useFocusEffect} from '@react-navigation/native'
-
-import {useSetMinimalShellMode} from '#/state/shell'
-import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
-import {makeRecordUri} from 'lib/strings/url-helpers'
-import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy'
-import {ViewHeader} from '../com/util/ViewHeader'
-
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
-export const PostLikedByScreen = ({route}: Props) => {
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {name, rkey} = route.params
-  const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
-  const {_} = useLingui()
-
-  useFocusEffect(
-    React.useCallback(() => {
-      setMinimalShellMode(false)
-    }, [setMinimalShellMode]),
-  )
-
-  return (
-    <View style={{flex: 1}}>
-      <ViewHeader title={_(msg`Liked By`)} />
-      <PostLikedByComponent uri={uri} />
-    </View>
-  )
-}
diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx
deleted file mode 100644
index eaacc6780..000000000
--- a/src/view/screens/PostRepostedBy.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useFocusEffect} from '@react-navigation/native'
-
-import {useSetMinimalShellMode} from '#/state/shell'
-import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
-import {makeRecordUri} from 'lib/strings/url-helpers'
-import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy'
-import {ViewHeader} from '../com/util/ViewHeader'
-
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
-export const PostRepostedByScreen = ({route}: Props) => {
-  const {name, rkey} = route.params
-  const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {_} = useLingui()
-
-  useFocusEffect(
-    React.useCallback(() => {
-      setMinimalShellMode(false)
-    }, [setMinimalShellMode]),
-  )
-
-  return (
-    <View style={{flex: 1}}>
-      <ViewHeader title={_(msg`Reposted By`)} />
-      <PostRepostedByComponent uri={uri} />
-    </View>
-  )
-}
diff --git a/src/view/shell/Composer.ios.tsx b/src/view/shell/Composer.ios.tsx
index a732e0cde..7d3780801 100644
--- a/src/view/shell/Composer.ios.tsx
+++ b/src/view/shell/Composer.ios.tsx
@@ -33,6 +33,7 @@ export const Composer = observer(function ComposerImpl({}: {
             replyTo={state?.replyTo}
             onPost={state?.onPost}
             quote={state?.quote}
+            quoteCount={state?.quoteCount}
             mention={state?.mention}
             text={state?.text}
             imageUris={state?.imageUris}
diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx
index b978d6b85..1c97df9c3 100644
--- a/src/view/shell/Composer.tsx
+++ b/src/view/shell/Composer.tsx
@@ -55,6 +55,7 @@ export const Composer = observer(function ComposerImpl({
         replyTo={state.replyTo}
         onPost={state.onPost}
         quote={state.quote}
+        quoteCount={state.quoteCount}
         mention={state.mention}
         text={state.text}
         imageUris={state.imageUris}
diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx
index 64353db23..5d80dc422 100644
--- a/src/view/shell/Composer.web.tsx
+++ b/src/view/shell/Composer.web.tsx
@@ -58,6 +58,7 @@ export function Composer({}: {winHeight: number}) {
         <ComposePost
           replyTo={state.replyTo}
           quote={state.quote}
+          quoteCount={state?.quoteCount}
           onPost={state.onPost}
           mention={state.mention}
           openPicker={onOpenPicker}