diff options
author | Samuel Newman <mozzius@protonmail.com> | 2024-06-04 02:49:50 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-04 00:49:50 +0100 |
commit | 891b432eadb6b4a1907943b1219631e1b84329d2 (patch) | |
tree | 4540d5ee5fc146c030774dea31f3d45565c06d1c /src | |
parent | 3b55f61d5f0111287be56b76a1a342256d3f2a95 (diff) | |
download | voidsky-891b432eadb6b4a1907943b1219631e1b84329d2.tar.zst |
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
Diffstat (limited to 'src')
-rw-r--r-- | src/components/icons/Image.tsx | 2 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 145 | ||||
-rw-r--r-- | src/view/com/composer/threadgate/ThreadgateBtn.tsx | 9 |
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> ) } |