Invites page (#114)

This commit is contained in:
Akinwale Ariwodola 2020-01-29 12:25:23 +01:00 committed by GitHub
parent 9bc5a2ecac
commit d9ada93ff3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 512 additions and 14 deletions

4
package-lock.json generated
View file

@ -7076,8 +7076,8 @@
}
},
"lbryinc": {
"version": "github:lbryio/lbryinc#053ca52f4f7f9bf8eb62a3581b183671a475ad1c",
"from": "github:lbryio/lbryinc#053ca52f4f7f9bf8eb62a3581b183671a475ad1c",
"version": "github:lbryio/lbryinc#138a053754ec8e3da8e9bf153d32f527c962f25c",
"from": "github:lbryio/lbryinc#138a053754ec8e3da8e9bf153d32f527c962f25c",
"requires": {
"reselect": "^3.0.0"
}

View file

@ -13,7 +13,7 @@
"@expo/vector-icons": "^8.1.0",
"gfycat-style-urls": "^1.0.3",
"lbry-redux": "lbryio/lbry-redux#c910cd2b80b165843a81fdf6ce96094429b94ec8",
"lbryinc": "lbryio/lbryinc#053ca52f4f7f9bf8eb62a3581b183671a475ad1c",
"lbryinc": "lbryio/lbryinc#138a053754ec8e3da8e9bf153d32f527c962f25c",
"lodash": ">=4.17.11",
"merge": ">=1.2.1",
"moment": "^2.22.1",

View file

@ -6,6 +6,7 @@ import DownloadsPage from 'page/downloads';
import DrawerContent from 'component/drawerContent';
import FilePage from 'page/file';
import FirstRunScreen from 'page/firstRun';
import InvitesPage from 'page/invites';
import PublishPage from 'page/publish';
import PublishesPage from 'page/publishes';
import RewardsPage from 'page/rewards';
@ -210,6 +211,12 @@ const drawer = createDrawerNavigator(
drawerIcon: ({ tintColor }) => <Icon name="award" size={drawerIconSize} style={{ color: tintColor }} />,
},
},
Invites: {
screen: InvitesPage,
navigationOptions: {
drawerIcon: ({ tintColor }) => <Icon name="user-friends" size={drawerIconSize} style={{ color: tintColor }} />,
},
},
Downloads: {
screen: DownloadsPage,
navigationOptions: {
@ -530,7 +537,7 @@ class AppWithNavigationState extends React.Component {
try {
verification = JSON.parse(atob(evt.url.substring(15)));
} catch (error) {
console.log(error);
// console.log(error);
}
if (verification.token && verification.recaptcha) {

View file

@ -36,10 +36,14 @@ export default class ChannelSelector extends React.PureComponent {
}
componentWillReceiveProps(nextProps) {
const { channels: prevChannels = [], channelName } = this.props;
const { channels = [] } = nextProps;
const { channels: prevChannels = [], channelName: prevChannelName } = this.props;
const { channels = [], channelName } = nextProps;
if (channels && channels.length !== prevChannels.length && channelName !== this.state.currentSelectedValue) {
this.setState({ currentSelectedValue: prevChannelName });
}
if (channelName !== prevChannelName) {
this.setState({ currentSelectedValue: channelName });
}
}
@ -189,10 +193,11 @@ export default class ChannelSelector extends React.PureComponent {
render() {
const channel = this.state.addingChannel ? 'new' : this.props.channel;
const { balance, enabled, fetchingChannels, channels = [] } = this.props;
const pickerItems = [Constants.ITEM_ANONYMOUS, Constants.ITEM_CREATE_A_CHANNEL].concat(
channels ? channels.map(ch => ch.name) : [],
);
const { balance, enabled, fetchingChannels, channels = [], showAnonymous } = this.props;
const pickerItems = (showAnonymous
? [Constants.ITEM_ANONYMOUS, Constants.ITEM_CREATE_A_CHANNEL]
: [Constants.ITEM_CREATE_A_CHANNEL]
).concat(channels ? channels.map(ch => ch.name) : []);
const {
newChannelName,

View file

@ -22,6 +22,7 @@ const groupedMenuItems = {
Wallet: [
{ icon: 'wallet', label: 'Wallet', route: Constants.DRAWER_ROUTE_WALLET },
{ icon: 'award', label: 'Rewards', route: Constants.DRAWER_ROUTE_REWARDS },
{ icon: 'user-friends', label: 'Invites', route: Constants.DRAWER_ROUTE_INVITES },
],
Settings: [
{ icon: 'cog', label: 'Settings', route: Constants.DRAWER_ROUTE_SETTINGS },

View file

@ -90,6 +90,7 @@ const Constants = {
DRAWER_ROUTE_TAG: 'Tag',
DRAWER_ROUTE_CHANNEL_CREATOR: 'ChannelCreator',
DRAWER_ROUTE_CHANNEL_CREATOR_FORM: 'ChannnelCreatorForm',
DRAWER_ROUTE_INVITES: 'Invites',
FULL_ROUTE_NAME_DISCOVER: 'DiscoverStack',
FULL_ROUTE_NAME_WALLET: 'WalletStack',
@ -165,6 +166,7 @@ export const DrawerRoutes = [
Constants.DRAWER_ROUTE_SEARCH,
Constants.DRAWER_ROUTE_TRANSACTION_HISTORY,
Constants.DRAWER_ROUTE_CHANNEL_CREATOR,
Constants.DRAWER_ROUTE_INVITES,
];
// sub-pages for main routes

46
src/page/invites/index.js Normal file
View file

@ -0,0 +1,46 @@
import { connect } from 'react-redux';
import { selectMyChannelClaims, selectFetchingMyChannels, doFetchChannelListMine, doToast } from 'lbry-redux';
import {
selectReferralReward,
selectUserInvitesRemaining,
selectUserInviteNewIsPending,
selectUserInviteNewErrorMessage,
selectUserInviteReferralLink,
selectUserInviteReferralCode,
selectUserInvitees,
selectUserInviteStatusIsPending,
doFetchInviteStatus,
doUserInviteNew,
} from 'lbryinc';
import { doPushDrawerStack, doPopDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
import { doUpdateChannelFormState, doClearChannelFormState } from 'redux/actions/form';
import { selectDrawerStack } from 'redux/selectors/drawer';
import { selectChannelFormState, selectHasChannelFormState } from 'redux/selectors/form';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import InvitesPage from './view';
const select = state => ({
channels: selectMyChannelClaims(state),
fetchingChannels: selectFetchingMyChannels(state),
fetchingInvitees: selectUserInviteStatusIsPending(state),
errorMessage: selectUserInviteNewErrorMessage(state),
invitesRemaining: selectUserInvitesRemaining(state),
referralCode: selectUserInviteReferralCode(state),
isPending: selectUserInviteNewIsPending(state),
invitees: selectUserInvitees(state),
referralReward: selectReferralReward(state),
});
const perform = dispatch => ({
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
fetchInviteStatus: () => dispatch(doFetchInviteStatus()),
inviteNew: email => dispatch(doUserInviteNew(email)),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_INVITES)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(false)),
notify: data => dispatch(doToast(data)),
});
export default connect(
select,
perform,
)(InvitesPage);

228
src/page/invites/view.js Normal file
View file

@ -0,0 +1,228 @@
import React from 'react';
import { Lbry, parseURI } from 'lbry-redux';
import {
ActivityIndicator,
Clipboard,
NativeModules,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
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';
import Button from 'component/button';
import ChannelSelector from 'component/channelSelector';
import PageHeader from 'component/pageHeader';
import RewardCard from 'component/rewardCard';
import RewardEnrolment from 'component/rewardEnrolment';
import UriBar from 'component/uriBar';
import invitesStyle from 'styles/invites';
class InvitesPage extends React.PureComponent {
state = {
channelName: null,
email: null,
inviteLink: null,
selectedChannel: null,
};
componentWillMount() {
const { navigation } = this.props;
// this.didFocusListener = navigation.addListener('didFocus', this.onComponentFocused);
}
componentWillUnmount() {
if (this.didFocusListener) {
this.didFocusListener.remove();
}
}
onComponentFocused = () => {
const { fetchChannelListMine, fetchInviteStatus, pushDrawerStack, navigation, setPlayerVisible, user } = this.props;
pushDrawerStack();
setPlayerVisible();
NativeModules.Firebase.setCurrentScreen('Invites').then(result => {
fetchChannelListMine();
fetchInviteStatus();
});
};
componentDidMount() {
this.onComponentFocused();
}
handleChannelChange = channelName => {
const { channels = [] } = this.props;
if (channels && channels.length > 0) {
const filtered = channels.filter(c => c.name === channelName);
if (filtered.length > 0) {
const channel = filtered[0];
this.setState({ channelName, inviteLink: this.getLinkForChannel(channel) });
}
}
};
getLinkForChannel = channel => {
const { claimId, claimName } = parseURI(channel.permanent_url);
return `https://lbry.tv/$/invite/${claimName}:${claimId}`;
};
handleInviteEmailChange = text => {
this.setState({ email: text });
};
handleInvitePress = () => {
const { inviteNew, notify } = this.props;
const { email } = this.state;
if (!email || email.indexOf('@') === -1) {
return notify({
message: __('Please enter a valid email address to send an invite to.'),
isError: true,
});
}
inviteNew(email);
};
componentWillReceiveProps(nextProps) {
const { isPending: prevPending, notify } = this.props;
const { channels = [], isPending, errorMessage } = nextProps;
const { email } = this.state;
if (!this.state.channelName && channels && channels.length > 0) {
const firstChannel = channels[0];
this.setState({ channelName: firstChannel.name, inviteLink: this.getLinkForChannel(firstChannel) });
}
if (prevPending && !isPending) {
if (errorMessage && errorMessage.trim().length > 0) {
notify({ message: errorMessage, isError: true });
} else {
notify({ message: __(`${email} was invited to the LBRY party!`) });
this.setState({ email: null });
}
}
}
handleInviteLinkPress = () => {
const { notify } = this.props;
Clipboard.setString(this.state.inviteLink);
notify({
message: __('Invite link copied'),
});
};
render() {
const { fetchingInvitees, user, navigation, notify, isPending, invitees } = this.props;
const { email, inviteLink } = this.state;
const hasInvitees = invitees && invitees.length > 0;
return (
<View style={invitesStyle.container}>
<UriBar navigation={navigation} />
<ScrollView style={invitesStyle.scrollContainer}>
<TouchableOpacity style={invitesStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}>
<Icon name="award" size={16} style={invitesStyle.rewardDriverIcon} />
<Text style={invitesStyle.rewardDriverText}>{__('Earn rewards for inviting your friends.')}</Text>
</TouchableOpacity>
<View style={invitesStyle.card}>
<Text style={invitesStyle.title}>{__('Invite Link')}</Text>
<Text style={invitesStyle.text}>
{__('Share this link with friends (or enemies) and get 20 LBC when they join lbry.tv')}
</Text>
<Text style={invitesStyle.subTitle}>{__('Your invite link')}</Text>
<View style={invitesStyle.row}>
<Text selectable numberOfLines={1} style={invitesStyle.inviteLink} onPress={this.handleInviteLinkPress}>
{this.state.inviteLink}
</Text>
<Button icon={'clipboard'} style={invitesStyle.button} onPress={this.handleInviteLinkPress} />
</View>
<Text style={invitesStyle.customizeTitle}>{__('Customize invite link')}</Text>
<ChannelSelector
showAnonymous={false}
channelName={this.state.channelName}
onChannelChange={this.handleChannelChange}
/>
</View>
<View style={invitesStyle.card}>
<Text style={invitesStyle.title}>{__('Invite by Email')}</Text>
<Text style={invitesStyle.text}>
{__('Invite someone you know by email and earn 20 LBC when they join lbry.tv.')}
</Text>
<TextInput
style={invitesStyle.emailInput}
editable={!isPending}
value={this.state.email}
onChangeText={this.handleInviteEmailChange}
placeholder={__('imaginary@friend.com')}
underlineColorAndroid={Colors.NextLbryGreen}
/>
<View style={invitesStyle.rightRow}>
{isPending && (
<ActivityIndicator size={'small'} color={Colors.NextLbryGreen} style={invitesStyle.loading} />
)}
<Button
disabled={!email || email.indexOf('@') === -1 || isPending}
style={invitesStyle.button}
text={__('Invite')}
onPress={this.handleInvitePress}
/>
</View>
</View>
<View style={[invitesStyle.card, invitesStyle.lastCard]}>
<View style={invitesStyle.titleRow}>
<Text style={invitesStyle.titleCol}>{__('Invite History')}</Text>
{fetchingInvitees && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />}
</View>
<Text style={invitesStyle.text}>
{__(
'Earn 20 LBC for inviting a friend, an enemy, a frenemy, or an enefriend. Everyone needs content freedom.',
)}
</Text>
<View style={invitesStyle.invitees}>
{hasInvitees && (
<View style={invitesStyle.inviteesHeader}>
<Text style={invitesStyle.emailHeader} numberOfLines={1}>
{__('Email')}
</Text>
<Text style={invitesStyle.rewardHeader} numberOfLines={1}>
{__('Reward')}
</Text>
</View>
)}
{hasInvitees &&
invitees.map(invitee => (
<View key={invitee.email} style={invitesStyle.inviteeItem}>
<Text style={invitesStyle.inviteeEmail} numberOfLines={1}>
{invitee.email}
</Text>
<Text style={invitesStyle.rewardStatus} numberOfLines={1}>
{invitee.invite_reward_claimed && __('Claimed')}
{!invitee.invite_reward_claimed &&
(invitee.invite_reward_claimable ? __('Claimable') : __('Unclaimable'))}
</Text>
</View>
))}
</View>
</View>
</ScrollView>
</View>
);
}
}
export default InvitesPage;

View file

@ -11,6 +11,10 @@ import RewardEnrolment from 'component/rewardEnrolment';
import UriBar from 'component/uriBar';
import rewardStyle from 'styles/reward';
const FILTER_ALL = 'all';
const FILTER_AVAILABLE = 'available';
const FILTER_CLAIMED = 'claimed';
class RewardsPage extends React.PureComponent {
state = {
isEmailVerified: false,
@ -19,6 +23,7 @@ class RewardsPage extends React.PureComponent {
verifyRequestStarted: false,
revealVerification: true,
firstRewardClaimed: false,
currentFilter: FILTER_AVAILABLE,
};
scrollView = null;
@ -182,8 +187,13 @@ class RewardsPage extends React.PureComponent {
});
};
setFilter = filter => {
this.setState({ currentFilter: filter });
};
render() {
const { user, navigation } = this.props;
const { currentFilter } = this.state;
return (
<View style={rewardStyle.container}>
@ -197,8 +207,29 @@ class RewardsPage extends React.PureComponent {
style={rewardStyle.scrollContainer}
contentContainerStyle={rewardStyle.scrollContentContainer}
>
{this.renderUnclaimedRewards()}
{this.renderClaimedRewards()}
<View style={rewardStyle.filterHeader}>
<Link
style={[rewardStyle.filterLink, currentFilter === FILTER_ALL ? rewardStyle.activeFilterLink : null]}
text={__('All')}
onPress={() => this.setFilter(FILTER_ALL)}
/>
<Link
style={[
rewardStyle.filterLink,
currentFilter === FILTER_AVAILABLE ? rewardStyle.activeFilterLink : null,
]}
text={__('Available')}
onPress={() => this.setFilter(FILTER_AVAILABLE)}
/>
<Link
style={[rewardStyle.filterLink, currentFilter === FILTER_CLAIMED ? rewardStyle.activeFilterLink : null]}
text={__('Claimed')}
onPress={() => this.setFilter(FILTER_CLAIMED)}
/>
</View>
{(currentFilter === FILTER_AVAILABLE || currentFilter === FILTER_ALL) && this.renderUnclaimedRewards()}
{(currentFilter === FILTER_CLAIMED || currentFilter === FILTER_ALL) && this.renderClaimedRewards()}
</ScrollView>
)}
</View>

View file

@ -1,16 +1,26 @@
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
export const doPushDrawerStack = (routeName, params) => dispatch =>
export const doPushDrawerStack = (routeName, params) => dispatch => {
dispatch({
type: Constants.ACTION_PUSH_DRAWER_STACK,
data: { routeName, params },
});
export const doPopDrawerStack = () => dispatch =>
if (window.persistor) {
window.persistor.flush();
}
};
export const doPopDrawerStack = () => dispatch => {
dispatch({
type: Constants.ACTION_POP_DRAWER_STACK,
});
if (window.persistor) {
window.persistor.flush();
}
};
export const doSetPlayerVisible = (visible, uri) => dispatch =>
dispatch({
type: Constants.ACTION_SET_PLAYER_VISIBLE,

150
src/styles/invites.js Normal file
View file

@ -0,0 +1,150 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const walletStyle = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.PageBackground,
},
scrollContainer: {
marginTop: 60,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
button: {
backgroundColor: Colors.LbryGreen,
alignSelf: 'flex-start',
},
card: {
backgroundColor: Colors.White,
marginTop: 16,
marginLeft: 16,
marginRight: 16,
padding: 16,
},
title: {
fontFamily: 'Inter-SemiBold',
fontSize: 20,
marginBottom: 8,
},
titleRow: {
alignItems: 'center',
flexDirection: 'row',
marginBottom: 8,
justifyContent: 'space-between',
},
titleCol: {
fontFamily: 'Inter-SemiBold',
fontSize: 20,
},
text: {
fontFamily: 'Inter-Regular',
fontSize: 14,
},
link: {
color: Colors.LbryGreen,
fontFamily: 'Inter-Regular',
fontSize: 14,
},
smallText: {
fontFamily: 'Inter-Regular',
fontSize: 12,
},
rewardDriverCard: {
alignItems: 'center',
backgroundColor: Colors.RewardDriverBlue,
flexDirection: 'row',
padding: 16,
marginLeft: 16,
marginTop: 16,
marginRight: 16,
},
rewardDriverIcon: {
color: Colors.White,
marginRight: 8,
},
rewardDriverText: {
fontFamily: 'Inter-Regular',
color: Colors.White,
fontSize: 14,
},
subTitle: {
fontFamily: 'Inter-Regular',
fontSize: 14,
marginTop: 12,
marginBottom: 4,
},
customizeTitle: {
fontFamily: 'Inter-Regular',
fontSize: 14,
marginTop: 12,
},
inviteLink: {
fontFamily: 'Inter-Regular',
borderWidth: 1,
borderRadius: 16,
borderStyle: 'dashed',
borderColor: '#e1e1e1',
backgroundColor: '#f9f9f9',
paddingTop: 8,
paddingLeft: 8,
paddingRight: 8,
paddingBottom: 6,
width: '88%',
},
emailInput: {
fontFamily: 'Inter-Regular',
fontSize: 14,
},
rightRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-end',
},
loading: {
marginRight: 8,
},
lastCard: {
marginBottom: 16,
},
invitees: {
marginTop: 8,
},
inviteesHeader: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
emailHeader: {
fontFamily: 'Inter-SemiBold',
fontSize: 14,
width: '65%',
},
rewardHeader: {
fontFamily: 'Inter-SemiBold',
fontSize: 14,
width: '35%',
},
inviteeItem: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
inviteeEmail: {
fontFamily: 'Inter-Regular',
fontSize: 12,
width: '65%',
},
rewardStatus: {
fontFamily: 'Inter-Regular',
fontSize: 12,
width: '35%',
},
});
export default walletStyle;

View file

@ -305,6 +305,23 @@ const rewardStyle = StyleSheet.create({
fontSize: 12,
lineHeight: 16,
},
filterHeader: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 16,
marginLeft: 16,
marginRight: 16,
padding: 16,
backgroundColor: Colors.White,
},
filterLink: {
fontFamily: 'Inter-Regular',
fontSize: 14,
marginRight: 24,
},
activeFilterLink: {
fontFamily: 'Inter-SemiBold',
},
});
export default rewardStyle;

View file

@ -10,6 +10,7 @@ const specialRouteMap = {
about: Constants.DRAWER_ROUTE_ABOUT,
allContent: Constants.DRAWER_ROUTE_TRENDING,
channels: Constants.DRAWER_ROUTE_CHANNEL_CREATOR,
invites: Constants.DRAWER_ROUTE_INVITES,
library: Constants.DRAWER_ROUTE_MY_LBRY,
publish: Constants.DRAWER_ROUTE_PUBLISH,
publishes: Constants.DRAWER_ROUTE_PUBLISHES,