This commit is contained in:
Marcos Rodriguez Velez 2024-11-15 22:56:31 -04:00
parent 055cde127e
commit e7c58778ee
7 changed files with 279 additions and 38 deletions

View File

@ -54,7 +54,13 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
subtitle: subaction.subtitle,
image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined,
state: subaction.menuState === undefined ? undefined : ((subaction.menuState ? 'on' : 'off') as MenuState),
attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden },
attributes: {
disabled: subaction.disabled,
destructive: subaction.destructive,
hidden: subaction.hidden,
},
subactions: subaction.subactions ? subaction.subactions.map(mapMenuItemForMenuView).filter(Boolean) : undefined,
displayInline: subaction.displayInline || false,
})) || [];
return {
@ -63,8 +69,12 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
subtitle: action.subtitle,
image: action.icon?.iconValue ? action.icon.iconValue : undefined,
state: action.menuState === undefined ? undefined : ((action.menuState ? 'on' : 'off') as MenuState),
attributes: { disabled: action.disabled, destructive: action.destructive, hidden: action.hidden },
subactions: subactions.length > 0 ? subactions : undefined,
attributes: {
disabled: action.disabled,
destructive: action.destructive,
hidden: action.hidden,
},
subactions: subactions.length > 0 ? (subactions.filter(Boolean) as MenuAction[]) : undefined,
displayInline: action.displayInline || false,
};
}, []);
@ -86,6 +96,7 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
.map(mapMenuItemForMenuView)
.filter(item => item !== null) as MenuAction[],
displayInline: true,
keepsMenuPresented: true,
};
} else if (!Array.isArray(actionGroup) && actionGroup.id) {
return mapMenuItemForMenuView(actionGroup);

View File

@ -4,6 +4,7 @@ import { Icon } from '@rneui/themed';
import { useTheme } from '../themes';
import ToolTipMenu from '../TooltipMenu';
import { Action } from '../types';
import { TouchableOpacityWrapper } from '../ListItem';
interface MoreOptionsButtonProps {
onPressMenuItem: (id: string) => void;
@ -11,6 +12,7 @@ interface MoreOptionsButtonProps {
actions: Action[] | Action[][];
testID?: string;
isMenuPrimaryAction: boolean;
disabled?: boolean;
}
const MoreOptionsButton: React.FC<MoreOptionsButtonProps> = ({
@ -19,6 +21,7 @@ const MoreOptionsButton: React.FC<MoreOptionsButtonProps> = ({
actions,
testID = 'MoreOptionsButton',
isMenuPrimaryAction = false,
disabled,
}) => {
const { colors } = useTheme();
@ -26,11 +29,14 @@ const MoreOptionsButton: React.FC<MoreOptionsButtonProps> = ({
<ToolTipMenu
onPressMenuItem={onPressMenuItem}
onPress={onPress}
disabled={disabled}
actions={actions}
isMenuPrimaryAction={isMenuPrimaryAction}
testID={testID}
>
<Icon
onPress={onPress}
Component={TouchableOpacityWrapper}
containerStyle={[style.buttonStyle, { backgroundColor: colors.lightButton }]}
size={22}
name="more-horiz"

View File

@ -1,14 +1,10 @@
import React, { useCallback, useMemo } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import { Icon } from '@rneui/themed';
import { useTheme } from '../themes';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import loc from '../../loc';
import ToolTipMenu from '../TooltipMenu';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import MoreOptionsButton from './MoreOptionsButton';
const SettingsButton = () => {
const { colors } = useTheme();
const { navigate } = useExtendedNavigation();
const onPress = () => {
navigate('Settings');
@ -29,28 +25,14 @@ const SettingsButton = () => {
const actions = useMemo(() => [CommonToolTipActions.ManageWallet], []);
return (
<ToolTipMenu onPressMenuItem={onPressMenuItem} actions={actions}>
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc.settings.default_title}
testID="SettingsButton"
style={[style.buttonStyle, { backgroundColor: colors.lightButton }]}
onPress={onPress}
>
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} />
</TouchableOpacity>
</ToolTipMenu>
<MoreOptionsButton
isMenuPrimaryAction={false}
onPress={onPress}
onPressMenuItem={onPressMenuItem}
actions={actions}
testID="SettingsButton"
/>
);
};
export default SettingsButton;
const style = StyleSheet.create({
buttonStyle: {
width: 30,
height: 30,
borderRadius: 15,
justifyContent: 'center',
alignContent: 'center',
},
});

View File

@ -11,11 +11,12 @@ export interface Action {
menuState?: 'mixed' | boolean | undefined;
displayInline?: boolean; // Indicates if subactions should be displayed inline or nested (iOS only)
image?: string;
keepsMenuPresented?: boolean;
imageColor?: ColorValue;
destructive?: boolean;
hidden?: boolean;
disabled?: boolean;
subactions?: Action[]; // Nested/Inline actions (subactions) within an action
subactions?: Action[];
}
export interface ToolTipMenuProps {

View File

@ -383,6 +383,7 @@
"add_bitcoin_explain": "Simple and powerful Bitcoin wallet",
"add_create": "Create",
"total_balance": "Total Balance",
"balance": "Balance",
"add_entropy_reset_title": "Reset Entropy",
"add_entropy_reset_message": "Changing the wallet type will reset the current entropy. Do you want to proceed?",
"add_entropy": "Entropy",
@ -473,6 +474,8 @@
"no_ln_wallet_error": "Before paying a Lightning invoice, you must first add a Lightning wallet.",
"looks_like_bip38": "This looks like a password-protected private key (BIP38).",
"manage_title": "Manage Wallets",
"sort_by_order": "Order",
"sort_by_property": "Property",
"no_results_found": "No results found.",
"please_continue_scanning": "Please continue scanning.",
"select_no_bitcoin": "There are currently no Bitcoin wallets available.",

View File

@ -1,5 +1,5 @@
import React, { useEffect, useLayoutEffect, useReducer, useCallback, useMemo, useRef } from 'react';
import { StyleSheet, TouchableOpacity, Image, Text, Alert, I18nManager, Animated, LayoutAnimation } from 'react-native';
import { StyleSheet, View, TouchableOpacity, Image, Text, Alert, I18nManager, Animated, LayoutAnimation } from 'react-native';
import {
NestableScrollContainer,
ScaleDecorator,
@ -25,12 +25,23 @@ import prompt from '../../helpers/prompt';
import HeaderRightButton from '../../components/HeaderRightButton';
import { ManageWalletsListItem } from '../../components/ManageWalletsListItem';
import { useSettings } from '../../hooks/context/useSettings';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import MoreOptionsButton from '../../components/icons/MoreOptionsButton';
import { Action } from '../../components/types';
enum ItemType {
WalletSection = 'wallet',
TransactionSection = 'transaction',
}
enum SortOption {
Balance = 'balance',
Label = 'label',
MostRecent = 'mostRecent',
ASC = 'asc',
DESC = 'desc',
}
interface WalletItem {
type: ItemType.WalletSection;
data: TWallet;
@ -50,6 +61,9 @@ const SET_FILTERED_ORDER = 'SET_FILTERED_ORDER';
const SET_TEMP_ORDER = 'SET_TEMP_ORDER';
const REMOVE_WALLET = 'REMOVE_WALLET';
const SAVE_CHANGES = 'SAVE_CHANGES';
const SET_CURRENT_SORT = 'SET_CURRENT_SORT';
const SET_CURRENT_SORT_ORDER = 'SET_CURRENT_SORT_ORDER';
const SET_CURRENT_SORT_PROPERTY = 'SET_CURRENT_SORT_PROPERTY';
interface SaveChangesAction {
type: typeof SAVE_CHANGES;
@ -86,14 +100,32 @@ interface RemoveWalletAction {
payload: string; // Wallet ID
}
type Action =
interface SetCurrentSortAction {
type: typeof SET_CURRENT_SORT;
payload: SortOption;
}
interface SetCurrentSortOrderAction {
type: typeof SET_CURRENT_SORT_ORDER;
payload: SortOption;
}
interface SetCurrentSortPropertyAction {
type: typeof SET_CURRENT_SORT_PROPERTY;
payload: SortOption;
}
type ReducerAction =
| SetSearchQueryAction
| SetIsSearchFocusedAction
| SetInitialOrderAction
| SetFilteredOrderAction
| SetTempOrderAction
| SaveChangesAction
| RemoveWalletAction;
| RemoveWalletAction
| SetCurrentSortAction
| SetCurrentSortOrderAction
| SetCurrentSortPropertyAction;
interface State {
searchQuery: string;
@ -102,6 +134,8 @@ interface State {
tempOrder: Item[];
wallets: TWallet[];
txMetadata: TTXMetadata;
currentSortOrder: SortOption;
currentSortProperty: SortOption;
}
const initialState: State = {
@ -111,13 +145,15 @@ const initialState: State = {
tempOrder: [],
wallets: [],
txMetadata: {},
currentSortOrder: SortOption.ASC,
currentSortProperty: SortOption.Balance,
};
const deepCopyWallets = (wallets: TWallet[]): TWallet[] => {
return wallets.map(wallet => Object.assign(Object.create(Object.getPrototypeOf(wallet)), wallet));
};
const reducer = (state: State, action: Action): State => {
const reducer = (state: State, action: ReducerAction): State => {
switch (action.type) {
case SET_SEARCH_QUERY:
return { ...state, searchQuery: action.payload };
@ -181,8 +217,12 @@ const reducer = (state: State, action: Action): State => {
tempOrder: updatedOrder,
};
}
case SET_CURRENT_SORT_ORDER:
return { ...state, currentSortOrder: action.payload };
case SET_CURRENT_SORT_PROPERTY:
return { ...state, currentSortProperty: action.payload };
default:
throw new Error(`Unhandled action type: ${(action as Action).type}`);
throw new Error(`Unhandled action type: ${(action as ReducerAction).type}`);
}
};
@ -222,6 +262,53 @@ const ManageWallets: React.FC = () => {
}
}, [debouncedSearchQuery, txMetadata]);
useEffect(() => {
const determineSortOptions = (): { order: SortOption; property: SortOption } => {
const wallets = state.tempOrder.filter(item => item.type === ItemType.WalletSection).map(item => item.data);
// Determine sort order
const isASC = wallets.every((wallet, index, arr) => {
if (index === 0) return true;
return arr[index - 1].getLabel()?.localeCompare(wallet.getLabel()!) <= 0;
});
const isDESC = wallets.every((wallet, index, arr) => {
if (index === 0) return true;
return arr[index - 1].getLabel()?.localeCompare(wallet.getLabel()!) >= 0;
});
let detectedSortOrder: SortOption = SortOption.ASC;
if (isASC) detectedSortOrder = SortOption.ASC;
else if (isDESC) detectedSortOrder = SortOption.DESC;
// Determine sort property
const isBalance = wallets.every((current, index, arr) => {
if (index === 0) return true;
return arr[index - 1].getBalance() <= current.getBalance();
});
const isLabel = wallets.every((wallet, index, arr) => {
if (index === 0) return true;
return arr[index - 1].getLabel()?.localeCompare(wallet.getLabel()!) <= 0;
});
const isMostRecent = wallets.every((current, index, arr) => {
if (index === 0) return true;
return (arr[index - 1].getTransactions()[0]?.time || 0) >= (current.getTransactions()[0]?.time || 0);
});
let detectedSortProperty: SortOption = SortOption.Balance;
if (isBalance) detectedSortProperty = SortOption.Balance;
else if (isLabel) detectedSortProperty = SortOption.Label;
else if (isMostRecent) detectedSortProperty = SortOption.MostRecent;
return { order: detectedSortOrder, property: detectedSortProperty };
};
const detectedSort = determineSortOptions();
if (detectedSort.order !== state.currentSortOrder || detectedSort.property !== state.currentSortProperty) {
dispatch({ type: SET_CURRENT_SORT_ORDER, payload: detectedSort.order });
dispatch({ type: SET_CURRENT_SORT_PROPERTY, payload: detectedSort.property });
}
}, [state.tempOrder, state.currentSortOrder, state.currentSortProperty]);
const handleClose = useCallback(() => {
if (state.searchQuery.length === 0 && !state.isSearchFocused) {
const newWalletOrder = state.tempOrder
@ -263,11 +350,144 @@ const ManageWallets: React.FC = () => {
[goBack, closeImage],
);
const moreOptionsActions = useMemo((): Action[] | Action[][] => {
return [
{
id: 'sort_by_menu',
text: loc.cc.sort_by,
subactions: [
{
id: 'sort_by_order',
displayInline: true,
text: loc.wallets.sort_by_order,
keepsMenuPresented: true,
subactions: [
{
...CommonToolTipActions.SortASC,
menuState: state.currentSortOrder === SortOption.ASC,
},
{
...CommonToolTipActions.SortDESC,
menuState: state.currentSortOrder === SortOption.DESC,
},
],
},
{
id: 'sort_by_property',
displayInline: true,
text: loc.wallets.sort_by_property,
subactions: [
{
...CommonToolTipActions.SortBalance,
menuState: state.currentSortProperty === SortOption.Balance,
disabled: state.currentSortProperty === SortOption.Balance,
},
{
...CommonToolTipActions.SortLabel,
menuState: state.currentSortProperty === SortOption.Label,
disabled: state.currentSortProperty === SortOption.Label,
},
{
...CommonToolTipActions.MostRecentTransaction,
menuState: state.currentSortProperty === SortOption.MostRecent,
disabled: state.currentSortProperty === SortOption.MostRecent,
},
],
},
{ ...CommonToolTipActions.Reset },
],
},
];
}, [state.currentSortOrder, state.currentSortProperty]);
const moreOptionsOnPressMenuItem = useCallback(
(id: string) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
switch (id) {
case CommonToolTipActions.SortASC.id: {
dispatch({ type: SET_CURRENT_SORT_ORDER, payload: SortOption.ASC });
const sortedWallets = state.tempOrder
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
.sort((a, b) => a.data.getLabel()!.localeCompare(b.data.getLabel()!));
dispatch({ type: SET_TEMP_ORDER, payload: sortedWallets });
break;
}
case CommonToolTipActions.SortDESC.id: {
dispatch({ type: SET_CURRENT_SORT_ORDER, payload: SortOption.DESC });
const sortedWallets = state.tempOrder
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
.sort((a, b) => b.data.getLabel()!.localeCompare(a.data.getLabel()!));
dispatch({ type: SET_TEMP_ORDER, payload: sortedWallets });
break;
}
case CommonToolTipActions.SortBalance.id: {
dispatch({ type: SET_CURRENT_SORT_PROPERTY, payload: SortOption.Balance });
const sortedWallets = state.tempOrder
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
.sort((a, b) => a.data.getBalance() - b.data.getBalance());
dispatch({ type: SET_TEMP_ORDER, payload: sortedWallets });
break;
}
case CommonToolTipActions.SortLabel.id: {
dispatch({ type: SET_CURRENT_SORT_PROPERTY, payload: SortOption.Label });
const sortedWalletsByLabel = state.tempOrder
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
.sort((a, b) => a.data.getLabel()!.localeCompare(b.data.getLabel()!));
dispatch({ type: SET_TEMP_ORDER, payload: sortedWalletsByLabel });
break;
}
case CommonToolTipActions.MostRecentTransaction.id: {
dispatch({ type: SET_CURRENT_SORT_PROPERTY, payload: SortOption.MostRecent });
const sortedWalletsByMostRecent = state.tempOrder
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
.sort((a, b) => {
return (b.data.getTransactions()[0]?.time || 0) - (a.data.getTransactions()[0]?.time || 0);
});
dispatch({ type: SET_TEMP_ORDER, payload: sortedWalletsByMostRecent });
break;
}
case CommonToolTipActions.Reset.id: {
dispatch({ type: SET_TEMP_ORDER, payload: state.order });
dispatch({ type: SET_CURRENT_SORT_ORDER, payload: SortOption.ASC });
dispatch({ type: SET_CURRENT_SORT_PROPERTY, payload: SortOption.Balance });
break;
}
default:
break;
}
},
[state.order, state.tempOrder],
);
const SaveButton = useMemo(
() => <HeaderRightButton disabled={!hasUnsavedChanges} title={loc.send.input_done} onPress={handleClose} />,
[handleClose, hasUnsavedChanges],
);
const MoreButton = useMemo(
() => (
<MoreOptionsButton
disabled={state.searchQuery.length > 0 || state.isSearchFocused}
isMenuPrimaryAction
actions={moreOptionsActions}
onPressMenuItem={moreOptionsOnPressMenuItem}
/>
),
[moreOptionsActions, moreOptionsOnPressMenuItem, state.isSearchFocused, state.searchQuery.length],
);
const HeaderRight = useCallback(
() => (
<>
{MoreButton}
<View style={styles.separation} />
{SaveButton}
</>
),
[MoreButton, SaveButton],
);
useLayoutEffect(() => {
const searchBarOptions = {
hideWhenScrolling: false,
@ -280,10 +500,10 @@ const ManageWallets: React.FC = () => {
setOptions({
headerLeft: () => HeaderLeftButton,
headerRight: () => SaveButton,
headerRight: HeaderRight,
headerSearchBarOptions: searchBarOptions,
});
}, [setOptions, HeaderLeftButton, SaveButton]);
}, [setOptions, HeaderLeftButton, SaveButton, MoreButton, HeaderRight]);
useFocusEffect(
useCallback(() => {
@ -577,4 +797,7 @@ const styles = StyleSheet.create({
dimmedText: {
opacity: 0.8,
},
separation: {
width: 8,
},
});

View File

@ -50,7 +50,9 @@ const keys = {
SortValue: 'sortValue',
SortLabel: 'sortLabel',
SortStatus: 'sortStatus',
SortBalance: 'sortBalance',
Reset: 'reset',
MostRecentTransaction: 'mostRecentTransaction',
} as const;
const icons = {
@ -96,6 +98,7 @@ const icons = {
SortASC: { iconValue: 'arrow.down.to.line' },
SortDESC: { iconValue: 'arrow.up.to.line' },
Reset: { iconValue: 'arrow.counterclockwise' },
MostRecentTransaction: { iconValue: 'clock' },
} as const;
export const CommonToolTipActions = {
@ -336,11 +339,13 @@ export const CommonToolTipActions = {
id: keys.SortASC,
text: loc.cc.sort_asc,
icon: icons.SortASC,
keepsMenuPresented: true,
},
SortDESC: {
id: keys.SortDESC,
text: loc.cc.sort_desc,
icon: icons.SortDESC,
keepsMenuPresented: true,
},
SortHeight: {
id: keys.SortHeight,
@ -358,4 +363,14 @@ export const CommonToolTipActions = {
id: keys.SortStatus,
text: loc.cc.sort_status,
},
SortBalance: {
id: keys.SortBalance,
text: loc.wallets.balance,
icon: icons.ViewInBitcoin,
},
MostRecentTransaction: {
id: keys.MostRecentTransaction,
text: loc.transactions.details_title,
icon: icons.MostRecentTransaction,
},
} as const;