about summary refs log tree commit diff
path: root/src/screens
diff options
context:
space:
mode:
Diffstat (limited to 'src/screens')
-rw-r--r--src/screens/PostThread/components/ThreadComposePrompt.tsx95
-rw-r--r--src/screens/PostThread/components/ThreadItemAnchor.tsx4
-rw-r--r--src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx139
-rw-r--r--src/screens/PostThread/index.tsx6
-rw-r--r--src/screens/Settings/ThreadPreferences.tsx158
-rw-r--r--src/screens/VideoFeed/index.tsx4
6 files changed, 241 insertions, 165 deletions
diff --git a/src/screens/PostThread/components/ThreadComposePrompt.tsx b/src/screens/PostThread/components/ThreadComposePrompt.tsx
new file mode 100644
index 000000000..e12c7e766
--- /dev/null
+++ b/src/screens/PostThread/components/ThreadComposePrompt.tsx
@@ -0,0 +1,95 @@
+import {type StyleProp, View, type ViewStyle} from 'react-native'
+import {LinearGradient} from 'expo-linear-gradient'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {PressableScale} from '#/lib/custom-animations/PressableScale'
+import {useHaptics} from '#/lib/haptics'
+import {useHideBottomBarBorderForScreen} from '#/lib/hooks/useHideBottomBarBorder'
+import {useProfileQuery} from '#/state/queries/profile'
+import {useSession} from '#/state/session'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {atoms as a, ios, native, useBreakpoints, useTheme} from '#/alf'
+import {transparentifyColor} from '#/alf/util/colorGeneration'
+import {useInteractionState} from '#/components/hooks/useInteractionState'
+import {Text} from '#/components/Typography'
+
+export function ThreadComposePrompt({
+  onPressCompose,
+  style,
+}: {
+  onPressCompose: () => void
+  style?: StyleProp<ViewStyle>
+}) {
+  const {currentAccount} = useSession()
+  const {data: profile} = useProfileQuery({did: currentAccount?.did})
+  const {_} = useLingui()
+  const {gtMobile} = useBreakpoints()
+  const t = useTheme()
+  const playHaptic = useHaptics()
+  const {
+    state: hovered,
+    onIn: onHoverIn,
+    onOut: onHoverOut,
+  } = useInteractionState()
+
+  useHideBottomBarBorderForScreen()
+
+  return (
+    <View
+      style={[
+        a.px_sm,
+        gtMobile
+          ? [a.py_xs, a.border_t, t.atoms.border_contrast_low, t.atoms.bg]
+          : [a.pb_2xs],
+        style,
+      ]}>
+      {!gtMobile && (
+        <LinearGradient
+          key={t.name} // android does not update when you change the colors. sigh.
+          start={[0.5, 0]}
+          end={[0.5, 1]}
+          colors={[
+            transparentifyColor(t.atoms.bg.backgroundColor, 0),
+            t.atoms.bg.backgroundColor,
+          ]}
+          locations={[0.15, 0.4]}
+          style={[a.absolute, a.inset_0]}
+        />
+      )}
+      <PressableScale
+        accessibilityRole="button"
+        accessibilityLabel={_(msg`Compose reply`)}
+        accessibilityHint={_(msg`Opens composer`)}
+        onPress={() => {
+          onPressCompose()
+          playHaptic('Light')
+        }}
+        onLongPress={ios(() => {
+          onPressCompose()
+          playHaptic('Heavy')
+        })}
+        onHoverIn={onHoverIn}
+        onHoverOut={onHoverOut}
+        style={[
+          a.flex_row,
+          a.align_center,
+          a.p_sm,
+          a.gap_sm,
+          a.rounded_full,
+          (!gtMobile || hovered) && t.atoms.bg_contrast_25,
+          native([a.border, t.atoms.border_contrast_low]),
+          a.transition_color,
+        ]}>
+        <UserAvatar
+          size={24}
+          avatar={profile?.avatar}
+          type={profile?.associated?.labeler ? 'labeler' : 'user'}
+        />
+        <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
+          <Trans>Write your reply</Trans>
+        </Text>
+      </PressableScale>
+    </View>
+  )
+}
diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx
index fc1f1caeb..550bddc6a 100644
--- a/src/screens/PostThread/components/ThreadItemAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx
@@ -32,9 +32,9 @@ import {useSession} from '#/state/session'
 import {type OnPostSuccessData} from '#/state/shell/composer'
 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
 import {type PostSource} from '#/state/unstable-post-source'
-import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
 import {formatCount} from '#/view/com/util/numeric/format'
 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
