From f2dd3a8b4b921be7011f7eaf87e64b3ea662a0cf Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:26:28 +0200 Subject: [PATCH] fix: float the notification prompt instead of pushing the list down The push onboarding card was laid out above the tab navigator. It arrives 1.5 s into a launch, so it pushed the whole mail list down, and it drew under the status bar because it ignored the top inset. It now floats above the tab bar like the webmail's card and the toasts, respecting the bottom inset, and fades and slides in (instantly when animations are off), so nothing else moves when it appears or goes. --- src/components/PushOnboardingPrompt.tsx | 113 ++++++++++++++++-------- 1 file changed, 78 insertions(+), 35 deletions(-) diff --git a/src/components/PushOnboardingPrompt.tsx b/src/components/PushOnboardingPrompt.tsx index 50c24984..c7bf6e1c 100644 --- a/src/components/PushOnboardingPrompt.tsx +++ b/src/components/PushOnboardingPrompt.tsx @@ -1,9 +1,11 @@ import React from 'react'; -import { View, Text, StyleSheet, Pressable } from 'react-native'; +import { View, Text, StyleSheet, Pressable, Animated, Easing } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { BellRing, X } from 'lucide-react-native'; import Button from './Button'; import { spacing, radius, typography, type ThemePalette } from '../theme/tokens'; import { useColors } from '../theme/colors'; +import { useAnimDuration } from '../theme/dynamic'; import { useAuthStore } from '../stores/auth-store'; import { useLocaleStore } from '../stores/locale-store'; import { useSettingsStore } from '../stores/settings-store'; @@ -22,12 +24,18 @@ import { // Mirrors the webmail's push-notification-prompt: a short delay after login // so the inbox renders first, and never again for an account once dismissed. const PROMPT_DELAY_MS = 1500; +// Clears the tab bar, like the toasts (ToastHost). +const TAB_BAR_CLEARANCE = 64; /** * Once-per-account invitation to turn on background notifications. Shown * only where push can actually work (Android with Play services), when the * user has not disabled mail notifications, and until either "Enable" or * "Not now" has been tapped for the active account. + * + * It floats above the tab bar and slides in, like the webmail's card, rather + * than taking space at the top: arriving 1.5 s into a launch it used to push + * the whole mail list down, and it drew under the status bar. */ export function PushOnboardingPrompt(): React.ReactElement | null { const c = useColors(); @@ -42,6 +50,9 @@ export function PushOnboardingPrompt(): React.ReactElement | null { const [visible, setVisible] = React.useState(false); const [busy, setBusy] = React.useState(false); const [error, setError] = React.useState(null); + const insets = useSafeAreaInsets(); + const duration = useAnimDuration(200); + const appear = React.useRef(new Animated.Value(0)).current; React.useEffect(() => { setVisible(false); @@ -63,6 +74,17 @@ export function PushOnboardingPrompt(): React.ReactElement | null { }; }, [isAuthenticated, client, activeAccountId, emailNotificationsEnabled]); + React.useEffect(() => { + if (!visible) return; + appear.setValue(0); + Animated.timing(appear, { + toValue: 1, + duration, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }).start(); + }, [visible, appear, duration]); + if (!visible || !activeAccountId) return null; const handleDismiss = () => { @@ -90,56 +112,77 @@ export function PushOnboardingPrompt(): React.ReactElement | null { }; return ( - - - - - - - {t('push_prompt.title', 'Get notified about new mail')} - - - {t( - 'push_prompt.body', - 'Turn on background notifications so new messages reach you while the app is closed.', - )} - - {error && {error}} - - - + + + + + + + {t('push_prompt.title', 'Get notified about new mail')} + + + {t( + 'push_prompt.body', + 'Turn on background notifications so new messages reach you while the app is closed.', + )} + + {error && {error}} + + + + + + + + - - - - + ); } function makeStyles(c: ThemePalette) { return StyleSheet.create({ + wrap: { + position: 'absolute', + left: spacing.md, + right: spacing.md, + zIndex: 50, + }, card: { flexDirection: 'row', alignItems: 'flex-start', gap: spacing.md, - marginHorizontal: spacing.md, - marginTop: spacing.sm, padding: spacing.md, borderRadius: radius.md, borderWidth: 1, borderColor: c.border, backgroundColor: c.card, + shadowColor: '#000', + shadowOpacity: 0.25, + shadowRadius: 10, + shadowOffset: { width: 0, height: 4 }, + elevation: 6, }, iconWrap: { width: 36,