From 9828490fd0fcc557a32b40e09ae81a575a8f925b Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 11 Jun 2025 20:34:59 +0300 Subject: [PATCH] Better reply prompt (#8474) * restyle reply prompt * hide bottom bar border for cleaner look * use new border hiding hook in DMs --- src/App.native.tsx | 19 ++--- src/App.web.tsx | 11 +-- src/lib/hooks/useHideBottomBarBorder.tsx | 50 +++++++++++++ .../Messages/components/MessagesList.tsx | 3 + src/screens/PostThread/index.tsx | 26 +++---- src/view/com/post-thread/PostThread.tsx | 36 ++++------ .../post-thread/PostThreadComposePrompt.tsx | 71 ++++++++++++------- src/view/shell/bottom-bar/BottomBar.tsx | 4 +- src/view/shell/bottom-bar/BottomBarWeb.tsx | 13 +++- 9 files changed, 153 insertions(+), 80 deletions(-) create mode 100644 src/lib/hooks/useHideBottomBarBorder.tsx diff --git a/src/App.native.tsx b/src/App.native.tsx index e3f85c0fe5..80e7d57f32 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -73,6 +73,7 @@ import {Provider as PortalProvider} from '#/components/Portal' import {Splash} from '#/Splash' import {BottomSheetProvider} from '../modules/bottom-sheet' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' +import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' SplashScreen.preventAutoHideAsync() if (isIOS) { @@ -152,14 +153,16 @@ function InnerApp() { - - - - - - - + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 97ada61485..ca23d0c668 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -62,6 +62,7 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' +import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' /** * Begin geolocation ASAP @@ -133,10 +134,12 @@ function InnerApp() { - - - - + + + + + + diff --git a/src/lib/hooks/useHideBottomBarBorder.tsx b/src/lib/hooks/useHideBottomBarBorder.tsx new file mode 100644 index 0000000000..e21184fda4 --- /dev/null +++ b/src/lib/hooks/useHideBottomBarBorder.tsx @@ -0,0 +1,50 @@ +import {createContext, useCallback, useContext, useState} from 'react' +import {useFocusEffect} from '@react-navigation/native' + +type HideBottomBarBorderSetter = () => () => void + +const HideBottomBarBorderContext = createContext(false) +const HideBottomBarBorderSetterContext = + createContext(null) + +export function useHideBottomBarBorderSetter() { + const hideBottomBarBorder = useContext(HideBottomBarBorderSetterContext) + if (!hideBottomBarBorder) { + throw new Error( + 'useHideBottomBarBorderSetter must be used within a HideBottomBarBorderProvider', + ) + } + return hideBottomBarBorder +} + +export function useHideBottomBarBorderForScreen() { + const hideBorder = useHideBottomBarBorderSetter() + + useFocusEffect( + useCallback(() => { + const cleanup = hideBorder() + return () => cleanup() + }, [hideBorder]), + ) +} + +export function useHideBottomBarBorder() { + return useContext(HideBottomBarBorderContext) +} + +export function Provider({children}: {children: React.ReactNode}) { + const [refCount, setRefCount] = useState(0) + + const setter = useCallback(() => { + setRefCount(prev => prev + 1) + return () => setRefCount(prev => prev - 1) + }, []) + + return ( + + 0}> + {children} + + + ) +} diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index ce33ca3aa9..c84371f2c0 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -16,6 +16,7 @@ import { RichText, } from '@atproto/api' +import {useHideBottomBarBorderForScreen} from '#/lib/hooks/useHideBottomBarBorder' import {ScrollProvider} from '#/lib/ScrollContext' import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' import { @@ -106,6 +107,8 @@ export function MessagesList({ const getPost = useGetPost() const {embedUri, setEmbed} = useMessageEmbed() + useHideBottomBarBorderForScreen() + const flatListRef = useAnimatedRef() const [newMessagesPill, setNewMessagesPill] = useState({ diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx index 790ef75bfc..4c9d489ac9 100644 --- a/src/screens/PostThread/index.tsx +++ b/src/screens/PostThread/index.tsx @@ -1,17 +1,15 @@ import {useCallback, useMemo, useRef, useState} from 'react' import {useWindowDimensions, View} from 'react-native' -import Animated from 'react-native-reanimated' -import {useSafeAreaInsets} from 'react-native-safe-area-context' +import Animated, {useAnimatedStyle} from 'react-native-reanimated' import {Trans} from '@lingui/macro' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' -import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' -import {clamp} from '#/lib/numbers' import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences' import {type ThreadItem, usePostThread} from '#/state/queries/usePostThread' import {useSession} from '#/state/session' import {type OnPostSuccessData} from '#/state/shell/composer' +import {useShellLayout} from '#/state/shell/shell-layout' import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt' import {List, type ListMethods} from '#/view/com/util/List' import {HeaderDropdown} from '#/screens/PostThread/components/HeaderDropdown' @@ -533,20 +531,16 @@ export function PostThread({uri}: {uri: string | undefined}) { } function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) { - const safeAreaInsets = useSafeAreaInsets() - const fabMinimalShellTransform = useMinimalShellFabTransform() + const {footerHeight} = useShellLayout() + + const animatedStyle = useAnimatedStyle(() => { + return { + bottom: footerHeight.get(), + } + }) return ( - + ) diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index fb16de20d8..3d1a15c7aa 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -1,8 +1,7 @@ import React, {memo, useRef, useState} from 'react' -import {StyleSheet, useWindowDimensions, View} from 'react-native' -import {runOnJS} from 'react-native-reanimated' +import {useWindowDimensions, View} from 'react-native' +import {runOnJS, useAnimatedStyle} from 'react-native-reanimated' import Animated from 'react-native-reanimated' -import {useSafeAreaInsets} from 'react-native-safe-area-context' import { AppBskyFeedDefs, type AppBskyFeedThreadgate, @@ -13,11 +12,9 @@ import {useLingui} from '@lingui/react' import {HITSLOP_10} from '#/lib/constants' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' -import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {clamp} from '#/lib/numbers' import {ScrollProvider} from '#/lib/ScrollContext' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {cleanError} from '#/lib/strings/errors' @@ -36,6 +33,7 @@ import { import {useSetThreadViewPreferencesMutation} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences' import {useSession} from '#/state/session' +import {useShellLayout} from '#/state/shell/shell-layout' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {List, type ListMethods} from '#/view/com/util/List' import {atoms as a, useTheme} from '#/alf' @@ -715,17 +713,16 @@ let ThreadMenu = ({ ThreadMenu = memo(ThreadMenu) function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) { - const safeAreaInsets = useSafeAreaInsets() - const fabMinimalShellTransform = useMinimalShellFabTransform() + const {footerHeight} = useShellLayout() + + const animatedStyle = useAnimatedStyle(() => { + return { + bottom: footerHeight.get(), + } + }) + return ( - + ) @@ -890,12 +887,3 @@ function hasBranchingReplies(node?: ThreadNode) { } return true } - -const styles = StyleSheet.create({ - prompt: { - // @ts-ignore web-only - position: isWeb ? 'fixed' : 'absolute', - left: 0, - right: 0, - }, -}) diff --git a/src/view/com/post-thread/PostThreadComposePrompt.tsx b/src/view/com/post-thread/PostThreadComposePrompt.tsx index 40acff3765..6b421e9de9 100644 --- a/src/view/com/post-thread/PostThreadComposePrompt.tsx +++ b/src/view/com/post-thread/PostThreadComposePrompt.tsx @@ -1,13 +1,15 @@ import {View} from 'react-native' +import {LinearGradient} from 'expo-linear-gradient' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {PressableScale} from '#/lib/custom-animations/PressableScale' import {useHaptics} from '#/lib/haptics' +import {useHideBottomBarBorderForScreen} from '#/lib/hooks/useHideBottomBarBorder' import {useProfileQuery} from '#/state/queries/profile' import {useSession} from '#/state/session' import {UserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, ios, useBreakpoints, useTheme} from '#/alf' +import {atoms as a, ios, native, useBreakpoints, useTheme} from '#/alf' import {useInteractionState} from '#/components/hooks/useInteractionState' import {Text} from '#/components/Typography' @@ -28,29 +30,49 @@ export function PostThreadComposePrompt({ onOut: onHoverOut, } = useInteractionState() + useHideBottomBarBorderForScreen() + return ( - { - onPressCompose() - playHaptic('Light') - }} - onLongPress={ios(() => { - onPressCompose() - playHaptic('Heavy') - })} - onHoverIn={onHoverIn} - onHoverOut={onHoverOut}> - + {!gtMobile && ( + + )} + { + onPressCompose() + playHaptic('Light') + }} + onLongPress={ios(() => { + onPressCompose() + playHaptic('Heavy') + })} + onHoverIn={onHoverIn} + onHoverOut={onHoverOut} style={[ a.flex_row, a.align_center, @@ -58,6 +80,7 @@ export function PostThreadComposePrompt({ a.gap_sm, a.rounded_full, (!gtMobile || hovered) && t.atoms.bg_contrast_25, + native([a.border, t.atoms.border_contrast_low]), a.transition_color, ]}> Write your reply - - + + ) } diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 5e9168ecd8..01aa4afc42 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -12,6 +12,7 @@ import {PressableScale} from '#/lib/custom-animations/PressableScale' import {BOTTOM_BAR_AVI} from '#/lib/demo' import {useHaptics} from '#/lib/haptics' import {useDedupe} from '#/lib/hooks/useDedupe' +import {useHideBottomBarBorder} from '#/lib/hooks/useHideBottomBarBorder' import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform' import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState' import {usePalette} from '#/lib/hooks/usePalette' @@ -73,6 +74,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { const playHaptic = useHaptics() const hasHomeBadge = useHomeBadge() const gate = useGate() + const hideBorder = useHideBottomBarBorder() const iconWidth = 28 const showSignIn = useCallback(() => { @@ -146,7 +148,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { style={[ styles.bottomBar, pal.view, - pal.border, + hideBorder ? {borderColor: pal.view.backgroundColor} : pal.border, {paddingBottom: clamp(safeAreaInsets.bottom, 15, 60)}, footerMinimalShellTransform, ]} diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index 7a320cb438..8dce85cd17 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -5,16 +5,18 @@ import {msg, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigationState} from '@react-navigation/native' +import {useHideBottomBarBorder} from '#/lib/hooks/useHideBottomBarBorder' import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform' import {getCurrentRoute, isTab} from '#/lib/routes/helpers' import {makeProfileLink} from '#/lib/routes/links' -import {CommonNavigatorParams} from '#/lib/routes/types' +import {type CommonNavigatorParams} from '#/lib/routes/types' import {useGate} from '#/lib/statsig/statsig' import {useHomeBadge} from '#/state/home-badge' import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations' import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {useShellLayout} from '#/state/shell/shell-layout' import {useCloseAllActiveElements} from '#/state/util' import {Link} from '#/view/com/util/Link' import {Logo} from '#/view/icons/Logo' @@ -49,6 +51,8 @@ export function BottomBarWeb() { const footerMinimalShellTransform = useMinimalShellFooterTransform() const {requestSwitchToAccount} = useLoggedOutViewControls() const closeAllActiveElements = useCloseAllActiveElements() + const {footerHeight} = useShellLayout() + const hideBorder = useHideBottomBarBorder() const iconWidth = 26 const unreadMessageCount = useUnreadMessageCount() @@ -74,9 +78,12 @@ export function BottomBarWeb() { styles.bottomBar, styles.bottomBarWeb, t.atoms.bg, - t.atoms.border_contrast_low, + hideBorder + ? {borderColor: t.atoms.bg.backgroundColor} + : t.atoms.border_contrast_low, footerMinimalShellTransform, - ]}> + ]} + onLayout={event => footerHeight.set(event.nativeEvent.layout.height)}> {hasSession ? ( <>