+import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
 import {
   LINEAR_AVI_WIDTH,
   OUTER_SPACE,
@@ -367,7 +367,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
           </Link>
           {showFollowButton && (
             <View collapsable={false}>
-              <PostThreadFollowBtn did={post.author.did} />
+              <ThreadItemAnchorFollowButton did={post.author.did} />
             </View>
           )}
         </View>
diff --git a/src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx b/src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx
new file mode 100644
index 000000000..d4cf120cf
--- /dev/null
+++ b/src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx
@@ -0,0 +1,139 @@
+import React from 'react'
+import {type AppBskyActorDefs} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
+
+import {logger} from '#/logger'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
+import {
+  useProfileFollowMutationQueue,
+  useProfileQuery,
+} from '#/state/queries/profile'
+import {useRequireAuth} from '#/state/session'
+import * as Toast from '#/view/com/util/Toast'
+import {atoms as a, useBreakpoints} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
+import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+
+export function ThreadItemAnchorFollowButton({did}: {did: string}) {
+  const {data: profile, isLoading} = useProfileQuery({did})
+
+  // We will never hit this - the profile will always be cached or loaded above
+  // but it keeps the typechecker happy
+  if (isLoading || !profile) return null
+
+  return <PostThreadFollowBtnLoaded profile={profile} />
+}
+
+function PostThreadFollowBtnLoaded({
+  profile: profileUnshadowed,
+}: {
+  profile: AppBskyActorDefs.ProfileViewDetailed
+}) {
+  const navigation = useNavigation()
+  const {_} = useLingui()
+  const {gtMobile} = useBreakpoints()
+  const profile = useProfileShadow(profileUnshadowed)
+  const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
+    profile,
+    'PostThreadItem',
+  )
+  const requireAuth = useRequireAuth()
+
+  const isFollowing = !!profile.viewer?.following
+  const isFollowedBy = !!profile.viewer?.followedBy
+  const [wasFollowing, setWasFollowing] = React.useState<boolean>(isFollowing)
+
+  // This prevents the button from disappearing as soon as we follow.
+  const showFollowBtn = React.useMemo(
+    () => !isFollowing || !wasFollowing,
+    [isFollowing, wasFollowing],
+  )
+
+  /**
+   * We want this button to stay visible even after following, so that the user can unfollow if they want.
+   * However, we need it to disappear after we push to a screen and then come back. We also need it to
+   * show up if we view the post while following, go to the profile and unfollow, then come back to the
+   * post.
+   *
+   * We want to update wasFollowing both on blur and on focus so that we hit all these cases. On native,
+   * we could do this only on focus because the transition animation gives us time to not notice the
+   * sudden rendering of the button. However, on web if we do this, there's an obvious flicker once the
+   * button renders. So, we update the state in both cases.
+   */
+  React.useEffect(() => {
+    const updateWasFollowing = () => {
+      if (wasFollowing !== isFollowing) {
+        setWasFollowing(isFollowing)
+      }
+    }
+
+    const unsubscribeFocus = navigation.addListener('focus', updateWasFollowing)
+    const unsubscribeBlur = navigation.addListener('blur', updateWasFollowing)
+
+    return () => {
+      unsubscribeFocus()
+      unsubscribeBlur()
+    }
+  }, [isFollowing, wasFollowing, navigation])
+
+  const onPress = React.useCallback(() => {
+    if (!isFollowing) {
+      requireAuth(async () => {
+        try {
+          await queueFollow()
+        } catch (e: any) {
+          if (e?.name !== 'AbortError') {
+            logger.error('Failed to follow', {message: String(e)})
+            Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
+          }
+        }
+      })
+    } else {
+      requireAuth(async () => {
+        try {
+          await queueUnfollow()
+        } catch (e: any) {
+          if (e?.name !== 'AbortError') {
+            logger.error('Failed to unfollow', {message: String(e)})
+            Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
+          }
+        }
+      })
+    }
+  }, [isFollowing, requireAuth, queueFollow, _, queueUnfollow])
+
+  if (!showFollowBtn) return null
+
+  return (
+    <Button
+      testID="followBtn"
+      label={_(msg`Follow ${profile.handle}`)}
+      onPress={onPress}
+      size="small"
+      variant="solid"
+      color={isFollowing ? 'secondary' : 'secondary_inverted'}
+      style={[a.rounded_full]}>
+      {gtMobile && (
+        <ButtonIcon
+          icon={isFollowing ? Check : Plus}
+          position="left"
+          size="sm"
+        />
+      )}
+      <ButtonText>
+        {!isFollowing ? (
+          isFollowedBy ? (
+            <Trans>Follow back</Trans>
+          ) : (
+            <Trans>Follow</Trans>
+          )
+        ) : (
+          <Trans>Following</Trans>
+        )}
+      </ButtonText>
+    </Button>
+  )
+}
diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx
index f91daf54b..7432f71db 100644
--- a/src/screens/PostThread/index.tsx
+++ b/src/screens/PostThread/index.tsx
@@ -12,9 +12,9 @@ import {useSession} from '#/state/session'
 import {type OnPostSuccessData} from '#/state/shell/composer'
 import {useShellLayout} from '#/state/shell/shell-layout'
 import {useUnstablePostSource} from '#/state/unstable-post-source'
