React Native codebase as a submodule (#604)

This commit is contained in:
Akinwale Ariwodola 2019-07-09 05:34:46 +01:00 committed by GitHub
parent 8459d10dc7
commit c977d2ed86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
185 changed files with 13 additions and 24994 deletions

View file

@ -8,6 +8,8 @@ build apk:
image: lbry/android-base:latest
before_script:
- export BUILD_VERSION=$(cat $CI_PROJECT_DIR/src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
- git submodule sync --recursive
- git submodule update --init --recursive
artifacts:
paths:
- bin/browser-*-release.apk

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "app"]
path = app
url = https://github.com/lbryio/lbry-react-native

View file

@ -118,6 +118,8 @@ echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platfo
#### Build and Deploy
Run `npm install -g react-native-cli` to install React Native CLI tools.
Initialise git submodules by running `git submodule update --init --recursive` in the `lbry-android` folder.
Run `npm i` in the `lbry-android/app` folder to install the necessary modules required by the React Native user interface, and then run `./bundle.sh`.
Run `./build.sh` in `lbry-android` to build the APK. The output can be found in the `bin` subdirectory.

View file

@ -24,7 +24,9 @@ tar -xvf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.build
rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
git clone https://github.com/lbryio/lbry-android
cd lbry-android;cp buildozer.spec.sample buildozer.spec
cd lbry-android
git submodule update --init --recursive
cp buildozer.spec.sample buildozer.spec
cd app;npm i;cd ..
cp scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
cp scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h

View file

@ -72,10 +72,11 @@ wget 'https://www.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -
```
### Step 7 of 10
Clone the lbryio/lbry-android git repository and create your `buildozer.spec` and `google-services.json` files. The provided `buildozer.spec.sample` contains defaults provided you followed steps 1 through 5 exactly as described. You can also customise the spec file if you want to. The `google-services.sample.json` can be used to ensure the build completes successfully.
Clone the lbryio/lbry-android git repository, initialise submodules and create your `buildozer.spec` and `google-services.json` files. The provided `buildozer.spec.sample` contains defaults provided you followed steps 1 through 5 exactly as described. You can also customise the spec file if you want to. The `google-services.sample.json` can be used to ensure the build completes successfully.
```
git clone https://github.com/lbryio/lbry-android
cd lbry-android
git submodule update --init --recursive
cp buildozer.spec.sample buildozer.spec
cp p4a/pythonforandroid/bootstraps/lbry/templates/google-services.sample.json p4a/pythonforandroid/bootstraps/lbry/templates/google-services.json
```

1
app Submodule

@ -0,0 +1 @@
Subproject commit e3f66e4fa67867b1e2b80ada480cd05808cad136

View file

@ -1,9 +0,0 @@
{
"presets": ["module:metro-react-native-babel-preset"],
"plugins": [
"@babel/plugin-proposal-nullish-coalescing-operator",
["module-resolver", {
root: ["./src"],
}],
]
}

View file

@ -1,36 +0,0 @@
{
"parser": "babel-eslint",
"extends": ["standard", "standard-jsx", "plugin:flowtype/recommended"],
"plugins": ["flowtype", "import"],
"env": {
"browser": true,
"node": true
},
"globals": {
"__": true
},
"rules": {
"no-multi-spaces": 0,
"new-cap": 0,
"prefer-promise-reject-errors": 0,
"no-unused-vars": 0,
"standard/object-curly-even-spacing": 0,
"handle-callback-err": 0,
"one-var": 0,
"object-curly-spacing": 0,
"no-redeclare": 0,
"no-return-await": 0,
"standard/no-callback-literal": 0,
"comma-dangle": ["error", "always-multiline"],
"space-before-function-paren": ["error", "never"],
"jsx-quotes": ["error", "prefer-double"],
"no-use-before-define": 0,
"semi": [
"error",
"always",
{
"omitLastInOneLineBlock": true
}
]
}
}

View file

@ -1,6 +0,0 @@
{
"linters": {
"src/**/*.{js,json}": ["prettier --write", "git add"],
"src/**/*.js": ["eslint --fix", "git add"]
}
}

View file

@ -1,5 +0,0 @@
{
"trailingComma": "es5",
"printWidth": 120,
"singleQuote": true
}

View file

@ -1,2 +0,0 @@
#!/bin/sh
react-native bundle --platform android --dev false --entry-file src/index.js --bundle-output ../src/main/assets/index.android.bundle --assets-dest ../src/main/res/

View file

@ -1,3 +0,0 @@
import LBRYApp from './src/index';
export default LBRYApp;

9781
app/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,65 +0,0 @@
{
"name": "LBRYApp",
"version": "0.0.1",
"private": "true",
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"format": "prettier 'src/**/*.{js,json}' --write",
"precommit": "lint-staged"
},
"dependencies": {
"base-64": "^0.1.0",
"@expo/vector-icons": "^8.1.0",
"gfycat-style-urls": "^1.0.3",
"lbry-redux": "lbryio/lbry-redux",
"lbryinc": "lbryio/lbryinc",
"lodash": ">=4.17.11",
"merge": ">=1.2.1",
"moment": "^2.22.1",
"react": "16.8.6",
"react-native": "0.59.3",
"@react-native-community/async-storage": "^1.2.2",
"react-native-camera": "^2.11.0",
"react-native-country-picker-modal": "^0.6.2",
"react-native-document-picker": "^2.3.0",
"react-native-exception-handler": "2.9.0",
"react-native-fast-image": "^5.0.3",
"react-native-fs": "^2.13.3",
"react-native-gesture-handler": "^1.1.0",
"react-native-image-zoom-viewer": "^2.2.5",
"react-native-password-strength-meter": "^0.0.2",
"react-native-phone-input": "lbryio/react-native-phone-input",
"react-native-super-grid": "^3.0.4",
"react-native-vector-icons": "^6.4.2",
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
"react-navigation": "^3.11.0",
"react-navigation-redux-helpers": "^3.0.2",
"react-redux": "^5.0.3",
"redux": "^3.6.0",
"redux-persist": "^4.10.2",
"redux-persist-filesystem-storage": "^1.3.2",
"redux-persist-transform-compress": "^4.2.0",
"redux-persist-transform-filter": "0.0.18",
"redux-thunk": "^2.3.0",
"rn-fetch-blob": "^0.10.15"
},
"devDependencies": {
"@babel/core": "^7.4.3",
"babel-preset-env": "^1.6.1",
"babel-preset-react-native": "5.0.2",
"babel-preset-stage-2": "^6.18.0",
"babel-plugin-module-resolver": "^3.1.1",
"eslint": "^5.16.0",
"eslint-config-standard": "^12.0.0",
"eslint-config-standard-jsx": "^6.0.2",
"eslint-plugin-flowtype": "^2.46.1",
"eslint-plugin-import": "^2.17.2",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.1.1",
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-standard": "^4.0.0",
"flow-babel-webpack-plugin": "^1.1.1",
"lint-staged": "^7.0.4",
"prettier": "^1.11.1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View file

@ -1,428 +0,0 @@
import React from 'react';
import AboutPage from 'page/about';
import DiscoverPage from 'page/discover';
import DownloadsPage from 'page/downloads';
import DrawerContent from 'component/drawerContent';
import FilePage from 'page/file';
import FirstRunScreen from 'page/firstRun';
import PublishPage from 'page/publish';
import RewardsPage from 'page/rewards';
import TrendingPage from 'page/trending';
import SearchPage from 'page/search';
import SettingsPage from 'page/settings';
import SplashScreen from 'page/splash';
import SubscriptionsPage from 'page/subscriptions';
import TransactionHistoryPage from 'page/transactionHistory';
import VerificationScreen from 'page/verification';
import WalletPage from 'page/wallet';
import { createDrawerNavigator, createStackNavigator, NavigationActions } from 'react-navigation';
import {
createReduxContainer,
createReactNavigationReduxMiddleware,
createNavigationReducer,
} from 'react-navigation-redux-helpers';
import { connect } from 'react-redux';
import { AppState, BackHandler, Linking, NativeModules, TextInput, ToastAndroid } from 'react-native';
import { selectDrawerStack } from 'redux/selectors/drawer';
import { SETTINGS, doDismissToast, doToast, selectToast } from 'lbry-redux';
import {
doGetSync,
doUserCheckEmailVerified,
doUserEmailVerify,
doUserEmailVerifyFailure,
selectEmailToVerify,
selectEmailVerifyIsPending,
selectEmailVerifyErrorMessage,
selectUser,
} from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { decode as atob } from 'base-64';
import { dispatchNavigateBack, dispatchNavigateToUri } from 'utils/helper';
import AsyncStorage from '@react-native-community/async-storage';
import Colors from 'styles/colors';
import Constants from 'constants';
import Icon from 'react-native-vector-icons/FontAwesome5';
import NavigationButton from 'component/navigationButton';
import discoverStyle from 'styles/discover';
import searchStyle from 'styles/search';
import SearchRightHeaderIcon from 'component/searchRightHeaderIcon';
const menuNavigationButton = navigation => (
<NavigationButton
name="bars"
size={24}
style={discoverStyle.drawerMenuButton}
iconStyle={discoverStyle.drawerHamburger}
onPress={() => navigation.openDrawer()}
/>
);
const discoverStack = createStackNavigator(
{
Discover: {
screen: DiscoverPage,
navigationOptions: ({ navigation }) => ({
title: 'Explore',
header: null,
}),
},
File: {
screen: FilePage,
navigationOptions: ({ navigation }) => ({
header: null,
}),
},
Search: {
screen: SearchPage,
navigationOptions: ({ navigation }) => ({
header: null,
}),
},
},
{
headerMode: 'screen',
transitionConfig: () => ({ screenInterpolator: () => null }),
}
);
discoverStack.navigationOptions = ({ navigation }) => {
let drawerLockMode = 'unlocked';
/*if (navigation.state.index > 0) {
drawerLockMode = 'locked-closed';
}*/
return {
drawerLockMode,
};
};
const walletStack = createStackNavigator(
{
Wallet: {
screen: WalletPage,
navigationOptions: ({ navigation }) => ({
title: 'Wallet',
header: null,
}),
},
TransactionHistory: {
screen: TransactionHistoryPage,
navigationOptions: {
title: 'Transaction History',
header: null,
},
},
},
{
headerMode: 'screen',
transitionConfig: () => ({ screenInterpolator: () => null }),
}
);
const drawer = createDrawerNavigator(
{
DiscoverStack: {
screen: discoverStack,
navigationOptions: {
title: 'Explore',
drawerIcon: ({ tintColor }) => <Icon name="home" size={20} style={{ color: tintColor }} />,
},
},
TrendingStack: {
screen: TrendingPage,
navigationOptions: {
title: 'Trending',
drawerIcon: ({ tintColor }) => <Icon name="fire" size={20} style={{ color: tintColor }} />,
},
},
MySubscriptionsStack: {
screen: SubscriptionsPage,
navigationOptions: {
title: 'Subscriptions',
drawerIcon: ({ tintColor }) => <Icon name="heart" solid={true} size={20} style={{ color: tintColor }} />,
},
},
WalletStack: {
screen: walletStack,
navigationOptions: {
title: 'Wallet',
drawerIcon: ({ tintColor }) => <Icon name="wallet" size={20} style={{ color: tintColor }} />,
},
},
Publish: {
screen: PublishPage,
navigationOptions: {
drawerIcon: ({ tintColor }) => <Icon name="upload" size={20} style={{ color: tintColor }} />,
},
},
Rewards: {
screen: RewardsPage,
navigationOptions: {
drawerIcon: ({ tintColor }) => <Icon name="award" size={20} style={{ color: tintColor }} />,
},
},
MyLBRYStack: {
screen: DownloadsPage,
navigationOptions: {
title: 'Library',
drawerIcon: ({ tintColor }) => <Icon name="download" size={20} style={{ color: tintColor }} />,
},
},
Settings: {
screen: SettingsPage,
navigationOptions: {
drawerLockMode: 'locked-closed',
drawerIcon: ({ tintColor }) => <Icon name="cog" size={20} style={{ color: tintColor }} />,
},
},
About: {
screen: AboutPage,
navigationOptions: {
drawerLockMode: 'locked-closed',
drawerIcon: ({ tintColor }) => <Icon name="info" size={20} style={{ color: tintColor }} />,
},
},
},
{
drawerWidth: 300,
headerMode: 'none',
contentComponent: DrawerContent,
contentOptions: {
activeTintColor: Colors.LbryGreen,
labelStyle: discoverStyle.menuText,
},
}
);
const mainStackNavigator = new createStackNavigator(
{
FirstRun: {
screen: FirstRunScreen,
navigationOptions: {
drawerLockMode: 'locked-closed',
},
},
Splash: {
screen: SplashScreen,
navigationOptions: {
drawerLockMode: 'locked-closed',
},
},
Main: {
screen: drawer,
},
Verification: {
screen: VerificationScreen,
navigationOptions: {
drawerLockMode: 'locked-closed',
},
},
},
{
headerMode: 'none',
}
);
export const AppNavigator = mainStackNavigator;
export const navigatorReducer = createNavigationReducer(AppNavigator);
export const reactNavigationMiddleware = createReactNavigationReduxMiddleware(state => state.nav);
const App = createReduxContainer(mainStackNavigator);
const appMapStateToProps = state => ({
state: state.nav,
});
const ReduxAppNavigator = connect(appMapStateToProps)(App);
class AppWithNavigationState extends React.Component {
static supportedDisplayTypes = ['toast'];
constructor() {
super();
this.emailVerifyCheckInterval = null;
this.state = {
emailVerifyDone: false,
verifyPending: false,
};
}
componentWillMount() {
AppState.addEventListener('change', this._handleAppStateChange);
BackHandler.addEventListener(
'hardwareBackPress',
function() {
const { dispatch, nav, drawerStack } = this.props;
// There should be a better way to check this
if (nav.routes.length > 0) {
if (nav.routes[0].routeName === 'Main') {
const mainRoute = nav.routes[0];
if (
mainRoute.index > 0 ||
mainRoute.routes[0].index > 0 /* Discover stack index */ ||
mainRoute.routes[4].index > 0 /* Wallet stack index */ ||
mainRoute.index >= 5 /* Settings and About screens */
) {
dispatchNavigateBack(dispatch, nav, drawerStack);
return true;
}
}
}
return false;
}.bind(this)
);
}
componentDidMount() {
this.emailVerifyCheckInterval = setInterval(() => this.checkEmailVerification(), 5000);
Linking.addEventListener('url', this._handleUrl);
}
checkEmailVerification = () => {
const { dispatch } = this.props;
AsyncStorage.getItem(Constants.KEY_EMAIL_VERIFY_PENDING).then(pending => {
this.setState({ verifyPending: 'true' === pending });
if ('true' === pending) {
dispatch(doUserCheckEmailVerified());
}
});
};
componentWillUnmount() {
AppState.removeEventListener('change', this._handleAppStateChange);
BackHandler.removeEventListener('hardwareBackPress');
Linking.removeEventListener('url', this._handleUrl);
}
componentDidUpdate() {
const { dispatch, user } = this.props;
if (this.state.verifyPending && this.emailVerifyCheckInterval > 0 && user && user.has_verified_email) {
clearInterval(this.emailVerifyCheckInterval);
AsyncStorage.setItem(Constants.KEY_EMAIL_VERIFY_PENDING, 'false');
this.setState({ verifyPending: false });
ToastAndroid.show('Your email address was successfully verified.', ToastAndroid.LONG);
// upon successful email verification, do wallet sync (if password has been set)
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
if (walletPassword && walletPassword.trim().length > 0) {
dispatch(doGetSync(walletPassword));
}
});
}
}
componentWillUpdate(nextProps) {
const { dispatch } = this.props;
const { toast, emailToVerify, emailVerifyPending, emailVerifyErrorMessage, user } = nextProps;
if (toast) {
const { message } = toast;
let currentDisplayType;
if (!currentDisplayType && message) {
// default to toast if no display type set and there is a message specified
currentDisplayType = 'toast';
}
if ('toast' === currentDisplayType) {
ToastAndroid.show(message, ToastAndroid.LONG);
}
dispatch(doDismissToast());
}
if (user && !emailVerifyPending && !this.state.emailVerifyDone && (emailToVerify || emailVerifyErrorMessage)) {
AsyncStorage.getItem(Constants.KEY_SHOULD_VERIFY_EMAIL).then(shouldVerify => {
if ('true' === shouldVerify) {
this.setState({ emailVerifyDone: true });
const message = emailVerifyErrorMessage
? String(emailVerifyErrorMessage)
: 'Your email address was successfully verified.';
if (!emailVerifyErrorMessage) {
AsyncStorage.removeItem(Constants.KEY_FIRST_RUN_EMAIL);
}
AsyncStorage.removeItem(Constants.KEY_SHOULD_VERIFY_EMAIL);
dispatch(doToast({ message }));
}
});
}
}
_handleAppStateChange = nextAppState => {
const { backgroundPlayEnabled, dispatch } = this.props;
// Check if the app was suspended
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
AsyncStorage.getItem('firstLaunchTime').then(start => {
if (start !== null && !isNaN(parseInt(start, 10))) {
// App suspended during first launch?
// If so, this needs to be included as a property when tracking
AsyncStorage.setItem('firstLaunchSuspended', 'true');
}
// Background media
if (backgroundPlayEnabled && NativeModules.BackgroundMedia && window.currentMediaInfo) {
const { title, channel, uri } = window.currentMediaInfo;
NativeModules.BackgroundMedia.showPlaybackNotification(title, channel, uri, false);
}
});
}
if (AppState.currentState && AppState.currentState.match(/active/)) {
if (backgroundPlayEnabled || NativeModules.BackgroundMedia) {
NativeModules.BackgroundMedia.hidePlaybackNotification();
}
}
};
_handleUrl = evt => {
const { dispatch, nav } = this.props;
if (evt.url) {
if (evt.url.startsWith('lbry://?verify=')) {
this.setState({ emailVerifyDone: false });
let verification = {};
try {
verification = JSON.parse(atob(evt.url.substring(15)));
} catch (error) {
console.log(error);
}
if (verification.token && verification.recaptcha) {
AsyncStorage.setItem(Constants.KEY_SHOULD_VERIFY_EMAIL, 'true');
try {
dispatch(doUserEmailVerify(verification.token, verification.recaptcha));
} catch (error) {
const message = 'Invalid Verification Token';
dispatch(doUserEmailVerifyFailure(message));
dispatch(doToast({ message }));
}
} else {
dispatch(
doToast({
message: 'Invalid Verification URI',
})
);
}
} else {
dispatchNavigateToUri(dispatch, nav, evt.url);
}
}
};
render() {
return <ReduxAppNavigator />;
}
}
const mapStateToProps = state => ({
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
nav: state.nav,
toast: selectToast(state),
drawerStack: selectDrawerStack(state),
emailToVerify: selectEmailToVerify(state),
emailVerifyPending: selectEmailVerifyIsPending(state),
emailVerifyErrorMessage: selectEmailVerifyErrorMessage(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state),
user: selectUser(state),
});
export default connect(mapStateToProps)(AppWithNavigationState);

View file

@ -1,10 +0,0 @@
import { connect } from 'react-redux';
import { doToast } from 'lbry-redux';
import Address from './view';
export default connect(
null,
{
doToast,
}
)(Address);

View file

@ -1,34 +0,0 @@
// @flow
import * as React from 'react';
import { Clipboard, Text, View } from 'react-native';
import Button from '../button';
import walletStyle from '../../styles/wallet';
type Props = {
address: string,
doToast: ({ message: string }) => void,
};
export default class Address extends React.PureComponent<Props> {
render() {
const { address, doToast, style } = this.props;
return (
<View style={[walletStyle.row, style]}>
<Text selectable={true} numberOfLines={1} style={walletStyle.address}>
{address || ''}
</Text>
<Button
icon={'clipboard'}
style={walletStyle.button}
onPress={() => {
Clipboard.setString(address);
doToast({
message: 'Address copied',
});
}}
/>
</View>
);
}
}

View file

@ -1,4 +0,0 @@
import { connect } from 'react-redux';
import Button from './view';
export default connect()(Button);

View file

@ -1,57 +0,0 @@
import React from 'react';
import { Text, TouchableOpacity } from 'react-native';
import buttonStyle from 'styles/button';
import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
export default class Button extends React.PureComponent {
render() {
const { disabled, style, text, icon, iconColor, solid, theme, onPress, onLayout } = this.props;
let styles = [buttonStyle.button, buttonStyle.row];
if (style) {
if (style.length) {
styles = styles.concat(style);
} else {
styles.push(style);
}
}
if (disabled) {
styles.push(buttonStyle.disabled);
}
const textStyles = [buttonStyle.text];
if (icon && icon.trim().length > 0) {
textStyles.push(buttonStyle.textWithIcon);
}
if (theme === 'light') {
textStyles.push(buttonStyle.textDark);
} else {
// Dark background, default
textStyles.push(buttonStyle.textLight);
}
let renderIcon = (
<Icon name={icon} size={18} color={iconColor ? iconColor : 'light' === theme ? Colors.DarkGrey : Colors.White} />
);
if (solid) {
renderIcon = (
<Icon
name={icon}
size={18}
color={iconColor ? iconColor : 'light' === theme ? Colors.DarkGrey : Colors.White}
solid
/>
);
}
return (
<TouchableOpacity disabled={disabled} style={styles} onPress={onPress} onLayout={onLayout}>
{icon && renderIcon}
{text && text.trim().length > 0 && <Text style={textStyles}>{text}</Text>}
</TouchableOpacity>
);
}
}

View file

@ -1,7 +0,0 @@
import { connect } from 'react-redux';
import CategoryList from './view';
export default connect(
null,
null
)(CategoryList);

View file

@ -1,39 +0,0 @@
import React from 'react';
import NavigationActions from 'react-navigation';
import { FlatList, Text, View } from 'react-native';
import { normalizeURI } from 'lbry-redux';
import FileItem from '/component/fileItem';
import discoverStyle from 'styles/discover';
class CategoryList extends React.PureComponent {
render() {
const { category, categoryMap, navigation } = this.props;
return (
<FlatList
style={discoverStyle.horizontalScrollContainer}
contentContainerStyle={discoverStyle.horizontalScrollPadding}
initialNumToRender={3}
maxToRenderPerBatch={3}
removeClippedSubviews={true}
renderItem={({ item }) => (
<FileItem
style={discoverStyle.fileItem}
mediaStyle={discoverStyle.fileItemMedia}
key={item}
uri={normalizeURI(item)}
navigation={navigation}
showDetails={true}
compactView={false}
/>
)}
horizontal={true}
showsHorizontalScrollIndicator={false}
data={categoryMap[category]}
keyExtractor={(item, index) => item}
/>
);
}
}
export default CategoryList;

View file

@ -1,27 +0,0 @@
import { connect } from 'react-redux';
import {
selectBalance,
selectMyChannelClaims,
selectFetchingMyChannels,
doFetchChannelListMine,
doCreateChannel,
doToast,
} from 'lbry-redux';
import ChannelSelector from './view';
const select = state => ({
channels: selectMyChannelClaims(state),
fetchingChannels: selectFetchingMyChannels(state),
balance: selectBalance(state),
});
const perform = dispatch => ({
notify: data => dispatch(doToast(data)),
createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)),
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
});
export default connect(
select,
perform
)(ChannelSelector);

View file

@ -1,241 +0,0 @@
import React from 'react';
import { CLAIM_VALUES, isNameValid } from 'lbry-redux';
import { ActivityIndicator, Picker, Text, TextInput, TouchableOpacity, View } from 'react-native';
import Button from 'component/button';
import Colors from 'styles/colors';
import Constants from 'constants';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link';
import channelSelectorStyle from 'styles/channelSelector';
export default class ChannelSelector extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
currentSelectedValue: Constants.ITEM_ANONYMOUS,
newChannelName: '',
newChannelBid: 0.1,
addingChannel: false,
creatingChannel: false,
newChannelNameError: '',
newChannelBidError: '',
createChannelError: undefined,
showCreateChannel: false,
};
}
componentDidMount() {
const { channels, fetchChannelListMine, fetchingChannels } = this.props;
if (!channels.length && !fetchingChannels) {
fetchChannelListMine();
}
}
handleCreateCancel = () => {
this.setState({ showCreateChannel: false, newChannelName: '', newChannelBid: 0.1 });
};
handlePickerValueChange = (itemValue, itemIndex) => {
if (Constants.ITEM_CREATE_A_CHANNEL === itemValue) {
this.setState({ showCreateChannel: true });
} else {
this.handleCreateCancel();
this.handleChannelChange(Constants.ITEM_ANONYMOUS === itemValue ? CLAIM_VALUES.CHANNEL_ANONYMOUS : itemValue);
}
this.setState({ currentSelectedValue: itemValue });
};
handleChannelChange = value => {
const { onChannelChange } = this.props;
const { newChannelBid } = this.state;
const channel = value;
if (channel === CLAIM_VALUES.CHANNEL_NEW) {
this.setState({ addingChannel: true });
if (onChannelChange) {
onChannelChange(channel);
}
this.handleNewChannelBidChange(newChannelBid);
} else {
this.setState({ addingChannel: false });
if (onChannelChange) {
onChannelChange(channel);
}
}
};
handleNewChannelNameChange = value => {
const { notify } = this.props;
let newChannelName = value;
if (newChannelName.startsWith('@')) {
newChannelName = newChannelName.slice(1);
}
this.setState({
newChannelName,
});
};
handleNewChannelBidChange = newChannelBid => {
const { balance, notify } = this.props;
let newChannelBidError;
if (newChannelBid <= 0) {
newChannelBidError = __('Please enter a deposit above 0');
} else if (newChannelBid === balance) {
newChannelBidError = __('Please decrease your deposit to account for transaction fees');
} else if (newChannelBid > balance) {
newChannelBidError = __('Deposit cannot be higher than your balance');
}
notify({ message: newChannelBidError });
this.setState({
newChannelBid,
newChannelBidError,
});
};
handleCreateChannelClick = () => {
const { balance, createChannel, onChannelChange, notify } = this.props;
const { newChannelBid, newChannelName } = this.state;
if (newChannelName.trim().length === 0 || !isNameValid(newChannelName.substr(1), false)) {
notify({ message: 'Your channel name contains invalid characters.' });
return;
}
if (this.channelExists(newChannelName)) {
notify({ message: 'You have already created a channel with the same name.' });
return;
}
if (newChannelBid > balance) {
notify({ message: 'Deposit cannot be higher than your balance' });
return;
}
const channelName = `@${newChannelName}`;
this.setState({
creatingChannel: true,
createChannelError: undefined,
});
const success = () => {
this.setState({
creatingChannel: false,
addingChannel: false,
currentSelectedValue: channelName,
showCreateChannel: false,
});
if (onChannelChange) {
onChannelChange(channelName);
}
};
const failure = () => {
notify({ message: 'Unable to create channel due to an internal error.' });
this.setState({
creatingChannel: false,
});
};
createChannel(channelName, newChannelBid).then(success, failure);
};
channelExists = name => {
const { channels = [] } = this.props;
for (let channel of channels) {
if (
name.toLowerCase() === channel.name.toLowerCase() ||
`@${name}`.toLowerCase() === channel.name.toLowerCase()
) {
return true;
}
}
return false;
};
render() {
const channel = this.state.addingChannel ? 'new' : this.props.channel;
const { fetchingChannels, channels = [] } = this.props;
const pickerItems = [{ name: Constants.ITEM_ANONYMOUS }, { name: Constants.ITEM_CREATE_A_CHANNEL }].concat(
channels
);
const {
newChannelName,
newChannelNameError,
newChannelBid,
newChannelBidError,
creatingChannel,
createChannelError,
addingChannel,
} = this.state;
return (
<View style={channelSelectorStyle.container}>
<Picker
selectedValue={this.state.currentSelectedValue}
style={channelSelectorStyle.channelPicker}
itemStyle={channelSelectorStyle.channelPickerItem}
onValueChange={this.handlePickerValueChange}
>
{pickerItems.map(item => (
<Picker.Item label={item.name} value={item.name} key={item.name} />
))}
</Picker>
{this.state.showCreateChannel && (
<View style={channelSelectorStyle.createChannelContainer}>
<View style={channelSelectorStyle.channelInputContainer}>
<Text style={channelSelectorStyle.channelAt}>@</Text>
<TextInput
style={channelSelectorStyle.channelNameInput}
value={this.state.newChannelName}
onChangeText={this.handleNewChannelNameChange}
placeholder={'Channel name'}
underlineColorAndroid={Colors.NextLbryGreen}
/>
</View>
<View style={channelSelectorStyle.bidRow}>
<Text style={channelSelectorStyle.label}>Deposit</Text>
<TextInput
style={channelSelectorStyle.bidAmountInput}
value={String(this.state.newChannelBid)}
onChangeText={this.handleNewChannelBidChange}
placeholder={'0.00'}
keyboardType={'number-pad'}
underlineColorAndroid={Colors.NextLbryGreen}
/>
<Text style={channelSelectorStyle.currency}>LBC</Text>
</View>
<Text style={channelSelectorStyle.helpText}>
This LBC remains yours. It is a deposit to reserve the name and can be undone at any time.
</Text>
<View style={channelSelectorStyle.buttonContainer}>
{creatingChannel && <ActivityIndicator size={'small'} color={Colors.LbryGreen} />}
{!creatingChannel && (
<View style={channelSelectorStyle.buttons}>
<Link style={channelSelectorStyle.cancelLink} text="Cancel" onPress={this.handleCreateCancel} />
<Button
style={channelSelectorStyle.createButton}
disabled={!(this.state.newChannelName.trim().length > 0 && this.state.newChannelBid > 0)}
text="Create"
onPress={this.handleCreateChannelClick}
/>
</View>
)}
</View>
</View>
)}
</View>
);
}
}

View file

@ -1,29 +0,0 @@
import { connect } from 'react-redux';
import { doToast } from 'lbry-redux';
import {
doClaimRewardType,
doClaimRewardClearError,
makeSelectClaimRewardError,
makeSelectIsRewardClaimPending,
rewards as REWARD_TYPES,
} from 'lbryinc';
import CustomRewardCard from './view';
const select = state => ({
rewardIsPending: makeSelectIsRewardClaimPending()(state, {
reward_type: REWARD_TYPES.TYPE_REWARD_CODE,
}),
error: makeSelectClaimRewardError()(state, { reward_type: REWARD_TYPES.TYPE_REWARD_CODE }),
});
const perform = dispatch => ({
claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)),
clearError: reward => dispatch(doClaimRewardClearError(reward)),
notify: data => dispatch(doToast(data)),
submitRewardCode: code => dispatch(doClaimRewardType(REWARD_TYPES.TYPE_REWARD_CODE, { params: { code } })),
});
export default connect(
select,
perform
)(CustomRewardCard);

View file

@ -1,96 +0,0 @@
// @flow
import React from 'react';
import { ActivityIndicator, Keyboard, Text, TextInput, TouchableOpacity, View } from 'react-native';
import Colors from '../../styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Button from '../button';
import Link from '../link';
import rewardStyle from '../../styles/reward';
class CustomRewardCard extends React.PureComponent<Props> {
state = {
claimStarted: false,
rewardCode: '',
};
componentWillReceiveProps(nextProps) {
const { error, rewardIsPending } = nextProps;
const { clearError, notify } = this.props;
if (this.state.claimStarted && !rewardIsPending) {
if (error && error.trim().length > 0) {
notify({ message: error });
} else {
notify({ message: 'Reward successfully claimed!' });
this.setState({ rewardCode: '' });
}
this.setState({ claimStarted: false });
}
}
onClaimPress = () => {
const { canClaim, notify, showVerification, submitRewardCode } = this.props;
const { rewardCode } = this.state;
Keyboard.dismiss();
if (!canClaim) {
if (showVerification) {
showVerification();
}
notify({ message: 'Unfortunately, you are not eligible to claim this reward at this time.' });
return;
}
if (!rewardCode || rewardCode.trim().length === 0) {
notify({ message: 'Please enter a reward code to claim.' });
return;
}
this.setState({ claimStarted: true }, () => {
submitRewardCode(rewardCode);
});
};
render() {
const { canClaim, rewardIsPending } = this.props;
return (
<View style={[rewardStyle.rewardCard, rewardStyle.row]}>
<View style={rewardStyle.leftCol}>
{rewardIsPending && <ActivityIndicator size="small" color={Colors.LbryGreen} />}
</View>
<View style={rewardStyle.midCol}>
<Text style={rewardStyle.rewardTitle}>Custom Code</Text>
<Text style={rewardStyle.rewardDescription}>
Are you a supermodel or rockstar that received a custom reward code? Claim it here.
</Text>
<View>
<TextInput
style={rewardStyle.customCodeInput}
placeholder={'0123abc'}
onChangeText={text => this.setState({ rewardCode: text })}
value={this.state.rewardCode}
/>
<Button
style={rewardStyle.redeemButton}
text={'Redeem'}
disabled={!this.state.rewardCode || this.state.rewardCode.trim().length === 0 || rewardIsPending}
onPress={() => {
if (!rewardIsPending) {
this.onClaimPress();
}
}}
/>
</View>
</View>
<View style={rewardStyle.rightCol}>
<Text style={rewardStyle.rewardAmount}>?</Text>
<Text style={rewardStyle.rewardCurrency}>LBC</Text>
</View>
</View>
);
}
}
export default CustomRewardCard;

View file

@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import { makeSelectDateForUri } from 'lbry-redux';
import DateTime from './view';
const select = (state, props) => ({
date: props.date || makeSelectDateForUri(props.uri)(state),
});
const perform = dispatch => ({
fetchBlock: height => dispatch(doFetchBlock(height)),
});
export default connect(
select,
perform
)(DateTime);

View file

@ -1,55 +0,0 @@
// @flow
import React from 'react';
import moment from 'moment';
import { View, Text } from 'react-native';
type Props = {
date?: number,
timeAgo?: boolean,
formatOptions: {},
show?: string,
};
class DateTime extends React.PureComponent<Props> {
static SHOW_DATE = 'date';
static SHOW_TIME = 'time';
static SHOW_BOTH = 'both';
static defaultProps = {
formatOptions: {
month: 'long',
day: 'numeric',
year: 'numeric',
},
};
render() {
const { date, formatOptions, timeAgo, style, textStyle } = this.props;
const show = this.props.show || DateTime.SHOW_BOTH;
const locale = 'en-US'; // default to en-US until we get a working i18n module for RN
if (timeAgo) {
return date ? (
<View style={style}>
<Text style={textStyle}>{moment(date).from(moment())}</Text>
</View>
) : null;
}
// TODO: formatOptions not working as expected in RN
// date.toLocaleDateString([locale, 'en-US'], formatOptions)}
return (
<View style={style}>
<Text style={textStyle}>
{date && (show === DateTime.SHOW_BOTH || show === DateTime.SHOW_DATE) && moment(date).format('MMMM D, YYYY')}
{show === DateTime.SHOW_BOTH && ' '}
{date && (show === DateTime.SHOW_BOTH || show === DateTime.SHOW_TIME) && date.toLocaleTimeString()}
{!date && '...'}
</Text>
</View>
);
}
}
export default DateTime;

View file

@ -1,4 +0,0 @@
import { connect } from 'react-redux';
import DrawerContent from './view';
export default connect()(DrawerContent);

View file

@ -1,38 +0,0 @@
import React from 'react';
import { DrawerItems, SafeAreaView } from 'react-navigation';
import { ScrollView } from 'react-native';
import Constants from 'constants';
import discoverStyle from 'styles/discover';
class DrawerContent extends React.PureComponent {
render() {
const props = this.props;
const { navigation, onItemPress } = props;
return (
<ScrollView>
<SafeAreaView style={discoverStyle.drawerContentContainer} forceInset={{ top: 'always', horizontal: 'never' }}>
<DrawerItems
{...props}
onItemPress={route => {
const { routeName } = route.route;
if (Constants.FULL_ROUTE_NAME_DISCOVER === routeName) {
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_DISCOVER });
return;
}
if (Constants.FULL_ROUTE_NAME_WALLET === routeName) {
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_WALLET });
return;
}
onItemPress(route);
}}
/>
</SafeAreaView>
</ScrollView>
);
}
}
export default DrawerContent;

View file

@ -1,28 +0,0 @@
import { connect } from 'react-redux';
import {
doPurchaseUri,
makeSelectFileInfoForUri,
makeSelectDownloadingForUri,
makeSelectLoadingForUri,
} from 'lbry-redux';
import { doFetchCostInfoForUri, makeSelectCostInfoForUri } from 'lbryinc';
import { doStartDownload } from 'redux/actions/file';
import FileDownloadButton from './view';
const select = (state, props) => ({
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
downloading: makeSelectDownloadingForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
loading: makeSelectLoadingForUri(props.uri)(state),
});
const perform = dispatch => ({
purchaseUri: (uri, costInfo, saveFile) => dispatch(doPurchaseUri(uri, costInfo, saveFile)),
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
});
export default connect(
select,
perform
)(FileDownloadButton);

View file

@ -1,110 +0,0 @@
import React from 'react';
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
import Button from '../button';
import fileDownloadButtonStyle from 'styles/fileDownloadButton';
class FileDownloadButton extends React.PureComponent {
componentDidMount() {
const { costInfo, fetchCostInfo, uri } = this.props;
if (costInfo === undefined) {
fetchCostInfo(uri);
}
}
componentWillReceiveProps(nextProps) {
//this.checkAvailability(nextProps.uri);
//this.restartDownload(nextProps);
}
restartDownload(props) {
const { downloading, fileInfo, uri, restartDownload } = props;
if (
!downloading &&
fileInfo &&
!fileInfo.completed &&
fileInfo.written_bytes !== false &&
fileInfo.written_bytes < fileInfo.total_bytes
) {
restartDownload(uri, fileInfo.outpoint);
}
}
render() {
const {
fileInfo,
downloading,
uri,
purchaseUri,
costInfo,
isPlayable,
isViewable,
onPlay,
onView,
loading,
doPause,
style,
openFile,
onButtonLayout,
} = this.props;
if ((fileInfo && !fileInfo.stopped) || loading || downloading) {
const progress = fileInfo && fileInfo.written_bytes ? (fileInfo.written_bytes / fileInfo.total_bytes) * 100 : 0,
label = fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...';
return (
<View style={[style, fileDownloadButtonStyle.container]}>
<View
style={{ width: `${progress}%`, backgroundColor: '#ff0000', position: 'absolute', left: 0, top: 0 }}
></View>
<Text style={fileDownloadButtonStyle.text}>{label}</Text>
</View>
);
} else if (!fileInfo && !downloading) {
if (!costInfo) {
return (
<View style={[style, fileDownloadButtonStyle.container]}>
<Text style={fileDownloadButtonStyle.text}>Fetching cost info...</Text>
</View>
);
}
return (
<Button
icon={isPlayable ? 'play' : null}
text={isPlayable ? 'Play' : isViewable ? 'View' : 'Download'}
onLayout={onButtonLayout}
style={[style, fileDownloadButtonStyle.container]}
onPress={() => {
if (NativeModules.Firebase) {
NativeModules.Firebase.track('purchase_uri', { uri: uri });
}
purchaseUri(uri, costInfo, !isPlayable);
if (NativeModules.UtilityModule) {
NativeModules.UtilityModule.checkDownloads();
}
if (isPlayable && onPlay) {
this.props.onPlay();
}
if (isViewable && onView) {
this.props.onView();
}
}}
/>
);
} else if (fileInfo && fileInfo.download_path) {
return (
<TouchableOpacity
onLayout={onButtonLayout}
style={[style, fileDownloadButtonStyle.container]}
onPress={openFile}
>
<Text style={fileDownloadButtonStyle.text}>{isViewable ? 'View' : 'Open'}</Text>
</TouchableOpacity>
);
}
return null;
}
}
export default FileDownloadButton;

View file

@ -1,35 +0,0 @@
import { connect } from 'react-redux';
import {
doResolveUri,
makeSelectClaimForUri,
makeSelectMetadataForUri,
makeSelectFileInfoForUri,
makeSelectThumbnailForUri,
makeSelectTitleForUri,
makeSelectIsUriResolving,
makeSelectClaimIsNsfw,
} from 'lbry-redux';
import { selectRewardContentClaimIds } from 'lbryinc';
import { selectShowNsfw } from 'redux/selectors/settings';
import FileItem from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state),
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
});
const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)),
});
export default connect(
select,
perform
)(FileItem);

View file

@ -1,132 +0,0 @@
import React from 'react';
import { normalizeURI } from 'lbry-redux';
import { NavigationActions } from 'react-navigation';
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
import { navigateToUri } from 'utils/helper';
import Colors from 'styles/colors';
import DateTime from 'component/dateTime';
import FileItemMedia from 'component/fileItemMedia';
import FilePrice from 'component/filePrice';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link';
import NsfwOverlay from 'component/nsfwOverlay';
import discoverStyle from 'styles/discover';
class FileItem extends React.PureComponent {
constructor(props) {
super(props);
}
componentWillMount() {
this.resolve(this.props);
}
componentWillReceiveProps(nextProps) {
this.resolve(nextProps);
}
resolve(props) {
const { isResolvingUri, resolveUri, claim, uri } = props;
if (!isResolvingUri && claim === undefined && uri) {
resolveUri(uri);
}
}
navigateToFileUri = () => {
const { navigation, uri } = this.props;
const normalizedUri = normalizeURI(uri);
if (NativeModules.Firebase) {
NativeModules.Firebase.track('explore_click', { uri: normalizedUri });
}
navigateToUri(navigation, normalizedUri);
};
render() {
const {
claim,
title,
thumbnail,
fileInfo,
metadata,
isResolvingUri,
rewardedContentClaimIds,
style,
mediaStyle,
navigation,
showDetails,
compactView,
titleBeforeThumbnail,
} = this.props;
const uri = normalizeURI(this.props.uri);
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
const signingChannel = claim ? claim.signing_channel : null;
const channelName = signingChannel ? signingChannel.name : null;
const channelClaimId =
claim && claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
const fullChannelUri = channelClaimId ? `${channelName}#${channelClaimId}` : channelName;
const height = claim ? claim.height : null;
return (
<View style={style}>
<TouchableOpacity style={discoverStyle.container} onPress={this.navigateToFileUri}>
{!compactView && titleBeforeThumbnail && (
<Text numberOfLines={1} style={[discoverStyle.fileItemName, discoverStyle.rewardTitle]}>
{title}
</Text>
)}
<FileItemMedia
title={title}
thumbnail={thumbnail}
blurRadius={obscureNsfw ? 15 : 0}
resizeMode="cover"
isResolvingUri={isResolvingUri}
style={mediaStyle}
/>
{!compactView && fileInfo && fileInfo.completed && fileInfo.download_path && (
<Icon
style={discoverStyle.downloadedIcon}
solid={true}
color={Colors.NextLbryGreen}
name={'folder'}
size={16}
/>
)}
{!compactView && (!fileInfo || !fileInfo.completed || !fileInfo.download_path) && (
<FilePrice uri={uri} style={discoverStyle.filePriceContainer} textStyle={discoverStyle.filePriceText} />
)}
{!compactView && (
<View style={isRewardContent ? discoverStyle.rewardTitleContainer : null}>
<Text numberOfLines={1} style={[discoverStyle.fileItemName, discoverStyle.rewardTitle]}>
{title}
</Text>
{isRewardContent && <Icon style={discoverStyle.rewardIcon} name="award" size={14} />}
</View>
)}
{!compactView && showDetails && (
<View style={discoverStyle.detailsRow}>
{channelName && (
<Link
style={discoverStyle.channelName}
text={channelName}
onPress={() => {
navigateToUri(navigation, normalizeURI(fullChannelUri));
}}
/>
)}
<DateTime style={discoverStyle.dateTime} textStyle={discoverStyle.dateTimeText} timeAgo uri={uri} />
</View>
)}
</TouchableOpacity>
{obscureNsfw && (
<NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />
)}
</View>
);
}
}
export default FileItem;

