diff --git a/package-lock.json b/package-lock.json
index de4aa37..98d19a1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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"
}
diff --git a/package.json b/package.json
index 2192486..02b1403 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/component/AppNavigator.js b/src/component/AppNavigator.js
index b6f64b9..db507e5 100644
--- a/src/component/AppNavigator.js
+++ b/src/component/AppNavigator.js
@@ -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 }) => ,
},
},
+ Invites: {
+ screen: InvitesPage,
+ navigationOptions: {
+ drawerIcon: ({ 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) {
diff --git a/src/component/channelSelector/view.js b/src/component/channelSelector/view.js
index d0ec6bb..952447d 100644
--- a/src/component/channelSelector/view.js
+++ b/src/component/channelSelector/view.js
@@ -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,
diff --git a/src/component/drawerContent/view.js b/src/component/drawerContent/view.js
index f6b810a..cdc2815 100644
--- a/src/component/drawerContent/view.js
+++ b/src/component/drawerContent/view.js
@@ -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 },
diff --git a/src/constants.js b/src/constants.js
index 20bf1d9..7c508d3 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -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
diff --git a/src/page/invites/index.js b/src/page/invites/index.js
new file mode 100644
index 0000000..230107a
--- /dev/null
+++ b/src/page/invites/index.js
@@ -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);
diff --git a/src/page/invites/view.js b/src/page/invites/view.js
new file mode 100644
index 0000000..efdf761
--- /dev/null
+++ b/src/page/invites/view.js
@@ -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 (
+
+
+
+
+ navigation.navigate('Rewards')}>
+
+ {__('Earn rewards for inviting your friends.')}
+
+
+
+ {__('Invite Link')}
+
+ {__('Share this link with friends (or enemies) and get 20 LBC when they join lbry.tv')}
+
+
+ {__('Your invite link')}
+
+
+ {this.state.inviteLink}
+
+
+
+
+ {__('Customize invite link')}
+
+
+
+
+ {__('Invite by Email')}
+
+ {__('Invite someone you know by email and earn 20 LBC when they join lbry.tv.')}
+
+
+
+
+ {isPending && (
+
+ )}
+
+
+
+
+
+
+ {__('Invite History')}
+ {fetchingInvitees && }
+
+
+
+ {__(
+ 'Earn 20 LBC for inviting a friend, an enemy, a frenemy, or an enefriend. Everyone needs content freedom.',
+ )}
+
+
+
+ {hasInvitees && (
+
+
+ {__('Email')}
+
+
+ {__('Reward')}
+
+
+ )}
+ {hasInvitees &&
+ invitees.map(invitee => (
+
+
+ {invitee.email}
+
+
+ {invitee.invite_reward_claimed && __('Claimed')}
+ {!invitee.invite_reward_claimed &&
+ (invitee.invite_reward_claimable ? __('Claimable') : __('Unclaimable'))}
+
+
+ ))}
+
+
+
+
+ );
+ }
+}
+
+export default InvitesPage;
diff --git a/src/page/rewards/view.js b/src/page/rewards/view.js
index cad8b19..c3aee39 100644
--- a/src/page/rewards/view.js
+++ b/src/page/rewards/view.js
@@ -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 (
@@ -197,8 +207,29 @@ class RewardsPage extends React.PureComponent {
style={rewardStyle.scrollContainer}
contentContainerStyle={rewardStyle.scrollContentContainer}
>
- {this.renderUnclaimedRewards()}
- {this.renderClaimedRewards()}
+
+ this.setFilter(FILTER_ALL)}
+ />
+ this.setFilter(FILTER_AVAILABLE)}
+ />
+ this.setFilter(FILTER_CLAIMED)}
+ />
+
+
+ {(currentFilter === FILTER_AVAILABLE || currentFilter === FILTER_ALL) && this.renderUnclaimedRewards()}
+ {(currentFilter === FILTER_CLAIMED || currentFilter === FILTER_ALL) && this.renderClaimedRewards()}
)}
diff --git a/src/redux/actions/drawer.js b/src/redux/actions/drawer.js
index d799ac7..344d820 100644
--- a/src/redux/actions/drawer.js
+++ b/src/redux/actions/drawer.js
@@ -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,
diff --git a/src/styles/invites.js b/src/styles/invites.js
new file mode 100644
index 0000000..3fa7278
--- /dev/null
+++ b/src/styles/invites.js
@@ -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;
diff --git a/src/styles/reward.js b/src/styles/reward.js
index db01262..f4b1178 100644
--- a/src/styles/reward.js
+++ b/src/styles/reward.js
@@ -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;
diff --git a/src/utils/helper.js b/src/utils/helper.js
index 380a7d7..49e2f62 100644
--- a/src/utils/helper.js
+++ b/src/utils/helper.js
@@ -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,