about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/icons/Image.tsx2
-rw-r--r--src/view/com/composer/Composer.tsx145
-rw-r--r--src/view/com/composer/threadgate/ThreadgateBtn.tsx9
3 files changed, 129 insertions, 27 deletions
diff --git a/src/components/icons/Image.tsx b/src/components/icons/Image.tsx
index 03702a0f4..eac296ad4 100644
--- a/src/components/icons/Image.tsx
+++ b/src/components/icons/Image.tsx
@@ -1,5 +1,5 @@
 import {createSinglePathSVG} from './TEMPLATE'
 
 export const Image_Stroke2_Corner0_Rounded = createSinglePathSVG({
-  path: 'M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm16 0H5v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5Zm0 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z',
+  path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5H5Zm14 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z',
 })
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index b1c020a10..ad79cdb58 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -9,6 +9,7 @@ import React, {
 import {
   ActivityIndicator,
   Keyboard,
+  LayoutChangeEvent,
   StyleSheet,
   TouchableOpacity,
   View,
@@ -19,6 +20,7 @@ import {
 } from 'react-native-keyboard-controller'
 import Animated, {
   interpolateColor,
+  runOnUI,
   useAnimatedStyle,
   useSharedValue,
   withTiming,
@@ -170,22 +172,6 @@ export const ComposePost = observer(function ComposePost({
     [insets, isKeyboardVisible],
   )
 
-  const hasScrolled = useSharedValue(0)
-  const scrollHandler = useAnimatedScrollHandler({
-    onScroll: event => {
-      hasScrolled.value = withTiming(event.contentOffset.y > 0 ? 1 : 0)
-    },
-  })
-  const topBarAnimatedStyle = useAnimatedStyle(() => {
-    return {
-      borderColor: interpolateColor(
-        hasScrolled.value,
-        [0, 1],
-        ['transparent', t.atoms.border_contrast_medium.borderColor],
-      ),
-    }
-  })
-
   const onPressCancel = useCallback(() => {
     if (graphemeLength > 0 || !gallery.isEmpty) {
       closeAllDialogs()
@@ -395,13 +381,21 @@ export const ComposePost = observer(function ComposePost({
     [setExtLink],
   )
 
+  const {
+    scrollHandler,
+    onScrollViewContentSizeChange,
+    onScrollViewLayout,
+    topBarAnimatedStyle,
+    bottomBarAnimatedStyle,
+  } = useAnimatedBorders()
+
   return (
     <>
       <KeyboardAvoidingView
         testID="composePostView"
         behavior="padding"
         style={a.flex_1}
-        keyboardVerticalOffset={replyTo ? 120 : isAndroid ? 180 : 150}>
+        keyboardVerticalOffset={replyTo ? 110 : isAndroid ? 180 : 140}>
         <View
           style={[a.flex_1, viewStyles]}
           aria-modal
@@ -509,7 +503,9 @@ export const ComposePost = observer(function ComposePost({
           <Animated.ScrollView
             onScroll={scrollHandler}
             style={styles.scrollView}
-            keyboardShouldPersistTaps="always">
+            keyboardShouldPersistTaps="always"
+            onContentSizeChange={onScrollViewContentSizeChange}
+            onLayout={onScrollViewLayout}>
             {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
 
             <View
@@ -575,7 +571,11 @@ export const ComposePost = observer(function ComposePost({
       <KeyboardStickyView
         offset={{closed: isIOS ? -insets.bottom : 0, opened: 0}}>
         {replyTo ? null : (
-          <ThreadgateBtn threadgate={threadgate} onChange={setThreadgate} />
+          <ThreadgateBtn
+            threadgate={threadgate}
+            onChange={setThreadgate}
+            style={bottomBarAnimatedStyle}
+          />
         )}
         <View
           style={[
@@ -625,10 +625,108 @@ export function useComposerCancelRef() {
   return useRef<CancelRef>(null)
 }
 
+function useAnimatedBorders() {
+  const t = useTheme()
+  const hasScrolledTop = useSharedValue(0)
+  const hasScrolledBottom = useSharedValue(0)
+  const contentOffset = useSharedValue(0)
+  const scrollViewHeight = useSharedValue(Infinity)
+  const contentHeight = useSharedValue(0)
+
+  /**
+   * Make sure to run this on the UI thread!
+   */
+  const showHideBottomBorder = useCallback(
+    ({
+      newContentHeight,
+      newContentOffset,
+      newScrollViewHeight,
+    }: {
+      newContentHeight?: number
+      newContentOffset?: number
+      newScrollViewHeight?: number
+    }) => {
+      'worklet'
+
+      if (typeof newContentHeight === 'number')
+        contentHeight.value = newContentHeight
+      if (typeof newContentOffset === 'number')
+        contentOffset.value = newContentOffset
+      if (typeof newScrollViewHeight === 'number')
+        scrollViewHeight.value = newScrollViewHeight
+
+      hasScrolledBottom.value = withTiming(
+        contentHeight.value - contentOffset.value >= scrollViewHeight.value
+          ? 1
+          : 0,
+      )
+    },
+    [contentHeight, contentOffset, scrollViewHeight, hasScrolledBottom],
+  )
+
+  const scrollHandler = useAnimatedScrollHandler({
+    onScroll: event => {
+      hasScrolledTop.value = withTiming(event.contentOffset.y > 0 ? 1 : 0)
+
+      // already on UI thread
+      showHideBottomBorder({
+        newContentOffset: event.contentOffset.y,
+        newContentHeight: event.contentSize.height,
+        newScrollViewHeight: event.layoutMeasurement.height,
+      })
+    },
+  })
+
+  const onScrollViewContentSizeChange = useCallback(
+    (_width: number, height: number) => {
+      runOnUI(showHideBottomBorder)({
+        newContentHeight: height,
+      })
+    },
+    [showHideBottomBorder],
+  )
+
+  const onScrollViewLayout = useCallback(
+    (evt: LayoutChangeEvent) => {
+      runOnUI(showHideBottomBorder)({
+        newScrollViewHeight: evt.nativeEvent.layout.height,
+      })
+    },
+    [showHideBottomBorder],
+  )
+
+  const topBarAnimatedStyle = useAnimatedStyle(() => {
+    return {
+      borderBottomWidth: hairlineWidth,
+      borderColor: interpolateColor(
+        hasScrolledTop.value,
+        [0, 1],
+        ['transparent', t.atoms.border_contrast_medium.borderColor],
+      ),
+    }
+  })
+  const bottomBarAnimatedStyle = useAnimatedStyle(() => {
+    return {
+      borderTopWidth: hairlineWidth,
+      borderColor: interpolateColor(
+        hasScrolledBottom.value,
+        [0, 1],
+        ['transparent', t.atoms.border_contrast_medium.borderColor],
+      ),
+    }
+  })
+
+  return {
+    scrollHandler,
+    onScrollViewContentSizeChange,
+    onScrollViewLayout,
+    topBarAnimatedStyle,
+    bottomBarAnimatedStyle,
+  }
+}
+
 const styles = StyleSheet.create({
-  topbar: {
-    borderBottomWidth: StyleSheet.hairlineWidth,
-  },
+  topbar: {},
   topbarDesktop: {
     paddingTop: 10,
     paddingBottom: 10,
@@ -698,7 +796,8 @@ const styles = StyleSheet.create({
   bottomBar: {
     flexDirection: 'row',
     paddingVertical: 4,
-    paddingLeft: 8,
+    // should be 8 but due to visual alignment we have to fudge it
+    paddingLeft: 7,
     paddingRight: 16,
     alignItems: 'center',
     borderTopWidth: hairlineWidth,
diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
index afc9f5bfa..2aefdfbbf 100644
--- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx
+++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
@@ -1,5 +1,6 @@
 import React from 'react'
-import {Keyboard, View} from 'react-native'
+import {Keyboard, StyleProp, ViewStyle} from 'react-native'
+import Animated, {AnimatedStyle} from 'react-native-reanimated'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -16,9 +17,11 @@ import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
 export function ThreadgateBtn({
   threadgate,
   onChange,
+  style,
 }: {
   threadgate: ThreadgateSetting[]
   onChange: (v: ThreadgateSetting[]) => void
+  style?: StyleProp<AnimatedStyle<ViewStyle>>
 }) {
   const {track} = useAnalytics()
   const {_} = useLingui()
@@ -46,7 +49,7 @@ export function ThreadgateBtn({
     : _(msg`Some people can reply`)
 
   return (
-    <View style={[a.flex_row, a.py_xs, a.px_sm, t.atoms.bg]}>
+    <Animated.View style={[a.flex_row, a.p_sm, t.atoms.bg, style]}>
       <Button
         variant="solid"
         color="secondary"
@@ -59,6 +62,6 @@ export function ThreadgateBtn({
         />
         <ButtonText>{label}</ButtonText>
       </Button>
-    </View>
+    </Animated.View>
   )
 }