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 threadzio/stable
parent
3b55f61d5f
commit
891b432ead
|
@ -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 |
|
@ -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',
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue