From 66ff6d71538ad5e63168c6e8201e65ae061eb6a0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 9 Sep 2026 15:22:26 -0700 Subject: [PATCH] fix(mobile): prevent text leaking through Android glass --- .../src/components/AndroidAnchoredMenu.tsx | 16 +----- apps/mobile/src/components/GlassBackdrop.tsx | 53 +++++++++++++++++++ apps/mobile/src/components/GlassSurface.tsx | 30 ++--------- 3 files changed, 58 insertions(+), 41 deletions(-) create mode 100644 apps/mobile/src/components/GlassBackdrop.tsx diff --git a/apps/mobile/src/components/AndroidAnchoredMenu.tsx b/apps/mobile/src/components/AndroidAnchoredMenu.tsx index b4e545fade71..1a4f11b8c7e0 100644 --- a/apps/mobile/src/components/AndroidAnchoredMenu.tsx +++ b/apps/mobile/src/components/AndroidAnchoredMenu.tsx @@ -1,5 +1,4 @@ import type { MenuAction, MenuComponentProps } from "@react-native-menu/menu"; -import { BlurView } from "expo-blur"; import type { ReactNode } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import type { StyleProp, ViewStyle } from "react-native"; @@ -8,11 +7,11 @@ import { useKeyboardState } from "react-native-keyboard-controller"; import Animated, { FadeIn } from "react-native-reanimated"; import { appBlurTargetRef } from "../lib/appBlurTarget"; -import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; import { cn } from "../lib/cn"; import { type AppSymbolName, SymbolView } from "./AppSymbol"; import { AppText as Text } from "./AppText"; import { OverlayPortal } from "./OverlayPortal"; +import { GlassBackdrop } from "./GlassBackdrop"; const MENU_WIDTH = 250; const SCREEN_MARGIN = 12; @@ -79,8 +78,6 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) { const anchorRef = useRef(null); const overlayRef = useRef(null); - const { themeAppearance } = useAppearancePreferences(); - const isDarkMode = themeAppearance === "dark"; const keyboardVisible = useKeyboardState((state) => state.isVisible); const keyboardHeight = useKeyboardState((state) => state.height); const close = useCallback(() => { @@ -227,16 +224,7 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) { : { bottom: (rootHeight ?? 0) - local.y + ANCHOR_GAP }), }} > - {/* Frosted backdrop: blur of the app content behind the menu, - washed with the translucent card tone so rows keep contrast. */} - - + {/* keyboardShouldPersistTaps: the menu often opens over an active editor; the first item tap must act, not just dismiss the keyboard. */} diff --git a/apps/mobile/src/components/GlassBackdrop.tsx b/apps/mobile/src/components/GlassBackdrop.tsx new file mode 100644 index 000000000000..f18ee7908db6 --- /dev/null +++ b/apps/mobile/src/components/GlassBackdrop.tsx @@ -0,0 +1,53 @@ +import { BlurView } from "expo-blur"; +import { useContext, type RefObject } from "react"; +import { Platform, StyleSheet, View, type ColorValue } from "react-native"; + +import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; +import { GlassBlurTargetContext } from "../lib/glassBlurTarget"; +import { themeColorWithAlpha } from "../lib/mobileTheme"; + +/** Frosted backdrop for containers that clip their children to their shape. */ +export function GlassBackdrop(props: { + readonly fallbackColor?: ColorValue; + readonly blurTarget?: RefObject; +}) { + const { themeAppearance } = useAppearancePreferences(); + const inheritedBlurTarget = useContext(GlassBlurTargetContext); + const target = props.blurTarget ?? inheritedBlurTarget; + const supportsBlur = + Platform.OS === "ios" || + (Platform.OS === "android" && Platform.Version >= 31 && target !== undefined); + const colorStyle = + props.fallbackColor === undefined + ? undefined + : { backgroundColor: themeColorWithAlpha(String(props.fallbackColor), 1) }; + + return ( + <> + {/* Android samples a separate target. An opaque backing prevents any + transparent pixels in that sample from exposing the unblurred feed. + iOS samples its actual backdrop, so a backing there would hide it. */} + {Platform.OS === "android" ? ( + + ) : null} + {supportsBlur ? ( + + ) : null} + + + ); +} diff --git a/apps/mobile/src/components/GlassSurface.tsx b/apps/mobile/src/components/GlassSurface.tsx index 390863460df3..014a760588ab 100644 --- a/apps/mobile/src/components/GlassSurface.tsx +++ b/apps/mobile/src/components/GlassSurface.tsx @@ -1,9 +1,7 @@ -import { BlurView } from "expo-blur"; import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect"; -import { useContext, type ReactNode, type Ref, type RefObject } from "react"; +import type { ReactNode, Ref, RefObject } from "react"; import { Platform, - StyleSheet, useColorScheme, View, type ColorValue, @@ -13,8 +11,7 @@ import { import { withUniwind } from "uniwind"; import { cn } from "../lib/cn"; -import { GlassBlurTargetContext } from "../lib/glassBlurTarget"; -import { themeColorWithAlpha } from "../lib/mobileTheme"; +import { GlassBackdrop } from "./GlassBackdrop"; // Explicit mappings keep the native glassEffectStyle enum out of style-array conversion. const ThemedGlassView = withUniwind(GlassView, { @@ -51,13 +48,6 @@ export function GlassSurface({ ...props }: GlassSurfaceProps) { const isDarkMode = useColorScheme() === "dark"; - const inheritedBlurTarget = useContext(GlassBlurTargetContext); - const target = blurTarget ?? inheritedBlurTarget; - const supportsBlur = - Platform.OS === "ios" || - (Platform.OS === "android" && Platform.Version >= 31 && target !== undefined); - const backgroundColor = - fallbackColor === undefined ? undefined : themeColorWithAlpha(String(fallbackColor), 1); const supportsGlass = Platform.OS === "ios" && isGlassEffectAPIAvailable(); const surfaceStyle: ViewStyle = { borderRadius: 32, @@ -113,21 +103,7 @@ export function GlassSurface({ )} style={[surfaceStyle, style]} > - {supportsBlur ? ( - - ) : null} - + {children} );