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'
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
export const Image_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
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 {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
|
LayoutChangeEvent,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
|
@ -19,6 +20,7 @@ import {
|
||||||
} from 'react-native-keyboard-controller'
|
} from 'react-native-keyboard-controller'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
interpolateColor,
|
interpolateColor,
|
||||||
|
runOnUI,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming,
|
withTiming,
|
||||||
|
@ -170,22 +172,6 @@ export const ComposePost = observer(function ComposePost({
|
||||||
[insets, isKeyboardVisible],
|
[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(() => {
|
const onPressCancel = useCallback(() => {
|
||||||
if (graphemeLength > 0 || !gallery.isEmpty) {
|
if (graphemeLength > 0 || !gallery.isEmpty) {
|
||||||
closeAllDialogs()
|
closeAllDialogs()
|
||||||
|
@ -395,13 +381,21 @@ export const ComposePost = observer(function ComposePost({
|
||||||
[setExtLink],
|
[setExtLink],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
scrollHandler,
|
||||||
|
onScrollViewContentSizeChange,
|
||||||
|
onScrollViewLayout,
|
||||||
|
topBarAnimatedStyle,
|
||||||
|
bottomBarAnimatedStyle,
|
||||||
|
} = useAnimatedBorders()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
testID="composePostView"
|
testID="composePostView"
|
||||||
behavior="padding"
|
behavior="padding"
|
||||||
style={a.flex_1}
|
style={a.flex_1}
|
||||||
keyboardVerticalOffset={replyTo ? 120 : isAndroid ? 180 : 150}>
|
keyboardVerticalOffset={replyTo ? 110 : isAndroid ? 180 : 140}>
|
||||||
<View
|
<View
|
||||||
style={[a.flex_1, viewStyles]}
|
style={[a.flex_1, viewStyles]}
|
||||||
aria-modal
|
aria-modal
|
||||||
|
@ -509,7 +503,9 @@ export const ComposePost = observer(function ComposePost({
|
||||||
<Animated.ScrollView
|
<Animated.ScrollView
|
||||||
onScroll={scrollHandler}
|
onScroll={scrollHandler}
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
keyboardShouldPersistTaps="always">
|
keyboardShouldPersistTaps="always"
|
||||||
|
onContentSizeChange={onScrollViewContentSizeChange}
|
||||||
|
onLayout={onScrollViewLayout}>
|
||||||
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
|
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
@ -575,7 +571,11 @@ export const ComposePost = observer(function ComposePost({
|
||||||
<KeyboardStickyView
|
<KeyboardStickyView
|
||||||
offset={{closed: isIOS ? -insets.bottom : 0, opened: 0}}>
|
offset={{closed: isIOS ? -insets.bottom : 0, opened: 0}}>
|
||||||
{replyTo ? null : (
|
{replyTo ? null : (
|
||||||
<ThreadgateBtn threadgate={threadgate} onChange={setThreadgate} />
|
<ThreadgateBtn
|
||||||
|
threadgate={threadgate}
|
||||||
|
onChange={setThreadgate}
|
||||||
|
style={bottomBarAnimatedStyle}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
@ -625,10 +625,108 @@ export function useComposerCancelRef() {
|
||||||
return useRef<CancelRef>(null)
|
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({
|
const styles = StyleSheet.create({
|
||||||
topbar: {
|
topbar: {},
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
||||||
},
|
|
||||||
topbarDesktop: {
|
topbarDesktop: {
|
||||||
paddingTop: 10,
|
paddingTop: 10,
|
||||||
paddingBottom: 10,
|
paddingBottom: 10,
|
||||||
|
@ -698,7 +796,8 @@ const styles = StyleSheet.create({
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
paddingLeft: 8,
|
// should be 8 but due to visual alignment we have to fudge it
|
||||||
|
paddingLeft: 7,
|
||||||
paddingRight: 16,
|
paddingRight: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderTopWidth: hairlineWidth,
|
borderTopWidth: hairlineWidth,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react'
|
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 {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
@ -16,9 +17,11 @@ import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
||||||
export function ThreadgateBtn({
|
export function ThreadgateBtn({
|
||||||
threadgate,
|
threadgate,
|
||||||
onChange,
|
onChange,
|
||||||
|
style,
|
||||||
}: {
|
}: {
|
||||||
threadgate: ThreadgateSetting[]
|
threadgate: ThreadgateSetting[]
|
||||||
onChange: (v: ThreadgateSetting[]) => void
|
onChange: (v: ThreadgateSetting[]) => void
|
||||||
|
style?: StyleProp<AnimatedStyle<ViewStyle>>
|
||||||
}) {
|
}) {
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
@ -46,7 +49,7 @@ export function ThreadgateBtn({
|
||||||
: _(msg`Some people can reply`)
|
: _(msg`Some people can reply`)
|
||||||
|
|
||||||
return (
|
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
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
@ -59,6 +62,6 @@ export function ThreadgateBtn({
|
||||||
/>
|
/>
|
||||||
<ButtonText>{label}</ButtonText>
|
<ButtonText>{label}</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue