Settle the compose pill: border follows the pill, less width change, light tint
- Blend the bar's top border away from the pill's own visibility rather than a per-screen border contribution. The latter was faded on a spring when a screen was popped, so the border flashed back mid-pop between two screens that both show the pill. - Give the current label a small head start when picking the most present screen, so the two cannot trade places repeatedly as they cross. - Grow the up-state side inset to 16pt so the pill changes width less on its way down to the safe-area inset. - Add a half-strength tint to the clear glass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QoggCVt9XgpKbjrTZjoJD9
This commit is contained in:
@@ -15,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} from '#/alf'
|
import {atoms as a, ios, tokens, useTheme, utils} 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'
|
||||||
@@ -28,9 +28,11 @@ import {useComposePromptMedia} from './useComposePromptMedia'
|
|||||||
|
|
||||||
const AVATAR_SIZE = 32
|
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. Once the bar
|
||||||
|
* hides, the inset grows to the bottom safe area for equal spacing, so the
|
||||||
|
* closer this is to that, the less the pill changes width on the way down.
|
||||||
*/
|
*/
|
||||||
const SIDE_MARGIN_UP = tokens.space.sm
|
const SIDE_MARGIN_UP = tokens.space.lg
|
||||||
/**
|
/**
|
||||||
* Gap between the pill and the top of the bottom bar.
|
* Gap between the pill and the top of the bottom bar.
|
||||||
*/
|
*/
|
||||||
@@ -145,6 +147,7 @@ export function ComposePromptPill() {
|
|||||||
style: glassVisible ? 'clear' : 'none',
|
style: glassVisible ? 'clear' : 'none',
|
||||||
animate: true,
|
animate: true,
|
||||||
}}
|
}}
|
||||||
|
tintColor={utils.alpha(t.palette.contrast_50, 0.5)}
|
||||||
style={[a.rounded_full]}
|
style={[a.rounded_full]}
|
||||||
fallbackStyle={[
|
fallbackStyle={[
|
||||||
a.border,
|
a.border,
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
import {scheduleOnRN} from 'react-native-worklets'
|
import {scheduleOnRN} from 'react-native-worklets'
|
||||||
|
|
||||||
import {SHELL_SPRING_CONFIG} from '#/lib/custom-animations/springs'
|
import {SHELL_SPRING_CONFIG} from '#/lib/custom-animations/springs'
|
||||||
import {useHideBottomBarBorderForScreen} from '#/lib/hooks/useHideBottomBarBorder'
|
|
||||||
import {
|
import {
|
||||||
useScreenCoverage,
|
useScreenCoverage,
|
||||||
useScreenPresence,
|
useScreenPresence,
|
||||||
@@ -101,10 +100,12 @@ const actionsContext = createContext<ActionsContext | null>(null)
|
|||||||
actionsContext.displayName = 'ComposePromptActionsContext'
|
actionsContext.displayName = 'ComposePromptActionsContext'
|
||||||
|
|
||||||
let nextId = 0
|
let nextId = 0
|
||||||
|
const LABEL_SWITCH_MARGIN = 0.1
|
||||||
|
|
||||||
export function Provider({children}: {children: React.ReactNode}) {
|
export function Provider({children}: {children: React.ReactNode}) {
|
||||||
const [entries, setEntries] = useState<Entry[]>([])
|
const [entries, setEntries] = useState<Entry[]>([])
|
||||||
const [activeId, setActiveId] = useState<number | null>(null)
|
const [activeId, setActiveId] = useState<number | null>(null)
|
||||||
|
const activeIdOnUI = useSharedValue<number | null>(null)
|
||||||
const coverage = useScreenCoverage()
|
const coverage = useScreenCoverage()
|
||||||
|
|
||||||
const visibility = useDerivedValue(() => {
|
const visibility = useDerivedValue(() => {
|
||||||
@@ -118,13 +119,17 @@ export function Provider({children}: {children: React.ReactNode}) {
|
|||||||
/*
|
/*
|
||||||
* The pill shows the label of whichever screen is the most present, so
|
* The pill shows the label of whichever screen is the most present, so
|
||||||
* that during a push or swipe-back the text switches at the midpoint of
|
* that during a push or swipe-back the text switches at the midpoint of
|
||||||
* the transition rather than when the outgoing screen unmounts.
|
* the transition rather than when the outgoing screen unmounts. The current
|
||||||
|
* label gets a small head start so the two cannot trade places repeatedly
|
||||||
|
* as they cross.
|
||||||
*/
|
*/
|
||||||
const mostPresentId = useDerivedValue(() => {
|
const mostPresentId = useDerivedValue(() => {
|
||||||
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 = entryPresence(entry, coverage.get())
|
const presence =
|
||||||
|
entryPresence(entry, coverage.get()) +
|
||||||
|
(entry.id === activeIdOnUI.get() ? LABEL_SWITCH_MARGIN : 0)
|
||||||
if (presence > bestPresence) {
|
if (presence > bestPresence) {
|
||||||
best = entry.id
|
best = entry.id
|
||||||
bestPresence = presence
|
bestPresence = presence
|
||||||
@@ -138,6 +143,7 @@ export function Provider({children}: {children: React.ReactNode}) {
|
|||||||
(current, previous) => {
|
(current, previous) => {
|
||||||
// keep the last label while everything fades out
|
// keep the last label while everything fades out
|
||||||
if (current !== previous && current !== null) {
|
if (current !== previous && current !== null) {
|
||||||
|
activeIdOnUI.set(current)
|
||||||
scheduleOnRN(setActiveId, current)
|
scheduleOnRN(setActiveId, current)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -223,9 +229,6 @@ export function useComposePromptForScreen(config: ComposePromptConfig | null) {
|
|||||||
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
|
|
||||||
useHideBottomBarBorderForScreen({enabled})
|
|
||||||
|
|
||||||
const getConfig = useEffectEvent(() => config)
|
const getConfig = useEffectEvent(() => config)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function useHideBottomBarBorderSetter() {
|
|||||||
* Hides the bottom bar's top border while the surrounding screen is present,
|
* Hides the bottom bar's top border while the surrounding screen is present,
|
||||||
* fading it with the screen transition.
|
* fading it with the screen transition.
|
||||||
*/
|
*/
|
||||||
export function useHideBottomBarBorderForScreen({enabled} = {enabled: true}) {
|
export function useHideBottomBarBorderForScreen() {
|
||||||
const register = useHideBottomBarBorderSetter()
|
const register = useHideBottomBarBorderSetter()
|
||||||
const {presence} = useScreenPresence()
|
const {presence} = useScreenPresence()
|
||||||
const contribution = useSharedValue(0)
|
const contribution = useSharedValue(0)
|
||||||
@@ -44,18 +44,13 @@ export function useHideBottomBarBorderForScreen({enabled} = {enabled: true}) {
|
|||||||
useAnimatedReaction(
|
useAnimatedReaction(
|
||||||
() => presence.get(),
|
() => presence.get(),
|
||||||
(current, previous) => {
|
(current, previous) => {
|
||||||
if (enabled && current !== previous) {
|
if (current !== previous) {
|
||||||
contribution.set(current)
|
contribution.set(current)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[enabled],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => register(contribution), [register, contribution])
|
||||||
if (!enabled) return
|
|
||||||
contribution.set(presence.get())
|
|
||||||
return register(contribution)
|
|
||||||
}, [enabled, register, contribution, presence])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,16 +68,17 @@ export function useHideBottomBarBorder() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Animated border color for the bottom bar, blending the border into the
|
* Animated border color for the bottom bar, blending the border into the
|
||||||
* background as screens that hide it come and go.
|
* background as screens that hide it come and go. `alsoHidden` is a further
|
||||||
|
* 0..1 hidden amount to combine in, for shell UI that sits on the bar.
|
||||||
*/
|
*/
|
||||||
export function useBottomBarBorderStyle() {
|
export function useBottomBarBorderStyle(alsoHidden?: SharedValue<number>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const hideBorder = useHideBottomBarBorder()
|
const hideBorder = useHideBottomBarBorder()
|
||||||
const visibleColor = t.atoms.border_contrast_low.borderColor
|
const visibleColor = t.atoms.border_contrast_low.borderColor
|
||||||
const hiddenColor = t.atoms.bg.backgroundColor
|
const hiddenColor = t.atoms.bg.backgroundColor
|
||||||
return useAnimatedStyle(() => ({
|
return useAnimatedStyle(() => ({
|
||||||
borderColor: interpolateColor(
|
borderColor: interpolateColor(
|
||||||
hideBorder.get(),
|
Math.max(hideBorder.get(), alsoHidden?.get() ?? 0),
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[visibleColor, hiddenColor],
|
[visibleColor, hiddenColor],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import {useAnalytics} from '#/analytics'
|
|||||||
import {
|
import {
|
||||||
ComposePromptGradient,
|
ComposePromptGradient,
|
||||||
ComposePromptPill,
|
ComposePromptPill,
|
||||||
|
useComposePromptState,
|
||||||
} from '#/features/composePrompt'
|
} 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'
|
||||||
@@ -84,7 +85,9 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
|||||||
const accountSwitchControl = useDialogControl()
|
const accountSwitchControl = useDialogControl()
|
||||||
const messagesMenuControl = Menu.useMenuControl()
|
const messagesMenuControl = Menu.useMenuControl()
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
const borderStyle = useBottomBarBorderStyle()
|
const {visibility: composePromptVisibility} = useComposePromptState()
|
||||||
|
// the border would cut across the compose pill's gradient
|
||||||
|
const borderStyle = useBottomBarBorderStyle(composePromptVisibility)
|
||||||
const iconWidth = 28
|
const iconWidth = 28
|
||||||
|
|
||||||
const showSignIn = useCallback(() => {
|
const showSignIn = useCallback(() => {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {useAnalytics} from '#/analytics'
|
|||||||
import {
|
import {
|
||||||
ComposePromptGradient,
|
ComposePromptGradient,
|
||||||
ComposePromptPill,
|
ComposePromptPill,
|
||||||
|
useComposePromptState,
|
||||||
} from '#/features/composePrompt'
|
} from '#/features/composePrompt'
|
||||||
import {styles} from './BottomBarStyles'
|
import {styles} from './BottomBarStyles'
|
||||||
|
|
||||||
@@ -61,7 +62,9 @@ export function BottomBarWeb() {
|
|||||||
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
||||||
const closeAllActiveElements = useCloseAllActiveElements()
|
const closeAllActiveElements = useCloseAllActiveElements()
|
||||||
const {footerHeight} = useShellLayout()
|
const {footerHeight} = useShellLayout()
|
||||||
const borderStyle = useBottomBarBorderStyle()
|
const {visibility: composePromptVisibility} = useComposePromptState()
|
||||||
|
// the border would cut across the compose pill's gradient
|
||||||
|
const borderStyle = useBottomBarBorderStyle(composePromptVisibility)
|
||||||
const accountSwitchControl = useDialogControl()
|
const accountSwitchControl = useDialogControl()
|
||||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||||
const iconWidth = 26
|
const iconWidth = 26
|
||||||
|
|||||||
Reference in New Issue
Block a user