authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-09 15:25:50-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-09 15:25:50-07:00
log383cc40f4d5d9f61d47b3caa28fac839a8b9c433
tree484db112fb69caf5e68249180cb4b3f6ca1de4ab
parent75e4ceb96489a2fe92c710c79dbea59778cccfd1
signature Signed by PGP key B5690EEEBB952194

fix(mobile): prevent text leaking through Android glass (#10998)


3 files changed, 58 insertions(+), 41 deletions(-)

apps/mobile/src/components/AndroidAnchoredMenu.tsx+2-14
......@@ -1,5 +1,4 @@
11import type { MenuAction, MenuComponentProps } from "@react-native-menu/menu";
2import { BlurView } from "expo-blur";
32import type { ReactNode } from "react";
43import { useCallback, useEffect, useRef, useState } from "react";
54import type { StyleProp, ViewStyle } from "react-native";
......@@ -8,11 +7,11 @@ import { useKeyboardState } from "react-native-keyboard-controller";
87import Animated, { FadeIn } from "react-native-reanimated";
98
109import { appBlurTargetRef } from "../lib/appBlurTarget";
11import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
1210import { cn } from "../lib/cn";
1311import { type AppSymbolName, SymbolView } from "./AppSymbol";
1412import { AppText as Text } from "./AppText";
1513import { OverlayPortal } from "./OverlayPortal";
14import { GlassBackdrop } from "./GlassBackdrop";
1615
1716const MENU_WIDTH = 250;
1817const SCREEN_MARGIN = 12;
......@@ -79,8 +78,6 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
7978 const anchorRef = useRef<View>(null);
8079 const overlayRef = useRef<View>(null);
8180
82 const { themeAppearance } = useAppearancePreferences();
83 const isDarkMode = themeAppearance === "dark";
8481 const keyboardVisible = useKeyboardState((state) => state.isVisible);
8582 const keyboardHeight = useKeyboardState((state) => state.height);
8683 const close = useCallback(() => {
......@@ -227,16 +224,7 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
227224 : { bottom: (rootHeight ?? 0) - local.y + ANCHOR_GAP }),
228225 }}
229226 >
230 {/* Frosted backdrop: blur of the app content behind the menu,
231 washed with the translucent card tone so rows keep contrast. */}
232 <BlurView
233 blurMethod="dimezisBlurView"
234 blurTarget={appBlurTargetRef}
235 intensity={40}
236 tint={isDarkMode ? "dark" : "light"}
237 className="absolute inset-0"
238 />
239 <View className="absolute inset-0 bg-card-translucent" />
227 <GlassBackdrop blurTarget={appBlurTargetRef} />
240228 {/* keyboardShouldPersistTaps: the menu often opens over an
241229 active editor; the first item tap must act, not just
242230 dismiss the keyboard. */}
apps/mobile/src/components/GlassBackdrop.tsx created+53
......@@ -0,0 +1,53 @@
1import { BlurView } from "expo-blur";
2import { useContext, type RefObject } from "react";
3import { Platform, StyleSheet, View, type ColorValue } from "react-native";
4
5import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
6import { GlassBlurTargetContext } from "../lib/glassBlurTarget";
7import { themeColorWithAlpha } from "../lib/mobileTheme";
8
9/** Frosted backdrop for containers that clip their children to their shape. */
10export function GlassBackdrop(props: {
11 readonly fallbackColor?: ColorValue;
12 readonly blurTarget?: RefObject<View | null>;
13}) {
14 const { themeAppearance } = useAppearancePreferences();
15 const inheritedBlurTarget = useContext(GlassBlurTargetContext);
16 const target = props.blurTarget ?? inheritedBlurTarget;
17 const supportsBlur =
18 Platform.OS === "ios" ||
19 (Platform.OS === "android" && Platform.Version >= 31 && target !== undefined);
20 const colorStyle =
21 props.fallbackColor === undefined
22 ? undefined
23 : { backgroundColor: themeColorWithAlpha(String(props.fallbackColor), 1) };
24
25 return (
26 <>
27 {/* Android samples a separate target. An opaque backing prevents any
28 transparent pixels in that sample from exposing the unblurred feed.
29 iOS samples its actual backdrop, so a backing there would hide it. */}
30 {Platform.OS === "android" ? (
31 <View pointerEvents="none" className="absolute inset-0 bg-card" style={colorStyle} />
32 ) : null}
33 {supportsBlur ? (
34 <BlurView
35 pointerEvents="none"
36 blurTarget={target}
37 blurMethod="dimezisBlurViewSdk31Plus"
38 intensity={80}
39 tint={themeAppearance === "dark" ? "dark" : "default"}
40 style={StyleSheet.absoluteFill}
41 />
42 ) : null}
43 <View
44 pointerEvents="none"
45 className="absolute inset-0 bg-card"
46 style={[
47 colorStyle,
48 { opacity: supportsBlur ? (themeAppearance === "dark" ? 0.25 : 0.55) : 1 },
49 ]}
50 />
51 </>
52 );
53}
apps/mobile/src/components/GlassSurface.tsx+3-27
......@@ -1,9 +1,7 @@
1import { BlurView } from "expo-blur";
21import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect";
3import { useContext, type ReactNode, type Ref, type RefObject } from "react";
2import type { ReactNode, Ref, RefObject } from "react";
43import {
54 Platform,
6 StyleSheet,
75 useColorScheme,
86 View,
97 type ColorValue,
......@@ -13,8 +11,7 @@ import {
1311import { withUniwind } from "uniwind";
1412
1513import { cn } from "../lib/cn";
16import { GlassBlurTargetContext } from "../lib/glassBlurTarget";
17import { themeColorWithAlpha } from "../lib/mobileTheme";
14import { GlassBackdrop } from "./GlassBackdrop";
1815
1916// Explicit mappings keep the native glassEffectStyle enum out of style-array conversion.
2017const ThemedGlassView = withUniwind(GlassView, {
......@@ -51,13 +48,6 @@ export function GlassSurface({
5148 ...props
5249}: GlassSurfaceProps) {
5350 const isDarkMode = useColorScheme() === "dark";
54 const inheritedBlurTarget = useContext(GlassBlurTargetContext);
55 const target = blurTarget ?? inheritedBlurTarget;
56 const supportsBlur =
57 Platform.OS === "ios" ||
58 (Platform.OS === "android" && Platform.Version >= 31 && target !== undefined);
59 const backgroundColor =
60 fallbackColor === undefined ? undefined : themeColorWithAlpha(String(fallbackColor), 1);
6151 const supportsGlass = Platform.OS === "ios" && isGlassEffectAPIAvailable();
6252 const surfaceStyle: ViewStyle = {
6353 borderRadius: 32,
......@@ -113,21 +103,7 @@ export function GlassSurface({
113103 )}
114104 style={[surfaceStyle, style]}
115105 >
116 {supportsBlur ? (
117 <BlurView
118 pointerEvents="none"
119 blurTarget={target}
120 blurMethod="dimezisBlurViewSdk31Plus"
121 intensity={80}
122 tint={isDarkMode ? "dark" : "default"}
123 style={StyleSheet.absoluteFill}
124 />
125 ) : null}
126 <View
127 pointerEvents="none"
128 className="absolute inset-0 bg-card"
129 style={{ backgroundColor, opacity: supportsBlur ? (isDarkMode ? 0.25 : 0.55) : 1 }}
130 />
106 <GlassBackdrop blurTarget={blurTarget} fallbackColor={fallbackColor} />
131107 {children}
132108 </View>
133109 );