Tune the compose pill: bar-attached gradient, clear glass, no scale
- Draw the fade behind the pill as part of the bottom bar, from transparent to the bar's background, so it meets the bar seamlessly and slides and fades with it instead of travelling with the pill. - Sit the pill almost on the bar, as the thread prompt did. - Use the clear glass style without a tint. - Drop the scale and the drop-while-fading motion; the pill now only translates down into the bar's space, keeping its equal edge insets there. - Sample each screen's presence directly in the registry instead of via a mirrored shared value per screen. The mirrors updated a frame apart, so the summed visibility dipped mid-transition between two screens that both show the pill, which read as a jitter. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QoggCVt9XgpKbjrTZjoJD9
This commit is contained in:
@@ -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 (
|
||||||
|
<Animated.View
|
||||||
|
pointerEvents="none"
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.left_0,
|
||||||
|
a.right_0,
|
||||||
|
{top: -GRADIENT_HEIGHT, height: GRADIENT_HEIGHT},
|
||||||
|
fadeStyle,
|
||||||
|
]}>
|
||||||
|
<LinearGradient
|
||||||
|
key={t.name} // android does not update when you change the colors. sigh.
|
||||||
|
start={[0.5, 0]}
|
||||||
|
end={[0.5, 1]}
|
||||||
|
colors={[
|
||||||
|
utils.alpha(t.atoms.bg.backgroundColor, 0),
|
||||||
|
t.atoms.bg.backgroundColor,
|
||||||
|
]}
|
||||||
|
locations={[0.15, 0.55]}
|
||||||
|
style={[a.flex_1]}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
import {useWindowDimensions} from 'react-native'
|
|
||||||
import Animated, {
|
import Animated, {
|
||||||
interpolate,
|
interpolate,
|
||||||
useAnimatedReaction,
|
useAnimatedReaction,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {scheduleOnRN} from 'react-native-worklets'
|
import {scheduleOnRN} from 'react-native-worklets'
|
||||||
import {LinearGradient} from 'expo-linear-gradient'
|
|
||||||
import {useLingui} from '@lingui/react/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||||
@@ -18,7 +15,7 @@ import {useSession} from '#/state/session'
|
|||||||
import {useMinimalShellMode} from '#/state/shell/minimal-mode'
|
import {useMinimalShellMode} from '#/state/shell/minimal-mode'
|
||||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
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 {Button} from '#/components/Button'
|
||||||
import {GlassView, IS_GLASS_AVAILABLE} from '#/components/GlassView'
|
import {GlassView, IS_GLASS_AVAILABLE} from '#/components/GlassView'
|
||||||
import {Camera_Stroke2_Corner0_Rounded as CameraIcon} from '#/components/icons/Camera'
|
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.
|
* 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.
|
* Gap between the pill and the top of the bottom bar.
|
||||||
*/
|
*/
|
||||||
const GAP_ABOVE_BAR = tokens.space.sm
|
const GAP_ABOVE_BAR = tokens.space._2xs
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The compose pill that lives in the bottom bar. Screens opt in with
|
* The compose pill that lives in the bottom bar. Screens opt in with
|
||||||
@@ -57,7 +46,6 @@ export function ComposePromptPill() {
|
|||||||
const {footerMode} = useMinimalShellMode()
|
const {footerMode} = useMinimalShellMode()
|
||||||
const {footerHeight} = useShellLayout()
|
const {footerHeight} = useShellLayout()
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const {width: windowWidth} = useWindowDimensions()
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
@@ -65,7 +53,6 @@ export function ComposePromptPill() {
|
|||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||||
const {onPressGallery, onPressCamera} = useComposePromptMedia(config?.open)
|
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
|
* 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
|
* The pill sits just above the bar and follows it down as it hides. In the
|
||||||
* edges by the same amount, so that its corners are concentric with the
|
* down state it is inset from the left, right and bottom edges by the same
|
||||||
* device bevels. The inner pill is scaled about its centre, so the outer
|
* amount, so that its corners are concentric with the device bevels.
|
||||||
* padding and offset are solved for the visual margins after scaling.
|
|
||||||
*/
|
*/
|
||||||
const downMargin = Math.max(insets.bottom, tokens.space.lg)
|
const downMargin = Math.max(insets.bottom, tokens.space.lg)
|
||||||
const wrapperStyle = useAnimatedStyle(() => {
|
const wrapperStyle = useAnimatedStyle(() => {
|
||||||
const mode = footerMode.get()
|
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 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 {
|
return {
|
||||||
bottom: footerHeight.get(),
|
bottom: footerHeight.get(),
|
||||||
paddingHorizontal,
|
paddingHorizontal: interpolate(
|
||||||
transform: [{translateY}],
|
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',
|
pointerEvents: shown > 0.5 ? 'auto' : 'none',
|
||||||
// a fully transparent ancestor stops Liquid Glass rendering at all
|
// a fully transparent ancestor stops Liquid Glass rendering at all
|
||||||
opacity: IS_GLASS_AVAILABLE ? 1 : shown,
|
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.left_0,
|
||||||
a.right_0,
|
a.right_0,
|
||||||
a.z_10,
|
a.z_10,
|
||||||
{paddingTop: GRADIENT_LEAD, paddingBottom: GAP_ABOVE_BAR},
|
{paddingBottom: GAP_ABOVE_BAR},
|
||||||
wrapperStyle,
|
wrapperStyle,
|
||||||
]}>
|
]}>
|
||||||
<Animated.View
|
|
||||||
pointerEvents="none"
|
|
||||||
style={[a.absolute, a.inset_0, fadeStyle]}>
|
|
||||||
<LinearGradient
|
|
||||||
key={t.name} // android does not update when you change the colors. sigh.
|
|
||||||
start={[0.5, 0]}
|
|
||||||
end={[0.5, 1]}
|
|
||||||
colors={[
|
|
||||||
utils.alpha(t.atoms.bg.backgroundColor, 0),
|
|
||||||
utils.alpha(t.atoms.bg.backgroundColor, 0.75),
|
|
||||||
]}
|
|
||||||
locations={[0, 0.65]}
|
|
||||||
style={[a.absolute, a.inset_0]}
|
|
||||||
/>
|
|
||||||
</Animated.View>
|
|
||||||
<Animated.View style={pillStyle}>
|
<Animated.View style={pillStyle}>
|
||||||
<PressableScale
|
<PressableScale
|
||||||
testID="composePromptPill"
|
testID="composePromptPill"
|
||||||
@@ -175,15 +138,13 @@ export function ComposePromptPill() {
|
|||||||
onLongPress={ios(() => {
|
onLongPress={ios(() => {
|
||||||
onPress()
|
onPress()
|
||||||
playHaptic('Heavy')
|
playHaptic('Heavy')
|
||||||
})}
|
})}>
|
||||||
onLayout={e => pillHeight.set(e.nativeEvent.layout.height)}>
|
|
||||||
<GlassView
|
<GlassView
|
||||||
isInteractive
|
isInteractive
|
||||||
glassEffectStyle={{
|
glassEffectStyle={{
|
||||||
style: glassVisible ? 'regular' : 'none',
|
style: glassVisible ? 'clear' : 'none',
|
||||||
animate: true,
|
animate: true,
|
||||||
}}
|
}}
|
||||||
tintColor={t.palette.contrast_50}
|
|
||||||
style={[a.rounded_full]}
|
style={[a.rounded_full]}
|
||||||
fallbackStyle={[
|
fallbackStyle={[
|
||||||
a.border,
|
a.border,
|
||||||
|
|||||||
@@ -49,10 +49,15 @@ export type ComposePromptConfig = {
|
|||||||
type Entry = {
|
type Entry = {
|
||||||
id: number
|
id: number
|
||||||
/**
|
/**
|
||||||
* How present the registering screen is, 0..1. Owned by the registering
|
* The registering screen's own presence, 0..1, read directly so that two
|
||||||
* hook so that it can be sprung to 0 on unregister.
|
* screens mid-transition are always sampled on the same frame.
|
||||||
*/
|
*/
|
||||||
presence: SharedValue<number>
|
presence: SharedValue<number>
|
||||||
|
/**
|
||||||
|
* 1 while registered, sprung to 0 on unregister so a screen that leaves
|
||||||
|
* without a transition still fades the pill out.
|
||||||
|
*/
|
||||||
|
weight: SharedValue<number>
|
||||||
config: ComposePromptConfig
|
config: ComposePromptConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +77,7 @@ type StateContext = {
|
|||||||
type ActionsContext = {
|
type ActionsContext = {
|
||||||
register: (
|
register: (
|
||||||
presence: SharedValue<number>,
|
presence: SharedValue<number>,
|
||||||
|
weight: SharedValue<number>,
|
||||||
config: ComposePromptConfig,
|
config: ComposePromptConfig,
|
||||||
) => number
|
) => number
|
||||||
update: (id: number, config: ComposePromptConfig) => void
|
update: (id: number, config: ComposePromptConfig) => void
|
||||||
@@ -92,7 +98,7 @@ export function Provider({children}: {children: React.ReactNode}) {
|
|||||||
const visibility = useDerivedValue(() => {
|
const visibility = useDerivedValue(() => {
|
||||||
let sum = 0
|
let sum = 0
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
sum += entry.presence.get()
|
sum += entry.presence.get() * entry.weight.get()
|
||||||
}
|
}
|
||||||
return clamp(sum, 0, 1)
|
return clamp(sum, 0, 1)
|
||||||
}, [entries])
|
}, [entries])
|
||||||
@@ -106,7 +112,7 @@ export function Provider({children}: {children: React.ReactNode}) {
|
|||||||
let best: number | null = null
|
let best: number | null = null
|
||||||
let bestPresence = 0
|
let bestPresence = 0
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const presence = entry.presence.get()
|
const presence = entry.presence.get() * entry.weight.get()
|
||||||
if (presence > bestPresence) {
|
if (presence > bestPresence) {
|
||||||
best = entry.id
|
best = entry.id
|
||||||
bestPresence = presence
|
bestPresence = presence
|
||||||
@@ -127,9 +133,9 @@ export function Provider({children}: {children: React.ReactNode}) {
|
|||||||
|
|
||||||
const actions = useMemo<ActionsContext>(
|
const actions = useMemo<ActionsContext>(
|
||||||
() => ({
|
() => ({
|
||||||
register(presence, config) {
|
register(presence, weight, config) {
|
||||||
const id = nextId++
|
const id = nextId++
|
||||||
setEntries(prev => [...prev, {id, presence, config}])
|
setEntries(prev => [...prev, {id, presence, weight, config}])
|
||||||
return id
|
return id
|
||||||
},
|
},
|
||||||
update(id, config) {
|
update(id, config) {
|
||||||
@@ -192,30 +198,20 @@ function useComposePromptActions() {
|
|||||||
export function useComposePromptForScreen(config: ComposePromptConfig | null) {
|
export function useComposePromptForScreen(config: ComposePromptConfig | null) {
|
||||||
const {register, update, unregister} = useComposePromptActions()
|
const {register, update, unregister} = useComposePromptActions()
|
||||||
const {presence} = useScreenPresence()
|
const {presence} = useScreenPresence()
|
||||||
const contribution = useSharedValue(0)
|
const weight = useSharedValue(0)
|
||||||
const idRef = useRef<number | null>(null)
|
const idRef = useRef<number | null>(null)
|
||||||
const enabled = config !== null
|
const enabled = config !== null
|
||||||
|
|
||||||
// the bar's top border would cut across the pill's gradient
|
// the bar's top border would cut across the pill's gradient
|
||||||
useHideBottomBarBorderForScreen({enabled})
|
useHideBottomBarBorderForScreen({enabled})
|
||||||
|
|
||||||
useAnimatedReaction(
|
|
||||||
() => presence.get(),
|
|
||||||
(current, previous) => {
|
|
||||||
if (enabled && current !== previous) {
|
|
||||||
contribution.set(current)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[enabled],
|
|
||||||
)
|
|
||||||
|
|
||||||
const getConfig = useEffectEvent(() => config)
|
const getConfig = useEffectEvent(() => config)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initial = getConfig()
|
const initial = getConfig()
|
||||||
if (!initial) return
|
if (!initial) return
|
||||||
contribution.set(presence.get())
|
weight.set(1)
|
||||||
const id = register(contribution, initial)
|
const id = register(presence, weight, initial)
|
||||||
idRef.current = id
|
idRef.current = id
|
||||||
return () => {
|
return () => {
|
||||||
idRef.current = null
|
idRef.current = null
|
||||||
@@ -223,7 +219,7 @@ export function useComposePromptForScreen(config: ComposePromptConfig | null) {
|
|||||||
* Fade out before removing so that a screen removed without a
|
* Fade out before removing so that a screen removed without a
|
||||||
* transition (or a config switched off) does not snap the pill away.
|
* transition (or a config switched off) does not snap the pill away.
|
||||||
*/
|
*/
|
||||||
contribution.set(
|
weight.set(
|
||||||
withSpring(0, SHELL_SPRING_CONFIG, finished => {
|
withSpring(0, SHELL_SPRING_CONFIG, finished => {
|
||||||
if (finished) {
|
if (finished) {
|
||||||
scheduleOnRN(unregister, id)
|
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 label = config?.label
|
||||||
const accessibilityLabel = config?.accessibilityLabel
|
const accessibilityLabel = config?.accessibilityLabel
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
|||||||
import {type ComposerLogContext} from '#/state/shell/composer'
|
import {type ComposerLogContext} from '#/state/shell/composer'
|
||||||
import {type ComposePromptConfig, useComposePromptForScreen} from './context'
|
import {type ComposePromptConfig, useComposePromptForScreen} from './context'
|
||||||
|
|
||||||
|
export {ComposePromptGradient} from './ComposePromptGradient'
|
||||||
export {ComposePromptPill} from './ComposePromptPill'
|
export {ComposePromptPill} from './ComposePromptPill'
|
||||||
export {
|
export {
|
||||||
type ComposePromptConfig,
|
type ComposePromptConfig,
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ import * as Toast from '#/components/Toast'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {ComposePromptPill} from '#/features/composePrompt'
|
import {
|
||||||
|
ComposePromptGradient,
|
||||||
|
ComposePromptPill,
|
||||||
|
} from '#/features/composePrompt'
|
||||||
import {useActorStatus} from '#/features/liveNow'
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
import {useDemoMode} from '#/storage/hooks/demo-mode'
|
import {useDemoMode} from '#/storage/hooks/demo-mode'
|
||||||
import {styles} from './BottomBarStyles'
|
import {styles} from './BottomBarStyles'
|
||||||
@@ -174,6 +177,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
|||||||
onLayout={e => {
|
onLayout={e => {
|
||||||
footerHeight.set(e.nativeEvent.layout.height)
|
footerHeight.set(e.nativeEvent.layout.height)
|
||||||
}}>
|
}}>
|
||||||
|
{hasSession && <ComposePromptGradient />}
|
||||||
{hasSession ? (
|
{hasSession ? (
|
||||||
<>
|
<>
|
||||||
<Btn
|
<Btn
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ import {
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {ComposePromptPill} from '#/features/composePrompt'
|
import {
|
||||||
|
ComposePromptGradient,
|
||||||
|
ComposePromptPill,
|
||||||
|
} from '#/features/composePrompt'
|
||||||
import {styles} from './BottomBarStyles'
|
import {styles} from './BottomBarStyles'
|
||||||
|
|
||||||
type NavItemValue = 'home' | 'search' | 'chat' | 'notifications' | 'profile'
|
type NavItemValue = 'home' | 'search' | 'chat' | 'notifications' | 'profile'
|
||||||
@@ -98,6 +101,7 @@ export function BottomBarWeb() {
|
|||||||
footerMinimalShellTransform,
|
footerMinimalShellTransform,
|
||||||
]}
|
]}
|
||||||
onLayout={event => footerHeight.set(event.nativeEvent.layout.height)}>
|
onLayout={event => footerHeight.set(event.nativeEvent.layout.height)}>
|
||||||
|
{hasSession && <ComposePromptGradient />}
|
||||||
{hasSession ? (
|
{hasSession ? (
|
||||||
<>
|
<>
|
||||||
<NavItem routeName="Home" href="/" navItem="home">
|
<NavItem routeName="Home" href="/" navItem="home">
|
||||||
|
|||||||
Reference in New Issue
Block a user