React Native codebase as a submodule #604
185 changed files with 13 additions and 24994 deletions
.gitlab-ci.yml.gitmodulesBUILD.mdDOCKER.mdQUICKSTART.mdapp
.babelrc.eslintrc.json.lintstagedrc.json.prettierrc.jsonbundle.shindex.jspackage-lock.jsonpackage.json
src
assets
component
AppNavigator.js
address
button
categoryList
channelSelector
customRewardCard
dateTime
drawerContent
fileDownloadButton
fileItem
fileItemMedia
fileList
fileListItem
filePrice
fileRewardsDriver
floatingWalletBalance
link
mediaPlayer
navigationButton
nsfwOverlay
pageHeader
progressBar
publishRewardsDriver
relatedContent
rewardCard
rewardEnrolment
rewardSummary
searchInput
searchRightHeaderIcon
storageStatsCard
subscribeButton
subscribeNotificationButton
suggestedSubscriptionItem
suggestedSubscriptions
tag
tagSearch
transactionList
transactionListRecent
uriBar
walletAddress
walletBalance
|
@ -8,6 +8,8 @@ build apk:
|
|||
image: lbry/android-base:latest
|
||||
before_script:
|
||||
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
|
||||
- git submodule sync --recursive
|
||||
- git submodule update --init --recursive
|
||||
artifacts:
|
||||
paths:
|
||||
- bin/browser-*-release.apk
|
||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "app"]
|
||||
path = app
|
||||
url = https://github.com/lbryio/lbry-react-native
|
2
BUILD.md
2
BUILD.md
|
@ -118,6 +118,8 @@ echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platfo
|
|||
#### Build and Deploy
|
||||
Run `npm install -g react-native-cli` to install React Native CLI tools.
|
||||
|
||||
Initialise git submodules by running `git submodule update --init --recursive` in the `lbry-android` folder.
|
||||
|
||||
Run `npm i` in the `lbry-android/app` folder to install the necessary modules required by the React Native user interface, and then run `./bundle.sh`.
|
||||
|
||||
Run `./build.sh` in `lbry-android` to build the APK. The output can be found in the `bin` subdirectory.
|
||||
|
|
|
@ -24,7 +24,9 @@ tar -xvf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.build
|
|||
rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
|
||||
ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
|
||||
git clone https://github.com/lbryio/lbry-android
|
||||
cd lbry-android;cp buildozer.spec.sample buildozer.spec
|
||||
cd lbry-android
|
||||
git submodule update --init --recursive
|
||||
cp buildozer.spec.sample buildozer.spec
|
||||
cd app;npm i;cd ..
|
||||
cp scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
|
||||
cp scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h
|
||||
|
|
|
@ -72,10 +72,11 @@ wget 'https://www.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -
|
|||
```
|
||||
|
||||
### Step 7 of 10
|
||||
Clone the lbryio/lbry-android git repository and create your `buildozer.spec` and `google-services.json` files. The provided `buildozer.spec.sample` contains defaults provided you followed steps 1 through 5 exactly as described. You can also customise the spec file if you want to. The `google-services.sample.json` can be used to ensure the build completes successfully.
|
||||
Clone the lbryio/lbry-android git repository, initialise submodules and create your `buildozer.spec` and `google-services.json` files. The provided `buildozer.spec.sample` contains defaults provided you followed steps 1 through 5 exactly as described. You can also customise the spec file if you want to. The `google-services.sample.json` can be used to ensure the build completes successfully.
|
||||
```
|
||||
git clone https://github.com/lbryio/lbry-android
|
||||
cd lbry-android
|
||||
git submodule update --init --recursive
|
||||
cp buildozer.spec.sample buildozer.spec
|
||||
cp p4a/pythonforandroid/bootstraps/lbry/templates/google-services.sample.json p4a/pythonforandroid/bootstraps/lbry/templates/google-services.json
|
||||
```
|
||||
|
|
1
app
Submodule
1
app
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit e3f66e4fa67867b1e2b80ada480cd05808cad136
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"presets": ["module:metro-react-native-babel-preset"],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
["module-resolver", {
|
||||
root: ["./src"],
|
||||
}],
|
||||
]
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"parser": "babel-eslint",
|
||||
"extends": ["standard", "standard-jsx", "plugin:flowtype/recommended"],
|
||||
"plugins": ["flowtype", "import"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"globals": {
|
||||
"__": true
|
||||
},
|
||||
"rules": {
|
||||
"no-multi-spaces": 0,
|
||||
"new-cap": 0,
|
||||
"prefer-promise-reject-errors": 0,
|
||||
"no-unused-vars": 0,
|
||||
"standard/object-curly-even-spacing": 0,
|
||||
"handle-callback-err": 0,
|
||||
"one-var": 0,
|
||||
"object-curly-spacing": 0,
|
||||
"no-redeclare": 0,
|
||||
"no-return-await": 0,
|
||||
"standard/no-callback-literal": 0,
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"jsx-quotes": ["error", "prefer-double"],
|
||||
"no-use-before-define": 0,
|
||||
"semi": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"omitLastInOneLineBlock": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"linters": {
|
||||
"src/**/*.{js,json}": ["prettier --write", "git add"],
|
||||
"src/**/*.js": ["eslint --fix", "git add"]
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 120,
|
||||
"singleQuote": true
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
react-native bundle --platform android --dev false --entry-file src/index.js --bundle-output ../src/main/assets/index.android.bundle --assets-dest ../src/main/res/
|
|
@ -1,3 +0,0 @@
|
|||
import LBRYApp from './src/index';
|
||||
|
||||
export default LBRYApp;
|
9781
app/package-lock.json
generated
9781
app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,65 +0,0 @@
|
|||
{
|
||||
"name": "LBRYApp",
|
||||
"version": "0.0.1",
|
||||
"private": "true",
|
||||
"scripts": {
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start",
|
||||
"format": "prettier 'src/**/*.{js,json}' --write",
|
||||
"precommit": "lint-staged"
|
||||
},
|
||||
"dependencies": {
|
||||
"base-64": "^0.1.0",
|
||||
"@expo/vector-icons": "^8.1.0",
|
||||
"gfycat-style-urls": "^1.0.3",
|
||||
"lbry-redux": "lbryio/lbry-redux",
|
||||
"lbryinc": "lbryio/lbryinc",
|
||||
"lodash": ">=4.17.11",
|
||||
"merge": ">=1.2.1",
|
||||
"moment": "^2.22.1",
|
||||
"react": "16.8.6",
|
||||
"react-native": "0.59.3",
|
||||
"@react-native-community/async-storage": "^1.2.2",
|
||||
"react-native-camera": "^2.11.0",
|
||||
"react-native-country-picker-modal": "^0.6.2",
|
||||
"react-native-document-picker": "^2.3.0",
|
||||
"react-native-exception-handler": "2.9.0",
|
||||
"react-native-fast-image": "^5.0.3",
|
||||
"react-native-fs": "^2.13.3",
|
||||
"react-native-gesture-handler": "^1.1.0",
|
||||
"react-native-image-zoom-viewer": "^2.2.5",
|
||||
"react-native-password-strength-meter": "^0.0.2",
|
||||
"react-native-phone-input": "lbryio/react-native-phone-input",
|
||||
"react-native-super-grid": "^3.0.4",
|
||||
"react-native-vector-icons": "^6.4.2",
|
||||
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
|
||||
"react-navigation": "^3.11.0",
|
||||
"react-navigation-redux-helpers": "^3.0.2",
|
||||
"react-redux": "^5.0.3",
|
||||
"redux": "^3.6.0",
|
||||
"redux-persist": "^4.10.2",
|
||||
"redux-persist-filesystem-storage": "^1.3.2",
|
||||
"redux-persist-transform-compress": "^4.2.0",
|
||||
"redux-persist-transform-filter": "0.0.18",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rn-fetch-blob": "^0.10.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.3",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react-native": "5.0.2",
|
||||
"babel-preset-stage-2": "^6.18.0",
|
||||
"babel-plugin-module-resolver": "^3.1.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-config-standard-jsx": "^6.0.2",
|
||||
"eslint-plugin-flowtype": "^2.46.1",
|
||||
"eslint-plugin-import": "^2.17.2",
|
||||
"eslint-plugin-node": "^8.0.1",
|
||||
"eslint-plugin-promise": "^4.1.1",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"flow-babel-webpack-plugin": "^1.1.1",
|
||||
"lint-staged": "^7.0.4",
|
||||
"prettier": "^1.11.1"
|
||||
}
|
||||
}
|
Binary file not shown.
Before ![]() (image error) Size: 29 KiB |
Binary file not shown.
Before ![]() (image error) Size: 354 KiB |
Binary file not shown.
Before ![]() (image error) Size: 13 KiB |
|
@ -1,428 +0,0 @@
|
|||
import React from 'react';
|
||||
import AboutPage from 'page/about';
|
||||
import DiscoverPage from 'page/discover';
|
||||
import DownloadsPage from 'page/downloads';
|
||||
import DrawerContent from 'component/drawerContent';
|
||||
import FilePage from 'page/file';
|
||||
import FirstRunScreen from 'page/firstRun';
|
||||
import PublishPage from 'page/publish';
|
||||
import RewardsPage from 'page/rewards';
|
||||
import TrendingPage from 'page/trending';
|
||||
import SearchPage from 'page/search';
|
||||
import SettingsPage from 'page/settings';
|
||||
import SplashScreen from 'page/splash';
|
||||
import SubscriptionsPage from 'page/subscriptions';
|
||||
import TransactionHistoryPage from 'page/transactionHistory';
|
||||
import VerificationScreen from 'page/verification';
|
||||
import WalletPage from 'page/wallet';
|
||||
import { createDrawerNavigator, createStackNavigator, NavigationActions } from 'react-navigation';
|
||||
import {
|
||||
createReduxContainer,
|
||||
createReactNavigationReduxMiddleware,
|
||||
createNavigationReducer,
|
||||
} from 'react-navigation-redux-helpers';
|
||||
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 {
|
||||
doGetSync,
|
||||
doUserCheckEmailVerified,
|
||||
doUserEmailVerify,
|
||||
doUserEmailVerifyFailure,
|
||||
selectEmailToVerify,
|
||||
selectEmailVerifyIsPending,
|
||||
selectEmailVerifyErrorMessage,
|
||||
selectUser,
|
||||
} from 'lbryinc';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { decode as atob } from 'base-64';
|
||||
import { dispatchNavigateBack, dispatchNavigateToUri } from 'utils/helper';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import Colors from 'styles/colors';
|
||||
import Constants from 'constants';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import NavigationButton from 'component/navigationButton';
|
||||
import discoverStyle from 'styles/discover';
|
||||
import searchStyle from 'styles/search';
|
||||
import SearchRightHeaderIcon from 'component/searchRightHeaderIcon';
|
||||
|
||||
const menuNavigationButton = navigation => (
|
||||
<NavigationButton
|
||||
name="bars"
|
||||
size={24}
|
||||
style={discoverStyle.drawerMenuButton}
|
||||
iconStyle={discoverStyle.drawerHamburger}
|
||||
onPress={() => navigation.openDrawer()}
|
||||
/>
|
||||
);
|
||||
|
||||
const discoverStack = createStackNavigator(
|
||||
{
|
||||
Discover: {
|
||||
screen: DiscoverPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: 'Explore',
|
||||
header: null,
|
||||
}),
|
||||
},
|
||||
File: {
|
||||
screen: FilePage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
header: null,
|
||||
}),
|
||||
},
|
||||
Search: {
|
||||
screen: SearchPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
header: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
headerMode: 'screen',
|
||||
transitionConfig: () => ({ screenInterpolator: () => null }),
|
||||
}
|
||||
);
|
||||
|
||||
discoverStack.navigationOptions = ({ navigation }) => {
|
||||
let drawerLockMode = 'unlocked';
|
||||
/*if (navigation.state.index > 0) {
|
||||
drawerLockMode = 'locked-closed';
|
||||
}*/
|
||||
|
||||
return {
|
||||
drawerLockMode,
|
||||
};
|
||||
};
|
||||
|
||||
const walletStack = createStackNavigator(
|
||||
{
|
||||
Wallet: {
|
||||
screen: WalletPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: 'Wallet',
|
||||
header: null,
|
||||
}),
|
||||
},
|
||||
TransactionHistory: {
|
||||
screen: TransactionHistoryPage,
|
||||
navigationOptions: {
|
||||
title: 'Transaction History',
|
||||
header: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headerMode: 'screen',
|
||||
transitionConfig: () => ({ screenInterpolator: () => null }),
|
||||
}
|
||||
);
|
||||
|
||||
const drawer = createDrawerNavigator(
|
||||
{
|
||||
DiscoverStack: {
|
||||
screen: discoverStack,
|
||||
navigationOptions: {
|
||||
title: 'Explore',
|
||||
drawerIcon: ({ tintColor }) => <Icon name="home" size={20} style={{ color: tintColor }} />,
|
||||
},
|
||||
},
|
||||
TrendingStack: {
|
||||
screen: TrendingPage,
|
||||
navigationOptions: {
|
||||
title: 'Trending',
|
||||
drawerIcon: ({ tintColor }) => <Icon name="fire" size={20} style={{ color: tintColor }} />,
|
||||
},
|
||||
},
|
||||
MySubscriptionsStack: {
|
||||
screen: SubscriptionsPage,
|
||||
navigationOptions: {
|
||||
title: 'Subscriptions',
|
||||
drawerIcon: ({ tintColor }) => <Icon name="heart" solid={true} size={20} style={{ color: tintColor }} />,
|
||||
},
|
||||
},
|
||||
WalletStack: {
|
||||
screen: walletStack,
|
||||
navigationOptions: {
|
||||
title: 'Wallet',
|
||||
drawerIcon: ({ tintColor }) => <Icon name="wallet" size={20} style={{ color: tintColor }} />,
|
||||
},
|
||||
},
|
||||
Publish: {
|
||||
screen: PublishPage,
|
||||
navigationOptions: {
|
||||
drawerIcon: ({ tintColor }) => <Icon name="upload" size={20} style={{ color: tintColor }} />,
|
||||
},
|
||||
},
|
||||
Rewards: {
|
||||
screen: RewardsPage,
|
||||
navigationOptions: {
|
||||
drawerIcon: ({ tintColor }) => <Icon name="award" size={20} style={{ color: tintColor }} />,
|
||||
},
|
||||
},
|
||||
MyLBRYStack: {
|
||||
screen: DownloadsPage,
|
||||
navigationOptions: {
|
||||
title: 'Library',
|
||||
drawerIcon: ({ tintColor }) => <Icon name="download" size={20} style={{ color: tintColor }} />,
|
||||
},
|
||||
},
|
||||
Settings: {
|
||||
screen: SettingsPage,
|
||||
navigationOptions: {
|
||||
drawerLockMode: 'locked-closed',
|
||||
drawerIcon: ({ tintColor }) => <Icon name="cog" size={20} style={{ color: tintColor }} />,
|
||||
},
|
||||
},
|
||||
About: {
|
||||
screen: AboutPage,
|
||||
navigationOptions: {
|
||||
drawerLockMode: 'locked-closed',
|
||||
drawerIcon: ({ tintColor }) => <Icon name="info" size={20} style={{ color: tintColor }} />,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
drawerWidth: 300,
|
||||
headerMode: 'none',
|
||||
contentComponent: DrawerContent,
|
||||
contentOptions: {
|
||||
activeTintColor: Colors.LbryGreen,
|
||||
labelStyle: discoverStyle.menuText,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const mainStackNavigator = new createStackNavigator(
|
||||
{
|
||||
FirstRun: {
|
||||
screen: FirstRunScreen,
|
||||
navigationOptions: {
|
||||
drawerLockMode: 'locked-closed',
|
||||
},
|
||||
},
|
||||
Splash: {
|
||||
screen: SplashScreen,
|
||||
navigationOptions: {
|
||||
drawerLockMode: 'locked-closed',
|
||||
},
|
||||
},
|
||||
Main: {
|
||||
screen: drawer,
|
||||
},
|
||||
Verification: {
|
||||
screen: VerificationScreen,
|
||||
navigationOptions: {
|
||||
drawerLockMode: 'locked-closed',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headerMode: 'none',
|
||||
}
|
||||
);
|
||||
|
||||
export const AppNavigator = mainStackNavigator;
|
||||
export const navigatorReducer = createNavigationReducer(AppNavigator);
|
||||
export const reactNavigationMiddleware = createReactNavigationReduxMiddleware(state => state.nav);
|
||||
|
||||
const App = createReduxContainer(mainStackNavigator);
|
||||
const appMapStateToProps = state => ({
|
||||
state: state.nav,
|
||||
});
|
||||
const ReduxAppNavigator = connect(appMapStateToProps)(App);
|
||||
|
||||
class AppWithNavigationState extends React.Component {
|
||||
static supportedDisplayTypes = ['toast'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.emailVerifyCheckInterval = null;
|
||||
this.state = {
|
||||
emailVerifyDone: false,
|
||||
verifyPending: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
AppState.addEventListener('change', this._handleAppStateChange);
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
function() {
|
||||
const { dispatch, nav, drawerStack } = this.props;
|
||||
// There should be a better way to check this
|
||||
if (nav.routes.length > 0) {
|
||||
if (nav.routes[0].routeName === 'Main') {
|
||||
const mainRoute = nav.routes[0];
|
||||
if (
|
||||
mainRoute.index > 0 ||
|
||||
mainRoute.routes[0].index > 0 /* Discover stack index */ ||
|
||||
mainRoute.routes[4].index > 0 /* Wallet stack index */ ||
|
||||
mainRoute.index >= 5 /* Settings and About screens */
|
||||
) {
|
||||
dispatchNavigateBack(dispatch, nav, drawerStack);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.emailVerifyCheckInterval = setInterval(() => this.checkEmailVerification(), 5000);
|
||||
Linking.addEventListener('url', this._handleUrl);
|
||||
}
|
||||
|
||||
checkEmailVerification = () => {
|
||||
const { dispatch } = this.props;
|
||||
AsyncStorage.getItem(Constants.KEY_EMAIL_VERIFY_PENDING).then(pending => {
|
||||
this.setState({ verifyPending: 'true' === pending });
|
||||
if ('true' === pending) {
|
||||
dispatch(doUserCheckEmailVerified());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
AppState.removeEventListener('change', this._handleAppStateChange);
|
||||
BackHandler.removeEventListener('hardwareBackPress');
|
||||
Linking.removeEventListener('url', this._handleUrl);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { dispatch, user } = this.props;
|
||||
if (this.state.verifyPending && this.emailVerifyCheckInterval > 0 && user && user.has_verified_email) {
|
||||
clearInterval(this.emailVerifyCheckInterval);
|
||||
AsyncStorage.setItem(Constants.KEY_EMAIL_VERIFY_PENDING, 'false');
|
||||
this.setState({ verifyPending: false });
|
||||
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
const { dispatch } = this.props;
|
||||
const { toast, emailToVerify, emailVerifyPending, emailVerifyErrorMessage, user } = nextProps;
|
||||
|
||||
if (toast) {
|
||||
const { message } = toast;
|
||||
let currentDisplayType;
|
||||
if (!currentDisplayType && message) {
|
||||
// default to toast if no display type set and there is a message specified
|
||||
currentDisplayType = 'toast';
|
||||
}
|
||||
|
||||
if ('toast' === currentDisplayType) {
|
||||
ToastAndroid.show(message, ToastAndroid.LONG);
|
||||
}
|
||||
|
||||
dispatch(doDismissToast());
|
||||
}
|
||||
|
||||
if (user && !emailVerifyPending && !this.state.emailVerifyDone && (emailToVerify || emailVerifyErrorMessage)) {
|
||||
AsyncStorage.getItem(Constants.KEY_SHOULD_VERIFY_EMAIL).then(shouldVerify => {
|
||||
if ('true' === shouldVerify) {
|
||||
this.setState({ emailVerifyDone: true });
|
||||
const message = emailVerifyErrorMessage
|
||||
? String(emailVerifyErrorMessage)
|
||||
: 'Your email address was successfully verified.';
|
||||
if (!emailVerifyErrorMessage) {
|
||||
AsyncStorage.removeItem(Constants.KEY_FIRST_RUN_EMAIL);
|
||||
}
|
||||
|
||||
AsyncStorage.removeItem(Constants.KEY_SHOULD_VERIFY_EMAIL);
|
||||
dispatch(doToast({ message }));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_handleAppStateChange = nextAppState => {
|
||||
const { backgroundPlayEnabled, dispatch } = this.props;
|
||||
// Check if the app was suspended
|
||||
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
|
||||
AsyncStorage.getItem('firstLaunchTime').then(start => {
|
||||
if (start !== null && !isNaN(parseInt(start, 10))) {
|
||||
// App suspended during first launch?
|
||||
// If so, this needs to be included as a property when tracking
|
||||
AsyncStorage.setItem('firstLaunchSuspended', 'true');
|
||||
}
|
||||
|
||||
// Background media
|
||||
if (backgroundPlayEnabled && NativeModules.BackgroundMedia && window.currentMediaInfo) {
|
||||
const { title, channel, uri } = window.currentMediaInfo;
|
||||
NativeModules.BackgroundMedia.showPlaybackNotification(title, channel, uri, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (AppState.currentState && AppState.currentState.match(/active/)) {
|
||||
if (backgroundPlayEnabled || NativeModules.BackgroundMedia) {
|
||||
NativeModules.BackgroundMedia.hidePlaybackNotification();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_handleUrl = evt => {
|
||||
const { dispatch, nav } = this.props;
|
||||
if (evt.url) {
|
||||
if (evt.url.startsWith('lbry://?verify=')) {
|
||||
this.setState({ emailVerifyDone: false });
|
||||
let verification = {};
|
||||
try {
|
||||
verification = JSON.parse(atob(evt.url.substring(15)));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
if (verification.token && verification.recaptcha) {
|
||||
AsyncStorage.setItem(Constants.KEY_SHOULD_VERIFY_EMAIL, 'true');
|
||||
try {
|
||||
dispatch(doUserEmailVerify(verification.token, verification.recaptcha));
|
||||
} catch (error) {
|
||||
const message = 'Invalid Verification Token';
|
||||
dispatch(doUserEmailVerifyFailure(message));
|
||||
dispatch(doToast({ message }));
|
||||
}
|
||||
} else {
|
||||
dispatch(
|
||||
doToast({
|
||||
message: 'Invalid Verification URI',
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
dispatchNavigateToUri(dispatch, nav, evt.url);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return <ReduxAppNavigator />;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
|
||||
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
|
||||
nav: state.nav,
|
||||
toast: selectToast(state),
|
||||
drawerStack: selectDrawerStack(state),
|
||||
emailToVerify: selectEmailToVerify(state),
|
||||
emailVerifyPending: selectEmailVerifyIsPending(state),
|
||||
emailVerifyErrorMessage: selectEmailVerifyErrorMessage(state),
|
||||
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state),
|
||||
user: selectUser(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(AppWithNavigationState);
|
|
@ -1,10 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import Address from './view';
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
{
|
||||
doToast,
|
||||
}
|
||||
)(Address);
|
|
@ -1,34 +0,0 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Clipboard, Text, View } from 'react-native';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
address: string,
|
||||
doToast: ({ message: string }) => void,
|
||||
};
|
||||
|
||||
export default class Address extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { address, doToast, style } = this.props;
|
||||
|
||||
return (
|
||||
<View style={[walletStyle.row, style]}>
|
||||
<Text selectable={true} numberOfLines={1} style={walletStyle.address}>
|
||||
{address || ''}
|
||||
</Text>
|
||||
<Button
|
||||
icon={'clipboard'}
|
||||
style={walletStyle.button}
|
||||
onPress={() => {
|
||||
Clipboard.setString(address);
|
||||
doToast({
|
||||
message: 'Address copied',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Button from './view';
|
||||
|
||||
export default connect()(Button);
|
|
@ -1,57 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Text, TouchableOpacity } from 'react-native';
|
||||
import buttonStyle from 'styles/button';
|
||||
import Colors from 'styles/colors';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
|
||||
export default class Button extends React.PureComponent {
|
||||
render() {
|
||||
const { disabled, style, text, icon, iconColor, solid, theme, onPress, onLayout } = this.props;
|
||||
|
||||
let styles = [buttonStyle.button, buttonStyle.row];
|
||||
if (style) {
|
||||
if (style.length) {
|
||||
styles = styles.concat(style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
styles.push(buttonStyle.disabled);
|
||||
}
|
||||
|
||||
const textStyles = [buttonStyle.text];
|
||||
if (icon && icon.trim().length > 0) {
|
||||
textStyles.push(buttonStyle.textWithIcon);
|
||||
}
|
||||
|
||||
if (theme === 'light') {
|
||||
textStyles.push(buttonStyle.textDark);
|
||||
} else {
|
||||
// Dark background, default
|
||||
textStyles.push(buttonStyle.textLight);
|
||||
}
|
||||
|
||||
let renderIcon = (
|
||||
<Icon name={icon} size={18} color={iconColor ? iconColor : 'light' === theme ? Colors.DarkGrey : Colors.White} />
|
||||
);
|
||||
if (solid) {
|
||||
renderIcon = (
|
||||
<Icon
|
||||
name={icon}
|
||||
size={18}
|
||||
color={iconColor ? iconColor : 'light' === theme ? Colors.DarkGrey : Colors.White}
|
||||
solid
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity disabled={disabled} style={styles} onPress={onPress} onLayout={onLayout}>
|
||||
{icon && renderIcon}
|
||||
{text && text.trim().length > 0 && <Text style={textStyles}>{text}</Text>}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import CategoryList from './view';
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
null
|
||||
)(CategoryList);
|
|
@ -1,39 +0,0 @@
|
|||
import React from 'react';
|
||||
import NavigationActions from 'react-navigation';
|
||||
import { FlatList, Text, View } from 'react-native';
|
||||
import { normalizeURI } from 'lbry-redux';
|
||||
import FileItem from '/component/fileItem';
|
||||
import discoverStyle from 'styles/discover';
|
||||
|
||||
class CategoryList extends React.PureComponent {
|
||||
render() {
|
||||
const { category, categoryMap, navigation } = this.props;
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
style={discoverStyle.horizontalScrollContainer}
|
||||
contentContainerStyle={discoverStyle.horizontalScrollPadding}
|
||||
initialNumToRender={3}
|
||||
maxToRenderPerBatch={3}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={({ item }) => (
|
||||
<FileItem
|
||||
style={discoverStyle.fileItem}
|
||||
mediaStyle={discoverStyle.fileItemMedia}
|
||||
key={item}
|
||||
uri={normalizeURI(item)}
|
||||
navigation={navigation}
|
||||
showDetails={true}
|
||||
compactView={false}
|
||||
/>
|
||||
)}
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
data={categoryMap[category]}
|
||||
keyExtractor={(item, index) => item}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CategoryList;
|
|
@ -1,27 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
selectBalance,
|
||||
selectMyChannelClaims,
|
||||
selectFetchingMyChannels,
|
||||
doFetchChannelListMine,
|
||||
doCreateChannel,
|
||||
doToast,
|
||||
} from 'lbry-redux';
|
||||
import ChannelSelector from './view';
|
||||
|
||||
const select = state => ({
|
||||
channels: selectMyChannelClaims(state),
|
||||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
balance: selectBalance(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
notify: data => dispatch(doToast(data)),
|
||||
createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)),
|
||||
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(ChannelSelector);
|
|
@ -1,241 +0,0 @@
|
|||
import React from 'react';
|
||||
import { CLAIM_VALUES, isNameValid } from 'lbry-redux';
|
||||
import { ActivityIndicator, Picker, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import Button from 'component/button';
|
||||
import Colors from 'styles/colors';
|
||||
import Constants from 'constants';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import Link from 'component/link';
|
||||
import channelSelectorStyle from 'styles/channelSelector';
|
||||
|
||||
export default class ChannelSelector extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
currentSelectedValue: Constants.ITEM_ANONYMOUS,
|
||||
newChannelName: '',
|
||||
newChannelBid: 0.1,
|
||||
addingChannel: false,
|
||||
creatingChannel: false,
|
||||
newChannelNameError: '',
|
||||
newChannelBidError: '',
|
||||
createChannelError: undefined,
|
||||
showCreateChannel: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { channels, fetchChannelListMine, fetchingChannels } = this.props;
|
||||
if (!channels.length && !fetchingChannels) {
|
||||
fetchChannelListMine();
|
||||
}
|
||||
}
|
||||
|
||||
handleCreateCancel = () => {
|
||||
this.setState({ showCreateChannel: false, newChannelName: '', newChannelBid: 0.1 });
|
||||
};
|
||||
|
||||
handlePickerValueChange = (itemValue, itemIndex) => {
|
||||
if (Constants.ITEM_CREATE_A_CHANNEL === itemValue) {
|
||||
this.setState({ showCreateChannel: true });
|
||||
} else {
|
||||
this.handleCreateCancel();
|
||||
this.handleChannelChange(Constants.ITEM_ANONYMOUS === itemValue ? CLAIM_VALUES.CHANNEL_ANONYMOUS : itemValue);
|
||||
}
|
||||
this.setState({ currentSelectedValue: itemValue });
|
||||
};
|
||||
|
||||
handleChannelChange = value => {
|
||||
const { onChannelChange } = this.props;
|
||||
const { newChannelBid } = this.state;
|
||||
const channel = value;
|
||||
|
||||
if (channel === CLAIM_VALUES.CHANNEL_NEW) {
|
||||
this.setState({ addingChannel: true });
|
||||
if (onChannelChange) {
|
||||
onChannelChange(channel);
|
||||
}
|
||||
this.handleNewChannelBidChange(newChannelBid);
|
||||
} else {
|
||||
this.setState({ addingChannel: false });
|
||||
if (onChannelChange) {
|
||||
onChannelChange(channel);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleNewChannelNameChange = value => {
|
||||
const { notify } = this.props;
|
||||
|
||||
let newChannelName = value;
|
||||
|
||||
if (newChannelName.startsWith('@')) {
|
||||
newChannelName = newChannelName.slice(1);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
newChannelName,
|
||||
});
|
||||
};
|
||||
|
||||
handleNewChannelBidChange = newChannelBid => {
|
||||
const { balance, notify } = this.props;
|
||||
let newChannelBidError;
|
||||
if (newChannelBid <= 0) {
|
||||
newChannelBidError = __('Please enter a deposit above 0');
|
||||
} else if (newChannelBid === balance) {
|
||||
newChannelBidError = __('Please decrease your deposit to account for transaction fees');
|
||||
} else if (newChannelBid > balance) {
|
||||
newChannelBidError = __('Deposit cannot be higher than your balance');
|
||||
}
|
||||
|
||||
notify({ message: newChannelBidError });
|
||||
|
||||
this.setState({
|
||||
newChannelBid,
|
||||
newChannelBidError,
|
||||
});
|
||||
};
|
||||
|
||||
handleCreateChannelClick = () => {
|
||||
const { balance, createChannel, onChannelChange, notify } = this.props;
|
||||
const { newChannelBid, newChannelName } = this.state;
|
||||
|
||||
if (newChannelName.trim().length === 0 || !isNameValid(newChannelName.substr(1), false)) {
|
||||
notify({ message: 'Your channel name contains invalid characters.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.channelExists(newChannelName)) {
|
||||
notify({ message: 'You have already created a channel with the same name.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (newChannelBid > balance) {
|
||||
notify({ message: 'Deposit cannot be higher than your balance' });
|
||||
return;
|
||||
}
|
||||
|
||||
const channelName = `@${newChannelName}`;
|
||||
|
||||
this.setState({
|
||||
creatingChannel: true,
|
||||
createChannelError: undefined,
|
||||
});
|
||||
|
||||
const success = () => {
|
||||
this.setState({
|
||||
creatingChannel: false,
|
||||
addingChannel: false,
|
||||
currentSelectedValue: channelName,
|
||||
showCreateChannel: false,
|
||||
});
|
||||
|
||||
if (onChannelChange) {
|
||||
onChannelChange(channelName);
|
||||
}
|
||||
};
|
||||
|
||||
const failure = () => {
|
||||
notify({ message: 'Unable to create channel due to an internal error.' });
|
||||
this.setState({
|
||||
creatingChannel: false,
|
||||
});
|
||||
};
|
||||
|
||||
createChannel(channelName, newChannelBid).then(success, failure);
|
||||
};
|
||||
|
||||
channelExists = name => {
|
||||
const { channels = [] } = this.props;
|
||||
for (let channel of channels) {
|
||||
if (
|
||||
name.toLowerCase() === channel.name.toLowerCase() ||
|
||||
`@${name}`.toLowerCase() === channel.name.toLowerCase()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
render() {
|
||||
const channel = this.state.addingChannel ? 'new' : this.props.channel;
|
||||
const { fetchingChannels, channels = [] } = this.props;
|
||||
const pickerItems = [{ name: Constants.ITEM_ANONYMOUS }, { name: Constants.ITEM_CREATE_A_CHANNEL }].concat(
|
||||
channels
|
||||
);
|
||||
const {
|
||||
newChannelName,
|
||||
newChannelNameError,
|
||||
newChannelBid,
|
||||
newChannelBidError,
|
||||
creatingChannel,
|
||||
createChannelError,
|
||||
addingChannel,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<View style={channelSelectorStyle.container}>
|
||||
<Picker
|
||||
selectedValue={this.state.currentSelectedValue}
|
||||
style={channelSelectorStyle.channelPicker}
|
||||
itemStyle={channelSelectorStyle.channelPickerItem}
|
||||
onValueChange={this.handlePickerValueChange}
|
||||
>
|
||||
{pickerItems.map(item => (
|
||||
<Picker.Item label={item.name} value={item.name} key={item.name} />
|
||||
))}
|
||||
</Picker>
|
||||
|
||||
{this.state.showCreateChannel && (
|
||||
<View style={channelSelectorStyle.createChannelContainer}>
|
||||
<View style={channelSelectorStyle.channelInputContainer}>
|
||||
<Text style={channelSelectorStyle.channelAt}>@</Text>
|
||||
|
||||
<TextInput
|
||||
style={channelSelectorStyle.channelNameInput}
|
||||
value={this.state.newChannelName}
|
||||
onChangeText={this.handleNewChannelNameChange}
|
||||
placeholder={'Channel name'}
|
||||
underlineColorAndroid={Colors.NextLbryGreen}
|
||||
/>
|
||||
</View>
|
||||
<View style={channelSelectorStyle.bidRow}>
|
||||
<Text style={channelSelectorStyle.label}>Deposit</Text>
|
||||
<TextInput
|
||||
style={channelSelectorStyle.bidAmountInput}
|
||||
value={String(this.state.newChannelBid)}
|
||||
onChangeText={this.handleNewChannelBidChange}
|
||||
placeholder={'0.00'}
|
||||
keyboardType={'number-pad'}
|
||||
underlineColorAndroid={Colors.NextLbryGreen}
|
||||
/>
|
||||
<Text style={channelSelectorStyle.currency}>LBC</Text>
|
||||
</View>
|
||||
<Text style={channelSelectorStyle.helpText}>
|
||||
This LBC remains yours. It is a deposit to reserve the name and can be undone at any time.
|
||||
</Text>
|
||||
|
||||
<View style={channelSelectorStyle.buttonContainer}>
|
||||
{creatingChannel && <ActivityIndicator size={'small'} color={Colors.LbryGreen} />}
|
||||
{!creatingChannel && (
|
||||
<View style={channelSelectorStyle.buttons}>
|
||||
<Link style={channelSelectorStyle.cancelLink} text="Cancel" onPress={this.handleCreateCancel} />
|
||||
<Button
|
||||
style={channelSelectorStyle.createButton}
|
||||
disabled={!(this.state.newChannelName.trim().length > 0 && this.state.newChannelBid > 0)}
|
||||
text="Create"
|
||||
onPress={this.handleCreateChannelClick}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import {
|
||||
doClaimRewardType,
|
||||
doClaimRewardClearError,
|
||||
makeSelectClaimRewardError,
|
||||
makeSelectIsRewardClaimPending,
|
||||
rewards as REWARD_TYPES,
|
||||
} from 'lbryinc';
|
||||
import CustomRewardCard from './view';
|
||||
|
||||
const select = state => ({
|
||||
rewardIsPending: makeSelectIsRewardClaimPending()(state, {
|
||||
reward_type: REWARD_TYPES.TYPE_REWARD_CODE,
|
||||
}),
|
||||
error: makeSelectClaimRewardError()(state, { reward_type: REWARD_TYPES.TYPE_REWARD_CODE }),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)),
|
||||
clearError: reward => dispatch(doClaimRewardClearError(reward)),
|
||||
notify: data => dispatch(doToast(data)),
|
||||
submitRewardCode: code => dispatch(doClaimRewardType(REWARD_TYPES.TYPE_REWARD_CODE, { params: { code } })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(CustomRewardCard);
|
|
@ -1,96 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, Keyboard, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import Colors from '../../styles/colors';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import Button from '../button';
|
||||
import Link from '../link';
|
||||
import rewardStyle from '../../styles/reward';
|
||||
|
||||
class CustomRewardCard extends React.PureComponent<Props> {
|
||||
state = {
|
||||
claimStarted: false,
|
||||
rewardCode: '',
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { error, rewardIsPending } = nextProps;
|
||||
const { clearError, notify } = this.props;
|
||||
if (this.state.claimStarted && !rewardIsPending) {
|
||||
if (error && error.trim().length > 0) {
|
||||
notify({ message: error });
|
||||
} else {
|
||||
notify({ message: 'Reward successfully claimed!' });
|
||||
this.setState({ rewardCode: '' });
|
||||
}
|
||||
this.setState({ claimStarted: false });
|
||||
}
|
||||
}
|
||||
|
||||
onClaimPress = () => {
|
||||
const { canClaim, notify, showVerification, submitRewardCode } = this.props;
|
||||
const { rewardCode } = this.state;
|
||||
|
||||
Keyboard.dismiss();
|
||||
|
||||
if (!canClaim) {
|
||||
if (showVerification) {
|
||||
showVerification();
|
||||
}
|
||||
notify({ message: 'Unfortunately, you are not eligible to claim this reward at this time.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rewardCode || rewardCode.trim().length === 0) {
|
||||
notify({ message: 'Please enter a reward code to claim.' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ claimStarted: true }, () => {
|
||||
submitRewardCode(rewardCode);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { canClaim, rewardIsPending } = this.props;
|
||||
|
||||
return (
|
||||
<View style={[rewardStyle.rewardCard, rewardStyle.row]}>
|
||||
<View style={rewardStyle.leftCol}>
|
||||
{rewardIsPending && <ActivityIndicator size="small" color={Colors.LbryGreen} />}
|
||||
</View>
|
||||
<View style={rewardStyle.midCol}>
|
||||
<Text style={rewardStyle.rewardTitle}>Custom Code</Text>
|
||||
<Text style={rewardStyle.rewardDescription}>
|
||||
Are you a supermodel or rockstar that received a custom reward code? Claim it here.
|
||||
</Text>
|
||||
|
||||
<View>
|
||||
<TextInput
|
||||
style={rewardStyle.customCodeInput}
|
||||
placeholder={'0123abc'}
|
||||
onChangeText={text => this.setState({ rewardCode: text })}
|
||||
value={this.state.rewardCode}
|
||||
/>
|
||||
<Button
|
||||
style={rewardStyle.redeemButton}
|
||||
text={'Redeem'}
|
||||
disabled={!this.state.rewardCode || this.state.rewardCode.trim().length === 0 || rewardIsPending}
|
||||
onPress={() => {
|
||||
if (!rewardIsPending) {
|
||||
this.onClaimPress();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={rewardStyle.rightCol}>
|
||||
<Text style={rewardStyle.rewardAmount}>?</Text>
|
||||
<Text style={rewardStyle.rewardCurrency}>LBC</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomRewardCard;
|
|
@ -1,16 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectDateForUri } from 'lbry-redux';
|
||||
import DateTime from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
date: props.date || makeSelectDateForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchBlock: height => dispatch(doFetchBlock(height)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(DateTime);
|
|
@ -1,55 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { View, Text } from 'react-native';
|
||||
|
||||
type Props = {
|
||||
date?: number,
|
||||
timeAgo?: boolean,
|
||||
formatOptions: {},
|
||||
show?: string,
|
||||
};
|
||||
|
||||
class DateTime extends React.PureComponent<Props> {
|
||||
static SHOW_DATE = 'date';
|
||||
static SHOW_TIME = 'time';
|
||||
static SHOW_BOTH = 'both';
|
||||
|
||||
static defaultProps = {
|
||||
formatOptions: {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
};
|
||||
|
||||
render() {
|
||||
const { date, formatOptions, timeAgo, style, textStyle } = this.props;
|
||||
const show = this.props.show || DateTime.SHOW_BOTH;
|
||||
const locale = 'en-US'; // default to en-US until we get a working i18n module for RN
|
||||
|
||||
if (timeAgo) {
|
||||
return date ? (
|
||||
<View style={style}>
|
||||
<Text style={textStyle}>{moment(date).from(moment())}</Text>
|
||||
</View>
|
||||
) : null;
|
||||
}
|
||||
|
||||
// TODO: formatOptions not working as expected in RN
|
||||
// date.toLocaleDateString([locale, 'en-US'], formatOptions)}
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<Text style={textStyle}>
|
||||
{date && (show === DateTime.SHOW_BOTH || show === DateTime.SHOW_DATE) && moment(date).format('MMMM D, YYYY')}
|
||||
{show === DateTime.SHOW_BOTH && ' '}
|
||||
{date && (show === DateTime.SHOW_BOTH || show === DateTime.SHOW_TIME) && date.toLocaleTimeString()}
|
||||
{!date && '...'}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DateTime;
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import DrawerContent from './view';
|
||||
|
||||
export default connect()(DrawerContent);
|
|
@ -1,38 +0,0 @@
|
|||
import React from 'react';
|
||||
import { DrawerItems, SafeAreaView } from 'react-navigation';
|
||||
import { ScrollView } from 'react-native';
|
||||
import Constants from 'constants';
|
||||
import discoverStyle from 'styles/discover';
|
||||
|
||||
class DrawerContent extends React.PureComponent {
|
||||
render() {
|
||||
const props = this.props;
|
||||
const { navigation, onItemPress } = props;
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<SafeAreaView style={discoverStyle.drawerContentContainer} forceInset={{ top: 'always', horizontal: 'never' }}>
|
||||
<DrawerItems
|
||||
{...props}
|
||||
onItemPress={route => {
|
||||
const { routeName } = route.route;
|
||||
if (Constants.FULL_ROUTE_NAME_DISCOVER === routeName) {
|
||||
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_DISCOVER });
|
||||
return;
|
||||
}
|
||||
|
||||
if (Constants.FULL_ROUTE_NAME_WALLET === routeName) {
|
||||
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_WALLET });
|
||||
return;
|
||||
}
|
||||
|
||||
onItemPress(route);
|
||||
}}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DrawerContent;
|
|
@ -1,28 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doPurchaseUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectDownloadingForUri,
|
||||
makeSelectLoadingForUri,
|
||||
} from 'lbry-redux';
|
||||
import { doFetchCostInfoForUri, makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { doStartDownload } from 'redux/actions/file';
|
||||
import FileDownloadButton from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
downloading: makeSelectDownloadingForUri(props.uri)(state),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
loading: makeSelectLoadingForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
purchaseUri: (uri, costInfo, saveFile) => dispatch(doPurchaseUri(uri, costInfo, saveFile)),
|
||||
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileDownloadButton);
|
|
@ -1,110 +0,0 @@
|
|||
import React from 'react';
|
||||
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
|
||||
import Button from '../button';
|
||||
import fileDownloadButtonStyle from 'styles/fileDownloadButton';
|
||||
|
||||
class FileDownloadButton extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const { costInfo, fetchCostInfo, uri } = this.props;
|
||||
if (costInfo === undefined) {
|
||||
fetchCostInfo(uri);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
//this.checkAvailability(nextProps.uri);
|
||||
//this.restartDownload(nextProps);
|
||||
}
|
||||
|
||||
restartDownload(props) {
|
||||
const { downloading, fileInfo, uri, restartDownload } = props;
|
||||
|
||||
if (
|
||||
!downloading &&
|
||||
fileInfo &&
|
||||
!fileInfo.completed &&
|
||||
fileInfo.written_bytes !== false &&
|
||||
fileInfo.written_bytes < fileInfo.total_bytes
|
||||
) {
|
||||
restartDownload(uri, fileInfo.outpoint);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
fileInfo,
|
||||
downloading,
|
||||
uri,
|
||||
purchaseUri,
|
||||
costInfo,
|
||||
isPlayable,
|
||||
isViewable,
|
||||
onPlay,
|
||||
onView,
|
||||
loading,
|
||||
doPause,
|
||||
style,
|
||||
openFile,
|
||||
onButtonLayout,
|
||||
} = this.props;
|
||||
|
||||
if ((fileInfo && !fileInfo.stopped) || loading || downloading) {
|
||||
const progress = fileInfo && fileInfo.written_bytes ? (fileInfo.written_bytes / fileInfo.total_bytes) * 100 : 0,
|
||||
label = fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...';
|
||||
|
||||
return (
|
||||
<View style={[style, fileDownloadButtonStyle.container]}>
|
||||
<View
|
||||
style={{ width: `${progress}%`, backgroundColor: '#ff0000', position: 'absolute', left: 0, top: 0 }}
|
||||
></View>
|
||||
<Text style={fileDownloadButtonStyle.text}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (!fileInfo && !downloading) {
|
||||
if (!costInfo) {
|
||||
return (
|
||||
<View style={[style, fileDownloadButtonStyle.container]}>
|
||||
<Text style={fileDownloadButtonStyle.text}>Fetching cost info...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
icon={isPlayable ? 'play' : null}
|
||||
text={isPlayable ? 'Play' : isViewable ? 'View' : 'Download'}
|
||||
onLayout={onButtonLayout}
|
||||
style={[style, fileDownloadButtonStyle.container]}
|
||||
onPress={() => {
|
||||
if (NativeModules.Firebase) {
|
||||
NativeModules.Firebase.track('purchase_uri', { uri: uri });
|
||||
}
|
||||
purchaseUri(uri, costInfo, !isPlayable);
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.checkDownloads();
|
||||
}
|
||||
if (isPlayable && onPlay) {
|
||||
this.props.onPlay();
|
||||
}
|
||||
if (isViewable && onView) {
|
||||
this.props.onView();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (fileInfo && fileInfo.download_path) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onLayout={onButtonLayout}
|
||||
style={[style, fileDownloadButtonStyle.container]}
|
||||
onPress={openFile}
|
||||
>
|
||||
<Text style={fileDownloadButtonStyle.text}>{isViewable ? 'View' : 'Open'}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default FileDownloadButton;
|
|
@ -1,35 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doResolveUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectMetadataForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectTitleForUri,
|
||||
makeSelectIsUriResolving,
|
||||
makeSelectClaimIsNsfw,
|
||||
} from 'lbry-redux';
|
||||
import { selectRewardContentClaimIds } from 'lbryinc';
|
||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||
import FileItem from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||
obscureNsfw: !selectShowNsfw(state),
|
||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileItem);
|
|
@ -1,132 +0,0 @@
|
|||
import React from 'react';
|
||||
import { normalizeURI } from 'lbry-redux';
|
||||
import { NavigationActions } from 'react-navigation';
|
||||
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
|
||||
import { navigateToUri } from 'utils/helper';
|
||||
import Colors from 'styles/colors';
|
||||
import DateTime from 'component/dateTime';
|
||||
import FileItemMedia from 'component/fileItemMedia';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import Link from 'component/link';
|
||||
import NsfwOverlay from 'component/nsfwOverlay';
|
||||
import discoverStyle from 'styles/discover';
|
||||
|
||||
class FileItem extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.resolve(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.resolve(nextProps);
|
||||
}
|
||||
|
||||
resolve(props) {
|
||||
const { isResolvingUri, resolveUri, claim, uri } = props;
|
||||
|
||||
if (!isResolvingUri && claim === undefined && uri) {
|
||||
resolveUri(uri);
|
||||
}
|
||||
}
|
||||
|
||||
navigateToFileUri = () => {
|
||||
const { navigation, uri } = this.props;
|
||||
const normalizedUri = normalizeURI(uri);
|
||||
if (NativeModules.Firebase) {
|
||||
NativeModules.Firebase.track('explore_click', { uri: normalizedUri });
|
||||
}
|
||||
navigateToUri(navigation, normalizedUri);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
claim,
|
||||
title,
|
||||
thumbnail,
|
||||
fileInfo,
|
||||
metadata,
|
||||
isResolvingUri,
|
||||
rewardedContentClaimIds,
|
||||
style,
|
||||
mediaStyle,
|
||||
navigation,
|
||||
showDetails,
|
||||
compactView,
|
||||
titleBeforeThumbnail,
|
||||
} = this.props;
|
||||
|
||||
const uri = normalizeURI(this.props.uri);
|
||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
|
||||
const signingChannel = claim ? claim.signing_channel : null;
|
||||
const channelName = signingChannel ? signingChannel.name : null;
|
||||
const channelClaimId =
|
||||
claim && claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
|
||||
const fullChannelUri = channelClaimId ? `${channelName}#${channelClaimId}` : channelName;
|
||||
const height = claim ? claim.height : null;
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<TouchableOpacity style={discoverStyle.container} onPress={this.navigateToFileUri}>
|
||||
{!compactView && titleBeforeThumbnail && (
|
||||
<Text numberOfLines={1} style={[discoverStyle.fileItemName, discoverStyle.rewardTitle]}>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
<FileItemMedia
|
||||
title={title}
|
||||
thumbnail={thumbnail}
|
||||
blurRadius={obscureNsfw ? 15 : 0}
|
||||
resizeMode="cover"
|
||||
isResolvingUri={isResolvingUri}
|
||||
style={mediaStyle}
|
||||
/>
|
||||
|
||||
{!compactView && fileInfo && fileInfo.completed && fileInfo.download_path && (
|
||||
<Icon
|
||||
style={discoverStyle.downloadedIcon}
|
||||
solid={true}
|
||||
color={Colors.NextLbryGreen}
|
||||
name={'folder'}
|
||||
size={16}
|
||||
/>
|
||||
)}
|
||||
{!compactView && (!fileInfo || !fileInfo.completed || !fileInfo.download_path) && (
|
||||
<FilePrice uri={uri} style={discoverStyle.filePriceContainer} textStyle={discoverStyle.filePriceText} />
|
||||
)}
|
||||
{!compactView && (
|
||||
<View style={isRewardContent ? discoverStyle.rewardTitleContainer : null}>
|
||||
<Text numberOfLines={1} style={[discoverStyle.fileItemName, discoverStyle.rewardTitle]}>
|
||||
{title}
|
||||
</Text>
|
||||
{isRewardContent && <Icon style={discoverStyle.rewardIcon} name="award" size={14} />}
|
||||
</View>
|
||||
)}
|
||||
{!compactView && showDetails && (
|
||||
<View style={discoverStyle.detailsRow}>
|
||||
{channelName && (
|
||||
<Link
|
||||
style={discoverStyle.channelName}
|
||||
text={channelName}
|
||||
onPress={() => {
|
||||
navigateToUri(navigation, normalizeURI(fullChannelUri));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DateTime style={discoverStyle.dateTime} textStyle={discoverStyle.dateTimeText} timeAgo uri={uri} />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{obscureNsfw && (
|
||||
<NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileItem;
|
|
@ -1,10 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import FileItemMedia from './view';
|
||||
|
||||
const select = state => ({});
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileItemMedia);
|
|
@ -1,112 +0,0 @@
|
|||
import React from 'react';
|
||||
import { ActivityIndicator, Image, Text, View } from 'react-native';
|
||||
import Colors from 'styles/colors';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import fileItemMediaStyle from 'styles/fileItemMedia';
|
||||
|
||||
class FileItemMedia extends React.PureComponent {
|
||||
static AUTO_THUMB_STYLES = [
|
||||
fileItemMediaStyle.autothumbPurple,
|
||||
fileItemMediaStyle.autothumbRed,
|
||||
fileItemMediaStyle.autothumbPink,
|
||||
fileItemMediaStyle.autothumbIndigo,
|
||||
fileItemMediaStyle.autothumbBlue,
|
||||
fileItemMediaStyle.autothumbLightBlue,
|
||||
fileItemMediaStyle.autothumbCyan,
|
||||
fileItemMediaStyle.autothumbTeal,
|
||||
fileItemMediaStyle.autothumbGreen,
|
||||
fileItemMediaStyle.autothumbYellow,
|
||||
fileItemMediaStyle.autothumbOrange,
|
||||
];
|
||||
|
||||
state: {
|
||||
imageLoadFailed: false,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
autoThumbStyle:
|
||||
FileItemMedia.AUTO_THUMB_STYLES[Math.floor(Math.random() * FileItemMedia.AUTO_THUMB_STYLES.length)],
|
||||
});
|
||||
}
|
||||
|
||||
getFastImageResizeMode(resizeMode) {
|
||||
switch (resizeMode) {
|
||||
case 'contain':
|
||||
return FastImage.resizeMode.contain;
|
||||
case 'stretch':
|
||||
return FastImage.resizeMode.stretch;
|
||||
case 'center':
|
||||
return FastImage.resizeMode.center;
|
||||
default:
|
||||
return FastImage.resizeMode.cover;
|
||||
}
|
||||
}
|
||||
|
||||
isThumbnailValid = thumbnail => {
|
||||
if (!thumbnail || typeof thumbnail !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (thumbnail.substring(0, 7) != 'http://' && thumbnail.substring(0, 8) != 'https://') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
render() {
|
||||
let style = this.props.style;
|
||||
const { blurRadius, isResolvingUri, thumbnail, title, resizeMode } = this.props;
|
||||
const atStyle = this.state.autoThumbStyle;
|
||||
if (this.isThumbnailValid(thumbnail) && !this.state.imageLoadFailed) {
|
||||
if (style == null) {
|
||||
style = fileItemMediaStyle.thumbnail;
|
||||
}
|
||||
|
||||
if (blurRadius > 0) {
|
||||
// No blur radius support in FastImage yet
|
||||
return (
|
||||
<Image
|
||||
source={{ uri: thumbnail }}
|
||||
blurRadius={blurRadius}
|
||||
resizeMode={resizeMode ? resizeMode : 'cover'}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FastImage
|
||||
source={{ uri: thumbnail }}
|
||||
onError={() => this.setState({ imageLoadFailed: true })}
|
||||
resizeMode={this.getFastImageResizeMode(resizeMode)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[style ? style : fileItemMediaStyle.autothumb, atStyle]}>
|
||||
{isResolvingUri && (
|
||||
<View style={fileItemMediaStyle.resolving}>
|
||||
<ActivityIndicator color={Colors.White} size={'large'} />
|
||||
<Text style={fileItemMediaStyle.text}>Resolving...</Text>
|
||||
</View>
|
||||
)}
|
||||
{!isResolvingUri && (
|
||||
<Text style={fileItemMediaStyle.autothumbText}>
|
||||
{title &&
|
||||
title.trim().length > 0 &&
|
||||
title
|
||||
.replace(/\s+/g, '')
|
||||
.substring(0, Math.min(title.replace(' ', '').length, 5))
|
||||
.toUpperCase()}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileItemMedia;
|
|
@ -1,14 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import FileList from './view';
|
||||
import { selectClaimsById } from 'lbry-redux';
|
||||
|
||||
const select = state => ({
|
||||
claimsById: selectClaimsById(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileList);
|
|
@ -1,191 +0,0 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { buildURI } from 'lbry-redux';
|
||||
import { FlatList } from 'react-native';
|
||||
import FileItem from 'component/fileItem';
|
||||
import fileListStyle from 'styles/fileList';
|
||||
|
||||
// In the future, all Flow types need to be specified in a common source (lbry-redux, perhaps?)
|
||||
type FileInfo = {
|
||||
name: string,
|
||||
channelName: ?string,
|
||||
pending?: boolean,
|
||||
channel_claim_id: string,
|
||||
value?: {
|
||||
publisherSignature: {
|
||||
certificateId: string,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
publisherSignature: {
|
||||
certificateId: string,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
hideFilter: boolean,
|
||||
sortByHeight?: boolean,
|
||||
claimsById: Array<{}>,
|
||||
fileInfos: Array<FileInfo>,
|
||||
checkPending?: boolean,
|
||||
};
|
||||
|
||||
type State = {
|
||||
sortBy: string,
|
||||
};
|
||||
|
||||
class FileList extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
hideFilter: false,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
sortBy: 'dateNew',
|
||||
};
|
||||
|
||||
(this: any).handleSortChanged = this.handleSortChanged.bind(this);
|
||||
|
||||
this.sortFunctions = {
|
||||
dateNew: fileInfos =>
|
||||
this.props.sortByHeight
|
||||
? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||
if (fileInfo1.pending) {
|
||||
return -1;
|
||||
}
|
||||
const height1 = this.props.claimsById[fileInfo1.claim_id]
|
||||
? this.props.claimsById[fileInfo1.claim_id].height
|
||||
: 0;
|
||||
const height2 = this.props.claimsById[fileInfo2.claim_id]
|
||||
? this.props.claimsById[fileInfo2.claim_id].height
|
||||
: 0;
|
||||
if (height1 > height2) {
|
||||
return -1;
|
||||
} else if (height1 < height2) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
: [...fileInfos].reverse(),
|
||||
dateOld: fileInfos =>
|
||||
this.props.sortByHeight
|
||||
? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||
const height1 = this.props.claimsById[fileInfo1.claim_id]
|
||||
? this.props.claimsById[fileInfo1.claim_id].height
|
||||
: 999999;
|
||||
const height2 = this.props.claimsById[fileInfo2.claim_id]
|
||||
? this.props.claimsById[fileInfo2.claim_id].height
|
||||
: 999999;
|
||||
if (height1 < height2) {
|
||||
return -1;
|
||||
} else if (height1 > height2) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
: fileInfos,
|
||||
title: fileInfos =>
|
||||
fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||
const getFileTitle = fileInfo => {
|
||||
const { value, metadata, name, claim_name: claimName } = fileInfo;
|
||||
if (metadata) {
|
||||
// downloaded claim
|
||||
return metadata.title || claimName;
|
||||
} else if (value) {
|
||||
// published claim
|
||||
const { title } = value.stream.metadata;
|
||||
return title || name;
|
||||
}
|
||||
// Invalid claim
|
||||
return '';
|
||||
};
|
||||
const title1 = getFileTitle(fileInfo1).toLowerCase();
|
||||
const title2 = getFileTitle(fileInfo2).toLowerCase();
|
||||
if (title1 < title2) {
|
||||
return -1;
|
||||
} else if (title1 > title2) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
filename: fileInfos =>
|
||||
fileInfos.slice().sort(({ file_name: fileName1 }, { file_name: fileName2 }) => {
|
||||
const fileName1Lower = fileName1.toLowerCase();
|
||||
const fileName2Lower = fileName2.toLowerCase();
|
||||
if (fileName1Lower < fileName2Lower) {
|
||||
return -1;
|
||||
} else if (fileName2Lower > fileName1Lower) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
getChannelSignature = (fileInfo: FileInfo) => {
|
||||
if (fileInfo.pending) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (fileInfo.value) {
|
||||
return fileInfo.value.publisherSignature.certificateId;
|
||||
}
|
||||
return fileInfo.channel_claim_id;
|
||||
};
|
||||
|
||||
handleSortChanged(event: SyntheticInputEvent<*>) {
|
||||
this.setState({
|
||||
sortBy: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
sortFunctions: {};
|
||||
|
||||
render() {
|
||||
const { contentContainerStyle, fileInfos, hideFilter, checkPending, navigation, onEndReached, style } = this.props;
|
||||
const { sortBy } = this.state;
|
||||
const items = [];
|
||||
|
||||
if (!fileInfos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
|
||||
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = fileInfo;
|
||||
const uriParams = {};
|
||||
|
||||
// This is unfortunate
|
||||
// https://github.com/lbryio/lbry/issues/1159
|
||||
const name = claimName || claimNameDownloaded;
|
||||
uriParams.contentName = name;
|
||||
uriParams.claimId = claimId;
|
||||
const uri = buildURI(uriParams);
|
||||
|
||||
items.push(uri);
|
||||
});
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
style={style}
|
||||
contentContainerStyle={contentContainerStyle}
|
||||
data={items}
|
||||
onEndReached={onEndReached}
|
||||
keyExtractor={(item, index) => item}
|
||||
renderItem={({ item }) => (
|
||||
<FileItem
|
||||
style={fileListStyle.fileItem}
|
||||
uri={item}
|
||||
navigation={navigation}
|
||||
showDetails={true}
|
||||
compactView={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileList;
|
|
@ -1,32 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doResolveUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectMetadataForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectIsUriResolving,
|
||||
makeSelectTitleForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
} from 'lbry-redux';
|
||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||
import FileListItem from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||
obscureNsfw: !selectShowNsfw(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileListItem);
|
|
@ -1,159 +0,0 @@
|
|||
import React from 'react';
|
||||
import { normalizeURI, parseURI } from 'lbry-redux';
|
||||
import { ActivityIndicator, Platform, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { navigateToUri, formatBytes } from 'utils/helper';
|
||||
import Colors from 'styles/colors';
|
||||
import DateTime from 'component/dateTime';
|
||||
import FileItemMedia from 'component/fileItemMedia';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import Link from 'component/link';
|
||||
import NsfwOverlay from 'component/nsfwOverlay';
|
||||
import ProgressBar from 'component/progressBar';
|
||||
import fileListStyle from 'styles/fileList';
|
||||
|
||||
class FileListItem extends React.PureComponent {
|
||||
getStorageForFileInfo = fileInfo => {
|
||||
if (!fileInfo.completed) {
|
||||
const written = formatBytes(fileInfo.written_bytes);
|
||||
const total = formatBytes(fileInfo.total_bytes);
|
||||
return `(${written} / ${total})`;
|
||||
}
|
||||
|
||||
return formatBytes(fileInfo.written_bytes);
|
||||
};
|
||||
|
||||
formatTitle = title => {
|
||||
if (!title) {
|
||||
return title;
|
||||
}
|
||||
|
||||
return title.length > 80 ? title.substring(0, 77).trim() + '...' : title;
|
||||
};
|
||||
|
||||
getDownloadProgress = fileInfo => {
|
||||
return Math.ceil((fileInfo.written_bytes / fileInfo.total_bytes) * 100);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { claim, resolveUri, uri } = this.props;
|
||||
if (!claim) {
|
||||
resolveUri(uri);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
claim,
|
||||
fileInfo,
|
||||
metadata,
|
||||
featuredResult,
|
||||
isResolvingUri,
|
||||
isDownloaded,
|
||||
style,
|
||||
onPress,
|
||||
navigation,
|
||||
thumbnail,
|
||||
title,
|
||||
} = this.props;
|
||||
|
||||
const uri = normalizeURI(this.props.uri);
|
||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
const isResolving = !fileInfo && isResolvingUri;
|
||||
|
||||
let name, channel, height, channelClaimId, fullChannelUri;
|
||||
if (claim) {
|
||||
name = claim.name;
|
||||
signingChannel = claim.signing_channel;
|
||||
channel = signingChannel ? signingChannel.name : null;
|
||||
height = claim.height;
|
||||
channelClaimId = claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
|
||||
fullChannelUri = channelClaimId ? `${channel}#${channelClaimId}` : channel;
|
||||
}
|
||||
|
||||
if (featuredResult && !isResolvingUri && !claim && !title && !name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<TouchableOpacity style={style} onPress={onPress}>
|
||||
<FileItemMedia
|
||||
style={fileListStyle.thumbnail}
|
||||
blurRadius={obscureNsfw ? 15 : 0}
|
||||
resizeMode="cover"
|
||||
title={title || name}
|
||||
thumbnail={thumbnail}
|
||||
/>
|
||||
{fileInfo && fileInfo.completed && fileInfo.download_path && (
|
||||
<Icon
|
||||
style={fileListStyle.downloadedIcon}
|
||||
solid={true}
|
||||
color={Colors.NextLbryGreen}
|
||||
name={'folder'}
|
||||
size={16}
|
||||
/>
|
||||
)}
|
||||
<View style={fileListStyle.detailsContainer}>
|
||||
{featuredResult && (
|
||||
<Text style={fileListStyle.featuredUri} numberOfLines={1}>
|
||||
{uri}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!title && !name && !channel && isResolving && (
|
||||
<View>
|
||||
{!title && !name && <Text style={fileListStyle.uri}>{uri}</Text>}
|
||||
{!title && !name && (
|
||||
<View style={fileListStyle.row}>
|
||||
<ActivityIndicator size={'small'} color={featuredResult ? Colors.White : Colors.LbryGreen} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{(title || name) && (
|
||||
<Text style={featuredResult ? fileListStyle.featuredTitle : fileListStyle.title}>
|
||||
{this.formatTitle(title) || this.formatTitle(name)}
|
||||
</Text>
|
||||
)}
|
||||
{channel && (
|
||||
<Link
|
||||
style={fileListStyle.publisher}
|
||||
text={channel}
|
||||
onPress={() => {
|
||||
navigateToUri(navigation, normalizeURI(fullChannelUri));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={fileListStyle.info}>
|
||||
{fileInfo && !isNaN(fileInfo.written_bytes) && fileInfo.written_bytes > 0 && (
|
||||
<Text style={fileListStyle.infoText}>{this.getStorageForFileInfo(fileInfo)}</Text>
|
||||
)}
|
||||
<DateTime style={fileListStyle.publishInfo} textStyle={fileListStyle.infoText} timeAgo uri={uri} />
|
||||
</View>
|
||||
|
||||
{fileInfo && fileInfo.download_path && (
|
||||
<View style={fileListStyle.downloadInfo}>
|
||||
{!fileInfo.completed && (
|
||||
<ProgressBar
|
||||
borderRadius={3}
|
||||
color={Colors.NextLbryGreen}
|
||||
height={3}
|
||||
style={fileListStyle.progress}
|
||||
progress={this.getDownloadProgress(fileInfo)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{obscureNsfw && (
|
||||
<NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileListItem;
|
|
@ -1,19 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { doFetchCostInfoForUri, makeSelectCostInfoForUri, makeSelectFetchingCostInfoForUri } from 'lbryinc';
|
||||
import FilePrice from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
fetching: makeSelectFetchingCostInfoForUri(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FilePrice);
|
|
@ -1,119 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Text, View } from 'react-native';
|
||||
import { formatCredits, formatFullPrice } from 'lbry-redux';
|
||||
|
||||
class CreditAmount extends React.PureComponent {
|
||||
static propTypes = {
|
||||
amount: PropTypes.number.isRequired,
|
||||
precision: PropTypes.number,
|
||||
isEstimate: PropTypes.bool,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
showFree: PropTypes.bool,
|
||||
showFullPrice: PropTypes.bool,
|
||||
showPlus: PropTypes.bool,
|
||||
look: PropTypes.oneOf(['indicator', 'plain', 'fee']),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
precision: 2,
|
||||
label: true,
|
||||
showFree: false,
|
||||
look: 'indicator',
|
||||
showFullPrice: false,
|
||||
showPlus: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const minimumRenderableAmount = Math.pow(10, -1 * this.props.precision);
|
||||
const { amount, precision, showFullPrice, style } = this.props;
|
||||
|
||||
let formattedAmount;
|
||||
const fullPrice = formatFullPrice(amount, 2);
|
||||
|
||||
if (showFullPrice) {
|
||||
formattedAmount = fullPrice;
|
||||
} else {
|
||||
formattedAmount =
|
||||
amount > 0 && amount < minimumRenderableAmount
|
||||
? `<${minimumRenderableAmount}`
|
||||
: formatCredits(amount, precision);
|
||||
}
|
||||
|
||||
let amountText;
|
||||
if (this.props.showFree && parseFloat(this.props.amount) === 0) {
|
||||
amountText = 'FREE';
|
||||
} else {
|
||||
if (this.props.label) {
|
||||
const label =
|
||||
typeof this.props.label === 'string' ? this.props.label : parseFloat(amount) == 1 ? 'credit' : 'credits';
|
||||
|
||||
amountText = `${formattedAmount} ${label}`;
|
||||
} else {
|
||||
amountText = formattedAmount;
|
||||
}
|
||||
if (this.props.showPlus && amount > 0) {
|
||||
amountText = `+${amountText}`;
|
||||
}
|
||||
}
|
||||
|
||||
/*{this.props.isEstimate ? (
|
||||
<span
|
||||
className="credit-amount__estimate"
|
||||
title={__('This is an estimate and does not include data fees')}
|
||||
>
|
||||
*
|
||||
</span>
|
||||
) : null}*/
|
||||
return <Text style={style}>{amountText}</Text>;
|
||||
}
|
||||
}
|
||||
|
||||
class FilePrice extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
this.fetchCost(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.fetchCost(nextProps);
|
||||
}
|
||||
|
||||
fetchCost(props) {
|
||||
const { costInfo, fetchCostInfo, uri, fetching, claim } = props;
|
||||
|
||||
if (costInfo === undefined && !fetching && claim) {
|
||||
fetchCostInfo(uri);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { costInfo, look = 'indicator', showFullPrice = false, style, textStyle } = this.props;
|
||||
|
||||
const isEstimate = costInfo ? !costInfo.includesData : null;
|
||||
|
||||
if (!costInfo) {
|
||||
return (
|
||||
<View style={style}>
|
||||
<Text style={textStyle}>???</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<CreditAmount
|
||||
style={textStyle}
|
||||
label={false}
|
||||
amount={parseFloat(costInfo.cost)}
|
||||
isEstimate={isEstimate}
|
||||
showFree
|
||||
showFullPrice={showFullPrice}
|
||||
>
|
||||
???
|
||||
</CreditAmount>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FilePrice;
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import FileRewardsDriver from './view';
|
||||
|
||||
export default connect()(FileRewardsDriver);
|
|
@ -1,19 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Text, TouchableOpacity } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import filePageStyle from 'styles/filePage';
|
||||
|
||||
class FileRewardsDriver extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={filePageStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}>
|
||||
<Icon name="award" size={16} style={filePageStyle.rewardIcon} />
|
||||
<Text style={filePageStyle.rewardDriverText}>Earn some credits to access this content.</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileRewardsDriver;
|
|
@ -1,17 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { selectBalance } from 'lbry-redux';
|
||||
import { selectUnclaimedRewardValue } from 'lbryinc';
|
||||
import Constants from 'constants';
|
||||
import FloatingWalletBalance from './view';
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
|
||||
rewardsNotInterested: makeSelectClientSetting(Constants.SETTING_REWARDS_NOT_INTERESTED)(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
null
|
||||
)(FloatingWalletBalance);
|
|
@ -1,44 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { formatCredits } from 'lbry-redux';
|
||||
import Address from 'component/address';
|
||||
import Button from 'component/button';
|
||||
import Colors from 'styles/colors';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import floatingButtonStyle from 'styles/floatingButton';
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
};
|
||||
|
||||
class FloatingWalletBalance extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { balance, navigation, rewardsNotInterested, unclaimedRewardAmount } = this.props;
|
||||
|
||||
return (
|
||||
<View style={[floatingButtonStyle.view, floatingButtonStyle.bottomRight]}>
|
||||
{!rewardsNotInterested && unclaimedRewardAmount > 0 && (
|
||||
<TouchableOpacity
|
||||
style={floatingButtonStyle.pendingContainer}
|
||||
onPress={() => navigation && navigation.navigate({ routeName: 'Rewards' })}
|
||||
>
|
||||
<Icon name="award" size={18} style={floatingButtonStyle.rewardIcon} />
|
||||
<Text style={floatingButtonStyle.text}>{unclaimedRewardAmount}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={floatingButtonStyle.container}
|
||||
onPress={() => navigation && navigation.navigate({ routeName: 'WalletStack' })}
|
||||
>
|
||||
{isNaN(balance) && <ActivityIndicator size="small" color={Colors.White} />}
|
||||
{(!isNaN(balance) || balance === 0) && (
|
||||
<Text style={floatingButtonStyle.text}>{formatCredits(parseFloat(balance), 2) + ' LBC'}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FloatingWalletBalance;
|
|
@ -1,12 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import Link from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
notify: data => dispatch(doToast(data)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
perform
|
||||
)(Link);
|
|
@ -1,67 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Linking, Text, TouchableOpacity } from 'react-native';
|
||||
|
||||
export default class Link extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tappedStyle: false,
|
||||
};
|
||||
this.addTappedStyle = this.addTappedStyle.bind(this);
|
||||
}
|
||||
|
||||
handlePress = () => {
|
||||
const { error, href, navigation, notify } = this.props;
|
||||
|
||||
if (navigation && href.startsWith('#')) {
|
||||
navigation.navigate(href.substring(1));
|
||||
} else {
|
||||
if (this.props.effectOnTap) this.addTappedStyle();
|
||||
Linking.openURL(href)
|
||||
.then(() =>
|
||||
setTimeout(() => {
|
||||
this.setState({ tappedStyle: false });
|
||||
}, 2000)
|
||||
)
|
||||
.catch(err => {
|
||||
notify({ message: error, isError: true });
|
||||
this.setState({ tappedStyle: false });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
addTappedStyle() {
|
||||
this.setState({ tappedStyle: true });
|
||||
setTimeout(() => {
|
||||
this.setState({ tappedStyle: false });
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ellipsizeMode, numberOfLines, onPress, style, text } = this.props;
|
||||
|
||||
let styles = [];
|
||||
if (style) {
|
||||
if (style.length) {
|
||||
styles = styles.concat(style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.effectOnTap && this.state.tappedStyle) {
|
||||
styles.push(this.props.effectOnTap);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={styles}
|
||||
numberOfLines={numberOfLines}
|
||||
ellipsizeMode={ellipsizeMode}
|
||||
onPress={onPress ? onPress : this.handlePress}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { SETTINGS, savePosition } from 'lbry-redux';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import { selectIsPlayerVisible } from 'redux/selectors/drawer';
|
||||
import MediaPlayer from './view';
|
||||
|
||||
const select = state => ({
|
||||
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
|
||||
isPlayerVisible: selectIsPlayerVisible(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
savePosition: (claimId, outpoint, position) => dispatch(savePosition(claimId, outpoint, position)),
|
||||
setPlayerVisible: () => dispatch(doSetPlayerVisible(true)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(MediaPlayer);
|
|
@ -1,503 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import {
|
||||
AppState,
|
||||
ActivityIndicator,
|
||||
DeviceEventEmitter,
|
||||
NativeModules,
|
||||
PanResponder,
|
||||
Text,
|
||||
View,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import Colors from 'styles/colors';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import Video from 'react-native-video';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import FileItemMedia from 'component/fileItemMedia';
|
||||
import mediaPlayerStyle from 'styles/mediaPlayer';
|
||||
|
||||
const positionSaveInterval = 10;
|
||||
|
||||
class MediaPlayer extends React.PureComponent {
|
||||
static ControlsTimeout = 3000;
|
||||
|
||||
seekResponder = null;
|
||||
|
||||
seekerWidth = 0;
|
||||
|
||||
trackingOffset = 0;
|
||||
|
||||
tracking = null;
|
||||
|
||||
video = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
buffering: false,
|
||||
backgroundPlayEnabled: false,
|
||||
autoPaused: false,
|
||||
rate: 1,
|
||||
volume: 1,
|
||||
muted: false,
|
||||
resizeMode: 'contain',
|
||||
duration: 0.0,
|
||||
currentTime: 0.0,
|
||||
paused: !props.autoPlay,
|
||||
fullscreenMode: false,
|
||||
areControlsVisible: true,
|
||||
controlsTimeout: -1,
|
||||
seekerOffset: 0,
|
||||
seekerPosition: 0,
|
||||
firstPlay: true,
|
||||
seekTimeout: -1,
|
||||
};
|
||||
}
|
||||
|
||||
formatTime(time) {
|
||||
let str = '';
|
||||
let minutes = 0,
|
||||
hours = 0,
|
||||
seconds = parseInt(time, 10);
|
||||
if (seconds > 60) {
|
||||
minutes = parseInt(seconds / 60, 10);
|
||||
seconds = seconds % 60;
|
||||
|
||||
if (minutes > 60) {
|
||||
hours = parseInt(minutes / 60, 10);
|
||||
minutes = minutes % 60;
|
||||
}
|
||||
|
||||
str = (hours > 0 ? this.pad(hours) + ':' : '') + this.pad(minutes) + ':' + this.pad(seconds);
|
||||
} else {
|
||||
str = '00:' + this.pad(seconds);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
pad(value) {
|
||||
if (value < 10) {
|
||||
return '0' + String(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
onLoad = data => {
|
||||
this.setState({
|
||||
duration: data.duration,
|
||||
});
|
||||
|
||||
const { position } = this.props;
|
||||
if (!isNaN(parseFloat(position)) && position > 0) {
|
||||
this.video.seek(position);
|
||||
this.setState({ currentTime: position }, () => this.setSeekerPosition(this.calculateSeekerPosition()));
|
||||
}
|
||||
|
||||
if (this.props.onMediaLoaded) {
|
||||
this.props.onMediaLoaded();
|
||||
}
|
||||
};
|
||||
|
||||
onProgress = data => {
|
||||
const { savePosition, claim } = this.props;
|
||||
|
||||
this.setState({ buffering: false, currentTime: data.currentTime });
|
||||
if (data.currentTime > 0 && Math.floor(data.currentTime) % positionSaveInterval === 0) {
|
||||
const { claim_id: claimId, txid, nout } = claim;
|
||||
savePosition(claimId, `${txid}:${nout}`, data.currentTime);
|
||||
}
|
||||
|
||||
if (!this.state.seeking) {
|
||||
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||
}
|
||||
|
||||
if (this.state.firstPlay) {
|
||||
if (this.props.onPlaybackStarted) {
|
||||
this.props.onPlaybackStarted();
|
||||
}
|
||||
this.setState({ firstPlay: false });
|
||||
|
||||
this.hidePlayerControls();
|
||||
}
|
||||
};
|
||||
|
||||
clearControlsTimeout = () => {
|
||||
if (this.state.controlsTimeout > -1) {
|
||||
clearTimeout(this.state.controlsTimeout);
|
||||
}
|
||||
};
|
||||
|
||||
showPlayerControls = () => {
|
||||
this.clearControlsTimeout();
|
||||
if (!this.state.areControlsVisible) {
|
||||
this.setState({ areControlsVisible: true });
|
||||
}
|
||||
this.hidePlayerControls();
|
||||
};
|
||||
|
||||
manualHidePlayerControls = () => {
|
||||
this.clearControlsTimeout();
|
||||
this.setState({ areControlsVisible: false });
|
||||
};
|
||||
|
||||
hidePlayerControls() {
|
||||
const player = this;
|
||||
let timeout = setTimeout(() => {
|
||||
player.setState({ areControlsVisible: false });
|
||||
}, MediaPlayer.ControlsTimeout);
|
||||
player.setState({ controlsTimeout: timeout });
|
||||
}
|
||||
|
||||
togglePlayerControls = () => {
|
||||
const { setPlayerVisible, isPlayerVisible } = this.props;
|
||||
if (!isPlayerVisible) {
|
||||
setPlayerVisible();
|
||||
}
|
||||
|
||||
if (this.state.areControlsVisible) {
|
||||
this.manualHidePlayerControls();
|
||||
} else {
|
||||
this.showPlayerControls();
|
||||
}
|
||||
};
|
||||
|
||||
togglePlay = () => {
|
||||
this.showPlayerControls();
|
||||
this.setState({ paused: !this.state.paused }, this.handlePausedState);
|
||||
};
|
||||
|
||||
handlePausedState = () => {
|
||||
if (!this.state.paused) {
|
||||
// onProgress will automatically clear this, so it's fine
|
||||
this.setState({ buffering: true });
|
||||
}
|
||||
};
|
||||
|
||||
toggleFullscreenMode = () => {
|
||||
this.showPlayerControls();
|
||||
const { onFullscreenToggled } = this.props;
|
||||
this.setState({ fullscreenMode: !this.state.fullscreenMode }, () => {
|
||||
if (onFullscreenToggled) {
|
||||
onFullscreenToggled(this.state.fullscreenMode);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onEnd = () => {
|
||||
this.setState({ paused: true });
|
||||
if (this.props.onPlaybackFinished) {
|
||||
this.props.onPlaybackFinished();
|
||||
}
|
||||
this.video.seek(0);
|
||||
};
|
||||
|
||||
setSeekerPosition(position = 0) {
|
||||
position = this.checkSeekerPosition(position);
|
||||
this.setState({ seekerPosition: position });
|
||||
if (!this.state.seeking) {
|
||||
this.setState({ seekerOffset: position });
|
||||
}
|
||||
}
|
||||
|
||||
checkSeekerPosition(val = 0) {
|
||||
if (val < 0) {
|
||||
val = 0;
|
||||
} else if (val >= this.seekerWidth) {
|
||||
return this.seekerWidth;
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
seekTo(time = 0) {
|
||||
if (time > this.state.duration) {
|
||||
return;
|
||||
}
|
||||
this.video.seek(time);
|
||||
this.setState({ currentTime: time });
|
||||
}
|
||||
|
||||
initSeeker() {
|
||||
this.seekResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: (evt, gestureState) => true,
|
||||
onMoveShouldSetPanResponder: (evt, gestureState) => true,
|
||||
|
||||
onPanResponderGrant: (evt, gestureState) => {
|
||||
this.clearControlsTimeout();
|
||||
if (this.state.seekTimeout > 0) {
|
||||
clearTimeout(this.state.seekTimeout);
|
||||
}
|
||||
this.setState({ seeking: true });
|
||||
},
|
||||
|
||||
onPanResponderMove: (evt, gestureState) => {
|
||||
const position = this.state.seekerOffset + gestureState.dx;
|
||||
this.setSeekerPosition(position);
|
||||
},
|
||||
|
||||
onPanResponderRelease: (evt, gestureState) => {
|
||||
const time = this.getCurrentTimeForSeekerPosition();
|
||||
if (time >= this.state.duration) {
|
||||
this.setState({ paused: true }, this.handlePausedState);
|
||||
this.onEnd();
|
||||
} else {
|
||||
this.seekTo(time);
|
||||
this.setState({
|
||||
seekTimeout: setTimeout(() => {
|
||||
this.setState({ seeking: false });
|
||||
}, 100),
|
||||
});
|
||||
}
|
||||
this.hidePlayerControls();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getTrackingOffset() {
|
||||
return this.state.fullscreenMode ? this.trackingOffset : 0;
|
||||
}
|
||||
|
||||
getCurrentTimeForSeekerPosition() {
|
||||
return this.state.duration * (this.state.seekerPosition / this.seekerWidth);
|
||||
}
|
||||
|
||||
calculateSeekerPosition() {
|
||||
return this.seekerWidth * this.getCurrentTimePercentage();
|
||||
}
|
||||
|
||||
getCurrentTimePercentage() {
|
||||
if (this.state.currentTime > 0) {
|
||||
return parseFloat(this.state.currentTime) / parseFloat(this.state.duration);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.initSeeker();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { isPlayerVisible } = nextProps;
|
||||
if (!isPlayerVisible && !this.state.backgroundPlayEnabled) {
|
||||
// force pause if the player is not visible and background play is not enabled
|
||||
this.setState({ paused: true });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { assignPlayer, backgroundPlayEnabled } = this.props;
|
||||
if (assignPlayer) {
|
||||
assignPlayer(this);
|
||||
}
|
||||
|
||||
this.setState({ backgroundPlayEnabled: !!backgroundPlayEnabled });
|
||||
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||
AppState.addEventListener('change', this.handleAppStateChange);
|
||||
DeviceEventEmitter.addListener('onBackgroundPlayPressed', this.play);
|
||||
DeviceEventEmitter.addListener('onBackgroundPausePressed', this.pause);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AppState.removeEventListener('change', this.handleAppStateChange);
|
||||
DeviceEventEmitter.removeListener('onBackgroundPlayPressed', this.play);
|
||||
DeviceEventEmitter.removeListener('onBackgroundPausePressed', this.pause);
|
||||
this.clearControlsTimeout();
|
||||
this.setState({ paused: true, fullscreenMode: false });
|
||||
const { onFullscreenToggled } = this.props;
|
||||
if (onFullscreenToggled) {
|
||||
onFullscreenToggled(false);
|
||||
}
|
||||
}
|
||||
|
||||
handleAppStateChange = () => {
|
||||
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
|
||||
if (!this.state.backgroundPlayEnabled && !this.state.paused) {
|
||||
this.setState({ paused: true, autoPaused: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (AppState.currentState && AppState.currentState.match(/active/)) {
|
||||
if (!this.state.backgroundPlayEnabled && this.state.autoPaused) {
|
||||
this.setState({ paused: false, autoPaused: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onBuffer = () => {
|
||||
if (!this.state.paused) {
|
||||
this.setState({ buffering: true }, () => this.manualHidePlayerControls());
|
||||
}
|
||||
};
|
||||
|
||||
play = () => {
|
||||
this.setState({ paused: false }, this.updateBackgroundMediaNotification);
|
||||
};
|
||||
|
||||
pause = () => {
|
||||
this.setState({ paused: true }, this.updateBackgroundMediaNotification);
|
||||
};
|
||||
|
||||
updateBackgroundMediaNotification = () => {
|
||||
this.handlePausedState();
|
||||
const { backgroundPlayEnabled } = this.props;
|
||||
if (backgroundPlayEnabled) {
|
||||
if (NativeModules.BackgroundMedia && window.currentMediaInfo) {
|
||||
const { title, channel, uri } = window.currentMediaInfo;
|
||||
NativeModules.BackgroundMedia.showPlaybackNotification(title, channel, uri, this.state.paused);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderPlayerControls() {
|
||||
const { onBackButtonPressed } = this.props;
|
||||
|
||||
if (this.state.areControlsVisible) {
|
||||
return (
|
||||
<View style={mediaPlayerStyle.playerControlsContainer}>
|
||||
<TouchableOpacity style={mediaPlayerStyle.backButton} onPress={onBackButtonPressed}>
|
||||
<Icon name={'arrow-left'} size={18} style={mediaPlayerStyle.backButtonIcon} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={mediaPlayerStyle.playPauseButton} onPress={this.togglePlay}>
|
||||
{this.state.paused && <Icon name="play" size={40} color="#ffffff" />}
|
||||
{!this.state.paused && <Icon name="pause" size={40} color="#ffffff" />}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={mediaPlayerStyle.toggleFullscreenButton} onPress={this.toggleFullscreenMode}>
|
||||
{this.state.fullscreenMode && <Icon name="compress" size={16} color="#ffffff" />}
|
||||
{!this.state.fullscreenMode && <Icon name="expand" size={16} color="#ffffff" />}
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={mediaPlayerStyle.elapsedDuration}>{this.formatTime(this.state.currentTime)}</Text>
|
||||
<Text style={mediaPlayerStyle.totalDuration}>{this.formatTime(this.state.duration)}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
onSeekerTouchAreaPressed = evt => {
|
||||
if (evt && evt.nativeEvent) {
|
||||
const newSeekerPosition = evt.nativeEvent.locationX;
|
||||
if (!isNaN(newSeekerPosition)) {
|
||||
const time = this.state.duration * (newSeekerPosition / this.seekerWidth);
|
||||
this.setSeekerPosition(newSeekerPosition);
|
||||
this.seekTo(time);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onTrackingLayout = evt => {
|
||||
this.trackingOffset = evt.nativeEvent.layout.x;
|
||||
this.seekerWidth = evt.nativeEvent.layout.width;
|
||||
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onLayout, source, style, thumbnail } = this.props;
|
||||
const completedWidth = this.getCurrentTimePercentage() * this.seekerWidth;
|
||||
const remainingWidth = this.seekerWidth - completedWidth;
|
||||
let styles = [this.state.fullscreenMode ? mediaPlayerStyle.fullscreenContainer : mediaPlayerStyle.container];
|
||||
if (style) {
|
||||
if (style.length) {
|
||||
styles = styles.concat(style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
|
||||
const trackingStyle = [
|
||||
mediaPlayerStyle.trackingControls,
|
||||
this.state.fullscreenMode
|
||||
? mediaPlayerStyle.fullscreenTrackingControls
|
||||
: mediaPlayerStyle.containedTrackingControls,
|
||||
];
|
||||
|
||||
return (
|
||||
<View style={styles} onLayout={onLayout}>
|
||||
<Video
|
||||
source={{ uri: source }}
|
||||
bufferConfig={{
|
||||
minBufferMs: 15000,
|
||||
maxBufferMs: 60000,
|
||||
bufferForPlaybackMs: 5000,
|
||||
bufferForPlaybackAfterRebufferMs: 5000,
|
||||
}}
|
||||
ref={(ref: Video) => {
|
||||
this.video = ref;
|
||||
}}
|
||||
resizeMode={this.state.resizeMode}
|
||||
playInBackground={this.state.backgroundPlayEnabled}
|
||||
style={mediaPlayerStyle.player}
|
||||
rate={this.state.rate}
|
||||
volume={this.state.volume}
|
||||
paused={this.state.paused}
|
||||
onLoad={this.onLoad}
|
||||
onBuffer={this.onBuffer}
|
||||
onProgress={this.onProgress}
|
||||
onEnd={this.onEnd}
|
||||
onError={this.onError}
|
||||
minLoadRetryCount={999}
|
||||
/>
|
||||
|
||||
{this.state.firstPlay && thumbnail && thumbnail.trim().length > 0 && (
|
||||
<FastImage
|
||||
source={{ uri: thumbnail }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
style={mediaPlayerStyle.playerThumbnail}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TouchableOpacity style={mediaPlayerStyle.playerControls} onPress={this.togglePlayerControls}>
|
||||
{this.renderPlayerControls()}
|
||||
</TouchableOpacity>
|
||||
|
||||
{(!this.state.fullscreenMode || (this.state.fullscreenMode && this.state.areControlsVisible)) && (
|
||||
<View style={trackingStyle} onLayout={this.onTrackingLayout}>
|
||||
<View style={mediaPlayerStyle.progress}>
|
||||
<View style={[mediaPlayerStyle.innerProgressCompleted, { width: completedWidth }]} />
|
||||
<View style={[mediaPlayerStyle.innerProgressRemaining, { width: remainingWidth }]} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{this.state.buffering && (
|
||||
<View style={mediaPlayerStyle.loadingContainer}>
|
||||
<ActivityIndicator color={Colors.LbryGreen} size="large" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{this.state.areControlsVisible && (
|
||||
<View style={{ left: this.getTrackingOffset(), width: this.seekerWidth }}>
|
||||
<View
|
||||
style={[
|
||||
mediaPlayerStyle.seekerHandle,
|
||||
this.state.fullscreenMode ? mediaPlayerStyle.seekerHandleFs : mediaPlayerStyle.seekerHandleContained,
|
||||
{ left: this.state.seekerPosition },
|
||||
]}
|
||||
{...this.seekResponder.panHandlers}
|
||||
>
|
||||
<View style={this.state.seeking ? mediaPlayerStyle.bigSeekerCircle : mediaPlayerStyle.seekerCircle} />
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
mediaPlayerStyle.seekerTouchArea,
|
||||
this.state.fullscreenMode
|
||||
? mediaPlayerStyle.seekerTouchAreaFs
|
||||
: mediaPlayerStyle.seekerTouchAreaContained,
|
||||
]}
|
||||
onPress={this.onSeekerTouchAreaPressed}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MediaPlayer;
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import NavigationButton from './view';
|
||||
|
||||
export default connect()(NavigationButton);
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
|
||||
class NavigationButton extends React.PureComponent {
|
||||
render() {
|
||||
const { iconStyle, name, onPress, size, style } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} style={style}>
|
||||
<Icon name={name} size={size} style={iconStyle} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default NavigationButton;
|
|
@ -1,9 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import NsfwOverlay from './view';
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
perform
|
||||
)(NsfwOverlay);
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Text, TouchableOpacity } from 'react-native';
|
||||
import discoverStyle from '../../styles/discover';
|
||||
|
||||
class NsfwOverlay extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<TouchableOpacity style={discoverStyle.overlay} activeOpacity={0.95} onPress={this.props.onPress}>
|
||||
<Text style={discoverStyle.overlayText}>
|
||||
This content is Not Safe For Work. To view adult content, please change your Settings.
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default NsfwOverlay;
|
|
@ -1,9 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import PageHeader from './view';
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
perform
|
||||
)(PageHeader);
|
|
@ -1,39 +0,0 @@
|
|||
// Based on https://github.com/react-navigation/react-navigation/blob/master/src/views/Header/Header.js
|
||||
import React from 'react';
|
||||
import { Animated, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import NavigationButton from 'component/navigationButton';
|
||||
import pageHeaderStyle from 'styles/pageHeader';
|
||||
|
||||
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
|
||||
const AnimatedText = Animated.Text;
|
||||
|
||||
class PageHeader extends React.PureComponent {
|
||||
render() {
|
||||
const { title, onBackPressed } = this.props;
|
||||
const containerStyles = [pageHeaderStyle.container, { height: APPBAR_HEIGHT }];
|
||||
|
||||
return (
|
||||
<View style={containerStyles}>
|
||||
<View style={pageHeaderStyle.flexOne}>
|
||||
<View style={pageHeaderStyle.header}>
|
||||
<View style={pageHeaderStyle.title}>
|
||||
<AnimatedText numberOfLines={1} style={pageHeaderStyle.titleText} accessibilityTraits="header">
|
||||
{title}
|
||||
</AnimatedText>
|
||||
</View>
|
||||
<NavigationButton
|
||||
name="arrow-left"
|
||||
style={pageHeaderStyle.left}
|
||||
size={24}
|
||||
iconStyle={pageHeaderStyle.backIcon}
|
||||
onPress={onBackPressed}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PageHeader;
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ProgressBar from './view';
|
||||
|
||||
export default connect()(ProgressBar);
|
|
@ -1,56 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View } from 'react-native';
|
||||
|
||||
const defaultHeight = 5;
|
||||
const defaultBorderRadius = 5;
|
||||
const minProgress = 0;
|
||||
const maxProgress = 100;
|
||||
|
||||
class ProgressBar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
borderRadius: PropTypes.number,
|
||||
color: PropTypes.string.isRequired,
|
||||
height: PropTypes.number,
|
||||
progress: function(props, propName, componentName) {
|
||||
const value = parseInt(props[propName], 10);
|
||||
if (isNaN(value) || props[propName] < minProgress || props[propName] > maxProgress) {
|
||||
return new Error('progress should be between 0 and 100');
|
||||
}
|
||||
},
|
||||
style: PropTypes.any,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { borderRadius, color, height, progress, style } = this.props;
|
||||
const currentProgress = Math.ceil(progress);
|
||||
|
||||
let styles = [];
|
||||
if (style) {
|
||||
if (style.length) {
|
||||
styles = styles.concat(style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
|
||||
styles.push({
|
||||
borderRadius: borderRadius || defaultBorderRadius,
|
||||
flexDirection: 'row',
|
||||
height: height || defaultHeight,
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles}>
|
||||
<View
|
||||
style={{ backgroundColor: color, borderRadius: borderRadius || defaultBorderRadius, flex: currentProgress }}
|
||||
/>
|
||||
<View style={{ backgroundColor: color, opacity: 0.2, flex: 100 - currentProgress }} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProgressBar;
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import PublishRewardsDriver from './view';
|
||||
|
||||
export default connect()(PublishRewardsDriver);
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Text, TouchableOpacity } from 'react-native';
|
||||
import Colors from 'styles/colors';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import publishStyle from 'styles/publish';
|
||||
|
||||
class PublishRewadsDriver extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={publishStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}>
|
||||
<Icon name="award" size={16} style={publishStyle.rewardIcon} />
|
||||
<Text style={publishStyle.rewardDriverText}>Earn some credits to be able to publish your content.</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PublishRewadsDriver;
|
|
@ -1,25 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
doSearch,
|
||||
makeSelectRecommendedContentForUri,
|
||||
makeSelectTitleForUri,
|
||||
selectIsSearching,
|
||||
} from 'lbry-redux';
|
||||
import RelatedContent from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
recommendedContent: makeSelectRecommendedContentForUri(props.uri)(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
isSearching: selectIsSearching(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
search: query => dispatch(doSearch(query, 20, undefined, true)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(RelatedContent);
|
|
@ -1,67 +0,0 @@
|
|||
import React from 'react';
|
||||
import { ActivityIndicator, FlatList, Text, View } from 'react-native';
|
||||
import { navigateToUri } from 'utils/helper';
|
||||
import Colors from 'styles/colors';
|
||||
import FileListItem from 'component/fileListItem';
|
||||
import fileListStyle from 'styles/fileList';
|
||||
import relatedContentStyle from 'styles/relatedContent';
|
||||
|
||||
export default class RelatedContent extends React.PureComponent<Props> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.didSearch = undefined;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getRecommendedContent();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { claim, uri } = this.props;
|
||||
|
||||
if (uri !== prevProps.uri) {
|
||||
this.didSearch = false;
|
||||
}
|
||||
|
||||
if (claim && !this.didSearch) {
|
||||
this.getRecommendedContent();
|
||||
}
|
||||
}
|
||||
|
||||
getRecommendedContent() {
|
||||
const { search, title } = this.props;
|
||||
|
||||
if (title) {
|
||||
search(title);
|
||||
this.didSearch = true;
|
||||
}
|
||||
}
|
||||
|
||||
didSearch: ?boolean;
|
||||
|
||||
render() {
|
||||
const { recommendedContent, isSearching, navigation } = this.props;
|
||||
|
||||
if (!isSearching && (!recommendedContent || recommendedContent.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={relatedContentStyle.container}>
|
||||
<Text style={relatedContentStyle.title}>Related Content</Text>
|
||||
{recommendedContent &&
|
||||
recommendedContent.map(recommendedUri => (
|
||||
<FileListItem
|
||||
style={fileListStyle.item}
|
||||
key={recommendedUri}
|
||||
uri={recommendedUri}
|
||||
navigation={navigation}
|
||||
onPress={() => navigateToUri(navigation, recommendedUri, { autoplay: true })}
|
||||
/>
|
||||
))}
|
||||
{isSearching && <ActivityIndicator size="small" color={Colors.LbryGreen} style={relatedContentStyle.loading} />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import {
|
||||
doClaimRewardType,
|
||||
doClaimRewardClearError,
|
||||
makeSelectClaimRewardError,
|
||||
makeSelectIsRewardClaimPending,
|
||||
} from 'lbryinc';
|
||||
import RewardCard from './view';
|
||||
|
||||
const makeSelect = () => {
|
||||
const selectIsPending = makeSelectIsRewardClaimPending();
|
||||
const selectError = makeSelectClaimRewardError();
|
||||
|
||||
const select = (state, props) => ({
|
||||
errorMessage: selectError(state, props),
|
||||
isPending: selectIsPending(state, props),
|
||||
});
|
||||
|
||||
return select;
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)),
|
||||
clearError: reward => dispatch(doClaimRewardClearError(reward)),
|
||||
notify: data => dispatch(doToast(data)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
makeSelect,
|
||||
perform
|
||||
)(RewardCard);
|
|
@ -1,112 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
|
||||
import Colors from '../../styles/colors';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import Link from '../link';
|
||||
import rewardStyle from '../../styles/reward';
|
||||
|
||||
type Props = {
|
||||
canClaim: boolean,
|
||||
onClaimPress: object,
|
||||
reward: {
|
||||
id: string,
|
||||
reward_title: string,
|
||||
reward_amount: number,
|
||||
transaction_id: string,
|
||||
created_at: string,
|
||||
reward_description: string,
|
||||
reward_type: string,
|
||||
},
|
||||
};
|
||||
|
||||
class RewardCard extends React.PureComponent<Props> {
|
||||
state = {
|
||||
claimStarted: false,
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { errorMessage, isPending } = nextProps;
|
||||
const { clearError, notify, reward } = this.props;
|
||||
if (this.state.claimStarted && !isPending) {
|
||||
if (errorMessage && errorMessage.trim().length > 0) {
|
||||
notify({ message: errorMessage });
|
||||
clearError(reward);
|
||||
} else {
|
||||
notify({ message: 'Reward successfully claimed!' });
|
||||
}
|
||||
this.setState({ claimStarted: false });
|
||||
}
|
||||
}
|
||||
|
||||
onClaimPress = () => {
|
||||
const { canClaim, claimReward, notify, reward, showVerification } = this.props;
|
||||
|
||||
if (!canClaim) {
|
||||
if (showVerification) {
|
||||
showVerification();
|
||||
}
|
||||
notify({ message: 'Unfortunately, you are not eligible to claim this reward at this time.' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ claimStarted: true }, () => {
|
||||
claimReward(reward);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { canClaim, isPending, onClaimPress, reward } = this.props;
|
||||
const claimed = !!reward.transaction_id;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[rewardStyle.rewardCard, rewardStyle.row]}
|
||||
onPress={() => {
|
||||
if (!isPending && !claimed) {
|
||||
this.onClaimPress();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={rewardStyle.leftCol}>
|
||||
{!isPending && (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (!claimed) {
|
||||
this.onClaimPress();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{claimed && (
|
||||
<Icon
|
||||
name={claimed ? 'check-circle' : 'circle'}
|
||||
style={claimed ? rewardStyle.claimed : canClaim ? rewardStyle.unclaimed : rewardStyle.disabled}
|
||||
size={20}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{isPending && <ActivityIndicator size="small" color={Colors.LbryGreen} />}
|
||||
</View>
|
||||
<View style={rewardStyle.midCol}>
|
||||
<Text style={rewardStyle.rewardTitle}>{reward.reward_title}</Text>
|
||||
<Text style={rewardStyle.rewardDescription}>{reward.reward_description}</Text>
|
||||
{claimed && (
|
||||
<Link
|
||||
style={rewardStyle.link}
|
||||
href={`https://explorer.lbry.com/tx/${reward.transaction_id}`}
|
||||
text={reward.transaction_id.substring(0, 7)}
|
||||
error={'The transaction URL could not be opened'}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View style={rewardStyle.rightCol}>
|
||||
<Text style={rewardStyle.rewardAmount}>{reward.reward_amount}</Text>
|
||||
<Text style={rewardStyle.rewardCurrency}>LBC</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RewardCard;
|
|
@ -1,22 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import { doRewardList, selectUnclaimedRewardValue, selectFetchingRewards, selectUser } from 'lbryinc';
|
||||
import RewardEnrolment from './view';
|
||||
|
||||
const select = state => ({
|
||||
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
|
||||
fetching: selectFetchingRewards(state),
|
||||
user: selectUser(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchRewards: () => dispatch(doRewardList()),
|
||||
notify: data => dispatch(doToast(data)),
|
||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(RewardEnrolment);
|
|
@ -1,53 +0,0 @@
|
|||
import React from 'react';
|
||||
import { NativeModules, Text, TouchableOpacity, View } from 'react-native';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import Button from 'component/button';
|
||||
import Constants from 'constants';
|
||||
import Link from 'component/link';
|
||||
import Colors from 'styles/colors';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import rewardStyle from 'styles/reward';
|
||||
|
||||
class RewardEnrolment extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.fetchRewards();
|
||||
}
|
||||
|
||||
onNotInterestedPressed = () => {
|
||||
const { navigation, setClientSetting } = this.props;
|
||||
setClientSetting(Constants.SETTING_REWARDS_NOT_INTERESTED, true);
|
||||
navigation.navigate({ routeName: 'DiscoverStack' });
|
||||
};
|
||||
|
||||
onEnrollPressed = () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate({ routeName: 'Verification', key: 'verification', params: { syncFlow: false } });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { fetching, navigation, unclaimedRewardAmount, user } = this.props;
|
||||
|
||||
return (
|
||||
<View style={rewardStyle.enrollContainer} onPress>
|
||||
<View style={rewardStyle.summaryRow}>
|
||||
<Icon name="award" size={36} color={Colors.White} />
|
||||
<Text style={rewardStyle.summaryText}>{unclaimedRewardAmount} unclaimed credits</Text>
|
||||
</View>
|
||||
|
||||
<View style={rewardStyle.onboarding}>
|
||||
<Text style={rewardStyle.enrollDescText}>
|
||||
LBRY credits allow you to purchase content, publish content, and influence the network. You can start
|
||||
earning credits by watching videos on LBRY.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={rewardStyle.buttonRow}>
|
||||
<Link style={rewardStyle.notInterestedLink} text={'Not interested'} onPress={this.onNotInterestedPressed} />
|
||||
<Button style={rewardStyle.enrollButton} theme={'light'} text={'Enroll'} onPress={this.onEnrollPressed} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RewardEnrolment;
|
|
@ -1,20 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import { doRewardList, selectUnclaimedRewardValue, selectFetchingRewards, selectUser } from 'lbryinc';
|
||||
import RewardSummary from './view';
|
||||
|
||||
const select = state => ({
|
||||
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
|
||||
fetching: selectFetchingRewards(state),
|
||||
user: selectUser(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchRewards: () => dispatch(doRewardList()),
|
||||
notify: data => dispatch(doToast(data)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(RewardSummary);
|
|
@ -1,82 +0,0 @@
|
|||
import React from 'react';
|
||||
import { NativeModules, Text, TouchableOpacity, View } from 'react-native';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import Button from 'component/button';
|
||||
import Colors from 'styles/colors';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import rewardStyle from 'styles/reward';
|
||||
|
||||
class RewardSummary extends React.Component {
|
||||
static itemKey = 'rewardSummaryDismissed';
|
||||
|
||||
state = {
|
||||
actionsLeft: 0,
|
||||
dismissed: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchRewards();
|
||||
|
||||
AsyncStorage.getItem(RewardSummary.itemKey).then(isDismissed => {
|
||||
if ('true' === isDismissed) {
|
||||
this.setState({ dismissed: true });
|
||||
}
|
||||
|
||||
const { user } = this.props;
|
||||
let actionsLeft = 0;
|
||||
if (!user || !user.has_verified_email) {
|
||||
actionsLeft++;
|
||||
}
|
||||
|
||||
if (!user || !user.is_identity_verified) {
|
||||
actionsLeft++;
|
||||
}
|
||||
|
||||
this.setState({ actionsLeft });
|
||||
});
|
||||
}
|
||||
|
||||
onDismissPressed = () => {
|
||||
AsyncStorage.setItem(RewardSummary.itemKey, 'true');
|
||||
this.setState({ dismissed: true });
|
||||
this.props.notify({
|
||||
message: 'You can always claim your rewards from the Rewards page.',
|
||||
});
|
||||
};
|
||||
|
||||
handleSummaryPressed = () => {
|
||||
const { showVerification } = this.props;
|
||||
if (showVerification) {
|
||||
showVerification();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { fetching, navigation, unclaimedRewardAmount, user } = this.props;
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.dismissed ||
|
||||
(user && user.is_reward_approved) ||
|
||||
this.state.actionsLeft === 0 ||
|
||||
unclaimedRewardAmount === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={rewardStyle.summaryContainer} onPress={this.handleSummaryPressed}>
|
||||
<View style={rewardStyle.summaryRow}>
|
||||
<Icon name="award" size={36} color={Colors.White} />
|
||||
<Text style={rewardStyle.summaryText}>{unclaimedRewardAmount} unclaimed credits</Text>
|
||||
</View>
|
||||
<Button style={rewardStyle.dismissButton} theme={'light'} text={'Dismiss'} onPress={this.onDismissPressed} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RewardSummary;
|
|
@ -1,19 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { NativeModules } from 'react-native';
|
||||
import { doSearch, doUpdateSearchQuery } from 'lbry-redux';
|
||||
import SearchInput from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
search: search => {
|
||||
if (NativeModules.Firebase) {
|
||||
NativeModules.Firebase.track('search', { query: search });
|
||||
}
|
||||
return dispatch(doSearch(search));
|
||||
},
|
||||
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query, false)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
perform
|
||||
)(SearchInput);
|
|
@ -1,41 +0,0 @@
|
|||
import React from 'react';
|
||||
import { TextInput } from 'react-native';
|
||||
|
||||
class SearchInput extends React.PureComponent {
|
||||
static INPUT_TIMEOUT = 500;
|
||||
|
||||
state = {
|
||||
changeTextTimeout: -1,
|
||||
};
|
||||
|
||||
handleChangeText = text => {
|
||||
clearTimeout(this.state.changeTextTimeout);
|
||||
if (!text || text.trim().length < 2) {
|
||||
// only perform a search if 2 or more characters have been input
|
||||
return;
|
||||
}
|
||||
const { search, updateSearchQuery } = this.props;
|
||||
updateSearchQuery(text);
|
||||
|
||||
let timeout = setTimeout(() => {
|
||||
search(text);
|
||||
}, SearchInput.INPUT_TIMEOUT);
|
||||
this.setState({ changeTextTimeout: timeout });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { style, value } = this.props;
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
style={style}
|
||||
placeholder="Search"
|
||||
underlineColorAndroid="transparent"
|
||||
value={value}
|
||||
onChangeText={text => this.handleChangeText(text)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchInput;
|
|
@ -1,14 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import SearchRightHeaderIcon from './view';
|
||||
import { ACTIONS } from 'lbry-redux';
|
||||
const perform = dispatch => ({
|
||||
clearQuery: () =>
|
||||
dispatch({
|
||||
type: ACTIONS.HISTORY_NAVIGATE,
|
||||
}),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
perform
|
||||
)(SearchRightHeaderIcon);
|
|
@ -1,19 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { NavigationActions } from 'react-navigation';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
|
||||
class SearchRightHeaderIcon extends React.PureComponent {
|
||||
clearAndGoBack() {
|
||||
const { navigation } = this.props;
|
||||
this.props.clearQuery();
|
||||
navigation.dispatch(NavigationActions.back());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { style } = this.props;
|
||||
return <Feather name="x" size={24} style={style} onPress={() => this.clearAndGoBack()} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchRightHeaderIcon;
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import StorageStatsCard from './view';
|
||||
|
||||
export default connect()(StorageStatsCard);
|
|
@ -1,133 +0,0 @@
|
|||
import React from 'react';
|
||||
import { normalizeURI, parseURI } from 'lbry-redux';
|
||||
import { ActivityIndicator, Platform, Switch, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { formatBytes } from '../../utils/helper';
|
||||
import Colors from '../../styles/colors';
|
||||
import storageStatsStyle from '../../styles/storageStats';
|
||||
|
||||
class StorageStatsCard extends React.PureComponent {
|
||||
state = {
|
||||
totalBytes: 0,
|
||||
totalAudioBytes: 0,
|
||||
totalAudioPercent: 0,
|
||||
totalImageBytes: 0,
|
||||
totalImagePercent: 0,
|
||||
totalVideoBytes: 0,
|
||||
totalVideoPercent: 0,
|
||||
totalOtherBytes: 0,
|
||||
totalOtherPercent: 0,
|
||||
showStats: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// calculate total bytes
|
||||
const { fileInfos } = this.props;
|
||||
|
||||
let totalBytes = 0,
|
||||
totalAudioBytes = 0,
|
||||
totalImageBytes = 0,
|
||||
totalVideoBytes = 0;
|
||||
let totalAudioPercent = 0,
|
||||
totalImagePercent = 0,
|
||||
totalVideoPercent = 0;
|
||||
|
||||
fileInfos.forEach(fileInfo => {
|
||||
if (fileInfo.completed) {
|
||||
const bytes = fileInfo.written_bytes;
|
||||
const type = fileInfo.mime_type;
|
||||
totalBytes += bytes;
|
||||
if (type) {
|
||||
if (type.startsWith('audio/')) totalAudioBytes += bytes;
|
||||
if (type.startsWith('image/')) totalImageBytes += bytes;
|
||||
if (type.startsWith('video/')) totalVideoBytes += bytes;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
totalAudioPercent = ((totalAudioBytes / totalBytes) * 100).toFixed(2);
|
||||
totalImagePercent = ((totalImageBytes / totalBytes) * 100).toFixed(2);
|
||||
totalVideoPercent = ((totalVideoBytes / totalBytes) * 100).toFixed(2);
|
||||
|
||||
this.setState({
|
||||
totalBytes,
|
||||
totalAudioBytes,
|
||||
totalAudioPercent,
|
||||
totalImageBytes,
|
||||
totalImagePercent,
|
||||
totalVideoBytes,
|
||||
totalVideoPercent,
|
||||
totalOtherBytes: totalBytes - (totalAudioBytes + totalImageBytes + totalVideoBytes),
|
||||
totalOtherPercent: (
|
||||
100 -
|
||||
(parseFloat(totalAudioPercent) + parseFloat(totalImagePercent) + parseFloat(totalVideoPercent))
|
||||
).toFixed(2),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.totalBytes == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={storageStatsStyle.card}>
|
||||
<View style={[storageStatsStyle.row, storageStatsStyle.totalSizeContainer]}>
|
||||
<View style={storageStatsStyle.summary}>
|
||||
<Text style={storageStatsStyle.totalSize}>{formatBytes(this.state.totalBytes, 2)}</Text>
|
||||
<Text style={storageStatsStyle.annotation}>used</Text>
|
||||
</View>
|
||||
<View style={[storageStatsStyle.row, storageStatsStyle.toggleStatsContainer]}>
|
||||
<Text style={storageStatsStyle.statsText}>Stats</Text>
|
||||
<Switch
|
||||
style={storageStatsStyle.statsToggle}
|
||||
value={this.state.showStats}
|
||||
onValueChange={value => this.setState({ showStats: value })}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{this.state.showStats && (
|
||||
<View>
|
||||
<View style={storageStatsStyle.distributionBar}>
|
||||
<View style={[storageStatsStyle.audioDistribution, { flex: parseFloat(this.state.totalAudioPercent) }]} />
|
||||
<View style={[storageStatsStyle.imageDistribution, { flex: parseFloat(this.state.totalImagePercent) }]} />
|
||||
<View style={[storageStatsStyle.videoDistribution, { flex: parseFloat(this.state.totalVideoPercent) }]} />
|
||||
<View style={[storageStatsStyle.otherDistribution, { flex: parseFloat(this.state.totalOtherPercent) }]} />
|
||||
</View>
|
||||
<View style={storageStatsStyle.legend}>
|
||||
{this.state.totalAudioBytes > 0 && (
|
||||
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
|
||||
<View style={[storageStatsStyle.legendBox, storageStatsStyle.audioDistribution]} />
|
||||
<Text style={storageStatsStyle.legendText}>Audio</Text>
|
||||
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalAudioBytes, 2)}</Text>
|
||||
</View>
|
||||
)}
|
||||
{this.state.totalImageBytes > 0 && (
|
||||
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
|
||||
<View style={[storageStatsStyle.legendBox, storageStatsStyle.imageDistribution]} />
|
||||
<Text style={storageStatsStyle.legendText}>Images</Text>
|
||||
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalImageBytes, 2)}</Text>
|
||||
</View>
|
||||
)}
|
||||
{this.state.totalVideoBytes > 0 && (
|
||||
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
|
||||
<View style={[storageStatsStyle.legendBox, storageStatsStyle.videoDistribution]} />
|
||||
<Text style={storageStatsStyle.legendText}>Videos</Text>
|
||||
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalVideoBytes, 2)}</Text>
|
||||
</View>
|
||||
)}
|
||||
{this.state.totalOtherBytes > 0 && (
|
||||
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
|
||||
<View style={[storageStatsStyle.legendBox, storageStatsStyle.otherDistribution]} />
|
||||
<Text style={storageStatsStyle.legendText}>Other</Text>
|
||||
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalOtherBytes, 2)}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StorageStatsCard;
|
|
@ -1,18 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doChannelSubscribe, doChannelUnsubscribe, selectSubscriptions, makeSelectIsSubscribed } from 'lbryinc';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import SubscribeButton from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
subscriptions: selectSubscriptions(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
{
|
||||
doChannelSubscribe,
|
||||
doChannelUnsubscribe,
|
||||
doToast,
|
||||
}
|
||||
)(SubscribeButton);
|
|
@ -1,44 +0,0 @@
|
|||
import React from 'react';
|
||||
import { normalizeURI, parseURI } from 'lbry-redux';
|
||||
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
|
||||
import Button from 'component/button';
|
||||
import Colors from 'styles/colors';
|
||||
|
||||
class SubscribeButton extends React.PureComponent {
|
||||
render() {
|
||||
const { uri, isSubscribed, doChannelSubscribe, doChannelUnsubscribe, style, hideText } = this.props;
|
||||
|
||||
let styles = [];
|
||||
if (style) {
|
||||
if (style.length) {
|
||||
styles = styles.concat(style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
|
||||
const iconColor = isSubscribed ? null : Colors.Red;
|
||||
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
|
||||
const subscriptionLabel = isSubscribed ? null : __('Subscribe');
|
||||
const { claimName } = parseURI(uri);
|
||||
|
||||
return (
|
||||
<Button
|
||||
style={styles}
|
||||
theme={'light'}
|
||||
icon={isSubscribed ? 'heart-broken' : 'heart'}
|
||||
iconColor={iconColor}
|
||||
solid={isSubscribed ? false : true}
|
||||
text={hideText ? null : subscriptionLabel}
|
||||
onPress={() => {
|
||||
subscriptionHandler({
|
||||
channelName: claimName,
|
||||
uri: normalizeURI(uri),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SubscribeButton;
|
|
@ -1,25 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doChannelSubscriptionEnableNotifications,
|
||||
doChannelSubscriptionDisableNotifications,
|
||||
selectEnabledChannelNotifications,
|
||||
selectSubscriptions,
|
||||
makeSelectIsSubscribed,
|
||||
} from 'lbryinc';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import SubscribeNotificationButton from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
enabledChannelNotifications: selectEnabledChannelNotifications(state),
|
||||
subscriptions: selectSubscriptions(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
{
|
||||
doChannelSubscriptionEnableNotifications,
|
||||
doChannelSubscriptionDisableNotifications,
|
||||
doToast,
|
||||
}
|
||||
)(SubscribeNotificationButton);
|
|
@ -1,56 +0,0 @@
|
|||
import React from 'react';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
|
||||
import Button from 'component/button';
|
||||
import Colors from 'styles/colors';
|
||||
|
||||
class SubscribeNotificationButton extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
uri,
|
||||
name,
|
||||
doChannelSubscriptionEnableNotifications,
|
||||
doChannelSubscriptionDisableNotifications,
|
||||
doToast,
|
||||
enabledChannelNotifications,
|
||||
isSubscribed,
|
||||
style,
|
||||
} = this.props;
|
||||
|
||||
if (!isSubscribed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let styles = [];
|
||||
if (style) {
|
||||
if (style.length) {
|
||||
styles = styles.concat(style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
|
||||
const shouldNotify = enabledChannelNotifications.indexOf(name) > -1;
|
||||
const { claimName } = parseURI(uri);
|
||||
|
||||
return (
|
||||
<Button
|
||||
style={styles}
|
||||
theme={'light'}
|
||||
icon={shouldNotify ? 'bell-slash' : 'bell'}
|
||||
solid={true}
|
||||
onPress={() => {
|
||||
if (shouldNotify) {
|
||||
doChannelSubscriptionDisableNotifications(name);
|
||||
doToast({ message: 'You will not receive notifications for new content.' });
|
||||
} else {
|
||||
doChannelSubscriptionEnableNotifications(name);
|
||||
doToast({ message: 'You will receive all notifications for new content.' });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SubscribeNotificationButton;
|
|
@ -1,25 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
makeSelectFetchingChannelClaims,
|
||||
makeSelectClaimsInChannelForPage,
|
||||
doFetchClaimsByChannel,
|
||||
doResolveUris,
|
||||
} from 'lbry-redux';
|
||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||
import SuggestedSubscriptionItem from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claims: makeSelectClaimsInChannelForPage(props.categoryLink)(state),
|
||||
fetching: makeSelectFetchingChannelClaims(props.categoryLink)(state),
|
||||
obscureNsfw: !selectShowNsfw(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchChannel: channel => dispatch(doFetchClaimsByChannel(channel)),
|
||||
resolveUris: uris => dispatch(doResolveUris(uris, true)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(SuggestedSubscriptionItem);
|
|
@ -1,77 +0,0 @@
|
|||
import React from 'react';
|
||||
import { buildURI, normalizeURI } from 'lbry-redux';
|
||||
import { ActivityIndicator, FlatList, Text, View } from 'react-native';
|
||||
import Colors from 'styles/colors';
|
||||
import discoverStyle from 'styles/discover';
|
||||
import FileItem from 'component/fileItem';
|
||||
import subscriptionsStyle from 'styles/subscriptions';
|
||||
|
||||
class SuggestedSubscriptionItem extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const { fetching, categoryLink, fetchChannel, resolveUris, claims } = this.props;
|
||||
if (!fetching && categoryLink && (!claims || claims.length)) {
|
||||
fetchChannel(categoryLink);
|
||||
}
|
||||
}
|
||||
|
||||
uriForClaim = claim => {
|
||||
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = claim;
|
||||
const uriParams = {};
|
||||
|
||||
// This is unfortunate
|
||||
// https://github.com/lbryio/lbry/issues/1159
|
||||
const name = claimName || claimNameDownloaded;
|
||||
uriParams.contentName = name;
|
||||
uriParams.claimId = claimId;
|
||||
const uri = buildURI(uriParams);
|
||||
|
||||
return uri;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { categoryLink, fetching, obscureNsfw, claims, navigation } = this.props;
|
||||
|
||||
if (!claims || !claims.length) {
|
||||
return (
|
||||
<View style={subscriptionsStyle.busyContainer}>
|
||||
<ActivityIndicator size={'small'} color={Colors.LbryGreen} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (claims && claims.length > 0) {
|
||||
return (
|
||||
<View style={subscriptionsStyle.suggestedContainer}>
|
||||
<FileItem
|
||||
style={subscriptionsStyle.compactMainFileItem}
|
||||
mediaStyle={subscriptionsStyle.fileItemMedia}
|
||||
uri={this.uriForClaim(claims[0])}
|
||||
navigation={navigation}
|
||||
/>
|
||||
{claims.length > 1 && (
|
||||
<FlatList
|
||||
style={subscriptionsStyle.compactItems}
|
||||
horizontal={true}
|
||||
renderItem={({ item }) => (
|
||||
<FileItem
|
||||
style={subscriptionsStyle.compactFileItem}
|
||||
mediaStyle={subscriptionsStyle.compactFileItemMedia}
|
||||
key={item}
|
||||
uri={normalizeURI(item)}
|
||||
navigation={navigation}
|
||||
compactView={true}
|
||||
/>
|
||||
)}
|
||||
data={claims.slice(1, 4).map(claim => this.uriForClaim(claim))}
|
||||
keyExtractor={(item, index) => item}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default SuggestedSubscriptionItem;
|
|
@ -1,13 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectSuggestedChannels, selectIsFetchingSuggested } from 'lbryinc';
|
||||
import SuggestedSubscriptions from './view';
|
||||
|
||||
const select = state => ({
|
||||
suggested: selectSuggestedChannels(state),
|
||||
loading: selectIsFetchingSuggested(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
null
|
||||
)(SuggestedSubscriptions);
|
|
@ -1,54 +0,0 @@
|
|||
import React from 'react';
|
||||
import { ActivityIndicator, SectionList, Text, View } from 'react-native';
|
||||
import { normalizeURI } from 'lbry-redux';
|
||||
import { navigateToUri } from 'utils/helper';
|
||||
import SubscribeButton from 'component/subscribeButton';
|
||||
import SuggestedSubscriptionItem from 'component/suggestedSubscriptionItem';
|
||||
import Colors from 'styles/colors';
|
||||
import discoverStyle from 'styles/discover';
|
||||
import subscriptionsStyle from 'styles/subscriptions';
|
||||
import Link from 'component/link';
|
||||
|
||||
class SuggestedSubscriptions extends React.PureComponent {
|
||||
render() {
|
||||
const { suggested, loading, navigation } = this.props;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View>
|
||||
<ActivityIndicator size="large" color={Colors.LbryGreen} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return suggested ? (
|
||||
<SectionList
|
||||
style={subscriptionsStyle.scrollContainer}
|
||||
renderItem={({ item, index, section }) => (
|
||||
<SuggestedSubscriptionItem key={item} categoryLink={normalizeURI(item)} navigation={navigation} />
|
||||
)}
|
||||
renderSectionHeader={({ section: { title } }) => {
|
||||
const titleParts = title.split(';');
|
||||
const channelName = titleParts[0];
|
||||
const channelUri = normalizeURI(titleParts[1]);
|
||||
return (
|
||||
<View style={subscriptionsStyle.titleRow}>
|
||||
<Link
|
||||
style={subscriptionsStyle.channelTitle}
|
||||
text={channelName}
|
||||
onPress={() => {
|
||||
navigateToUri(navigation, normalizeURI(channelUri));
|
||||
}}
|
||||
/>
|
||||
<SubscribeButton style={subscriptionsStyle.subscribeButton} uri={channelUri} name={channelName} />
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
sections={suggested.map(({ uri, label }) => ({ title: label + ';' + uri, data: [uri] }))}
|
||||
keyExtractor={(item, index) => item}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
|
||||
export default SuggestedSubscriptions;
|
|
@ -1,4 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Tag from './view';
|
||||
|
||||
export default connect()(Tag);
|
|
@ -1,55 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Text, TouchableOpacity, View } from 'react-native';
|
||||
import tagStyle from 'styles/tag';
|
||||
import Colors from 'styles/colors';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
|
||||
export default class Tag extends React.PureComponent {
|
||||
onPressDefault = () => {
|
||||
const { name, navigation, type, onAddPress, onRemovePress } = this.props;
|
||||
if ('add' === type) {
|
||||
if (onAddPress) {
|
||||
onAddPress(name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ('remove' === type) {
|
||||
if (onRemovePress) {
|
||||
onRemovePress(name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigation) {
|
||||
// navigate to tag page
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { name, onPress, style, type } = this.props;
|
||||
|
||||
let styles = [];
|
||||
if (style) {
|
||||
if (style.length) {
|
||||
styles = styles.concat(style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
|
||||
styles.push({
|
||||
backgroundColor: Colors.TagGreen,
|
||||
borderRadius: 8,
|
||||
marginBottom: 4,
|
||||
});
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={styles} onPress={onPress || this.onPressDefault}>
|
||||
<View style={tagStyle.content}>
|
||||
<Text style={tagStyle.text}>{name}</Text>
|
||||
{type && <Icon style={tagStyle.icon} name={type === 'add' ? 'plus' : 'times'} size={8} />}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectUnfollowedTags } from 'lbry-redux';
|
||||
import TagSearch from './view';
|
||||
|
||||
const select = state => ({
|
||||
unfollowedTags: selectUnfollowedTags(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
null
|
||||
)(TagSearch);
|
|
@ -1,81 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import Tag from 'component/tag';
|
||||
import tagStyle from 'styles/tag';
|
||||
import Colors from 'styles/colors';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
|
||||
export default class TagSearch extends React.PureComponent {
|
||||
state = {
|
||||
tag: null,
|
||||
tagResults: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { selectedTags = [] } = this.props;
|
||||
this.updateTagResults(this.state.tag, selectedTags);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { selectedTags: prevSelectedTags = [] } = this.props;
|
||||
const { selectedTags = [] } = nextProps;
|
||||
|
||||
if (selectedTags.length !== prevSelectedTags.length) {
|
||||
this.updateTagResults(this.state.tag, selectedTags);
|
||||
}
|
||||
}
|
||||
|
||||
onAddTagPress = tag => {
|
||||
const { handleAddTag } = this.props;
|
||||
if (handleAddTag) {
|
||||
handleAddTag(tag);
|
||||
}
|
||||
};
|
||||
|
||||
handleTagChange = tag => {
|
||||
const { selectedTags = [] } = this.props;
|
||||
this.setState({ tag });
|
||||
this.updateTagResults(tag, selectedTags);
|
||||
};
|
||||
|
||||
updateTagResults = (tag, selectedTags = []) => {
|
||||
const { unfollowedTags } = this.props;
|
||||
|
||||
// the search term should always be the first result
|
||||
let results = [];
|
||||
const tagNotSelected = name => selectedTags.indexOf(name.toLowerCase()) === -1;
|
||||
const suggestedTagsSet = new Set(unfollowedTags.map(tag => tag.name));
|
||||
const suggestedTags = Array.from(suggestedTagsSet).filter(tagNotSelected);
|
||||
if (tag && tag.trim().length > 0) {
|
||||
results.push(tag.toLowerCase());
|
||||
const doesTagMatch = name => name.toLowerCase().includes(tag.toLowerCase());
|
||||
results = results.concat(suggestedTags.filter(doesTagMatch).slice(0, 5));
|
||||
} else {
|
||||
results = results.concat(suggestedTags.slice(0, 5));
|
||||
}
|
||||
|
||||
this.setState({ tagResults: results });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { name, style, type, selectedTags = [] } = this.props;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TextInput
|
||||
style={tagStyle.searchInput}
|
||||
placeholder={'Search for more tags'}
|
||||
underlineColorAndroid={Colors.NextLbryGreen}
|
||||
value={this.state.tag}
|
||||
numberOfLines={1}
|
||||
onChangeText={this.handleTagChange}
|
||||
/>
|
||||
<View style={tagStyle.tagResultsList}>
|
||||
{this.state.tagResults.map(tag => (
|
||||
<Tag key={tag} name={tag} style={tagStyle.tag} type="add" onAddPress={name => this.onAddTagPress(name)} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
//import { selectClaimedRewardsByTransactionId } from 'redux/selectors/rewards';
|
||||
import { selectAllMyClaimsByOutpoint } from 'lbry-redux';
|
||||
import TransactionList from './view';
|
||||
|
||||
const select = state => ({
|
||||
//rewards: selectClaimedRewardsByTransactionId(state),
|
||||
myClaims: selectAllMyClaimsByOutpoint(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
null
|
||||
)(TransactionList);
|
|
@ -1,61 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Text, View, Linking } from 'react-native';
|
||||
import { buildURI, formatCredits } from 'lbry-redux';
|
||||
import { navigateToUri } from '../../../utils/helper';
|
||||
import Link from '../../link';
|
||||
import moment from 'moment';
|
||||
import transactionListStyle from '../../../styles/transactionList';
|
||||
|
||||
class TransactionListItem extends React.PureComponent {
|
||||
capitalize(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { transaction, navigation } = this.props;
|
||||
const { amount, claim_id: claimId, claim_name: name, date, fee, txid, type } = transaction;
|
||||
|
||||
return (
|
||||
<View style={transactionListStyle.listItem}>
|
||||
<View style={[transactionListStyle.row, transactionListStyle.topRow]}>
|
||||
<View style={transactionListStyle.col}>
|
||||
<Text style={transactionListStyle.text}>{this.capitalize(type)}</Text>
|
||||
{name && claimId && (
|
||||
<Link
|
||||
style={transactionListStyle.link}
|
||||
onPress={() => navigateToUri(navigation, buildURI({ claimName: name, claimId }))}
|
||||
text={name}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View style={transactionListStyle.col}>
|
||||
<Text style={[transactionListStyle.amount, transactionListStyle.text]}>{formatCredits(amount, 8)}</Text>
|
||||
{fee !== 0 && (
|
||||
<Text style={[transactionListStyle.amount, transactionListStyle.text]}>fee {formatCredits(fee, 8)}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View style={transactionListStyle.row}>
|
||||
<View style={transactionListStyle.col}>
|
||||
<Link
|
||||
style={transactionListStyle.smallLink}
|
||||
text={txid.substring(0, 8)}
|
||||
href={`https://explorer.lbry.com/tx/${txid}`}
|
||||
error={'The transaction URL could not be opened'}
|
||||
/>
|
||||
</View>
|
||||
<View style={transactionListStyle.col}>
|
||||
{date ? (
|
||||
<Text style={transactionListStyle.smallText}>{moment(date).format('MMM D')}</Text>
|
||||
) : (
|
||||
<Text style={transactionListStyle.smallText}>Pending</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionListItem;
|
|
@ -1,70 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import TransactionListItem from './internal/transaction-list-item';
|
||||
import transactionListStyle from '../../styles/transactionList';
|
||||
|
||||
export type Transaction = {
|
||||
amount: number,
|
||||
claim_id: string,
|
||||
claim_name: string,
|
||||
fee: number,
|
||||
nout: number,
|
||||
txid: string,
|
||||
type: string,
|
||||
date: Date,
|
||||
};
|
||||
|
||||
class TransactionList extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filter: 'all',
|
||||
};
|
||||
|
||||
(this: any).handleFilterChanged = this.handleFilterChanged.bind(this);
|
||||
(this: any).filterTransaction = this.filterTransaction.bind(this);
|
||||
}
|
||||
|
||||
handleFilterChanged(event: React.SyntheticInputEvent<*>) {
|
||||
this.setState({
|
||||
filter: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
filterTransaction(transaction: Transaction) {
|
||||
const { filter } = this.state;
|
||||
|
||||
return filter === 'all' || filter === transaction.type;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { emptyMessage, rewards, transactions, navigation } = this.props;
|
||||
const { filter } = this.state;
|
||||
const transactionList = transactions.filter(this.filterTransaction);
|
||||
|
||||
return (
|
||||
<View>
|
||||
{!transactionList.length && (
|
||||
<Text style={transactionListStyle.noTransactions}>{emptyMessage || 'No transactions to list.'}</Text>
|
||||
)}
|
||||
|
||||
{!!transactionList.length && (
|
||||
<View>
|
||||
{transactionList.map(t => (
|
||||
<TransactionListItem
|
||||
key={`${t.txid}:${t.nout}`}
|
||||
transaction={t}
|
||||
navigation={navigation}
|
||||
reward={rewards && rewards[t.txid]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionList;
|
|
@ -1,23 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchTransactions,
|
||||
selectRecentTransactions,
|
||||
selectHasTransactions,
|
||||
selectIsFetchingTransactions,
|
||||
} from 'lbry-redux';
|
||||
import TransactionListRecent from './view';
|
||||
|
||||
const select = state => ({
|
||||
fetchingTransactions: selectIsFetchingTransactions(state),
|
||||
transactions: selectRecentTransactions(state),
|
||||
hasTransactions: selectHasTransactions(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchTransactions: () => dispatch(doFetchTransactions()),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(TransactionListRecent);
|
|
@ -1,45 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
//import BusyIndicator from 'component/common/busy-indicator';
|
||||
import { Text, View } from 'react-native';
|
||||
import Button from '../button';
|
||||
import Link from '../link';
|
||||
import TransactionList from '../transactionList';
|
||||
import type { Transaction } from '../transactionList/view';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
fetchTransactions: () => void,
|
||||
fetchingTransactions: boolean,
|
||||
hasTransactions: boolean,
|
||||
transactions: Array<Transaction>,
|
||||
};
|
||||
|
||||
class TransactionListRecent extends React.PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchTransactions();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fetchingTransactions, hasTransactions, transactions, navigation } = this.props;
|
||||
|
||||
return (
|
||||
<View style={walletStyle.transactionsCard}>
|
||||
<View style={[walletStyle.row, walletStyle.transactionsHeader]}>
|
||||
<Text style={walletStyle.transactionsTitle}>Recent Transactions</Text>
|
||||
<Link style={walletStyle.link} navigation={navigation} text={'View All'} href={'#TransactionHistory'} />
|
||||
</View>
|
||||
{fetchingTransactions && <Text style={walletStyle.infoText}>Fetching transactions...</Text>}
|
||||
{!fetchingTransactions && (
|
||||
<TransactionList
|
||||
navigation={navigation}
|
||||
transactions={transactions}
|
||||
emptyMessage={"Looks like you don't have any recent transactions."}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionListRecent;
|
|
@ -1,29 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doUpdateSearchQuery,
|
||||
selectSearchState as selectSearch,
|
||||
selectSearchValue,
|
||||
selectSearchSuggestions,
|
||||
} from 'lbry-redux';
|
||||
import { selectCurrentRoute } from 'redux/selectors/drawer';
|
||||
import UriBar from './view';
|
||||
|
||||
const select = state => {
|
||||
const { ...searchState } = selectSearch(state);
|
||||
|
||||
return {
|
||||
...searchState,
|
||||
query: selectSearchValue(state),
|
||||
currentRoute: selectCurrentRoute(state),
|
||||
suggestions: selectSearchSuggestions(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(UriBar);
|
|
@ -1,47 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { SEARCH_TYPES, normalizeURI } from 'lbry-redux';
|
||||
import { Text, TouchableOpacity, View } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import uriBarStyle from '../../../styles/uriBar';
|
||||
|
||||
class UriBarItem extends React.PureComponent {
|
||||
render() {
|
||||
const { item, onPress } = this.props;
|
||||
const { shorthand, type, value } = item;
|
||||
|
||||
let icon;
|
||||
switch (type) {
|
||||
case SEARCH_TYPES.CHANNEL:
|
||||
icon = <Icon name="at" size={18} />;
|
||||
break;
|
||||
|
||||
case SEARCH_TYPES.SEARCH:
|
||||
icon = <Icon name="search" size={18} />;
|
||||
break;
|
||||
|
||||
case SEARCH_TYPES.FILE:
|
||||
default:
|
||||
icon = <Icon name="file" size={18} />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={uriBarStyle.item} onPress={onPress}>
|
||||
{icon}
|
||||
<View style={uriBarStyle.itemContent}>
|
||||
<Text style={uriBarStyle.itemText} numberOfLines={1}>
|
||||
{shorthand || value} - {type === SEARCH_TYPES.SEARCH ? 'Search' : value}
|
||||
</Text>
|
||||
<Text style={uriBarStyle.itemDesc} numberOfLines={1}>
|
||||
{type === SEARCH_TYPES.SEARCH && `Search for '${value}'`}
|
||||
{type === SEARCH_TYPES.CHANNEL && `View the @${shorthand} channel`}
|
||||
{type === SEARCH_TYPES.FILE && `View content at ${value}`}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UriBarItem;
|
|
@ -1,203 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { SEARCH_TYPES, isNameValid, isURIValid, normalizeURI } from 'lbry-redux';
|
||||
import { FlatList, Keyboard, TextInput, View } from 'react-native';
|
||||
import { navigateToUri } from 'utils/helper';
|
||||
import Constants from 'constants';
|
||||
import UriBarItem from './internal/uri-bar-item';
|
||||
import NavigationButton from 'component/navigationButton';
|
||||
import discoverStyle from 'styles/discover';
|
||||
import uriBarStyle from 'styles/uriBar';
|
||||
|
||||
class UriBar extends React.PureComponent {
|
||||
static INPUT_TIMEOUT = 2500; // 2.5 seconds
|
||||
|
||||
textInput = null;
|
||||
|
||||
keyboardDidHideListener = null;
|
||||
|
||||
componentDidMount() {
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
|
||||
this.setSelection();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.keyboardDidHideListener) {
|
||||
this.keyboardDidHideListener.remove();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { currentRoute, query } = nextProps;
|
||||
const { currentRoute: prevRoute } = this.props;
|
||||
|
||||
if (Constants.DRAWER_ROUTE_SEARCH === currentRoute && currentRoute !== prevRoute) {
|
||||
this.setState({ currentValue: query, inputText: query });
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
changeTextTimeout: null,
|
||||
currentValue: null,
|
||||
inputText: null,
|
||||
focused: false,
|
||||
// TODO: Add a setting to enable / disable direct search?
|
||||
directSearch: true,
|
||||
};
|
||||
}
|
||||
|
||||
handleChangeText = text => {
|
||||
const newValue = text ? text : '';
|
||||
clearTimeout(this.state.changeTextTimeout);
|
||||
const { updateSearchQuery, onSearchSubmitted, navigation } = this.props;
|
||||
|
||||
let timeout = setTimeout(() => {
|
||||
if (text.trim().length === 0) {
|
||||
// don't do anything if the text is empty
|
||||
return;
|
||||
}
|
||||
|
||||
updateSearchQuery(text);
|
||||
|
||||
if (!text.startsWith('lbry://')) {
|
||||
// not a URI input, so this is a search, perform a direct search
|
||||
if (onSearchSubmitted) {
|
||||
onSearchSubmitted(text);
|
||||
} else {
|
||||
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: text } });
|
||||
}
|
||||
}
|
||||
}, UriBar.INPUT_TIMEOUT);
|
||||
this.setState({ inputText: newValue, currentValue: newValue, changeTextTimeout: timeout });
|
||||
};
|
||||
|
||||
handleItemPress = item => {
|
||||
const { navigation, onSearchSubmitted, updateSearchQuery } = this.props;
|
||||
const { type, value } = item;
|
||||
|
||||
Keyboard.dismiss();
|
||||
|
||||
if (SEARCH_TYPES.SEARCH === type) {
|
||||
this.setState({ currentValue: value });
|
||||
updateSearchQuery(value);
|
||||
|
||||
if (onSearchSubmitted) {
|
||||
onSearchSubmitted(value);
|
||||
return;
|
||||
}
|
||||
|
||||
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: value } });
|
||||
} else {
|
||||
const uri = normalizeURI(value);
|
||||
navigateToUri(navigation, uri);
|
||||
}
|
||||
};
|
||||
|
||||
_keyboardDidHide = () => {
|
||||
if (this.textInput) {
|
||||
this.textInput.blur();
|
||||
}
|
||||
this.setState({ focused: false });
|
||||
};
|
||||
|
||||
setSelection() {
|
||||
if (this.textInput) {
|
||||
this.textInput.setNativeProps({ selection: { start: 0, end: 0 } });
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmitEditing = () => {
|
||||
const { navigation, onSearchSubmitted, updateSearchQuery } = this.props;
|
||||
if (this.state.inputText) {
|
||||
let inputText = this.state.inputText;
|
||||
if (inputText.startsWith('lbry://') && isURIValid(inputText)) {
|
||||
// if it's a URI (lbry://...), open the file page
|
||||
const uri = normalizeURI(inputText);
|
||||
navigateToUri(navigation, uri);
|
||||
} else {
|
||||
updateSearchQuery(inputText);
|
||||
// Not a URI, default to a search request
|
||||
if (onSearchSubmitted) {
|
||||
// Only the search page sets the onSearchSubmitted prop, so call this prop if set
|
||||
onSearchSubmitted(inputText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the search page with the query populated
|
||||
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: inputText } });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onSearchPageBlurred() {
|
||||
this.setState({ currenValueSet: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { navigation, suggestions, query, value } = this.props;
|
||||
if (this.state.currentValue === null) {
|
||||
this.setState({ currentValue: value });
|
||||
}
|
||||
|
||||
let style = [uriBarStyle.overlay];
|
||||
|
||||
// TODO: Add optional setting to enable URI / search bar suggestions
|
||||
/*if (this.state.focused) { style.push(uriBarStyle.inFocus); }*/
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<View style={uriBarStyle.uriContainer}>
|
||||
<NavigationButton
|
||||
name="bars"
|
||||
size={24}
|
||||
style={uriBarStyle.drawerMenuButton}
|
||||
iconStyle={discoverStyle.drawerHamburger}
|
||||
onPress={() => navigation.openDrawer()}
|
||||
/>
|
||||
<TextInput
|
||||
ref={ref => {
|
||||
this.textInput = ref;
|
||||
}}
|
||||
style={uriBarStyle.uriText}
|
||||
onLayout={() => {
|
||||
this.setSelection();
|
||||
}}
|
||||
selectTextOnFocus={true}
|
||||
placeholder={'Search movies, music, and more'}
|
||||
underlineColorAndroid={'transparent'}
|
||||
numberOfLines={1}
|
||||
clearButtonMode={'while-editing'}
|
||||
value={this.state.currentValue}
|
||||
returnKeyType={'go'}
|
||||
inlineImageLeft={'baseline_search_black_24'}
|
||||
inlineImagePadding={16}
|
||||
onFocus={() => this.setState({ focused: true })}
|
||||
onBlur={() => {
|
||||
this.setState({ focused: false });
|
||||
this.setSelection();
|
||||
}}
|
||||
onChangeText={this.handleChangeText}
|
||||
onSubmitEditing={this.handleSubmitEditing}
|
||||
/>
|
||||
{this.state.focused && !this.state.directSearch && (
|
||||
<View style={uriBarStyle.suggestions}>
|
||||
<FlatList
|
||||
style={uriBarStyle.suggestionList}
|
||||
data={suggestions}
|
||||
keyboardShouldPersistTaps={'handled'}
|
||||
keyExtractor={(item, value) => item.value}
|
||||
renderItem={({ item }) => (
|
||||
<UriBarItem item={item} navigation={navigation} onPress={() => this.handleItemPress(item)} />
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UriBar;
|
|
@ -1,18 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doCheckAddressIsMine, doGetNewAddress, selectReceiveAddress, selectGettingNewAddress } from 'lbry-redux';
|
||||
import WalletAddress from './view';
|
||||
|
||||
const select = state => ({
|
||||
receiveAddress: selectReceiveAddress(state),
|
||||
gettingNewAddress: selectGettingNewAddress(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
checkAddressIsMine: address => dispatch(doCheckAddressIsMine(address)),
|
||||
getNewAddress: () => dispatch(doGetNewAddress()),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(WalletAddress);
|
|
@ -1,51 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import Address from '../address';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
checkAddressIsMine: string => void,
|
||||
receiveAddress: string,
|
||||
getNewAddress: () => void,
|
||||
gettingNewAddress: boolean,
|
||||
};
|
||||
|
||||
class WalletAddress extends React.PureComponent<Props> {
|
||||
componentWillMount() {
|
||||
const { checkAddressIsMine, receiveAddress, getNewAddress } = this.props;
|
||||
if (!receiveAddress) {
|
||||
getNewAddress();
|
||||
} else {
|
||||
checkAddressIsMine(receiveAddress);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { receiveAddress, getNewAddress, gettingNewAddress } = this.props;
|
||||
|
||||
return (
|
||||
<View style={walletStyle.card}>
|
||||
<Text style={walletStyle.title}>Receive Credits</Text>
|
||||
<Text style={[walletStyle.text, walletStyle.bottomMarginMedium]}>
|
||||
Use this wallet address to receive credits sent by another user (or yourself).
|
||||
</Text>
|
||||
<Address address={receiveAddress} style={walletStyle.bottomMarginSmall} />
|
||||
<Button
|
||||
style={[walletStyle.button, walletStyle.bottomMarginLarge]}
|
||||
icon={'sync'}
|
||||
text={'Get new address'}
|
||||
onPress={getNewAddress}
|
||||
disabled={gettingNewAddress}
|
||||
/>
|
||||
<Text style={walletStyle.smallText}>
|
||||
You can generate a new address at any time, and any previous addresses will continue to work. Using multiple
|
||||
addresses can be helpful for keeping track of incoming payments from multiple sources.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletAddress;
|
|
@ -1,12 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectBalance } from 'lbry-redux';
|
||||
import WalletBalance from './view';
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
null
|
||||
)(WalletBalance);
|
|
@ -1,29 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Image, Text, View } from 'react-native';
|
||||
import { Lbry, formatCredits } from 'lbry-redux';
|
||||
import Address from 'component/address';
|
||||
import Button from 'component/button';
|
||||
import walletStyle from 'styles/wallet';
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
};
|
||||
|
||||
class WalletBalance extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { balance } = this.props;
|
||||
return (
|
||||
<View style={walletStyle.balanceCard}>
|
||||
<Image style={walletStyle.balanceBackground} resizeMode={'cover'} source={require('../../assets/stripe.png')} />
|
||||
<Text style={walletStyle.balanceTitle}>Balance</Text>
|
||||
<Text style={walletStyle.balanceCaption}>You currently have</Text>
|
||||
<Text style={walletStyle.balance}>
|
||||
{(balance || balance === 0) && formatCredits(parseFloat(balance), 2) + ' LBC'}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletBalance;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue