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 @@...@@ -1,5 +1,4 @@
1import type { MenuAction, MenuComponentProps } from "@react-native-menu/menu";1import type { MenuAction, MenuComponentProps } from "@react-native-menu/menu";
2import { BlurView } from "expo-blur";
3import type { ReactNode } from "react";2import type { ReactNode } from "react";
4import { useCallback, useEffect, useRef, useState } from "react";3import { useCallback, useEffect, useRef, useState } from "react";
5import type { StyleProp, ViewStyle } from "react-native";4import type { StyleProp, ViewStyle } from "react-native";
...@@ -8,11 +7,11 @@ import { useKeyboardState } from "react-native-keyboard-controller";...@@ -8,11 +7,11 @@ import { useKeyboardState } from "react-native-keyboard-controller";
8import Animated, { FadeIn } from "react-native-reanimated";7import Animated, { FadeIn } from "react-native-reanimated";
98
10import { appBlurTargetRef } from "../lib/appBlurTarget";9import { appBlurTargetRef } from "../lib/appBlurTarget";
11import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
12import { cn } from "../lib/cn";10import { cn } from "../lib/cn";
13import { type AppSymbolName, SymbolView } from "./AppSymbol";11import { type AppSymbolName, SymbolView } from "./AppSymbol";
14import { AppText as Text } from "./AppText";12import { AppText as Text } from "./AppText";
15import { OverlayPortal } from "./OverlayPortal";13import { OverlayPortal } from "./OverlayPortal";
14import { GlassBackdrop } from "./GlassBackdrop";
1615
17const MENU_WIDTH = 250;16const MENU_WIDTH = 250;
18const SCREEN_MARGIN = 12;17const SCREEN_MARGIN = 12;
...@@ -79,8 +78,6 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {...@@ -79,8 +78,6 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
79 const anchorRef = useRef<View>(null);78 const anchorRef = useRef<View>(null);
80 const overlayRef = useRef<View>(null);79 const overlayRef = useRef<View>(null);
8180
82 const { themeAppearance } = useAppearancePreferences();
83 const isDarkMode = themeAppearance === "dark";
84 const keyboardVisible = useKeyboardState((state) => state.isVisible);81 const keyboardVisible = useKeyboardState((state) => state.isVisible);
85 const keyboardHeight = useKeyboardState((state) => state.height);82 const keyboardHeight = useKeyboardState((state) => state.height);
86 const close = useCallback(() => {83 const close = useCallback(() => {
...@@ -227,16 +224,7 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {...@@ -227,16 +224,7 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
227 : { bottom: (rootHeight ?? 0) - local.y + ANCHOR_GAP }),224 : { bottom: (rootHeight ?? 0) - local.y + ANCHOR_GAP }),
228 }}225 }}
229 >226 >
230 {/* Frosted backdrop: blur of the app content behind the menu,227 <GlassBackdrop blurTarget={appBlurTargetRef} />
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" />
240 {/* keyboardShouldPersistTaps: the menu often opens over an228 {/* keyboardShouldPersistTaps: the menu often opens over an
241 active editor; the first item tap must act, not just229 active editor; the first item tap must act, not just
242 dismiss the keyboard. */}230 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 @@...@@ -1,9 +1,7 @@
1import { BlurView } from "expo-blur";
2import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect";1import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect";
3import { useContext, type ReactNode, type Ref, type RefObject } from "react";2import type { ReactNode, Ref, RefObject } from "react";
4import {3import {
5 Platform,4 Platform,
6 StyleSheet,
7 useColorScheme,5 useColorScheme,
8 View,6 View,
9 type ColorValue,7 type ColorValue,
...@@ -13,8 +11,7 @@ import {...@@ -13,8 +11,7 @@ import {
13import { withUniwind } from "uniwind";11import { withUniwind } from "uniwind";
1412
15import { cn } from "../lib/cn";13import { cn } from "../lib/cn";
16import { GlassBlurTargetContext } from "../lib/glassBlurTarget";14import { GlassBackdrop } from "./GlassBackdrop";
17import { themeColorWithAlpha } from "../lib/mobileTheme";
1815
19// Explicit mappings keep the native glassEffectStyle enum out of style-array conversion.16// Explicit mappings keep the native glassEffectStyle enum out of style-array conversion.
20const ThemedGlassView = withUniwind(GlassView, {17const ThemedGlassView = withUniwind(GlassView, {
...@@ -51,13 +48,6 @@ export function GlassSurface({...@@ -51,13 +48,6 @@ export function GlassSurface({
51 ...props48 ...props
52}: GlassSurfaceProps) {49}: GlassSurfaceProps) {
53 const isDarkMode = useColorScheme() === "dark";50 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);
61 const supportsGlass = Platform.OS === "ios" && isGlassEffectAPIAvailable();51 const supportsGlass = Platform.OS === "ios" && isGlassEffectAPIAvailable();
62 const surfaceStyle: ViewStyle = {52 const surfaceStyle: ViewStyle = {
63 borderRadius: 32,53 borderRadius: 32,
...@@ -113,21 +103,7 @@ export function GlassSurface({...@@ -113,21 +103,7 @@ export function GlassSurface({
113 )}103 )}
114 style={[surfaceStyle, style]}104 style={[surfaceStyle, style]}
115 >105 >
116 {supportsBlur ? (106 <GlassBackdrop blurTarget={blurTarget} fallbackColor={fallbackColor} />
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 />
131 {children}107 {children}
132 </View>108 </View>
133 );109 );