From 63ba6ecf477e727a5a7c1d53d1a3cc8f78471b60 Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola <akinwale@gmail.com> Date: Fri, 24 Jan 2020 18:24:59 +0100 Subject: [PATCH 1/2] Invites page --- package-lock.json | 4 +- package.json | 2 +- src/component/AppNavigator.js | 9 +- src/component/channelSelector/view.js | 17 +- src/component/drawerContent/view.js | 1 + src/constants.js | 2 + src/page/channelCreator/view.js | 3 +- src/page/invites/index.js | 46 +++++ src/page/invites/view.js | 233 ++++++++++++++++++++++++++ src/redux/actions/drawer.js | 14 +- src/styles/invites.js | 150 +++++++++++++++++ src/utils/helper.js | 1 + 12 files changed, 469 insertions(+), 13 deletions(-) create mode 100644 src/page/invites/index.js create mode 100644 src/page/invites/view.js create mode 100644 src/styles/invites.js 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 }) => <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) { 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/channelCreator/view.js b/src/page/channelCreator/view.js index 5903bfe..aff437a 100644 --- a/src/page/channelCreator/view.js +++ b/src/page/channelCreator/view.js @@ -931,7 +931,8 @@ export default class ChannelCreator extends React.PureComponent { source={{ uri: thumbnailUrl }} /> )} - {(!!thumbnailUrl || thumbnailUrl.trim().length === 0) && newChannelName.length > 0 && ( + {(!!thumbnailUrl || (!!thumbnailUrl && thumbnailUrl.trim().length === 0)) && + newChannelName.length > 0 && ( <Text style={channelIconStyle.autothumbCharacter}> {newChannelName.substring(0, 1).toUpperCase()} </Text> 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..cb4539d --- /dev/null +++ b/src/page/invites/view.js @@ -0,0 +1,233 @@ +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: __(`An invite was successfully sent to ${email}`) }); + this.setState({ email: null }); + } + } + } + + render() { + const { fetchingInvitees, user, navigation, notify, isPending, invitees } = this.props; + const { email, inviteLink } = this.state; + const hasInvitees = !fetchingInvitees && 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}> + {this.state.inviteLink} + </Text> + <Button + icon={'clipboard'} + style={invitesStyle.button} + onPress={() => { + Clipboard.setString(inviteLink); + notify({ + message: __('Invite link copied'), + }); + }} + /> + </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={__('myfriend@example.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]}> + <Text style={invitesStyle.title}>{__('Invite History')}</Text> + <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}> + {fetchingInvitees && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />} + + {hasInvitees && ( + <View style={invitesStyle.inviteesHeader}> + <Text style={invitesStyle.emailHeader} numberOfLines={1}> + Email + </Text> + <Text style={invitesStyle.statusHeader} numberOfLines={1}> + Status + </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.inviteStatus} numberOfLines={1}> + {invitee.accepted ? __('Accepted') : __('Not Accepted')} + </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; 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..c70fca3 --- /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, + }, + 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: 16, + marginTop: 12, + marginBottom: 8, + }, + customizeTitle: { + fontFamily: 'Inter-Regular', + fontSize: 16, + 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: '50%', + }, + statusHeader: { + fontFamily: 'Inter-SemiBold', + fontSize: 14, + width: '30%', + }, + rewardHeader: { + fontFamily: 'Inter-SemiBold', + fontSize: 14, + width: '20%', + }, + inviteeItem: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + marginBottom: 8, + }, + inviteeEmail: { + fontFamily: 'Inter-Regular', + fontSize: 12, + width: '50%', + }, + inviteStatus: { + fontFamily: 'Inter-Regular', + fontSize: 12, + width: '30%', + }, + rewardStatus: { + fontFamily: 'Inter-Regular', + fontSize: 12, + width: '20%', + }, +}); + +export default walletStyle; 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, From 1db0e9a09fc5152e6f6a22a2ca612cb6ed745a99 Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola <akinwale@gmail.com> Date: Wed, 29 Jan 2020 12:24:20 +0100 Subject: [PATCH 2/2] fixes as per review --- src/page/invites/view.js | 49 ++++++++++++++++++---------------------- src/page/rewards/view.js | 35 ++++++++++++++++++++++++++-- src/styles/invites.js | 34 ++++++++++++++-------------- src/styles/reward.js | 17 ++++++++++++++ 4 files changed, 89 insertions(+), 46 deletions(-) diff --git a/src/page/invites/view.js b/src/page/invites/view.js index cb4539d..efdf761 100644 --- a/src/page/invites/view.js +++ b/src/page/invites/view.js @@ -101,18 +101,26 @@ class InvitesPage extends React.PureComponent { if (prevPending && !isPending) { if (errorMessage && errorMessage.trim().length > 0) { - notify({ message: __(errorMessage), isError: true }); + notify({ message: errorMessage, isError: true }); } else { - notify({ message: __(`An invite was successfully sent to ${email}`) }); + 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 = !fetchingInvitees && invitees && invitees.length > 0; + const hasInvitees = invitees && invitees.length > 0; return ( <View style={invitesStyle.container}> @@ -132,19 +140,10 @@ class InvitesPage extends React.PureComponent { <Text style={invitesStyle.subTitle}>{__('Your invite link')}</Text> <View style={invitesStyle.row}> - <Text selectable numberOfLines={1} style={invitesStyle.inviteLink}> + <Text selectable numberOfLines={1} style={invitesStyle.inviteLink} onPress={this.handleInviteLinkPress}> {this.state.inviteLink} </Text> - <Button - icon={'clipboard'} - style={invitesStyle.button} - onPress={() => { - Clipboard.setString(inviteLink); - notify({ - message: __('Invite link copied'), - }); - }} - /> + <Button icon={'clipboard'} style={invitesStyle.button} onPress={this.handleInviteLinkPress} /> </View> <Text style={invitesStyle.customizeTitle}>{__('Customize invite link')}</Text> @@ -166,7 +165,7 @@ class InvitesPage extends React.PureComponent { editable={!isPending} value={this.state.email} onChangeText={this.handleInviteEmailChange} - placeholder={__('myfriend@example.com')} + placeholder={__('imaginary@friend.com')} underlineColorAndroid={Colors.NextLbryGreen} /> <View style={invitesStyle.rightRow}> @@ -176,14 +175,18 @@ class InvitesPage extends React.PureComponent { <Button disabled={!email || email.indexOf('@') === -1 || isPending} style={invitesStyle.button} - text={'Invite'} + text={__('Invite')} onPress={this.handleInvitePress} /> </View> </View> <View style={[invitesStyle.card, invitesStyle.lastCard]}> - <Text style={invitesStyle.title}>{__('Invite History')}</Text> + <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.', @@ -191,18 +194,13 @@ class InvitesPage extends React.PureComponent { </Text> <View style={invitesStyle.invitees}> - {fetchingInvitees && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />} - {hasInvitees && ( <View style={invitesStyle.inviteesHeader}> <Text style={invitesStyle.emailHeader} numberOfLines={1}> - Email - </Text> - <Text style={invitesStyle.statusHeader} numberOfLines={1}> - Status + {__('Email')} </Text> <Text style={invitesStyle.rewardHeader} numberOfLines={1}> - Reward + {__('Reward')} </Text> </View> )} @@ -212,9 +210,6 @@ class InvitesPage extends React.PureComponent { <Text style={invitesStyle.inviteeEmail} numberOfLines={1}> {invitee.email} </Text> - <Text style={invitesStyle.inviteStatus} numberOfLines={1}> - {invitee.accepted ? __('Accepted') : __('Not Accepted')} - </Text> <Text style={invitesStyle.rewardStatus} numberOfLines={1}> {invitee.invite_reward_claimed && __('Claimed')} {!invitee.invite_reward_claimed && 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 ( <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> diff --git a/src/styles/invites.js b/src/styles/invites.js index c70fca3..3fa7278 100644 --- a/src/styles/invites.js +++ b/src/styles/invites.js @@ -30,6 +30,16 @@ const walletStyle = StyleSheet.create({ 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, @@ -63,13 +73,13 @@ const walletStyle = StyleSheet.create({ }, subTitle: { fontFamily: 'Inter-Regular', - fontSize: 16, + fontSize: 14, marginTop: 12, - marginBottom: 8, + marginBottom: 4, }, customizeTitle: { fontFamily: 'Inter-Regular', - fontSize: 16, + fontSize: 14, marginTop: 12, }, inviteLink: { @@ -112,17 +122,12 @@ const walletStyle = StyleSheet.create({ emailHeader: { fontFamily: 'Inter-SemiBold', fontSize: 14, - width: '50%', - }, - statusHeader: { - fontFamily: 'Inter-SemiBold', - fontSize: 14, - width: '30%', + width: '65%', }, rewardHeader: { fontFamily: 'Inter-SemiBold', fontSize: 14, - width: '20%', + width: '35%', }, inviteeItem: { flex: 1, @@ -133,17 +138,12 @@ const walletStyle = StyleSheet.create({ inviteeEmail: { fontFamily: 'Inter-Regular', fontSize: 12, - width: '50%', - }, - inviteStatus: { - fontFamily: 'Inter-Regular', - fontSize: 12, - width: '30%', + width: '65%', }, rewardStatus: { fontFamily: 'Inter-Regular', fontSize: 12, - width: '20%', + width: '35%', }, }); 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;