about summary refs log tree commit diff
path: root/src/view/com/util/post-ctrls
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/util/post-ctrls')
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx283
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.tsx95
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.web.tsx86
3 files changed, 464 insertions, 0 deletions
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
new file mode 100644
index 000000000..5c0296e28
--- /dev/null
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -0,0 +1,283 @@
+import React, {useCallback} from 'react'
+import {
+  StyleProp,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+  ViewStyle,
+} from 'react-native'
+import {
+  FontAwesomeIcon,
+  FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import ReactNativeHapticFeedback, {
+  HapticFeedbackTypes,
+} from 'react-native-haptic-feedback'
+// DISABLED see #135
+// import {
+//   TriggerableAnimated,
+//   TriggerableAnimatedRef,
+// } from './anim/TriggerableAnimated'
+import {Text} from '../text/Text'
+import {PostDropdownBtn} from '../forms/DropdownButton'
+import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
+import {s, colors} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
+import {useStores} from 'state/index'
+import {isIOS, isNative} from 'platform/detection'
+import {RepostButton} from './RepostButton'
+
+interface PostCtrlsOpts {
+  itemUri: string
+  itemCid: string
+  itemHref: string
+  itemTitle: string
+  isAuthor: boolean
+  author: {
+    handle: string
+    displayName: string
+    avatar: string
+  }
+  text: string
+  indexedAt: string
+  big?: boolean
+  style?: StyleProp<ViewStyle>
+  replyCount?: number
+  repostCount?: number
+  likeCount?: number
+  isReposted: boolean
+  isLiked: boolean
+  isThreadMuted: boolean
+  onPressReply: () => void
+  onPressToggleRepost: () => Promise<void>
+  onPressToggleLike: () => Promise<void>
+  onCopyPostText: () => void
+  onOpenTranslate: () => void
+  onToggleThreadMute: () => void
+  onDeletePost: () => void
+}
+
+const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5}
+const hapticImpact: HapticFeedbackTypes = isIOS ? 'impactMedium' : 'impactLight' // Users said the medium impact was too strong on Android; see APP-537
+
+// DISABLED see #135
+/*
+function ctrlAnimStart(interp: Animated.Value) {
+  return Animated.sequence([
+    Animated.timing(interp, {
+      toValue: 1,
+      duration: 250,
+      useNativeDriver: true,
+    }),
+    Animated.delay(50),
+    Animated.timing(interp, {
+      toValue: 0,
+      duration: 20,
+      useNativeDriver: true,
+    }),
+  ])
+}
+
+function ctrlAnimStyle(interp: Animated.Value) {
+  return {
+    transform: [
+      {
+        scale: interp.interpolate({
+          inputRange: [0, 1.0],
+          outputRange: [1.0, 4.0],
+        }),
+      },
+    ],
+    opacity: interp.interpolate({
+      inputRange: [0, 1.0],
+      outputRange: [1.0, 0.0],
+    }),
+  }
+}
+*/
+
+export function PostCtrls(opts: PostCtrlsOpts) {
+  const store = useStores()
+  const theme = useTheme()
+  const defaultCtrlColor = React.useMemo(
+    () => ({
+      color: theme.palette.default.postCtrl,
+    }),
+    [theme],
+  ) as StyleProp<ViewStyle>
+  // DISABLED see #135
+  // const repostRef = React.useRef<TriggerableAnimatedRef | null>(null)
+  // const likeRef = React.useRef<TriggerableAnimatedRef | null>(null)
+  const onRepost = useCallback(() => {
+    store.shell.closeModal()
+    if (!opts.isReposted) {
+      if (isNative) {
+        ReactNativeHapticFeedback.trigger(hapticImpact)
+      }
+      opts.onPressToggleRepost().catch(_e => undefined)
+      // DISABLED see #135
+      // repostRef.current?.trigger(
+      //   {start: ctrlAnimStart, style: ctrlAnimStyle},
+      //   async () => {
+      //     await opts.onPressToggleRepost().catch(_e => undefined)
+      //     setRepostMod(0)
+      //   },
+      // )
+    } else {
+      opts.onPressToggleRepost().catch(_e => undefined)
+    }
+  }, [opts, store.shell])
+
+  const onQuote = useCallback(() => {
+    store.shell.closeModal()
+    store.shell.openComposer({
+      quote: {
+        uri: opts.itemUri,
+        cid: opts.itemCid,
+        text: opts.text,
+        author: opts.author,
+        indexedAt: opts.indexedAt,
+      },
+    })
+
+    if (isNative) {
+      ReactNativeHapticFeedback.trigger(hapticImpact)
+    }
+  }, [
+    opts.author,
+    opts.indexedAt,
+    opts.itemCid,
+    opts.itemUri,
+    opts.text,
+    store.shell,
+  ])
+
+  const onPressToggleLikeWrapper = async () => {
+    if (!opts.isLiked) {
+      ReactNativeHapticFeedback.trigger(hapticImpact)
+      await opts.onPressToggleLike().catch(_e => undefined)
+      // DISABLED see #135
+      // likeRef.current?.trigger(
+      //   {start: ctrlAnimStart, style: ctrlAnimStyle},
+      //   async () => {
+      //     await opts.onPressToggleLike().catch(_e => undefined)
+      //     setLikeMod(0)
+      //   },
+      // )
+      // setIsLikedPressed(false)
+    } else {
+      await opts.onPressToggleLike().catch(_e => undefined)
+      // setIsLikedPressed(false)
+    }
+  }
+
+  return (
+    <View style={[styles.ctrls, opts.style]}>
+      <TouchableOpacity
+        testID="replyBtn"
+        style={styles.ctrl}
+        hitSlop={HITSLOP}
+        onPress={opts.onPressReply}
+        accessibilityRole="button"
+        accessibilityLabel="Reply"
+        accessibilityHint="reply composer">
+        <CommentBottomArrow
+          style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]}
+          strokeWidth={3}
+          size={opts.big ? 20 : 15}
+        />
+        {typeof opts.replyCount !== 'undefined' ? (
+          <Text style={[defaultCtrlColor, s.ml5, s.f15]}>
+            {opts.replyCount}
+          </Text>
+        ) : undefined}
+      </TouchableOpacity>
+      <RepostButton {...opts} onRepost={onRepost} onQuote={onQuote} />
+      <TouchableOpacity
+        testID="likeBtn"
+        style={styles.ctrl}
+        hitSlop={HITSLOP}
+        onPress={onPressToggleLikeWrapper}
+        accessibilityRole="button"
+        accessibilityLabel={opts.isLiked ? 'Unlike' : 'Like'}
+        accessibilityHint={
+          opts.isReposted ? `Removes like from the post` : `Like the post`
+        }>
+        {opts.isLiked ? (
+          <HeartIconSolid
+            style={styles.ctrlIconLiked as StyleProp<ViewStyle>}
+            size={opts.big ? 22 : 16}
+          />
+        ) : (
+          <HeartIcon
+            style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]}
+            strokeWidth={3}
+            size={opts.big ? 20 : 16}
+          />
+        )}
+        {typeof opts.likeCount !== 'undefined' ? (
+          <Text
+            testID="likeCount"
+            style={
+              opts.isLiked
+                ? [s.bold, s.red3, s.f15, s.ml5]
+                : [defaultCtrlColor, s.f15, s.ml5]
+            }>
+            {opts.likeCount}
+          </Text>
+        ) : undefined}
+      </TouchableOpacity>
+      <View>
+        {opts.big ? undefined : (
+          <PostDropdownBtn
+            testID="postDropdownBtn"
+            style={styles.ctrl}
+            itemUri={opts.itemUri}
+            itemCid={opts.itemCid}
+            itemHref={opts.itemHref}
+            itemTitle={opts.itemTitle}
+            isAuthor={opts.isAuthor}
+            isThreadMuted={opts.isThreadMuted}
+            onCopyPostText={opts.onCopyPostText}
+            onOpenTranslate={opts.onOpenTranslate}
+            onToggleThreadMute={opts.onToggleThreadMute}
+            onDeletePost={opts.onDeletePost}>
+            <FontAwesomeIcon
+              icon="ellipsis-h"
+              size={18}
+              style={[
+                s.mt2,
+                s.mr5,
+                {
+                  color:
+                    theme.colorScheme === 'light' ? colors.gray4 : colors.gray5,
+                } as FontAwesomeIconStyle,
+              ]}
+            />
+          </PostDropdownBtn>
+        )}
+      </View>
+      {/* used for adding pad to the right side */}
+      <View />
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  ctrls: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+  },
+  ctrl: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    padding: 5,
+    margin: -5,
+  },
+  ctrlIconLiked: {
+    color: colors.red3,
+  },
+  mt1: {
+    marginTop: 1,
+  },
+})
diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx
new file mode 100644
index 000000000..e6de4cb19
--- /dev/null
+++ b/src/view/com/util/post-ctrls/RepostButton.tsx
@@ -0,0 +1,95 @@
+import React, {useCallback} from 'react'
+import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native'
+import {RepostIcon} from 'lib/icons'
+import {s, colors} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
+import {Text} from '../text/Text'
+import {useStores} from 'state/index'
+
+const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5}
+
+interface Props {
+  isReposted: boolean
+  repostCount?: number
+  big?: boolean
+  onRepost: () => void
+  onQuote: () => void
+}
+
+export const RepostButton = ({
+  isReposted,
+  repostCount,
+  big,
+  onRepost,
+  onQuote,
+}: Props) => {
+  const store = useStores()
+  const theme = useTheme()
+
+  const defaultControlColor = React.useMemo(
+    () => ({
+      color: theme.palette.default.postCtrl,
+    }),
+    [theme],
+  )
+
+  const onPressToggleRepostWrapper = useCallback(() => {
+    store.shell.openModal({
+      name: 'repost',
+      onRepost: onRepost,
+      onQuote: onQuote,
+      isReposted,
+    })
+  }, [onRepost, onQuote, isReposted, store.shell])
+
+  return (
+    <TouchableOpacity
+      testID="repostBtn"
+      hitSlop={HITSLOP}
+      onPress={onPressToggleRepostWrapper}
+      style={styles.control}
+      accessibilityRole="button"
+      accessibilityLabel={isReposted ? 'Undo repost' : 'Repost'}
+      accessibilityHint={
+        isReposted
+          ? `Remove your repost of the post`
+          : `Repost or quote post the post`
+      }>
+      <RepostIcon
+        style={
+          isReposted
+            ? (styles.reposted as StyleProp<ViewStyle>)
+            : defaultControlColor
+        }
+        strokeWidth={2.4}
+        size={big ? 24 : 20}
+      />
+      {typeof repostCount !== 'undefined' ? (
+        <Text
+          testID="repostCount"
+          style={
+            isReposted
+              ? [s.bold, s.green3, s.f15, s.ml5]
+              : [defaultControlColor, s.f15, s.ml5]
+          }>
+          {repostCount}
+        </Text>
+      ) : undefined}
+    </TouchableOpacity>
+  )
+}
+
+const styles = StyleSheet.create({
+  control: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    padding: 5,
+    margin: -5,
+  },
+  reposted: {
+    color: colors.green3,
+  },
+  repostCount: {
+    color: 'currentColor',
+  },
+})
diff --git a/src/view/com/util/post-ctrls/RepostButton.web.tsx b/src/view/com/util/post-ctrls/RepostButton.web.tsx
new file mode 100644
index 000000000..66cc0d123
--- /dev/null
+++ b/src/view/com/util/post-ctrls/RepostButton.web.tsx
@@ -0,0 +1,86 @@
+import React, {useMemo} from 'react'
+import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {RepostIcon} from 'lib/icons'
+import {DropdownButton} from '../forms/DropdownButton'
+import {colors} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
+import {Text} from '../text/Text'
+
+interface Props {
+  isReposted: boolean
+  repostCount?: number
+  big?: boolean
+  onRepost: () => void
+  onQuote: () => void
+}
+
+export const RepostButton = ({
+  isReposted,
+  repostCount,
+  big,
+  onRepost,
+  onQuote,
+}: Props) => {
+  const theme = useTheme()
+
+  const defaultControlColor = React.useMemo(
+    () => ({
+      color: theme.palette.default.postCtrl,
+    }),
+    [theme],
+  )
+
+  const items = useMemo(
+    () => [
+      {
+        label: isReposted ? 'Undo repost' : 'Repost',
+        icon: 'retweet' as const,
+        onPress: onRepost,
+      },
+      {label: 'Quote post', icon: 'quote-left' as const, onPress: onQuote},
+    ],
+    [isReposted, onRepost, onQuote],
+  )
+
+  return (
+    <DropdownButton
+      type="bare"
+      items={items}
+      bottomOffset={4}
+      openToRight
+      rightOffset={-40}>
+      <View
+        style={[
+          styles.control,
+          (isReposted
+            ? styles.reposted
+            : defaultControlColor) as StyleProp<ViewStyle>,
+        ]}>
+        <RepostIcon strokeWidth={2.4} size={big ? 24 : 20} />
+        {typeof repostCount !== 'undefined' ? (
+          <Text
+            testID="repostCount"
+            type={isReposted ? 'md-bold' : 'md-medium'}
+            style={styles.repostCount}>
+            {repostCount ?? 0}
+          </Text>
+        ) : undefined}
+      </View>
+    </DropdownButton>
+  )
+}
+
+const styles = StyleSheet.create({
+  control: {
+    display: 'flex',
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 4,
+  },
+  reposted: {
+    color: colors.green3,
+  },
+  repostCount: {
+    color: 'currentColor',
+  },
+})