View file

@ -1,10 +0,0 @@
import { connect } from 'react-redux';
import FileItemMedia from './view';
const select = state => ({});
const perform = dispatch => ({});
export default connect(
select,
perform
)(FileItemMedia);

View file

@ -1,112 +0,0 @@
import React from 'react';
import { ActivityIndicator, Image, Text, View } from 'react-native';
import Colors from 'styles/colors';
import FastImage from 'react-native-fast-image';
import fileItemMediaStyle from 'styles/fileItemMedia';
class FileItemMedia extends React.PureComponent {
static AUTO_THUMB_STYLES = [
fileItemMediaStyle.autothumbPurple,
fileItemMediaStyle.autothumbRed,
fileItemMediaStyle.autothumbPink,
fileItemMediaStyle.autothumbIndigo,
fileItemMediaStyle.autothumbBlue,
fileItemMediaStyle.autothumbLightBlue,
fileItemMediaStyle.autothumbCyan,
fileItemMediaStyle.autothumbTeal,
fileItemMediaStyle.autothumbGreen,
fileItemMediaStyle.autothumbYellow,
fileItemMediaStyle.autothumbOrange,
];
state: {
imageLoadFailed: false,
};
componentWillMount() {
this.setState({
autoThumbStyle:
FileItemMedia.AUTO_THUMB_STYLES[Math.floor(Math.random() * FileItemMedia.AUTO_THUMB_STYLES.length)],
});
}
getFastImageResizeMode(resizeMode) {
switch (resizeMode) {
case 'contain':
return FastImage.resizeMode.contain;
case 'stretch':
return FastImage.resizeMode.stretch;
case 'center':
return FastImage.resizeMode.center;
default:
return FastImage.resizeMode.cover;
}
}
isThumbnailValid = thumbnail => {
if (!thumbnail || typeof thumbnail !== 'string') {
return false;
}
if (thumbnail.substring(0, 7) != 'http://' && thumbnail.substring(0, 8) != 'https://') {
return false;
}
return true;
};
render() {
let style = this.props.style;
const { blurRadius, isResolvingUri, thumbnail, title, resizeMode } = this.props;
const atStyle = this.state.autoThumbStyle;
if (this.isThumbnailValid(thumbnail) && !this.state.imageLoadFailed) {
if (style == null) {
style = fileItemMediaStyle.thumbnail;
}
if (blurRadius > 0) {
// No blur radius support in FastImage yet
return (
<Image
source={{ uri: thumbnail }}
blurRadius={blurRadius}
resizeMode={resizeMode ? resizeMode : 'cover'}
style={style}
/>
);
}
return (
<FastImage
source={{ uri: thumbnail }}
onError={() => this.setState({ imageLoadFailed: true })}
resizeMode={this.getFastImageResizeMode(resizeMode)}
style={style}
/>
);
}
return (
<View style={[style ? style : fileItemMediaStyle.autothumb, atStyle]}>
{isResolvingUri && (
<View style={fileItemMediaStyle.resolving}>
<ActivityIndicator color={Colors.White} size={'large'} />
<Text style={fileItemMediaStyle.text}>Resolving...</Text>
</View>
)}
{!isResolvingUri && (
<Text style={fileItemMediaStyle.autothumbText}>
{title &&
title.trim().length > 0 &&
title
.replace(/\s+/g, '')
.substring(0, Math.min(title.replace(' ', '').length, 5))
.toUpperCase()}
</Text>
)}
</View>
);
}
}
export default FileItemMedia;

