React Native codebase as a submodule #604
185 changed files with 13 additions and 24994 deletions
|
@ -8,6 +8,8 @@ build apk:
|
|||
image: lbry/android-base:latest
|
||||
before_script:
|
||||
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
|
||||
- git submodule sync --recursive
|
||||
- git submodule update --init --recursive
|
||||
artifacts:
|
||||
paths:
|
||||
- bin/browser-*-release.apk
|
||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "app"]
|
||||
path = app
|
||||
url = https://github.com/lbryio/lbry-react-native
|
2
BUILD.md
2
BUILD.md
|
@ -118,6 +118,8 @@ echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platfo
|
|||
#### Build and Deploy
|
||||
Run `npm install -g react-native-cli` to install React Native CLI tools.
|
||||
|
||||
Initialise git submodules by running `git submodule update --init --recursive` in the `lbry-android` folder.
|
||||
|
||||
Run `npm i` in the `lbry-android/app` folder to install the necessary modules required by the React Native user interface, and then run `./bundle.sh`.
|
||||
|
||||
Run `./build.sh` in `lbry-android` to build the APK. The output can be found in the `bin` subdirectory.
|
||||
|
|
|
@ -24,7 +24,9 @@ tar -xvf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.build
|
|||
rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
|
||||
ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
|
||||
git clone https://github.com/lbryio/lbry-android
|
||||
cd lbry-android;cp buildozer.spec.sample buildozer.spec
|
||||
cd lbry-android
|
||||
git submodule update --init --recursive
|
||||
cp buildozer.spec.sample buildozer.spec
|
||||
cd app;npm i;cd ..
|
||||
cp scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
|
||||
cp scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h
|
||||
|
|
|
@ -72,10 +72,11 @@ wget 'https://www.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -
|
|||
```
|
||||
|
||||
### Step 7 of 10
|
||||
Clone the lbryio/lbry-android git repository and create your `buildozer.spec` and `google-services.json` files. The provided `buildozer.spec.sample` contains defaults provided you followed steps 1 through 5 exactly as described. You can also customise the spec file if you want to. The `google-services.sample.json` can be used to ensure the build completes successfully.
|
||||
Clone the lbryio/lbry-android git repository, initialise submodules and create your `buildozer.spec` and `google-services.json` files. The provided `buildozer.spec.sample` contains defaults provided you followed steps 1 through 5 exactly as described. You can also customise the spec file if you want to. The `google-services.sample.json` can be used to ensure the build completes successfully.
|
||||
```
|
||||
git clone https://github.com/lbryio/lbry-android
|
||||
cd lbry-android
|
||||
git submodule update --init --recursive
|
||||
cp buildozer.spec.sample buildozer.spec
|
||||
cp p4a/pythonforandroid/bootstraps/lbry/templates/google-services.sample.json p4a/pythonforandroid/bootstraps/lbry/templates/google-services.json
|
||||
```
|
||||
|
|
1
app
Submodule
1
app
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit e3f66e4fa67867b1e2b80ada480cd05808cad136
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"presets": ["module:metro-react-native-babel-preset"],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
["module-resolver", {
|
||||
root: ["./src"],
|
||||
}],
|
||||
]
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"parser": "babel-eslint",
|
||||
"extends": ["standard", "standard-jsx", "plugin:flowtype/recommended"],
|
||||
"plugins": ["flowtype", "import"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"globals": {
|
||||
"__": true
|
||||
},
|
||||
"rules": {
|
||||
"no-multi-spaces": 0,
|
||||
"new-cap": 0,
|
||||
"prefer-promise-reject-errors": 0,
|
||||
"no-unused-vars": 0,
|
||||
"standard/object-curly-even-spacing": 0,
|
||||
"handle-callback-err": 0,
|
||||
"one-var": 0,
|
||||
"object-curly-spacing": 0,
|
||||
"no-redeclare": 0,
|
||||
"no-return-await": 0,
|
||||
"standard/no-callback-literal": 0,
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"jsx-quotes": ["error", "prefer-double"],
|
||||
"no-use-before-define": 0,
|
||||
"semi": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"omitLastInOneLineBlock": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"linters": {
|
||||
"src/**/*.{js,json}": ["prettier --write", "git add"],
|
||||
"src/**/*.js": ["eslint --fix", "git add"]
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 120,
|
||||
"singleQuote": true
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
react-native bundle --platform android --dev false --entry-file src/index.js --bundle-output ../src/main/assets/index.android.bundle --assets-dest ../src/main/res/
|
|
@ -1,3 +0,0 @@
|
|||
import LBRYApp from './src/index';
|
||||
|
||||
export default LBRYApp;
|
9781
app/package-lock.json
generated
9781
app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,65 +0,0 @@
|
|||
{
|
||||
"name": "LBRYApp",
|
||||
"version": "0.0.1",
|
||||
"private": "true",
|
||||
"scripts": {
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start",
|
||||
"format": "prettier 'src/**/*.{js,json}' --write",
|
||||
"precommit": "lint-staged"
|
||||
},
|
||||
"dependencies": {
|
||||
"base-64": "^0.1.0",
|
||||
"@expo/vector-icons": "^8.1.0",
|
||||
"gfycat-style-urls": "^1.0.3",
|
||||
"lbry-redux": "lbryio/lbry-redux",
|
||||
"lbryinc": "lbryio/lbryinc",
|
||||
"lodash": ">=4.17.11",
|
||||
"merge": ">=1.2.1",
|
||||
"moment": "^2.22.1",
|
||||
"react": "16.8.6",
|
||||
"react-native": "0.59.3",
|
||||
"@react-native-community/async-storage": "^1.2.2",
|
||||
"react-native-camera": "^2.11.0",
|
||||
"react-native-country-picker-modal": "^0.6.2",
|
||||
"react-native-document-picker": "^2.3.0",
|
||||
"react-native-exception-handler": "2.9.0",
|
||||
"react-native-fast-image": "^5.0.3",
|
||||
"react-native-fs": "^2.13.3",
|
||||
"react-native-gesture-handler": "^1.1.0",
|
||||
"react-native-image-zoom-viewer": "^2.2.5",
|
||||
"react-native-password-strength-meter": "^0.0.2",
|
||||
"react-native-phone-input": "lbryio/react-native-phone-input",
|
||||
"react-native-super-grid": "^3.0.4",
|
||||
"react-native-vector-icons": "^6.4.2",
|
||||
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
|
||||
"react-navigation": "^3.11.0",
|
||||
"react-navigation-redux-helpers": "^3.0.2",
|
||||
"react-redux": "^5.0.3",
|
||||
"redux": "^3.6.0",
|
||||
"redux-persist": "^4.10.2",
|
||||
"redux-persist-filesystem-storage": "^1.3.2",
|
||||
"redux-persist-transform-compress": "^4.2.0",
|
||||
"redux-persist-transform-filter": "0.0.18",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rn-fetch-blob": "^0.10.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.3",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react-native": "5.0.2",
|
||||
"babel-preset-stage-2": "^6.18.0",
|
||||
"babel-plugin-module-resolver": "^3.1.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-config-standard-jsx": "^6.0.2",
|
||||
"eslint-plugin-flowtype": "^2.46.1",
|
||||
"eslint-plugin-import": "^2.17.2",
|
||||
"eslint-plugin-node": "^8.0.1",
|
||||
"eslint-plugin-promise": "^4.1.1",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"flow-babel-webpack-plugin": "^1.1.1",
|
||||
"lint-staged": "^7.0.4",
|
||||
"prettier": "^1.11.1"
|
||||
}
|
||||
}
|
Binary file not shown.
Before 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