From b0dbe0966d76f70d9b2097f8af896b8864ca5728 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Fri, 15 Nov 2024 19:45:14 -0400 Subject: [PATCH] ADD: Manage Wallets Sort By --- components/TooltipMenu.tsx | 17 ++- components/icons/MoreOptionsButton.tsx | 50 ++++++++ components/icons/SettingsButton.tsx | 36 ++---- components/types.ts | 3 +- loc/en.json | 3 + screen/wallets/ManageWallets.tsx | 154 +++++++++++++++++++++++-- typings/CommonToolTipActions.ts | 22 ++++ 7 files changed, 247 insertions(+), 38 deletions(-) create mode 100644 components/icons/MoreOptionsButton.tsx diff --git a/components/TooltipMenu.tsx b/components/TooltipMenu.tsx index 39a5be982..28375c5af 100644 --- a/components/TooltipMenu.tsx +++ b/components/TooltipMenu.tsx @@ -54,7 +54,13 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref) => { 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) => { 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) => { .map(mapMenuItemForMenuView) .filter(item => item !== null) as MenuAction[], displayInline: true, + keepsMenuPresented: true, }; } else if (!Array.isArray(actionGroup) && actionGroup.id) { return mapMenuItemForMenuView(actionGroup); diff --git a/components/icons/MoreOptionsButton.tsx b/components/icons/MoreOptionsButton.tsx new file mode 100644 index 000000000..b4947edf4 --- /dev/null +++ b/components/icons/MoreOptionsButton.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { Icon } from '@rneui/themed'; +import { useTheme } from '../themes'; +import ToolTipMenu from '../TooltipMenu'; +import { Action } from '../types'; + +interface MoreOptionsButtonProps { + onPressMenuItem: (id: string) => void; + onPress?: () => void; + actions: Action[] | Action[][]; + testID?: string; + isMenuPrimaryAction: boolean; +} + +const MoreOptionsButton: React.FC = ({ + onPressMenuItem, + onPress, + actions, + testID = 'MoreOptionsButton', + isMenuPrimaryAction = false, +}) => { + const { colors } = useTheme(); + + return ( + + + + ); +}; + +export default MoreOptionsButton; + +const style = StyleSheet.create({ + buttonStyle: { + width: 30, + height: 30, + borderRadius: 15, + justifyContent: 'center', + alignContent: 'center', + }, +}); diff --git a/components/icons/SettingsButton.tsx b/components/icons/SettingsButton.tsx index 63ad9ab05..03f6666a1 100644 --- a/components/icons/SettingsButton.tsx +++ b/components/icons/SettingsButton.tsx @@ -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 ( - - - - - + ); }; export default SettingsButton; - -const style = StyleSheet.create({ - buttonStyle: { - width: 30, - height: 30, - borderRadius: 15, - justifyContent: 'center', - alignContent: 'center', - }, -}); diff --git a/components/types.ts b/components/types.ts index cca1824ec..5da1f52da 100644 --- a/components/types.ts +++ b/components/types.ts @@ -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 { diff --git a/loc/en.json b/loc/en.json index e82dcd61a..4d78945d9 100644 --- a/loc/en.json +++ b/loc/en.json @@ -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.", diff --git a/screen/wallets/ManageWallets.tsx b/screen/wallets/ManageWallets.tsx index 7be138614..4a7d3ae3d 100644 --- a/screen/wallets/ManageWallets.tsx +++ b/screen/wallets/ManageWallets.tsx @@ -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,7 @@ 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'; interface SaveChangesAction { type: typeof SAVE_CHANGES; @@ -86,14 +98,20 @@ interface RemoveWalletAction { payload: string; // Wallet ID } -type Action = +interface SetCurrentSortAction { + type: typeof SET_CURRENT_SORT; + payload: SortOption; +} + +type ReducerAction = | SetSearchQueryAction | SetIsSearchFocusedAction | SetInitialOrderAction | SetFilteredOrderAction | SetTempOrderAction | SaveChangesAction - | RemoveWalletAction; + | RemoveWalletAction + | SetCurrentSortAction; interface State { searchQuery: string; @@ -102,6 +120,7 @@ interface State { tempOrder: Item[]; wallets: TWallet[]; txMetadata: TTXMetadata; + currentSort: SortOption; } const initialState: State = { @@ -111,13 +130,14 @@ const initialState: State = { tempOrder: [], wallets: [], txMetadata: {}, + currentSort: 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 +201,10 @@ const reducer = (state: State, action: Action): State => { tempOrder: updatedOrder, }; } + case SET_CURRENT_SORT: + return { ...state, currentSort: action.payload }; default: - throw new Error(`Unhandled action type: ${(action as Action).type}`); + throw new Error(`Unhandled action type: ${(action as ReducerAction).type}`); } }; @@ -263,11 +285,126 @@ 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.currentSort === SortOption.ASC }, + { ...CommonToolTipActions.SortDESC, menuState: state.currentSort === SortOption.DESC }, + ], + }, + { + id: 'sort_by_wallet', + displayInline: true, + text: loc.wallets.sort_by_property, + subactions: [ + { + ...CommonToolTipActions.SortBalance, + menuState: state.currentSort === SortOption.Balance, + disabled: state.currentSort === SortOption.Balance, + }, + { + ...CommonToolTipActions.SortLabel, + menuState: state.currentSort === SortOption.Label, + disabled: state.currentSort === SortOption.Label, + }, + { + ...CommonToolTipActions.MostRecentTransaction, + menuState: state.currentSort === SortOption.MostRecent, + disabled: state.currentSort === SortOption.MostRecent, + }, + ], + }, + { ...CommonToolTipActions.Reset }, + ], + }, + ]; + }, [state.currentSort]); + + const moreOptionsOnPressMenuItem = useCallback( + (id: string) => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + switch (id) { + case CommonToolTipActions.SortASC.id: { + dispatch({ type: SET_CURRENT_SORT, 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, 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, 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, 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, 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 - a.data.getTransactions()[0]?.time; + }); + dispatch({ type: SET_TEMP_ORDER, payload: sortedWalletsByMostRecent }); + break; + } + case CommonToolTipActions.Reset.id: { + dispatch({ type: SET_TEMP_ORDER, payload: state.order }); + break; + } + } + }, + [state.order, state.tempOrder], + ); + const SaveButton = useMemo( () => , [handleClose, hasUnsavedChanges], ); + const MoreButton = useMemo( + () => , + [moreOptionsActions, moreOptionsOnPressMenuItem], + ); + + const HeaderRight = useCallback( + () => ( + <> + {MoreButton} + + {SaveButton} + + ), + [MoreButton, SaveButton], + ); + useLayoutEffect(() => { const searchBarOptions = { hideWhenScrolling: false, @@ -280,10 +417,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 +714,7 @@ const styles = StyleSheet.create({ dimmedText: { opacity: 0.8, }, + separation: { + width: 16, + }, }); diff --git a/typings/CommonToolTipActions.ts b/typings/CommonToolTipActions.ts index d7b9d3e37..cd2010095 100644 --- a/typings/CommonToolTipActions.ts +++ b/typings/CommonToolTipActions.ts @@ -50,6 +50,9 @@ const keys = { SortValue: 'sortValue', SortLabel: 'sortLabel', SortStatus: 'sortStatus', + SortBalance: 'sortBalance', + MostRecentTransaction: 'mostRecentTransaction', + Reset: 'reset', } as const; const icons = { @@ -94,6 +97,8 @@ const icons = { ClearClipboard: { iconValue: 'clipboard' }, SortASC: { iconValue: 'arrow.down.to.line' }, SortDESC: { iconValue: 'arrow.up.to.line' }, + MostRecentTransaction: { iconValue: 'clock' }, + Reset: { iconValue: 'arrow.counterclockwise' }, } as const; export const CommonToolTipActions = { @@ -309,6 +314,11 @@ export const CommonToolTipActions = { id: keys.ResetToDefault, text: loc.settings.electrum_reset, }, + Reset: { + id: keys.Reset, + text: loc.receive.reset, + Icon: icons.Reset, + }, ClearHistory: { id: keys.ClearHistory, text: loc.settings.electrum_clear, @@ -329,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, @@ -351,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;