View file

@ -1,14 +0,0 @@
import { connect } from 'react-redux';
import FileList from './view';
import { selectClaimsById } from 'lbry-redux';
const select = state => ({
claimsById: selectClaimsById(state),
});
const perform = dispatch => ({});
export default connect(
select,
perform
)(FileList);

View file

@ -1,191 +0,0 @@
// @flow
import * as React from 'react';
import { buildURI } from 'lbry-redux';
import { FlatList } from 'react-native';
import FileItem from 'component/fileItem';
import fileListStyle from 'styles/fileList';
// In the future, all Flow types need to be specified in a common source (lbry-redux, perhaps?)
type FileInfo = {
name: string,
channelName: ?string,
pending?: boolean,
channel_claim_id: string,
value?: {
publisherSignature: {
certificateId: string,
},
},
metadata: {
publisherSignature: {
certificateId: string,
},
},
};
type Props = {
hideFilter: boolean,
sortByHeight?: boolean,
claimsById: Array<{}>,
fileInfos: Array<FileInfo>,
checkPending?: boolean,
};
type State = {
sortBy: string,
};
class FileList extends React.PureComponent<Props, State> {
static defaultProps = {
hideFilter: false,
};
constructor(props: Props) {
super(props);
this.state = {
sortBy: 'dateNew',
};
(this: any).handleSortChanged = this.handleSortChanged.bind(this);
this.sortFunctions = {
dateNew: fileInfos =>
this.props.sortByHeight
? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
if (fileInfo1.pending) {
return -1;
}
const height1 = this.props.claimsById[fileInfo1.claim_id]
? this.props.claimsById[fileInfo1.claim_id].height
: 0;
const height2 = this.props.claimsById[fileInfo2.claim_id]
? this.props.claimsById[fileInfo2.claim_id].height
: 0;
if (height1 > height2) {
return -1;
} else if (height1 < height2) {
return 1;
}
return 0;
})
: [...fileInfos].reverse(),
dateOld: fileInfos =>
this.props.sortByHeight
? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
const height1 = this.props.claimsById[fileInfo1.claim_id]
? this.props.claimsById[fileInfo1.claim_id].height
: 999999;
const height2 = this.props.claimsById[fileInfo2.claim_id]
? this.props.claimsById[fileInfo2.claim_id].height
: 999999;
if (height1 < height2) {
return -1;
} else if (height1 > height2) {
return 1;
}
return 0;
})
: fileInfos,
title: fileInfos =>
fileInfos.slice().sort((fileInfo1, fileInfo2) => {
const getFileTitle = fileInfo => {
const { value, metadata, name, claim_name: claimName } = fileInfo;
if (metadata) {
// downloaded claim
return metadata.title || claimName;
} else if (value) {
// published claim
const { title } = value.stream.metadata;
return title || name;
}
// Invalid claim
return '';
};
const title1 = getFileTitle(fileInfo1).toLowerCase();
const title2 = getFileTitle(fileInfo2).toLowerCase();
if (title1 < title2) {
return -1;
} else if (title1 > title2) {
return 1;
}
return 0;
}),
filename: fileInfos =>
fileInfos.slice().sort(({ file_name: fileName1 }, { file_name: fileName2 }) => {
const fileName1Lower = fileName1.toLowerCase();
const fileName2Lower = fileName2.toLowerCase();
if (fileName1Lower < fileName2Lower) {
return -1;
} else if (fileName2Lower > fileName1Lower) {
return 1;
}
return 0;
}),
};
}
getChannelSignature = (fileInfo: FileInfo) => {
if (fileInfo.pending) {
return undefined;
}
if (fileInfo.value) {
return fileInfo.value.publisherSignature.certificateId;
}
return fileInfo.channel_claim_id;
};
handleSortChanged(event: SyntheticInputEvent<*>) {
this.setState({
sortBy: event.target.value,
});
}
sortFunctions: {};
render() {
const { contentContainerStyle, fileInfos, hideFilter, checkPending, navigation, onEndReached, style } = this.props;
const { sortBy } = this.state;
const items = [];
if (!fileInfos) {
return null;
}
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = fileInfo;
const uriParams = {};
// This is unfortunate
// https://github.com/lbryio/lbry/issues/1159
const name = claimName || claimNameDownloaded;
uriParams.contentName = name;
uriParams.claimId = claimId;
const uri = buildURI(uriParams);
items.push(uri);
});
return (
<FlatList
style={style}
contentContainerStyle={contentContainerStyle}
data={items}
onEndReached={onEndReached}
keyExtractor={(item, index) => item}
renderItem={({ item }) => (
<FileItem
style={fileListStyle.fileItem}
uri={item}
navigation={navigation}
showDetails={true}
compactView={false}
/>
)}
/>
);
}
}
export default FileList;

