This commit is contained in:
Marcos Rodriguez Velez 2025-04-12 20:08:08 -04:00
parent 1c05f6cd98
commit ef5322b6ca
11 changed files with 68 additions and 249 deletions

View File

@ -1,4 +1,4 @@
import { Dimensions, Platform, PixelRatio, AppState, AppStateStatus } from 'react-native';
import { Dimensions, Platform, AppState, AppStateStatus } from 'react-native';
import { useState, useEffect } from 'react';
import { isDesktop } from './environment';
@ -6,7 +6,7 @@ import { isDesktop } from './environment';
export enum SizeClass {
Compact, // Small size (iPhone width or height in landscape)
Regular, // Standard size (iPad, or iPhone height in portrait)
Large, // Additional size for larger screens (not in iOS, but useful for our app)
Large, // Additional size for larger screens (not in iOS, but useful for our app)
}
// Interface for the result of getSizeClass
@ -14,17 +14,17 @@ export interface SizeClassInfo {
// Size classes
horizontalSizeClass: SizeClass;
verticalSizeClass: SizeClass;
// Overall size class (derived from horizontal and vertical)
sizeClass: SizeClass;
// Orientation
orientation: 'portrait' | 'landscape';
// Helper properties
isCompact: boolean;
isLarge: boolean;
// Legacy support
isLargeScreen: boolean;
}
@ -38,13 +38,12 @@ export function getSizeClass(): SizeClassInfo {
const { width, height } = Dimensions.get('window');
const isLandscape = width > height;
const orientation = isLandscape ? 'landscape' : 'portrait';
// Get density for more accurate sizing
const pixelDensity = PixelRatio.get();
// Determine horizontal size class (following iOS conventions)
let horizontalSizeClass: SizeClass;
if (Platform.OS === 'ios' && Platform.isPad) {
// iPads always have Regular width
horizontalSizeClass = SizeClass.Regular;
@ -59,10 +58,10 @@ export function getSizeClass(): SizeClassInfo {
// Regular iPhones: Compact width
horizontalSizeClass = SizeClass.Compact;
}
// Determine vertical size class (following iOS conventions)
let verticalSizeClass: SizeClass;
if (Platform.OS === 'ios' && Platform.isPad) {
// iPads always have Regular height
verticalSizeClass = SizeClass.Regular;
@ -76,31 +75,31 @@ export function getSizeClass(): SizeClassInfo {
// iPhones in portrait: Regular height
verticalSizeClass = SizeClass.Regular;
}
// Derive overall size class based on the user's specification:
// Regular width + any height = Large screen
let sizeClass: SizeClass;
if (horizontalSizeClass === SizeClass.Compact) {
sizeClass = SizeClass.Compact;
} else {
// If width is Regular or Large, overall size class is Large
sizeClass = SizeClass.Large;
}
// Determine isLargeScreen based on horizontal size class being at least Regular
const isLargeScreen = horizontalSizeClass !== SizeClass.Compact;
return {
horizontalSizeClass,
verticalSizeClass,
sizeClass,
orientation,
// Helper properties
isCompact: sizeClass === SizeClass.Compact,
isLarge: sizeClass === SizeClass.Large,
// Legacy support
isLargeScreen,
};
@ -111,7 +110,7 @@ export function getSizeClass(): SizeClassInfo {
*/
export function useSizeClass(): SizeClassInfo {
const [sizeClassInfo, setSizeClassInfo] = useState<SizeClassInfo>(getSizeClass());
useEffect(() => {
// Update size class when dimensions change
const updateSizeClass = () => {
@ -122,12 +121,12 @@ export function useSizeClass(): SizeClassInfo {
`horizontal=${SizeClass[newInfo.horizontalSizeClass]}`,
`vertical=${SizeClass[newInfo.verticalSizeClass]}`,
`orientation=${newInfo.orientation}`,
`isLargeScreen=${newInfo.isLargeScreen}`
`isLargeScreen=${newInfo.isLargeScreen}`,
);
};
const dimensionSubscription = Dimensions.addEventListener('change', updateSizeClass);
// Also update when app becomes active
const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === 'active') {
@ -136,16 +135,13 @@ export function useSizeClass(): SizeClassInfo {
};
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
// Clean up
return () => {
dimensionSubscription.remove();
appStateSubscription.remove();
};
}, []);
return sizeClassInfo;
}
// For backward compatibility
export const useIsLargeScreen = useSizeClass;

View File

@ -1,173 +0,0 @@
import React, { createContext, ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { AppState, AppStateStatus, Dimensions, Platform, useWindowDimensions } from 'react-native';
import { isDesktop } from '../../blue_modules/environment';
interface ILargeScreenContext {
isLargeScreen: boolean;
}
const DRAWER_WIDTH = 320;
const MIN_CONTENT_WIDTH = 375;
const MIN_CONTENT_HEIGHT_PORTRAIT = 500; // Height requirement in portrait mode
const MIN_CONTENT_HEIGHT_LANDSCAPE = 400;
const REQUIRED_WIDTH = DRAWER_WIDTH + MIN_CONTENT_WIDTH;
const WIDE_SCREEN_RATIO = 2.0; // Width/height ratio that indicates a wide screen (typical for large phones in landscape)
const useLargeScreenDetection = () => {
const dimensions = useWindowDimensions();
const previousValidWidthRef = useRef<number>(dimensions.width || 500);
const previousValidHeightRef = useRef<number>(dimensions.height || 800);
const [dimensionState, setDimensionState] = useState({
width: dimensions.width,
height: dimensions.height,
});
useEffect(() => {
const handleDimensionChange = ({ window }: { window: { width: number; height: number } }) => {
console.debug('[LargeScreen] Dimension changed:', window.width, window.height);
setDimensionState({ width: window.width, height: window.height });
};
const dimensionSubscription = Dimensions.addEventListener('change', handleDimensionChange);
return () => {
dimensionSubscription.remove();
};
}, []);
useEffect(() => {
if (dimensions.width > 0 && dimensions.height > 0) {
previousValidWidthRef.current = dimensions.width;
previousValidHeightRef.current = dimensions.height;
console.debug('[LargeScreen] Valid dimensions update:', dimensions.width, dimensions.height);
}
}, [dimensions.width, dimensions.height]);
useEffect(() => {
const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === 'active') {
const currentDimensions = Dimensions.get('window');
console.debug('[LargeScreen] App active, dimension check:', currentDimensions.width, currentDimensions.height);
if (currentDimensions.width > 0 && currentDimensions.height > 0) {
previousValidWidthRef.current = currentDimensions.width;
previousValidHeightRef.current = currentDimensions.height;
setDimensionState({
width: currentDimensions.width,
height: currentDimensions.height,
});
}
} else {
console.debug('[LargeScreen] App state changed to:', nextAppState);
}
};
console.debug('[LargeScreen] Setting up AppState subscription');
const subscription = AppState.addEventListener('change', handleAppStateChange);
// Trigger an initial check with current state
handleAppStateChange(AppState.currentState);
return () => {
console.debug('[LargeScreen] Cleaning up AppState subscription');
try {
subscription.remove();
} catch (error) {
console.warn('[LargeScreen] Error cleaning up AppState subscription:', error);
// Fallback for older React Native versions if needed
try {
// @ts-ignore - for backward compatibility
AppState.removeEventListener?.('change', handleAppStateChange);
} catch (fallbackError) {
console.error('[LargeScreen] Failed to clean up with fallback:', fallbackError);
}
}
};
}, []);
const isLargeScreen = useMemo(() => {
// Prioritize the state from dimension changes for more reliable rotation support
const effectiveWidth =
dimensionState.width > 0 ? dimensionState.width : dimensions.width > 0 ? dimensions.width : previousValidWidthRef.current;
const effectiveHeight =
dimensionState.height > 0 ? dimensionState.height : dimensions.height > 0 ? dimensions.height : previousValidHeightRef.current;
// For rotation cases, always use the larger dimension as "width" and smaller as "height"
const largerDimension = Math.max(effectiveWidth, effectiveHeight);
const smallerDimension = Math.min(effectiveWidth, effectiveHeight);
// In portrait, width is smaller; in landscape, height is smaller
const isLandscape = effectiveWidth > effectiveHeight;
// Calculate screen ratio - wide screens like large phones in landscape have high ratios
const screenRatio = largerDimension / smallerDimension;
const isWideScreen = screenRatio >= WIDE_SCREEN_RATIO;
// Use different height requirements based on orientation
const minRequiredHeight = isLandscape ? MIN_CONTENT_HEIGHT_LANDSCAPE : MIN_CONTENT_HEIGHT_PORTRAIT;
if (smallerDimension <= 375) {
console.debug(`[LargeScreen] Smaller dimension ${smallerDimension} <= 375, forcing isLargeScreen=false`);
return false;
}
// Special case for large phones in landscape mode (wide screens)
// They typically have plenty of width but might be slightly short on height
const isLargeLandscapePhone =
isLandscape &&
isWideScreen &&
largerDimension >= 900 && // Common for large phone models in landscape
smallerDimension >= 400; // Still need reasonable height
// For proper drawer display, require adequate width and height
const hasAdequateWidth = largerDimension >= REQUIRED_WIDTH;
const hasAdequateHeight = smallerDimension >= minRequiredHeight;
const isTabletOrDesktop = Platform.OS === 'ios' ? Platform.isPad || isDesktop : false;
const defaultValue = isTabletOrDesktop;
// Consider a device "large screen" if:
// 1. It has adequate width AND adequate height, OR
// 2. It's a large phone in landscape mode with sufficient dimensions, OR
// 3. It's a tablet/desktop by platform detection
const result = (hasAdequateWidth && hasAdequateHeight) || isLargeLandscapePhone || defaultValue;
console.debug(
`[LargeScreen] Calculation:`,
`dimensions=${effectiveWidth}x${effectiveHeight}`,
`isLandscape=${isLandscape}`,
`screenRatio=${screenRatio.toFixed(2)}`,
`isWideScreen=${isWideScreen}`,
`largerDim=${largerDimension}`,
`smallerDim=${smallerDimension}`,
`requiredWidth=${REQUIRED_WIDTH}`,
`requiredHeight=${minRequiredHeight}`,
`hasWidth=${hasAdequateWidth}`,
`hasHeight=${hasAdequateHeight}`,
`isLargeLandscapePhone=${isLargeLandscapePhone}`,
`result=${result}`,
`default=${defaultValue}`,
);
return result;
}, [dimensions.width, dimensions.height, dimensionState.width, dimensionState.height]);
return { isLargeScreen };
};
type LargeScreenProviderProps = {
children: ReactNode;
};
export const LargeScreenContext = createContext<ILargeScreenContext>({
isLargeScreen: false,
});
export const LargeScreenProvider: React.FC<LargeScreenProviderProps> = ({ children }) => {
const { isLargeScreen } = useLargeScreenDetection();
const contextValue = useMemo(() => ({ isLargeScreen }), [isLargeScreen]);
return <LargeScreenContext.Provider value={contextValue}>{children}</LargeScreenContext.Provider>;
};

View File

@ -1,6 +1,7 @@
import React, { createContext, ReactNode, useEffect, useMemo, useState } from 'react';
import { AppState, AppStateStatus, Dimensions, Platform, useWindowDimensions } from 'react-native';
import { Dimensions, Platform, useWindowDimensions } from 'react-native';
import { isDesktop, isTablet } from '../../blue_modules/environment';
import useAppState from '../../hooks/useAppState';
export enum SizeClass {
Compact,
@ -70,30 +71,12 @@ const useSizeClassDetection = () => {
};
}, []);
const { currentAppState } = useAppState();
useEffect(() => {
const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === 'active') {
determineSize();
}
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
handleAppStateChange(AppState.currentState);
return () => {
console.debug('[SizeClass] Cleaning up AppState subscription');
try {
subscription.remove();
} catch (error) {
console.warn('[SizeClass] Error cleaning up AppState subscription:', error);
try {
AppState.removeEventListener?.('change', handleAppStateChange);
} catch (fallbackError) {
console.error('[SizeClass] Failed to clean up with fallback:', fallbackError);
}
}
};
}, []);
if (currentAppState === 'active') {
determineSize();
}
}, [currentAppState]);
const sizeClass = useMemo(() => {
if (

View File

@ -6,6 +6,7 @@ import { useTheme } from './themes';
interface SafeAreaProps extends ViewProps {
floatingButtonHeight?: number;
orientation?: 'portrait' | 'landscape';
}
const SafeArea = (props: SafeAreaProps) => {
@ -13,19 +14,32 @@ const SafeArea = (props: SafeAreaProps) => {
const { colors } = useTheme();
const insets = useSafeAreaInsets();
const padding = useMemo(
() =>
props.orientation === 'portrait'
? {
paddingTop: insets.top,
paddingBottom: insets.bottom,
}
: {
paddingTop: insets.left,
paddingBottom: insets.right + (floatingButtonHeight ?? 0),
paddingLeft: insets.top,
paddingRight: insets.bottom,
},
[insets, props.orientation, floatingButtonHeight],
);
const componentStyle = useMemo(() => {
return StyleSheet.compose(
{
flex: 1,
backgroundColor: colors.background,
paddingTop: insets.top,
paddingBottom: insets.bottom + (floatingButtonHeight ?? 0),
paddingLeft: insets.left,
paddingRight: insets.right,
...padding,
},
style,
);
}, [colors.background, style, insets, floatingButtonHeight]);
}, [colors.background, padding, style]);
return <View style={componentStyle} {...otherProps} />;
};

View File

@ -418,7 +418,14 @@ type FlatListRefType = FlatList<any> & {
getNativeScrollRef(): View;
};
const ListHeaderSeparator = () => <View style={{ width: 16, height: 20 }} />;
const styles = StyleSheet.create({
listHeaderSeparator: {
width: 16,
height: 20,
},
});
const ListHeaderSeparator = () => <View style={styles.listHeaderSeparator} />;
const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props, ref) => {
const {
@ -704,10 +711,6 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
contentLargeScreen: {
paddingHorizontal: sizeClass === SizeClass.Large ? 16 : 12,
},
separatorStyle: {
width: 16,
height: 20,
},
});
return isFlatList ? (

View File

@ -1,2 +0,0 @@
// Reexport from the new location for backward compatibility
export { SizeClass, useIsLargeScreen, useSizeClass } from './useSizeClass';

View File

@ -1,9 +1,9 @@
// This file reexports from the main sizeClass module for backward compatibility
import { useSizeClass as useSizeClassOriginal, SizeClass, useIsLargeScreen } from '../blue_modules/sizeClass';
import { useSizeClass as useSizeClassOriginal, SizeClass } from '../blue_modules/sizeClass';
import type { SizeClassInfo } from '../blue_modules/sizeClass';
export { SizeClass, useIsLargeScreen };
export { SizeClass };
export type { SizeClassInfo };
// Main hook
export const useSizeClass = useSizeClassOriginal;
export const useIsLargeScreen = useSizeClassOriginal;

View File

@ -12,6 +12,7 @@ export type ScanQRCodeParamList = {
onBarScanned?: (data: string) => void;
showFileImportButton?: boolean;
backdoorVisible?: boolean;
orientation?: 'portrait';
animatedQRCodeData?: Record<string, any>;
};

View File

@ -35,7 +35,6 @@ const DrawerRoot = () => {
const { sizeClass, isLargeScreen } = useSizeClass();
const getDrawerWidth = useMemo(() => {
// Use size class for more flexible drawer width
switch (sizeClass) {
case SizeClass.Large:
return 320;

View File

@ -15,14 +15,11 @@ const WalletsAddMultisig = lazy(() => import('../screen/wallets/WalletsAddMultis
const WalletsAddMultisigStep2 = lazy(() => import('../screen/wallets/addMultisigStep2'));
const WalletsAddMultisigHelp = lazy(() => import('../screen/wallets/addMultisigHelp'));
export const AddComponent: React.FC = () => {
console.log('Rendering AddComponent wrapper');
return (
<Suspense fallback={<LazyLoadingIndicator />}>
<WalletsAdd />
</Suspense>
);
};
export const AddComponent: React.FC = () => (
<Suspense fallback={<LazyLoadingIndicator />}>
<WalletsAdd />
</Suspense>
);
export const ImportWalletDiscoveryComponent = () => (
<Suspense fallback={<LazyLoadingIndicator />}>

View File

@ -195,6 +195,7 @@ const MainRoot = () => {
options={{
headerShown: false,
statusBarHidden: true,
orientation: 'portrait',
presentation: 'fullScreenModal',
}}
/>