about summary refs log tree commit diff
path: root/src/components/PostControls/index.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/PostControls/index.tsx')
-rw-r--r--src/components/PostControls/index.tsx292
1 files changed, 292 insertions, 0 deletions
diff --git a/src/components/PostControls/index.tsx b/src/components/PostControls/index.tsx
new file mode 100644
index 000000000..7739da56b
--- /dev/null
+++ b/src/components/PostControls/index.tsx
@@ -0,0 +1,292 @@
+import {memo, useState} from 'react'
+import {type StyleProp, View, type ViewStyle} from 'react-native'
+import {
+  type AppBskyFeedDefs,
+  type AppBskyFeedPost,
+  type AppBskyFeedThreadgate,
+  type RichText as RichTextAPI,
+} from '@atproto/api'
+import {msg, plural} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {CountWheel} from '#/lib/custom-animations/CountWheel'
+import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
+import {useHaptics} from '#/lib/haptics'
+import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
+import {type Shadow} from '#/state/cache/types'
+import {useFeedFeedbackContext} from '#/state/feed-feedback'
+import {
+  usePostLikeMutationQueue,
+  usePostRepostMutationQueue,
+} from '#/state/queries/post'
+import {useRequireAuth} from '#/state/session'
+import {
+  ProgressGuideAction,
+  useProgressGuideControls,
+} from '#/state/shell/progress-guide'
+import {formatCount} from '#/view/com/util/numeric/format'
+import * as Toast from '#/view/com/util/Toast'
+import {atoms as a, useBreakpoints} from '#/alf'
+import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
+import {
+  PostControlButton,
+  PostControlButtonIcon,
+  PostControlButtonText,
+} from './PostControlButton'
+import {PostMenuButton} from './PostMenu'
+import {RepostButton} from './RepostButton'
+import {ShareMenuButton} from './ShareMenu'
+
+let PostControls = ({
+  big,
+  post,
+  record,
+  richText,
+  feedContext,
+  reqId,
+  style,
+  onPressReply,
+  onPostReply,
+  logContext,
+  threadgateRecord,
+  onShowLess,
+}: {
+  big?: boolean
+  post: Shadow<AppBskyFeedDefs.PostView>
+  record: AppBskyFeedPost.Record
+  richText: RichTextAPI
+  feedContext?: string | undefined
+  reqId?: string | undefined
+  style?: StyleProp<ViewStyle>
+  onPressReply: () => void
+  onPostReply?: (postUri: string | undefined) => void
+  logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
+  threadgateRecord?: AppBskyFeedThreadgate.Record
+  onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
+}): React.ReactNode => {
+  const {_, i18n} = useLingui()
+  const {gtMobile} = useBreakpoints()
+  const {openComposer} = useOpenComposer()
+  const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext)
+  const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(
+    post,
+    logContext,
+  )
+  const requireAuth = useRequireAuth()
+  const {sendInteraction} = useFeedFeedbackContext()
+  const {captureAction} = useProgressGuideControls()
+  const playHaptic = useHaptics()
+  const isBlocked = Boolean(
+    post.author.viewer?.blocking ||
+      post.author.viewer?.blockedBy ||
+      post.author.viewer?.blockingByList,
+  )
+  const replyDisabled = post.viewer?.replyDisabled
+
+  const [hasLikeIconBeenToggled, setHasLikeIconBeenToggled] = useState(false)
+
+  const onPressToggleLike = async () => {
+    if (isBlocked) {
+      Toast.show(
+        _(msg`Cannot interact with a blocked user`),
+        'exclamation-circle',
+      )
+      return
+    }
+
+    try {
+      setHasLikeIconBeenToggled(true)
+      if (!post.viewer?.like) {
+        playHaptic('Light')
+        sendInteraction({
+          item: post.uri,
+          event: 'app.bsky.feed.defs#interactionLike',
+          feedContext,
+          reqId,
+        })
+        captureAction(ProgressGuideAction.Like)
+        await queueLike()
+      } else {
+        await queueUnlike()
+      }
+    } catch (e: any) {
+      if (e?.name !== 'AbortError') {
+        throw e
+      }
+    }
+  }
+
+  const onRepost = async () => {
+    if (isBlocked) {
+      Toast.show(
+        _(msg`Cannot interact with a blocked user`),
+        'exclamation-circle',
+      )
+      return
+    }
+
+    try {
+      if (!post.viewer?.repost) {
+        sendInteraction({
+          item: post.uri,
+          event: 'app.bsky.feed.defs#interactionRepost',
+          feedContext,
+          reqId,
+        })
+        await queueRepost()
+      } else {
+        await queueUnrepost()
+      }
+    } catch (e: any) {
+      if (e?.name !== 'AbortError') {
+        throw e
+      }
+    }
+  }
+
+  const onQuote = () => {
+    if (isBlocked) {
+      Toast.show(
+        _(msg`Cannot interact with a blocked user`),
+        'exclamation-circle',
+      )
+      return
+    }
+
+    sendInteraction({
+      item: post.uri,
+      event: 'app.bsky.feed.defs#interactionQuote',
+      feedContext,
+      reqId,
+    })
+    openComposer({
+      quote: post,
+      onPost: onPostReply,
+    })
+  }
+
+  const onShare = () => {
+    sendInteraction({
+      item: post.uri,
+      event: 'app.bsky.feed.defs#interactionShare',
+      feedContext,
+      reqId,
+    })
+  }
+
+  return (
+    <View style={[a.flex_row, a.justify_between, a.align_center, style]}>
+      <View
+        style={[
+          big ? a.align_center : [a.flex_1, a.align_start, {marginLeft: -6}],
+          replyDisabled ? {opacity: 0.5} : undefined,
+        ]}>
+        <PostControlButton
+          testID="replyBtn"
+          onPress={
+            !replyDisabled ? () => requireAuth(() => onPressReply()) : undefined
+          }
+          label={_(
+            msg({
+              message: `Reply (${plural(post.replyCount || 0, {
+                one: '# reply',
+                other: '# replies',
+              })})`,
+              comment:
+                'Accessibility label for the reply button, verb form followed by number of replies and noun form',
+            }),
+          )}
+          big={big}>
+          <PostControlButtonIcon icon={Bubble} />
+          {typeof post.replyCount !== 'undefined' && post.replyCount > 0 && (
+            <PostControlButtonText>
+              {formatCount(i18n, post.replyCount)}
+            </PostControlButtonText>
+          )}
+        </PostControlButton>
+      </View>
+      <View style={big ? a.align_center : [a.flex_1, a.align_start]}>
+        <RepostButton
+          isReposted={!!post.viewer?.repost}
+          repostCount={(post.repostCount ?? 0) + (post.quoteCount ?? 0)}
+          onRepost={onRepost}
+          onQuote={onQuote}
+          big={big}
+          embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)}
+        />
+      </View>
+      <View style={big ? a.align_center : [a.flex_1, a.align_start]}>
+        <PostControlButton
+          testID="likeBtn"
+          big={big}
+          onPress={() => requireAuth(() => onPressToggleLike())}
+          label={
+            post.viewer?.like
+              ? _(
+                  msg({
+                    message: `Unlike (${plural(post.likeCount || 0, {
+                      one: '# like',
+                      other: '# likes',
+                    })})`,
+                    comment:
+                      'Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun',
+                  }),
+                )
+              : _(
+                  msg({
+                    message: `Like (${plural(post.likeCount || 0, {
+                      one: '# like',
+                      other: '# likes',
+                    })})`,
+                    comment:
+                      'Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form',
+                  }),
+                )
+          }>
+          <AnimatedLikeIcon
+            isLiked={Boolean(post.viewer?.like)}
+            big={big}
+            hasBeenToggled={hasLikeIconBeenToggled}
+          />
+          <CountWheel
+            likeCount={post.likeCount ?? 0}
+            big={big}
+            isLiked={Boolean(post.viewer?.like)}
+            hasBeenToggled={hasLikeIconBeenToggled}
+          />
+        </PostControlButton>
+      </View>
+      <View style={big ? a.align_center : [a.flex_1, a.align_start]}>
+        <View style={[!big && a.ml_sm]}>
+          <ShareMenuButton
+            testID="postShareBtn"
+            post={post}
+            big={big}
+            record={record}
+            richText={richText}
+            timestamp={post.indexedAt}
+            threadgateRecord={threadgateRecord}
+            onShare={onShare}
+          />
+        </View>
+      </View>
+      <View
+        style={big ? a.align_center : [gtMobile && a.flex_1, a.align_start]}>
+        <PostMenuButton
+          testID="postDropdownBtn"
+          post={post}
+          postFeedContext={feedContext}
+          postReqId={reqId}
+          big={big}
+          record={record}
+          richText={richText}
+          timestamp={post.indexedAt}
+          threadgateRecord={threadgateRecord}
+          onShowLess={onShowLess}
+        />
+      </View>
+    </View>
+  )
+}
+PostControls = memo(PostControls)
+export {PostControls}