View file

@ -1,32 +0,0 @@
import { connect } from 'react-redux';
import {
doResolveUri,
makeSelectClaimForUri,
makeSelectMetadataForUri,
makeSelectFileInfoForUri,
makeSelectIsUriResolving,
makeSelectTitleForUri,
makeSelectThumbnailForUri,
} from 'lbry-redux';
import { selectShowNsfw } from 'redux/selectors/settings';
import FileListItem from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
title: makeSelectTitleForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
});
const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)),
});
export default connect(
select,
perform
)(FileListItem);

View file

@ -1,159 +0,0 @@
import React from 'react';
import { normalizeURI, parseURI } from 'lbry-redux';
import { ActivityIndicator, Platform, Text, TouchableOpacity, View } from 'react-native';
import { navigateToUri, formatBytes } from 'utils/helper';
import Colors from 'styles/colors';
import DateTime from 'component/dateTime';
import FileItemMedia from 'component/fileItemMedia';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link';
import NsfwOverlay from 'component/nsfwOverlay';
import ProgressBar from 'component/progressBar';
import fileListStyle from 'styles/fileList';
class FileListItem extends React.PureComponent {
getStorageForFileInfo = fileInfo => {
if (!fileInfo.completed) {
const written = formatBytes(fileInfo.written_bytes);
const total = formatBytes(fileInfo.total_bytes);
return `(${written} / ${total})`;
}
return formatBytes(fileInfo.written_bytes);
};
formatTitle = title => {
if (!title) {
return title;
}
return title.length > 80 ? title.substring(0, 77).trim() + '...' : title;
};
getDownloadProgress = fileInfo => {
return Math.ceil((fileInfo.written_bytes / fileInfo.total_bytes) * 100);
};
componentDidMount() {
const { claim, resolveUri, uri } = this.props;
if (!claim) {
resolveUri(uri);
}
}
render() {
const {
claim,
fileInfo,
metadata,
featuredResult,
isResolvingUri,
isDownloaded,
style,
onPress,
navigation,
thumbnail,
title,
} = this.props;
const uri = normalizeURI(this.props.uri);
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
const isResolving = !fileInfo && isResolvingUri;
let name, channel, height, channelClaimId, fullChannelUri;
if (claim) {
name = claim.name;
signingChannel = claim.signing_channel;
channel = signingChannel ? signingChannel.name : null;
height = claim.height;
channelClaimId = claim.value && claim.value.publisherSignature && claim.value.publisherSignature.certificateId;
fullChannelUri = channelClaimId ? `${channel}#${channelClaimId}` : channel;
}
if (featuredResult && !isResolvingUri && !claim && !title && !name) {
return null;
}
return (
<View style={style}>
<TouchableOpacity style={style} onPress={onPress}>
<FileItemMedia
style={fileListStyle.thumbnail}
blurRadius={obscureNsfw ? 15 : 0}
resizeMode="cover"
title={title || name}
thumbnail={thumbnail}
/>
{fileInfo && fileInfo.completed && fileInfo.download_path && (
<Icon
style={fileListStyle.downloadedIcon}
solid={true}
color={Colors.NextLbryGreen}
name={'folder'}
size={16}
/>
)}
<View style={fileListStyle.detailsContainer}>
{featuredResult && (
<Text style={fileListStyle.featuredUri} numberOfLines={1}>
{uri}
</Text>
)}
{!title && !name && !channel && isResolving && (
<View>
{!title && !name && <Text style={fileListStyle.uri}>{uri}</Text>}
{!title && !name && (
<View style={fileListStyle.row}>
<ActivityIndicator size={'small'} color={featuredResult ? Colors.White : Colors.LbryGreen} />
</View>
)}
</View>
)}
{(title || name) && (
<Text style={featuredResult ? fileListStyle.featuredTitle : fileListStyle.title}>
{this.formatTitle(title) || this.formatTitle(name)}
</Text>
)}
{channel && (
<Link
style={fileListStyle.publisher}
text={channel}
onPress={() => {
navigateToUri(navigation, normalizeURI(fullChannelUri));
}}
/>
)}
<View style={fileListStyle.info}>
{fileInfo && !isNaN(fileInfo.written_bytes) && fileInfo.written_bytes > 0 && (
<Text style={fileListStyle.infoText}>{this.getStorageForFileInfo(fileInfo)}</Text>
)}
<DateTime style={fileListStyle.publishInfo} textStyle={fileListStyle.infoText} timeAgo uri={uri} />
</View>
{fileInfo && fileInfo.download_path && (
<View style={fileListStyle.downloadInfo}>
{!fileInfo.completed && (
<ProgressBar
borderRadius={3}
color={Colors.NextLbryGreen}
height={3}
style={fileListStyle.progress}
progress={this.getDownloadProgress(fileInfo)}
/>
)}
</View>
)}
</View>
</TouchableOpacity>
{obscureNsfw && (
<NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />
)}
</View>
);
}
}
export default FileListItem;

View file

@ -1,19 +0,0 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri } from 'lbry-redux';
import { doFetchCostInfoForUri, makeSelectCostInfoForUri, makeSelectFetchingCostInfoForUri } from 'lbryinc';
import FilePrice from './view';
const select = (state, props) => ({
costInfo: makeSelectCostInfoForUri(props.uri)(state),
fetching: makeSelectFetchingCostInfoForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
});
const perform = dispatch => ({
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
});
export default connect(
select,
perform
)(FilePrice);

View file

@ -1,119 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, View } from 'react-native';
import { formatCredits, formatFullPrice } from 'lbry-redux';
class CreditAmount extends React.PureComponent {
static propTypes = {
amount: PropTypes.number.isRequired,
precision: PropTypes.number,
isEstimate: PropTypes.bool,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
showFree: PropTypes.bool,
showFullPrice: PropTypes.bool,
showPlus: PropTypes.bool,
look: PropTypes.oneOf(['indicator', 'plain', 'fee']),
};
static defaultProps = {
precision: 2,
label: true,
showFree: false,
look: 'indicator',
showFullPrice: false,
showPlus: false,
};
render() {
const minimumRenderableAmount = Math.pow(10, -1 * this.props.precision);
const { amount, precision, showFullPrice, style } = this.props;
let formattedAmount;
const fullPrice = formatFullPrice(amount, 2);
if (showFullPrice) {
formattedAmount = fullPrice;
} else {
formattedAmount =
amount > 0 && amount < minimumRenderableAmount
? `<${minimumRenderableAmount}`
: formatCredits(amount, precision);
}
let amountText;
if (this.props.showFree && parseFloat(this.props.amount) === 0) {
amountText = 'FREE';
} else {
if (this.props.label) {
const label =
typeof this.props.label === 'string' ? this.props.label : parseFloat(amount) == 1 ? 'credit' : 'credits';
amountText = `${formattedAmount} ${label}`;
} else {
amountText = formattedAmount;
}
if (this.props.showPlus && amount > 0) {
amountText = `+${amountText}`;
}
}
/*{this.props.isEstimate ? (
<span
className="credit-amount__estimate"
title={__('This is an estimate and does not include data fees')}
>
*
</span>
) : null}*/
return <Text style={style}>{amountText}</Text>;
}
}
class FilePrice extends React.PureComponent {
componentWillMount() {
this.fetchCost(this.props);
}
componentWillReceiveProps(nextProps) {
this.fetchCost(nextProps);
}
fetchCost(props) {
const { costInfo, fetchCostInfo, uri, fetching, claim } = props;
if (costInfo === undefined && !fetching && claim) {
fetchCostInfo(uri);
}
}
render() {
const { costInfo, look = 'indicator', showFullPrice = false, style, textStyle } = this.props;
const isEstimate = costInfo ? !costInfo.includesData : null;
if (!costInfo) {
return (
<View style={style}>
<Text style={textStyle}>???</Text>
</View>
);
}
return (
<View style={style}>
<CreditAmount
style={textStyle}
label={false}
amount={parseFloat(costInfo.cost)}
isEstimate={isEstimate}
showFree
showFullPrice={showFullPrice}
>
???
</CreditAmount>
</View>
);
}
}
export default FilePrice;

View file

@ -1,4 +0,0 @@
import { connect } from 'react-redux';
import FileRewardsDriver from './view';
export default connect()(FileRewardsDriver);

View file

@ -1,19 +0,0 @@
import React from 'react';
import { Text, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome5';
import filePageStyle from 'styles/filePage';
class FileRewardsDriver extends React.PureComponent<Props> {
render() {
const { navigation } = this.props;
return (
<TouchableOpacity style={filePageStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}>
<Icon name="award" size={16} style={filePageStyle.rewardIcon} />
<Text style={filePageStyle.rewardDriverText}>Earn some credits to access this content.</Text>
</TouchableOpacity>
);
}
}
export default FileRewardsDriver;

View file

@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { selectBalance } from 'lbry-redux';
import { selectUnclaimedRewardValue } from 'lbryinc';
import Constants from 'constants';
import FloatingWalletBalance from './view';
const select = state => ({
balance: selectBalance(state),
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
rewardsNotInterested: makeSelectClientSetting(Constants.SETTING_REWARDS_NOT_INTERESTED)(state),
});
export default connect(
select,
null
)(FloatingWalletBalance);

View file

@ -1,44 +0,0 @@
// @flow
import React from 'react';
import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
import { formatCredits } from 'lbry-redux';
import Address from 'component/address';
import Button from 'component/button';
import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
import floatingButtonStyle from 'styles/floatingButton';
type Props = {
balance: number,
};
class FloatingWalletBalance extends React.PureComponent<Props> {
render() {
const { balance, navigation, rewardsNotInterested, unclaimedRewardAmount } = this.props;
return (
<View style={[floatingButtonStyle.view, floatingButtonStyle.bottomRight]}>
{!rewardsNotInterested && unclaimedRewardAmount > 0 && (
<TouchableOpacity
style={floatingButtonStyle.pendingContainer}
onPress={() => navigation && navigation.navigate({ routeName: 'Rewards' })}
>
<Icon name="award" size={18} style={floatingButtonStyle.rewardIcon} />
<Text style={floatingButtonStyle.text}>{unclaimedRewardAmount}</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={floatingButtonStyle.container}
onPress={() => navigation && navigation.navigate({ routeName: 'WalletStack' })}
>
{isNaN(balance) && <ActivityIndicator size="small" color={Colors.White} />}
{(!isNaN(balance) || balance === 0) && (
<Text style={floatingButtonStyle.text}>{formatCredits(parseFloat(balance), 2) + ' LBC'}</Text>
)}
</TouchableOpacity>
</View>
);
}
}
export default FloatingWalletBalance;

View file

@ -1,12 +0,0 @@
import { connect } from 'react-redux';
import { doToast } from 'lbry-redux';
import Link from './view';
const perform = dispatch => ({
notify: data => dispatch(doToast(data)),
});
export default connect(
null,
perform
)(Link);

View file

@ -1,67 +0,0 @@
import React from 'react';
import { Linking, Text, TouchableOpacity } from 'react-native';
export default class Link extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
tappedStyle: false,
};
this.addTappedStyle = this.addTappedStyle.bind(this);
}
handlePress = () => {
const { error, href, navigation, notify } = this.props;
if (navigation && href.startsWith('#')) {
navigation.navigate(href.substring(1));
} else {
if (this.props.effectOnTap) this.addTappedStyle();
Linking.openURL(href)
.then(() =>
setTimeout(() => {
this.setState({ tappedStyle: false });
}, 2000)
)
.catch(err => {
notify({ message: error, isError: true });
this.setState({ tappedStyle: false });
});
}
};
addTappedStyle() {
this.setState({ tappedStyle: true });
setTimeout(() => {
this.setState({ tappedStyle: false });
}, 2000);
}
render() {
const { ellipsizeMode, numberOfLines, onPress, style, text } = this.props;
let styles = [];
if (style) {
if (style.length) {
styles = styles.concat(style);
} else {
styles.push(style);
}
}
if (this.props.effectOnTap && this.state.tappedStyle) {
styles.push(this.props.effectOnTap);
}
return (
<Text
style={styles}
numberOfLines={numberOfLines}
ellipsizeMode={ellipsizeMode}
onPress={onPress ? onPress : this.handlePress}
>
{text}
</Text>
);
}
}

View file

@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import { SETTINGS, savePosition } from 'lbry-redux';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetPlayerVisible } from 'redux/actions/drawer';
import { selectIsPlayerVisible } from 'redux/selectors/drawer';
import MediaPlayer from './view';
const select = state => ({
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
isPlayerVisible: selectIsPlayerVisible(state),
});
const perform = dispatch => ({
savePosition: (claimId, outpoint, position) => dispatch(savePosition(claimId, outpoint, position)),
setPlayerVisible: () => dispatch(doSetPlayerVisible(true)),
});
export default connect(
select,
perform
)(MediaPlayer);

