React Native codebase as a submodule (#604)
This commit is contained in:
parent
8459d10dc7
commit
c977d2ed86
185 changed files with 13 additions and 24994 deletions
|
@ -8,6 +8,8 @@ build apk:
|
||||||
image: lbry/android-base:latest
|
image: lbry/android-base:latest
|
||||||
before_script:
|
before_script:
|
||||||
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
|
- 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:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- bin/browser-*-release.apk
|
- 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
|
#### Build and Deploy
|
||||||
Run `npm install -g react-native-cli` to install React Native CLI tools.
|
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 `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.
|
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
|
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
|
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
|
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 ..
|
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/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
|
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
|
### 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
|
git clone https://github.com/lbryio/lbry-android
|
||||||
cd lbry-android
|
cd lbry-android
|
||||||
|
git submodule update --init --recursive
|
||||||
cp buildozer.spec.sample buildozer.spec
|
cp buildozer.spec.sample buildozer.spec
|
||||||
cp p4a/pythonforandroid/bootstraps/lbry/templates/google-services.sample.json p4a/pythonforandroid/bootstraps/lbry/templates/google-services.json
|
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 Width: | Height: | Size: 29 KiB |
Binary file not shown.
Before Width: | Height: | Size: 354 KiB |
Binary file not shown.
Before Width: | Height: | 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…
Reference in a new issue