Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32d3f77f4f | ||
|
|
f26ff9189c | ||
|
|
1fa290652c | ||
|
|
099f6f46a6 | ||
|
|
01a11bc8dd |
2
Gemfile
2
Gemfile
@ -7,7 +7,7 @@ gem "fastlane", "~> 2.234.0"
|
||||
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
|
||||
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
|
||||
gem 'xcodeproj', '< 1.26.0'
|
||||
gem 'concurrent-ruby', '< 1.3.4'
|
||||
gem 'concurrent-ruby', '< 1.3.8'
|
||||
|
||||
# Ruby 3.4.0 removed these from the standard library
|
||||
gem 'bigdecimal'
|
||||
|
||||
@ -87,7 +87,7 @@ GEM
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
concurrent-ruby (1.3.3)
|
||||
concurrent-ruby (1.3.7)
|
||||
connection_pool (3.0.2)
|
||||
csv (3.3.5)
|
||||
declarative (0.0.20)
|
||||
@ -337,7 +337,7 @@ DEPENDENCIES
|
||||
benchmark
|
||||
bigdecimal
|
||||
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
|
||||
concurrent-ruby (< 1.3.4)
|
||||
concurrent-ruby (< 1.3.8)
|
||||
fastlane (~> 2.234.0)
|
||||
fastlane-plugin-browserstack
|
||||
fastlane-plugin-bugsnag
|
||||
@ -377,7 +377,7 @@ CHECKSUMS
|
||||
colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c
|
||||
colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a
|
||||
commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9
|
||||
concurrent-ruby (1.3.3) sha256=4f9cd28965c4dcf83ffd3ea7304f9323277be8525819cb18a3b61edcb56a7c6a
|
||||
concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0
|
||||
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
||||
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
|
||||
declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9
|
||||
|
||||
@ -52,7 +52,14 @@ const useFloatButtonAnimation = (initialHeight: number) => {
|
||||
};
|
||||
};
|
||||
|
||||
const useFloatButtonLayout = (width: number, sizeClass: SizeClass) => {
|
||||
const getScaledButtonHeight = (fontScale: number): number => Math.round(LAYOUT.BUTTON_HEIGHT * fontScale);
|
||||
|
||||
/** Scroll padding so list content clears float buttons (excludes safe-area inset). Default 70 at fontScale 1. */
|
||||
const FLOAT_BUTTON_LIST_CLEARANCE = 18;
|
||||
|
||||
export const getFloatingButtonReservedHeight = (fontScale = 1): number => getScaledButtonHeight(fontScale) + FLOAT_BUTTON_LIST_CLEARANCE;
|
||||
|
||||
const useFloatButtonLayout = (width: number, sizeClass: SizeClass, fontScale: number) => {
|
||||
const lastVerticalDecision = useRef(false);
|
||||
|
||||
const shouldUseVerticalLayout = useCallback(
|
||||
@ -152,15 +159,19 @@ const useFloatButtonLayout = (width: number, sizeClass: SizeClass) => {
|
||||
[width, sizeClass, shouldUseVerticalLayout],
|
||||
);
|
||||
|
||||
const calculateContainerHeight = useCallback((childrenCount: number, isVerticalLayout: boolean) => {
|
||||
if (!isVerticalLayout) return { height: '8%', minHeight: LAYOUT.BUTTON_HEIGHT };
|
||||
const calculateContainerHeight = useCallback(
|
||||
(childrenCount: number, isVerticalLayout: boolean) => {
|
||||
const buttonHeight = getScaledButtonHeight(fontScale);
|
||||
if (!isVerticalLayout) return { height: '8%', minHeight: buttonHeight };
|
||||
|
||||
const totalButtonsHeight = childrenCount * LAYOUT.BUTTON_HEIGHT;
|
||||
const totalMarginsHeight = (childrenCount - 1) * LAYOUT.BUTTON_MARGIN;
|
||||
const calculatedHeight = totalButtonsHeight + totalMarginsHeight;
|
||||
const totalButtonsHeight = childrenCount * buttonHeight;
|
||||
const totalMarginsHeight = (childrenCount - 1) * LAYOUT.BUTTON_MARGIN;
|
||||
const calculatedHeight = totalButtonsHeight + totalMarginsHeight;
|
||||
|
||||
return { height: calculatedHeight };
|
||||
}, []);
|
||||
return { height: calculatedHeight };
|
||||
},
|
||||
[fontScale],
|
||||
);
|
||||
|
||||
const calculateButtonFontSize = useMemo(() => {
|
||||
const divisor = sizeClass === SizeClass.Large ? 22 : sizeClass === SizeClass.Regular ? 24 : 28;
|
||||
@ -267,6 +278,7 @@ interface FButtonProps {
|
||||
isVertical?: boolean;
|
||||
borderRadius?: number;
|
||||
fontSize?: number;
|
||||
buttonHeight?: number;
|
||||
disabled?: boolean;
|
||||
testID?: string;
|
||||
onPress: () => void;
|
||||
@ -277,13 +289,14 @@ interface ButtonContentProps {
|
||||
icon: ReactNode;
|
||||
text: string;
|
||||
textStyle: StyleProp<TextStyle>;
|
||||
buttonHeight: number;
|
||||
}
|
||||
|
||||
const getScaledIconSize = (fontSize: number): number => {
|
||||
return Math.max(Math.round(fontSize * 1.2), 16);
|
||||
};
|
||||
|
||||
const ButtonContent = ({ icon, text, textStyle }: ButtonContentProps) => {
|
||||
const ButtonContent = ({ icon, text, textStyle, buttonHeight }: ButtonContentProps) => {
|
||||
const computedStyle = StyleSheet.flatten(textStyle);
|
||||
const fontSize = computedStyle.fontSize || LAYOUT.MAX_BUTTON_FONT_SIZE;
|
||||
const iconSize = getScaledIconSize(Number(fontSize));
|
||||
@ -307,9 +320,14 @@ const ButtonContent = ({ icon, text, textStyle }: ButtonContentProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={buttonContentStaticStyles.contentContainer}>
|
||||
<View style={[buttonContentStaticStyles.contentContainer, { minHeight: buttonHeight }]}>
|
||||
<View style={buttonStyles.iconContainer}>{scaledIcon}</View>
|
||||
<Text numberOfLines={1} adjustsFontSizeToFit style={[textStyle, buttonStyles.centeredText, { lineHeight: fontSize * 1.2 }]}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.8}
|
||||
style={[textStyle, buttonStyles.centeredText, { lineHeight: fontSize * 1.2 }]}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
@ -325,6 +343,7 @@ export const FButton = ({
|
||||
isVertical,
|
||||
borderRadius = LAYOUT.PILL_BORDER_RADIUS,
|
||||
fontSize = LAYOUT.MAX_BUTTON_FONT_SIZE,
|
||||
buttonHeight = LAYOUT.BUTTON_HEIGHT,
|
||||
testID,
|
||||
...props
|
||||
}: FButtonProps) => {
|
||||
@ -347,6 +366,8 @@ export const FButton = ({
|
||||
return {
|
||||
root: {
|
||||
...baseStyles,
|
||||
height: buttonHeight,
|
||||
minHeight: buttonHeight,
|
||||
backgroundColor: colors.buttonBackgroundColor,
|
||||
},
|
||||
text: {
|
||||
@ -360,7 +381,7 @@ export const FButton = ({
|
||||
marginBottom: buttonContentStaticStyles.marginBottom,
|
||||
textBase: buttonContentStaticStyles.textBase,
|
||||
};
|
||||
}, [colors, fontSize]);
|
||||
}, [colors, fontSize, buttonHeight]);
|
||||
|
||||
const style: Record<string, any> = {};
|
||||
const additionalStyles = !last ? (isVertical ? customButtonStyles.marginBottom : customButtonStyles.marginRight) : {};
|
||||
@ -397,7 +418,7 @@ export const FButton = ({
|
||||
style={[buttonStyles.root, customButtonStyles.root, style, { borderRadius }]}
|
||||
{...props}
|
||||
>
|
||||
<ButtonContent icon={icon} text={text} textStyle={textStyle} />
|
||||
<ButtonContent icon={icon} text={text} textStyle={textStyle} buttonHeight={buttonHeight} />
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
@ -405,8 +426,9 @@ export const FButton = ({
|
||||
|
||||
export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { height, width } = useWindowDimensions();
|
||||
const { height, width, fontScale } = useWindowDimensions();
|
||||
const { sizeClass } = useSizeClass();
|
||||
const scaledButtonHeight = getScaledButtonHeight(fontScale);
|
||||
|
||||
const childrenCount = React.Children.toArray(props.children).filter(Boolean).length;
|
||||
|
||||
@ -419,6 +441,7 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
|
||||
const { calculateButtonWidth, calculateVisualParameters, calculateContainerHeight, buttonFontSize } = useFloatButtonLayout(
|
||||
width,
|
||||
sizeClass,
|
||||
fontScale,
|
||||
);
|
||||
|
||||
// Compute initial geometry up-front so the slide-in animation starts at the final (computed) size,
|
||||
@ -508,7 +531,7 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
|
||||
|
||||
useEffect(() => {
|
||||
debouncedCalculateLayout();
|
||||
}, [debouncedCalculateLayout, width, height, childrenCount, sizeClass]);
|
||||
}, [debouncedCalculateLayout, width, height, childrenCount, sizeClass, fontScale]);
|
||||
|
||||
const onLayout = (event: { nativeEvent: { layout: { width: number } } }) => {
|
||||
const { width: currentLayoutWidth } = event.nativeEvent.layout;
|
||||
@ -545,6 +568,7 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
|
||||
isVertical,
|
||||
borderRadius: buttonBorderRadius,
|
||||
fontSize: buttonFontSize,
|
||||
buttonHeight: scaledButtonHeight,
|
||||
});
|
||||
};
|
||||
|
||||
@ -561,10 +585,10 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
|
||||
props.inline ? containerStyles.rootInline : containerStyles.rootAbsolute,
|
||||
bottomInsets,
|
||||
effectiveNewWidth ? (isVertical ? containerStyles.rootPostVertical : containerStyles.rootPost) : containerStyles.rootPre,
|
||||
isVertical ? containerHeight : null,
|
||||
isVertical ? containerHeight : { minHeight: scaledButtonHeight },
|
||||
{ transform: [{ translateY: slideAnimation }] },
|
||||
],
|
||||
[props.inline, bottomInsets, effectiveNewWidth, isVertical, containerHeight, slideAnimation],
|
||||
[props.inline, bottomInsets, effectiveNewWidth, isVertical, containerHeight, slideAnimation, scaledButtonHeight],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Pressable, StyleProp, StyleSheet, Switch, SwitchProps, Text, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import { Pressable, StyleProp, StyleSheet, Switch, SwitchProps, Text, TextStyle, useWindowDimensions, View, ViewStyle } from 'react-native';
|
||||
import { useLocale } from '@react-navigation/native';
|
||||
|
||||
import Icon from './Icon';
|
||||
import { useTheme } from './themes';
|
||||
|
||||
/** Base row height for transaction list `getItemLayout` (padding + title + subtitle at fontScale 1). */
|
||||
export const TX_ROW_BASE_HEIGHT = 64;
|
||||
|
||||
interface ListItemProps {
|
||||
leftAvatar?: React.JSX.Element;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
@ -55,12 +58,20 @@ const ListItem: React.FC<ListItemProps> = React.memo(
|
||||
}: ListItemProps) => {
|
||||
const { colors } = useTheme();
|
||||
const { direction } = useLocale();
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const isRtl = direction === 'rtl';
|
||||
const contentRowStyle = useMemo(
|
||||
() => ({
|
||||
paddingVertical: Math.round(12 * fontScale),
|
||||
}),
|
||||
[fontScale],
|
||||
);
|
||||
const stylesHook = StyleSheet.create({
|
||||
title: {
|
||||
color: disabled ? colors.buttonDisabledTextColor : colors.foregroundColor,
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
lineHeight: Math.round(22 * fontScale),
|
||||
writingDirection: direction,
|
||||
},
|
||||
rightMemoText: {
|
||||
@ -72,7 +83,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
|
||||
color: colors.alternativeTextColor,
|
||||
fontWeight: '400',
|
||||
paddingVertical: switchProps ? 8 : 0,
|
||||
lineHeight: 20,
|
||||
lineHeight: Math.round(20 * fontScale),
|
||||
fontSize: 14,
|
||||
marginTop: 2,
|
||||
},
|
||||
@ -93,7 +104,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
|
||||
const enableFeedback = !noFeedback && !!onPress && !disabled;
|
||||
|
||||
const renderContent = () => (
|
||||
<View style={styles.contentRow}>
|
||||
<View style={[styles.contentRow, contentRowStyle]}>
|
||||
{leftAvatar && (
|
||||
<View style={styles.leftAvatarContainer}>
|
||||
{leftAvatar}
|
||||
@ -114,7 +125,14 @@ const ListItem: React.FC<ListItemProps> = React.memo(
|
||||
{rightTitle || rightSubtitle ? (
|
||||
<View style={styles.rightColumn}>
|
||||
{rightTitle ? (
|
||||
<Text style={rightTitleStyle} numberOfLines={1} accessibilityRole="text" selectable={rightTitleSelectable}>
|
||||
<Text
|
||||
style={rightTitleStyle}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.75}
|
||||
accessibilityRole="text"
|
||||
selectable={rightTitleSelectable}
|
||||
>
|
||||
{rightTitle}
|
||||
</Text>
|
||||
) : null}
|
||||
@ -192,16 +210,20 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
leftAvatarContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
rightColumn: {
|
||||
marginStart: 8,
|
||||
minWidth: 0,
|
||||
flexShrink: 0,
|
||||
alignItems: 'flex-end',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
rightMemoWrapper: {
|
||||
flexShrink: 1,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { TouchableOpacity, Text, StyleSheet, View } from 'react-native';
|
||||
import { TouchableOpacity, Text, StyleSheet, View, useWindowDimensions } from 'react-native';
|
||||
import { useStorage } from '../hooks/context/useStorage';
|
||||
import loc, { formatBalanceWithoutSuffix } from '../loc';
|
||||
import { BitcoinUnit } from '../models/bitcoinUnits';
|
||||
@ -22,6 +22,7 @@ const TotalWalletsBalance: React.FC = React.memo(() => {
|
||||
setTotalBalancePreferredUnitStorage,
|
||||
} = useSettings();
|
||||
const { colors } = useTheme();
|
||||
const { fontScale } = useWindowDimensions();
|
||||
|
||||
const totalBalanceFormatted = useMemo(() => {
|
||||
const totalBalance = wallets.reduce((prev, curr) => {
|
||||
@ -31,6 +32,22 @@ const TotalWalletsBalance: React.FC = React.memo(() => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [wallets, totalBalancePreferredUnit, preferredFiatCurrency]);
|
||||
|
||||
const scaledStyles = useMemo(
|
||||
() => ({
|
||||
container: {
|
||||
paddingVertical: Math.round(8 * fontScale),
|
||||
},
|
||||
label: {
|
||||
lineHeight: Math.round(18 * fontScale),
|
||||
marginBottom: Math.round(2 * fontScale),
|
||||
},
|
||||
balance: {
|
||||
lineHeight: Math.round(38 * Math.max(1, fontScale)),
|
||||
},
|
||||
}),
|
||||
[fontScale],
|
||||
);
|
||||
|
||||
const toolTipActions = useMemo(
|
||||
() => [
|
||||
{
|
||||
@ -92,13 +109,20 @@ const TotalWalletsBalance: React.FC = React.memo(() => {
|
||||
|
||||
return (
|
||||
<ToolTipMenu actions={toolTipActions} onPressMenuItem={onPressMenuItem} shouldOpenOnLongPress style={styles.menuContainer}>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.label}>{loc.wallets.total_balance}</Text>
|
||||
<TouchableOpacity onPress={handleBalanceOnPress}>
|
||||
<Text style={[styles.balance, { color: colors.foregroundColor }]}>
|
||||
{totalBalanceFormatted}{' '}
|
||||
<View style={[styles.container, scaledStyles.container]}>
|
||||
<Text style={[styles.label, scaledStyles.label]} numberOfLines={1} adjustsFontSizeToFit minimumFontScale={0.8}>
|
||||
{loc.wallets.total_balance}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={handleBalanceOnPress} style={styles.balanceTouchable}>
|
||||
<Text
|
||||
style={[styles.balance, scaledStyles.balance, { color: colors.foregroundColor }]}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.55}
|
||||
>
|
||||
{totalBalanceFormatted}
|
||||
{totalBalancePreferredUnit !== BitcoinUnit.LOCAL_CURRENCY && (
|
||||
<Text style={[styles.currency, { color: colors.foregroundColor }]}>{totalBalancePreferredUnit}</Text>
|
||||
<Text style={[styles.currency, { color: colors.foregroundColor }]}>{` ${totalBalancePreferredUnit}`}</Text>
|
||||
)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@ -116,6 +140,11 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-start',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
width: '100%',
|
||||
},
|
||||
balanceTouchable: {
|
||||
alignSelf: 'stretch',
|
||||
width: '100%',
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
@ -125,6 +154,7 @@ const styles = StyleSheet.create({
|
||||
balance: {
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 38,
|
||||
},
|
||||
currency: {
|
||||
fontSize: 18,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import { Animated, Easing, Linking, Pressable, Text, TextStyle, ViewStyle, StyleSheet, View } from 'react-native';
|
||||
import { Animated, Easing, Linking, Pressable, Text, TextStyle, ViewStyle, StyleSheet, View, useWindowDimensions } from 'react-native';
|
||||
import Lnurl from '../class/lnurl';
|
||||
import { LightningArkWallet } from '../class/wallets/lightning-ark-wallet';
|
||||
import { LightningTransaction, Transaction } from '../class/wallets/types';
|
||||
@ -29,9 +29,6 @@ import { uint8ArrayToHex } from '../blue_modules/uint8array-extras';
|
||||
import ListItem from './ListItem';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
dateLine: {
|
||||
fontSize: 13,
|
||||
},
|
||||
fullWidthButton: {
|
||||
width: '100%',
|
||||
alignSelf: 'stretch',
|
||||
@ -133,6 +130,7 @@ const TransactionListItemComponent: React.FC<TransactionListItemProps> = ({
|
||||
const { txMetadata, counterpartyMetadata, wallets } = useStorage();
|
||||
const { language, selectedBlockExplorer } = useSettings();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const containerStyle = useMemo(
|
||||
() => ({
|
||||
backgroundColor: colors.background,
|
||||
@ -248,6 +246,7 @@ const TransactionListItemComponent: React.FC<TransactionListItemProps> = ({
|
||||
color,
|
||||
fontSize: 14,
|
||||
fontWeight: '600' as TextStyle['fontWeight'],
|
||||
lineHeight: Math.round(20 * fontScale),
|
||||
textAlign: 'right',
|
||||
paddingRight: insets.right,
|
||||
paddingLeft: insets.left,
|
||||
@ -262,6 +261,7 @@ const TransactionListItemComponent: React.FC<TransactionListItemProps> = ({
|
||||
item.ispaid,
|
||||
insets.right,
|
||||
insets.left,
|
||||
fontScale,
|
||||
]);
|
||||
|
||||
const determineTransactionTypeAndAvatar = () => {
|
||||
@ -549,7 +549,7 @@ const TransactionListItemComponent: React.FC<TransactionListItemProps> = ({
|
||||
<ListItem
|
||||
leftAvatar={avatar}
|
||||
title={listTitle}
|
||||
subtitle={<Text style={styles.dateLine}>{dateLine}</Text>}
|
||||
subtitle={dateLine}
|
||||
chevron={false}
|
||||
rightTitle={rowTitle}
|
||||
rightTitleStyle={rowTitleStyle}
|
||||
|
||||
@ -254,6 +254,7 @@ const styles = StyleSheet.create({
|
||||
position: 'relative',
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
paddingTop: WALLET_LABEL_TOP_GAP,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: HERO_BOTTOM_PADDING,
|
||||
|
||||
@ -30,6 +30,7 @@ import WalletGradient from '../class/wallet-gradient';
|
||||
import { useSizeClass, SizeClass } from '../blue_modules/sizeClass';
|
||||
import loc, { formatBalance, transactionTimeToReadable } from '../loc';
|
||||
import { BlurredBalanceView } from './BlurredBalanceView';
|
||||
import { withAlpha } from './color';
|
||||
import { useTheme } from './themes';
|
||||
import { Transaction, TWallet } from '../class/wallets/types';
|
||||
import { BlueSpacing10 } from './BlueSpacing';
|
||||
@ -37,6 +38,30 @@ import { useLocale } from '@react-navigation/native';
|
||||
|
||||
export const WALLET_CAROUSEL_HEADER_WIDTH = 16;
|
||||
|
||||
/** Base card body height at default Dynamic Type — grows with larger Dynamic Type, never shrinks below default. */
|
||||
export const WALLET_CARD_BASE_MIN_HEIGHT = 164;
|
||||
/** Top inset above wallet cards in the horizontal home carousel. */
|
||||
export const WALLET_CAROUSEL_PADDING_TOP = 12;
|
||||
/** Bottom inset so iOS card shadows (offset 4 + radius 8) are not clipped by the list row. */
|
||||
export const WALLET_CAROUSEL_PADDING_BOTTOM = 20;
|
||||
|
||||
/** Scale layout metrics up for accessibility sizes; keep the design default when fontScale ≤ 1. */
|
||||
const scaleLayoutUp = (base: number, fontScale: number): number => Math.round(base * Math.max(1, fontScale));
|
||||
|
||||
export const getWalletCardMinHeight = (fontScale = 1): number => scaleLayoutUp(WALLET_CARD_BASE_MIN_HEIGHT, fontScale);
|
||||
|
||||
export const getWalletCarouselHeight = (fontScale = 1): number =>
|
||||
scaleLayoutUp(WALLET_CAROUSEL_PADDING_TOP, fontScale) +
|
||||
getWalletCardMinHeight(fontScale) +
|
||||
scaleLayoutUp(WALLET_CAROUSEL_PADDING_BOTTOM, fontScale);
|
||||
|
||||
/** Default carousel row height at `fontScale` 1 — prefer `getWalletCarouselHeight(fontScale)` when layout depends on Dynamic Type. */
|
||||
export const WALLET_CAROUSEL_HEIGHT = getWalletCarouselHeight(1);
|
||||
|
||||
/** Vertical gap between the wallet title/balance block and the latest-tx footer on carousel cards. */
|
||||
const WALLET_CARD_SECTION_GAP = 12;
|
||||
const WALLET_CARD_TEXT_OPACITY = 0.85;
|
||||
|
||||
export const getWalletCarouselItemWidth = (screenWidth: number) => Math.round(screenWidth * 0.82 > 375 ? 375 : screenWidth * 0.82);
|
||||
|
||||
interface NewWalletPanelProps {
|
||||
@ -160,23 +185,28 @@ const iStyles = StyleSheet.create({
|
||||
borderRadius: 12,
|
||||
minHeight: 164,
|
||||
overflow: 'hidden',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
gradCompact: {
|
||||
borderRadius: 10,
|
||||
minHeight: 132,
|
||||
overflow: 'hidden',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
gradContent: {
|
||||
padding: 15,
|
||||
width: '100%',
|
||||
},
|
||||
gradContentCompact: {
|
||||
padding: 12,
|
||||
},
|
||||
balanceContainer: {
|
||||
height: 40,
|
||||
minHeight: 40,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
balanceContainerCompact: {
|
||||
height: 32,
|
||||
minHeight: 32,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
image: {
|
||||
width: 99,
|
||||
@ -189,9 +219,6 @@ const iStyles = StyleSheet.create({
|
||||
width: 78,
|
||||
height: 74,
|
||||
},
|
||||
br: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
label: {
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: 19,
|
||||
@ -206,7 +233,6 @@ const iStyles = StyleSheet.create({
|
||||
},
|
||||
balanceCompact: {
|
||||
fontSize: 28,
|
||||
lineHeight: 34,
|
||||
},
|
||||
latestTx: {
|
||||
backgroundColor: 'transparent',
|
||||
@ -282,11 +308,32 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
||||
const balanceOpacity = useSharedValue(1);
|
||||
const balanceTranslateY = useSharedValue(0);
|
||||
const { colors } = useTheme();
|
||||
const { width } = useWindowDimensions();
|
||||
const { width, fontScale } = useWindowDimensions();
|
||||
const itemWidth = getWalletCarouselItemWidth(width);
|
||||
const { sizeClass } = useSizeClass();
|
||||
const isCompact = sizeVariant === 'compact';
|
||||
const { direction } = useLocale();
|
||||
const scaledCardStyles = useMemo(
|
||||
() => ({
|
||||
grad: { minHeight: getWalletCardMinHeight(fontScale) },
|
||||
gradContent: { padding: scaleLayoutUp(15, fontScale) },
|
||||
balanceContainer: { minHeight: scaleLayoutUp(40, fontScale) },
|
||||
textSpacer: { height: scaleLayoutUp(WALLET_CARD_SECTION_GAP, fontScale) },
|
||||
label: { lineHeight: scaleLayoutUp(24, fontScale) },
|
||||
balance: { lineHeight: scaleLayoutUp(38, fontScale) },
|
||||
balanceCompact: { lineHeight: scaleLayoutUp(30, fontScale) },
|
||||
latestTx: { lineHeight: scaleLayoutUp(18, fontScale) },
|
||||
latestTxTime: { lineHeight: scaleLayoutUp(22, fontScale) },
|
||||
}),
|
||||
[fontScale],
|
||||
);
|
||||
const cardTextStyle = useMemo(
|
||||
() => ({
|
||||
color: withAlpha(colors.inverseForegroundColor, WALLET_CARD_TEXT_OPACITY),
|
||||
writingDirection: direction,
|
||||
}),
|
||||
[colors.inverseForegroundColor, direction],
|
||||
);
|
||||
const previousBalance = useRef<string | undefined>(undefined);
|
||||
const balance = !hideBalance && formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true);
|
||||
const safeBalance = balance || undefined;
|
||||
@ -431,23 +478,23 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
||||
{ backgroundColor: colors.background, shadowColor: colors.shadowColor },
|
||||
]}
|
||||
>
|
||||
<LinearGradient colors={WalletGradient.gradientsFor(item.type)} style={[iStyles.grad, isCompact && iStyles.gradCompact]}>
|
||||
<LinearGradient
|
||||
colors={WalletGradient.gradientsFor(item.type)}
|
||||
style={[iStyles.grad, isCompact && iStyles.gradCompact, scaledCardStyles.grad]}
|
||||
>
|
||||
<ImageBackground source={image} style={[iStyles.image, isCompact && iStyles.imageCompact]} />
|
||||
<View style={[iStyles.gradContent, isCompact && iStyles.gradContentCompact]}>
|
||||
<Text style={iStyles.br} />
|
||||
<View style={[iStyles.gradContent, isCompact && iStyles.gradContentCompact, !isCompact && scaledCardStyles.gradContent]}>
|
||||
{!isPlaceHolder && (
|
||||
<>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
iStyles.label,
|
||||
isCompact && iStyles.labelCompact,
|
||||
{ color: colors.inverseForegroundColor, writingDirection: direction },
|
||||
]}
|
||||
style={[iStyles.label, isCompact && iStyles.labelCompact, scaledCardStyles.label, cardTextStyle]}
|
||||
>
|
||||
{renderHighlightedText ? renderHighlightedText(walletLabel, searchQuery || '') : walletLabel}
|
||||
</Text>
|
||||
<View style={[iStyles.balanceContainer, isCompact && iStyles.balanceContainerCompact]}>
|
||||
<View
|
||||
style={[iStyles.balanceContainer, isCompact && iStyles.balanceContainerCompact, scaledCardStyles.balanceContainer]}
|
||||
>
|
||||
{hideBalance ? (
|
||||
<>
|
||||
<BlueSpacing10 />
|
||||
@ -457,11 +504,13 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
||||
<Animated.Text
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.55}
|
||||
key={`${balance}`} // force component recreation on balance change. To fix right-to-left languages, like Farsi
|
||||
style={[
|
||||
iStyles.balance,
|
||||
isCompact && iStyles.balanceCompact,
|
||||
{ color: colors.inverseForegroundColor, writingDirection: direction },
|
||||
isCompact ? scaledCardStyles.balanceCompact : scaledCardStyles.balance,
|
||||
cardTextStyle,
|
||||
animatedBalanceStyle,
|
||||
]}
|
||||
>
|
||||
@ -469,24 +518,20 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
||||
</Animated.Text>
|
||||
)}
|
||||
</View>
|
||||
<Text style={iStyles.br} />
|
||||
<View style={scaledCardStyles.textSpacer} />
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
iStyles.latestTx,
|
||||
isCompact && iStyles.latestTxCompact,
|
||||
{ color: colors.inverseForegroundColor, writingDirection: direction },
|
||||
]}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.8}
|
||||
style={[iStyles.latestTx, isCompact && iStyles.latestTxCompact, scaledCardStyles.latestTx, cardTextStyle]}
|
||||
>
|
||||
{loc.wallets.list_latest_transaction}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
iStyles.latestTxTime,
|
||||
isCompact && iStyles.latestTxTimeCompact,
|
||||
{ color: colors.inverseForegroundColor, writingDirection: direction },
|
||||
]}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.8}
|
||||
style={[iStyles.latestTxTime, isCompact && iStyles.latestTxTimeCompact, scaledCardStyles.latestTxTime, cardTextStyle]}
|
||||
>
|
||||
{latestTransactionText}
|
||||
</Text>
|
||||
@ -541,7 +586,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
|
||||
animateChanges = false,
|
||||
} = props;
|
||||
|
||||
const { width } = useWindowDimensions();
|
||||
const { width, fontScale } = useWindowDimensions();
|
||||
const itemWidth = React.useMemo(() => getWalletCarouselItemWidth(width), [width]);
|
||||
const snapInterval = React.useMemo(() => itemWidth, [itemWidth]);
|
||||
const snapOffsets = React.useMemo(() => {
|
||||
@ -650,7 +695,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
|
||||
console.warn('[WalletsCarousel] Error scrolling to wallet:', error);
|
||||
// Fallback: try scrolling to offset
|
||||
// Use different measurement based on orientation
|
||||
const itemSize = horizontal ? itemWidth : 195; // 195 is the approximate height of wallet card
|
||||
const itemSize = horizontal ? itemWidth : WALLET_CAROUSEL_HEIGHT;
|
||||
flatListRef.current.scrollToOffset({
|
||||
offset: itemSize * walletIndex,
|
||||
animated,
|
||||
@ -772,7 +817,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
|
||||
|
||||
const keyExtractor = useCallback((item: TWallet, index: number) => (item?.getID ? item.getID() : index.toString()), []);
|
||||
|
||||
const sliderHeight = 195;
|
||||
const sliderHeight = getWalletCarouselHeight(fontScale);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@ -855,7 +900,8 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
|
||||
|
||||
const cStyles = StyleSheet.create({
|
||||
content: {
|
||||
paddingTop: 16,
|
||||
paddingTop: scaleLayoutUp(WALLET_CAROUSEL_PADDING_TOP, fontScale),
|
||||
paddingBottom: scaleLayoutUp(WALLET_CAROUSEL_PADDING_BOTTOM, fontScale),
|
||||
},
|
||||
contentLargeScreen: {
|
||||
paddingHorizontal: sizeClass === SizeClass.Large ? 16 : 12,
|
||||
@ -886,7 +932,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
|
||||
automaticallyAdjustContentInsets
|
||||
automaticallyAdjustKeyboardInsets
|
||||
automaticallyAdjustsScrollIndicatorInsets
|
||||
style={{ minHeight: sliderHeight + 12 }}
|
||||
style={{ minHeight: sliderHeight }}
|
||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||
ListFooterComponent={onNewWalletPress ? <NewWalletPanel onPress={onNewWalletPress} /> : null}
|
||||
{...props}
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState }
|
||||
import { ActivityIndicator, BackHandler, Linking, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native';
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
import { RouteProp, useRoute } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import Icon from '../../components/Icon';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
@ -63,6 +63,10 @@ enum ButtonStatus {
|
||||
type RouteProps = RouteProp<DetailViewStackParamList, 'TransactionStatus'>;
|
||||
type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList, 'TransactionStatus'>;
|
||||
|
||||
type TransactionStatusHeaderOptions = NativeStackNavigationOptions & {
|
||||
headerTitleContainerStyle?: { flex: number; maxWidth: number };
|
||||
};
|
||||
|
||||
enum ActionType {
|
||||
SetCPFPPossible,
|
||||
SetRBFBumpFeePossible,
|
||||
@ -136,8 +140,12 @@ type TransactionDetailHeaderTitleProps = {
|
||||
|
||||
const TransactionDetailHeaderTitle: React.FC<TransactionDetailHeaderTitleProps> = ({ direction, date, directionStyle, dateStyle }) => (
|
||||
<View style={styles.headerTitleContainer}>
|
||||
<BlueText style={directionStyle}>{direction}</BlueText>
|
||||
<BlueText style={dateStyle}>{date}</BlueText>
|
||||
<BlueText style={directionStyle} numberOfLines={1} adjustsFontSizeToFit minimumFontScale={0.8}>
|
||||
{direction}
|
||||
</BlueText>
|
||||
<BlueText style={dateStyle} numberOfLines={2} adjustsFontSizeToFit minimumFontScale={0.8}>
|
||||
{date}
|
||||
</BlueText>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -153,10 +161,57 @@ const TransactionStatus: React.FC = () => {
|
||||
const subscribedWallet = useWalletSubscribe(walletID);
|
||||
const { navigate, goBack, setOptions } = useExtendedNavigation<NavigationProps>();
|
||||
const { colors } = useTheme();
|
||||
const { width: windowWidth } = useWindowDimensions();
|
||||
const { width: windowWidth, fontScale } = useWindowDimensions();
|
||||
const { selectedBlockExplorer } = useSettings();
|
||||
const fetchTxInterval = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
|
||||
const scaledStyles = useMemo(() => {
|
||||
const valueLineHeight = Math.round(48 * fontScale);
|
||||
const valuePaddingTop = Math.round(8 * fontScale);
|
||||
|
||||
return {
|
||||
value: {
|
||||
lineHeight: valueLineHeight,
|
||||
paddingTop: valuePaddingTop,
|
||||
minHeight: valueLineHeight + valuePaddingTop,
|
||||
},
|
||||
localCurrency: {
|
||||
lineHeight: Math.round(20 * fontScale),
|
||||
marginTop: Math.round(6 * fontScale),
|
||||
},
|
||||
headerTitleDirection: {
|
||||
lineHeight: Math.round(22 * fontScale),
|
||||
},
|
||||
headerTitleDate: {
|
||||
lineHeight: Math.round(18 * fontScale),
|
||||
},
|
||||
stateLabel: {
|
||||
lineHeight: Math.round(22 * fontScale),
|
||||
},
|
||||
stateValue: {
|
||||
lineHeight: Math.round(18 * fontScale),
|
||||
},
|
||||
advancedHeader: {
|
||||
minHeight: Math.round(44 * fontScale),
|
||||
},
|
||||
explorerButton: {
|
||||
paddingVertical: Math.round(6 * fontScale),
|
||||
paddingHorizontal: Math.round(12 * fontScale),
|
||||
},
|
||||
addButton: {
|
||||
paddingVertical: Math.round(4 * fontScale),
|
||||
paddingHorizontal: Math.round(12 * fontScale),
|
||||
},
|
||||
detailRow: {
|
||||
minHeight: Math.round(24 * fontScale),
|
||||
paddingVertical: Math.round(12 * fontScale),
|
||||
},
|
||||
sectionTitle: {
|
||||
paddingVertical: Math.round(16 * fontScale),
|
||||
},
|
||||
};
|
||||
}, [fontScale]);
|
||||
|
||||
// Explicit width for To/ID text so Android StaticLayout can apply ellipsis (flex alone often fails on Android)
|
||||
const detailValueMaxWidth = useMemo(() => Math.max(0, Math.floor((windowWidth - 48) / 2)), [windowWidth]);
|
||||
const detailValueWidthStyle = useMemo(() => ({ width: detailValueMaxWidth }), [detailValueMaxWidth]);
|
||||
@ -921,15 +976,20 @@ const TransactionStatus: React.FC = () => {
|
||||
<TransactionDetailHeaderTitle
|
||||
direction={transactionDirection}
|
||||
date={transactionDate}
|
||||
directionStyle={[styles.headerTitleDirection, stylesHook.headerTitleDirection]}
|
||||
dateStyle={[styles.headerTitleDate, stylesHook.titleDate]}
|
||||
directionStyle={[styles.headerTitleDirection, stylesHook.headerTitleDirection, scaledStyles.headerTitleDirection]}
|
||||
dateStyle={[styles.headerTitleDate, stylesHook.titleDate, scaledStyles.headerTitleDate]}
|
||||
/>
|
||||
),
|
||||
});
|
||||
headerTitleAlign: 'left',
|
||||
headerTitleContainerStyle: {
|
||||
flex: 1,
|
||||
maxWidth: Math.max(0, windowWidth - 96),
|
||||
},
|
||||
} as TransactionStatusHeaderOptions);
|
||||
}
|
||||
// stylesHook is derived from colors; omitting to avoid unnecessary effect runs
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tx, transactionDirection, transactionDate, setOptions, colors]);
|
||||
}, [tx, transactionDirection, transactionDate, setOptions, colors, windowWidth, scaledStyles]);
|
||||
|
||||
if (loadingError) {
|
||||
return (
|
||||
@ -962,15 +1022,20 @@ const TransactionStatus: React.FC = () => {
|
||||
{/* Value Section */}
|
||||
<View style={styles.valueCard}>
|
||||
<View style={styles.valueContent}>
|
||||
<Text style={[styles.value, stylesHook.value]} selectable numberOfLines={1} adjustsFontSizeToFit minimumFontScale={0.55}>
|
||||
<Text
|
||||
style={[styles.value, stylesHook.value, scaledStyles.value, styles.valueFullWidth]}
|
||||
selectable
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.55}
|
||||
>
|
||||
{txValue !== null ? formatBalanceWithoutSuffix(txValue, preferredBalanceUnit, true) : '-'}
|
||||
{` `}
|
||||
{preferredBalanceUnit !== BitcoinUnit.LOCAL_CURRENCY && (
|
||||
<Text style={[styles.valueUnit, stylesHook.valueUnit]}>{preferredBalanceUnit}</Text>
|
||||
<Text style={[styles.valueUnit, stylesHook.valueUnit]}>{` ${preferredBalanceUnit}`}</Text>
|
||||
)}
|
||||
</Text>
|
||||
{txValue !== null && (
|
||||
<Text style={[styles.localCurrency, stylesHook.localCurrency]}>
|
||||
<Text style={[styles.localCurrency, stylesHook.localCurrency, scaledStyles.localCurrency]}>
|
||||
{preferredBalanceUnit === BitcoinUnit.LOCAL_CURRENCY
|
||||
? `${formatBalanceWithoutSuffix(Math.abs(txValue), BitcoinUnit.BTC, true)} ${BitcoinUnit.BTC}`
|
||||
: satoshiToLocalCurrency(Math.abs(txValue))}
|
||||
@ -996,8 +1061,10 @@ const TransactionStatus: React.FC = () => {
|
||||
<View style={styles.stateIndicator}>
|
||||
<TransactionPendingIcon />
|
||||
<View style={styles.stateLabelContainer}>
|
||||
<BlueText style={[styles.stateLabel, stylesHook.stateLabelPending]}>{loc.transactions.pending}</BlueText>
|
||||
<BlueText style={[styles.stateValue, stylesHook.stateValuePending, styles.stateValueInline]}>
|
||||
<BlueText style={[styles.stateLabel, stylesHook.stateLabelPending, scaledStyles.stateLabel]}>
|
||||
{loc.transactions.pending}
|
||||
</BlueText>
|
||||
<BlueText style={[styles.stateValue, stylesHook.stateValuePending, styles.stateValueInline, scaledStyles.stateValue]}>
|
||||
{eta || loc.transactions.details_eta_analyzing}
|
||||
</BlueText>
|
||||
</View>
|
||||
@ -1029,9 +1096,11 @@ const TransactionStatus: React.FC = () => {
|
||||
<View style={styles.stateIndicator}>
|
||||
<TransactionOutgoingIcon />
|
||||
<View style={styles.stateLabelContainer}>
|
||||
<BlueText style={[styles.stateLabel, stylesHook.stateLabelSent]}>{loc.transactions.details_sent}</BlueText>
|
||||
<BlueText style={[styles.stateLabel, stylesHook.stateLabelSent, scaledStyles.stateLabel]}>
|
||||
{loc.transactions.details_sent}
|
||||
</BlueText>
|
||||
{isOnChainTx && (
|
||||
<BlueText style={[styles.stateValue, stylesHook.stateValueSent, styles.stateValueInline]}>
|
||||
<BlueText style={[styles.stateValue, stylesHook.stateValueSent, styles.stateValueInline, scaledStyles.stateValue]}>
|
||||
{loc.formatString(loc.transactions.confirmations_lowercase, {
|
||||
confirmations: parsedConfirmations > 6 ? '6+' : parsedConfirmations,
|
||||
})}
|
||||
@ -1043,9 +1112,11 @@ const TransactionStatus: React.FC = () => {
|
||||
<View style={styles.stateIndicator}>
|
||||
<TransactionIncomingIcon />
|
||||
<View style={styles.stateLabelContainer}>
|
||||
<BlueText style={[styles.stateLabel, stylesHook.stateLabelReceived]}>{loc.transactions.details_received}</BlueText>
|
||||
<BlueText style={[styles.stateLabel, stylesHook.stateLabelReceived, scaledStyles.stateLabel]}>
|
||||
{loc.transactions.details_received}
|
||||
</BlueText>
|
||||
{isOnChainTx && (
|
||||
<BlueText style={[styles.stateValue, stylesHook.stateValueReceived, styles.stateValueInline]}>
|
||||
<BlueText style={[styles.stateValue, stylesHook.stateValueReceived, styles.stateValueInline, scaledStyles.stateValue]}>
|
||||
{loc.formatString(loc.transactions.confirmations_lowercase, {
|
||||
confirmations: parsedConfirmations > 6 ? '6+' : parsedConfirmations,
|
||||
})}
|
||||
@ -1080,20 +1151,29 @@ const TransactionStatus: React.FC = () => {
|
||||
{/* Details Section */}
|
||||
<View style={[styles.detailsCard, stylesHook.detailsCard]}>
|
||||
{/* Details Title */}
|
||||
<View style={[styles.sectionTitle, styles.sectionTitleWithButton, stylesHook.sectionTitle]}>
|
||||
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText]}>{loc.transactions.details_section}</BlueText>
|
||||
<View style={[styles.sectionTitle, styles.sectionTitleWithButton, stylesHook.sectionTitle, scaledStyles.sectionTitle]}>
|
||||
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText, styles.sectionTitleTextFlexible]}>
|
||||
{loc.transactions.details_section}
|
||||
</BlueText>
|
||||
{tx?.hash && (
|
||||
<TouchableOpacity
|
||||
onPress={handleOpenBlockExplorer}
|
||||
style={[styles.explorerButton, stylesHook.explorerButton]}
|
||||
style={[styles.explorerButton, stylesHook.explorerButton, scaledStyles.explorerButton]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<BlueText style={[styles.explorerButtonText, stylesHook.explorerButtonText]}>{loc.transactions.details_explorer}</BlueText>
|
||||
<BlueText
|
||||
style={[styles.explorerButtonText, stylesHook.explorerButtonText]}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.8}
|
||||
>
|
||||
{loc.transactions.details_explorer}
|
||||
</BlueText>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
{/* Network Fee */}
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_network_fee}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
<CopyTextToClipboard
|
||||
@ -1117,7 +1197,7 @@ const TransactionStatus: React.FC = () => {
|
||||
const displayText = externalAddresses.map(shortenCounterpartyName).join(', ');
|
||||
const copyText = externalAddresses.join(', ');
|
||||
return (
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_to_address}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
<View style={styles.detailValueCopyContainer}>
|
||||
@ -1143,7 +1223,7 @@ const TransactionStatus: React.FC = () => {
|
||||
|
||||
{/* Transaction ID - display shortened so it stays on one line on Android; copy still gets full hash */}
|
||||
{tx.hash && (
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_id}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
<View style={styles.detailValueCopyContainer}>
|
||||
@ -1170,7 +1250,7 @@ const TransactionStatus: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Note/Memo */}
|
||||
<View style={[styles.detailRow, styles.detailRowLast, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, styles.detailRowLast, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_note}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
{memo ? (
|
||||
@ -1180,8 +1260,19 @@ const TransactionStatus: React.FC = () => {
|
||||
</BlueText>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity onPress={handleNotePress} style={[styles.addButton, stylesHook.addButton]} activeOpacity={0.7}>
|
||||
<BlueText style={[styles.addButtonText, stylesHook.addButtonText]}>{loc.transactions.details_add_note}</BlueText>
|
||||
<TouchableOpacity
|
||||
onPress={handleNotePress}
|
||||
style={[styles.addButton, stylesHook.addButton, scaledStyles.addButton]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<BlueText
|
||||
style={[styles.addButtonText, stylesHook.addButtonText]}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.8}
|
||||
>
|
||||
{loc.transactions.details_add_note}
|
||||
</BlueText>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
@ -1192,11 +1283,13 @@ const TransactionStatus: React.FC = () => {
|
||||
<View style={[styles.detailsCard, stylesHook.detailsCard]}>
|
||||
<TouchableOpacity
|
||||
onPress={() => setIsAdvancedExpanded(!isAdvancedExpanded)}
|
||||
style={[styles.advancedHeader, stylesHook.advancedHeader]}
|
||||
style={[styles.advancedHeader, stylesHook.advancedHeader, scaledStyles.advancedHeader]}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<View style={[styles.sectionTitle, stylesHook.sectionTitle, styles.sectionTitleRow]}>
|
||||
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText]}>{loc.transactions.details_advanced}</BlueText>
|
||||
<View style={[styles.sectionTitle, stylesHook.sectionTitle, styles.sectionTitleRow, scaledStyles.sectionTitle]}>
|
||||
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText, styles.sectionTitleTextFlexible]} numberOfLines={2}>
|
||||
{loc.transactions.details_advanced}
|
||||
</BlueText>
|
||||
<Icon
|
||||
name={isAdvancedExpanded ? 'chevron-up' : 'chevron-down'}
|
||||
type="font-awesome"
|
||||
@ -1209,7 +1302,7 @@ const TransactionStatus: React.FC = () => {
|
||||
{isAdvancedExpanded && (
|
||||
<View style={[styles.advancedContent, stylesHook.advancedContent]}>
|
||||
{/* Fee Rate */}
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_fee_rate}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
<CopyTextToClipboard
|
||||
@ -1221,7 +1314,7 @@ const TransactionStatus: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{/* Size */}
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_size}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
<CopyTextToClipboard
|
||||
@ -1233,7 +1326,7 @@ const TransactionStatus: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{/* Virtual Size */}
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_virtual_size}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
<CopyTextToClipboard
|
||||
@ -1245,7 +1338,7 @@ const TransactionStatus: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{/* Transaction Hex */}
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_tx_hex}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
{txHex ? (
|
||||
@ -1310,6 +1403,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
headerTitleDirection: {
|
||||
fontSize: 17,
|
||||
@ -1357,15 +1451,20 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-start',
|
||||
overflow: 'visible',
|
||||
width: '100%',
|
||||
},
|
||||
value: {
|
||||
fontSize: 40,
|
||||
fontWeight: '700',
|
||||
letterSpacing: -0.5,
|
||||
lineHeight: 32,
|
||||
lineHeight: 48,
|
||||
paddingTop: 8,
|
||||
minHeight: 38,
|
||||
},
|
||||
valueFullWidth: {
|
||||
width: '100%',
|
||||
flexShrink: 1,
|
||||
},
|
||||
valueUnit: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
@ -1383,7 +1482,6 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 24,
|
||||
marginBottom: 42,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
stateSection: {
|
||||
alignItems: 'flex-start',
|
||||
@ -1401,6 +1499,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-start',
|
||||
marginLeft: 8,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
stateLabel: {
|
||||
fontSize: 16,
|
||||
@ -1486,17 +1585,23 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
sectionTitleText: {
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
},
|
||||
sectionTitleTextFlexible: {
|
||||
flex: 1,
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
explorerButton: {
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 6,
|
||||
alignSelf: 'flex-end',
|
||||
minWidth: 50,
|
||||
flexShrink: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
@ -1507,7 +1612,7 @@ const styles = StyleSheet.create({
|
||||
detailRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 0,
|
||||
minHeight: 24,
|
||||
paddingVertical: 12,
|
||||
@ -1531,6 +1636,8 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
flex: 1,
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
lineHeight: 22,
|
||||
paddingRight: 12,
|
||||
},
|
||||
@ -1544,11 +1651,12 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
maxWidth: '100%',
|
||||
flexWrap: 'nowrap',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'flex-end',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 8,
|
||||
flexShrink: 0,
|
||||
},
|
||||
detailValueCopyContainer: {
|
||||
flex: 1,
|
||||
@ -1596,7 +1704,7 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 6,
|
||||
alignSelf: 'flex-end',
|
||||
minWidth: 50,
|
||||
flexShrink: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
@ -1614,7 +1722,6 @@ const styles = StyleSheet.create({
|
||||
borderWidth: 1,
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
advancedContent: {
|
||||
marginTop: 0,
|
||||
|
||||
@ -33,6 +33,7 @@ import presentAlert, { AlertType } from '../../components/Alert';
|
||||
import { FButton, FContainer, FloatButtonsBottomFade } from '../../components/FloatButtons';
|
||||
import { useTheme } from '../../components/themes';
|
||||
import { TransactionListItem } from '../../components/TransactionListItem';
|
||||
import { TX_ROW_BASE_HEIGHT } from '../../components/ListItem';
|
||||
import TransactionsNavigationHeader, { actionKeys } from '../../components/TransactionsNavigationHeader';
|
||||
import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
|
||||
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
||||
@ -437,11 +438,17 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
[name, navigate, navigation, onWalletSelect, walletID, wallets],
|
||||
);
|
||||
|
||||
const getItemLayout = (_: any, index: number) => ({
|
||||
length: 64,
|
||||
offset: 64 * index,
|
||||
index,
|
||||
});
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const txRowHeight = Math.round(TX_ROW_BASE_HEIGHT * fontScale);
|
||||
|
||||
const getItemLayout = useCallback(
|
||||
(_: any, index: number) => ({
|
||||
length: txRowHeight,
|
||||
offset: txRowHeight * index,
|
||||
index,
|
||||
}),
|
||||
[txRowHeight],
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
// react/no-unused-prop-types misfires on inline arrow renderers: it reads the
|
||||
|
||||
@ -8,10 +8,15 @@ import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/h
|
||||
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
|
||||
import { ExtendedTransaction, Transaction, TWallet } from '../../class/wallets/types';
|
||||
import presentAlert from '../../components/Alert';
|
||||
import { FButton, FContainer, FloatButtonsBottomFade } from '../../components/FloatButtons';
|
||||
import { FButton, FContainer, FloatButtonsBottomFade, getFloatingButtonReservedHeight } from '../../components/FloatButtons';
|
||||
import { useTheme } from '../../components/themes';
|
||||
import { TransactionListItem } from '../../components/TransactionListItem';
|
||||
import WalletsCarousel, { getWalletCarouselItemWidth, CarouselListRefType } from '../../components/WalletsCarousel';
|
||||
import { TX_ROW_BASE_HEIGHT } from '../../components/ListItem';
|
||||
import WalletsCarousel, {
|
||||
getWalletCarouselItemWidth,
|
||||
CarouselListRefType,
|
||||
getWalletCarouselHeight,
|
||||
} from '../../components/WalletsCarousel';
|
||||
import { useSizeClass, SizeClass } from '../../blue_modules/sizeClass';
|
||||
import loc from '../../loc';
|
||||
import ActionSheet from '../ActionSheet';
|
||||
@ -28,6 +33,7 @@ import { scanQrHelper } from '../../helpers/scan-qr';
|
||||
import { isIOS26OrHigher } from '../../components/platform';
|
||||
|
||||
const WalletsListSections = { CAROUSEL: 'CAROUSEL', TRANSACTIONS: 'TRANSACTIONS' };
|
||||
const SECTION_HEADER_BASE_HEIGHT = 56;
|
||||
|
||||
/** Electrum `ping` while the list is visible; detects mid-session drops without polling when user is elsewhere. */
|
||||
const ELECTRUM_HEALTH_POLL_WHILE_WALLETS_LIST_FOCUSED_MS = 30_000;
|
||||
@ -108,7 +114,11 @@ const WalletsList: React.FC = () => {
|
||||
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
|
||||
const { wallets, getTransactions, refreshAllWalletTransactions } = useStorage();
|
||||
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
|
||||
const { width } = useWindowDimensions();
|
||||
const { width, fontScale } = useWindowDimensions();
|
||||
const carouselHeight = getWalletCarouselHeight(fontScale);
|
||||
const transactionItemHeight = Math.round(TX_ROW_BASE_HEIGHT * fontScale);
|
||||
const sectionHeaderHeight = Math.round(SECTION_HEADER_BASE_HEIGHT * fontScale);
|
||||
const floatingButtonHeight = getFloatingButtonReservedHeight(fontScale);
|
||||
const { colors, scanImage } = useTheme();
|
||||
const navigation = useExtendedNavigation<NavigationProps>();
|
||||
const isFocused = useIsFocused();
|
||||
@ -124,9 +134,11 @@ const WalletsList: React.FC = () => {
|
||||
listHeaderBack: {
|
||||
backgroundColor: colors.background,
|
||||
paddingTop: sizeClass === SizeClass.Large ? 8 : 0,
|
||||
minHeight: sectionHeaderHeight,
|
||||
},
|
||||
listHeaderText: {
|
||||
color: colors.foregroundColor,
|
||||
marginVertical: Math.round(16 * fontScale),
|
||||
},
|
||||
});
|
||||
|
||||
@ -493,14 +505,9 @@ const WalletsList: React.FC = () => {
|
||||
}, [sizeClass, dataSource]);
|
||||
|
||||
// Constants for layout calculations
|
||||
const TRANSACTION_ITEM_HEIGHT = 80;
|
||||
const CAROUSEL_HEIGHT = 195;
|
||||
const SECTION_HEADER_HEIGHT = 56; // Base height
|
||||
const LARGE_TITLE_EXTRA_HEIGHT = 20; // Additional height for large titles
|
||||
|
||||
const getSectionHeaderHeight = useCallback(() => {
|
||||
return SECTION_HEADER_HEIGHT + (sizeClass === SizeClass.Large ? LARGE_TITLE_EXTRA_HEIGHT : 0);
|
||||
}, [sizeClass]);
|
||||
return sectionHeaderHeight + (sizeClass === SizeClass.Large ? Math.round(20 * fontScale) : 0);
|
||||
}, [sizeClass, sectionHeaderHeight, fontScale]);
|
||||
|
||||
const getItemLayout = useCallback(
|
||||
(data: any, index: number) => {
|
||||
@ -509,8 +516,8 @@ const WalletsList: React.FC = () => {
|
||||
if (sizeClass === SizeClass.Large) {
|
||||
// On large screens: only transaction items, no carousel
|
||||
return {
|
||||
length: TRANSACTION_ITEM_HEIGHT,
|
||||
offset: TRANSACTION_ITEM_HEIGHT * index,
|
||||
length: transactionItemHeight,
|
||||
offset: transactionItemHeight * index,
|
||||
index,
|
||||
};
|
||||
} else {
|
||||
@ -518,7 +525,7 @@ const WalletsList: React.FC = () => {
|
||||
// First section: Carousel
|
||||
if (index === 0) {
|
||||
return {
|
||||
length: CAROUSEL_HEIGHT,
|
||||
length: carouselHeight,
|
||||
offset: 0,
|
||||
index,
|
||||
};
|
||||
@ -531,13 +538,13 @@ const WalletsList: React.FC = () => {
|
||||
// 3. Transaction items
|
||||
const transactionIndex = index - 1; // Adjust index to account for carousel
|
||||
return {
|
||||
length: TRANSACTION_ITEM_HEIGHT,
|
||||
offset: CAROUSEL_HEIGHT + headerHeight + TRANSACTION_ITEM_HEIGHT * transactionIndex,
|
||||
length: transactionItemHeight,
|
||||
offset: carouselHeight + headerHeight + transactionItemHeight * transactionIndex,
|
||||
index,
|
||||
};
|
||||
}
|
||||
},
|
||||
[sizeClass, getSectionHeaderHeight],
|
||||
[sizeClass, getSectionHeaderHeight, carouselHeight, transactionItemHeight],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -550,7 +557,7 @@ const WalletsList: React.FC = () => {
|
||||
initialNumToRender={10}
|
||||
renderSectionFooter={renderSectionFooter}
|
||||
sections={sections}
|
||||
floatingButtonHeight={70}
|
||||
floatingButtonHeight={floatingButtonHeight}
|
||||
maxToRenderPerBatch={10}
|
||||
updateCellsBatchingPeriod={50}
|
||||
getItemLayout={getItemLayout}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user