Composer - add animated bottom border (#4325)

* start adding bottom border (wip)

* add content change listener

* add layout listener and move to hook

* remove logs

* use square-er image icon

* visually align bottom bar icons

* reduce keyboard vertical offset slightly

* only add border to top/bottom

* run worklet function on UI thread
zio/stable
Samuel Newman 2024-06-04 02:49:50 +03:00 committed by GitHub
parent 3b55f61d5f
commit 891b432ead
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 130 additions and 28 deletions

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 514 B

View File

@ -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',
})

View File

@ -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,

View File

@ -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>
)
}