REF: Hierarchy navigator screen

This commit is contained in:
João Dias 2021-07-06 06:37:15 -03:00 committed by GitHub
parent 370f79f47e
commit fc93e9243e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 274 additions and 79 deletions

View File

@ -1,6 +1,6 @@
import React, { useRef } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useTheme } from '@react-navigation/native';
import { useNavigation, useTheme } from '@react-navigation/native';
import { ListItem } from 'react-native-elements';
import PropTypes from 'prop-types';
import { AddressTypeBadge } from './AddressTypeBadge';
@ -9,11 +9,13 @@ import TooltipMenu from '../TooltipMenu';
import Clipboard from '@react-native-clipboard/clipboard';
import Share from 'react-native-share';
const AddressItem = ({ item, balanceUnit, onPress }) => {
const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }) => {
const { colors } = useTheme();
const tooltip = useRef();
const listItem = useRef();
const hasTransactions = item.transactions > 0;
const stylesHook = StyleSheet.create({
container: {
borderBottomColor: colors.lightBorder,
@ -28,8 +30,33 @@ const AddressItem = ({ item, balanceUnit, onPress }) => {
balance: {
color: colors.alternativeTextColor,
},
address: {
color: hasTransactions ? colors.darkGray : colors.buttonTextColor,
},
});
const { navigate } = useNavigation();
const navigateToReceive = () => {
navigate('ReceiveDetailsRoot', {
screen: 'ReceiveDetails',
params: {
walletID,
address: item.address,
},
});
};
const navigateToSignVerify = () => {
navigate('SignVerifyRoot', {
screen: 'SignVerify',
params: {
walletID,
address: item.address,
},
});
};
const showToolTipMenu = () => {
tooltip.current.showMenu();
};
@ -44,30 +71,40 @@ const AddressItem = ({ item, balanceUnit, onPress }) => {
Share.open({ message: item.address }).catch(error => console.log(error));
};
const getAvailableActions = () => {
const actions = [
{
id: 'copyToClipboard',
text: loc.transactions.details_copy,
onPress: handleCopyPress,
},
{
id: 'share',
text: loc.receive.details_share,
onPress: handleSharePress,
},
];
if (allowSignVerifyMessage) {
actions.push({
id: 'signVerify',
text: loc.addresses.sign_title,
onPress: navigateToSignVerify,
});
}
return actions;
};
const render = () => {
return (
<View>
<TooltipMenu
ref={tooltip}
anchorRef={listItem}
actions={[
{
id: 'copyToClipboard',
text: loc.transactions.details_copy,
onPress: handleCopyPress,
},
{
id: 'share',
text: loc.receive.details_share,
onPress: handleSharePress,
},
]}
/>
<TooltipMenu ref={tooltip} anchorRef={listItem} actions={getAvailableActions()} />
<ListItem
ref={listItem}
key={`${item.key}`}
button
onPress={onPress}
onPress={navigateToReceive}
containerStyle={stylesHook.container}
onLongPress={showToolTipMenu}
>
@ -76,9 +113,16 @@ const AddressItem = ({ item, balanceUnit, onPress }) => {
<Text style={[styles.index, stylesHook.index]}>{item.index + 1}</Text>{' '}
<Text style={[stylesHook.address, styles.address]}>{item.address}</Text>
</ListItem.Title>
<ListItem.Subtitle style={[stylesHook.list, styles.balance, stylesHook.balance]}>{balance}</ListItem.Subtitle>
<View style={styles.subtitle}>
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>{balance}</Text>
</View>
</ListItem.Content>
<AddressTypeBadge isInternal={item.isInternal} />
<View style={styles.labels}>
<AddressTypeBadge isInternal={item.isInternal} hasTransactions={hasTransactions} />
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>
{loc.addresses.transactions}: {item.transactions}
</Text>
</View>
</ListItem>
</View>
);
@ -89,7 +133,7 @@ const AddressItem = ({ item, balanceUnit, onPress }) => {
const styles = StyleSheet.create({
address: {
fontWeight: '600',
fontWeight: 'bold',
marginHorizontal: 40,
},
index: {
@ -99,6 +143,12 @@ const styles = StyleSheet.create({
marginTop: 8,
marginLeft: 14,
},
subtitle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
},
});
AddressItem.propTypes = {

View File

@ -9,27 +9,46 @@ const styles = StyleSheet.create({
paddingVertical: 4,
paddingHorizontal: 10,
borderRadius: 20,
alignSelf: 'flex-end',
},
badgeText: {
fontSize: 12,
textAlign: 'center',
},
});
const AddressTypeBadge = ({ isInternal }) => {
const AddressTypeBadge = ({ isInternal, hasTransactions }) => {
const { colors } = useTheme();
const stylesHook = StyleSheet.create({
changeBadge: { backgroundColor: colors.changeBackground },
receiveBadge: { backgroundColor: colors.receiveBackground },
usedBadge: { backgroundColor: colors.buttonDisabledBackgroundColor },
changeText: { color: colors.changeText },
receiveText: { color: colors.receiveText },
usedText: { color: colors.alternativeTextColor },
});
const badgeLabel = isInternal ? loc.addresses.type_change : loc.addresses.type_receive;
// eslint-disable-next-line prettier/prettier
const badgeLabel = hasTransactions
? loc.addresses.type_used
: isInternal
? loc.addresses.type_change
: loc.addresses.type_receive;
const badgeStyle = isInternal ? stylesHook.changeBadge : stylesHook.receiveBadge;
// eslint-disable-next-line prettier/prettier
const badgeStyle = hasTransactions
? stylesHook.usedBadge
: isInternal
? stylesHook.changeBadge
: stylesHook.receiveBadge;
const textStyle = isInternal ? stylesHook.changeText : stylesHook.receiveText;
// eslint-disable-next-line prettier/prettier
const textStyle = hasTransactions
? stylesHook.usedText
: isInternal
? stylesHook.changeText
: stylesHook.receiveText;
return (
<View style={[styles.container, badgeStyle]}>
@ -40,6 +59,7 @@ const AddressTypeBadge = ({ isInternal }) => {
AddressTypeBadge.propTypes = {
isInternal: PropTypes.bool,
hasTransactions: PropTypes.bool,
};
export { AddressTypeBadge };

View File

@ -0,0 +1,94 @@
import { useTheme } from '@react-navigation/native';
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import loc from '../../loc';
export const TABS = {
EXTERNAL: 'receive',
INTERNAL: 'change',
};
const AddressTypeTabs = ({ currentTab, setCurrentTab }) => {
const { colors } = useTheme();
const stylesHook = StyleSheet.create({
activeTab: {
backgroundColor: colors.modal,
},
activeText: {
fontWeight: 'bold',
color: colors.foregroundColor,
},
inactiveTab: {
fontWeight: 'normal',
color: colors.foregroundColor,
},
backTabs: {
backgroundColor: colors.buttonDisabledBackgroundColor,
},
});
const tabs = Object.entries(TABS).map(([key, value]) => {
return {
key,
value,
name: loc.addresses[`type_${value}`],
};
});
const changeToTab = tabKey => {
if (tabKey in TABS) {
setCurrentTab(TABS[tabKey]);
}
};
const render = () => {
const tabsButtons = tabs.map(tab => {
const isActive = tab.value === currentTab;
const tabStyle = isActive ? stylesHook.activeTab : stylesHook.inactiveTab;
const textStyle = isActive ? stylesHook.activeText : stylesHook.inactiveTab;
return (
<View key={tab.key} onPress={() => changeToTab(tab.key)} style={[styles.tab, tabStyle]}>
<Text onPress={() => changeToTab(tab.key)} style={textStyle}>{tab.name}</Text>
</View>
);
});
return (
<View style={styles.container}>
<View style={[stylesHook.backTabs, styles.backTabs]}>
<View style={styles.tabs}>{tabsButtons}</View>
</View>
</View>
);
};
return render();
};
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
backTabs: {
padding: 4,
marginVertical: 8,
borderRadius: 8,
},
tabs: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
tab: {
borderRadius: 6,
paddingVertical: 8,
paddingHorizontal: 16,
},
});
export { AddressTypeTabs };

View File

@ -579,7 +579,6 @@
"sign_placeholder_signature": "التوقيع",
"sign_aopp_title": "AOPP",
"sign_aopp_confirm": "هل تريد إرسال رسالة موقعة إلى {hostname}؟",
"address_balance": "الرصيد: {Balance} ساتوشي",
"addresses_title": "العنوان",
"type_change": "تغيير",
"type_receive": "استلام"

View File

@ -579,7 +579,6 @@
"sign_placeholder_signature": "Podpis",
"sign_aopp_title": "AOPP",
"sign_aopp_confirm": "Chcete poslat podepsanou zprávu uživateli {hostname}?",
"address_balance": "Zůstatek: {balance} sats",
"addresses_title": "Adresy",
"type_change": "Změnit",
"type_receive": "Příjmout"

View File

@ -283,7 +283,6 @@
"sign_placeholder_address": "Cyfeiriad",
"sign_placeholder_message": "Neges",
"sign_placeholder_signature": "Llofnod",
"address_balance": "Balans: {balance} sats",
"addresses_title": "Cyfeiriadau",
"type_change": "Newid",
"type_receive": "Derbyn"

View File

@ -579,7 +579,6 @@
"sign_placeholder_signature": "Signatur",
"sign_aopp_title": "AOPP",
"sign_aopp_confirm": "Senden der signierten Nachricht nach {hostname}?",
"address_balance": "Guthaben: {balance} sats",
"addresses_title": "Adressen",
"type_change": "Wechsel",
"type_receive": "Empfang"

View File

@ -579,10 +579,11 @@
"sign_placeholder_signature": "Signature",
"sign_aopp_title": "AOPP",
"sign_aopp_confirm": "Do you want to send signed message to {hostname}?",
"address_balance": "Balance: {balance} sats",
"addresses_title": "Addresses",
"type_change": "Change",
"type_receive": "Receive"
"type_receive": "Receive",
"type_used": "Used",
"transactions": "Transactions"
},
"aopp": {
"title": "Select Address",

View File

@ -579,7 +579,6 @@
"sign_placeholder_signature": "Firma",
"sign_aopp_title": "AOPP",
"sign_aopp_confirm": "¿Quieres enviar un mensaje firmado a {hostname}?",
"address_balance": "Saldo: {balance} sats",
"addresses_title": "Direcciones",
"type_change": "Cambio",
"type_receive": "Recibir"

View File

@ -579,7 +579,6 @@
"sign_placeholder_signature": "امضا",
"sign_aopp_title": "پروتکل اثبات مالکیت آدرس (AOPP)",
"sign_aopp_confirm": "آیا مایل به ارسال پیام امضاشده به {hostname} هستید؟",
"address_balance": "موجودی: {balance} ساتوشی",
"addresses_title": "آدرس‌ها",
"type_change": "باقی‌مانده",
"type_receive": "دریافت"

View File

@ -579,7 +579,6 @@
"sign_placeholder_signature": "Signature",
"sign_aopp_title": "AOPP",
"sign_aopp_confirm": "Voulez-vous envoyer un message signé à {hostname} ?",
"address_balance": "Solde : {balance} sats",
"addresses_title": "Adresses",
"type_change": "Monnaie",
"type_receive": "Réception"

View File

@ -579,7 +579,6 @@
"sign_placeholder_signature": "Podpis",
"sign_aopp_title": "AOPP",
"sign_aopp_confirm": "Czy chcesz wysłać podpisaną wiadomość do [hostname]?",
"address_balance": "Saldo: {balance} sats",
"addresses_title": "Adresy",
"type_change": "Reszta",
"type_receive": "Otrzymaj"

View File

@ -578,7 +578,6 @@
"sign_placeholder_signature": "Assinatura",
"sign_aopp_title": "AOPP",
"sign_aopp_confirm": "Você deseja enviar a mensagem assinada para {hostname}?",
"address_balance": "Saldo: {balance} sats",
"addresses_title": "Endereços",
"type_change": "Troco",
"type_receive": "Receber"

View File

@ -578,7 +578,6 @@
"sign_placeholder_signature": "Podpis",
"sign_aopp_title": "AOPP",
"sign_aopp_confirm": "Ali želite podpisano sporočilo poslati na {hostname}?",
"address_balance": "Stanje: {balance} sats",
"addresses_title": "Naslovi",
"type_change": "Vračilo",
"type_receive": "Prejemni"

View File

@ -1,11 +1,12 @@
import React, { useCallback, useState, useContext, useRef, useEffect } from 'react';
import { ActivityIndicator, FlatList, StyleSheet, View, StatusBar } from 'react-native';
import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native';
import { useFocusEffect, useRoute, useTheme } from '@react-navigation/native';
import Privacy from '../../blue_modules/Privacy';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import loc from '../../loc';
import navigationStyle from '../../components/navigationStyle';
import { AddressItem } from '../../components/addresses/AddressItem';
import { AddressTypeTabs, TABS } from '../../components/addresses/AddressTypeTabs';
import { WatchOnlyWallet } from '../../class';
export const totalBalance = ({ c, u } = { c: 0, u: 0 }) => c + u;
@ -13,13 +14,16 @@ export const totalBalance = ({ c, u } = { c: 0, u: 0 }) => c + u;
export const getAddress = (wallet, index, isInternal) => {
let address;
let balance = 0;
let transactions = 0;
if (isInternal) {
address = wallet._getInternalAddressByIndex(index);
balance = totalBalance(wallet._balances_by_internal_index[index]);
transactions = wallet._txs_by_internal_index[index].length;
} else {
address = wallet._getExternalAddressByIndex(index);
balance = totalBalance(wallet._balances_by_external_index[index]);
transactions = wallet._txs_by_external_index[index].length;
}
return {
@ -28,16 +32,22 @@ export const getAddress = (wallet, index, isInternal) => {
address,
isInternal,
balance,
transactions: 0,
transactions,
};
};
export const sortByIndexAndType = (a, b) => {
if (a.isInternal > b.isInternal) return 1;
if (a.isInternal < b.isInternal) return -1;
export const sortByAddressIndex = (a, b) => {
if (a.index > b.index) {
return 1;
}
return -1;
};
if (a.index > b.index) return 1;
if (a.index < b.index) return -1;
export const filterByAddressType = (type, isInternal, currentType) => {
if (currentType === type) {
return isInternal === true;
}
return isInternal === false;
};
const WalletAddresses = () => {
@ -45,6 +55,8 @@ const WalletAddresses = () => {
const [addresses, setAddresses] = useState([]);
const [currentTab, setCurrentTab] = useState(TABS.EXTERNAL);
const { wallets } = useContext(BlueStorageContext);
const { walletID } = useRoute().params;
@ -55,18 +67,25 @@ const WalletAddresses = () => {
const balanceUnit = wallet.getPreferredBalanceUnit();
const walletInstance = wallet.type === WatchOnlyWallet.type ? wallet._hdWalletInstance : wallet;
const isWatchOnly = wallet.type === WatchOnlyWallet.type;
const walletInstance = isWatchOnly ? wallet._hdWalletInstance : wallet;
const allowSignVerifyMessage = 'allowSignVerifyMessage' in wallet && wallet.allowSignVerifyMessage();
const { colors } = useTheme();
const { navigate } = useNavigation();
const stylesHook = StyleSheet.create({
root: {
backgroundColor: colors.elevated,
},
});
// computed property
const filteredAddresses = addresses
.filter(address => filterByAddressType(TABS.INTERNAL, address.isInternal, currentTab))
.sort(sortByAddressIndex);
useEffect(() => {
if (showAddresses) {
addressList.current.scrollToIndex({ animated: false, index: 0 });
@ -76,7 +95,7 @@ const WalletAddresses = () => {
const getAddresses = () => {
const addressList = [];
for (let index = 0; index < walletInstance.next_free_change_address_index + walletInstance.gap_limit; index++) {
for (let index = 0; index <= walletInstance.next_free_change_address_index; index++) {
const address = getAddress(walletInstance, index, true);
addressList.push(address);
@ -88,7 +107,7 @@ const WalletAddresses = () => {
addressList.push(address);
}
setAddresses(addressList.sort(sortByIndexAndType));
setAddresses(addressList);
setShowAddresses(true);
};
@ -102,18 +121,8 @@ const WalletAddresses = () => {
}, []),
);
const navigateToReceive = item => {
navigate('ReceiveDetailsRoot', {
screen: 'ReceiveDetails',
params: {
walletID,
address: item.item.address,
},
});
};
const renderRow = item => {
return <AddressItem {...item} balanceUnit={balanceUnit} onPress={() => navigateToReceive(item)} />;
return <AddressItem {...item} balanceUnit={balanceUnit} walletID={walletID} allowSignVerifyMessage={allowSignVerifyMessage} />;
};
return (
@ -122,13 +131,14 @@ const WalletAddresses = () => {
<FlatList
contentContainerStyle={stylesHook.root}
ref={addressList}
data={addresses}
extraData={addresses}
initialNumToRender={40}
data={filteredAddresses}
extraData={filteredAddresses}
initialNumToRender={20}
renderItem={renderRow}
ListEmptyComponent={<ActivityIndicator />}
centerContent={!showAddresses}
contentInsetAdjustmentBehavior="automatic"
ListHeaderComponent={<AddressTypeTabs currentTab={currentTab} setCurrentTab={setCurrentTab} />}
/>
</View>
);

View File

@ -1,21 +1,49 @@
import assert from 'assert';
import { getAddress, sortByIndexAndType, totalBalance } from '../../screen/wallets/addresses';
import { getAddress, sortByAddressIndex, totalBalance, filterByAddressType } from '../../screen/wallets/addresses';
import { TABS } from '../../components/addresses/AddressTypeTabs';
const mockAddressesList = [
{ index: 2, isInternal: false, key: 'third_external_address' },
{ index: 0, isInternal: true, key: 'first_internal_address' },
{ index: 1, isInternal: false, key: 'second_external_address' },
{ index: 1, isInternal: true, key: 'second_internal_address' },
{ index: 0, isInternal: false, key: 'first_external_address' },
];
describe('Addresses', () => {
it('Sort by index ASC and externals first', () => {
const originalList = [
{ index: 0, isInternal: true, key: 'first_internal_address' },
{ index: 1, isInternal: false, key: 'second_external_address' },
{ index: 1, isInternal: true, key: 'second_internal_address' },
{ index: 0, isInternal: false, key: 'first_external_address' },
];
it('Sort by index', () => {
const sortedList = mockAddressesList.sort(sortByAddressIndex);
const sortedList = originalList.sort(sortByIndexAndType);
assert.strictEqual(sortedList[0].index, 0);
assert.strictEqual(sortedList[2].index, 1);
assert.strictEqual(sortedList[4].index, 2);
});
assert.strictEqual(sortedList[0].key, 'first_external_address');
assert.strictEqual(sortedList[1].key, 'second_external_address');
assert.strictEqual(sortedList[2].key, 'first_internal_address');
assert.strictEqual(sortedList[3].key, 'second_internal_address');
it('Have tabs defined', () => {
const tabsEnum = {
EXTERNAL: 'receive',
INTERNAL: 'change',
};
assert.deepStrictEqual(TABS, tabsEnum);
});
it('Filter by type', () => {
let currentTab = TABS.EXTERNAL;
const externalAddresses = mockAddressesList.filter(address => filterByAddressType(TABS.INTERNAL, address.isInternal, currentTab));
currentTab = TABS.INTERNAL;
const internalAddresses = mockAddressesList.filter(address => filterByAddressType(TABS.INTERNAL, address.isInternal, currentTab));
externalAddresses.forEach(address => {
assert.strictEqual(address.isInternal, false);
});
internalAddresses.forEach(address => {
assert.strictEqual(address.isInternal, true);
});
});
it('Sum confirmed/unconfirmed balance', () => {
@ -34,6 +62,8 @@ describe('Addresses', () => {
_getInternalAddressByIndex: index => `internal_address_${index}`,
_balances_by_external_index: [{ c: 0, u: 0 }],
_balances_by_internal_index: [{ c: 0, u: 0 }],
_txs_by_external_index: { 0: [{}] },
_txs_by_internal_index: { 0: [{}, {}] },
};
const firstExternalAddress = getAddress(fakeWallet, 0, false);
@ -45,7 +75,7 @@ describe('Addresses', () => {
index: 0,
isInternal: false,
key: 'external_address_0',
transactions: 0,
transactions: 1,
});
assert.deepStrictEqual(firstInternalAddress, {
@ -54,7 +84,7 @@ describe('Addresses', () => {
index: 0,
isInternal: true,
key: 'internal_address_0',
transactions: 0,
transactions: 2,
});
});
});