diff --git a/package-lock.json b/package-lock.json
index 81cc5c6..0cc3e74 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5640,8 +5640,8 @@
}
},
"lbry-redux": {
- "version": "github:lbryio/lbry-redux#123efacf4d45289ebda9dc291976d475de227a55",
- "from": "github:lbryio/lbry-redux#123efacf4d45289ebda9dc291976d475de227a55",
+ "version": "github:lbryio/lbry-redux#d44cd9ca56dee784dba42c0cc13061ae75cbd46c",
+ "from": "github:lbryio/lbry-redux#d44cd9ca56dee784dba42c0cc13061ae75cbd46c",
"requires": {
"proxy-polyfill": "0.1.6",
"reselect": "^3.0.0",
@@ -5649,8 +5649,8 @@
}
},
"lbryinc": {
- "version": "github:lbryio/lbryinc#b9f354ae50bd57691765a7d042c5054167878bf4",
- "from": "github:lbryio/lbryinc#b9f354ae50bd57691765a7d042c5054167878bf4",
+ "version": "github:lbryio/lbryinc#d250096a6fc5df16be4f82812ecce28d6e558b6e",
+ "from": "github:lbryio/lbryinc#d250096a6fc5df16be4f82812ecce28d6e558b6e",
"requires": {
"reselect": "^3.0.0"
}
diff --git a/package.json b/package.json
index 57d482b..9a434c5 100644
--- a/package.json
+++ b/package.json
@@ -12,8 +12,8 @@
"base-64": "^0.1.0",
"@expo/vector-icons": "^8.1.0",
"gfycat-style-urls": "^1.0.3",
- "lbry-redux": "lbryio/lbry-redux#123efacf4d45289ebda9dc291976d475de227a55",
- "lbryinc": "lbryio/lbryinc#b9f354ae50bd57691765a7d042c5054167878bf4",
+ "lbry-redux": "lbryio/lbry-redux#d44cd9ca56dee784dba42c0cc13061ae75cbd46c",
+ "lbryinc": "lbryio/lbryinc#d250096a6fc5df16be4f82812ecce28d6e558b6e",
"lodash": ">=4.17.11",
"merge": ">=1.2.1",
"moment": "^2.22.1",
diff --git a/src/assets/gerbil-happy.png b/src/assets/gerbil-happy.png
new file mode 100644
index 0000000..4247f83
Binary files /dev/null and b/src/assets/gerbil-happy.png differ
diff --git a/src/assets/gerbil-sad.png b/src/assets/gerbil-sad.png
new file mode 100644
index 0000000..153d4ad
Binary files /dev/null and b/src/assets/gerbil-sad.png differ
diff --git a/src/component/AppNavigator.js b/src/component/AppNavigator.js
index db24edb..a5fb642 100644
--- a/src/component/AppNavigator.js
+++ b/src/component/AppNavigator.js
@@ -29,8 +29,9 @@ import {
import { connect } from 'react-redux';
import { AppState, BackHandler, Linking, NativeModules, TextInput, ToastAndroid } from 'react-native';
import { selectDrawerStack } from 'redux/selectors/drawer';
-import { SETTINGS, doDismissToast, doToast, selectToast } from 'lbry-redux';
+import { SETTINGS, doDismissToast, doPopulateSharedUserState, doToast, selectToast } from 'lbry-redux';
import {
+ Lbryio,
doGetSync,
doUserCheckEmailVerified,
doUserEmailVerify,
@@ -305,6 +306,13 @@ class AppWithNavigationState extends React.Component {
});
};
+ getUserSettings = () => {
+ const { dispatch } = this.props;
+ Lbryio.call('user_settings', 'get').then(settings => {
+ dispatch(doPopulateSharedUserState(settings));
+ });
+ };
+
componentWillUnmount() {
AppState.removeEventListener('change', this._handleAppStateChange);
BackHandler.removeEventListener('hardwareBackPress');
@@ -321,12 +329,8 @@ class AppWithNavigationState extends React.Component {
NativeModules.Firebase.track('email_verified', { email: user.primary_email });
ToastAndroid.show('Your email address was successfully verified.', ToastAndroid.LONG);
- // upon successful email verification, do wallet sync (if password has been set)
- NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
- if (walletPassword && walletPassword.trim().length > 0) {
- dispatch(doGetSync(walletPassword));
- }
- });
+ // get user settings after email verification
+ this.getUserSettings();
}
}
diff --git a/src/component/channelSelector/view.js b/src/component/channelSelector/view.js
index e5e8c29..b8d81a8 100644
--- a/src/component/channelSelector/view.js
+++ b/src/component/channelSelector/view.js
@@ -26,8 +26,8 @@ export default class ChannelSelector extends React.PureComponent {
}
componentDidMount() {
- const { channels, channelName, fetchChannelListMine, fetchingChannels } = this.props;
- if (!channels.length && !fetchingChannels) {
+ const { channels = [], channelName, fetchChannelListMine, fetchingChannels } = this.props;
+ if ((!channels || channels.length === 0) && !fetchingChannels) {
fetchChannelListMine();
}
this.setState({ currentSelectedValue: channelName });
@@ -37,7 +37,7 @@ export default class ChannelSelector extends React.PureComponent {
const { channels: prevChannels = [], channelName } = this.props;
const { channels = [] } = nextProps;
- if (channels.length !== prevChannels.length && channelName !== this.state.currentSelectedValue) {
+ if (channels && channels.length !== prevChannels.length && channelName !== this.state.currentSelectedValue) {
this.setState({ currentSelectedValue: channelName });
}
}
@@ -182,7 +182,9 @@ export default class ChannelSelector extends React.PureComponent {
render() {
const channel = this.state.addingChannel ? 'new' : this.props.channel;
const { enabled, fetchingChannels, channels = [] } = this.props;
- const pickerItems = [Constants.ITEM_ANONYMOUS, Constants.ITEM_CREATE_A_CHANNEL].concat(channels.map(ch => ch.name));
+ const pickerItems = [Constants.ITEM_ANONYMOUS, Constants.ITEM_CREATE_A_CHANNEL].concat(
+ channels ? channels.map(ch => ch.name) : []
+ );
const {
newChannelName,
diff --git a/src/component/emptyStateView/index.js b/src/component/emptyStateView/index.js
new file mode 100644
index 0000000..53e1c84
--- /dev/null
+++ b/src/component/emptyStateView/index.js
@@ -0,0 +1,4 @@
+import { connect } from 'react-redux';
+import EmptyStateView from './view';
+
+export default connect()(EmptyStateView);
diff --git a/src/component/emptyStateView/view.js b/src/component/emptyStateView/view.js
new file mode 100644
index 0000000..e69ceda
--- /dev/null
+++ b/src/component/emptyStateView/view.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import { NativeModules, Text, View, Image, TouchableOpacity } from 'react-native';
+import Button from '../button';
+import emptyStateStyle from 'styles/emptyState';
+
+class EmptyStateView extends React.PureComponent {
+ render() {
+ const { message, buttonText, inner, onButtonPress } = this.props;
+
+ return (
+ <View
+ style={[emptyStateStyle.container, inner ? emptyStateStyle.innerContainer : emptyStateStyle.outerContainer]}
+ >
+ <Image style={emptyStateStyle.image} resizeMode={'stretch'} source={require('../../assets/gerbil-happy.png')} />
+ <Text style={emptyStateStyle.message}>{message}</Text>
+ {buttonText && (
+ <View style={emptyStateStyle.buttonContainer}>
+ <Button style={emptyStateStyle.button} text={buttonText} onPress={onButtonPress} />
+ </View>
+ )}
+ </View>
+ );
+ }
+}
+
+export default EmptyStateView;
diff --git a/src/component/rewardCard/view.js b/src/component/rewardCard/view.js
index e5faaca..8db4147 100644
--- a/src/component/rewardCard/view.js
+++ b/src/component/rewardCard/view.js
@@ -59,7 +59,8 @@ class RewardCard extends React.PureComponent<Props> {
getDisplayAmount = () => {
const { reward } = this.props;
if (reward) {
- if (reward.reward_range && reward.reward_range.includes('-')) {
+ const claimed = !!reward.transaction_id;
+ if (!claimed && reward.reward_range && reward.reward_range.includes('-')) {
return reward.reward_range.split('-')[0] + '+'; // ex: 5+
} else if (reward.reward_amount > 0) {
return reward.reward_amount;
diff --git a/src/index.js b/src/index.js
index 9b92508..4013fe2 100644
--- a/src/index.js
+++ b/src/index.js
@@ -15,6 +15,7 @@ import {
walletReducer,
} from 'lbry-redux';
import {
+ Lbryio,
authReducer,
blacklistReducer,
costInfoReducer,
@@ -41,6 +42,7 @@ import formReducer from 'redux/reducers/form';
import drawerReducer from 'redux/reducers/drawer';
import settingsReducer from 'redux/reducers/settings';
import thunk from 'redux-thunk';
+import isEqual from 'utils/deep-equal';
const globalExceptionHandler = (error, isFatal) => {
if (error && NativeModules.Firebase) {
@@ -145,22 +147,27 @@ const persistor = persistStore(store, persistOptions, err => {
});
window.persistor = persistor;
-/*
-const persistFilter = {
- 'auth': ['authToken'],
- 'claims': ['byId', 'claimsByUri'],
- 'content': ['positions'],
- 'subscriptions': ['enabledChannelNotifications', 'subscriptions'],
- 'settings': ['clientSettings'],
- 'tags': ['followedTags'],
- 'wallet': ['receiveAddress']
-};
-
+let currentPayload;
store.subscribe(() => {
- const state = (({ auth, claims, content, subscriptions, settings, tags, wallet }) =>
- ({ auth, claims, content, subscriptions, settings, tags, wallet }))(store.getState());
- NativeModules.StatePersistor.update(state, persistFilter);
-}); */
+ const state = store.getState();
+ const subscriptions = state.subscriptions.subscriptions.map(({ uri }) => uri);
+ const tags = state.tags.followedTags;
+
+ const newPayload = {
+ version: '0.1',
+ shared: {
+ subscriptions,
+ tags,
+ },
+ };
+
+ if (!isEqual(newPayload, currentPayload)) {
+ currentPayload = newPayload;
+ if (Lbryio.authToken) {
+ Lbryio.call('user_settings', 'set', { settings: JSON.stringify(newPayload) });
+ }
+ }
+});
// TODO: Find i18n module that is compatible with react-native
global.__ = str => str;
diff --git a/src/page/channel/index.js b/src/page/channel/index.js
index fae3125..42c42cf 100644
--- a/src/page/channel/index.js
+++ b/src/page/channel/index.js
@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
-import { makeSelectClaimForUri, selectMyChannelClaims } from 'lbry-redux';
+import { doAbandonClaim, doFetchChannelListMine, makeSelectClaimForUri, selectMyChannelClaims } from 'lbry-redux';
import { doPopDrawerStack } from 'redux/actions/drawer';
import { doSetSortByItem, doSetTimeItem } from 'redux/actions/settings';
import { selectDrawerStack } from 'redux/selectors/drawer';
@@ -15,6 +15,8 @@ const select = (state, props) => ({
});
const perform = dispatch => ({
+ abandonClaim: (txid, nout) => dispatch(doAbandonClaim(txid, nout)),
+ fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
popDrawerStack: () => dispatch(doPopDrawerStack()),
setSortByItem: item => dispatch(doSetSortByItem(item)),
setTimeItem: item => dispatch(doSetTimeItem(item)),
diff --git a/src/page/channel/view.js b/src/page/channel/view.js
index d20ffd5..2a787be 100644
--- a/src/page/channel/view.js
+++ b/src/page/channel/view.js
@@ -19,6 +19,7 @@ import ClaimList from 'component/claimList';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import Button from 'component/button';
+import EmptyStateView from 'component/emptyStateView';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link';
import ModalPicker from 'component/modalPicker';
@@ -47,7 +48,9 @@ class ChannelPage extends React.PureComponent {
}
componentDidMount() {
+ const { fetchChannelListMine } = this.props;
NativeModules.Firebase.setCurrentScreen('Channel');
+ fetchChannelListMine();
}
handleSortByItemSelected = item => {
@@ -128,9 +131,7 @@ class ChannelPage extends React.PureComponent {
return (
<View style={channelPageStyle.aboutTab}>
{!websiteUrl && !email && !description && (
- <View style={channelPageStyle.busyContainer}>
- <Text style={channelPageStyle.infoText}>Nothing here yet. Please check back later.</Text>
- </View>
+ <EmptyStateView message={"There's nothing here yet.\nPlease check back later."} />
)}
{(websiteUrl || email || description) && (
@@ -266,7 +267,7 @@ class ChannelPage extends React.PureComponent {
)}
{ownedChannel && (
<Button
- style={channelPageStyle.deleteButton}
+ style={[channelPageStyle.actionButton, channelPageStyle.deleteButton]}
theme={'light'}
icon={'trash-alt'}
text={'Delete'}
diff --git a/src/page/channelCreator/view.js b/src/page/channelCreator/view.js
index e41e0dc..76cc322 100644
--- a/src/page/channelCreator/view.js
+++ b/src/page/channelCreator/view.js
@@ -19,6 +19,7 @@ import Button from 'component/button';
import ChannelIconItem from 'component/channelIconItem';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
+import EmptyStateView from 'component/emptyStateView';
import FloatingWalletBalance from 'component/floatingWalletBalance';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link';
@@ -175,10 +176,8 @@ export default class ChannelCreator extends React.PureComponent {
if (!isEditMode && hasFormState) {
this.loadPendingFormState();
- this.setState({ currentPhase: Constants.PHASE_CREATE });
- } else {
- this.setState({ currentPhase: Constants.PHASE_LIST });
}
+ this.setState({ currentPhase: isEditMode || hasFormState ? Constants.PHASE_CREATE : Constants.PHASE_LIST });
});
};
@@ -240,7 +239,7 @@ export default class ChannelCreator extends React.PureComponent {
componentDidUpdate() {
const { channels = [] } = this.props;
const { editChannelUrl } = this.state;
- if (channels.length > 0) {
+ if (channels && channels.length > 0) {
if (this.state.autoStyles.length !== channels.length) {
this.setState({
autoStyles: this.generateAutoStyles(channels.length),
@@ -534,10 +533,12 @@ export default class ChannelCreator extends React.PureComponent {
};
showChannelList = () => {
- const { popDrawerStack } = this.props;
- popDrawerStack();
- this.resetChannelCreator();
+ const { drawerStack, popDrawerStack } = this.props;
+ if (drawerStack[drawerStack.length - 1].route === Constants.DRAWER_ROUTE_CHANNEL_CREATOR_FORM) {
+ popDrawerStack();
+ }
this.setState({ currentPhase: Constants.PHASE_LIST });
+ this.resetChannelCreator();
};
resetChannelCreator = () => {
@@ -606,6 +607,7 @@ export default class ChannelCreator extends React.PureComponent {
this.setState({
claimId: channel.claim_id,
currentPhase: Constants.PHASE_CREATE,
+ displayName: value && value.title ? value.title : channel.name.substring(1),
editMode: true,
coverImageUrl: value && value.cover ? value.cover.url : null,
currentChannelName: channel.name.substring(1),
@@ -744,6 +746,8 @@ export default class ChannelCreator extends React.PureComponent {
uploadingImage,
} = this.state;
+ const hasChannels = channels && channels.length > 0;
+
return (
<View style={channelCreatorStyle.container}>
<UriBar
@@ -756,34 +760,34 @@ export default class ChannelCreator extends React.PureComponent {
onExitSelectionMode={this.onExitSelectionMode}
/>
+ {fetchingChannels && (
+ <View style={channelCreatorStyle.loading}>
+ <ActivityIndicator size={'large'} color={Colors.NextLbryGreen} />
+ </View>
+ )}
+
+ {currentPhase === Constants.PHASE_LIST && !fetchingChannels && !hasChannels && (
+ <EmptyStateView
+ message={'You have not created a channel.\nStart now by creating a new channel!'}
+ buttonText={'Create a channel'}
+ onButtonPress={this.handleNewChannelPress}
+ />
+ )}
+
{currentPhase === Constants.PHASE_LIST && (
<FlatList
extraData={this.state}
- ListHeaderComponent={
- fetchingChannels ? (
- <View style={channelCreatorStyle.listHeader}>
- <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />
- </View>
- ) : null
- }
- ListEmptyComponent={
- fetchingChannels ? null : (
- <View style={channelCreatorStyle.listEmpty}>
- <Text style={channelCreatorStyle.listEmptyText}>
- You have not created a channel. Start now by creating a new channel!
- </Text>
+ ListFooterComponent={
+ !channels || channels.length === 0 ? null : (
+ <View style={channelCreatorStyle.listFooter}>
+ <Button
+ style={channelCreatorStyle.createChannelButton}
+ text={'Create a channel'}
+ onPress={this.handleNewChannelPress}
+ />
</View>
)
}
- ListFooterComponent={
- <View style={channelCreatorStyle.listFooter}>
- <Button
- style={channelCreatorStyle.createChannelButton}
- text={'Create a channel'}
- onPress={this.handleNewChannelPress}
- />
- </View>
- }
style={channelCreatorStyle.scrollContainer}
contentContainerStyle={channelCreatorStyle.scrollPadding}
initialNumToRender={10}
@@ -825,7 +829,7 @@ export default class ChannelCreator extends React.PureComponent {
</TouchableOpacity>
);
}}
- data={channels.filter(channel => !abandoningClaimIds.includes(channel.claim_id))}
+ data={channels ? channels.filter(channel => !abandoningClaimIds.includes(channel.claim_id)) : []}
keyExtractor={(item, index) => item.claim_id}
/>
)}
@@ -864,7 +868,7 @@ export default class ChannelCreator extends React.PureComponent {
source={{ uri: thumbnailUrl }}
/>
)}
- {thumbnailUrl !== null && thumbnailUrl.trim().length === 0 && newChannelName.length > 1 && (
+ {(thumbnailUrl === null || thumbnailUrl.trim().length === 0) && newChannelName.length > 1 && (
<Text style={channelIconStyle.autothumbCharacter}>
{newChannelName.substring(0, 1).toUpperCase()}
</Text>
diff --git a/src/page/downloads/view.js b/src/page/downloads/view.js
index ddc5206..aa349d6 100644
--- a/src/page/downloads/view.js
+++ b/src/page/downloads/view.js
@@ -15,6 +15,7 @@ import { __, navigateToUri, uriFromFileInfo } from 'utils/helper';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import PageHeader from 'component/pageHeader';
+import EmptyStateView from 'component/emptyStateView';
import FileListItem from 'component/fileListItem';
import FloatingWalletBalance from 'component/floatingWalletBalance';
import StorageStatsCard from 'component/storageStatsCard';
@@ -154,6 +155,10 @@ class DownloadsPage extends React.PureComponent {
onDeleteActionPressed={this.onDeleteActionPressed}
/>
+ {!fetching && !hasDownloads && (
+ <EmptyStateView message={'You do not have any\ndownloaded content on this device.'} />
+ )}
+
<View style={downloadsStyle.subContainer}>
{hasDownloads && <StorageStatsCard fileInfos={this.getFilteredFileInfos()} />}
{fetching && (
@@ -161,11 +166,7 @@ class DownloadsPage extends React.PureComponent {
<ActivityIndicator size="large" color={Colors.NextLbryGreen} style={downloadsStyle.loading} />
</View>
)}
- {!fetching && !hasDownloads && (
- <View style={downloadsStyle.busyContainer}>
- <Text style={downloadsStyle.noDownloadsText}>You do not have any downloaded content on this device.</Text>
- </View>
- )}
+
{!fetching && hasDownloads && (
<FlatList
extraData={this.state}
diff --git a/src/page/file/view.js b/src/page/file/view.js
index 950511c..36a0d63 100644
--- a/src/page/file/view.js
+++ b/src/page/file/view.js
@@ -22,6 +22,7 @@ import { navigateBack, navigateToUri } from 'utils/helper';
import Icon from 'react-native-vector-icons/FontAwesome5';
import ImageViewer from 'react-native-image-zoom-viewer';
import Button from 'component/button';
+import EmptyStateView from 'component/emptyStateView';
import Tag from 'component/tag';
import ChannelPage from 'page/channel';
import Colors from 'styles/colors';
@@ -100,7 +101,7 @@ class FilePage extends React.PureComponent {
DeviceEventEmitter.addListener('onDownloadUpdated', this.handleDownloadUpdated);
DeviceEventEmitter.addListener('onDownloadCompleted', this.handleDownloadCompleted);
- const { fetchChannelListMine, fileInfo, isResolvingUri, resolveUri, navigation } = this.props;
+ const { fetchMyClaims, fileInfo, isResolvingUri, resolveUri, navigation } = this.props;
const { uri, uriVars } = navigation.state.params;
this.setState({ uri, uriVars });
@@ -108,7 +109,7 @@ class FilePage extends React.PureComponent {
this.fetchFileInfo(this.props);
this.fetchCostInfo(this.props);
- fetchChannelListMine();
+ fetchMyClaims();
if (NativeModules.Firebase) {
NativeModules.Firebase.track('open_file_page', { uri: uri });
@@ -590,14 +591,15 @@ class FilePage extends React.PureComponent {
} = this.props;
const { uri, autoplay } = navigation.state.params;
+ const { isChannel } = parseURI(uri);
const myChannelUris = channels ? channels.map(channel => channel.permanent_url) : [];
const ownedClaim = myClaimUris.includes(uri) || myChannelUris.includes(uri);
- const { isChannel } = parseURI(uri);
let innerContent = null;
if ((isResolvingUri && !claim) || !claim) {
return (
<View style={filePageStyle.container}>
+ <UriBar value={uri} navigation={navigation} />
{isResolvingUri && (
<View style={filePageStyle.busyContainer}>
<ActivityIndicator size="large" color={Colors.NextLbryGreen} />
@@ -607,16 +609,29 @@ class FilePage extends React.PureComponent {
{claim === null && !isResolvingUri && (
<View style={filePageStyle.container}>
{ownedClaim && (
- <Text style={filePageStyle.emptyClaimText}>
- {isChannel
- ? 'It looks like you just created this channel. It will appear in a few minutes.'
- : 'It looks you just published this content. It will appear in a few minutes.'}
- </Text>
+ <EmptyStateView
+ message={
+ isChannel
+ ? 'It looks like you just created this channel. It will appear in a few minutes.'
+ : 'It looks you just published this content. It will appear in a few minutes.'
+ }
+ />
+ )}
+ {!ownedClaim && (
+ <EmptyStateView
+ message={"There's nothing at this location."}
+ buttonText={'Publish something here'}
+ onButtonPress={() =>
+ navigation.navigate({
+ routeName: Constants.DRAWER_ROUTE_PUBLISH,
+ params: { vanityUrl: uri.trim() },
+ })
+ }
+ />
)}
- {!ownedClaim && <Text style={filePageStyle.emptyClaimText}>There's nothing at this location.</Text>}
</View>
)}
- <UriBar value={uri} navigation={navigation} />
+ <FloatingWalletBalance navigation={navigation} />
</View>
);
}
diff --git a/src/page/publish/index.js b/src/page/publish/index.js
index ffbadf8..009d602 100644
--- a/src/page/publish/index.js
+++ b/src/page/publish/index.js
@@ -4,7 +4,6 @@ import {
doResolveUri,
doToast,
doUpdatePublishForm,
- doUploadThumbnail,
selectBalance,
selectPublishFormValues,
} from 'lbry-redux';
@@ -28,7 +27,6 @@ const perform = dispatch => ({
clearPublishFormState: () => dispatch(doClearPublishFormState()),
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
updatePublishFormState: data => dispatch(doUpdatePublishFormState(data)),
- uploadThumbnail: (filePath, fsAdapter) => dispatch(doUploadThumbnail(filePath, null, fsAdapter)),
publish: (success, fail) => dispatch(doPublish(success, fail)),
resolveUri: uri => dispatch(doResolveUri(uri)),
pushDrawerStack: (routeName, params) => dispatch(doPushDrawerStack(routeName, params)),
diff --git a/src/page/publish/view.js b/src/page/publish/view.js
index 5d5d2f2..0d6a4c7 100644
--- a/src/page/publish/view.js
+++ b/src/page/publish/view.js
@@ -43,7 +43,7 @@ import Tag from 'component/tag';
import TagSearch from 'component/tagSearch';
import UriBar from 'component/uriBar';
import publishStyle from 'styles/publish';
-import { __, navigateToUri } from 'utils/helper';
+import { __, navigateToUri, uploadImageAsset } from 'utils/helper';
const languages = {
en: 'English',
@@ -129,6 +129,8 @@ class PublishPage extends React.PureComponent {
uploadedThumbnailUri: null,
vanityUrlSet: false,
+ thumbnailImagePickerOpen: false,
+
// other
publishStarted: false,
};
@@ -218,10 +220,8 @@ class PublishPage extends React.PureComponent {
// replace name with the specified vanity URL if there was one in the pending state
this.setState({ name: this.state.vanityUrl });
}
- this.setState({ currentPhase: Constants.PHASE_DETAILS });
- } else {
- this.setState({ currentPhase: Constants.PHASE_SELECTOR });
}
+ this.setState({ currentPhase: isEditMode || hasFormState ? Constants.PHASE_DETAILS : Constants.PHASE_SELECTOR });
});
};
@@ -256,6 +256,7 @@ class PublishPage extends React.PureComponent {
this.setState(
{
editMode: true,
+ publishStarted: false,
currentPhase: Constants.PHASE_DETAILS,
hasEditedContentAddress: true,
@@ -382,10 +383,12 @@ class PublishPage extends React.PureComponent {
};
handlePublishSuccess = data => {
- const { navigation, notify } = this.props;
+ const { clearPublishFormState, navigation, notify } = this.props;
notify({
message: `Your content was successfully published to ${this.state.uri}. It will be available in a few mintues.`,
});
+ clearPublishFormState();
+ this.setState({ publishStarted: false });
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_PUBLISHES, params: { publishSuccess: true } });
};
@@ -407,14 +410,6 @@ class PublishPage extends React.PureComponent {
this.onComponentFocused();
}
- if (publishFormValues) {
- if (publishFormValues.thumbnail && !this.state.uploadedThumbnailUri) {
- const { thumbnail } = publishFormValues;
- updatePublishFormState({ currentThumbnailUri: thumbnail, uploadedThumbnailUri: thumbnail });
- this.setState({ currentThumbnailUri: thumbnail, uploadedThumbnailUri: thumbnail });
- }
- }
-
if (
this.state.currentPhase === Constants.PHASE_DETAILS &&
prevDrawerStack[prevDrawerStack.length - 1].route === Constants.DRAWER_ROUTE_PUBLISH_FORM &&
@@ -432,6 +427,7 @@ class PublishPage extends React.PureComponent {
updatePublishFormState({ currentMedia: media, name: newName });
this.setState(
{
+ publishStarted: false,
currentMedia: media,
title: null, // no title autogeneration (user will fill this in)
name: newName,
@@ -449,7 +445,7 @@ class PublishPage extends React.PureComponent {
};
showSelector() {
- const { updatePublishForm } = this.props;
+ const { clearPublishFormState, updatePublishForm } = this.props;
this.setState(
{
@@ -486,9 +482,12 @@ class PublishPage extends React.PureComponent {
selectedChannel: null,
uploadedThumbnailUri: null,
+ thumbnailImagePickerOpen: false,
+
vanityUrlSet: false,
},
() => {
+ clearPublishFormState();
// reset thumbnail
updatePublishForm({ thumbnail: null });
}
@@ -522,19 +521,59 @@ class PublishPage extends React.PureComponent {
);
};
- onFilePicked = evt => {
- this.setState({ documentPickerOpen: false }, () => {
- const currentMedia = {
- id: -1,
- filePath: `file://${evt.path}`,
- duration: 0,
- };
- this.setCurrentMedia(currentMedia);
+ handleThumbnailUploadSuccess = ({ url }) => {
+ const { updatePublishFormState } = this.props;
+
+ this.setState({
+ uploadThumbnailStarted: false,
+ currentThumbnailUri: url,
+ uploadedThumbnailUri: url,
});
+ updatePublishFormState({ currentThumbnailUri: url, uploadedThumbnailUri: url });
+ };
+
+ handleThumbnailUploadFailure = err => {
+ const { notify } = this.props;
+ this.setState({ uploadThumbnailStarted: false });
+ notify({ message: 'The thumbnail could not be uploaded. Please try again.' });
+ };
+
+ onFilePicked = evt => {
+ const { notify } = this.props;
+ if (evt.path && evt.path.length > 0) {
+ const fileUrl = `file://${evt.path}`;
+
+ if (this.state.documentPickerOpen) {
+ this.setState({ documentPickerOpen: false, thumbnailImagePickerOpen: false }, () => {
+ const currentMedia = {
+ id: -1,
+ filePath: fileUrl,
+ duration: 0,
+ };
+ this.setCurrentMedia(currentMedia);
+ });
+ } else if (this.state.thumbnailImagePickerOpen) {
+ this.setState(
+ {
+ documentPickerOpen: false,
+ thumbnailImagePickerOpen: false,
+ uploadThumbnailStarted: true,
+ currentThumbnailUri: fileUrl,
+ },
+ () => {
+ // upload a new thumbnail
+ uploadImageAsset(fileUrl, this.handleThumbnailUploadSuccess, this.handleThumbnailUploadFailure);
+ }
+ );
+ }
+ } else {
+ // could not determine the file path
+ notify({ message: 'The path could not be determined. Please try a different file.' });
+ }
};
onPickerCanceled = () => {
- this.setState({ documentPickerOpen: false });
+ this.setState({ documentPickerOpen: false, thumbnailImagePickerOpen: false });
};
handleCloseCameraPressed = () => {
@@ -569,6 +608,7 @@ class PublishPage extends React.PureComponent {
{
currentThumbnailUri: null,
updatingThumbnailUri: false,
+ publishStarted: false,
currentPhase: Constants.PHASE_DETAILS,
showCameraOverlay: false,
videoRecordingMode: false,
@@ -593,6 +633,7 @@ class PublishPage extends React.PureComponent {
{
currentPhase: Constants.PHASE_DETAILS,
currentThumbnailUri: null,
+ publishStarted: false,
updatingThumbnailUri: false,
showCameraOverlay: false,
videoRecordingMode: false,
@@ -611,8 +652,6 @@ class PublishPage extends React.PureComponent {
});
};
- handleUploadPressed = () => {};
-
getRandomFileId = () => {
// generate a random id for a photo or recorded video between 1 and 20 (for creating thumbnails)
const id = Math.floor(Math.random() * (20 - 2)) + 1;
@@ -699,7 +738,7 @@ class PublishPage extends React.PureComponent {
return;
}
- const { notify, uploadThumbnail } = this.props;
+ const { notify } = this.props;
const { thumbnailPath } = this.state;
this.setState({ updatingThumbnailUri: true });
@@ -714,7 +753,13 @@ class PublishPage extends React.PureComponent {
// upload the thumbnail
if (!this.state.uploadedThumbnailUri) {
- this.setState({ uploadThumbnailStarted: true }, () => uploadThumbnail(this.getFilePathFromUri(uri), RNFS));
+ this.setState({ uploadThumbnailStarted: true }, () =>
+ uploadImageAsset(
+ this.getFilePathFromUri(uri),
+ this.handleThumbnailUploadSuccess,
+ this.handleThumbnailUploadFailure
+ )
+ );
}
} else if (mediaType === 'image' || mediaType === 'video') {
const create =
@@ -725,7 +770,9 @@ class PublishPage extends React.PureComponent {
.then(path => {
this.setState({ currentThumbnailUri: `file://${path}`, updatingThumbnailUri: false });
if (!this.state.uploadedThumbnailUri) {
- this.setState({ uploadThumbnailStarted: true }, () => uploadThumbnail(path, RNFS));
+ this.setState({ uploadThumbnailStarted: true }, () =>
+ uploadImageAsset(path, this.handleThumbnailUploadSuccess, this.handleThumbnailUploadFailure)
+ );
}
})
.catch(err => {
@@ -781,7 +828,7 @@ class PublishPage extends React.PureComponent {
: '';
const licenseUrl = LICENSES.CC_LICENSES.reduce((value, item) => {
if (typeof value === 'object') {
- value = '';
+ value = license === value.value ? item.url : '';
}
if (license === item.value) {
value = item.url;
@@ -799,6 +846,25 @@ class PublishPage extends React.PureComponent {
this.setState({ otherLicenseDescription });
};
+ handleThumbnailPressed = () => {
+ const { notify } = this.props;
+ if (this.state.thumbnailImagePickerOpen || this.state.uploadThumbnailStarted) {
+ if (this.state.uploadThumbnailStarted) {
+ notify({ message: 'A thumbnail is already being uploaded. Please wait for the upload to finish.' });
+ }
+ return;
+ }
+
+ this.setState(
+ {
+ thumbnailImagePickerOpen: true,
+ },
+ () => {
+ NativeModules.UtilityModule.openDocumentPicker('image/*');
+ }
+ );
+ };
+
render() {
const { balance, navigation, notify, publishFormValues } = this.props;
const {
@@ -893,22 +959,24 @@ class PublishPage extends React.PureComponent {
}
content = (
<ScrollView style={publishStyle.publishDetails}>
- {currentThumbnailUri && currentThumbnailUri.trim().length > 0 && (
- <View style={publishStyle.mainThumbnailContainer}>
- <FastImage
- style={publishStyle.mainThumbnail}
- resizeMode={FastImage.resizeMode.contain}
- source={{ uri: currentThumbnailUri }}
- />
+ <TouchableOpacity style={publishStyle.mainThumbnailContainer} onPress={this.handleThumbnailPressed}>
+ <FastImage
+ style={publishStyle.mainThumbnail}
+ resizeMode={FastImage.resizeMode.contain}
+ source={{ uri: currentThumbnailUri }}
+ />
- {this.state.uploadThumbnailStarted && !this.state.uploadedThumbnailUri && (
- <View style={publishStyle.thumbnailUploadContainer}>
- <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />
- <Text style={publishStyle.thumbnailUploadText}>Uploading thumbnail...</Text>
- </View>
- )}
+ <View style={publishStyle.thumbnailEditOverlay}>
+ <Icon name={'edit'} style={publishStyle.editIcon} />
</View>
- )}
+
+ {this.state.uploadThumbnailStarted && (
+ <View style={publishStyle.thumbnailUploadContainer}>
+ <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />
+ <Text style={publishStyle.thumbnailUploadText}>Uploading thumbnail...</Text>
+ </View>
+ )}
+ </TouchableOpacity>
{!this.state.canPublish && <PublishRewardsDriver navigation={navigation} />}
<View style={publishStyle.card}>
@@ -1105,21 +1173,21 @@ class PublishPage extends React.PureComponent {
</View>
<View style={publishStyle.actionButtons}>
- {(this.state.publishStarted || publishFormValues.publishing) && (
+ {this.state.publishStarted && (
<View style={publishStyle.progress}>
<ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />
</View>
)}
- {!publishFormValues.publishing && !this.state.publishStarted && (
+ {!this.state.publishStarted && (
<Link style={publishStyle.cancelLink} text="Cancel" onPress={() => this.showSelector()} />
)}
- {!publishFormValues.publishing && !this.state.publishStarted && (
+ {!this.state.publishStarted && (
<View style={publishStyle.rightActionButtons}>
<Button
style={publishStyle.publishButton}
- disabled={!this.state.canPublish || !this.state.uploadedThumbnailUri}
+ disabled={!this.state.canPublish || this.state.uploadThumbnailStarted}
text={this.state.editMode ? 'Save changes' : 'Publish'}
onPress={this.handlePublishPressed}
/>
diff --git a/src/page/publishes/view.js b/src/page/publishes/view.js
index 68a3a85..61a3bb5 100644
--- a/src/page/publishes/view.js
+++ b/src/page/publishes/view.js
@@ -3,6 +3,7 @@ import { ActivityIndicator, Alert, FlatList, NativeModules, Text, TouchableOpaci
import Button from 'component/button';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
+import EmptyStateView from 'component/emptyStateView';
import FileListItem from 'component/fileListItem';
import FloatingWalletBalance from 'component/floatingWalletBalance';
import UriBar from 'component/uriBar';
@@ -137,16 +138,11 @@ class PublishesPage extends React.PureComponent {
)}
{!fetching && (!uris || uris.length === 0) && (
- <View style={publishStyle.noPublishes}>
- <Text style={publishStyle.noPublishText}>
- {__('It looks like you have not published anything to LBRY yet.')}
- </Text>
- <Button
- style={publishStyle.publishNowButton}
- text={__('Publish something new')}
- onPress={() => navigation.navigate({ routeName: Constants.DRAWER_ROUTE_PUBLISH })}
- />
- </View>
+ <EmptyStateView
+ message={__('It looks like you have not\npublished any content to LBRY yet.')}
+ buttonText={__('Publish something new')}
+ onButtonPress={() => navigation.navigate({ routeName: Constants.DRAWER_ROUTE_PUBLISH })}
+ />
)}
{uris && uris.length > 0 && (
diff --git a/src/page/splash/index.js b/src/page/splash/index.js
index d3b644e..031aaf8 100644
--- a/src/page/splash/index.js
+++ b/src/page/splash/index.js
@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
-import { doBalanceSubscribe, doUpdateBlockHeight, doToast } from 'lbry-redux';
+import { doBalanceSubscribe, doUpdateBlockHeight, doPopulateSharedUserState, doToast } from 'lbry-redux';
import {
doAuthenticate,
doBlackListedOutpointsSubscribe,
@@ -34,6 +34,7 @@ const perform = dispatch => ({
notify: data => dispatch(doToast(data)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
+ populateSharedUserState: settings => dispatch(doPopulateSharedUserState(settings)),
updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
verifyUserEmail: (token, recaptcha) => dispatch(doUserEmailVerify(token, recaptcha)),
verifyUserEmailFailure: error => dispatch(doUserEmailVerifyFailure(error)),
diff --git a/src/page/splash/view.js b/src/page/splash/view.js
index 567cfec..0355a83 100644
--- a/src/page/splash/view.js
+++ b/src/page/splash/view.js
@@ -1,5 +1,6 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
+import { Lbryio } from 'lbryinc';
import { ActivityIndicator, Linking, NativeModules, Platform, Text, View } from 'react-native';
import { NavigationActions, StackActions } from 'react-navigation';
import { decode as atob } from 'base-64';
@@ -15,6 +16,8 @@ import splashStyle from 'styles/splash';
const BLOCK_HEIGHT_INTERVAL = 1000 * 60 * 2.5; // every 2.5 minutes
+const SETTINGS_GET_INTERVAL = 1000 * 60 * 5; // every 5 minutes
+
const testingNetwork = 'Testing network';
const waitingForResolution = 'Waiting for name resolution';
@@ -108,6 +111,13 @@ class SplashScreen extends React.PureComponent {
}
}
+ getUserSettings = () => {
+ const { populateSharedUserState } = this.props;
+ Lbryio.call('user_settings', 'get').then(settings => {
+ populateSharedUserState(settings);
+ });
+ };
+
finishSplashScreen = () => {
const {
authenticate,
@@ -127,6 +137,10 @@ class SplashScreen extends React.PureComponent {
filteredOutpointsSubscribe();
checkSubscriptionsInit();
+ // get user settings interval
+ this.getUserSettings();
+ setInterval(() => this.getUserSettings(), SETTINGS_GET_INTERVAL);
+
if (user && user.id && user.has_verified_email) {
// user already authenticated
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
diff --git a/src/page/transactionHistory/view.js b/src/page/transactionHistory/view.js
index 0fe6839..47d5a93 100644
--- a/src/page/transactionHistory/view.js
+++ b/src/page/transactionHistory/view.js
@@ -2,6 +2,7 @@ import React from 'react';
import { ActivityIndicator, NativeModules, View, ScrollView, Text } from 'react-native';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
+import EmptyStateView from 'component/emptyStateView';
import TransactionList from 'component/transactionList';
import UriBar from 'component/uriBar';
import walletStyle from 'styles/wallet';
@@ -53,11 +54,9 @@ class TransactionHistoryPage extends React.PureComponent {
<Text style={walletStyle.loadingText}>Loading transactions...</Text>
</View>
)}
+ {!fetchingTransactions && transactions.length === 0 && <EmptyStateView message={'No transactions to list.'} />}
<ScrollView style={walletStyle.transactionHistoryScroll}>
<View style={walletStyle.historyList}>
- {!fetchingTransactions && transactions.length === 0 && (
- <Text style={walletStyle.infoText}>No transactions to list.</Text>
- )}
{!fetchingTransactions && transactions && transactions.length > 0 && (
<TransactionList navigation={navigation} transactions={transactions} />
)}
diff --git a/src/page/verification/internal/sync-verify-page.js b/src/page/verification/internal/sync-verify-page.js
index 6862733..8bccb37 100644
--- a/src/page/verification/internal/sync-verify-page.js
+++ b/src/page/verification/internal/sync-verify-page.js
@@ -43,7 +43,7 @@ class SyncVerifyPage extends React.PureComponent {
navigation.goBack();
});
} else {
- syncApply(syncHash, syncData, this.state.password);
+ syncApply(syncHash, syncData, this.state.password ? this.state.password : '');
}
});
};
diff --git a/src/styles/channelCreator.js b/src/styles/channelCreator.js
index 39eda77..8f0de60 100644
--- a/src/styles/channelCreator.js
+++ b/src/styles/channelCreator.js
@@ -121,7 +121,7 @@ const channelCreatorStyle = StyleSheet.create({
height: '100%',
},
listFooter: {
- marginTop: 24,
+ marginTop: 8,
},
createChannelButton: {
backgroundColor: Colors.LbryGreen,
@@ -254,6 +254,15 @@ const channelCreatorStyle = StyleSheet.create({
fontSize: 12,
marginLeft: 4,
},
+ loading: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
});
export default channelCreatorStyle;
diff --git a/src/styles/channelPage.js b/src/styles/channelPage.js
index e76f8a2..d9963da 100644
--- a/src/styles/channelPage.js
+++ b/src/styles/channelPage.js
@@ -169,6 +169,9 @@ const channelPageStyle = StyleSheet.create({
actionButton: {
backgroundColor: Colors.White,
},
+ deleteButton: {
+ marginLeft: 8,
+ },
});
export default channelPageStyle;
diff --git a/src/styles/emptyState.js b/src/styles/emptyState.js
new file mode 100644
index 0000000..4d30c8d
--- /dev/null
+++ b/src/styles/emptyState.js
@@ -0,0 +1,40 @@
+import { StyleSheet } from 'react-native';
+import Colors from './colors';
+
+const emptyStateStyle = StyleSheet.create({
+ container: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ bottom: 0,
+ paddingLeft: 24,
+ paddingRight: 24,
+ alignItems: 'center',
+ justifyContent: 'center',
+ zIndex: 99,
+ },
+ outerContainer: {
+ top: 60,
+ },
+ innerContainer: {
+ top: 0,
+ },
+ button: {
+ backgroundColor: Colors.LbryGreen,
+ fontSize: 18,
+ },
+ image: {
+ width: 128,
+ height: 170,
+ },
+ message: {
+ marginTop: 24,
+ textAlign: 'center',
+ fontFamily: 'Inter-UI-Regular',
+ fontSize: 18,
+ lineHeight: 28,
+ marginBottom: 24,
+ },
+});
+
+export default emptyStateStyle;
diff --git a/src/styles/publish.js b/src/styles/publish.js
index cfaa6f5..0967c55 100644
--- a/src/styles/publish.js
+++ b/src/styles/publish.js
@@ -1,6 +1,9 @@
-import { StyleSheet } from 'react-native';
+import { Dimensions, StyleSheet } from 'react-native';
import Colors from './colors';
+const screenDimension = Dimensions.get('window');
+const screenWidth = screenDimension.width;
+
const publishStyle = StyleSheet.create({
container: {
flex: 1,
@@ -434,7 +437,7 @@ const publishStyle = StyleSheet.create({
color: Colors.DescriptionGrey,
},
publishesFooter: {
- marginTop: 16,
+ marginTop: 2,
marginLeft: 16,
marginRight: 16,
},
@@ -443,6 +446,21 @@ const publishStyle = StyleSheet.create({
backgroundColor: Colors.LbryGreen,
marginTop: 16,
},
+ thumbnailEditOverlay: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 24,
+ position: 'absolute',
+ padding: 8,
+ left: screenWidth / 2 - 32 / 2,
+ bottom: 8,
+ backgroundColor: '#00000077',
+ },
+ editIcon: {
+ color: Colors.White,
+ fontFamily: 'Inter-UI-SemiBold',
+ fontSize: 12,
+ },
});
export default publishStyle;
diff --git a/src/utils/deep-equal.js b/src/utils/deep-equal.js
new file mode 100644
index 0000000..1925763
--- /dev/null
+++ b/src/utils/deep-equal.js
@@ -0,0 +1,117 @@
+/* eslint-disable */
+// underscore's deep equal function
+// https://github.com/jashkenas/underscore/blob/master/underscore.js#L1189
+
+export default function isEqual(a, b, aStack, bStack) {
+ // Identical objects are equal. `0 === -0`, but they aren't identical.
+ // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
+ if (a === b) return a !== 0 || 1 / a === 1 / b;
+ // `null` or `undefined` only equal to itself (strict comparison).
+ if (a == null || b == null) return false;
+ // `NaN`s are equivalent, but non-reflexive.
+ if (a !== a) return b !== b;
+ // Exhaust primitive checks
+ var type = typeof a;
+ if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
+ return deepEq(a, b, aStack, bStack);
+}
+
+function deepEq(a, b, aStack, bStack) {
+ // Compare `[[Class]]` names.
+ var className = toString.call(a);
+ if (className !== toString.call(b)) return false;
+ switch (className) {
+ // Strings, numbers, regular expressions, dates, and booleans are compared by value.
+ case '[object RegExp]':
+ // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
+ case '[object String]':
+ // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+ // equivalent to `new String("5")`.
+ return '' + a === '' + b;
+ case '[object Number]':
+ // `NaN`s are equivalent, but non-reflexive.
+ // Object(NaN) is equivalent to NaN.
+ if (+a !== +a) return +b !== +b;
+ // An `egal` comparison is performed for other numeric values.
+ return +a === 0 ? 1 / +a === 1 / b : +a === +b;
+ case '[object Date]':
+ case '[object Boolean]':
+ // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+ // millisecond representations. Note that invalid dates with millisecond representations
+ // of `NaN` are not equivalent.
+ return +a === +b;
+ case '[object Symbol]':
+ return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
+ }
+
+ var areArrays = className === '[object Array]';
+ if (!areArrays) {
+ if (typeof a != 'object' || typeof b != 'object') return false;
+
+ // Objects with different constructors are not equivalent, but `Object`s or `Array`s
+ // from different frames are.
+ var aCtor = a.constructor,
+ bCtor = b.constructor;
+ if (
+ aCtor !== bCtor &&
+ !(
+ typeof aCtor === 'function' &&
+ aCtor instanceof aCtor &&
+ typeof bCtor === 'function' &&
+ bCtor instanceof bCtor
+ ) &&
+ ('constructor' in a && 'constructor' in b)
+ ) {
+ return false;
+ }
+ }
+ // Assume equality for cyclic structures. The algorithm for detecting cyclic
+ // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+
+ // Initializing stack of traversed objects.
+ // It's done here since we only need them for objects and arrays comparison.
+ aStack = aStack || [];
+ bStack = bStack || [];
+ var length = aStack.length;
+ while (length--) {
+ // Linear search. Performance is inversely proportional to the number of
+ // unique nested structures.
+ if (aStack[length] === a) return bStack[length] === b;
+ }
+
+ // Add the first object to the stack of traversed objects.
+ aStack.push(a);
+ bStack.push(b);
+
+ // Recursively compare objects and arrays.
+ if (areArrays) {
+ // Compare array lengths to determine if a deep comparison is necessary.
+ length = a.length;
+ if (length !== b.length) return false;
+ // Deep compare the contents, ignoring non-numeric properties.
+ while (length--) {
+ if (!isEqual(a[length], b[length], aStack, bStack)) return false;
+ }
+ } else {
+ // Deep compare objects.
+ var keys = Object.keys(a),
+ key;
+ length = keys.length;
+ // Ensure that both objects contain the same number of properties before comparing deep equality.
+ if (Object.keys(b).length !== length) return false;
+ while (length--) {
+ // Deep compare each member
+ key = keys[length];
+ if (!(has(b, key) && isEqual(a[key], b[key], aStack, bStack))) return false;
+ }
+ }
+ // Remove the first object from the stack of traversed objects.
+ aStack.pop();
+ bStack.pop();
+ return true;
+}
+
+function has(obj, path) {
+ return obj != null && hasOwnProperty.call(obj, path);
+}
+/* eslint-enable */