tip button on channels, modal send tip component #100
7 changed files with 267 additions and 83 deletions
18
src/component/modalTipView/index.js
Normal file
18
src/component/modalTipView/index.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doSendTip, doToast, selectBalance } from 'lbry-redux';
|
||||
import ModalTipView from './view';
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
notify: data => dispatch(doToast(data)),
|
||||
sendTip: (amount, claimId, isSupport, successCallback, errorCallback) =>
|
||||
dispatch(doSendTip(amount, claimId, isSupport, successCallback, errorCallback)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(ModalTipView);
|
139
src/component/modalTipView/view.js
Normal file
139
src/component/modalTipView/view.js
Normal file
|
@ -0,0 +1,139 @@
|
|||
import React from 'react';
|
||||
import { ActivityIndicator, Alert, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { formatCredits } from 'lbry-redux';
|
||||
import modalStyle from 'styles/modal';
|
||||
import modalTipStyle from 'styles/modalTip';
|
||||
import Button from 'component/button';
|
||||
import Colors from 'styles/colors';
|
||||
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import Link from 'component/link';
|
||||
|
||||
export default class ModalTipView extends React.PureComponent {
|
||||
state = {
|
||||
creditsInputFocused: false,
|
||||
sendTipStarted: false,
|
||||
tipAmount: null,
|
||||
};
|
||||
|
||||
handleSendTip = () => {
|
||||
const { claim, balance, notify, onSendTipFailed, onSendTipSuccessful, sendTip } = this.props;
|
||||
const { tipAmount } = this.state;
|
||||
|
||||
if (tipAmount > balance) {
|
||||
notify({
|
||||
message: 'Insufficient credits',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = parseInt(tipAmount, 10);
|
||||
const message =
|
||||
amount === 1
|
||||
? __('Are you sure you want to tip %amount% credit', { amount })
|
||||
: __('Are you sure you want to tip %amount% credits', { amount });
|
||||
|
||||
Alert.alert(
|
||||
__('Send tip'),
|
||||
message,
|
||||
[
|
||||
{ text: __('No') },
|
||||
{
|
||||
text: __('Yes'),
|
||||
onPress: () => {
|
||||
this.setState({ sendTipStarted: true }, () =>
|
||||
sendTip(
|
||||
tipAmount,
|
||||
claim.claim_id,
|
||||
false,
|
||||
() => {
|
||||
// success
|
||||
this.setState({ tipAmount: null, sendTipStarted: false });
|
||||
if (onSendTipSuccessful) onSendTipSuccessful();
|
||||
},
|
||||
() => {
|
||||
// error
|
||||
if (onSendTipFailed) onSendTipFailed();
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
{ cancelable: true }
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { balance, channelName, contentName, onCancelPress, onOverlayPress } = this.props;
|
||||
const canSendTip = this.state.tipAmount > 0;
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={modalStyle.overlay} activeOpacity={1} onPress={onOverlayPress}>
|
||||
<TouchableOpacity style={modalStyle.container} activeOpacity={1}>
|
||||
<View style={modalTipStyle.container}>
|
||||
<Text style={modalTipStyle.title} numberOfLines={1}>
|
||||
{channelName ? __('Send a tip to %channel%', { channel: channelName }) : __('Send a tip')}
|
||||
</Text>
|
||||
|
||||
<View style={modalTipStyle.row}>
|
||||
<View style={modalTipStyle.amountRow}>
|
||||
<TextInput
|
||||
editable={!this.state.sendTipStarted}
|
||||
ref={ref => (this.tipAmountInput = ref)}
|
||||
onChangeText={value => this.setState({ tipAmount: value })}
|
||||
underlineColorAndroid={Colors.NextLbryGreen}
|
||||
keyboardType={'numeric'}
|
||||
onFocus={() => this.setState({ creditsInputFocused: true })}
|
||||
onBlur={() => this.setState({ creditsInputFocused: false })}
|
||||
placeholder={'0'}
|
||||
value={this.state.tipAmount}
|
||||
selectTextOnFocus
|
||||
style={modalTipStyle.tipAmountInput}
|
||||
/>
|
||||
<Text style={modalTipStyle.currency}>LBC</Text>
|
||||
<View style={modalTipStyle.balance}>
|
||||
{this.state.creditsInputFocused && <Icon name="coins" size={12} />}
|
||||
{this.state.creditsInputFocused && (
|
||||
<Text style={modalTipStyle.balanceText}>{formatCredits(parseFloat(balance), 1, true)}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{this.state.sendTipStarted && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />}
|
||||
</View>
|
||||
|
||||
<View style={modalTipStyle.info}>
|
||||
<Text style={modalTipStyle.infoText}>
|
||||
{__(
|
||||
'This will appear as a tip for %content%, which will boost its ability to be discovered while active.',
|
||||
{ content: contentName }
|
||||
)}
|
||||
</Text>
|
||||
<Link
|
||||
style={modalTipStyle.learnMoreLink}
|
||||
text={__('Learn more.')}
|
||||
href={'https://lbry.com/faq/tipping'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={modalTipStyle.buttonRow}>
|
||||
<Link
|
||||
style={modalTipStyle.cancelTipLink}
|
||||
text={__('Cancel')}
|
||||
onPress={() => {
|
||||
if (onCancelPress) onCancelPress();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
text={__('Send')}
|
||||
style={modalTipStyle.button}
|
||||
disabled={!canSendTip || this.state.sendTipStarted}
|
||||
onPress={this.handleSendTip}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -57,7 +57,6 @@ window.__ = __;
|
|||
|
||||
const globalExceptionHandler = (error, isFatal) => {
|
||||
if (error && NativeModules.Firebase) {
|
||||
console.log(error);
|
||||
NativeModules.Firebase.logException(isFatal, error.message ? error.message : 'No message', JSON.stringify(error));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ import EmptyStateView from 'component/emptyStateView';
|
|||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import Link from 'component/link';
|
||||
import ModalPicker from 'component/modalPicker';
|
||||
import ModalTipView from 'component/modalTipView';
|
||||
import PageHeader from 'component/pageHeader';
|
||||
import SubscribeButton from 'component/subscribeButton';
|
||||
import SubscribeNotificationButton from 'component/subscribeNotificationButton';
|
||||
|
@ -36,6 +37,7 @@ class ChannelPage extends React.PureComponent {
|
|||
autoStyle: null,
|
||||
showSortPicker: false,
|
||||
showTimePicker: false,
|
||||
showTipView: false,
|
||||
orderBy: ['release_time'], // sort by new by default
|
||||
activeTab: Constants.CONTENT_TAB,
|
||||
currentSortByItem: Constants.CLAIM_SEARCH_SORT_BY_ITEMS[1], // should always default to sorting channel pages by new
|
||||
|
@ -176,6 +178,10 @@ class ChannelPage extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
onTipPressed = () => {
|
||||
this.setState({ showTipView: true });
|
||||
};
|
||||
|
||||
onSharePressed = () => {
|
||||
const { claim } = this.props;
|
||||
if (claim) {
|
||||
|
@ -212,7 +218,7 @@ class ChannelPage extends React.PureComponent {
|
|||
render() {
|
||||
const { channels, claim, navigation, uri, drawerStack, popDrawerStack, subCount, timeItem } = this.props;
|
||||
const { name, permanent_url: permanentUrl } = claim;
|
||||
const { autoStyle, currentSortByItem, showSortPicker, showTimePicker } = this.state;
|
||||
const { autoStyle, currentSortByItem, showSortPicker, showTimePicker, showTipView } = this.state;
|
||||
const ownedChannel = channels ? channels.map(channel => channel.permanent_url).includes(permanentUrl) : false;
|
||||
|
||||
let thumbnailUrl,
|
||||
|
@ -297,6 +303,12 @@ class ChannelPage extends React.PureComponent {
|
|||
icon={'share-alt'}
|
||||
onPress={this.onSharePressed}
|
||||
/>
|
||||
<Button
|
||||
style={[channelPageStyle.actionButton, channelPageStyle.tipButton]}
|
||||
theme={'light'}
|
||||
icon={'gift'}
|
||||
onPress={this.onTipPressed}
|
||||
/>
|
||||
{!ownedChannel && <SubscribeButton style={channelPageStyle.subscribeButton} uri={fullUri} name={name} />}
|
||||
{false && !ownedChannel && (
|
||||
<SubscribeNotificationButton
|
||||
|
@ -347,6 +359,16 @@ class ChannelPage extends React.PureComponent {
|
|||
items={Constants.CLAIM_SEARCH_TIME_ITEMS}
|
||||
/>
|
||||
)}
|
||||
{showTipView && (
|
||||
<ModalTipView
|
||||
claim={claim}
|
||||
channelName={claim.name}
|
||||
contentName={title}
|
||||
onCancelPress={() => this.setState({ showTipView: false })}
|
||||
onOverlayPress={() => this.setState({ showTipView: false })}
|
||||
onSendTipSuccessful={() => this.setState({ showTipView: false })}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import FilePrice from 'component/filePrice';
|
|||
import FloatingWalletBalance from 'component/floatingWalletBalance';
|
||||
import Link from 'component/link';
|
||||
import MediaPlayer from 'component/mediaPlayer';
|
||||
import ModalTipView from 'component/modalTipView';
|
||||
import RelatedContent from 'component/relatedContent';
|
||||
import SubscribeButton from 'component/subscribeButton';
|
||||
import SubscribeNotificationButton from 'component/subscribeNotificationButton';
|
||||
|
@ -51,8 +52,6 @@ class FilePage extends React.PureComponent {
|
|||
title: '',
|
||||
};
|
||||
|
||||
tipAmountInput = null;
|
||||
|
||||
playerBackground = null;
|
||||
|
||||
scrollView = null;
|
||||
|
@ -85,7 +84,6 @@ class FilePage extends React.PureComponent {
|
|||
showTipView: false,
|
||||
playerBgHeight: 0,
|
||||
playerHeight: 0,
|
||||
tipAmount: null,
|
||||
uri: null,
|
||||
uriVars: null,
|
||||
stopDownloadConfirmed: false,
|
||||
|
@ -584,39 +582,6 @@ class FilePage extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
handleSendTip = () => {
|
||||
const { claim, balance, navigation, notify, sendTip } = this.props;
|
||||
const { uri } = navigation.state.params;
|
||||
const { tipAmount } = this.state;
|
||||
|
||||
if (tipAmount > balance) {
|
||||
notify({
|
||||
message: 'Insufficient credits',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const suffix = 'credit' + (parseInt(tipAmount, 10) === 1 ? '' : 's');
|
||||
Alert.alert(
|
||||
'Send tip',
|
||||
`Are you sure you want to tip ${tipAmount} ${suffix}?`,
|
||||
[
|
||||
{ text: 'No' },
|
||||
{
|
||||
text: 'Yes',
|
||||
onPress: () => {
|
||||
this.setState({ sendTipStarted: true }, () =>
|
||||
sendTip(tipAmount, claim.claim_id, false, () => {
|
||||
this.setState({ tipAmount: null, showTipView: false, sendTipStarted: false });
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
{ cancelable: true }
|
||||
);
|
||||
};
|
||||
|
||||
renderTags = tags => {
|
||||
const { navigation } = this.props;
|
||||
return tags.map((tag, i) => (
|
||||
|
@ -830,7 +795,6 @@ class FilePage extends React.PureComponent {
|
|||
const { height, signing_channel: signingChannel, value } = claim;
|
||||
const channelName = signingChannel && signingChannel.name;
|
||||
const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id;
|
||||
const canSendTip = this.state.tipAmount > 0;
|
||||
const fullUri = `${claim.name}#${claim.claim_id}`;
|
||||
const canEdit = myClaimUris.includes(normalizeURI(fullUri));
|
||||
const showActions =
|
||||
|
@ -1169,48 +1133,6 @@ class FilePage extends React.PureComponent {
|
|||
</View>
|
||||
</View>
|
||||
|
||||
{this.state.showTipView && <View style={filePageStyle.divider} />}
|
||||
{this.state.showTipView && (
|
||||
<View style={filePageStyle.tipCard}>
|
||||
<View style={filePageStyle.row}>
|
||||
<View style={filePageStyle.amountRow}>
|
||||
<TextInput
|
||||
editable={!this.state.sendTipStarted}
|
||||
ref={ref => (this.tipAmountInput = ref)}
|
||||
onChangeText={value => this.setState({ tipAmount: value })}
|
||||
onFocus={() => this.setState({ creditsInputFocused: true })}
|
||||
onBlur={() => this.setState({ creditsInputFocused: false })}
|
||||
underlineColorAndroid={Colors.NextLbryGreen}
|
||||
keyboardType={'numeric'}
|
||||
placeholder={'0'}
|
||||
value={this.state.tipAmount}
|
||||
selectTextOnFocus
|
||||
style={[filePageStyle.input, filePageStyle.tipAmountInput]}
|
||||
/>
|
||||
<Text style={[filePageStyle.text, filePageStyle.currency]}>LBC</Text>
|
||||
<View style={filePageStyle.balance}>
|
||||
{this.state.creditsInputFocused && <Icon name="coins" size={12} />}
|
||||
{this.state.creditsInputFocused && (
|
||||
<Text style={filePageStyle.balanceText}>{formatCredits(parseFloat(balance), 1, true)}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{this.state.sendTipStarted && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />}
|
||||
<Link
|
||||
style={[filePageStyle.link, filePageStyle.cancelTipLink]}
|
||||
text={__('Cancel')}
|
||||
onPress={() => this.setState({ showTipView: false })}
|
||||
/>
|
||||
<Button
|
||||
text={__('Send a tip')}
|
||||
style={[filePageStyle.button, filePageStyle.sendButton]}
|
||||
disabled={!canSendTip || this.state.sendTipStarted}
|
||||
onPress={this.handleSendTip}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{this.state.showDescription && description && description.length > 0 && (
|
||||
<View style={filePageStyle.divider} />
|
||||
)}
|
||||
|
@ -1243,8 +1165,18 @@ class FilePage extends React.PureComponent {
|
|||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
{!this.state.creditsInputFocused &&
|
||||
!this.state.fullscreenMode &&
|
||||
{this.state.showTipView && (
|
||||
<ModalTipView
|
||||
claim={claim}
|
||||
channelName={channelName}
|
||||
contentName={title}
|
||||
onCancelPress={() => this.setState({ showTipView: false })}
|
||||
onOverlayPress={() => this.setState({ showTipView: false })}
|
||||
onSendTipSuccessful={() => this.setState({ showTipView: false })}
|
||||
/>
|
||||
)}
|
||||
{!this.state.fullscreenMode &&
|
||||
!this.state.showTipView &&
|
||||
!this.state.showImageViewer &&
|
||||
!this.state.showWebView && <FloatingWalletBalance navigation={navigation} />}
|
||||
</View>
|
||||
|
|
|
@ -176,6 +176,9 @@ const channelPageStyle = StyleSheet.create({
|
|||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
},
|
||||
tipButton: {
|
||||
marginRight: 8,
|
||||
},
|
||||
followerCount: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 12,
|
||||
|
|
71
src/styles/modalTip.js
Normal file
71
src/styles/modalTip.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const modalTipStyle = StyleSheet.create({
|
||||
container: {
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 24,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
},
|
||||
amountRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginRight: 24,
|
||||
},
|
||||
tipAmountInput: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 14,
|
||||
alignSelf: 'flex-start',
|
||||
textAlign: 'right',
|
||||
width: 80,
|
||||
letterSpacing: 1,
|
||||
},
|
||||
currency: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 12,
|
||||
marginLeft: 4,
|
||||
},
|
||||
buttonRow: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 16,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: Colors.LbryGreen,
|
||||
},
|
||||
cancelTipLink: {
|
||||
color: Colors.Grey,
|
||||
},
|
||||
balance: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
marginLeft: 24,
|
||||
},
|
||||
balanceText: {
|
||||
fontFamily: 'Inter-UI-SemiBold',
|
||||
fontSize: 14,
|
||||
marginLeft: 4,
|
||||
},
|
||||
info: {
|
||||
marginTop: 4,
|
||||
},
|
||||
infoText: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 14,
|
||||
color: Colors.DescriptionGrey,
|
||||
},
|
||||
learnMoreLink: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 14,
|
||||
color: Colors.LbryGreen,
|
||||
},
|
||||
});
|
||||
|
||||
export default modalTipStyle;
|
Loading…
Reference in a new issue