initial commit
This commit is contained in:
commit
e3f66e4fa6
181 changed files with 25033 additions and 0 deletions
9
.babelrc
Normal file
9
.babelrc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"presets": ["module:metro-react-native-babel-preset"],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
["module-resolver", {
|
||||
root: ["./src"],
|
||||
}],
|
||||
]
|
||||
}
|
36
.eslintrc.json
Normal file
36
.eslintrc.json
Normal file
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
node_modules/
|
||||
*.log
|
||||
.vagrant
|
||||
|
6
.lintstagedrc.json
Normal file
6
.lintstagedrc.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"linters": {
|
||||
"src/**/*.{js,json}": ["prettier --write", "git add"],
|
||||
"src/**/*.js": ["eslint --fix", "git add"]
|
||||
}
|
||||
}
|
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 120,
|
||||
"singleQuote": true
|
||||
}
|
10
LICENSE
Normal file
10
LICENSE
Normal file
|
@ -0,0 +1,10 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017-2018 LBRY Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
30
README.md
Normal file
30
README.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
# LBRY Android
|
||||
[![pipeline status](https://ci.lbry.tech/lbry/lbry-android/badges/master/pipeline.svg)](https://ci.lbry.tech/lbry/lbry-android/commits/master)
|
||||
|
||||
An Android browser and wallet for the [LBRY](https://lbry.com) network. This app bundles [lbrynet-daemon](https://github.com/lbryio/lbry) as a background service with a UI layer built with React Native. The APK is built using buildozer and the Gradle build tool.
|
||||
|
||||
<img src="https://spee.ch/8/lbry-android.png" alt="LBRY Android Screenshot" width="384px" />
|
||||
|
||||
## Installation
|
||||
The minimum supported Android version is 5.0 Lollipop. There are two ways to install:
|
||||
|
||||
1. Via the Google Play Store. Anyone can join the [open beta](https://play.google.com/apps/testing/io.lbry.browser) in order to install the app from the Play Store.
|
||||
1. Direct APK install available at [http://build.lbry.io/android/latest.apk](http://build.lbry.io/android/latest.apk). You will need to enable installation from third-party sources on your device in order to install from this source.
|
||||
|
||||
## Usage
|
||||
The app can be launched by opening **LBRY Browser** from the device's app drawer or via the shortcut on the home screen if that was created upon installation.
|
||||
|
||||
## Running from Source
|
||||
The app is built from source via [Buildozer](https://github.com/kivy/buildozer). After cloning the repository, copy `buildozer.spec.sample` to `buildozer.spec` and modify this file as necessary for your environment. Please see [BUILD.md](BUILD.md) for detailed build instructions.
|
||||
|
||||
## Contributing
|
||||
Contributions to this project are welcome, encouraged, and compensated. For more details, see https://lbry.io/faq/contributing
|
||||
|
||||
## License
|
||||
This project is MIT licensed. For the full license, see [LICENSE](LICENSE).
|
||||
|
||||
## Security
|
||||
We take security seriously. Please contact security@lbry.com regarding any security issues. Our PGP key is [here](https://keybase.io/lbry/key.asc) if you need it.
|
||||
|
||||
## Contact
|
||||
The primary contact for this project is [@akinwale](https://github.com/akinwale) (akinwale@lbry.com)
|
2
bundle.sh
Executable file
2
bundle.sh
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/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/
|
3
index.js
Normal file
3
index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import LBRYApp from './src/index';
|
||||
|
||||
export default LBRYApp;
|
9781
package-lock.json
generated
Normal file
9781
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
65
package.json
Normal file
65
package.json
Normal file
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
BIN
src/assets/default_avatar.jpg
Normal file
BIN
src/assets/default_avatar.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
BIN
src/assets/default_channel_cover.png
Normal file
BIN
src/assets/default_channel_cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 354 KiB |
BIN
src/assets/stripe@2x.png
Normal file
BIN
src/assets/stripe@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
428
src/component/AppNavigator.js
Normal file
428
src/component/AppNavigator.js
Normal file
|
@ -0,0 +1,428 @@
|
|||
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);
|
10
src/component/address/index.js
Normal file
10
src/component/address/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import Address from './view';
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
{
|
||||
doToast,
|
||||
}
|
||||
)(Address);
|
34
src/component/address/view.js
Normal file
34
src/component/address/view.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
// @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>
|
||||
);
|
||||
}
|
||||
}
|
4
src/component/button/index.js
Normal file
4
src/component/button/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Button from './view';
|
||||
|
||||
export default connect()(Button);
|
57
src/component/button/view.js
Normal file
57
src/component/button/view.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
7
src/component/categoryList/index.js
Normal file
7
src/component/categoryList/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import CategoryList from './view';
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
null
|
||||
)(CategoryList);
|
39
src/component/categoryList/view.js
Normal file
39
src/component/categoryList/view.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
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;
|
27
src/component/channelSelector/index.js
Normal file
27
src/component/channelSelector/index.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
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);
|
241
src/component/channelSelector/view.js
Normal file
241
src/component/channelSelector/view.js
Normal file
|
@ -0,0 +1,241 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
29
src/component/customRewardCard/index.js
Normal file
29
src/component/customRewardCard/index.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
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);
|
96
src/component/customRewardCard/view.js
Normal file
96
src/component/customRewardCard/view.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
// @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;
|
16
src/component/dateTime/index.js
Normal file
16
src/component/dateTime/index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
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);
|
55
src/component/dateTime/view.js
Normal file
55
src/component/dateTime/view.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
// @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;
|
4
src/component/drawerContent/index.js
Normal file
4
src/component/drawerContent/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import DrawerContent from './view';
|
||||
|
||||
export default connect()(DrawerContent);
|
38
src/component/drawerContent/view.js
Normal file
38
src/component/drawerContent/view.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
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;
|
28
src/component/fileDownloadButton/index.js
Normal file
28
src/component/fileDownloadButton/index.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
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);
|
110
src/component/fileDownloadButton/view.js
Normal file
110
src/component/fileDownloadButton/view.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
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;
|
35
src/component/fileItem/index.js
Normal file
35
src/component/fileItem/index.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
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);
|
132
src/component/fileItem/view.js
Normal file
132
src/component/fileItem/view.js
Normal file
|
@ -0,0 +1,132 @@
|
|||
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;
|
10
src/component/fileItemMedia/index.js
Normal file
10
src/component/fileItemMedia/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import FileItemMedia from './view';
|
||||
|
||||
const select = state => ({});
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileItemMedia);
|
112
src/component/fileItemMedia/view.js
Normal file
112
src/component/fileItemMedia/view.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
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;
|
14
src/component/fileList/index.js
Normal file
14
src/component/fileList/index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
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);
|
191
src/component/fileList/view.js
Normal file
191
src/component/fileList/view.js
Normal file
|
@ -0,0 +1,191 @@
|
|||
// @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;
|
32
src/component/fileListItem/index.js
Normal file
32
src/component/fileListItem/index.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
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);
|
159
src/component/fileListItem/view.js
Normal file
159
src/component/fileListItem/view.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
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;
|
19
src/component/filePrice/index.js
Normal file
19
src/component/filePrice/index.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
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);
|
119
src/component/filePrice/view.js
Normal file
119
src/component/filePrice/view.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
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;
|
4
src/component/fileRewardsDriver/index.js
Normal file
4
src/component/fileRewardsDriver/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import FileRewardsDriver from './view';
|
||||
|
||||
export default connect()(FileRewardsDriver);
|
19
src/component/fileRewardsDriver/view.js
Normal file
19
src/component/fileRewardsDriver/view.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
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;
|
17
src/component/floatingWalletBalance/index.js
Normal file
17
src/component/floatingWalletBalance/index.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
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);
|
44
src/component/floatingWalletBalance/view.js
Normal file
44
src/component/floatingWalletBalance/view.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
// @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;
|
12
src/component/link/index.js
Normal file
12
src/component/link/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
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);
|
67
src/component/link/view.js
Normal file
67
src/component/link/view.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
21
src/component/mediaPlayer/index.js
Normal file
21
src/component/mediaPlayer/index.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
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);
|
503
src/component/mediaPlayer/view.js
Normal file
503
src/component/mediaPlayer/view.js
Normal file
|
@ -0,0 +1,503 @@
|
|||
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;
|
4
src/component/navigationButton/index.js
Normal file
4
src/component/navigationButton/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import NavigationButton from './view';
|
||||
|
||||
export default connect()(NavigationButton);
|
17
src/component/navigationButton/view.js
Normal file
17
src/component/navigationButton/view.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
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;
|
9
src/component/nsfwOverlay/index.js
Normal file
9
src/component/nsfwOverlay/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import NsfwOverlay from './view';
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
perform
|
||||
)(NsfwOverlay);
|
17
src/component/nsfwOverlay/view.js
Normal file
17
src/component/nsfwOverlay/view.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
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;
|
9
src/component/pageHeader/index.js
Normal file
9
src/component/pageHeader/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import PageHeader from './view';
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
perform
|
||||
)(PageHeader);
|
39
src/component/pageHeader/view.js
Normal file
39
src/component/pageHeader/view.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
// 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;
|
4
src/component/progressBar/index.js
Normal file
4
src/component/progressBar/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ProgressBar from './view';
|
||||
|
||||
export default connect()(ProgressBar);
|
56
src/component/progressBar/view.js
Normal file
56
src/component/progressBar/view.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
// @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;
|
4
src/component/publishRewardsDriver/index.js
Normal file
4
src/component/publishRewardsDriver/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import PublishRewardsDriver from './view';
|
||||
|
||||
export default connect()(PublishRewardsDriver);
|
20
src/component/publishRewardsDriver/view.js
Normal file
20
src/component/publishRewardsDriver/view.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
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;
|
25
src/component/relatedContent/index.js
Normal file
25
src/component/relatedContent/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
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);
|
67
src/component/relatedContent/view.js
Normal file
67
src/component/relatedContent/view.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
32
src/component/rewardCard/index.js
Normal file
32
src/component/rewardCard/index.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
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);
|
112
src/component/rewardCard/view.js
Normal file
112
src/component/rewardCard/view.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
// @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;
|
22
src/component/rewardEnrolment/index.js
Normal file
22
src/component/rewardEnrolment/index.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
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);
|
53
src/component/rewardEnrolment/view.js
Normal file
53
src/component/rewardEnrolment/view.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
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;
|
20
src/component/rewardSummary/index.js
Normal file
20
src/component/rewardSummary/index.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
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);
|
82
src/component/rewardSummary/view.js
Normal file
82
src/component/rewardSummary/view.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
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;
|
19
src/component/searchInput/index.js
Normal file
19
src/component/searchInput/index.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
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);
|
41
src/component/searchInput/view.js
Normal file
41
src/component/searchInput/view.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
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;
|
14
src/component/searchRightHeaderIcon/index.js
Normal file
14
src/component/searchRightHeaderIcon/index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
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);
|
19
src/component/searchRightHeaderIcon/view.js
Normal file
19
src/component/searchRightHeaderIcon/view.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
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;
|
4
src/component/storageStatsCard/index.js
Normal file
4
src/component/storageStatsCard/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import StorageStatsCard from './view';
|
||||
|
||||
export default connect()(StorageStatsCard);
|
133
src/component/storageStatsCard/view.js
Normal file
133
src/component/storageStatsCard/view.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
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;
|
18
src/component/subscribeButton/index.js
Normal file
18
src/component/subscribeButton/index.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
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);
|
44
src/component/subscribeButton/view.js
Normal file
44
src/component/subscribeButton/view.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
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;
|
25
src/component/subscribeNotificationButton/index.js
Normal file
25
src/component/subscribeNotificationButton/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
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);
|
56
src/component/subscribeNotificationButton/view.js
Normal file
56
src/component/subscribeNotificationButton/view.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
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;
|
25
src/component/suggestedSubscriptionItem/index.js
Normal file
25
src/component/suggestedSubscriptionItem/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
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);
|
77
src/component/suggestedSubscriptionItem/view.js
Normal file
77
src/component/suggestedSubscriptionItem/view.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
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;
|
13
src/component/suggestedSubscriptions/index.js
Normal file
13
src/component/suggestedSubscriptions/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
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);
|
54
src/component/suggestedSubscriptions/view.js
Normal file
54
src/component/suggestedSubscriptions/view.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
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;
|
4
src/component/tag/index.js
Normal file
4
src/component/tag/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Tag from './view';
|
||||
|
||||
export default connect()(Tag);
|
55
src/component/tag/view.js
Normal file
55
src/component/tag/view.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
12
src/component/tagSearch/index.js
Normal file
12
src/component/tagSearch/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
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);
|
81
src/component/tagSearch/view.js
Normal file
81
src/component/tagSearch/view.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
14
src/component/transactionList/index.js
Normal file
14
src/component/transactionList/index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
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);
|
|
@ -0,0 +1,61 @@
|
|||
// @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;
|
70
src/component/transactionList/view.js
Normal file
70
src/component/transactionList/view.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
// @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;
|
23
src/component/transactionListRecent/index.js
Normal file
23
src/component/transactionListRecent/index.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
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);
|
45
src/component/transactionListRecent/view.js
Normal file
45
src/component/transactionListRecent/view.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
// @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;
|
29
src/component/uriBar/index.js
Normal file
29
src/component/uriBar/index.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
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);
|
47
src/component/uriBar/internal/uri-bar-item.js
Normal file
47
src/component/uriBar/internal/uri-bar-item.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
// @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;
|
203
src/component/uriBar/view.js
Normal file
203
src/component/uriBar/view.js
Normal file
|
@ -0,0 +1,203 @@
|
|||
// @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;
|
18
src/component/walletAddress/index.js
Normal file
18
src/component/walletAddress/index.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
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);
|
51
src/component/walletAddress/view.js
Normal file
51
src/component/walletAddress/view.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
// @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;
|
12
src/component/walletBalance/index.js
Normal file
12
src/component/walletBalance/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
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);
|
29
src/component/walletBalance/view.js
Normal file
29
src/component/walletBalance/view.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
// @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;
|
4
src/component/walletRewardsDriver/index.js
Normal file
4
src/component/walletRewardsDriver/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import WalletRewardsDriver from './view';
|
||||
|
||||
export default connect()(WalletRewardsDriver);
|
19
src/component/walletRewardsDriver/view.js
Normal file
19
src/component/walletRewardsDriver/view.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import { Text, TouchableOpacity } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import walletStyle from 'styles/wallet';
|
||||
|
||||
class WalletRewardsDriver extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={walletStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}>
|
||||
<Icon name="award" size={16} style={walletStyle.rewardIcon} />
|
||||
<Text style={walletStyle.rewardDriverText}>Earn credits while using the LBRY app.</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletRewardsDriver;
|
25
src/component/walletSend/index.js
Normal file
25
src/component/walletSend/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doToast,
|
||||
doSendDraftTransaction,
|
||||
selectDraftTransaction,
|
||||
selectDraftTransactionError,
|
||||
selectBalance,
|
||||
} from 'lbry-redux';
|
||||
import WalletSend from './view';
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
draftTransaction: selectDraftTransaction(state),
|
||||
transactionError: selectDraftTransactionError(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
sendToAddress: (address, amount) => dispatch(doSendDraftTransaction(address, amount)),
|
||||
notify: data => dispatch(doToast(data)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(WalletSend);
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue