| author | |
| committer | |
| log | 383cc40f4d5d9f61d47b3caa28fac839a8b9c433 |
| tree | 484db112fb69caf5e68249180cb4b3f6ca1de4ab |
| parent | 75e4ceb96489a2fe92c710c79dbea59778cccfd1 |
| signature | Signed by PGP key B5690EEEBB952194 |
3 files changed, 58 insertions(+), 41 deletions(-)
apps/mobile/src/components/AndroidAnchoredMenu.tsx+2-14| ... | @@ -1,5 +1,4 @@ | ... | @@ -1,5 +1,4 @@ |
| 1 | import type { MenuAction, MenuComponentProps } from "@react-native-menu/menu"; | 1 | import type { MenuAction, MenuComponentProps } from "@react-native-menu/menu"; |
| 2 | import { BlurView } from "expo-blur"; | ||
| 3 | import type { ReactNode } from "react"; | 2 | import type { ReactNode } from "react"; |
| 4 | import { useCallback, useEffect, useRef, useState } from "react"; | 3 | import { useCallback, useEffect, useRef, useState } from "react"; |
| 5 | import type { StyleProp, ViewStyle } from "react-native"; | 4 | import 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"; |
| 8 | import Animated, { FadeIn } from "react-native-reanimated"; | 7 | import Animated, { FadeIn } from "react-native-reanimated"; |
| 9 | 8 | ||
| 10 | import { appBlurTargetRef } from "../lib/appBlurTarget"; | 9 | import { appBlurTargetRef } from "../lib/appBlurTarget"; |
| 11 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; | ||
| 12 | import { cn } from "../lib/cn"; | 10 | import { cn } from "../lib/cn"; |
| 13 | import { type AppSymbolName, SymbolView } from "./AppSymbol"; | 11 | import { type AppSymbolName, SymbolView } from "./AppSymbol"; |
| 14 | import { AppText as Text } from "./AppText"; | 12 | import { AppText as Text } from "./AppText"; |
| 15 | import { OverlayPortal } from "./OverlayPortal"; | 13 | import { OverlayPortal } from "./OverlayPortal"; |
| 14 | import { GlassBackdrop } from "./GlassBackdrop"; | ||
| 16 | 15 | ||
| 17 | const MENU_WIDTH = 250; | 16 | const MENU_WIDTH = 250; |
| 18 | const SCREEN_MARGIN = 12; | 17 | const 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); |
| 81 | 80 | ||
| 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 an | 228 | {/* keyboardShouldPersistTaps: the menu often opens over an |
| 241 | active editor; the first item tap must act, not just | 229 | 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 @@ | ||
| 1 | import { BlurView } from "expo-blur"; | ||
| 2 | import { useContext, type RefObject } from "react"; | ||
| 3 | import { Platform, StyleSheet, View, type ColorValue } from "react-native"; | ||
| 4 | |||
| 5 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; | ||
| 6 | import { GlassBlurTargetContext } from "../lib/glassBlurTarget"; | ||
| 7 | import { themeColorWithAlpha } from "../lib/mobileTheme"; | ||
| 8 | |||
| 9 | /** Frosted backdrop for containers that clip their children to their shape. */ | ||
| 10 | export 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 @@ |
| 1 | import { BlurView } from "expo-blur"; | ||
| 2 | import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect"; | 1 | import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect"; |
| 3 | import { useContext, type ReactNode, type Ref, type RefObject } from "react"; | 2 | import type { ReactNode, Ref, RefObject } from "react"; |
| 4 | import { | 3 | import { |
| 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 { |
| 13 | import { withUniwind } from "uniwind"; | 11 | import { withUniwind } from "uniwind"; |
| 14 | 12 | ||
| 15 | import { cn } from "../lib/cn"; | 13 | import { cn } from "../lib/cn"; |
| 16 | import { GlassBlurTargetContext } from "../lib/glassBlurTarget"; | 14 | import { GlassBackdrop } from "./GlassBackdrop"; |
| 17 | import { themeColorWithAlpha } from "../lib/mobileTheme"; | ||
| 18 | 15 | ||
| 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. |
| 20 | const ThemedGlassView = withUniwind(GlassView, { | 17 | const ThemedGlassView = withUniwind(GlassView, { |
| ... | @@ -51,13 +48,6 @@ export function GlassSurface({ | ... | @@ -51,13 +48,6 @@ export function GlassSurface({ |
| 51 | ...props | 48 | ...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 | ); |