diff --git a/src/features/composePrompt/ComposePromptGradient.tsx b/src/features/composePrompt/ComposePromptGradient.tsx
new file mode 100644
index 0000000000..83b4e33ead
--- /dev/null
+++ b/src/features/composePrompt/ComposePromptGradient.tsx
@@ -0,0 +1,53 @@
+import Animated, {useAnimatedStyle} from 'react-native-reanimated'
+import {LinearGradient} from 'expo-linear-gradient'
+
+import {atoms as a, useTheme, utils} from '#/alf'
+import {useComposePromptState} from './context'
+
+/**
+ * Tall enough to cover the pill sitting just above the bar, plus a lead-in
+ * over the content above it.
+ */
+const GRADIENT_HEIGHT = 64
+
+/**
+ * Fade from the content into the bottom bar's background, drawn above the
+ * bar so it moves and fades with it. Rendered inside the bar; only shown
+ * while a compose pill is.
+ */
+export function ComposePromptGradient() {
+ const {visibility, config} = useComposePromptState()
+ const t = useTheme()
+
+ const fadeStyle = useAnimatedStyle(() => ({
+ opacity: visibility.get(),
+ }))
+
+ if (!config) {
+ return null
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/src/features/composePrompt/ComposePromptPill.tsx b/src/features/composePrompt/ComposePromptPill.tsx
index 2af12022e7..e9766e761e 100644
--- a/src/features/composePrompt/ComposePromptPill.tsx
+++ b/src/features/composePrompt/ComposePromptPill.tsx
@@ -1,14 +1,11 @@
import {useState} from 'react'
-import {useWindowDimensions} from 'react-native'
import Animated, {
interpolate,
useAnimatedReaction,
useAnimatedStyle,
- useSharedValue,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
-import {LinearGradient} from 'expo-linear-gradient'
import {useLingui} from '@lingui/react/macro'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
@@ -18,7 +15,7 @@ import {useSession} from '#/state/session'
import {useMinimalShellMode} from '#/state/shell/minimal-mode'
import {useShellLayout} from '#/state/shell/shell-layout'
import {UserAvatar} from '#/view/com/util/UserAvatar'
-import {atoms as a, ios, tokens, useTheme, utils} from '#/alf'
+import {atoms as a, ios, tokens, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {GlassView, IS_GLASS_AVAILABLE} from '#/components/GlassView'
import {Camera_Stroke2_Corner0_Rounded as CameraIcon} from '#/components/icons/Camera'
@@ -33,19 +30,11 @@ const AVATAR_SIZE = 32
/**
* Horizontal inset of the pill while the bottom bar is showing.
*/
-const SIDE_MARGIN_UP = tokens.space.md
+const SIDE_MARGIN_UP = tokens.space.sm
/**
* Gap between the pill and the top of the bottom bar.
*/
-const GAP_ABOVE_BAR = tokens.space.sm
-/**
- * Extra room above the pill for the gradient to fade in over.
- */
-const GRADIENT_LEAD = tokens.space._4xl
-/**
- * How much the pill shrinks once it has dropped into the bar's space.
- */
-const DOWN_SCALE = 0.92
+const GAP_ABOVE_BAR = tokens.space._2xs
/**
* The compose pill that lives in the bottom bar. Screens opt in with
@@ -57,7 +46,6 @@ export function ComposePromptPill() {
const {footerMode} = useMinimalShellMode()
const {footerHeight} = useShellLayout()
const insets = useSafeAreaInsets()
- const {width: windowWidth} = useWindowDimensions()
const t = useTheme()
const {t: l} = useLingui()
const ax = useAnalytics()
@@ -65,7 +53,6 @@ export function ComposePromptPill() {
const {currentAccount} = useSession()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const {onPressGallery, onPressCamera} = useComposePromptMedia(config?.open)
- const pillHeight = useSharedValue(AVATAR_SIZE + tokens.space.sm * 2)
/*
* Liquid Glass materializes and dissolves through the system's own
@@ -83,30 +70,24 @@ export function ComposePromptPill() {
)
/*
- * In the down state the pill is inset from the left, right and bottom
- * edges by the same amount, so that its corners are concentric with the
- * device bevels. The inner pill is scaled about its centre, so the outer
- * padding and offset are solved for the visual margins after scaling.
+ * The pill sits just above the bar and follows it down as it hides. In the
+ * down state it is inset from the left, right and bottom edges by the same
+ * amount, so that its corners are concentric with the device bevels.
*/
const downMargin = Math.max(insets.bottom, tokens.space.lg)
const wrapperStyle = useAnimatedStyle(() => {
const mode = footerMode.get()
- const scale = interpolate(mode, [0, 1], [1, DOWN_SCALE])
- const sideMargin = interpolate(mode, [0, 1], [SIDE_MARGIN_UP, downMargin])
- const paddingHorizontal =
- (sideMargin - ((1 - scale) * windowWidth) / 2) / scale
const upBottom = footerHeight.get() + GAP_ABOVE_BAR
- const bottomMargin = interpolate(mode, [0, 1], [upBottom, downMargin])
- const translateY =
- upBottom -
- bottomMargin +
- ((1 - scale) * pillHeight.get()) / 2 +
- // a small drop while fading, like a sheet dismissing
- (1 - visibility.get()) * tokens.space.md
return {
bottom: footerHeight.get(),
- paddingHorizontal,
- transform: [{translateY}],
+ paddingHorizontal: interpolate(
+ mode,
+ [0, 1],
+ [SIDE_MARGIN_UP, downMargin],
+ ),
+ transform: [
+ {translateY: interpolate(mode, [0, 1], [0, upBottom - downMargin])},
+ ],
}
})
@@ -116,9 +97,6 @@ export function ComposePromptPill() {
pointerEvents: shown > 0.5 ? 'auto' : 'none',
// a fully transparent ancestor stops Liquid Glass rendering at all
opacity: IS_GLASS_AVAILABLE ? 1 : shown,
- transform: [
- {scale: interpolate(footerMode.get(), [0, 1], [1, DOWN_SCALE])},
- ],
}
})
@@ -143,24 +121,9 @@ export function ComposePromptPill() {
a.left_0,
a.right_0,
a.z_10,
- {paddingTop: GRADIENT_LEAD, paddingBottom: GAP_ABOVE_BAR},
+ {paddingBottom: GAP_ABOVE_BAR},
wrapperStyle,
]}>
-
-
-
{
onPress()
playHaptic('Heavy')
- })}
- onLayout={e => pillHeight.set(e.nativeEvent.layout.height)}>
+ })}>
+ /**
+ * 1 while registered, sprung to 0 on unregister so a screen that leaves
+ * without a transition still fades the pill out.
+ */
+ weight: SharedValue
config: ComposePromptConfig
}
@@ -72,6 +77,7 @@ type StateContext = {
type ActionsContext = {
register: (
presence: SharedValue,
+ weight: SharedValue,
config: ComposePromptConfig,
) => number
update: (id: number, config: ComposePromptConfig) => void
@@ -92,7 +98,7 @@ export function Provider({children}: {children: React.ReactNode}) {
const visibility = useDerivedValue(() => {
let sum = 0
for (const entry of entries) {
- sum += entry.presence.get()
+ sum += entry.presence.get() * entry.weight.get()
}
return clamp(sum, 0, 1)
}, [entries])
@@ -106,7 +112,7 @@ export function Provider({children}: {children: React.ReactNode}) {
let best: number | null = null
let bestPresence = 0
for (const entry of entries) {
- const presence = entry.presence.get()
+ const presence = entry.presence.get() * entry.weight.get()
if (presence > bestPresence) {
best = entry.id
bestPresence = presence
@@ -127,9 +133,9 @@ export function Provider({children}: {children: React.ReactNode}) {
const actions = useMemo(
() => ({
- register(presence, config) {
+ register(presence, weight, config) {
const id = nextId++
- setEntries(prev => [...prev, {id, presence, config}])
+ setEntries(prev => [...prev, {id, presence, weight, config}])
return id
},
update(id, config) {
@@ -192,30 +198,20 @@ function useComposePromptActions() {
export function useComposePromptForScreen(config: ComposePromptConfig | null) {
const {register, update, unregister} = useComposePromptActions()
const {presence} = useScreenPresence()
- const contribution = useSharedValue(0)
+ const weight = useSharedValue(0)
const idRef = useRef(null)
const enabled = config !== null
// the bar's top border would cut across the pill's gradient
useHideBottomBarBorderForScreen({enabled})
- useAnimatedReaction(
- () => presence.get(),
- (current, previous) => {
- if (enabled && current !== previous) {
- contribution.set(current)
- }
- },
- [enabled],
- )
-
const getConfig = useEffectEvent(() => config)
useEffect(() => {
const initial = getConfig()
if (!initial) return
- contribution.set(presence.get())
- const id = register(contribution, initial)
+ weight.set(1)
+ const id = register(presence, weight, initial)
idRef.current = id
return () => {
idRef.current = null
@@ -223,7 +219,7 @@ export function useComposePromptForScreen(config: ComposePromptConfig | null) {
* Fade out before removing so that a screen removed without a
* transition (or a config switched off) does not snap the pill away.
*/
- contribution.set(
+ weight.set(
withSpring(0, SHELL_SPRING_CONFIG, finished => {
if (finished) {
scheduleOnRN(unregister, id)
@@ -231,7 +227,7 @@ export function useComposePromptForScreen(config: ComposePromptConfig | null) {
}),
)
}
- }, [enabled, register, unregister, contribution, presence])
+ }, [enabled, register, unregister, weight, presence])
const label = config?.label
const accessibilityLabel = config?.accessibilityLabel
diff --git a/src/features/composePrompt/index.tsx b/src/features/composePrompt/index.tsx
index f54079e879..1f7ddae2fc 100644
--- a/src/features/composePrompt/index.tsx
+++ b/src/features/composePrompt/index.tsx
@@ -4,6 +4,7 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {type ComposerLogContext} from '#/state/shell/composer'
import {type ComposePromptConfig, useComposePromptForScreen} from './context'
+export {ComposePromptGradient} from './ComposePromptGradient'
export {ComposePromptPill} from './ComposePromptPill'
export {
type ComposePromptConfig,
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index c592abdb09..97bd1db55b 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -56,7 +56,10 @@ import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAgeAssurance} from '#/ageAssurance'
import {useAnalytics} from '#/analytics'
-import {ComposePromptPill} from '#/features/composePrompt'
+import {
+ ComposePromptGradient,
+ ComposePromptPill,
+} from '#/features/composePrompt'
import {useActorStatus} from '#/features/liveNow'
import {useDemoMode} from '#/storage/hooks/demo-mode'
import {styles} from './BottomBarStyles'
@@ -174,6 +177,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
onLayout={e => {
footerHeight.set(e.nativeEvent.layout.height)
}}>
+ {hasSession && }
{hasSession ? (
<>
footerHeight.set(event.nativeEvent.layout.height)}>
+ {hasSession && }
{hasSession ? (
<>