View file

@ -1,503 +0,0 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import {
AppState,
ActivityIndicator,
DeviceEventEmitter,
NativeModules,
PanResponder,
Text,
View,
ScrollView,
TouchableOpacity,
} from 'react-native';
import Colors from 'styles/colors';
import FastImage from 'react-native-fast-image';
import Video from 'react-native-video';
import Icon from 'react-native-vector-icons/FontAwesome5';
import FileItemMedia from 'component/fileItemMedia';
import mediaPlayerStyle from 'styles/mediaPlayer';
const positionSaveInterval = 10;
class MediaPlayer extends React.PureComponent {
static ControlsTimeout = 3000;
seekResponder = null;
seekerWidth = 0;
trackingOffset = 0;
tracking = null;
video = null;
constructor(props) {
super(props);
this.state = {
buffering: false,
backgroundPlayEnabled: false,
autoPaused: false,
rate: 1,
volume: 1,
muted: false,
resizeMode: 'contain',
duration: 0.0,
currentTime: 0.0,
paused: !props.autoPlay,
fullscreenMode: false,
areControlsVisible: true,
controlsTimeout: -1,
seekerOffset: 0,
seekerPosition: 0,
firstPlay: true,
seekTimeout: -1,
};
}
formatTime(time) {
let str = '';
let minutes = 0,
hours = 0,
seconds = parseInt(time, 10);
if (seconds > 60) {
minutes = parseInt(seconds / 60, 10);
seconds = seconds % 60;
if (minutes > 60) {
hours = parseInt(minutes / 60, 10);
minutes = minutes % 60;
}
str = (hours > 0 ? this.pad(hours) + ':' : '') + this.pad(minutes) + ':' + this.pad(seconds);
} else {
str = '00:' + this.pad(seconds);
}
return str;
}
pad(value) {
if (value < 10) {
return '0' + String(value);
}
return value;
}
onLoad = data => {
this.setState({
duration: data.duration,
});
const { position } = this.props;
if (!isNaN(parseFloat(position)) && position > 0) {
this.video.seek(position);
this.setState({ currentTime: position }, () => this.setSeekerPosition(this.calculateSeekerPosition()));
}
if (this.props.onMediaLoaded) {
this.props.onMediaLoaded();
}
};
onProgress = data => {
const { savePosition, claim } = this.props;
this.setState({ buffering: false, currentTime: data.currentTime });
if (data.currentTime > 0 && Math.floor(data.currentTime) % positionSaveInterval === 0) {
const { claim_id: claimId, txid, nout } = claim;
savePosition(claimId, `${txid}:${nout}`, data.currentTime);
}
if (!this.state.seeking) {
this.setSeekerPosition(this.calculateSeekerPosition());
}
if (this.state.firstPlay) {
if (this.props.onPlaybackStarted) {
this.props.onPlaybackStarted();
}
this.setState({ firstPlay: false });
this.hidePlayerControls();
}
};
clearControlsTimeout = () => {
if (this.state.controlsTimeout > -1) {
clearTimeout(this.state.controlsTimeout);
}
};
showPlayerControls = () => {
this.clearControlsTimeout();
if (!this.state.areControlsVisible) {
this.setState({ areControlsVisible: true });
}
this.hidePlayerControls();
};
manualHidePlayerControls = () => {
this.clearControlsTimeout();
this.setState({ areControlsVisible: false });
};
hidePlayerControls() {
const player = this;
let timeout = setTimeout(() => {
player.setState({ areControlsVisible: false });
}, MediaPlayer.ControlsTimeout);
player.setState({ controlsTimeout: timeout });
}
togglePlayerControls = () => {
const { setPlayerVisible, isPlayerVisible } = this.props;
if (!isPlayerVisible) {
setPlayerVisible();
}
if (this.state.areControlsVisible) {
this.manualHidePlayerControls();
} else {
this.showPlayerControls();
}
};
togglePlay = () => {
this.showPlayerControls();
this.setState({ paused: !this.state.paused }, this.handlePausedState);
};
handlePausedState = () => {
if (!this.state.paused) {
// onProgress will automatically clear this, so it's fine
this.setState({ buffering: true });
}
};
toggleFullscreenMode = () => {
this.showPlayerControls();
const { onFullscreenToggled } = this.props;
this.setState({ fullscreenMode: !this.state.fullscreenMode }, () => {
if (onFullscreenToggled) {
onFullscreenToggled(this.state.fullscreenMode);
}
});
};
onEnd = () => {
this.setState({ paused: true });
if (this.props.onPlaybackFinished) {
this.props.onPlaybackFinished();
}
this.video.seek(0);
};
setSeekerPosition(position = 0) {
position = this.checkSeekerPosition(position);
this.setState({ seekerPosition: position });
if (!this.state.seeking) {
this.setState({ seekerOffset: position });
}
}
checkSeekerPosition(val = 0) {
if (val < 0) {
val = 0;
} else if (val >= this.seekerWidth) {
return this.seekerWidth;
}
return val;
}
seekTo(time = 0) {
if (time > this.state.duration) {
return;
}
this.video.seek(time);
this.setState({ currentTime: time });
}
initSeeker() {
this.seekResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
this.clearControlsTimeout();
if (this.state.seekTimeout > 0) {
clearTimeout(this.state.seekTimeout);
}
this.setState({ seeking: true });
},
onPanResponderMove: (evt, gestureState) => {
const position = this.state.seekerOffset + gestureState.dx;
this.setSeekerPosition(position);
},
onPanResponderRelease: (evt, gestureState) => {
const time = this.getCurrentTimeForSeekerPosition();
if (time >= this.state.duration) {
this.setState({ paused: true }, this.handlePausedState);
this.onEnd();
} else {
this.seekTo(time);
this.setState({
seekTimeout: setTimeout(() => {
this.setState({ seeking: false });
}, 100),
});
}
this.hidePlayerControls();
},
});
}
getTrackingOffset() {
return this.state.fullscreenMode ? this.trackingOffset : 0;
}
getCurrentTimeForSeekerPosition() {
return this.state.duration * (this.state.seekerPosition / this.seekerWidth);
}
calculateSeekerPosition() {
return this.seekerWidth * this.getCurrentTimePercentage();
}
getCurrentTimePercentage() {
if (this.state.currentTime > 0) {
return parseFloat(this.state.currentTime) / parseFloat(this.state.duration);
}
return 0;
}
componentWillMount() {
this.initSeeker();
}
componentWillReceiveProps(nextProps) {
const { isPlayerVisible } = nextProps;
if (!isPlayerVisible && !this.state.backgroundPlayEnabled) {
// force pause if the player is not visible and background play is not enabled
this.setState({ paused: true });
}
}
componentDidMount() {
const { assignPlayer, backgroundPlayEnabled } = this.props;
if (assignPlayer) {
assignPlayer(this);
}
this.setState({ backgroundPlayEnabled: !!backgroundPlayEnabled });
this.setSeekerPosition(this.calculateSeekerPosition());
AppState.addEventListener('change', this.handleAppStateChange);
DeviceEventEmitter.addListener('onBackgroundPlayPressed', this.play);
DeviceEventEmitter.addListener('onBackgroundPausePressed', this.pause);
}
componentWillUnmount() {
AppState.removeEventListener('change', this.handleAppStateChange);
DeviceEventEmitter.removeListener('onBackgroundPlayPressed', this.play);
DeviceEventEmitter.removeListener('onBackgroundPausePressed', this.pause);
this.clearControlsTimeout();
this.setState({ paused: true, fullscreenMode: false });
const { onFullscreenToggled } = this.props;
if (onFullscreenToggled) {
onFullscreenToggled(false);
}
}
handleAppStateChange = () => {
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
if (!this.state.backgroundPlayEnabled && !this.state.paused) {
this.setState({ paused: true, autoPaused: true });
}
}
if (AppState.currentState && AppState.currentState.match(/active/)) {
if (!this.state.backgroundPlayEnabled && this.state.autoPaused) {
this.setState({ paused: false, autoPaused: false });
}
}
};
onBuffer = () => {
if (!this.state.paused) {
this.setState({ buffering: true }, () => this.manualHidePlayerControls());
}
};
play = () => {
this.setState({ paused: false }, this.updateBackgroundMediaNotification);
};
pause = () => {
this.setState({ paused: true }, this.updateBackgroundMediaNotification);
};
updateBackgroundMediaNotification = () => {
this.handlePausedState();
const { backgroundPlayEnabled } = this.props;
if (backgroundPlayEnabled) {
if (NativeModules.BackgroundMedia && window.currentMediaInfo) {
const { title, channel, uri } = window.currentMediaInfo;
NativeModules.BackgroundMedia.showPlaybackNotification(title, channel, uri, this.state.paused);
}
}
};
renderPlayerControls() {
const { onBackButtonPressed } = this.props;
if (this.state.areControlsVisible) {
return (
<View style={mediaPlayerStyle.playerControlsContainer}>
<TouchableOpacity style={mediaPlayerStyle.backButton} onPress={onBackButtonPressed}>
<Icon name={'arrow-left'} size={18} style={mediaPlayerStyle.backButtonIcon} />
</TouchableOpacity>
<TouchableOpacity style={mediaPlayerStyle.playPauseButton} onPress={this.togglePlay}>
{this.state.paused && <Icon name="play" size={40} color="#ffffff" />}
{!this.state.paused && <Icon name="pause" size={40} color="#ffffff" />}
</TouchableOpacity>
<TouchableOpacity style={mediaPlayerStyle.toggleFullscreenButton} onPress={this.toggleFullscreenMode}>
{this.state.fullscreenMode && <Icon name="compress" size={16} color="#ffffff" />}
{!this.state.fullscreenMode && <Icon name="expand" size={16} color="#ffffff" />}
</TouchableOpacity>
<Text style={mediaPlayerStyle.elapsedDuration}>{this.formatTime(this.state.currentTime)}</Text>
<Text style={mediaPlayerStyle.totalDuration}>{this.formatTime(this.state.duration)}</Text>
</View>
);
}
return null;
}
onSeekerTouchAreaPressed = evt => {
if (evt && evt.nativeEvent) {
const newSeekerPosition = evt.nativeEvent.locationX;
if (!isNaN(newSeekerPosition)) {
const time = this.state.duration * (newSeekerPosition / this.seekerWidth);
this.setSeekerPosition(newSeekerPosition);
this.seekTo(time);
}
}
};
onTrackingLayout = evt => {
this.trackingOffset = evt.nativeEvent.layout.x;
this.seekerWidth = evt.nativeEvent.layout.width;
this.setSeekerPosition(this.calculateSeekerPosition());
};
render() {
const { onLayout, source, style, thumbnail } = this.props;
const completedWidth = this.getCurrentTimePercentage() * this.seekerWidth;
const remainingWidth = this.seekerWidth - completedWidth;
let styles = [this.state.fullscreenMode ? mediaPlayerStyle.fullscreenContainer : mediaPlayerStyle.container];
if (style) {
if (style.length) {
styles = styles.concat(style);
} else {
styles.push(style);
}
}
const trackingStyle = [
mediaPlayerStyle.trackingControls,
this.state.fullscreenMode
? mediaPlayerStyle.fullscreenTrackingControls
: mediaPlayerStyle.containedTrackingControls,
];
return (
<View style={styles} onLayout={onLayout}>
<Video
source={{ uri: source }}
bufferConfig={{
minBufferMs: 15000,
maxBufferMs: 60000,
bufferForPlaybackMs: 5000,
bufferForPlaybackAfterRebufferMs: 5000,
}}
ref={(ref: Video) => {
this.video = ref;
}}
resizeMode={this.state.resizeMode}
playInBackground={this.state.backgroundPlayEnabled}
style={mediaPlayerStyle.player}
rate={this.state.rate}
volume={this.state.volume}
paused={this.state.paused}
onLoad={this.onLoad}
onBuffer={this.onBuffer}
onProgress={this.onProgress}
onEnd={this.onEnd}
onError={this.onError}
minLoadRetryCount={999}
/>
{this.state.firstPlay && thumbnail && thumbnail.trim().length > 0 && (
<FastImage
source={{ uri: thumbnail }}
resizeMode={FastImage.resizeMode.cover}
style={mediaPlayerStyle.playerThumbnail}
/>
)}
<TouchableOpacity style={mediaPlayerStyle.playerControls} onPress={this.togglePlayerControls}>
{this.renderPlayerControls()}
</TouchableOpacity>
{(!this.state.fullscreenMode || (this.state.fullscreenMode && this.state.areControlsVisible)) && (
<View style={trackingStyle} onLayout={this.onTrackingLayout}>
<View style={mediaPlayerStyle.progress}>
<View style={[mediaPlayerStyle.innerProgressCompleted, { width: completedWidth }]} />
<View style={[mediaPlayerStyle.innerProgressRemaining, { width: remainingWidth }]} />
</View>
</View>
)}
{this.state.buffering && (
<View style={mediaPlayerStyle.loadingContainer}>
<ActivityIndicator color={Colors.LbryGreen} size="large" />
</View>
)}
{this.state.areControlsVisible && (
<View style={{ left: this.getTrackingOffset(), width: this.seekerWidth }}>
<View
style={[
mediaPlayerStyle.seekerHandle,
this.state.fullscreenMode ? mediaPlayerStyle.seekerHandleFs : mediaPlayerStyle.seekerHandleContained,
{ left: this.state.seekerPosition },
]}
{...this.seekResponder.panHandlers}
>
<View style={this.state.seeking ? mediaPlayerStyle.bigSeekerCircle : mediaPlayerStyle.seekerCircle} />
</View>
<TouchableOpacity
style={[
mediaPlayerStyle.seekerTouchArea,
this.state.fullscreenMode
? mediaPlayerStyle.seekerTouchAreaFs
: mediaPlayerStyle.seekerTouchAreaContained,
]}
onPress={this.onSeekerTouchAreaPressed}
/>
</View>
)}
</View>
);
}
}
export default MediaPlayer;

View file

@ -1,4 +0,0 @@
import { connect } from 'react-redux';
import NavigationButton from './view';
export default connect()(NavigationButton);

View file

@ -1,17 +0,0 @@
import React from 'react';
import Icon from 'react-native-vector-icons/FontAwesome5';
import { TouchableOpacity } from 'react-native';
class NavigationButton extends React.PureComponent {
render() {
const { iconStyle, name, onPress, size, style } = this.props;
return (
<TouchableOpacity onPress={onPress} style={style}>
<Icon name={name} size={size} style={iconStyle} />
</TouchableOpacity>
);
}
}
export default NavigationButton;

View file

@ -1,9 +0,0 @@
import { connect } from 'react-redux';
import NsfwOverlay from './view';
const perform = dispatch => ({});
export default connect(
null,
perform
)(NsfwOverlay);

View file

@ -1,17 +0,0 @@
import React from 'react';
import { Text, TouchableOpacity } from 'react-native';
import discoverStyle from '../../styles/discover';
class NsfwOverlay extends React.PureComponent {
render() {
return (
<TouchableOpacity style={discoverStyle.overlay} activeOpacity={0.95} onPress={this.props.onPress}>
<Text style={discoverStyle.overlayText}>
This content is Not Safe For Work. To view adult content, please change your Settings.
</Text>
</TouchableOpacity>
);
}
}
export default NsfwOverlay;

View file

@ -1,9 +0,0 @@
import { connect } from 'react-redux';
import PageHeader from './view';
const perform = dispatch => ({});
export default connect(
null,
perform
)(PageHeader);

View file

