REF: Refactor Batch UI to use FlatList

This commit is contained in:
Marcos Rodriguez Vélez 2020-11-10 09:21:54 -05:00 committed by GitHub
parent 826a0ea499
commit b0898fe501
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -14,8 +14,9 @@ import {
StyleSheet, StyleSheet,
Dimensions, Dimensions,
Platform, Platform,
ScrollView,
Text, Text,
LayoutAnimation,
FlatList,
} from 'react-native'; } from 'react-native';
import { Icon } from 'react-native-elements'; import { Icon } from 'react-native-elements';
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
@ -64,7 +65,6 @@ const styles = StyleSheet.create({
backgroundColor: BlueCurrentTheme.colors.elevated, backgroundColor: BlueCurrentTheme.colors.elevated,
}, },
scrollViewContent: { scrollViewContent: {
flexWrap: 'wrap',
flexDirection: 'row', flexDirection: 'row',
}, },
modalContent: { modalContent: {
@ -212,6 +212,7 @@ const styles = StyleSheet.create({
export default class SendDetails extends Component { export default class SendDetails extends Component {
static contextType = BlueStorageContext; static contextType = BlueStorageContext;
state = { isLoading: true }; state = { isLoading: true };
scrollView = React.createRef();
constructor(props, context) { constructor(props, context) {
super(props); super(props);
@ -254,7 +255,7 @@ export default class SendDetails extends Component {
feeUnit: fromWallet.getPreferredBalanceUnit(), feeUnit: fromWallet.getPreferredBalanceUnit(),
amountUnit: fromWallet.preferredBalanceUnit, // default for whole screen amountUnit: fromWallet.preferredBalanceUnit, // default for whole screen
renderWalletSelectionButtonHidden: false, renderWalletSelectionButtonHidden: false,
width: Dimensions.get('window').width - 320, width: Dimensions.get('window').width,
}; };
} }
} }
@ -450,15 +451,8 @@ export default class SendDetails extends Component {
} }
} }
if (error) { if (error) {
if (index === 0) { this.scrollView.current.scrollToIndex({ index });
this.scrollView.scrollTo(); this.setState({ isLoading: false });
} else if (index === this.state.addresses.length - 1) {
this.scrollView.scrollToEnd();
} else {
const page = Math.round(this.state.width * (this.state.addresses.length - 2));
this.scrollView.scrollTo({ x: page, y: 0, animated: true });
}
this.setState({ isLoading: false, recipientsScrollIndex: index });
alert(error); alert(error);
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
return; return;
@ -696,7 +690,8 @@ export default class SendDetails extends Component {
const feeSatoshi = new BigNumber(element.amount).multipliedBy(100000000); const feeSatoshi = new BigNumber(element.amount).multipliedBy(100000000);
return element.address.length > 0 && feeSatoshi > 0; return element.address.length > 0 && feeSatoshi > 0;
}) || this.state.addresses[0]; }) || this.state.addresses[0];
this.setState({ addresses: [firstTransaction], recipientsScrollIndex: 0 }, () => changeWallet()); LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState({ addresses: [firstTransaction] }, () => changeWallet());
}, },
style: 'default', style: 'default',
}, },
@ -718,7 +713,8 @@ export default class SendDetails extends Component {
return element.amount === BitcoinUnit.MAX; return element.amount === BitcoinUnit.MAX;
}) || this.state.addresses[0]; }) || this.state.addresses[0];
firstTransaction.amount = 0; firstTransaction.amount = 0;
this.setState({ addresses: [firstTransaction], recipientsScrollIndex: 0 }, () => changeWallet()); LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState({ addresses: [firstTransaction] }, () => changeWallet());
}, },
style: 'default', style: 'default',
}, },
@ -965,14 +961,15 @@ export default class SendDetails extends Component {
handleAddRecipient = () => { handleAddRecipient = () => {
const { addresses } = this.state; const { addresses } = this.state;
addresses.push(new BitcoinTransaction()); addresses.push(new BitcoinTransaction());
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut, () => this.scrollView.current.scrollToEnd());
this.setState( this.setState(
{ {
addresses, addresses,
isAdvancedTransactionOptionsVisible: false, isAdvancedTransactionOptionsVisible: false,
}, },
() => { () => {
this.scrollView.scrollToEnd(); this.scrollView.current.scrollToEnd();
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators(); if (this.state.addresses.length > 1) this.scrollView.current.flashScrollIndicators();
// after adding recipient it automatically scrolls to the last one // after adding recipient it automatically scrolls to the last one
this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 }); this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 });
}, },
@ -982,13 +979,14 @@ export default class SendDetails extends Component {
handleRemoveRecipient = () => { handleRemoveRecipient = () => {
const { addresses } = this.state; const { addresses } = this.state;
addresses.splice(this.state.recipientsScrollIndex, 1); addresses.splice(this.state.recipientsScrollIndex, 1);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState( this.setState(
{ {
addresses, addresses,
isAdvancedTransactionOptionsVisible: false, isAdvancedTransactionOptionsVisible: false,
}, },
() => { () => {
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators(); if (this.state.addresses.length > 1) this.scrollView.current.flashScrollIndicators();
// after deletion it automatically scrolls to the last one // after deletion it automatically scrolls to the last one
this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 }); this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 });
}, },
@ -1081,6 +1079,16 @@ export default class SendDetails extends Component {
this.setState({ isTransactionReplaceable: value }); this.setState({ isTransactionReplaceable: value });
}; };
scrollViewCurrentIndex = () => {
Keyboard.dismiss();
const offset = this.scrollView.current.contentOffset;
if (offset) {
const page = Math.round(offset.x / Dimensions.get('window').width);
return page;
}
return 0;
};
renderCreateButton = () => { renderCreateButton = () => {
return ( return (
<View style={styles.createButton}> <View style={styles.createButton}>
@ -1122,110 +1130,84 @@ export default class SendDetails extends Component {
); );
}; };
handlePageChange = e => { renderBitcoinTransactionInfoFields = ({ item, index }) => {
Keyboard.dismiss(); return (
const offset = e.nativeEvent.contentOffset; <View style={{ width: this.state.width }}>
if (offset) { <BlueBitcoinAmount
const page = Math.round(offset.x / this.state.width); isLoading={this.state.isLoading}
if (this.state.recipientsScrollIndex !== page) { amount={item.amount ? item.amount.toString() : null}
this.setState({ recipientsScrollIndex: page }); onAmountUnitChange={unit => {
} const units = this.state.units;
} units[index] = unit;
};
scrollViewCurrentIndex = () => { const addresses = this.state.addresses;
Keyboard.dismiss(); const item = addresses[index];
const offset = this.scrollView.contentOffset;
if (offset) {
const page = Math.round(offset.x / this.state.width);
return page;
}
return 0;
};
renderBitcoinTransactionInfoFields = () => { switch (unit) {
const rows = []; case BitcoinUnit.SATS:
item.amountSats = parseInt(item.amount);
break;
case BitcoinUnit.BTC:
item.amountSats = currency.btcToSatoshi(item.amount);
break;
case BitcoinUnit.LOCAL_CURRENCY:
// also accounting for cached fiat->sat conversion to avoid rounding error
item.amountSats =
BlueBitcoinAmount.getCachedSatoshis(item.amount) || currency.btcToSatoshi(currency.fiatToBTC(item.amount));
break;
}
for (const [index, item] of this.state.addresses.entries()) { addresses[index] = item;
rows.push( this.setState({ units, addresses });
<View key={index} style={{ width: this.state.width }}> }}
<BlueBitcoinAmount onChangeText={text => {
isLoading={this.state.isLoading} item.amount = text;
amount={item.amount ? item.amount.toString() : null} switch (this.state.units[index] || this.state.amountUnit) {
onAmountUnitChange={unit => { case BitcoinUnit.BTC:
const units = this.state.units; item.amountSats = currency.btcToSatoshi(item.amount);
units[index] = unit; break;
case BitcoinUnit.LOCAL_CURRENCY:
const addresses = this.state.addresses; item.amountSats = currency.btcToSatoshi(currency.fiatToBTC(item.amount));
const item = addresses[index]; break;
default:
switch (unit) { case BitcoinUnit.SATS:
case BitcoinUnit.SATS: item.amountSats = parseInt(text);
item.amountSats = parseInt(item.amount); break;
break; }
case BitcoinUnit.BTC: const addresses = this.state.addresses;
item.amountSats = currency.btcToSatoshi(item.amount); addresses[index] = item;
break; this.setState({ addresses }, this.reCalcTx);
case BitcoinUnit.LOCAL_CURRENCY: }}
// also accounting for cached fiat->sat conversion to avoid rounding error unit={this.state.units[index] || this.state.amountUnit}
item.amountSats = inputAccessoryViewID={this.state.fromWallet.allowSendMax() ? BlueUseAllFundsButton.InputAccessoryViewID : null}
BlueBitcoinAmount.getCachedSatoshis(item.amount) || currency.btcToSatoshi(currency.fiatToBTC(item.amount)); />
break; <BlueAddressInput
} onChangeText={async text => {
text = text.trim();
addresses[index] = item; const transactions = this.state.addresses;
this.setState({ units, addresses }); const { address, amount, memo, payjoinUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(text);
}} item.address = address || text;
onChangeText={text => { item.amount = amount || item.amount;
item.amount = text; transactions[index] = item;
switch (this.state.units[index] || this.state.amountUnit) { this.setState({
case BitcoinUnit.BTC: addresses: transactions,
item.amountSats = currency.btcToSatoshi(item.amount); memo: memo || this.state.memo,
break; isLoading: false,
case BitcoinUnit.LOCAL_CURRENCY: payjoinUrl,
item.amountSats = currency.btcToSatoshi(currency.fiatToBTC(item.amount)); });
break; this.reCalcTx();
default: }}
case BitcoinUnit.SATS: onBarScanned={this.processAddressData}
item.amountSats = parseInt(text); address={item.address}
break; isLoading={this.state.isLoading}
} inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
const addresses = this.state.addresses; launchedBy={this.props.route.name}
addresses[index] = item; />
this.setState({ addresses }, this.reCalcTx); {this.state.addresses.length > 1 && (
}} <BlueText style={styles.of}>{loc.formatString(loc._.of, { number: index + 1, total: this.state.addresses.length })}</BlueText>
unit={this.state.units[index] || this.state.amountUnit} )}
inputAccessoryViewID={this.state.fromWallet.allowSendMax() ? BlueUseAllFundsButton.InputAccessoryViewID : null} </View>
/> );
<BlueAddressInput
onChangeText={async text => {
text = text.trim();
const transactions = this.state.addresses;
const { address, amount, memo, payjoinUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(text);
item.address = address || text;
item.amount = amount || item.amount;
transactions[index] = item;
this.setState({
addresses: transactions,
memo: memo || this.state.memo,
isLoading: false,
payjoinUrl,
});
this.reCalcTx();
}}
onBarScanned={this.processAddressData}
address={item.address}
isLoading={this.state.isLoading}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
launchedBy={this.props.route.name}
/>
{this.state.addresses.length > 1 && (
<BlueText style={styles.of}>{loc.formatString(loc._.of, { number: index + 1, total: this.state.addresses.length })}</BlueText>
)}
</View>,
);
}
return rows;
}; };
onUseAllPressed = () => { onUseAllPressed = () => {
@ -1238,13 +1220,13 @@ export default class SendDetails extends Component {
text: loc._.ok, text: loc._.ok,
onPress: async () => { onPress: async () => {
Keyboard.dismiss(); Keyboard.dismiss();
const recipient = this.state.addresses[this.state.recipientsScrollIndex]; const recipient = this.state.addresses[this.scrollViewCurrentIndex()];
recipient.amount = BitcoinUnit.MAX; recipient.amount = BitcoinUnit.MAX;
recipient.amountSats = BitcoinUnit.MAX; recipient.amountSats = BitcoinUnit.MAX;
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState({ this.setState({
addresses: [recipient], addresses: [recipient],
units: [BitcoinUnit.BTC], units: [BitcoinUnit.BTC],
recipientsScrollIndex: 0,
isAdvancedTransactionOptionsVisible: false, isAdvancedTransactionOptionsVisible: false,
}); });
}, },
@ -1271,6 +1253,8 @@ export default class SendDetails extends Component {
this.setState({ width: e.nativeEvent.layout.width }); this.setState({ width: e.nativeEvent.layout.width });
}; };
keyExtractor = (_item, index) => `${index}`;
render() { render() {
if (this.state.isLoading || typeof this.state.fromWallet === 'undefined') { if (this.state.isLoading || typeof this.state.fromWallet === 'undefined') {
return ( return (
@ -1286,20 +1270,20 @@ export default class SendDetails extends Component {
<StatusBar barStyle="light-content" /> <StatusBar barStyle="light-content" />
<View> <View>
<KeyboardAvoidingView behavior="position"> <KeyboardAvoidingView behavior="position">
<ScrollView <FlatList
pagingEnabled
horizontal
contentContainerStyle={styles.scrollViewContent}
ref={ref => (this.scrollView = ref)}
keyboardShouldPersistTaps="always" keyboardShouldPersistTaps="always"
onContentSizeChange={() => this.scrollView.scrollToEnd()}
onLayout={() => this.scrollView.scrollToEnd()}
onMomentumScrollEnd={this.handlePageChange}
scrollEnabled={this.state.addresses.length > 1} scrollEnabled={this.state.addresses.length > 1}
extraData={this.state.addresses}
data={this.state.addresses}
renderItem={this.renderBitcoinTransactionInfoFields}
keyExtractor={this.keyExtractor}
ref={this.scrollView}
horizontal
pagingEnabled
onMomentumScrollBegin={Keyboard.dismiss}
scrollIndicatorInsets={{ top: 0, left: 8, bottom: 0, right: 8 }} scrollIndicatorInsets={{ top: 0, left: 8, bottom: 0, right: 8 }}
> contentContainerStyle={styles.scrollViewContent}
{this.renderBitcoinTransactionInfoFields()} />
</ScrollView>
<View hide={!this.state.showMemoRow} style={styles.memo}> <View hide={!this.state.showMemoRow} style={styles.memo}>
<TextInput <TextInput
onChangeText={text => this.setState({ memo: text })} onChangeText={text => this.setState({ memo: text })}