-import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
 import {List, type ListMethods} from '#/view/com/util/List'
 import {HeaderDropdown} from '#/screens/PostThread/components/HeaderDropdown'
+import {ThreadComposePrompt} from '#/screens/PostThread/components/ThreadComposePrompt'
 import {ThreadError} from '#/screens/PostThread/components/ThreadError'
 import {
   ThreadItemAnchor,
@@ -455,7 +455,7 @@ export function PostThread({uri}: {uri: string}) {
         return (
           <View>
             {gtMobile && (
-              <PostThreadComposePrompt onPressCompose={onReplyToAnchor} />
+              <ThreadComposePrompt onPressCompose={onReplyToAnchor} />
             )}
           </View>
         )
@@ -586,7 +586,7 @@ function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) {
 
   return (
     <Animated.View style={[a.fixed, a.left_0, a.right_0, animatedStyle]}>
-      <PostThreadComposePrompt onPressCompose={onPressReply} />
+      <ThreadComposePrompt onPressCompose={onPressReply} />
     </Animated.View>
   )
 }
diff --git a/src/screens/Settings/ThreadPreferences.tsx b/src/screens/Settings/ThreadPreferences.tsx
index af3cf915f..cba896a76 100644
--- a/src/screens/Settings/ThreadPreferences.tsx
+++ b/src/screens/Settings/ThreadPreferences.tsx
@@ -6,11 +6,6 @@ import {
   type CommonNavigatorParams,
   type NativeStackScreenProps,
 } from '#/lib/routes/types'
-import {useGate} from '#/lib/statsig/statsig'
-import {
-  usePreferencesQuery,
-  useSetThreadViewPreferencesMutation,
-} from '#/state/queries/preferences'
 import {
   normalizeSort,
   normalizeView,
@@ -18,7 +13,6 @@ import {
 } from '#/state/queries/preferences/useThreadPreferences'
 import {atoms as a, useTheme} from '#/alf'
 import * as Toggle from '#/components/forms/Toggle'
-import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
 import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
 import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
 import {Tree_Stroke2_Corner0_Rounded as TreeIcon} from '#/components/icons/Tree'
@@ -28,16 +22,6 @@ import * as SettingsList from './components/SettingsList'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
 export function ThreadPreferencesScreen({}: Props) {
-  const gate = useGate()
-
-  return gate('post_threads_v2_unspecced') ? (
-    <ThreadPreferencesV2 />
-  ) : (
-    <ThreadPreferencesV1 />
-  )
-}
-
-export function ThreadPreferencesV2() {
   const t = useTheme()
   const {_} = useLingui()
   const {
@@ -150,145 +134,3 @@ export function ThreadPreferencesV2() {
     </Layout.Screen>
   )
 }
-
-export function ThreadPreferencesV1() {
-  const {_} = useLingui()
-  const t = useTheme()
-
-  const {data: preferences} = usePreferencesQuery()
-  const {mutate: setThreadViewPrefs, variables} =
-    useSetThreadViewPreferencesMutation()
-
-  const sortReplies = variables?.sort ?? preferences?.threadViewPrefs?.sort
-
-  const prioritizeFollowedUsers = Boolean(
-    variables?.prioritizeFollowedUsers ??
-      preferences?.threadViewPrefs?.prioritizeFollowedUsers,
-  )
-  const treeViewEnabled = Boolean(
-    variables?.lab_treeViewEnabled ??
-      preferences?.threadViewPrefs?.lab_treeViewEnabled,
-  )
-
-  return (
-    <Layout.Screen testID="threadPreferencesScreen">
-      <Layout.Header.Outer>
-        <Layout.Header.BackButton />
-        <Layout.Header.Content>
-          <Layout.Header.TitleText>
-            <Trans>Thread Preferences</Trans>
-          </Layout.Header.TitleText>
-        </Layout.Header.Content>
-        <Layout.Header.Slot />
-      </Layout.Header.Outer>
-      <Layout.Content>
-        <SettingsList.Container>
-          <SettingsList.Group>
-            <SettingsList.ItemIcon icon={BubblesIcon} />
-            <SettingsList.ItemText>
-              <Trans>Sort replies</Trans>
-            </SettingsList.ItemText>
-            <View style={[a.w_full, a.gap_md]}>
-              <Text style={[a.flex_1, t.atoms.text_contrast_medium]}>
-                <Trans>Sort replies to the same post by:</Trans>
-              </Text>
-              <Toggle.Group
-                label={_(msg`Sort replies by`)}
-                type="radio"
-                values={sortReplies ? [sortReplies] : []}
-                onChange={values => setThreadViewPrefs({sort: values[0]})}>
-                <View style={[a.gap_sm, a.flex_1]}>
-                  <Toggle.Item name="hotness" label={_(msg`Hot replies first`)}>
-                    <Toggle.Radio />
-                    <Toggle.LabelText>
-                      <Trans>Hot replies first</Trans>
-                    </Toggle.LabelText>
-                  </Toggle.Item>
-                  <Toggle.Item
-                    name="oldest"
-                    label={_(msg`Oldest replies first`)}>
-                    <Toggle.Radio />
-                    <Toggle.LabelText>
-                      <Trans>Oldest replies first</Trans>
-                    </Toggle.LabelText>
-                  </Toggle.Item>
-                  <Toggle.Item
-                    name="newest"
-                    label={_(msg`Newest replies first`)}>
-                    <Toggle.Radio />
-                    <Toggle.LabelText>
-                      <Trans>Newest replies first</Trans>
-                    </Toggle.LabelText>
-                  </Toggle.Item>
-                  <Toggle.Item
-                    name="most-likes"
-                    label={_(msg`Most-liked replies first`)}>
-                    <Toggle.Radio />
-                    <Toggle.LabelText>
-                      <Trans>Most-liked first</Trans>
-                    </Toggle.LabelText>
-                  </Toggle.Item>
-                  <Toggle.Item
-                    name="random"
-                    label={_(msg`Random (aka "Poster's Roulette")`)}>
-                    <Toggle.Radio />
-                    <Toggle.LabelText>
-                      <Trans>Random (aka "Poster's Roulette")</Trans>
-                    </Toggle.LabelText>
-                  </Toggle.Item>
-                </View>
-              </Toggle.Group>
-            </View>
-          </SettingsList.Group>
-          <SettingsList.Group>
-            <SettingsList.ItemIcon icon={PersonGroupIcon} />
-            <SettingsList.ItemText>
-              <Trans>Prioritize your Follows</Trans>
-            </SettingsList.ItemText>
-            <Toggle.Item
-              type="checkbox"
-              name="prioritize-follows"
-              label={_(msg`Prioritize your Follows`)}
-              value={prioritizeFollowedUsers}
-              onChange={value =>
-                setThreadViewPrefs({
-                  prioritizeFollowedUsers: value,
-                })
-              }
-              style={[a.w_full, a.gap_md]}>
-              <Toggle.LabelText style={[a.flex_1]}>
-                <Trans>
-                  Show replies by people you follow before all other replies
-                </Trans>
-              </Toggle.LabelText>
-              <Toggle.Platform />
-            </Toggle.Item>
-          </SettingsList.Group>
-          <SettingsList.Divider />
-          <SettingsList.Group>
-            <SettingsList.ItemIcon icon={BeakerIcon} />
-            <SettingsList.ItemText>
-              <Trans>Experimental</Trans>
-            </SettingsList.ItemText>
-            <Toggle.Item
-              type="checkbox"
-              name="threaded-mode"
-              label={_(msg`Threaded mode`)}
-              value={treeViewEnabled}
-              onChange={value =>
-                setThreadViewPrefs({
-                  lab_treeViewEnabled: value,
-                })
-              }
-              style={[a.w_full, a.gap_md]}>
-              <Toggle.LabelText style={[a.flex_1]}>
-                <Trans>Show replies as threaded</Trans>
-              </Toggle.LabelText>
-              <Toggle.Platform />
-            </Toggle.Item>
-          </SettingsList.Group>
-        </SettingsList.Container>
-      </Layout.Content>
-    </Layout.Screen>
-  )
-}
diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx
index b53593010..22989e6c2 100644
--- a/src/screens/VideoFeed/index.tsx
+++ b/src/screens/VideoFeed/index.tsx
@@ -80,9 +80,9 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile'
 import {useSession} from '#/state/session'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
-import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
 import {List} from '#/view/com/util/List'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {ThreadComposePrompt} from '#/screens/PostThread/components/ThreadComposePrompt'
 import {Header} from '#/screens/VideoFeed/components/Header'
 import {atoms as a, ios, platform, ThemeProvider, useTheme} from '#/alf'
 import {setSystemUITheme} from '#/alf/util/systemUI'
@@ -883,7 +883,7 @@ function Overlay({
               player={player}
               seekingAnimationSV={seekingAnimationSV}
               scrollGesture={scrollGesture}>
-              <PostThreadComposePrompt
+              <ThreadComposePrompt
                 onPressCompose={onPressReply}
                 style={[a.pt_md, a.pb_sm]}
               />