@ -1,39 +0,0 @@
// Based on https://github.com/react-navigation/react-navigation/blob/master/src/views/Header/Header.js
import React from 'react';
import { Animated, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome5';
import NavigationButton from 'component/navigationButton';
import pageHeaderStyle from 'styles/pageHeader';
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
const AnimatedText = Animated.Text;
class PageHeader extends React.PureComponent {
render() {
const { title, onBackPressed } = this.props;
const containerStyles = [pageHeaderStyle.container, { height: APPBAR_HEIGHT }];
return (
<View style={containerStyles}>
<View style={pageHeaderStyle.flexOne}>
<View style={pageHeaderStyle.header}>
<View style={pageHeaderStyle.title}>
<AnimatedText numberOfLines={1} style={pageHeaderStyle.titleText} accessibilityTraits="header">
{title}
</AnimatedText>
</View>
<NavigationButton
name="arrow-left"
style={pageHeaderStyle.left}
size={24}
iconStyle={pageHeaderStyle.backIcon}
onPress={onBackPressed}
/>
</View>
</View>
</View>
);
}
}
export default PageHeader;

View file

@ -1,4 +0,0 @@
import { connect } from 'react-redux';
import ProgressBar from './view';
export default connect()(ProgressBar);

View file

@ -1,56 +0,0 @@
// @flow
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
const defaultHeight = 5;
const defaultBorderRadius = 5;
const minProgress = 0;
const maxProgress = 100;
class ProgressBar extends React.PureComponent {
static propTypes = {
borderRadius: PropTypes.number,
color: PropTypes.string.isRequired,
height: PropTypes.number,
progress: function(props, propName, componentName) {
const value = parseInt(props[propName], 10);
if (isNaN(value) || props[propName] < minProgress || props[propName] > maxProgress) {
return new Error('progress should be between 0 and 100');
}
},
style: PropTypes.any,
};
render() {
const { borderRadius, color, height, progress, style } = this.props;
const currentProgress = Math.ceil(progress);
let styles = [];
if (style) {
if (style.length) {
styles = styles.concat(style);
} else {
styles.push(style);
}
}
styles.push({
borderRadius: borderRadius || defaultBorderRadius,
flexDirection: 'row',
height: height || defaultHeight,
overflow: 'hidden',
});
return (
<View style={styles}>
<View
style={{ backgroundColor: color, borderRadius: borderRadius || defaultBorderRadius, flex: currentProgress }}
/>
<View style={{ backgroundColor: color, opacity: 0.2, flex: 100 - currentProgress }} />
</View>
);
}
}
export default ProgressBar;

View file

@ -1,4 +0,0 @@
import { connect } from 'react-redux';
import PublishRewardsDriver from './view';
export default connect()(PublishRewardsDriver);

View file

@ -1,20 +0,0 @@
import React from 'react';
import { Text, TouchableOpacity } from 'react-native';
import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
import publishStyle from 'styles/publish';
class PublishRewadsDriver extends React.PureComponent<Props> {
render() {
const { navigation } = this.props;
return (
<TouchableOpacity style={publishStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}>
<Icon name="award" size={16} style={publishStyle.rewardIcon} />
<Text style={publishStyle.rewardDriverText}>Earn some credits to be able to publish your content.</Text>
</TouchableOpacity>
);
}
}
export default PublishRewadsDriver;

View file

@ -1,25 +0,0 @@
import { connect } from 'react-redux';
import {
makeSelectClaimForUri,
doSearch,
makeSelectRecommendedContentForUri,
makeSelectTitleForUri,
selectIsSearching,
} from 'lbry-redux';
import RelatedContent from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
recommendedContent: makeSelectRecommendedContentForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state),
isSearching: selectIsSearching(state),
});
const perform = dispatch => ({
search: query => dispatch(doSearch(query, 20, undefined, true)),
});
export default connect(
select,
perform
)(RelatedContent);

View file

@ -1,67 +0,0 @@
import React from 'react';
import { ActivityIndicator, FlatList, Text, View } from 'react-native';
import { navigateToUri } from 'utils/helper';
import Colors from 'styles/colors';
import FileListItem from 'component/fileListItem';
import fileListStyle from 'styles/fileList';
import relatedContentStyle from 'styles/relatedContent';
export default class RelatedContent extends React.PureComponent<Props> {
constructor() {
super();
this.didSearch = undefined;
}
componentDidMount() {
this.getRecommendedContent();
}
componentDidUpdate(prevProps: Props) {
const { claim, uri } = this.props;
if (uri !== prevProps.uri) {
this.didSearch = false;
}
if (claim && !this.didSearch) {
this.getRecommendedContent();
}
}
getRecommendedContent() {
const { search, title } = this.props;
if (title) {
search(title);
this.didSearch = true;
}
}
didSearch: ?boolean;
render() {
const { recommendedContent, isSearching, navigation } = this.props;
if (!isSearching && (!recommendedContent || recommendedContent.length === 0)) {
return null;
}
return (
<View style={relatedContentStyle.container}>
<Text style={relatedContentStyle.title}>Related Content</Text>
{recommendedContent &&
recommendedContent.map(recommendedUri => (
<FileListItem
style={fileListStyle.item}
key={recommendedUri}
uri={recommendedUri}
navigation={navigation}
onPress={() => navigateToUri(navigation, recommendedUri, { autoplay: true })}
/>
))}
{isSearching && <ActivityIndicator size="small" color={Colors.LbryGreen} style={relatedContentStyle.loading} />}
</View>
);
}
}

View file

@ -1,32 +0,0 @@
import { connect } from 'react-redux';
import { doToast } from 'lbry-redux';
import {
doClaimRewardType,
doClaimRewardClearError,
makeSelectClaimRewardError,
makeSelectIsRewardClaimPending,
} from 'lbryinc';
import RewardCard from './view';
const makeSelect = () => {
const selectIsPending = makeSelectIsRewardClaimPending();
const selectError = makeSelectClaimRewardError();
const select = (state, props) => ({
errorMessage: selectError(state, props),
isPending: selectIsPending(state, props),
});
return select;
};
const perform = dispatch => ({
claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)),
clearError: reward => dispatch(doClaimRewardClearError(reward)),
notify: data => dispatch(doToast(data)),
});
export default connect(
makeSelect,
perform
)(RewardCard);

View file

@ -1,112 +0,0 @@
// @flow
import React from 'react';
import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
import Colors from '../../styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from '../link';
import rewardStyle from '../../styles/reward';
type Props = {
canClaim: boolean,
onClaimPress: object,
reward: {
id: string,
reward_title: string,
reward_amount: number,
transaction_id: string,
created_at: string,
reward_description: string,
reward_type: string,
},
};
class RewardCard extends React.PureComponent<Props> {
state = {
claimStarted: false,
};
componentWillReceiveProps(nextProps) {
const { errorMessage, isPending } = nextProps;
const { clearError, notify, reward } = this.props;
if (this.state.claimStarted && !isPending) {
if (errorMessage && errorMessage.trim().length > 0) {
notify({ message: errorMessage });
clearError(reward);
} else {
notify({ message: 'Reward successfully claimed!' });
}
this.setState({ claimStarted: false });
}
}
onClaimPress = () => {
const { canClaim, claimReward, notify, reward, showVerification } = this.props;
if (!canClaim) {
if (showVerification) {
showVerification();
}
notify({ message: 'Unfortunately, you are not eligible to claim this reward at this time.' });
return;
}
this.setState({ claimStarted: true }, () => {
claimReward(reward);
});
};
render() {
const { canClaim, isPending, onClaimPress, reward } = this.props;
const claimed = !!reward.transaction_id;
return (
<TouchableOpacity
style={[rewardStyle.rewardCard, rewardStyle.row]}
onPress={() => {
if (!isPending && !claimed) {
this.onClaimPress();
}
}}
>
<View style={rewardStyle.leftCol}>
{!isPending && (
<TouchableOpacity
onPress={() => {
if (!claimed) {
this.onClaimPress();
}
}}
>
{claimed && (
<Icon
name={claimed ? 'check-circle' : 'circle'}
style={claimed ? rewardStyle.claimed : canClaim ? rewardStyle.unclaimed : rewardStyle.disabled}
size={20}
/>
)}
</TouchableOpacity>
)}
{isPending && <ActivityIndicator size="small" color={Colors.LbryGreen} />}
</View>
<View style={rewardStyle.midCol}>
<Text style={rewardStyle.rewardTitle}>{reward.reward_title}</Text>
<Text style={rewardStyle.rewardDescription}>{reward.reward_description}</Text>
{claimed && (
<Link
style={rewardStyle.link}
href={`https://explorer.lbry.com/tx/${reward.transaction_id}`}
text={reward.transaction_id.substring(0, 7)}
error={'The transaction URL could not be opened'}
/>
)}
</View>
<View style={rewardStyle.rightCol}>
<Text style={rewardStyle.rewardAmount}>{reward.reward_amount}</Text>
<Text style={rewardStyle.rewardCurrency}>LBC</Text>
</View>
</TouchableOpacity>
);
}
}
export default RewardCard;

View file

@ -1,22 +0,0 @@
import { connect } from 'react-redux';
import { doToast } from 'lbry-redux';
import { doSetClientSetting } from 'redux/actions/settings';
import { doRewardList, selectUnclaimedRewardValue, selectFetchingRewards, selectUser } from 'lbryinc';
import RewardEnrolment from './view';
const select = state => ({
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
fetching: selectFetchingRewards(state),
user: selectUser(state),
});
const perform = dispatch => ({
fetchRewards: () => dispatch(doRewardList()),
notify: data => dispatch(doToast(data)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
});
export default connect(
select,
perform
)(RewardEnrolment);

View file

@ -1,53 +0,0 @@
import React from 'react';
import { NativeModules, Text, TouchableOpacity, View } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import Button from 'component/button';
import Constants from 'constants';
import Link from 'component/link';
import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
import rewardStyle from 'styles/reward';
class RewardEnrolment extends React.Component {
componentDidMount() {
this.props.fetchRewards();
}
onNotInterestedPressed = () => {
const { navigation, setClientSetting } = this.props;
setClientSetting(Constants.SETTING_REWARDS_NOT_INTERESTED, true);
navigation.navigate({ routeName: 'DiscoverStack' });
};
onEnrollPressed = () => {
const { navigation } = this.props;
navigation.navigate({ routeName: 'Verification', key: 'verification', params: { syncFlow: false } });
};
render() {
const { fetching, navigation, unclaimedRewardAmount, user } = this.props;
return (
<View style={rewardStyle.enrollContainer} onPress>
<View style={rewardStyle.summaryRow}>
<Icon name="award" size={36} color={Colors.White} />
<Text style={rewardStyle.summaryText}>{unclaimedRewardAmount} unclaimed credits</Text>
</View>
<View style={rewardStyle.onboarding}>
<Text style={rewardStyle.enrollDescText}>
LBRY credits allow you to purchase content, publish content, and influence the network. You can start
earning credits by watching videos on LBRY.
</Text>
</View>
<View style={rewardStyle.buttonRow}>
<Link style={rewardStyle.notInterestedLink} text={'Not interested'} onPress={this.onNotInterestedPressed} />
<Button style={rewardStyle.enrollButton} theme={'light'} text={'Enroll'} onPress={this.onEnrollPressed} />
</View>
</View>
);
}
}
export default RewardEnrolment;

View file

@ -1,20 +0,0 @@
import { connect } from 'react-redux';
import { doToast } from 'lbry-redux';
import { doRewardList, selectUnclaimedRewardValue, selectFetchingRewards, selectUser } from 'lbryinc';
import RewardSummary from './view';
const select = state => ({
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
fetching: selectFetchingRewards(state),
user: selectUser(state),
});
const perform = dispatch => ({
fetchRewards: () => dispatch(doRewardList()),
notify: data => dispatch(doToast(data)),
});
export default connect(
select,
perform
)(RewardSummary);

View file

@ -1,82 +0,0 @@
import React from 'react';
import { NativeModules, Text, TouchableOpacity, View } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import Button from 'component/button';
import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
import rewardStyle from 'styles/reward';
class RewardSummary extends React.Component {
static itemKey = 'rewardSummaryDismissed';
state = {
actionsLeft: 0,
dismissed: false,
};
componentDidMount() {
this.props.fetchRewards();
AsyncStorage.getItem(RewardSummary.itemKey).then(isDismissed => {
if ('true' === isDismissed) {
this.setState({ dismissed: true });
}
const { user } = this.props;
let actionsLeft = 0;
if (!user || !user.has_verified_email) {
actionsLeft++;
}
if (!user || !user.is_identity_verified) {
actionsLeft++;
}
this.setState({ actionsLeft });
});
}
onDismissPressed = () => {
AsyncStorage.setItem(RewardSummary.itemKey, 'true');
this.setState({ dismissed: true });
this.props.notify({
message: 'You can always claim your rewards from the Rewards page.',
});
};
handleSummaryPressed = () => {
const { showVerification } = this.props;
if (showVerification) {
showVerification();
}
};
render() {
const { fetching, navigation, unclaimedRewardAmount, user } = this.props;
if (!user) {
return null;
}
if (
this.state.dismissed ||
(user && user.is_reward_approved) ||
this.state.actionsLeft === 0 ||
unclaimedRewardAmount === 0
) {
return null;
}
return (
<TouchableOpacity style={rewardStyle.summaryContainer} onPress={this.handleSummaryPressed}>
<View style={rewardStyle.summaryRow}>
<Icon name="award" size={36} color={Colors.White} />
<Text style={rewardStyle.summaryText}>{unclaimedRewardAmount} unclaimed credits</Text>
</View>
<Button style={rewardStyle.dismissButton} theme={'light'} text={'Dismiss'} onPress={this.onDismissPressed} />
</TouchableOpacity>
);
}
}
export default RewardSummary;

View file

@ -1,19 +0,0 @@
import { connect } from 'react-redux';
import { NativeModules } from 'react-native';
import { doSearch, doUpdateSearchQuery } from 'lbry-redux';
import SearchInput from './view';
const perform = dispatch => ({
search: search => {
if (NativeModules.Firebase) {
NativeModules.Firebase.track('search', { query: search });
}
return dispatch(doSearch(search));
},
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query, false)),
});
export default connect(
null,
perform
)(SearchInput);

View file

@ -1,41 +0,0 @@
import React from 'react';
import { TextInput } from 'react-native';
class SearchInput extends React.PureComponent {
static INPUT_TIMEOUT = 500;
state = {
changeTextTimeout: -1,
};
handleChangeText = text => {
clearTimeout(this.state.changeTextTimeout);
if (!text || text.trim().length < 2) {
// only perform a search if 2 or more characters have been input
return;
}
const { search, updateSearchQuery } = this.props;
updateSearchQuery(text);
let timeout = setTimeout(() => {
search(text);
}, SearchInput.INPUT_TIMEOUT);
this.setState({ changeTextTimeout: timeout });
};
render() {
const { style, value } = this.props;
return (
<TextInput
style={style}
placeholder="Search"
underlineColorAndroid="transparent"
value={value}
onChangeText={text => this.handleChangeText(text)}
/>
);
}
}
export default SearchInput;

View file

@ -1,14 +0,0 @@
import { connect } from 'react-redux';
import SearchRightHeaderIcon from './view';
import { ACTIONS } from 'lbry-redux';
const perform = dispatch => ({
clearQuery: () =>
dispatch({
type: ACTIONS.HISTORY_NAVIGATE,
}),
});
export default connect(
null,
perform
)(SearchRightHeaderIcon);

View file

@ -1,19 +0,0 @@
import React from 'react';
import { NavigationActions } from 'react-navigation';
import Feather from 'react-native-vector-icons/Feather';
class SearchRightHeaderIcon extends React.PureComponent {
clearAndGoBack() {
const { navigation } = this.props;
this.props.clearQuery();
navigation.dispatch(NavigationActions.back());
}
render() {
const { style } = this.props;
return <Feather name="x" size={24} style={style} onPress={() => this.clearAndGoBack()} />;
}
}
export default SearchRightHeaderIcon;

View file

@ -1,4 +0,0 @@
import { connect } from 'react-redux';
import StorageStatsCard from './view';
export default connect()(StorageStatsCard);

View file

@ -1,133 +0,0 @@
import React from 'react';
import { normalizeURI, parseURI } from 'lbry-redux';
import { ActivityIndicator, Platform, Switch, Text, TouchableOpacity, View } from 'react-native';
import { formatBytes } from '../../utils/helper';
import Colors from '../../styles/colors';
import storageStatsStyle from '../../styles/storageStats';
class StorageStatsCard extends React.PureComponent {
state = {
totalBytes: 0,
totalAudioBytes: 0,
totalAudioPercent: 0,
totalImageBytes: 0,
totalImagePercent: 0,
totalVideoBytes: 0,
totalVideoPercent: 0,
totalOtherBytes: 0,
totalOtherPercent: 0,
showStats: false,
};
componentDidMount() {
// calculate total bytes
const { fileInfos } = this.props;
let totalBytes = 0,
totalAudioBytes = 0,
totalImageBytes = 0,
totalVideoBytes = 0;
let totalAudioPercent = 0,
totalImagePercent = 0,
totalVideoPercent = 0;
fileInfos.forEach(fileInfo => {
if (fileInfo.completed) {
const bytes = fileInfo.written_bytes;
const type = fileInfo.mime_type;
totalBytes += bytes;
if (type) {
if (type.startsWith('audio/')) totalAudioBytes += bytes;
if (type.startsWith('image/')) totalImageBytes += bytes;
if (type.startsWith('video/')) totalVideoBytes += bytes;
}
}
});
totalAudioPercent = ((totalAudioBytes / totalBytes) * 100).toFixed(2);
totalImagePercent = ((totalImageBytes / totalBytes) * 100).toFixed(2);
totalVideoPercent = ((totalVideoBytes / totalBytes) * 100).toFixed(2);
this.setState({
totalBytes,
totalAudioBytes,
totalAudioPercent,
totalImageBytes,
totalImagePercent,
totalVideoBytes,
totalVideoPercent,
totalOtherBytes: totalBytes - (totalAudioBytes + totalImageBytes + totalVideoBytes),
totalOtherPercent: (
100 -
(parseFloat(totalAudioPercent) + parseFloat(totalImagePercent) + parseFloat(totalVideoPercent))
).toFixed(2),
});
}
render() {
if (this.state.totalBytes == 0) {
return null;
}
return (
<View style={storageStatsStyle.card}>
<View style={[storageStatsStyle.row, storageStatsStyle.totalSizeContainer]}>
<View style={storageStatsStyle.summary}>
<Text style={storageStatsStyle.totalSize}>{formatBytes(this.state.totalBytes, 2)}</Text>
<Text style={storageStatsStyle.annotation}>used</Text>
</View>
<View style={[storageStatsStyle.row, storageStatsStyle.toggleStatsContainer]}>
<Text style={storageStatsStyle.statsText}>Stats</Text>
<Switch
style={storageStatsStyle.statsToggle}
value={this.state.showStats}
onValueChange={value => this.setState({ showStats: value })}
/>
</View>
</View>
{this.state.showStats && (
<View>
<View style={storageStatsStyle.distributionBar}>
<View style={[storageStatsStyle.audioDistribution, { flex: parseFloat(this.state.totalAudioPercent) }]} />
<View style={[storageStatsStyle.imageDistribution, { flex: parseFloat(this.state.totalImagePercent) }]} />
<View style={[storageStatsStyle.videoDistribution, { flex: parseFloat(this.state.totalVideoPercent) }]} />
<View style={[storageStatsStyle.otherDistribution, { flex: parseFloat(this.state.totalOtherPercent) }]} />
</View>
<View style={storageStatsStyle.legend}>
{this.state.totalAudioBytes > 0 && (
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
<View style={[storageStatsStyle.legendBox, storageStatsStyle.audioDistribution]} />
<Text style={storageStatsStyle.legendText}>Audio</Text>
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalAudioBytes, 2)}</Text>
</View>
)}
{this.state.totalImageBytes > 0 && (
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
<View style={[storageStatsStyle.legendBox, storageStatsStyle.imageDistribution]} />
<Text style={storageStatsStyle.legendText}>Images</Text>
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalImageBytes, 2)}</Text>
</View>
)}
{this.state.totalVideoBytes > 0 && (
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
<View style={[storageStatsStyle.legendBox, storageStatsStyle.videoDistribution]} />
<Text style={storageStatsStyle.legendText}>Videos</Text>
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalVideoBytes, 2)}</Text>
</View>
)}
{this.state.totalOtherBytes > 0 && (
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
<View style={[storageStatsStyle.legendBox, storageStatsStyle.otherDistribution]} />
<Text style={storageStatsStyle.legendText}>Other</Text>
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalOtherBytes, 2)}</Text>
</View>
)}
</View>
</View>
)}
</View>
);
}
}
export default StorageStatsCard;

View file

@ -1,18 +0,0 @@
import { connect } from 'react-redux';
import { doChannelSubscribe, doChannelUnsubscribe, selectSubscriptions, makeSelectIsSubscribed } from 'lbryinc';
import { doToast } from 'lbry-redux';
import SubscribeButton from './view';
const select = (state, props) => ({
subscriptions: selectSubscriptions(state),
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
});
export default connect(
select,
{
doChannelSubscribe,
doChannelUnsubscribe,
doToast,
}
)(SubscribeButton);

View file

@ -1,44 +0,0 @@
import React from 'react';
import { normalizeURI, parseURI } from 'lbry-redux';
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
import Button from 'component/button';
import Colors from 'styles/colors';
class SubscribeButton extends React.PureComponent {
render() {
const { uri, isSubscribed, doChannelSubscribe, doChannelUnsubscribe, style, hideText } = this.props;
let styles = [];
if (style) {
if (style.length) {
styles = styles.concat(style);
} else {
styles.push(style);
}
}
const iconColor = isSubscribed ? null : Colors.Red;
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
const subscriptionLabel = isSubscribed ? null : __('Subscribe');
const { claimName } = parseURI(uri);
return (
<Button
style={styles}
theme={'light'}
icon={isSubscribed ? 'heart-broken' : 'heart'}
iconColor={iconColor}
solid={isSubscribed ? false : true}
text={hideText ? null : subscriptionLabel}
onPress={() => {
subscriptionHandler({
channelName: claimName,
uri: normalizeURI(uri),
});
}}
/>
);
}
}
export default SubscribeButton;

View file

@ -1,25 +0,0 @@
import { connect } from 'react-redux';
import {
doChannelSubscriptionEnableNotifications,
doChannelSubscriptionDisableNotifications,
selectEnabledChannelNotifications,
selectSubscriptions,
makeSelectIsSubscribed,
} from 'lbryinc';
import { doToast } from 'lbry-redux';
import SubscribeNotificationButton from './view';
const select = (state, props) => ({
enabledChannelNotifications: selectEnabledChannelNotifications(state),
subscriptions: selectSubscriptions(state),
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
});
export default connect(
select,
{
doChannelSubscriptionEnableNotifications,
doChannelSubscriptionDisableNotifications,
doToast,
}
)(SubscribeNotificationButton);

View file

@ -1,56 +0,0 @@
import React from 'react';
import { parseURI } from 'lbry-redux';
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
import Button from 'component/button';
import Colors from 'styles/colors';
class SubscribeNotificationButton extends React.PureComponent {
render() {
const {
uri,
name,
doChannelSubscriptionEnableNotifications,
doChannelSubscriptionDisableNotifications,
doToast,
enabledChannelNotifications,
isSubscribed,
style,
} = this.props;
if (!isSubscribed) {
return null;
}
let styles = [];
if (style) {
if (style.length) {
styles = styles.concat(style);
} else {
styles.push(style);
}
}
const shouldNotify = enabledChannelNotifications.indexOf(name) > -1;
const { claimName } = parseURI(uri);
return (
<Button
style={styles}
theme={'light'}
icon={shouldNotify ? 'bell-slash' : 'bell'}
solid={true}
onPress={() => {
if (shouldNotify) {
doChannelSubscriptionDisableNotifications(name);
doToast({ message: 'You will not receive notifications for new content.' });
} else {
doChannelSubscriptionEnableNotifications(name);
doToast({ message: 'You will receive all notifications for new content.' });
}
}}
/>
);
}
}
export default SubscribeNotificationButton;

View file

@ -1,25 +0,0 @@
import { connect } from 'react-redux';
import {
makeSelectFetchingChannelClaims,
makeSelectClaimsInChannelForPage,
doFetchClaimsByChannel,
doResolveUris,
} from 'lbry-redux';
import { selectShowNsfw } from 'redux/selectors/settings';
import SuggestedSubscriptionItem from './view';
const select = (state, props) => ({
claims: makeSelectClaimsInChannelForPage(props.categoryLink)(state),
fetching: makeSelectFetchingChannelClaims(props.categoryLink)(state),
obscureNsfw: !selectShowNsfw(state),
});
const perform = dispatch => ({
fetchChannel: channel => dispatch(doFetchClaimsByChannel(channel)),
resolveUris: uris => dispatch(doResolveUris(uris, true)),
});
export default connect(
select,
perform
)(SuggestedSubscriptionItem);

View file

@ -1,77 +0,0 @@
import React from 'react';
import { buildURI, normalizeURI } from 'lbry-redux';
import { ActivityIndicator, FlatList, Text, View } from 'react-native';
import Colors from 'styles/colors';
import discoverStyle from 'styles/discover';
import FileItem from 'component/fileItem';
import subscriptionsStyle from 'styles/subscriptions';
class SuggestedSubscriptionItem extends React.PureComponent {
componentDidMount() {
const { fetching, categoryLink, fetchChannel, resolveUris, claims } = this.props;
if (!fetching && categoryLink && (!claims || claims.length)) {
fetchChannel(categoryLink);
}
}
uriForClaim = claim => {
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = claim;
const uriParams = {};
// This is unfortunate
// https://github.com/lbryio/lbry/issues/1159
const name = claimName || claimNameDownloaded;
uriParams.contentName = name;
uriParams.claimId = claimId;
const uri = buildURI(uriParams);
return uri;
};
render() {
const { categoryLink, fetching, obscureNsfw, claims, navigation } = this.props;
if (!claims || !claims.length) {
return (
<View style={subscriptionsStyle.busyContainer}>
<ActivityIndicator size={'small'} color={Colors.LbryGreen} />
</View>
);
}
if (claims && claims.length > 0) {
return (
<View style={subscriptionsStyle.suggestedContainer}>
<FileItem
style={subscriptionsStyle.compactMainFileItem}
mediaStyle={subscriptionsStyle.fileItemMedia}
uri={this.uriForClaim(claims[0])}
navigation={navigation}
/>
{claims.length > 1 && (
<FlatList
style={subscriptionsStyle.compactItems}
horizontal={true}
renderItem={({ item }) => (
<FileItem
style={subscriptionsStyle.compactFileItem}
mediaStyle={subscriptionsStyle.compactFileItemMedia}
key={item}
uri={normalizeURI(item)}
navigation={navigation}
compactView={true}
/>
)}
data={claims.slice(1, 4).map(claim => this.uriForClaim(claim))}
keyExtractor={(item, index) => item}
/>
)}
</View>
);
}
return null;
}
}
export default SuggestedSubscriptionItem;

View file

@ -1,13 +0,0 @@
import { connect } from 'react-redux';
import { selectSuggestedChannels, selectIsFetchingSuggested } from 'lbryinc';
import SuggestedSubscriptions from './view';
const select = state => ({
suggested: selectSuggestedChannels(state),
loading: selectIsFetchingSuggested(state),
});
export default connect(
select,
null
)(SuggestedSubscriptions);

View file

@ -1,54 +0,0 @@
import React from 'react';
import { ActivityIndicator, SectionList, Text, View } from 'react-native';
import { normalizeURI } from 'lbry-redux';
import { navigateToUri } from 'utils/helper';
import SubscribeButton from 'component/subscribeButton';
import SuggestedSubscriptionItem from 'component/suggestedSubscriptionItem';
import Colors from 'styles/colors';
import discoverStyle from 'styles/discover';
import subscriptionsStyle from 'styles/subscriptions';
import Link from 'component/link';
class SuggestedSubscriptions extends React.PureComponent {
render() {
const { suggested, loading, navigation } = this.props;
if (loading) {
return (
<View>
<ActivityIndicator size="large" color={Colors.LbryGreen} />
</View>
);
}
return suggested ? (
<SectionList
style={subscriptionsStyle.scrollContainer}
renderItem={({ item, index, section }) => (
<SuggestedSubscriptionItem key={item} categoryLink={normalizeURI(item)} navigation={navigation} />
)}
renderSectionHeader={({ section: { title } }) => {
const titleParts = title.split(';');
const channelName = titleParts[0];
const channelUri = normalizeURI(titleParts[1]);
return (
<View style={subscriptionsStyle.titleRow}>
<Link
style={subscriptionsStyle.channelTitle}
text={channelName}
onPress={() => {
navigateToUri(navigation, normalizeURI(channelUri));
}}
/>
<SubscribeButton style={subscriptionsStyle.subscribeButton} uri={channelUri} name={channelName} />
</View>
);
}}
sections={suggested.map(({ uri, label }) => ({ title: label + ';' + uri, data: [uri] }))}
keyExtractor={(item, index) => item}
/>
) : null;
}
}
export default SuggestedSubscriptions;

View file

@ -1,4 +0,0 @@
import { connect } from 'react-redux';
import Tag from './view';
export default connect()(Tag);

View file

@ -1,55 +0,0 @@
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import tagStyle from 'styles/tag';
import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
export default class Tag extends React.PureComponent {
onPressDefault = () => {
const { name, navigation, type, onAddPress, onRemovePress } = this.props;
if ('add' === type) {
if (onAddPress) {
onAddPress(name);
}
return;
}
if ('remove' === type) {
if (onRemovePress) {
onRemovePress(name);
}
return;
}
if (navigation) {
// navigate to tag page
}
};
render() {
const { name, onPress, style, type } = this.props;
let styles = [];
if (style) {
if (style.length) {
styles = styles.concat(style);
} else {
styles.push(style);
}
}
styles.push({
backgroundColor: Colors.TagGreen,
borderRadius: 8,
marginBottom: 4,
});
return (
<TouchableOpacity style={styles} onPress={onPress || this.onPressDefault}>
<View style={tagStyle.content}>
<Text style={tagStyle.text}>{name}</Text>
{type && <Icon style={tagStyle.icon} name={type === 'add' ? 'plus' : 'times'} size={8} />}
</View>
</TouchableOpacity>
);
}
}

View file

@ -1,12 +0,0 @@
import { connect } from 'react-redux';
import { selectUnfollowedTags } from 'lbry-redux';
import TagSearch from './view';
const select = state => ({
unfollowedTags: selectUnfollowedTags(state),
});
export default connect(
select,
null
)(TagSearch);

View file

@ -1,81 +0,0 @@
import React from 'react';
import { Text, TextInput, TouchableOpacity, View } from 'react-native';
import Tag from 'component/tag';
import tagStyle from 'styles/tag';
import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
export default class TagSearch extends React.PureComponent {
state = {
tag: null,
tagResults: [],
};
componentDidMount() {
const { selectedTags = [] } = this.props;
this.updateTagResults(this.state.tag, selectedTags);
}
componentWillReceiveProps(nextProps) {
const { selectedTags: prevSelectedTags = [] } = this.props;
const { selectedTags = [] } = nextProps;
if (selectedTags.length !== prevSelectedTags.length) {
this.updateTagResults(this.state.tag, selectedTags);
}
}
onAddTagPress = tag => {
const { handleAddTag } = this.props;
if (handleAddTag) {
handleAddTag(tag);
}
};
handleTagChange = tag => {
const { selectedTags = [] } = this.props;
this.setState({ tag });
this.updateTagResults(tag, selectedTags);
};
updateTagResults = (tag, selectedTags = []) => {
const { unfollowedTags } = this.props;
// the search term should always be the first result
let results = [];
const tagNotSelected = name => selectedTags.indexOf(name.toLowerCase()) === -1;
const suggestedTagsSet = new Set(unfollowedTags.map(tag => tag.name));
const suggestedTags = Array.from(suggestedTagsSet).filter(tagNotSelected);
if (tag && tag.trim().length > 0) {
results.push(tag.toLowerCase());
const doesTagMatch = name => name.toLowerCase().includes(tag.toLowerCase());
results = results.concat(suggestedTags.filter(doesTagMatch).slice(0, 5));
} else {
results = results.concat(suggestedTags.slice(0, 5));
}
this.setState({ tagResults: results });
};
render() {
const { name, style, type, selectedTags = [] } = this.props;
return (
<View>
<TextInput
style={tagStyle.searchInput}
placeholder={'Search for more tags'}
underlineColorAndroid={Colors.NextLbryGreen}
value={this.state.tag}
numberOfLines={1}
onChangeText={this.handleTagChange}
/>
<View style={tagStyle.tagResultsList}>
{this.state.tagResults.map(tag => (
<Tag key={tag} name={tag} style={tagStyle.tag} type="add" onAddPress={name => this.onAddTagPress(name)} />
))}
</View>
</View>
);
}
}

View file

@ -1,14 +0,0 @@
import { connect } from 'react-redux';
//import { selectClaimedRewardsByTransactionId } from 'redux/selectors/rewards';
import { selectAllMyClaimsByOutpoint } from 'lbry-redux';
import TransactionList from './view';
const select = state => ({
//rewards: selectClaimedRewardsByTransactionId(state),
myClaims: selectAllMyClaimsByOutpoint(state),
});
export default connect(
select,
null
)(TransactionList);

View file

@ -1,61 +0,0 @@
// @flow
import React from 'react';
import { Text, View, Linking } from 'react-native';
import { buildURI, formatCredits } from 'lbry-redux';
import { navigateToUri } from '../../../utils/helper';
import Link from '../../link';
import moment from 'moment';
import transactionListStyle from '../../../styles/transactionList';
class TransactionListItem extends React.PureComponent {
capitalize(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
render() {
const { transaction, navigation } = this.props;
const { amount, claim_id: claimId, claim_name: name, date, fee, txid, type } = transaction;
return (
<View style={transactionListStyle.listItem}>
<View style={[transactionListStyle.row, transactionListStyle.topRow]}>
<View style={transactionListStyle.col}>
<Text style={transactionListStyle.text}>{this.capitalize(type)}</Text>
{name && claimId && (
<Link
style={transactionListStyle.link}
onPress={() => navigateToUri(navigation, buildURI({ claimName: name, claimId }))}
text={name}
/>
)}
</View>
<View style={transactionListStyle.col}>
<Text style={[transactionListStyle.amount, transactionListStyle.text]}>{formatCredits(amount, 8)}</Text>
{fee !== 0 && (
<Text style={[transactionListStyle.amount, transactionListStyle.text]}>fee {formatCredits(fee, 8)}</Text>
)}
</View>
</View>
<View style={transactionListStyle.row}>
<View style={transactionListStyle.col}>
<Link
style={transactionListStyle.smallLink}
text={txid.substring(0, 8)}
href={`https://explorer.lbry.com/tx/${txid}`}
error={'The transaction URL could not be opened'}
/>
</View>
<View style={transactionListStyle.col}>
{date ? (
<Text style={transactionListStyle.smallText}>{moment(date).format('MMM D')}</Text>
) : (
<Text style={transactionListStyle.smallText}>Pending</Text>
)}
</View>
</View>
</View>
);
}
}
export default TransactionListItem;

View file

@ -1,70 +0,0 @@
// @flow
import React from 'react';
import { Text, View } from 'react-native';
import TransactionListItem from './internal/transaction-list-item';
import transactionListStyle from '../../styles/transactionList';
export type Transaction = {
amount: number,
claim_id: string,
claim_name: string,
fee: number,
nout: number,
txid: string,
type: string,
date: Date,
};
class TransactionList extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
filter: 'all',
};
(this: any).handleFilterChanged = this.handleFilterChanged.bind(this);
(this: any).filterTransaction = this.filterTransaction.bind(this);
}
handleFilterChanged(event: React.SyntheticInputEvent<*>) {
this.setState({
filter: event.target.value,
});
}
filterTransaction(transaction: Transaction) {
const { filter } = this.state;
return filter === 'all' || filter === transaction.type;
}
render() {
const { emptyMessage, rewards, transactions, navigation } = this.props;
const { filter } = this.state;
const transactionList = transactions.filter(this.filterTransaction);
return (
<View>
{!transactionList.length && (
<Text style={transactionListStyle.noTransactions}>{emptyMessage || 'No transactions to list.'}</Text>
)}
{!!transactionList.length && (
<View>
{transactionList.map(t => (
<TransactionListItem
key={`${t.txid}:${t.nout}`}
transaction={t}
navigation={navigation}
reward={rewards && rewards[t.txid]}
/>
))}
</View>
)}
</View>
);
}
}
export default TransactionList;

View file

@ -1,23 +0,0 @@
import { connect } from 'react-redux';
import {
doFetchTransactions,
selectRecentTransactions,
selectHasTransactions,
selectIsFetchingTransactions,
} from 'lbry-redux';
import TransactionListRecent from './view';
const select = state => ({
fetchingTransactions: selectIsFetchingTransactions(state),
transactions: selectRecentTransactions(state),
hasTransactions: selectHasTransactions(state),
});
const perform = dispatch => ({
fetchTransactions: () => dispatch(doFetchTransactions()),
});
export default connect(
select,
perform
)(TransactionListRecent);

View file

@ -1,45 +0,0 @@
// @flow
import React from 'react';
//import BusyIndicator from 'component/common/busy-indicator';
import { Text, View } from 'react-native';
import Button from '../button';
import Link from '../link';
import TransactionList from '../transactionList';
import type { Transaction } from '../transactionList/view';
import walletStyle from '../../styles/wallet';
type Props = {
fetchTransactions: () => void,
fetchingTransactions: boolean,
hasTransactions: boolean,
transactions: Array<Transaction>,
};
class TransactionListRecent extends React.PureComponent<Props> {
componentDidMount() {
this.props.fetchTransactions();
}
render() {
const { fetchingTransactions, hasTransactions, transactions, navigation } = this.props;
return (
<View style={walletStyle.transactionsCard}>
<View style={[walletStyle.row, walletStyle.transactionsHeader]}>
<Text style={walletStyle.transactionsTitle}>Recent Transactions</Text>
<Link style={walletStyle.link} navigation={navigation} text={'View All'} href={'#TransactionHistory'} />
</View>
{fetchingTransactions && <Text style={walletStyle.infoText}>Fetching transactions...</Text>}
{!fetchingTransactions && (
<TransactionList
navigation={navigation}
transactions={transactions}
emptyMessage={"Looks like you don't have any recent transactions."}
/>
)}
</View>
);
}
}
export default TransactionListRecent;

View file

@ -1,29 +0,0 @@
import { connect } from 'react-redux';
import {
doUpdateSearchQuery,
selectSearchState as selectSearch,
selectSearchValue,
selectSearchSuggestions,
} from 'lbry-redux';
import { selectCurrentRoute } from 'redux/selectors/drawer';
import UriBar from './view';
const select = state => {
const { ...searchState } = selectSearch(state);
return {
...searchState,
query: selectSearchValue(state),
currentRoute: selectCurrentRoute(state),
suggestions: selectSearchSuggestions(state),
};
};
const perform = dispatch => ({
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)),
});
export default connect(
select,
perform
)(UriBar);

View file

@ -1,47 +0,0 @@
// @flow
import React from 'react';
import { SEARCH_TYPES, normalizeURI } from 'lbry-redux';
import { Text, TouchableOpacity, View } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome5';
import uriBarStyle from '../../../styles/uriBar';
class UriBarItem extends React.PureComponent {
render() {
const { item, onPress } = this.props;
const { shorthand, type, value } = item;
let icon;
switch (type) {
case SEARCH_TYPES.CHANNEL:
icon = <Icon name="at" size={18} />;
break;
case SEARCH_TYPES.SEARCH:
icon = <Icon name="search" size={18} />;
break;
case SEARCH_TYPES.FILE:
default:
icon = <Icon name="file" size={18} />;
break;
}
return (
<TouchableOpacity style={uriBarStyle.item} onPress={onPress}>
{icon}
<View style={uriBarStyle.itemContent}>
<Text style={uriBarStyle.itemText} numberOfLines={1}>
{shorthand || value} - {type === SEARCH_TYPES.SEARCH ? 'Search' : value}
</Text>
<Text style={uriBarStyle.itemDesc} numberOfLines={1}>
{type === SEARCH_TYPES.SEARCH && `Search for '${value}'`}
{type === SEARCH_TYPES.CHANNEL && `View the @${shorthand} channel`}
{type === SEARCH_TYPES.FILE && `View content at ${value}`}
</Text>
</View>
</TouchableOpacity>
);
}
}
export default UriBarItem;

View file

@ -1,203 +0,0 @@
// @flow
import React from 'react';
import { SEARCH_TYPES, isNameValid, isURIValid, normalizeURI } from 'lbry-redux';
import { FlatList, Keyboard, TextInput, View } from 'react-native';
import { navigateToUri } from 'utils/helper';
import Constants from 'constants';
import UriBarItem from './internal/uri-bar-item';
import NavigationButton from 'component/navigationButton';
import discoverStyle from 'styles/discover';
import uriBarStyle from 'styles/uriBar';
class UriBar extends React.PureComponent {
static INPUT_TIMEOUT = 2500; // 2.5 seconds
textInput = null;
keyboardDidHideListener = null;
componentDidMount() {
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
this.setSelection();
}
componentWillUnmount() {
if (this.keyboardDidHideListener) {
this.keyboardDidHideListener.remove();
}
}
componentWillReceiveProps(nextProps) {
const { currentRoute, query } = nextProps;
const { currentRoute: prevRoute } = this.props;
if (Constants.DRAWER_ROUTE_SEARCH === currentRoute && currentRoute !== prevRoute) {
this.setState({ currentValue: query, inputText: query });
}
}
constructor(props) {
super(props);
this.state = {
changeTextTimeout: null,
currentValue: null,
inputText: null,
focused: false,
// TODO: Add a setting to enable / disable direct search?
directSearch: true,
};
}
handleChangeText = text => {
const newValue = text ? text : '';
clearTimeout(this.state.changeTextTimeout);
const { updateSearchQuery, onSearchSubmitted, navigation } = this.props;
let timeout = setTimeout(() => {
if (text.trim().length === 0) {
// don't do anything if the text is empty
return;
}
updateSearchQuery(text);
if (!text.startsWith('lbry://')) {
// not a URI input, so this is a search, perform a direct search
if (onSearchSubmitted) {
onSearchSubmitted(text);
} else {
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: text } });
}
}
}, UriBar.INPUT_TIMEOUT);
this.setState({ inputText: newValue, currentValue: newValue, changeTextTimeout: timeout });
};
handleItemPress = item => {
const { navigation, onSearchSubmitted, updateSearchQuery } = this.props;
const { type, value } = item;
Keyboard.dismiss();
if (SEARCH_TYPES.SEARCH === type) {
this.setState({ currentValue: value });
updateSearchQuery(value);
if (onSearchSubmitted) {
onSearchSubmitted(value);
return;
}
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: value } });
} else {
const uri = normalizeURI(value);
navigateToUri(navigation, uri);
}
};
_keyboardDidHide = () => {
if (this.textInput) {
this.textInput.blur();
}
this.setState({ focused: false });
};
setSelection() {
if (this.textInput) {
this.textInput.setNativeProps({ selection: { start: 0, end: 0 } });
}
}
handleSubmitEditing = () => {
const { navigation, onSearchSubmitted, updateSearchQuery } = this.props;
if (this.state.inputText) {
let inputText = this.state.inputText;
if (inputText.startsWith('lbry://') && isURIValid(inputText)) {
// if it's a URI (lbry://...), open the file page
const uri = normalizeURI(inputText);
navigateToUri(navigation, uri);
} else {
updateSearchQuery(inputText);
// Not a URI, default to a search request
if (onSearchSubmitted) {
// Only the search page sets the onSearchSubmitted prop, so call this prop if set
onSearchSubmitted(inputText);
return;
}
// Open the search page with the query populated
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: inputText } });
}
}
};
onSearchPageBlurred() {
this.setState({ currenValueSet: false });
}
render() {
const { navigation, suggestions, query, value } = this.props;
if (this.state.currentValue === null) {
this.setState({ currentValue: value });
}
let style = [uriBarStyle.overlay];
// TODO: Add optional setting to enable URI / search bar suggestions
/*if (this.state.focused) { style.push(uriBarStyle.inFocus); }*/
return (
<View style={style}>
<View style={uriBarStyle.uriContainer}>
<NavigationButton
name="bars"
size={24}
style={uriBarStyle.drawerMenuButton}
iconStyle={discoverStyle.drawerHamburger}
onPress={() => navigation.openDrawer()}
/>
<TextInput
ref={ref => {
this.textInput = ref;
}}
style={uriBarStyle.uriText}
onLayout={() => {
this.setSelection();
}}
selectTextOnFocus={true}
placeholder={'Search movies, music, and more'}
underlineColorAndroid={'transparent'}
numberOfLines={1}
clearButtonMode={'while-editing'}
value={this.state.currentValue}
returnKeyType={'go'}
inlineImageLeft={'baseline_search_black_24'}
inlineImagePadding={16}
onFocus={() => this.setState({ focused: true })}
onBlur={() => {
this.setState({ focused: false });
this.setSelection();
}}
onChangeText={this.handleChangeText}
onSubmitEditing={this.handleSubmitEditing}
/>
{this.state.focused && !this.state.directSearch && (
<View style={uriBarStyle.suggestions}>
<FlatList
style={uriBarStyle.suggestionList}
data={suggestions}
keyboardShouldPersistTaps={'handled'}
keyExtractor={(item, value) => item.value}
renderItem={({ item }) => (
<UriBarItem item={item} navigation={navigation} onPress={() => this.handleItemPress(item)} />
)}
/>
</View>
)}
</View>
</View>
);
}
}
export default UriBar;

View file

@ -1,18 +0,0 @@
import { connect } from 'react-redux';
import { doCheckAddressIsMine, doGetNewAddress, selectReceiveAddress, selectGettingNewAddress } from 'lbry-redux';
import WalletAddress from './view';
const select = state => ({
receiveAddress: selectReceiveAddress(state),
gettingNewAddress: selectGettingNewAddress(state),
});
const perform = dispatch => ({
checkAddressIsMine: address => dispatch(doCheckAddressIsMine(address)),
getNewAddress: () => dispatch(doGetNewAddress()),
});
export default connect(
select,
perform
)(WalletAddress);

View file

@ -1,51 +0,0 @@
// @flow
import React from 'react';
import { Text, View } from 'react-native';
import Address from '../address';
import Button from '../button';
import walletStyle from '../../styles/wallet';
type Props = {
checkAddressIsMine: string => void,
receiveAddress: string,
getNewAddress: () => void,
gettingNewAddress: boolean,
};
class WalletAddress extends React.PureComponent<Props> {
componentWillMount() {
const { checkAddressIsMine, receiveAddress, getNewAddress } = this.props;
if (!receiveAddress) {
getNewAddress();
} else {
checkAddressIsMine(receiveAddress);
}
}
render() {
const { receiveAddress, getNewAddress, gettingNewAddress } = this.props;
return (
<View style={walletStyle.card}>
<Text style={walletStyle.title}>Receive Credits</Text>
<Text style={[walletStyle.text, walletStyle.bottomMarginMedium]}>
Use this wallet address to receive credits sent by another user (or yourself).
</Text>
<Address address={receiveAddress} style={walletStyle.bottomMarginSmall} />
<Button
style={[walletStyle.button, walletStyle.bottomMarginLarge]}
icon={'sync'}
text={'Get new address'}
onPress={getNewAddress}
disabled={gettingNewAddress}
/>
<Text style={walletStyle.smallText}>
You can generate a new address at any time, and any previous addresses will continue to work. Using multiple
addresses can be helpful for keeping track of incoming payments from multiple sources.
</Text>
</View>
);
}
}
export default WalletAddress;

View file

@ -1,12 +0,0 @@
import { connect } from 'react-redux';
import { selectBalance } from 'lbry-redux';
import WalletBalance from './view';
const select = state => ({
balance: selectBalance(state),
});
export default connect(
select,
null
)(WalletBalance);

View file

@ -1,29 +0,0 @@
// @flow
import React from 'react';
import { Image, Text, View } from 'react-native';
import { Lbry, formatCredits } from 'lbry-redux';
import Address from 'component/address';
import Button from 'component/button';
import walletStyle from 'styles/wallet';
type Props = {
balance: number,
};
class WalletBalance extends React.PureComponent<Props> {
render() {
const { balance } = this.props;
return (
<View style={walletStyle.balanceCard}>
<Image style={walletStyle.balanceBackground} resizeMode={'cover'} source={require('../../assets/stripe.png')} />
<Text style={walletStyle.balanceTitle}>Balance</Text>
<Text style={walletStyle.balanceCaption}>You currently have</Text>
<Text style={walletStyle.balance}>
{(balance || balance === 0) && formatCredits(parseFloat(balance), 2) + ' LBC'}
</Text>
</View>
);
}
}
export default WalletBalance;

Some files were not shown because too many files have changed in this diff Show more