Compare commits

..

8 commits

Author SHA1 Message Date
Akinwale Ariwodola
9d3aabb00b demo 2019-09-13 16:52:45 +01:00
Akinwale Ariwodola
836ff2ae13 redux-persist v5. style tweaks. 2019-08-09 07:17:44 +01:00
Akinwale Ariwodola
9d7d2c821f fix time picker for top content on tag page 2019-08-08 22:48:07 +01:00
Akinwale Ariwodola
a594c69e60 performance improvements 2019-08-08 22:33:58 +01:00
Akinwale Ariwodola
24176b3e91 disable redux-persist 2019-07-31 23:38:05 +01:00
Akinwale Ariwodola
60b4210c53 hide blacklisted and filtered outpoints from lists 2019-07-31 12:18:09 +01:00
Akinwale Ariwodola
4fc0056e9e pin new lbry-redux commit 2019-07-30 19:45:34 +01:00
Akinwale Ariwodola
2aedcb8b4d fixes and style tweaks 2019-07-30 19:35:41 +01:00
219 changed files with 13956 additions and 22544 deletions

View file

@ -10,7 +10,6 @@
"__": true "__": true
}, },
"rules": { "rules": {
"no-console": 2,
"no-multi-spaces": 0, "no-multi-spaces": 0,
"new-cap": 0, "new-cap": 0,
"prefer-promise-reject-errors": 0, "prefer-promise-reject-errors": 0,

64
.gitignore vendored
View file

@ -1,66 +1,4 @@
# OSX
#
.DS_Store
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
# node.js
#
node_modules/ node_modules/
npm-debug.log
yarn-error.log
# BUCK
buck-out/
\.buckd/
*.keystore
!debug.keystore
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/
*/fastlane/report.xml
*/fastlane/Preview.html
*/fastlane/screenshots
# Bundle artifact
*.jsbundle
# CocoaPods
/ios/Pods/
# Other Files
android/app/google-services.json
*.log *.log
.vagrant .vagrant
android/app/build/*
android/bin

View file

@ -1,8 +0,0 @@
stages:
- build
build:
variables:
REACT_NATIVE_BRANCH: $CI_COMMIT_REF_NAME
stage: build
trigger: lbry/lbry-android

3
.gitmodules vendored
View file

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

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017-2020 LBRY Inc Copyright (c) 2017-2018 LBRY Inc
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View file

@ -1,7 +1,7 @@
# LBRY React Native # LBRY Android
[![pipeline status](https://ci.lbry.tech/lbry/lbry-android/badges/master/pipeline.svg)](https://ci.lbry.tech/lbry/lbry-android/commits/master) [![pipeline status](https://ci.lbry.tech/lbry/lbry-android/badges/master/pipeline.svg)](https://ci.lbry.tech/lbry/lbry-android/commits/master)
A mobile browser and wallet for the [LBRY](https://lbry.com) network. This app bundles [LBRY SDK](https://github.com/lbryio/lbry) as a background service with a UI layer built with React Native. An Android browser and wallet for the [LBRY](https://lbry.com) network. This app bundles [lbrynet-daemon](https://github.com/lbryio/lbry) as a background service with a UI layer built with React Native. The APK is built using buildozer and the Gradle build tool.
<img src="https://spee.ch/8/lbry-android.png" alt="LBRY Android Screenshot" width="384px" /> <img src="https://spee.ch/8/lbry-android.png" alt="LBRY Android Screenshot" width="384px" />
@ -12,41 +12,10 @@ The minimum supported Android version is 5.0 Lollipop. There are two ways to ins
1. Direct APK install available at [http://build.lbry.io/android/latest.apk](http://build.lbry.io/android/latest.apk). You will need to enable installation from third-party sources on your device in order to install from this source. 1. Direct APK install available at [http://build.lbry.io/android/latest.apk](http://build.lbry.io/android/latest.apk). You will need to enable installation from third-party sources on your device in order to install from this source.
## Usage ## Usage
The app can be launched by opening **LBRY** from the device's app drawer or via the shortcut on the home screen if that was created upon installation. The app can be launched by opening **LBRY Browser** from the device's app drawer or via the shortcut on the home screen if that was created upon installation.
## Running from Source ## Running from Source
### Software Requirements The app is built from source via [Buildozer](https://github.com/kivy/buildozer). After cloning the repository, copy `buildozer.spec.sample` to `buildozer.spec` and modify this file as necessary for your environment. Please see [BUILD.md](BUILD.md) for detailed build instructions.
* Android Studio
* WebStorm (or other IDE for editing React Native / JavaScript code)
* npm
* yarn
### Android Steps
* Clone the repository using `git clone https://github.com/lbryio/lbry-react-native`
* Initialise the submodules.
```
cd lbry-react-native
git submodule update --init --recursive
```
* Install `react-native-cli` globally using `npm install -g react-native-cli`.
* Install the required package modules by running `yarn` in the cloned repository folder.
* Download a `google-services.json` from the Firebase console (https://console.firebase.google.com/) and place it in the `android/app` folder. Alternatively, use the included sample JSON file.
```
cp android/app/google-services.sample.json android/app/google-services.json
```
* Open Android Studio and click File > Open...
* Navigate to the cloned repository on your local filesystem and select the `android` subfolder.
* Connect your Android device in USB debugging mode, or create an ARM emulator (slower) to run the app.
* Click Run > Run... and select the corresponding app configuration. Note that it may take a while for the project files to sync before you can run the app
* In order to edit the React Native / JavaScript files, open the cloned repository folder using WebStorm (or your favourite IDE).
### React Native Fast Refresh
In order to enable fast refresh when updating React Native code
* Connect your Android device in USB debugging mode, or create an ARM emulator
* Run `adb reverse tcp:8081 tcp:8081` (`adb` can be found in the `platform-tools` folder of your Android SDK installation)
* Run `yarn start`
* Press `r` to reload the app.
* Anytime you make an update to the React Native code, the app should automatically refresh.
## Contributing ## Contributing
Contributions to this project are welcome, encouraged, and compensated. For more details, see https://lbry.io/faq/contributing Contributions to this project are welcome, encouraged, and compensated. For more details, see https://lbry.io/faq/contributing

View file

@ -1,14 +0,0 @@
/**
* @format
*/
import 'react-native';
import React from 'react';
import App from '../App';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
it('renders correctly', () => {
renderer.create(<App />);
});

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

View file

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

View file

@ -1,7 +1,3 @@
if (__DEV__) {
import('./reactotron').then(() => console.log('Reactotron Configured'));
}
import LBRYApp from './src/index'; import LBRYApp from './src/index';
export default LBRYApp; export default LBRYApp;

View file

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View file

@ -1,782 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
00E356F31AD99517003FC87E /* LbryAppTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* LbryAppTests.m */; };
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
2D02E4BC1E0B4A80006451C7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
2D02E4BD1E0B4A84006451C7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
2D02E4BF1E0B4AB3006451C7 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
2DCD954D1E0B4F2C00145EB5 /* LbryAppTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* LbryAppTests.m */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 13B07F861A680F5B00A75B9A;
remoteInfo = LbryApp;
};
2D02E4911E0B4A5D006451C7 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 2D02E47A1E0B4A5D006451C7;
remoteInfo = "LbryApp-tvOS";
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = "<group>"; };
00E356EE1AD99517003FC87E /* LbryAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LbryAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
00E356F21AD99517003FC87E /* LbryAppTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LbryAppTests.m; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* LbryApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LbryApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = LbryApp/AppDelegate.h; sourceTree = "<group>"; };
13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = LbryApp/AppDelegate.m; sourceTree = "<group>"; };
13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = LbryApp/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = LbryApp/Info.plist; sourceTree = "<group>"; };
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = LbryApp/main.m; sourceTree = "<group>"; };
2D02E47B1E0B4A5D006451C7 /* LbryApp-tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "LbryApp-tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
2D02E4901E0B4A5D006451C7 /* LbryApp-tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "LbryApp-tvOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
00E356EB1AD99517003FC87E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
2D02E4781E0B4A5D006451C7 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
2D02E48D1E0B4A5D006451C7 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
00E356EF1AD99517003FC87E /* LbryAppTests */ = {
isa = PBXGroup;
children = (
00E356F21AD99517003FC87E /* LbryAppTests.m */,
00E356F01AD99517003FC87E /* Supporting Files */,
);
path = LbryAppTests;
sourceTree = "<group>";
};
00E356F01AD99517003FC87E /* Supporting Files */ = {
isa = PBXGroup;
children = (
00E356F11AD99517003FC87E /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
13B07FAE1A68108700A75B9A /* LbryApp */ = {
isa = PBXGroup;
children = (
008F07F21AC5B25A0029DE68 /* main.jsbundle */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
13B07FB01A68108700A75B9A /* AppDelegate.m */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
13B07FB71A68108700A75B9A /* main.m */,
);
name = LbryApp;
sourceTree = "<group>";
};
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
ED2971642150620600B7C4FE /* JavaScriptCore.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
);
name = Libraries;
sourceTree = "<group>";
};
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
13B07FAE1A68108700A75B9A /* LbryApp */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
00E356EF1AD99517003FC87E /* LbryAppTests */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
);
indentWidth = 2;
sourceTree = "<group>";
tabWidth = 2;
usesTabs = 0;
};
83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup;
children = (
13B07F961A680F5B00A75B9A /* LbryApp.app */,
00E356EE1AD99517003FC87E /* LbryAppTests.xctest */,
2D02E47B1E0B4A5D006451C7 /* LbryApp-tvOS.app */,
2D02E4901E0B4A5D006451C7 /* LbryApp-tvOSTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
00E356ED1AD99517003FC87E /* LbryAppTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "LbryAppTests" */;
buildPhases = (
00E356EA1AD99517003FC87E /* Sources */,
00E356EB1AD99517003FC87E /* Frameworks */,
00E356EC1AD99517003FC87E /* Resources */,
);
buildRules = (
);
dependencies = (
00E356F51AD99517003FC87E /* PBXTargetDependency */,
);
name = LbryAppTests;
productName = LbryAppTests;
productReference = 00E356EE1AD99517003FC87E /* LbryAppTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
13B07F861A680F5B00A75B9A /* LbryApp */ = {
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "LbryApp" */;
buildPhases = (
FD10A7F022414F080027D42C /* Start Packager */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
);
buildRules = (
);
dependencies = (
);
name = LbryApp;
productName = "LbryApp";
productReference = 13B07F961A680F5B00A75B9A /* LbryApp.app */;
productType = "com.apple.product-type.application";
};
2D02E47A1E0B4A5D006451C7 /* LbryApp-tvOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2D02E4BA1E0B4A5E006451C7 /* Build configuration list for PBXNativeTarget "LbryApp-tvOS" */;
buildPhases = (
FD10A7F122414F3F0027D42C /* Start Packager */,
2D02E4771E0B4A5D006451C7 /* Sources */,
2D02E4781E0B4A5D006451C7 /* Frameworks */,
2D02E4791E0B4A5D006451C7 /* Resources */,
2D02E4CB1E0B4B27006451C7 /* Bundle React Native Code And Images */,
);
buildRules = (
);
dependencies = (
);
name = "LbryApp-tvOS";
productName = "LbryApp-tvOS";
productReference = 2D02E47B1E0B4A5D006451C7 /* LbryApp-tvOS.app */;
productType = "com.apple.product-type.application";
};
2D02E48F1E0B4A5D006451C7 /* LbryApp-tvOSTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2D02E4BB1E0B4A5E006451C7 /* Build configuration list for PBXNativeTarget "LbryApp-tvOSTests" */;
buildPhases = (
2D02E48C1E0B4A5D006451C7 /* Sources */,
2D02E48D1E0B4A5D006451C7 /* Frameworks */,
2D02E48E1E0B4A5D006451C7 /* Resources */,
);
buildRules = (
);
dependencies = (
2D02E4921E0B4A5D006451C7 /* PBXTargetDependency */,
);
name = "LbryApp-tvOSTests";
productName = "LbryApp-tvOSTests";
productReference = 2D02E4901E0B4A5D006451C7 /* LbryApp-tvOSTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0940;
ORGANIZATIONNAME = Facebook;
TargetAttributes = {
00E356ED1AD99517003FC87E = {
CreatedOnToolsVersion = 6.2;
TestTargetID = 13B07F861A680F5B00A75B9A;
};
2D02E47A1E0B4A5D006451C7 = {
CreatedOnToolsVersion = 8.2.1;
ProvisioningStyle = Automatic;
};
2D02E48F1E0B4A5D006451C7 = {
CreatedOnToolsVersion = 8.2.1;
ProvisioningStyle = Automatic;
TestTargetID = 2D02E47A1E0B4A5D006451C7;
};
};
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "LbryApp" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
13B07F861A680F5B00A75B9A /* LbryApp */,
00E356ED1AD99517003FC87E /* LbryAppTests */,
2D02E47A1E0B4A5D006451C7 /* LbryApp-tvOS */,
2D02E48F1E0B4A5D006451C7 /* LbryApp-tvOSTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
00E356EC1AD99517003FC87E /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
13B07F8E1A680F5B00A75B9A /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
2D02E4791E0B4A5D006451C7 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2D02E4BD1E0B4A84006451C7 /* Images.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
2D02E48E1E0B4A5D006451C7 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Bundle React Native code and images";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh";
};
2D02E4CB1E0B4B27006451C7 /* Bundle React Native Code And Images */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Bundle React Native Code And Images";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh";
};
FD10A7F022414F080027D42C /* Start Packager */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Start Packager";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n";
showEnvVarsInLog = 0;
};
FD10A7F122414F3F0027D42C /* Start Packager */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Start Packager";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
00E356EA1AD99517003FC87E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
00E356F31AD99517003FC87E /* LbryAppTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
13B07F871A680F5B00A75B9A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
2D02E4771E0B4A5D006451C7 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2D02E4BF1E0B4AB3006451C7 /* main.m in Sources */,
2D02E4BC1E0B4A80006451C7 /* AppDelegate.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
2D02E48C1E0B4A5D006451C7 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2DCD954D1E0B4F2C00145EB5 /* LbryAppTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
00E356F51AD99517003FC87E /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 13B07F861A680F5B00A75B9A /* LbryApp */;
targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */;
};
2D02E4921E0B4A5D006451C7 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 2D02E47A1E0B4A5D006451C7 /* LbryApp-tvOS */;
targetProxy = 2D02E4911E0B4A5D006451C7 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = {
isa = PBXVariantGroup;
children = (
13B07FB21A68108700A75B9A /* Base */,
);
name = LaunchScreen.xib;
path = LbryApp;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
00E356F61AD99517003FC87E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
INFOPLIST_FILE = LbryAppTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
OTHER_LDFLAGS = (
"-ObjC",
"-lc++",
"$(inherited)",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LbryApp.app/LbryApp";
};
name = Debug;
};
00E356F71AD99517003FC87E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
COPY_PHASE_STRIP = NO;
INFOPLIST_FILE = LbryAppTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
OTHER_LDFLAGS = (
"-ObjC",
"-lc++",
"$(inherited)",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LbryApp.app/LbryApp";
};
name = Release;
};
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = NO;
INFOPLIST_FILE = LbryApp/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = LbryApp;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = LbryApp/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = LbryApp;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
2D02E4971E0B4A5E006451C7 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
CLANG_ANALYZER_NONNULL = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_TESTABILITY = YES;
GCC_NO_COMMON_BLOCKS = YES;
INFOPLIST_FILE = "LbryApp-tvOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.REACT.LbryApp-tvOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 9.2;
};
name = Debug;
};
2D02E4981E0B4A5E006451C7 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
CLANG_ANALYZER_NONNULL = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
GCC_NO_COMMON_BLOCKS = YES;
INFOPLIST_FILE = "LbryApp-tvOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.REACT.LbryApp-tvOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 9.2;
};
name = Release;
};
2D02E4991E0B4A5E006451C7 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ANALYZER_NONNULL = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_TESTABILITY = YES;
GCC_NO_COMMON_BLOCKS = YES;
INFOPLIST_FILE = "LbryApp-tvOSTests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.REACT.LbryApp-tvOSTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LbryApp-tvOS.app/LbryApp-tvOS";
TVOS_DEPLOYMENT_TARGET = 10.1;
};
name = Debug;
};
2D02E49A1E0B4A5E006451C7 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ANALYZER_NONNULL = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
GCC_NO_COMMON_BLOCKS = YES;
INFOPLIST_FILE = "LbryApp-tvOSTests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.REACT.LbryApp-tvOSTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LbryApp-tvOS.app/LbryApp-tvOS";
TVOS_DEPLOYMENT_TARGET = 10.1;
};
name = Release;
};
83CBBA201A601CBA00E9B192 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
};
name = Debug;
};
83CBBA211A601CBA00E9B192 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "LbryAppTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
00E356F61AD99517003FC87E /* Debug */,
00E356F71AD99517003FC87E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "LbryApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
13B07F941A680F5B00A75B9A /* Debug */,
13B07F951A680F5B00A75B9A /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
2D02E4BA1E0B4A5E006451C7 /* Build configuration list for PBXNativeTarget "LbryApp-tvOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2D02E4971E0B4A5E006451C7 /* Debug */,
2D02E4981E0B4A5E006451C7 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
2D02E4BB1E0B4A5E006451C7 /* Build configuration list for PBXNativeTarget "LbryApp-tvOSTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2D02E4991E0B4A5E006451C7 /* Debug */,
2D02E49A1E0B4A5E006451C7 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "LbryApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
83CBBA201A601CBA00E9B192 /* Debug */,
83CBBA211A601CBA00E9B192 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
}

View file

@ -1,129 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0940"
version = "1.3">
<BuildAction
parallelizeBuildables = "NO"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D2A28121D9B038B00D4039D"
BuildableName = "libReact.a"
BlueprintName = "React-tvOS"
ReferencedContainer = "container:../node_modules/react-native/React/React.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
BuildableName = "LbryApp-tvOS.app"
BlueprintName = "LbryApp-tvOS"
ReferencedContainer = "container:LbryApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E48F1E0B4A5D006451C7"
BuildableName = "LbryApp-tvOSTests.xctest"
BlueprintName = "LbryApp-tvOSTests"
ReferencedContainer = "container:LbryApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E48F1E0B4A5D006451C7"
BuildableName = "LbryApp-tvOSTests.xctest"
BlueprintName = "LbryApp-tvOSTests"
ReferencedContainer = "container:LbryApp.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
BuildableName = "LbryApp-tvOS.app"
BlueprintName = "LbryApp-tvOS"
ReferencedContainer = "container:LbryApp.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
BuildableName = "LbryApp-tvOS.app"
BlueprintName = "LbryApp-tvOS"
ReferencedContainer = "container:LbryApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2D02E47A1E0B4A5D006451C7"
BuildableName = "LbryApp-tvOS.app"
BlueprintName = "LbryApp-tvOS"
ReferencedContainer = "container:LbryApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -1,129 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0940"
version = "1.3">
<BuildAction
parallelizeBuildables = "NO"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "83CBBA2D1A601D0E00E9B192"
BuildableName = "libReact.a"
BlueprintName = "React"
ReferencedContainer = "container:../node_modules/react-native/React/React.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "LbryApp.app"
BlueprintName = "LbryApp"
ReferencedContainer = "container:LbryApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "LbryAppTests.xctest"
BlueprintName = "LbryAppTests"
ReferencedContainer = "container:LbryApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "LbryAppTests.xctest"
BlueprintName = "LbryAppTests"
ReferencedContainer = "container:LbryApp.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "LbryApp.app"
BlueprintName = "LbryApp"
ReferencedContainer = "container:LbryApp.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "LbryApp.app"
BlueprintName = "LbryApp"
ReferencedContainer = "container:LbryApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "LbryApp.app"
BlueprintName = "LbryApp"
ReferencedContainer = "container:LbryApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -1,15 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTBridgeDelegate.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate, RCTBridgeDelegate>
@property (nonatomic, strong) UIWindow *window;
@end

View file

@ -1,42 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "AppDelegate.h"
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"LbryApp"
initialProperties:nil];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
@end

View file

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="7702" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7701"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Powered by React Native" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
<rect key="frame" x="20" y="439" width="441" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="LbryApp" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
<rect key="frame" x="20" y="140" width="441" height="43"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC"/>
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk"/>
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l"/>
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0"/>
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9"/>
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="548" y="455"/>
</view>
</objects>
</document>

View file

@ -1,38 +0,0 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -1,6 +0,0 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>LbryApp</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View file

@ -1,16 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View file

@ -1,72 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import <React/RCTLog.h>
#import <React/RCTRootView.h>
#define TIMEOUT_SECONDS 600
#define TEXT_TO_LOOK_FOR @"Welcome to React"
@interface LbryAppTests : XCTestCase
@end
@implementation LbryAppTests
- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test
{
if (test(view)) {
return YES;
}
for (UIView *subview in [view subviews]) {
if ([self findSubviewInView:subview matching:test]) {
return YES;
}
}
return NO;
}
- (void)testRendersWelcomeScreen
{
UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
BOOL foundElement = NO;
__block NSString *redboxError = nil;
#ifdef DEBUG
RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
if (level >= RCTLogLevelError) {
redboxError = message;
}
});
#endif
while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
[[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) {
if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
return YES;
}
return NO;
}];
}
#ifdef DEBUG
RCTSetLogFunction(RCTDefaultLogFunction);
#endif
XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
}
@end

View file

@ -1,53 +0,0 @@
platform :ios, '9.0'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
target 'LbryApp' do
# Pods for LbryAndroid
pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector"
pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec"
pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired"
pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety"
pod 'React', :path => '../node_modules/react-native/'
pod 'React-Core', :path => '../node_modules/react-native/'
pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules'
pod 'React-Core/DevSupport', :path => '../node_modules/react-native/'
pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS'
pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation'
pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob'
pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image'
pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS'
pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network'
pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings'
pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text'
pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration'
pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/'
pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact'
pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi'
pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor'
pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector'
pod 'ReactCommon/jscallinvoker', :path => "../node_modules/react-native/ReactCommon"
pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon"
pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga'
pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
target 'LbryAndroidTests' do
inherit! :search_paths
# Pods for testing
end
use_native_modules!
end
target 'LbryAndroid-tvOS' do
# Pods for LbryAndroid-tvOS
target 'LbryAndroid-tvOSTests' do
inherit! :search_paths
# Pods for testing
end
end

View file

@ -1,326 +0,0 @@
{
"Please wait while we get some things ready...": "Please wait while we get some things ready...",
"Welcome to LBRY.": "Welcome to LBRY.",
"LBRY is a community-controlled content platform where you can find and publish videos, music, books, and more.": "LBRY is a community-controlled content platform where you can find and publish videos, music, books, and more.",
"Continue": "Continue",
"Setup account": "Setup account",
"A lbry.tv account allows you to earn rewards, backup your wallet, and keep everything in sync.": "A lbry.tv account allows you to earn rewards, backup your wallet, and keep everything in sync.",
"This information is disclosed only to LBRY, Inc. and not to the LBRY network.": "This information is disclosed only to LBRY, Inc. and not to the LBRY network.",
"No, thanks": "No, thanks",
"Use LBRY": "Use LBRY",
"Are you sure?": "Are you sure?",
"Without an account, you will not receive rewards, sync and backup services, or security updates.": "Without an account, you will not receive rewards, sync and backup services, or security updates.",
"I understand that by uninstalling LBRY I will lose any balances or published content with no recovery option if it is not backed up manually (see wallet page)": "I understand that by uninstalling LBRY I will lose any balances or published content with no recovery option if it is not backed up manually (see wallet page)",
"Starting up": "Starting up",
"Connecting": "Connecting",
"Testing network": "Testing network",
"Waiting for name resolution": "Waiting for name resolution",
"Search movies, music, and more": "Search movies, music, and more",
"Your Tags": "Your Tags",
"Trending": "Trending",
"Customize": "Customize",
"Sign In": "Sign In",
"SIGN IN": "SIGN IN",
"Find content": "Find content",
"Subscriptions": "Subscriptions",
"All Content": "All Content",
"Your content": "Your content",
"Channels": "Channels",
"Library": "Library",
"Publishes": "Publishes",
"New Publish": "New Publish",
"Wallet": "Wallet",
"Rewards": "Rewards",
"Settings": "Settings",
"About": "About",
"All tags you follow": "All tags you follow",
"More tags you follow": "More tags you follow",
"FREE": "FREE",
"more": "more",
"You have not created a channel.\nStart now by creating a new channel!": "You have not created a channel.\nStart now by creating a new channel!",
"Create a channel": "Create a channel",
"CONTENT": "CONTENT",
"ABOUT": "ABOUT",
"Follow": "Follow",
"No content to display at this time. Please check back later.": "No content to display at this time. Please check back later.",
"New": "New",
"Share": "Share",
"Tip": "Tip",
"Report": "Report",
"Fetching cost info...": "Fetching cost info...",
"Play": "Play",
"Connecting...": "Connecting...",
"Related Content": "Related Content",
"Loading...": "Loading...",
"Content Freedom": "Content Freedom",
"LBRY is a free, open, and community-run digital marketplace. It is a decentralized peer-to-peer content distribution platform for creators to upload and share content, and earn LBRY credits for their effort. Users will be able to find a wide selection of videos, music, ebooks and other digital content they are interested in.": "LBRY is a free, open, and community-run digital marketplace. It is a decentralized peer-to-peer content distribution platform for creators to upload and share content, and earn LBRY credits for their effort. Users will be able to find a wide selection of videos, music, ebooks and other digital content they are interested in.",
"What is LBRY?": "What is LBRY?",
"Android App Basics": "Android App Basics",
"Frequently Asked Questions": "Frequently Asked Questions",
"Get Social": "Get Social",
"You can interact with the LBRY team and members of the community on Discord, Facebook, Instagram, Twitter or Reddit.": "You can interact with the LBRY team and members of the community on Discord, Facebook, Instagram, Twitter or Reddit.",
"App info": "App info",
"App version": "App version",
"LBRY SDK": "LBRY SDK",
"Platform": "Platform",
"Installation ID": "Installation ID",
"Logs": "Logs",
"Send log": "Send log",
"Account Recommended": "Account Recommended",
"Without an account, you assume all responsibility for securing your wallet and LBRY data.": "Without an account, you assume all responsibility for securing your wallet and LBRY data.",
"Skip Account": "Skip Account",
"Sign Up": "Sign Up",
"Blockchain Sync": "Blockchain Sync",
"Catching up with the blockchain (%progress%%)": "Catching up with the blockchain (%progress%%)",
"Network Loading": "Network Loading",
"Initializing LBRY service": "Initializing LBRY service",
"%amount% available credits": "%amount% available credits",
"LBRY credits allow you to purchase content, publish content, and influence the network.": "LBRY credits allow you to purchase content, publish content, and influence the network.",
"You get credits for free for providing an email address and taking other basic actions.": "You get credits for free for providing an email address and taking other basic actions.",
"Learn more": "Learn more",
"Not interested": "Not interested",
"Get started": "Get started",
"Get %amount% free credits after creating an account.": "Get %amount% free credits after creating an account.",
"Balance": "Balance",
"You currently have": "You currently have",
"Receive Credits": "Receive Credits",
"Use this wallet address to receive credits sent by another user (or yourself).": "Use this wallet address to receive credits sent by another user (or yourself).",
"Get new address": "Get new address",
"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.": "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.",
"Send Credits": "Send Credits",
"Recipient address": "Recipient address",
"Amount": "Amount",
"Send": "Send",
"Recent Transactions": "Recent Transactions",
"View All": "View All",
"Looks like you don't have any recent transactions.": "Looks like you don't have any recent transactions.",
"Wallet Sync": "Wallet Sync",
"Sync status": "Sync status",
"Off": "Off",
"Manual backup": "Manual backup",
"Sync FAQ": "Sync FAQ",
"Fetching transactions...": "Fetching transactions...",
"Anonymous": "Anonymous",
"Delete": "Delete",
"Title": "Title",
"Channel": "Channel",
"Deposit": "Deposit",
"This LBC remains yours. It is a deposit to reserve the name and can be undone at any time.": "This LBC remains yours. It is a deposit to reserve the name and can be undone at any time.",
"Show optional fields": "Show optional fields",
"Cancel": "Cancel",
"Create": "Create",
"Channel creation requires credits.": "Channel creation requires credits.",
"Tap here to get some for free.": "Tap here to get some for free.",
"%balance% available": "%balance% available",
"Bal: %balance%": "Bal: %balance%",
"Send a tip": "Send a tip",
"Record": "Record",
"Take a photo": "Take a photo",
"Upload a file": "Upload a file",
"Please wait while we load your videos...": "Please wait while we load your videos...",
"Description": "Description",
"Tags": "Tags",
"Price": "Price",
"Your content will be free. Press the toggle to set a price.": "Your content will be free. Press the toggle to set a price.",
"Content address": "Content address",
"The address where people can find your content (ex. lbry://myvideo). ": "The address where people can find your content (ex. lbry://myvideo). ",
"This LBC remains yours and the deposit can be undone at any time.": "This LBC remains yours and the deposit can be undone at any time.",
"Show extra fields": "Show extra fields",
"Publish": "Publish",
"Publishing requires credits.": "Publishing requires credits.",
"Search for more tags": "Search for more tags",
"Mature tags": "Mature tags",
"Publish anonymously": "Publish anonymously",
"Create a channel...": "Create a channel...",
"Uploading thumbnail...": "Uploading thumbnail...",
"Channel name": "Channel name",
"Deposit cannot be higher than your balance": "Deposit cannot be higher than your balance",
"There are no results to display for \"%query%\". Please try a different search term.": "There are no results to display for \"%query%\". Please try a different search term.",
"Nothing here. Publish something!": "Nothing here. Publish something!",
"Explore content for this tag": "Explore content for this tag",
"URL does not include name.": "URL does not include name.",
"Loading decentralized data...": "Loading decentralized data...",
"View": "View",
"Additional Options": "Additional Options",
"Language": "Language",
"English": "English",
"Chinese": "Chinese",
"French": "French",
"German": "German",
"Japanese": "Japanese",
"Russian": "Russian",
"Spanish": "Spanish",
"Indonesian": "Indonesian",
"Italian": "Italian",
"Dutch": "Dutch",
"Turkish": "Turkish",
"Polish": "Polish",
"Malay": "Malay",
"Portuguese": "Portuguese",
"Vietnamese": "Vietnamese",
"Thai": "Thai",
"Arabic": "Arabic",
"Czech": "Czech",
"Croatian": "Croatian",
"Cambodian": "Cambodian",
"Korean": "Korean",
"Norwegian": "Norwegian",
"Romanian": "Romanian",
"Hindi": "Hindi",
"Greek": "Greek",
"License": "License",
"None": "None",
"Public Domain": "Public Domain",
"Copyrighted...": "Copyrighted...",
"Other...": "Other...",
"Hide extra fields": "Hide extra fields",
"Download": "Download",
"%progress%% complete": "%progress%% complete",
"Email": "Email",
"Please provide an email address.": "Please provide an email address.",
"An email has been sent to": "An email has been sent to",
"Please click the link in the message to complete signing in.": "Please click the link in the message to complete signing in.",
"Resend": "Resend",
"Edit": "Edit",
"Your email address was successfully verified.": "Your email address was successfully verified.",
"password": "password",
"Please enter a password to secure your account and wallet.": "Please enter a password to secure your account and wallet.",
"Retrieving your account information...": "Retrieving your account information...",
"Please enter the password you used to secure your wallet.": "Please enter the password you used to secure your wallet.",
"Note: for wallet security purposes, LBRY is unable to reset your password.": "Note: for wallet security purposes, LBRY is unable to reset your password.",
"On": "On",
"Connected email": "Connected email",
"Address copied": "Address copied",
"Unlocking account": "Unlocking account",
"Decrypting wallet": "Decrypting wallet",
"used": "used",
"Stats": "Stats",
"Channels you follow": "Channels you follow",
"Suggested": "Suggested",
"ALL": "ALL",
"Customize your tags": "Customize your tags",
"Done": "Done",
"Unfollow": "Unfollow",
"This content is Not Safe For Work. To view adult content, please change your Settings.": "This content is Not Safe For Work. To view adult content, please change your Settings.",
"No views": "No views",
"%view% views": "%view% views",
"Content": "Content",
"Enable background media playback": "Enable background media playback",
"Enable this option to play audio or video in the background when the app is suspended.": "Enable this option to play audio or video in the background when the app is suspended.",
"Choose language": "Choose language",
"Use device language": "Use device language",
"Gujarati": "Gujarati",
"Show mature content": "Show mature content",
"Notifications": "Notifications",
"Choose the notifications you would like to receive.": "Choose the notifications you would like to receive.",
"Content Interests": "Content Interests",
"Search": "Search",
"Show URL suggestions": "Show URL suggestions",
"Other": "Other",
"Keep the SDK background service running after closing the app": "Keep the SDK background service running after closing the app",
"Enable this option for quicker app launch and to keep the synchronisation with the blockchain up to date.": "Enable this option for quicker app launch and to keep the synchronisation with the blockchain up to date.",
"There's nothing here yet.\nPlease check back later.": "There's nothing here yet.\nPlease check back later.",
"%follower% followers": "%follower% followers",
"Are you sure you want to tip %amount% credits": "Are you sure you want to tip %amount% credits",
"Send tip": "Send tip",
"No": "No",
"Yes": "Yes",
"History": "History",
"/wallet": "/wallet",
"Are you sure you want to tip %amount% credit": "Are you sure you want to tip %amount% credit",
"Enjoying LBRY?": "Enjoying LBRY?",
"Are you enjoying your experience with the LBRY app? You can leave a review for us on the Play Store.": "Are you enjoying your experience with the LBRY app? You can leave a review for us on the Play Store.",
"Never ask again": "Never ask again",
"Maybe later": "Maybe later",
"Rate app": "Rate app",
"This will appear as a tip for %content%, which will boost its ability to be discovered while active.": "This will appear as a tip for %content%, which will boost its ability to be discovered while active.",
"Learn more.": "Learn more.",
"Send a tip to %channel%": "Send a tip to %channel%",
"The transaction URL could not be opened": "The transaction URL could not be opened",
"Show All": "Show All",
"Fetching rewards...": "Fetching rewards...",
"Custom Code": "Custom Code",
"Are you a supermodel or rockstar that received a custom reward code? Claim it here.": "Are you a supermodel or rockstar that received a custom reward code? Claim it here.",
"Redeem": "Redeem",
"Please confirm that you want to use LBRY without creating an account.": "Please confirm that you want to use LBRY without creating an account.",
"You sent %amount% LBC as a tip, Mahalo!": "You sent %amount% LBC as a tip, Mahalo!",
"%follower% follower": "%follower% follower",
"Authenticating": "Authenticating",
"Waiting for authentication": "Waiting for authentication",
"Delete file": "Delete file",
"Are you sure you want to remove this file from your device?": "Are you sure you want to remove this file from your device?",
"Following": "Following",
"Update mailing preferences": "Update mailing preferences",
"Please confirm you want to tip %amount% credits": "Please confirm you want to tip %amount% credits",
"Send Tip": "Send Tip",
"You do not have any\ndownloaded content on this device.": "You do not have any\ndownloaded content on this device.",
"%view% view": "%view% view",
"No channel name after @.": "No channel name after @.",
"Loading transactions...": "Loading transactions...",
"Pending": "Pending",
"Phone Number": "Phone Number",
"Please provide a phone number to prevent fraud.": "Please provide a phone number to prevent fraud.",
"Send verification text": "Send verification text",
"Open": "Open",
"for": "for",
"Everyone": "Everyone",
"Suggested channels": "Suggested channels",
"You might also like": "You might also like",
"Publish something new": "Publish something new",
"Filter for": "Filter for",
"Tags you follow": "Tags you follow",
"Sort content by": "Sort content by",
"Trending content": "Trending content",
"New content": "New content",
"Top content": "Top content",
"Top": "Top",
"Past week": "Past week",
"It looks like you have not\npublished any content to LBRY yet.": "It looks like you have not\npublished any content to LBRY yet.",
"Stop": "Stop",
"Unsupported Content": "Unsupported Content",
"all tags you follow": "all tags you follow",
"Send LBC": "Send LBC",
"from": "from",
"This file cannot be displayed in the LBRY app.": "This file cannot be displayed in the LBRY app.",
"Please press the Play button.": "Please press the Play button.",
"Stop download": "Stop download",
"Are you sure you want to stop downloading this file?": "Are you sure you want to stop downloading this file?",
"The download will stop momentarily. You do not need to wait to discover something else.": "The download will stop momentarily. You do not need to wait to discover something else.",
"Connection Failure": "Connection Failure",
"We could not establish a connection to the SDK. Your data connection may be preventing LBRY from connecting. Contact hello@lbry.com if you think this is a software bug.": "We could not establish a connection to the SDK. Your data connection may be preventing LBRY from connecting. Contact hello@lbry.com if you think this is a software bug.",
"%num% block behind": "%num% block behind",
"Delete files": "Delete files",
"Are you sure you want to delete the selected content?": "Are you sure you want to delete the selected content?",
"We could not find any videos on your device. Take a photo or record a video to get started.": "We could not find any videos on your device. Take a photo or record a video to get started.",
"You have already published to the specified content address. Please enter a new address.": "You have already published to the specified content address. Please enter a new address.",
"Camera": "Camera",
"Please grant access to make use of your camera": "Please grant access to make use of your camera",
"OK": "OK",
"Audio": "Audio",
"Please grant access to record audio": "Please grant access to record audio",
"Camera not authorized": "Camera not authorized",
"There's nothing at this location.": "There's nothing at this location.",
"Publish something here": "Publish something here",
"This content cannot be viewed at this time. Please try again in a bit.": "This content cannot be viewed at this time. Please try again in a bit.",
"Download file": "Download file",
"Save %title% (%size%) to your device": "Save %title% (%size%) to your device",
"Save \"%title%\" (%size%) to your device": "Save \"%title%\" (%size%) to your device",
"Find Channels to follow": "Find Channels to follow",
"LBRY works better if you follow at least 5 creators you like. Sign in to show creators you follow if you already have an account.": "LBRY works better if you follow at least 5 creators you like. Sign in to show creators you follow if you already have an account.",
"%remaining% more...": "%remaining% more...",
"Did you know that you can earn free credits worth up to %amount%?": "Did you know that you can earn free credits worth up to %amount%?",
"SHOW ME": "SHOW ME",
"Convert credits to USD on Bittrex": "Convert credits to USD on Bittrex",
"You also have": "You also have",
"in tips": "in tips",
"Earn more tips by uploading cool videos": "Earn more tips by uploading cool videos",
"You staked": "You staked",
"in your publishes": "in your publishes",
"in your supports": "in your supports",
"Your wallet is not currently synced with lbry.tv. You are responsible for backing up your wallet.": "Your wallet is not currently synced with lbry.tv. You are responsible for backing up your wallet.",
"A backup of your wallet is synced with lbry.tv": "A backup of your wallet is synced with lbry.tv",
"What does this mean?": "What does this mean?",
"LBRY credits allow you to publish or purchase content.": "LBRY credits allow you to publish or purchase content.",
"You can obtain free credits worth %amount% after you provide an email address.": "You can obtain free credits worth %amount% after you provide an email address.",
"up to": "up to"
}

10421
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,90 +1,70 @@
{ {
"name": "LBRYApp", "name": "LBRYApp",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": "true",
"scripts": { "scripts": {
"android": "react-native run-android", "start": "node node_modules/react-native/local-cli/cli.js start",
"ios": "react-native run-ios", "devtools": "react-devtools",
"start": "react-native start", "format": "prettier 'src/**/*.{js,json}' --write",
"test": "jest", "precommit": "lint-staged"
"lint": "eslint .", },
"format": "prettier 'src/**/*.{js,json}' --write", "dependencies": {
"precommit": "lint-staged" "base-64": "^0.1.0",
}, "@expo/vector-icons": "^8.1.0",
"dependencies": { "gfycat-style-urls": "^1.0.3",
"base-64": "^0.1.0", "lbry-redux": "lbryio/lbry-redux#67a654f60630710cae72419448a73a18d076d18a",
"@expo/vector-icons": "^8.1.0", "lbryinc": "lbryio/lbryinc",
"gfycat-style-urls": "^1.0.3", "lodash": ">=4.17.11",
"lbry-redux": "lbryio/lbry-redux#69ffd110dbf3633e5847f61f008751edec033017", "merge": ">=1.2.1",
"lbryinc": "lbryio/lbryinc#667024ebb7cb207609273174ca422cee47469270", "moment": "^2.22.1",
"lodash": ">=4.17.11", "react": "16.8.6",
"merge": ">=1.2.1", "react-native": "0.59.10",
"moment": "^2.22.1", "@react-native-community/async-storage": "^1.5.1",
"react": "16.9.0", "react-native-camera": "^2.11.0",
"react-native": "0.61.5", "react-native-country-picker-modal": "^0.6.2",
"@react-native-community/async-storage": "^1.5.1", "react-native-document-picker": "^2.3.0",
"@react-native-community/masked-view": "^0.1.5", "react-native-exception-handler": "2.9.0",
"react-native-camera": "^3.15.0", "react-native-fast-image": "^5.0.3",
"react-native-country-picker-modal": "^1.10.0", "react-native-fs": "^2.13.3",
"react-native-exception-handler": "2.10.8", "react-native-gesture-handler": "^1.1.0",
"react-native-fast-image": "^7.0.2", "react-native-image-zoom-viewer": "^2.2.5",
"react-native-fs": "^2.16.6", "react-native-password-strength-meter": "^0.0.2",
"react-native-gesture-handler": "1.5.2", "react-native-phone-input": "lbryio/react-native-phone-input",
"react-native-image-zoom-viewer": "^2.2.5", "react-native-super-grid": "^3.0.4",
"react-native-password-strength-meter": "^0.0.2", "react-native-vector-icons": "^6.4.2",
"react-native-phone-input": "lbryio/react-native-phone-input", "react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
"react-native-progress-circle": "2.1.0", "react-navigation": "^3.11.0",
"react-native-reanimated": "1.4.0", "react-navigation-redux-helpers": "^3.0.2",
"react-native-safe-area-context": "^0.6.2", "react-redux": "^5.0.3",
"react-native-screens": "^2.0.0", "redux": "^4.0.4",
"react-native-snackbar": "2.0.4", "redux-persist": "^5.10.0",
"react-native-super-grid": "^3.0.4", "redux-persist-filesystem-storage": "^1.3.2",
"react-native-vector-icons": "^6.6.0", "redux-persist-transform-compress": "^4.2.0",
"react-native-video": "lbryio/react-native-video#7992ff945872f9bd00a3736d9ff1318f343abf47", "redux-persist-transform-filter": "0.0.18",
"react-native-webview": "^8.0.2", "redux-thunk": "^2.3.0",
"react-navigation": "^4.0.10", "rn-fetch-blob": "0.10.15"
"react-navigation-drawer": "2.3.3", },
"react-navigation-redux-helpers": "^3.0.2", "devDependencies": {
"react-navigation-tabs": "^2.7.0", "@babel/core": "^7.5.4",
"react-navigation-stack": "^1.10.3", "babel-eslint": "10.0.2",
"react-redux": "^5.0.3", "@babel/plugin-proposal-object-rest-spread": "^7.5.4",
"redux": "^4.0.4", "babel-preset-env": "^1.6.1",
"redux-persist": "^6.0.0", "babel-preset-stage-2": "^6.18.0",
"redux-persist-filesystem-storage": "^2.1.0", "babel-plugin-module-resolver": "^3.1.1",
"redux-persist-transform-compress": "^4.2.0", "eslint": "^5.16.0",
"redux-persist-transform-filter": "0.0.18", "eslint-config-standard": "^12.0.0",
"redux-thunk": "^2.3.0", "eslint-config-standard-jsx": "^6.0.2",
"rn-fetch-blob": "0.12.0", "eslint-plugin-flowtype": "^2.46.1",
"seedrandom": "3.0.3", "eslint-plugin-import": "^2.17.2",
"showdown": "1.9.1" "eslint-plugin-node": "^8.0.1",
}, "eslint-plugin-promise": "^4.1.1",
"devDependencies": { "eslint-plugin-react": "^7.12.4",
"@babel/core": "^7.6.2", "eslint-plugin-standard": "^4.0.0",
"babel-eslint": "10.0.2", "flow-babel-webpack-plugin": "^1.1.1",
"@babel/plugin-proposal-object-rest-spread": "^7.5.4", "husky": "^0.14.3",
"babel-preset-env": "^1.6.1", "lint-staged": "^7.0.4",
"babel-preset-stage-2": "^6.18.0", "metro-react-native-babel-preset": "^0.55.0",
"babel-plugin-module-resolver": "^3.1.1", "prettier": "^1.11.1",
"eslint": "^6.5.1", "react-devtools": "^3.6.3"
"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",
"husky": "^0.14.3",
"lint-staged": "^7.0.4",
"metro-react-native-babel-preset": "0.56.3",
"prettier": "^1.11.1",
"@react-native-community/eslint-config": "^0.0.5",
"react-devtools": "^3.6.3",
"reactotron-react-native": "4.0.2",
"reactotron-redux": "3.1.2"
},
"jest": {
"preset": "react-native"
}
} }

View file

@ -1,12 +0,0 @@
import AsyncStorage from '@react-native-community/async-storage';
import Reactotron from 'reactotron-react-native';
import { reactotronRedux } from 'reactotron-redux';
const reactotron = Reactotron
.setAsyncStorageHandler(AsyncStorage) // AsyncStorage would either come from `react-native` or `@react-native-community/async-storage` depending on where you get it from
.configure() // controls connection & communication settings
.useReactNative() // add all built-in react native plugins
.use(reactotronRedux())
.connect();
export default reactotron;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

View file

@ -1,13 +1,10 @@
import React from 'react'; import React from 'react';
import AboutPage from 'page/about'; import AboutPage from 'page/about';
import ChannelCreatorPage from 'page/channelCreator';
import DiscoverPage from 'page/discover'; import DiscoverPage from 'page/discover';
import DownloadsPage from 'page/downloads'; import DownloadsPage from 'page/downloads';
import DrawerContent from 'component/drawerContent'; import DrawerContent from 'component/drawerContent';
import FilePage from 'page/file'; import FilePage from 'page/file';
import LiteFilePage from 'page/liteFile';
import FirstRunScreen from 'page/firstRun'; import FirstRunScreen from 'page/firstRun';
import InvitesPage from 'page/invites';
import PublishPage from 'page/publish'; import PublishPage from 'page/publish';
import PublishesPage from 'page/publishes'; import PublishesPage from 'page/publishes';
import RewardsPage from 'page/rewards'; import RewardsPage from 'page/rewards';
@ -20,44 +17,17 @@ import SubscriptionsPage from 'page/subscriptions';
import TransactionHistoryPage from 'page/transactionHistory'; import TransactionHistoryPage from 'page/transactionHistory';
import VerificationScreen from 'page/verification'; import VerificationScreen from 'page/verification';
import WalletPage from 'page/wallet'; import WalletPage from 'page/wallet';
import { NavigationActions, StackActions } from 'react-navigation'; import { createDrawerNavigator, createStackNavigator, NavigationActions } from 'react-navigation';
import { createDrawerNavigator } from 'react-navigation-drawer';
import { createStackNavigator } from 'react-navigation-stack';
import { import {
createReduxContainer, createReduxContainer,
createReactNavigationReduxMiddleware, createReactNavigationReduxMiddleware,
createNavigationReducer, createNavigationReducer,
} from 'react-navigation-redux-helpers'; } from 'react-navigation-redux-helpers';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import { AppState, BackHandler, Linking, NativeModules, TextInput, ToastAndroid } from 'react-native';
AppState,
Alert,
BackHandler,
DeviceEventEmitter,
Linking,
NativeModules,
StatusBar,
TextInput,
ToastAndroid,
} from 'react-native';
import { selectDrawerStack } from 'redux/selectors/drawer'; import { selectDrawerStack } from 'redux/selectors/drawer';
import { SETTINGS, doDismissToast, doToast, selectToast } from 'lbry-redux';
import { import {
Lbry,
ACTIONS,
SETTINGS,
doBalanceSubscribe,
doDismissToast,
doPopulateSharedUserState,
doPreferenceGet,
doToast,
selectToast,
} from 'lbry-redux';
import {
Lbryio,
rewards as REWARD_TYPES,
doBlackListedOutpointsSubscribe,
doClaimRewardType,
doFilteredOutpointsSubscribe,
doGetSync, doGetSync,
doUserCheckEmailVerified, doUserCheckEmailVerified,
doUserEmailVerify, doUserEmailVerify,
@ -65,13 +35,11 @@ import {
selectEmailToVerify, selectEmailToVerify,
selectEmailVerifyIsPending, selectEmailVerifyIsPending,
selectEmailVerifyErrorMessage, selectEmailVerifyErrorMessage,
selectHashChanged,
selectUser, selectUser,
} from 'lbryinc'; } from 'lbryinc';
import { doStartDownload, doUpdateDownload, doCompleteDownload } from 'redux/actions/file'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { makeSelectClientSetting, selectFullscreenMode } from 'redux/selectors/settings';
import { decode as atob } from 'base-64'; import { decode as atob } from 'base-64';
import { dispatchNavigateBack, dispatchNavigateToUri, transformUrl } from 'utils/helper'; import { dispatchNavigateBack, dispatchNavigateToUri } from 'utils/helper';
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
@ -80,20 +48,25 @@ import NavigationButton from 'component/navigationButton';
import discoverStyle from 'styles/discover'; import discoverStyle from 'styles/discover';
import searchStyle from 'styles/search'; import searchStyle from 'styles/search';
import SearchRightHeaderIcon from 'component/searchRightHeaderIcon'; import SearchRightHeaderIcon from 'component/searchRightHeaderIcon';
import Snackbar from 'react-native-snackbar';
import { doSetSdkReady } from 'redux/actions/settings';
const SYNC_GET_INTERVAL = 1000 * 60 * 5; // every 5 minutes const menuNavigationButton = navigation => (
<NavigationButton
name="bars"
size={24}
style={discoverStyle.drawerMenuButton}
iconStyle={discoverStyle.drawerHamburger}
onPress={() => navigation.openDrawer()}
/>
);
const discoverStack = createStackNavigator( const discoverStack = createStackNavigator(
{ {
Subscriptions: { Discover: {
screen: SubscriptionsPage, screen: DiscoverPage,
navigationOptions: { navigationOptions: ({ navigation }) => ({
title: 'Following', title: 'Explore',
header: null, header: null,
drawerIcon: ({ tintColor }) => <Icon name="heart" solid size={drawerIconSize} style={{ color: tintColor }} />, }),
},
}, },
File: { File: {
screen: FilePage, screen: FilePage,
@ -117,7 +90,7 @@ const discoverStack = createStackNavigator(
{ {
headerMode: 'screen', headerMode: 'screen',
transitionConfig: () => ({ screenInterpolator: () => null }), transitionConfig: () => ({ screenInterpolator: () => null }),
}, }
); );
discoverStack.navigationOptions = ({ navigation }) => { discoverStack.navigationOptions = ({ navigation }) => {
@ -151,107 +124,88 @@ const walletStack = createStackNavigator(
{ {
headerMode: 'screen', headerMode: 'screen',
transitionConfig: () => ({ screenInterpolator: () => null }), transitionConfig: () => ({ screenInterpolator: () => null }),
}, }
); );
const drawerIconSize = 18;
const drawer = createDrawerNavigator( const drawer = createDrawerNavigator(
{ {
DiscoverStack: { DiscoverStack: {
screen: discoverStack, screen: discoverStack,
navigationOptions: { navigationOptions: {
title: 'Following', title: 'Explore',
drawerIcon: ({ tintColor }) => <Icon name="home" size={drawerIconSize} style={{ color: tintColor }} />, drawerIcon: ({ tintColor }) => <Icon name="home" size={20} style={{ color: tintColor }} />,
}, },
}, },
Discover: { TrendingStack: {
screen: DiscoverPage,
navigationOptions: ({ navigation }) => ({
title: 'Your Tags',
header: null,
}),
},
Trending: {
screen: TrendingPage, screen: TrendingPage,
navigationOptions: { navigationOptions: {
title: 'All Content', title: 'Trending',
drawerIcon: ({ tintColor }) => <Icon name="fire" size={drawerIconSize} style={{ color: tintColor }} />, drawerIcon: ({ tintColor }) => <Icon name="fire" size={20} style={{ color: tintColor }} />,
},
},
MySubscriptionsStack: {
screen: SubscriptionsPage,
navigationOptions: {
title: 'Subscriptions',
drawerIcon: ({ tintColor }) => <Icon name="heart" solid size={20} style={{ color: tintColor }} />,
}, },
}, },
WalletStack: { WalletStack: {
screen: walletStack, screen: walletStack,
navigationOptions: { navigationOptions: {
title: 'Wallet', title: 'Wallet',
drawerIcon: ({ tintColor }) => <Icon name="wallet" size={drawerIconSize} style={{ color: tintColor }} />, drawerIcon: ({ tintColor }) => <Icon name="wallet" size={20} style={{ color: tintColor }} />,
},
},
ChannelCreator: {
screen: ChannelCreatorPage,
navigationOptions: {
drawerIcon: ({ tintColor }) => <Icon name="at" size={drawerIconSize} style={{ color: tintColor }} />,
}, },
}, },
Publish: { Publish: {
screen: PublishPage, screen: PublishPage,
navigationOptions: { navigationOptions: {
drawerIcon: ({ tintColor }) => <Icon name="upload" size={drawerIconSize} style={{ color: tintColor }} />, drawerIcon: ({ tintColor }) => <Icon name="upload" size={20} style={{ color: tintColor }} />,
}, },
}, },
Publishes: { Publishes: {
screen: PublishesPage, screen: PublishesPage,
navigationOptions: { navigationOptions: {
drawerIcon: ({ tintColor }) => ( drawerIcon: ({ tintColor }) => <Icon name="cloud-upload-alt" size={20} style={{ color: tintColor }} />,
<Icon name="cloud-upload-alt" size={drawerIconSize} style={{ color: tintColor }} />
),
}, },
}, },
Rewards: { Rewards: {
screen: RewardsPage, screen: RewardsPage,
navigationOptions: { navigationOptions: {
drawerIcon: ({ tintColor }) => <Icon name="award" size={drawerIconSize} style={{ color: tintColor }} />, drawerIcon: ({ tintColor }) => <Icon name="award" size={20} style={{ color: tintColor }} />,
}, },
}, },
Invites: { MyLBRYStack: {
screen: InvitesPage,
navigationOptions: {
drawerIcon: ({ tintColor }) => <Icon name="user-friends" size={drawerIconSize} style={{ color: tintColor }} />,
},
},
Downloads: {
screen: DownloadsPage, screen: DownloadsPage,
navigationOptions: { navigationOptions: {
title: 'Library', title: 'Library',
drawerIcon: ({ tintColor }) => <Icon name="download" size={drawerIconSize} style={{ color: tintColor }} />, drawerIcon: ({ tintColor }) => <Icon name="download" size={20} style={{ color: tintColor }} />,
}, },
}, },
Settings: { Settings: {
screen: SettingsPage, screen: SettingsPage,
navigationOptions: { navigationOptions: {
drawerLockMode: 'locked-closed', drawerLockMode: 'locked-closed',
drawerIcon: ({ tintColor }) => <Icon name="cog" size={drawerIconSize} style={{ color: tintColor }} />, drawerIcon: ({ tintColor }) => <Icon name="cog" size={20} style={{ color: tintColor }} />,
}, },
}, },
About: { About: {
screen: AboutPage, screen: AboutPage,
navigationOptions: { navigationOptions: {
drawerLockMode: 'locked-closed', drawerLockMode: 'locked-closed',
drawerIcon: ({ tintColor }) => <Icon name="info" size={drawerIconSize} style={{ color: tintColor }} />, drawerIcon: ({ tintColor }) => <Icon name="info" size={20} style={{ color: tintColor }} />,
}, },
}, },
}, },
{ {
drawerWidth: 299, drawerWidth: 300,
drawerBackgroundColor: 'transparent',
headerMode: 'none', headerMode: 'none',
backBehavior: 'none',
unmountInactiveRoutes: true,
contentComponent: DrawerContent, contentComponent: DrawerContent,
overlayColor: 'rgba(0, 0, 0, 0.7)',
contentOptions: { contentOptions: {
activeTintColor: Colors.LbryGreen, activeTintColor: Colors.LbryGreen,
labelStyle: discoverStyle.menuText, labelStyle: discoverStyle.menuText,
}, },
}, }
); );
const mainStackNavigator = new createStackNavigator( const mainStackNavigator = new createStackNavigator(
@ -277,16 +231,10 @@ const mainStackNavigator = new createStackNavigator(
drawerLockMode: 'locked-closed', drawerLockMode: 'locked-closed',
}, },
}, },
LiteFile: {
screen: LiteFilePage,
navigationOptions: {
drawerLockMode: 'locked-closed',
},
},
}, },
{ {
headerMode: 'none', headerMode: 'none',
}, }
); );
export const AppNavigator = mainStackNavigator; export const AppNavigator = mainStackNavigator;
@ -305,11 +253,9 @@ class AppWithNavigationState extends React.Component {
constructor() { constructor() {
super(); super();
this.emailVerifyCheckInterval = null; this.emailVerifyCheckInterval = null;
this.syncGetInterval = null;
this.state = { this.state = {
emailVerifyDone: false, emailVerifyDone: false,
verifyPending: false, verifyPending: false,
syncHashChanged: false,
}; };
} }
@ -319,36 +265,29 @@ class AppWithNavigationState extends React.Component {
'hardwareBackPress', 'hardwareBackPress',
function() { function() {
const { dispatch, nav, drawerStack } = this.props; const { dispatch, nav, drawerStack } = this.props;
if (drawerStack.length > 1) { // There should be a better way to check this
dispatchNavigateBack(dispatch, nav, drawerStack); if (nav.routes.length > 0) {
return true; 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; return false;
}.bind(this), }.bind(this)
); );
} }
componentDidMount() { componentDidMount() {
const { dispatch, user } = this.props;
this.emailVerifyCheckInterval = setInterval(() => this.checkEmailVerification(), 5000); this.emailVerifyCheckInterval = setInterval(() => this.checkEmailVerification(), 5000);
Linking.addEventListener('url', this._handleUrl); Linking.addEventListener('url', this._handleUrl);
DeviceEventEmitter.addListener('onSdkReady', this.handleSdkReady);
DeviceEventEmitter.addListener('onDownloadAborted', this.handleDownloadAborted);
DeviceEventEmitter.addListener('onDownloadStarted', this.handleDownloadStarted);
DeviceEventEmitter.addListener('onDownloadUpdated', this.handleDownloadUpdated);
DeviceEventEmitter.addListener('onDownloadCompleted', this.handleDownloadCompleted);
// call /sync/get with interval
this.syncGetInterval = setInterval(() => {
this.setState({ syncHashChanged: false }); // reset local state
if (user && user.has_verified_email) {
NativeModules.UtilityModule.getSecureValue(Constants.KEY_WALLET_PASSWORD).then(walletPassword => {
dispatch(doGetSync(walletPassword, () => this.getUserSettings()));
});
}
}, SYNC_GET_INTERVAL);
} }
checkEmailVerification = () => { checkEmailVerification = () => {
@ -361,152 +300,27 @@ class AppWithNavigationState extends React.Component {
}); });
}; };
getUserSettings = () => {
const { dispatch } = this.props;
doPreferenceGet(
'shared',
preference => {
dispatch(doPopulateSharedUserState(preference));
},
error => {
/* failed */
},
);
};
checkNewAndroidReward = () => {
const { dispatch, doToast } = this.props;
const claimRewardCallback = err => {
if (err) {
// an error occurred, do not display anything
return;
}
// reward successfully claimed
NativeModules.UtilityModule.setNativeBooleanSetting(Constants.SETTING_NEW_ANDROID_REWARD_CLAIMED, true);
};
NativeModules.UtilityModule.getNativeBooleanSetting(Constants.SETTING_NEW_ANDROID_REWARD_CLAIMED, false).then(
rewardClaimed => {
if (!rewardClaimed) {
dispatch(
doClaimRewardType(REWARD_TYPES.TYPE_NEW_ANDROID, { notifyError: false, callback: claimRewardCallback }),
);
}
},
);
};
handleSdkReady = () => {
const { dispatch } = this.props;
dispatch(doSetSdkReady());
dispatch(doBalanceSubscribe());
dispatch(doBlackListedOutpointsSubscribe());
dispatch(doFilteredOutpointsSubscribe());
Lbry.wallet_status().then(secureWalletStatus => {
// For now, automatically unlock the wallet if a password is set so that downloads work
NativeModules.UtilityModule.getSecureValue(Constants.KEY_WALLET_PASSWORD).then(password => {
if ((secureWalletStatus.is_encrypted && !secureWalletStatus.is_locked) || secureWalletStatus.is_locked) {
this.setState({
message: __('Unlocking account'),
details: __('Decrypting wallet'),
});
// unlock the wallet and then finish the splash screen
Lbry.wallet_unlock({ password: password || '' }).then(unlocked => {
if (unlocked) {
} else {
// this.handleAccountUnlockFailed();
}
});
}
});
});
this.checkNewAndroidReward();
};
handleAccountUnlockFailed() {
const { dispatch } = this.props;
dispatch(
doToast({
message: __(
'Your wallet failed to unlock, which means you may not be able to play any videos or access your funds. Restart the app to fix this.',
),
isError: true,
}),
);
}
handleDownloadStarted = evt => {
const { dispatch } = this.props;
const { uri, outpoint, fileInfo } = evt;
dispatch(doStartDownload(uri, outpoint, fileInfo));
};
handleDownloadUpdated = evt => {
const { dispatch } = this.props;
const { uri, outpoint, fileInfo, progress } = evt;
dispatch(doUpdateDownload(uri, outpoint, fileInfo, progress));
};
handleDownloadCompleted = evt => {
const { dispatch } = this.props;
const { uri, outpoint, fileInfo } = evt;
dispatch(doCompleteDownload(uri, outpoint, fileInfo));
};
handleDownloadAborted = evt => {
const { dispatch } = this.props;
const { uri, outpoint } = evt;
dispatch({
type: ACTIONS.DOWNLOADING_CANCELED,
data: { uri, outpoint },
});
dispatch({
type: ACTIONS.FILE_DELETE,
data: {
outpoint,
},
});
};
componentWillUnmount() { componentWillUnmount() {
DeviceEventEmitter.removeListener('onSdkReady', this.handleSdkReady);
DeviceEventEmitter.removeListener('onDownloadAborted', this.handleDownloadAborted);
DeviceEventEmitter.removeListener('onDownloadStarted', this.handleDownloadStarted);
DeviceEventEmitter.removeListener('onDownloadUpdated', this.handleDownloadUpdated);
DeviceEventEmitter.removeListener('onDownloadCompleted', this.handleDownloadCompleted);
AppState.removeEventListener('change', this._handleAppStateChange); AppState.removeEventListener('change', this._handleAppStateChange);
BackHandler.removeEventListener('hardwareBackPress'); BackHandler.removeEventListener('hardwareBackPress');
Linking.removeEventListener('url', this._handleUrl); Linking.removeEventListener('url', this._handleUrl);
if (this.emailVerifyCheckInterval > -1) {
clearInterval(this.emailVerifyCheckInterval);
}
if (this.syncGetInterval > -1) {
clearInterval(this.syncGetInterval);
}
} }
componentDidUpdate() { componentDidUpdate() {
const { dispatch, user, hashChanged } = this.props; const { dispatch, user } = this.props;
if (this.state.verifyPending && this.emailVerifyCheckInterval > 0 && user && user.has_verified_email) { if (this.state.verifyPending && this.emailVerifyCheckInterval > 0 && user && user.has_verified_email) {
clearInterval(this.emailVerifyCheckInterval); clearInterval(this.emailVerifyCheckInterval);
AsyncStorage.setItem(Constants.KEY_EMAIL_VERIFY_PENDING, 'false'); AsyncStorage.setItem(Constants.KEY_EMAIL_VERIFY_PENDING, 'false');
this.setState({ verifyPending: false }); this.setState({ verifyPending: false });
NativeModules.Firebase.track('email_verified', { email: user.primary_email }); ToastAndroid.show('Your email address was successfully verified.', ToastAndroid.LONG);
Snackbar.show({ title: __('Your email address was successfully verified.'), duration: Snackbar.LENGTH_LONG });
// get user settings after email verification // upon successful email verification, do wallet sync (if password has been set)
this.getUserSettings(); NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
} if (walletPassword && walletPassword.trim().length > 0) {
dispatch(doGetSync(walletPassword));
if (hashChanged && !this.state.syncHashChanged) { }
this.setState({ syncHashChanged: true }); });
this.getUserSettings();
} }
} }
@ -515,7 +329,7 @@ class AppWithNavigationState extends React.Component {
const { toast, emailToVerify, emailVerifyPending, emailVerifyErrorMessage, user } = nextProps; const { toast, emailToVerify, emailVerifyPending, emailVerifyErrorMessage, user } = nextProps;
if (toast) { if (toast) {
const { message, isError } = toast; const { message } = toast;
let currentDisplayType; let currentDisplayType;
if (!currentDisplayType && message) { if (!currentDisplayType && message) {
// default to toast if no display type set and there is a message specified // default to toast if no display type set and there is a message specified
@ -523,14 +337,7 @@ class AppWithNavigationState extends React.Component {
} }
if (currentDisplayType === 'toast') { if (currentDisplayType === 'toast') {
const props = { ToastAndroid.show(message, ToastAndroid.LONG);
title: message,
duration: Snackbar.LENGTH_LONG,
};
if (isError) {
props.backgroundColor = Colors.Red;
}
Snackbar.show(props);
} }
dispatch(doDismissToast()); dispatch(doDismissToast());
@ -542,7 +349,7 @@ class AppWithNavigationState extends React.Component {
this.setState({ emailVerifyDone: true }); this.setState({ emailVerifyDone: true });
const message = emailVerifyErrorMessage const message = emailVerifyErrorMessage
? String(emailVerifyErrorMessage) ? String(emailVerifyErrorMessage)
: __('Your email address was successfully verified.'); : 'Your email address was successfully verified.';
if (!emailVerifyErrorMessage) { if (!emailVerifyErrorMessage) {
AsyncStorage.removeItem(Constants.KEY_FIRST_RUN_EMAIL); AsyncStorage.removeItem(Constants.KEY_FIRST_RUN_EMAIL);
} }
@ -577,26 +384,6 @@ class AppWithNavigationState extends React.Component {
if (backgroundPlayEnabled || NativeModules.BackgroundMedia) { if (backgroundPlayEnabled || NativeModules.BackgroundMedia) {
NativeModules.BackgroundMedia.hidePlaybackNotification(); NativeModules.BackgroundMedia.hidePlaybackNotification();
} }
// check fullscreen mode and show / hide navigation bar accordingly
this.checkFullscreenMode();
}
};
checkFullscreenMode = () => {
const { fullscreenMode } = this.props;
StatusBar.setHidden(fullscreenMode);
if (fullscreenMode) {
// fullscreen, so change orientation to landscape mode
NativeModules.ScreenOrientation.lockOrientationLandscape();
// hide the navigation bar (on devices that have the soft navigation bar)
NativeModules.UtilityModule.hideNavigationBar();
} else {
// Switch back to portrait mode when the media is not fullscreen
NativeModules.ScreenOrientation.lockOrientationPortrait();
// hide the navigation bar (on devices that have the soft navigation bar)
NativeModules.UtilityModule.showNavigationBar();
} }
}; };
@ -609,7 +396,7 @@ class AppWithNavigationState extends React.Component {
try { try {
verification = JSON.parse(atob(evt.url.substring(15))); verification = JSON.parse(atob(evt.url.substring(15)));
} catch (error) { } catch (error) {
// console.log(error); console.log(error);
} }
if (verification.token && verification.recaptcha) { if (verification.token && verification.recaptcha) {
@ -617,7 +404,7 @@ class AppWithNavigationState extends React.Component {
try { try {
dispatch(doUserEmailVerify(verification.token, verification.recaptcha)); dispatch(doUserEmailVerify(verification.token, verification.recaptcha));
} catch (error) { } catch (error) {
const message = __('Invalid Verification Token'); const message = 'Invalid Verification Token';
dispatch(doUserEmailVerifyFailure(message)); dispatch(doUserEmailVerifyFailure(message));
dispatch(doToast({ message })); dispatch(doToast({ message }));
} }
@ -625,11 +412,11 @@ class AppWithNavigationState extends React.Component {
dispatch( dispatch(
doToast({ doToast({
message: 'Invalid Verification URI', message: 'Invalid Verification URI',
}), })
); );
} }
} else { } else {
dispatchNavigateToUri(dispatch, nav, transformUrl(evt.url)); dispatchNavigateToUri(dispatch, nav, evt.url);
} }
} }
}; };
@ -641,7 +428,6 @@ class AppWithNavigationState extends React.Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state), backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
hashChanged: selectHashChanged(state),
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state), keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
nav: state.nav, nav: state.nav,
toast: selectToast(state), toast: selectToast(state),
@ -651,7 +437,6 @@ const mapStateToProps = state => ({
emailVerifyErrorMessage: selectEmailVerifyErrorMessage(state), emailVerifyErrorMessage: selectEmailVerifyErrorMessage(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state), showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state),
user: selectUser(state), user: selectUser(state),
fullscreenMode: selectFullscreenMode(state),
}); });
export default connect(mapStateToProps)(AppWithNavigationState); export default connect(mapStateToProps)(AppWithNavigationState);

View file

@ -15,7 +15,7 @@ export default class Address extends React.PureComponent<Props> {
return ( return (
<View style={[walletStyle.row, style]}> <View style={[walletStyle.row, style]}>
<Text selectable numberOfLines={1} style={walletStyle.address}> <Text selectable={true} numberOfLines={1} style={walletStyle.address}>
{address || ''} {address || ''}
</Text> </Text>
<Button <Button
@ -24,7 +24,7 @@ export default class Address extends React.PureComponent<Props> {
onPress={() => { onPress={() => {
Clipboard.setString(address); Clipboard.setString(address);
doToast({ doToast({
message: __('Address copied'), message: 'Address copied',
}); });
}} }}
/> />

View file

@ -34,11 +34,16 @@ export default class Button extends React.PureComponent {
} }
let renderIcon = ( let renderIcon = (
<Icon name={icon} size={16} color={iconColor || (theme === 'light' ? Colors.DarkGrey : Colors.White)} /> <Icon name={icon} size={18} color={iconColor ? iconColor : 'light' === theme ? Colors.DarkGrey : Colors.White} />
); );
if (solid) { if (solid) {
renderIcon = ( renderIcon = (
<Icon name={icon} size={16} color={iconColor || (theme === 'light' ? Colors.DarkGrey : Colors.White)} solid /> <Icon
name={icon}
size={18}
color={iconColor ? iconColor : 'light' === theme ? Colors.DarkGrey : Colors.White}
solid
/>
); );
} }

View file

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

View file

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

View file

@ -1,17 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import { doResolveUri, makeSelectClaimForUri, makeSelectThumbnailForUri, makeSelectIsUriResolving } from 'lbry-redux';
doResolveUri,
makeSelectClaimForUri,
makeSelectThumbnailForUri,
makeSelectTitleForUri,
makeSelectIsUriResolving,
} from 'lbry-redux';
import ChannelIconItem from './view'; import ChannelIconItem from './view';
const select = (state, props) => ({ const select = (state, props) => ({
thumbnail: makeSelectThumbnailForUri(props.uri)(state), thumbnail: makeSelectThumbnailForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state), isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
}); });
@ -19,4 +12,7 @@ const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)), resolveUri: uri => dispatch(doResolveUri(uri)),
}); });
export default connect(select, perform)(ChannelIconItem); export default connect(
select,
perform
)(ChannelIconItem);

View file

@ -1,40 +1,9 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator, Image, Text, TouchableOpacity, View } from 'react-native'; import { ActivityIndicator, Image, Text, TouchableOpacity, View } from 'react-native';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
import autothumbStyle from 'styles/autothumb';
import channelIconStyle from 'styles/channelIcon'; import channelIconStyle from 'styles/channelIcon';
export default class ChannelIconItem extends React.PureComponent { export default class ChannelIconItem extends React.PureComponent {
static AUTO_THUMB_STYLES = [
autothumbStyle.autothumbPurple,
autothumbStyle.autothumbRed,
autothumbStyle.autothumbPink,
autothumbStyle.autothumbIndigo,
autothumbStyle.autothumbBlue,
autothumbStyle.autothumbLightBlue,
autothumbStyle.autothumbCyan,
autothumbStyle.autothumbGreen,
autothumbStyle.autothumbYellow,
autothumbStyle.autothumbOrange,
autothumbStyle.autothumbDeepPurple,
autothumbStyle.autothumbAmber,
autothumbStyle.autothumbLime,
autothumbStyle.autothumbLightGreen,
autothumbStyle.autothumbDeepOrange,
autothumbStyle.autothumbBrown,
];
state = {
autoStyle: null,
};
componentWillMount() {
this.setState({
autoStyle:
ChannelIconItem.AUTO_THUMB_STYLES[Math.floor(Math.random() * ChannelIconItem.AUTO_THUMB_STYLES.length)],
});
}
componentDidMount() { componentDidMount() {
const { claim, isPlaceholder, uri, resolveUri } = this.props; const { claim, isPlaceholder, uri, resolveUri } = this.props;
if (!claim && !isPlaceholder) { if (!claim && !isPlaceholder) {
@ -44,40 +13,36 @@ export default class ChannelIconItem extends React.PureComponent {
render() { render() {
const { claim, isPlaceholder, isResolvingUri, onPress, thumbnail, title } = this.props; const { claim, isPlaceholder, isResolvingUri, onPress, thumbnail, title } = this.props;
const displayName = title || (claim ? claim.name : '');
const substrIndex = displayName.startsWith('@') ? 1 : 0;
return ( return (
<TouchableOpacity style={channelIconStyle.container} onPress={onPress}> <TouchableOpacity style={channelIconStyle.container} onPress={onPress}>
{isResolvingUri && ( {isResolvingUri && (
<View style={channelIconStyle.centered}> <View style={channelIconStyle.centered}>
<ActivityIndicator size={'small'} color={Colors.NextLbryGreen} /> <ActivityIndicator size={'small'} color={Colors.LbryGreen} />
</View> </View>
)} )}
<View <View
style={[ style={[
channelIconStyle.thumbnailContainer, channelIconStyle.thumbnailContainer,
isPlaceholder ? channelIconStyle.borderedThumbnailContainer : null, isPlaceholder ? channelIconStyle.borderedThumbnailContainer : null,
isPlaceholder ? null : this.state.autoStyle,
]} ]}
> >
{isPlaceholder && ( {isPlaceholder && (
<View style={channelIconStyle.centered}> <View style={channelIconStyle.centered}>
<Text style={channelIconStyle.placeholderText}>{__('ALL')}</Text> <Text style={channelIconStyle.placeholderText}>ALL</Text>
</View> </View>
)} )}
{!isPlaceholder && thumbnail && ( {!isPlaceholder && (
<Image style={channelIconStyle.thumbnail} resizeMode={'cover'} source={{ uri: thumbnail }} /> <Image
)} style={channelIconStyle.thumbnail}
{!isPlaceholder && !thumbnail && ( resizeMode={'cover'}
<Text style={channelIconStyle.autothumbCharacter}> source={thumbnail ? { uri: thumbnail } : require('../../assets/default_avatar.jpg')}
{displayName.substring(substrIndex, substrIndex + 1).toUpperCase()} />
</Text>
)} )}
</View> </View>
{!isPlaceholder && ( {!isPlaceholder && (
<Text style={channelIconStyle.title} numberOfLines={1}> <Text style={channelIconStyle.title} numberOfLines={1}>
{displayName} {title || (claim ? claim.name : '')}
</Text> </Text>
)} )}
</TouchableOpacity> </TouchableOpacity>

View file

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

View file

@ -1,24 +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 ChannelRewardsDriver 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}>
{__('Channel creation requires credits.')}
{'\n'}
{__('Tap here to get some for free.')}
</Text>
</TouchableOpacity>
);
}
}
export default ChannelRewardsDriver;

View file

@ -7,7 +7,6 @@ import {
doCreateChannel, doCreateChannel,
doToast, doToast,
} from 'lbry-redux'; } from 'lbry-redux';
import { doGetSync } from 'lbryinc';
import ChannelSelector from './view'; import ChannelSelector from './view';
const select = state => ({ const select = state => ({
@ -19,11 +18,10 @@ const select = state => ({
const perform = dispatch => ({ const perform = dispatch => ({
notify: data => dispatch(doToast(data)), notify: data => dispatch(doToast(data)),
createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)), createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)),
fetchChannelListMine: () => dispatch(doFetchChannelListMine(1, 99999, true)), fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
getSync: (password, callback) => dispatch(doGetSync(password, callback)),
}); });
export default connect( export default connect(
select, select,
perform, perform
)(ChannelSelector); )(ChannelSelector);

View file

@ -1,10 +1,9 @@
import React from 'react'; import React from 'react';
import { CLAIM_VALUES, formatCredits, isNameValid } from 'lbry-redux'; import { CLAIM_VALUES, isNameValid } from 'lbry-redux';
import { ActivityIndicator, NativeModules, Picker, Text, TextInput, TouchableOpacity, View } from 'react-native'; import { ActivityIndicator, Picker, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { logPublish } from 'utils/helper';
import Button from 'component/button'; import Button from 'component/button';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api import Constants from 'constants';
import Icon from 'react-native-vector-icons/FontAwesome5'; import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link'; import Link from 'component/link';
import channelSelectorStyle from 'styles/channelSelector'; import channelSelectorStyle from 'styles/channelSelector';
@ -14,7 +13,6 @@ export default class ChannelSelector extends React.PureComponent {
super(props); super(props);
this.state = { this.state = {
creditsInputFocused: false,
currentSelectedValue: Constants.ITEM_ANONYMOUS, currentSelectedValue: Constants.ITEM_ANONYMOUS,
newChannelName: '', newChannelName: '',
newChannelBid: 0.1, newChannelBid: 0.1,
@ -28,24 +26,10 @@ export default class ChannelSelector extends React.PureComponent {
} }
componentDidMount() { componentDidMount() {
const { channels = [], channelName, fetchChannelListMine, fetchingChannels } = this.props; const { channels, fetchChannelListMine, fetchingChannels } = this.props;
if ((!channels || channels.length === 0) && !fetchingChannels) { if (!channels.length && !fetchingChannels) {
fetchChannelListMine(); fetchChannelListMine();
} }
this.setState({ currentSelectedValue: channelName });
}
componentWillReceiveProps(nextProps) {
const { channels: prevChannels = [], channelName: prevChannelName } = this.props;
const { channels = [], channelName } = nextProps;
if (channels && channels.length !== prevChannels.length && channelName !== this.state.currentSelectedValue) {
this.setState({ currentSelectedValue: prevChannelName });
}
if (channelName !== prevChannelName) {
this.setState({ currentSelectedValue: channelName });
}
} }
handleCreateCancel = () => { handleCreateCancel = () => {
@ -70,13 +54,13 @@ export default class ChannelSelector extends React.PureComponent {
if (channel === CLAIM_VALUES.CHANNEL_NEW) { if (channel === CLAIM_VALUES.CHANNEL_NEW) {
this.setState({ addingChannel: true }); this.setState({ addingChannel: true });
if (onChannelChange) { if (onChannelChange) {
onChannelChange(value); onChannelChange(channel);
} }
this.handleNewChannelBidChange(newChannelBid); this.handleNewChannelBidChange(newChannelBid);
} else { } else {
this.setState({ addingChannel: false }); this.setState({ addingChannel: false });
if (onChannelChange) { if (onChannelChange) {
onChannelChange(value); onChannelChange(channel);
} }
} }
}; };
@ -84,22 +68,14 @@ export default class ChannelSelector extends React.PureComponent {
handleNewChannelNameChange = value => { handleNewChannelNameChange = value => {
const { notify } = this.props; const { notify } = this.props;
let newChannelName = value, let newChannelName = value;
newChannelNameError = '';
if (newChannelName.startsWith('@')) { if (newChannelName.startsWith('@')) {
newChannelName = newChannelName.slice(1); newChannelName = newChannelName.slice(1);
} }
if (newChannelName.trim().length > 0 && !isNameValid(newChannelName)) {
newChannelNameError = __('Your channel name contains invalid characters.');
} else if (this.channelExists(newChannelName)) {
newChannelNameError = __('You have already created a channel with the same name.');
}
this.setState({ this.setState({
newChannelName, newChannelName,
newChannelNameError,
}); });
}; };
@ -123,21 +99,21 @@ export default class ChannelSelector extends React.PureComponent {
}; };
handleCreateChannelClick = () => { handleCreateChannelClick = () => {
const { balance, createChannel, getSync, onChannelChange, notify } = this.props; const { balance, createChannel, onChannelChange, notify } = this.props;
const { newChannelBid, newChannelName } = this.state; const { newChannelBid, newChannelName } = this.state;
if (newChannelName.trim().length === 0 || !isNameValid(newChannelName.substr(1), false)) { if (newChannelName.trim().length === 0 || !isNameValid(newChannelName.substr(1), false)) {
notify({ message: __('Your channel name contains invalid characters.') }); notify({ message: 'Your channel name contains invalid characters.' });
return; return;
} }
if (this.channelExists(newChannelName)) { if (this.channelExists(newChannelName)) {
notify({ message: __('You have already created a channel with the same name.') }); notify({ message: 'You have already created a channel with the same name.' });
return; return;
} }
if (newChannelBid > balance) { if (newChannelBid > balance) {
notify({ message: __('Deposit cannot be higher than your balance') }); notify({ message: 'Deposit cannot be higher than your balance' });
return; return;
} }
@ -148,25 +124,21 @@ export default class ChannelSelector extends React.PureComponent {
createChannelError: undefined, createChannelError: undefined,
}); });
const success = channelClaim => { const success = () => {
this.setState({ this.setState({
creatingChannel: false, creatingChannel: false,
addingChannel: false, addingChannel: false,
currentSelectedValue: channelName, currentSelectedValue: channelName,
showCreateChannel: false, showCreateChannel: false,
}); });
logPublish(channelClaim);
if (onChannelChange) { if (onChannelChange) {
onChannelChange(channelName); onChannelChange(channelName);
} }
// sync wallet
NativeModules.UtilityModule.getSecureValue(Constants.KEY_WALLET_PASSWORD).then(password => getSync(password));
}; };
const failure = () => { const failure = () => {
notify({ message: __('Unable to create channel due to an internal error.') }); notify({ message: 'Unable to create channel due to an internal error.' });
this.setState({ this.setState({
creatingChannel: false, creatingChannel: false,
}); });
@ -177,14 +149,12 @@ export default class ChannelSelector extends React.PureComponent {
channelExists = name => { channelExists = name => {
const { channels = [] } = this.props; const { channels = [] } = this.props;
if (channels) { for (let channel of channels) {
for (let channel of channels) { if (
if ( name.toLowerCase() === channel.name.toLowerCase() ||
name.toLowerCase() === channel.name.toLowerCase() || `@${name}`.toLowerCase() === channel.name.toLowerCase()
`@${name}`.toLowerCase() === channel.name.toLowerCase() ) {
) { return true;
return true;
}
} }
} }
@ -193,12 +163,10 @@ export default class ChannelSelector extends React.PureComponent {
render() { render() {
const channel = this.state.addingChannel ? 'new' : this.props.channel; const channel = this.state.addingChannel ? 'new' : this.props.channel;
const { balance, enabled, fetchingChannels, channels = [], showAnonymous } = this.props; const { fetchingChannels, channels = [] } = this.props;
const pickerItems = (showAnonymous const pickerItems = [{ name: Constants.ITEM_ANONYMOUS }, { name: Constants.ITEM_CREATE_A_CHANNEL }].concat(
? [Constants.ITEM_ANONYMOUS, Constants.ITEM_CREATE_A_CHANNEL] channels
: [Constants.ITEM_CREATE_A_CHANNEL] );
).concat(channels ? channels.map(ch => ch.name) : []);
const { const {
newChannelName, newChannelName,
newChannelNameError, newChannelNameError,
@ -207,28 +175,22 @@ export default class ChannelSelector extends React.PureComponent {
creatingChannel, creatingChannel,
createChannelError, createChannelError,
addingChannel, addingChannel,
showCreateChannel,
} = this.state; } = this.state;
return ( return (
<View style={channelSelectorStyle.container}> <View style={channelSelectorStyle.container}>
<Picker <Picker
enabled={enabled}
selectedValue={this.state.currentSelectedValue} selectedValue={this.state.currentSelectedValue}
style={channelSelectorStyle.channelPicker} style={channelSelectorStyle.channelPicker}
itemStyle={channelSelectorStyle.channelPickerItem} itemStyle={channelSelectorStyle.channelPickerItem}
onValueChange={this.handlePickerValueChange} onValueChange={this.handlePickerValueChange}
> >
{pickerItems.map(item => ( {pickerItems.map(item => (
<Picker.Item <Picker.Item label={item.name} value={item.name} key={item.name} />
label={[Constants.ITEM_ANONYMOUS, Constants.ITEM_CREATE_A_CHANNEL].includes(item) ? __(item) : item}
value={item}
key={item}
/>
))} ))}
</Picker> </Picker>
{showCreateChannel && ( {this.state.showCreateChannel && (
<View style={channelSelectorStyle.createChannelContainer}> <View style={channelSelectorStyle.createChannelContainer}>
<View style={channelSelectorStyle.channelInputContainer}> <View style={channelSelectorStyle.channelInputContainer}>
<Text style={channelSelectorStyle.channelAt}>@</Text> <Text style={channelSelectorStyle.channelAt}>@</Text>
@ -237,46 +199,35 @@ export default class ChannelSelector extends React.PureComponent {
style={channelSelectorStyle.channelNameInput} style={channelSelectorStyle.channelNameInput}
value={this.state.newChannelName} value={this.state.newChannelName}
onChangeText={this.handleNewChannelNameChange} onChangeText={this.handleNewChannelNameChange}
placeholder={__('Channel name')} placeholder={'Channel name'}
underlineColorAndroid={Colors.NextLbryGreen} underlineColorAndroid={Colors.NextLbryGreen}
/> />
</View> </View>
{newChannelNameError.length > 0 && (
<Text style={channelSelectorStyle.inlineError}>{newChannelNameError}</Text>
)}
<View style={channelSelectorStyle.bidRow}> <View style={channelSelectorStyle.bidRow}>
<Text style={channelSelectorStyle.label}>{__('Deposit')}</Text> <Text style={channelSelectorStyle.label}>Deposit</Text>
<TextInput <TextInput
style={channelSelectorStyle.bidAmountInput} style={channelSelectorStyle.bidAmountInput}
value={String(newChannelBid)} value={String(this.state.newChannelBid)}
onChangeText={this.handleNewChannelBidChange} onChangeText={this.handleNewChannelBidChange}
onFocus={() => this.setState({ creditsInputFocused: true })}
onBlur={() => this.setState({ creditsInputFocused: false })}
placeholder={'0.00'} placeholder={'0.00'}
keyboardType={'number-pad'} keyboardType={'number-pad'}
underlineColorAndroid={Colors.NextLbryGreen} underlineColorAndroid={Colors.NextLbryGreen}
/> />
<Text style={channelSelectorStyle.currency}>LBC</Text> <Text style={channelSelectorStyle.currency}>LBC</Text>
<View style={channelSelectorStyle.balance}>
{this.state.creditsInputFocused && <Icon name="coins" size={12} />}
{this.state.creditsInputFocused && (
<Text style={channelSelectorStyle.balanceText}>{formatCredits(parseFloat(balance), 1, true)}</Text>
)}
</View>
</View> </View>
<Text style={channelSelectorStyle.helpText}> <Text style={channelSelectorStyle.helpText}>
{__('This LBC remains yours. It is a deposit to reserve the name and can be undone at any time.')} This LBC remains yours. It is a deposit to reserve the name and can be undone at any time.
</Text> </Text>
<View style={channelSelectorStyle.buttonContainer}> <View style={channelSelectorStyle.buttonContainer}>
{creatingChannel && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />} {creatingChannel && <ActivityIndicator size={'small'} color={Colors.LbryGreen} />}
{!creatingChannel && ( {!creatingChannel && (
<View style={channelSelectorStyle.buttons}> <View style={channelSelectorStyle.buttons}>
<Link style={channelSelectorStyle.cancelLink} text={__('Cancel')} onPress={this.handleCreateCancel} /> <Link style={channelSelectorStyle.cancelLink} text="Cancel" onPress={this.handleCreateCancel} />
<Button <Button
style={channelSelectorStyle.createButton} style={channelSelectorStyle.createButton}
disabled={!(newChannelName.trim().length > 0 && newChannelBid > 0)} disabled={!(this.state.newChannelName.trim().length > 0 && this.state.newChannelBid > 0)}
text={__('Create')} text="Create"
onPress={this.handleCreateChannelClick} onPress={this.handleCreateChannelClick}
/> />
</View> </View>

View file

@ -2,25 +2,47 @@ import { connect } from 'react-redux';
import { import {
MATURE_TAGS, MATURE_TAGS,
doClaimSearch, doClaimSearch,
selectClaimSearchByQuery, doClaimSearchByTags,
selectClaimSearchByQueryLastPageReached, makeSelectClaimSearchUrisForTags,
selectFetchingClaimSearchByQuery, makeSelectFetchingClaimSearchForTags,
selectFetchingClaimSearch, selectFetchingClaimSearch,
selectLastClaimSearchUris,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectShowNsfw } from 'redux/selectors/settings';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import ClaimList from './view'; import ClaimList from './view';
const select = state => ({ const select = (state, props) => {
claimSearchByQuery: selectClaimSearchByQuery(state), return {
lastPageReached: selectClaimSearchByQueryLastPageReached(state), loading: makeSelectFetchingClaimSearchForTags(props.tags)(state),
loadingByQuery: selectFetchingClaimSearchByQuery(state), uris: makeSelectClaimSearchUrisForTags(props.tags)(state),
loading: selectFetchingClaimSearch(state), // for subscriptions
showNsfwContent: selectShowNsfw(state), claimSearchLoading: selectFetchingClaimSearch(state),
}); claimSearchUris: selectLastClaimSearchUris(state),
};
};
const perform = dispatch => ({ const perform = dispatch => ({
claimSearch: options => dispatch(doClaimSearch(options)), claimSearch: options => dispatch(doClaimSearch(Constants.DEFAULT_PAGE_SIZE, options)),
searchByTags: (tags, orderBy = Constants.DEFAULT_ORDER_BY, page = 1, additionalOptions = {}) =>
dispatch(
doClaimSearchByTags(
tags,
Constants.DEFAULT_PAGE_SIZE,
Object.assign(
{},
{
no_totals: true,
order_by: orderBy,
page,
not_tags: MATURE_TAGS,
},
additionalOptions
)
)
),
}); });
export default connect(select, perform)(ClaimList); export default connect(
select,
perform
)(ClaimList);

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import NavigationActions from 'react-navigation'; import NavigationActions from 'react-navigation';
import { ActivityIndicator, FlatList, Text, TouchableOpacity, View } from 'react-native'; import { ActivityIndicator, FlatList, Text, TouchableOpacity, View } from 'react-native';
import { MATURE_TAGS, normalizeURI, createNormalizedClaimSearchKey } from 'lbry-redux'; import { MATURE_TAGS, normalizeURI } from 'lbry-redux';
import _ from 'lodash'; import _ from 'lodash';
import FileItem from 'component/fileItem'; import FileItem from 'component/fileItem';
import FileListItem from 'component/fileListItem'; import FileListItem from 'component/fileListItem';
@ -12,7 +12,7 @@ import claimListStyle from 'styles/claimList';
import discoverStyle from 'styles/discover'; import discoverStyle from 'styles/discover';
import moment from 'moment'; import moment from 'moment';
const horizontalLimit = 10; const horizontalLimit = 7;
const softLimit = 500; const softLimit = 500;
class ClaimList extends React.PureComponent { class ClaimList extends React.PureComponent {
@ -21,82 +21,94 @@ class ClaimList extends React.PureComponent {
state = { state = {
currentPage: 1, // initial page load is page 1 currentPage: 1, // initial page load is page 1
subscriptionsView: false, // whether or not this claim list is for subscriptions subscriptionsView: false, // whether or not this claim list is for subscriptions
lastPageReached: false, trendingForAllView: false,
}; };
componentDidMount() { componentDidMount() {
const { channelIds } = this.props; const {
channelIds,
trendingForAll,
claimSearch,
orderBy = Constants.DEFAULT_ORDER_BY,
searchByTags,
tags,
time,
} = this.props;
if (channelIds || trendingForAll) {
const options = {
order_by: orderBy,
no_totals: true,
not_tags: MATURE_TAGS,
page: this.state.currentPage,
};
if (channelIds) {
this.setState({ subscriptionsView: true });
options.channel_ids = channelIds;
} else if (trendingForAll) {
this.setState({ trendingForAllView: true });
}
if (channelIds) { claimSearch(options);
this.setState({ subscriptionsView: true }); } else if (tags && tags.length > 0) {
const additionalOptions = {};
if (orderBy && orderBy[0] === Constants.ORDER_BY_EFFECTIVE_AMOUNT && Constants.TIME_ALL !== time) {
additionalOptions.release_time = this.getReleaseTimeOption(time);
}
searchByTags(tags, orderBy, this.state.currentPage, additionalOptions);
} }
this.doClaimSearch();
} }
componentDidUpdate(prevProps) { componentWillReceiveProps(nextProps) {
const { const {
claimSearchByQuery: prevClaimSearchByQuery, claimSearch,
orderBy: prevOrderBy, orderBy: prevOrderBy,
searchByTags, searchByTags,
tags: prevTags, tags: prevTags,
channelIds: prevChannelIds, channelIds: prevChannelIds,
trendingForAll: prevTrendingForAll,
time: prevTime, time: prevTime,
} = prevProps; } = this.props;
const { claimSearchByQuery, orderBy, tags, channelIds, time } = this.props; const { orderBy, tags, channelIds, trendingForAll, time } = nextProps;
if ( if (
!_.isEqual(orderBy, prevOrderBy) || !_.isEqual(orderBy, prevOrderBy) ||
!_.isEqual(tags, prevTags) || !_.isEqual(tags, prevTags) ||
!_.isEqual(channelIds, prevChannelIds) || !_.isEqual(channelIds, prevChannelIds) ||
time !== prevTime time !== prevTime ||
trendingForAll !== prevTrendingForAll
) { ) {
// reset to page 1 because the order, tags or channelIds changed // reset to page 1 because the order, tags or channelIds changed
this.setState({ currentPage: 1 }, () => { this.setState({ currentPage: 1 }, () => {
if (this.scrollView) { if (this.scrollView) {
this.scrollView.scrollToOffset({ animated: true, offset: 0 }); this.scrollView.scrollToOffset({ animated: true, offset: 0 });
} }
if (trendingForAll || (prevChannelIds && channelIds)) {
if (prevChannelIds && channelIds) { const options = {
order_by: orderBy,
no_totals: true,
not_tags: MATURE_TAGS,
page: this.state.currentPage,
};
if (channelIds) { if (channelIds) {
this.setState({ subscriptionsView: true }); this.setState({ subscriptionsView: true });
options.channel_ids = channelIds;
}
if (trendingForAll) {
this.setState({ trendingForAllView: true });
} }
} else if (tags && tags.length > 0) {
this.setState({ subscriptionsView: false });
}
this.doClaimSearch(); claimSearch(options);
} else if (tags && tags.length > 0) {
this.setState({ subscriptionsView: false, trendingForAllView: false });
const additionalOptions = {};
if (orderBy && orderBy[0] === Constants.ORDER_BY_EFFECTIVE_AMOUNT && Constants.TIME_ALL !== time) {
additionalOptions.release_time = this.getReleaseTimeOption(time);
}
searchByTags(tags, orderBy, this.state.currentPage, additionalOptions);
}
}); });
} }
} }
buildClaimSearchOptions() {
const { orderBy, channelIds, showNsfwContent, tags, time } = this.props;
const { currentPage, subscriptionsView } = this.state;
const options = {
order_by: orderBy,
no_totals: true,
page: currentPage,
page_size: Constants.DEFAULT_PAGE_SIZE,
};
if (channelIds) {
options.channel_ids = channelIds;
} else if (tags && tags.length > 0) {
options.any_tags = tags;
}
if (!showNsfwContent) {
options.not_tags = MATURE_TAGS;
}
if (orderBy && orderBy[0] === Constants.ORDER_BY_EFFECTIVE_AMOUNT && Constants.TIME_ALL !== time) {
options.release_time = this.getReleaseTimeOption(time);
}
return options;
}
getReleaseTimeOption = time => { getReleaseTimeOption = time => {
return `>${Math.floor( return `>${Math.floor(
moment() moment()
@ -105,27 +117,36 @@ class ClaimList extends React.PureComponent {
)}`; )}`;
}; };
doClaimSearch() {
const { claimSearch } = this.props;
const options = this.buildClaimSearchOptions();
claimSearch(options);
}
handleVerticalEndReached = () => { handleVerticalEndReached = () => {
// fetch more content // fetch more content
const { claimSearchByQuery, lastPageReached } = this.props; const { channelIds, claimSearch, claimSearchUris, orderBy, searchByTags, tags, time, uris } = this.props;
const { subscriptionsView, trendingForAllView } = this.state;
const options = this.buildClaimSearchOptions(); if ((claimSearchUris && claimSearchUris.length >= softLimit) || (uris && uris.length >= softLimit)) {
const claimSearchKey = createNormalizedClaimSearchKey(options); // don't fetch more than the specified limit to be displayed
const uris = claimSearchByQuery[claimSearchKey];
if (
lastPageReached[claimSearchKey] ||
((uris.length > 0 && uris.length < Constants.DEFAULT_PAGE_SIZE) || uris.length >= softLimit)
) {
return; return;
} }
this.setState({ currentPage: this.state.currentPage + 1 }, () => this.doClaimSearch()); this.setState({ currentPage: this.state.currentPage + 1 }, () => {
if (subscriptionsView || trendingForAllView) {
const options = {
order_by: orderBy,
no_totals: true,
not_tags: MATURE_TAGS,
page: this.state.currentPage,
};
if (subscriptionsView) {
options.channel_ids = channelIds;
}
claimSearch(options);
} else {
const additionalOptions = {};
if (orderBy && orderBy[0] === Constants.ORDER_BY_EFFECTIVE_AMOUNT && Constants.TIME_ALL !== time) {
additionalOptions.release_time = this.getReleaseTimeOption(time);
}
searchByTags(tags, orderBy, this.state.currentPage, additionalOptions);
}
});
}; };
appendMorePlaceholder = items => { appendMorePlaceholder = items => {
@ -140,79 +161,35 @@ class ClaimList extends React.PureComponent {
if (tags.length === 1) { if (tags.length === 1) {
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_TAG, key: 'tagPage', params: { tag: tags[0] } }); navigation.navigate({ routeName: Constants.DRAWER_ROUTE_TAG, key: 'tagPage', params: { tag: tags[0] } });
} else { } else {
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_TRENDING, params: { filterForTags: true } }); navigation.navigate({ routeName: Constants.FULL_ROUTE_NAME_TRENDING });
} }
}; };
renderMorePlaceholder = () => { renderMorePlaceholder = () => {
return ( return (
<TouchableOpacity style={discoverStyle.fileItemMore} onPress={this.onMorePressed}> <TouchableOpacity style={discoverStyle.fileItemMore} onPress={this.onMorePressed}>
<Text style={discoverStyle.moreText}>{__('more')}</Text> <Text style={discoverStyle.moreText}>more</Text>
<Icon style={discoverStyle.moreIcon} name={'angle-double-down'} color={Colors.White} size={16} /> <Icon style={discoverStyle.moreIcon} name={'angle-double-down'} color={Colors.White} size={16} />
</TouchableOpacity> </TouchableOpacity>
); );
}; };
verticalListEmptyComponent = () => {
return (
<Text style={claimListStyle.noContentText}>
{__('No content to display at this time. Please check back later.')}
</Text>
);
};
renderVerticalItem = ({ item }) => {
const { hideChannel, navigation } = this.props;
return (
<FileListItem
key={item}
uri={item}
hideChannel={hideChannel}
style={claimListStyle.verticalListItem}
navigation={navigation}
/>
);
};
renderHorizontalItem = ({ item }) => {
const { navigation } = this.props;
return item === Constants.MORE_PLACEHOLDER ? (
this.renderMorePlaceholder()
) : (
<FileItem
style={discoverStyle.fileItem}
mediaStyle={discoverStyle.fileItemMedia}
key={item}
uri={normalizeURI(item)}
navigation={navigation}
showDetails
compactView={false}
/>
);
};
render() { render() {
const { const {
ListHeaderComponent, ListHeaderComponent,
loading, loading,
claimSearchLoading,
claimSearchUris,
morePlaceholder, morePlaceholder,
navigation, navigation,
orientation = Constants.ORIENTATION_VERTICAL, orientation = Constants.ORIENTATION_VERTICAL,
style, style,
claimSearchByQuery, uris,
} = this.props; } = this.props;
const { subscriptionsView } = this.state; const { subscriptionsView, trendingForAllView } = this.state;
const options = this.buildClaimSearchOptions();
const claimSearchKey = createNormalizedClaimSearchKey(options);
let uris = claimSearchByQuery[claimSearchKey];
if (uris) {
uris = uris.filter(uri => uri && uri.length > 0);
}
if (Constants.ORIENTATION_VERTICAL === orientation) { if (Constants.ORIENTATION_VERTICAL === orientation) {
const data = subscriptionsView || trendingForAllView ? claimSearchUris : uris;
return ( return (
<View style={style}> <View style={style}>
<FlatList <FlatList
@ -220,21 +197,22 @@ class ClaimList extends React.PureComponent {
this.scrollView = ref; this.scrollView = ref;
}} }}
ListHeaderComponent={ListHeaderComponent} ListHeaderComponent={ListHeaderComponent}
ListEmptyComponent={loading ? null : this.verticalListEmptyComponent}
style={claimListStyle.verticalScrollContainer} style={claimListStyle.verticalScrollContainer}
contentContainerStyle={claimListStyle.verticalScrollPadding} contentContainerStyle={claimListStyle.verticalScrollPadding}
initialNumToRender={10} initialNumToRender={10}
maxToRenderPerBatch={20} maxToRenderPerBatch={20}
removeClippedSubviews removeClippedSubviews
renderItem={this.renderVerticalItem} renderItem={({ item }) => (
data={uris} <FileListItem key={item} uri={item} style={claimListStyle.verticalListItem} navigation={navigation} />
)}
data={data}
keyExtractor={(item, index) => item} keyExtractor={(item, index) => item}
onEndReached={this.handleVerticalEndReached} onEndReached={this.handleVerticalEndReached}
onEndReachedThreshold={0.2} onEndReachedThreshold={0.2}
/> />
{loading && ( {(((subscriptionsView || trendingForAllView) && claimSearchLoading) || loading) && (
<View style={claimListStyle.verticalLoading}> <View style={claimListStyle.verticalLoading}>
<ActivityIndicator size={'small'} color={Colors.NextLbryGreen} /> <ActivityIndicator size={'small'} color={Colors.LbryGreen} />
</View> </View>
)} )}
</View> </View>
@ -245,7 +223,7 @@ class ClaimList extends React.PureComponent {
if (loading) { if (loading) {
return ( return (
<View style={discoverStyle.listLoading}> <View style={discoverStyle.listLoading}>
<ActivityIndicator size={'small'} color={Colors.NextLbryGreen} /> <ActivityIndicator size={'small'} color={Colors.LbryGreen} />
</View> </View>
); );
} }
@ -257,7 +235,21 @@ class ClaimList extends React.PureComponent {
initialNumToRender={3} initialNumToRender={3}
maxToRenderPerBatch={3} maxToRenderPerBatch={3}
removeClippedSubviews removeClippedSubviews
renderItem={this.renderHorizontalItem} renderItem={({ item }) => {
return item === Constants.MORE_PLACEHOLDER ? (
this.renderMorePlaceholder()
) : (
<FileItem
style={discoverStyle.fileItem}
mediaStyle={discoverStyle.fileItemMedia}
key={item}
uri={normalizeURI(item)}
navigation={navigation}
showDetails
compactView={false}
/>
);
}}
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
data={uris ? this.appendMorePlaceholder(uris.slice(0, horizontalLimit)) : []} data={uris ? this.appendMorePlaceholder(uris.slice(0, horizontalLimit)) : []}

View file

@ -1,39 +0,0 @@
import { connect } from 'react-redux';
import {
doResolveUri,
makeSelectClaimForUri,
makeSelectMetadataForUri,
makeSelectFileInfoForUri,
makeSelectIsUriResolving,
makeSelectClaimIsNsfw,
makeSelectShortUrlForUri,
makeSelectTitleForUri,
makeSelectThumbnailForUri,
} from 'lbry-redux';
import { doSetPlayerVisible } from 'redux/actions/drawer';
import { selectBlackListedOutpoints, selectFilteredOutpoints, selectRewardContentClaimIds } from 'lbryinc';
import { selectShowNsfw } from 'redux/selectors/settings';
import ClaimResultItem from './view';
const select = (state, props) => ({
blackListedOutpoints: selectBlackListedOutpoints(state),
claim: makeSelectClaimForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
filteredOutpoints: selectFilteredOutpoints(state),
isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state),
shortUrl: makeSelectShortUrlForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
});
const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)),
setPlayerVisible: (visible, uri) => dispatch(doSetPlayerVisible(visible, uri)),
});
export default connect(select, perform)(ClaimResultItem);

View file

@ -1,180 +0,0 @@
import React from 'react';
import { normalizeURI, parseURI } from 'lbry-redux';
import { ActivityIndicator, Platform, Text, TouchableOpacity, View } from 'react-native';
import { navigateToUri, getDownloadProgress, getStorageForFileInfo } from 'utils/helper';
import Colors from 'styles/colors';
import ChannelIconItem from 'component/channelIconItem';
import channelIconStyle from 'styles/channelIcon';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import DateTime from 'component/dateTime';
import FastImage from 'react-native-fast-image';
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 ProgressBar from 'component/progressBar';
import fileListStyle from 'styles/fileList';
import seedrandom from 'seedrandom';
class ClaimResultItem extends React.PureComponent {
state = {
autoStyle: null,
};
componentDidMount() {
const { result } = this.props;
if (!result || !result.name || !result.claimId) {
this.setState({
autoStyle:
ChannelIconItem.AUTO_THUMB_STYLES[Math.floor(Math.random() * ChannelIconItem.AUTO_THUMB_STYLES.length)],
});
} else {
// result property set, use deterministic random style
const rng = seedrandom(normalizeURI(`${result.name}#${result.claimId}`));
const index = Math.floor(rng.quick() * ChannelIconItem.AUTO_THUMB_STYLES.length);
this.setState({ autoStyle: ChannelIconItem.AUTO_THUMB_STYLES[index] });
}
}
onPressHandler = () => {
const { autoplay, navigation, result, urlOpenHandler, setPlayerVisible } = this.props;
const { claimId, name } = result;
const url = normalizeURI(`${name}#${claimId}`);
if (urlOpenHandler) {
urlOpenHandler(url);
} else {
navigateToUri(navigation, url, { autoplay }, false, url, setPlayerVisible);
}
};
render() {
const { fileInfo, navigation, obscureNsfw, result, rewardedContentClaimIds, setPlayerVisible, style } = this.props;
const {
channel,
channel_claim_id: channelClaimId,
claimId,
duration,
fee,
name,
nsfw,
release_time: releaseTime,
thumbnail_url: thumbnailUrl,
title,
} = result;
const isChannel = name && name.startsWith('@');
const hasThumbnail = !!thumbnailUrl;
const obscure = obscureNsfw && nsfw;
const url = normalizeURI(`${name}#${claimId}`);
const hasChannel = !!channel;
const channelUrl = hasChannel ? normalizeURI(`${channel}#${channelClaimId}`) : null;
const isRewardContent = rewardedContentClaimIds.includes(claimId);
return (
<View style={style}>
<TouchableOpacity
style={[style, isChannel ? fileListStyle.channelContainer : null]}
onPress={this.onPressHandler}
>
{!isChannel && (
<FileItemMedia
style={fileListStyle.thumbnail}
duration={duration}
resizeMode="cover"
title={title || name || normalizeURI(url).substring(7)}
thumbnail={thumbnailUrl}
/>
)}
{isChannel && (
<View style={fileListStyle.channelThumbnailView}>
<View style={[fileListStyle.channelThumbnailContainer, this.state.autoStyle]}>
{hasThumbnail && (
<FastImage
style={fileListStyle.channelThumbnail}
resizeMode={FastImage.resizeMode.cover}
source={{ uri: thumbnailUrl }}
/>
)}
{!hasThumbnail && (
<Text style={channelIconStyle.autothumbCharacter}>
{title ? title.substring(0, 1).toUpperCase() : name.substring(1, 2).toUpperCase()}
</Text>
)}
</View>
</View>
)}
{fileInfo && fileInfo.completed && fileInfo.download_path && (
<Icon style={fileListStyle.downloadedIcon} solid color={Colors.NextLbryGreen} name={'folder'} size={16} />
)}
<FilePrice
cost={fee ? parseFloat(fee) / 100000000 : 0}
uri={url}
style={fileListStyle.filePriceContainer}
iconStyle={fileListStyle.filePriceIcon}
textStyle={fileListStyle.filePriceText}
/>
<View style={fileListStyle.detailsContainer}>
{(title || name) && (
<View style={fileListStyle.titleContainer}>
<Text style={fileListStyle.title} numberOfLines={3}>
{title || name}
</Text>
{isRewardContent && <Icon style={fileListStyle.rewardIcon} name="award" size={12} />}
</View>
)}
{(hasChannel || isChannel) && (
<Link
style={fileListStyle.publisher}
text={isChannel ? name : channel}
onPress={() => {
navigateToUri(
navigation,
normalizeURI(isChannel ? url : channelUrl),
null,
false,
isChannel ? url : channelUrl,
setPlayerVisible,
);
}}
/>
)}
<View style={fileListStyle.info}>
{fileInfo && !isNaN(fileInfo.written_bytes) && fileInfo.written_bytes > 0 && (
<Text style={fileListStyle.infoText}>{getStorageForFileInfo(fileInfo)}</Text>
)}
<DateTime
style={fileListStyle.publishInfo}
textStyle={fileListStyle.infoText}
timeAgo
date={releaseTime}
/>
</View>
{fileInfo && fileInfo.download_path && (
<View style={fileListStyle.downloadInfo}>
{!fileInfo.completed && (
<ProgressBar
borderRadius={3}
color={Colors.NextLbryGreen}
height={3}
style={fileListStyle.progress}
progress={getDownloadProgress(fileInfo)}
/>
)}
</View>
)}
</View>
</TouchableOpacity>
{obscure && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />}
</View>
);
}
}
export default ClaimResultItem;

View file

@ -20,7 +20,7 @@ class CustomRewardCard extends React.PureComponent<Props> {
if (error && error.trim().length > 0) { if (error && error.trim().length > 0) {
notify({ message: error }); notify({ message: error });
} else { } else {
notify({ message: __('Reward successfully claimed!') }); notify({ message: 'Reward successfully claimed!' });
this.setState({ rewardCode: '' }); this.setState({ rewardCode: '' });
} }
this.setState({ claimStarted: false }); this.setState({ claimStarted: false });
@ -37,12 +37,12 @@ class CustomRewardCard extends React.PureComponent<Props> {
if (showVerification) { if (showVerification) {
showVerification(); showVerification();
} }
notify({ message: __('Unfortunately, you are not eligible to claim this reward at this time.') }); notify({ message: 'Unfortunately, you are not eligible to claim this reward at this time.' });
return; return;
} }
if (!rewardCode || rewardCode.trim().length === 0) { if (!rewardCode || rewardCode.trim().length === 0) {
notify({ message: __('Please enter a reward code to claim.') }); notify({ message: 'Please enter a reward code to claim.' });
return; return;
} }
@ -60,9 +60,9 @@ class CustomRewardCard extends React.PureComponent<Props> {
{rewardIsPending && <ActivityIndicator size="small" color={Colors.NextLbryGreen} />} {rewardIsPending && <ActivityIndicator size="small" color={Colors.NextLbryGreen} />}
</View> </View>
<View style={rewardStyle.midCol}> <View style={rewardStyle.midCol}>
<Text style={rewardStyle.rewardTitle}>{__('Custom Code')}</Text> <Text style={rewardStyle.rewardTitle}>Custom Code</Text>
<Text style={rewardStyle.rewardDescription}> <Text style={rewardStyle.rewardDescription}>
{__('Are you a supermodel or rockstar that received a custom reward code? Claim it here.')} Are you a supermodel or rockstar that received a custom reward code? Claim it here.
</Text> </Text>
<View> <View>
@ -74,7 +74,7 @@ class CustomRewardCard extends React.PureComponent<Props> {
/> />
<Button <Button
style={rewardStyle.redeemButton} style={rewardStyle.redeemButton}
text={__('Redeem')} text={'Redeem'}
disabled={!this.state.rewardCode || this.state.rewardCode.trim().length === 0 || rewardIsPending} disabled={!this.state.rewardCode || this.state.rewardCode.trim().length === 0 || rewardIsPending}
onPress={() => { onPress={() => {
if (!rewardIsPending) { if (!rewardIsPending) {

View file

@ -1,19 +1,4 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doToast, selectBalance, selectMyChannelClaims } from 'lbry-redux';
import { selectUnclaimedRewardValue, selectUser } from 'lbryinc';
import { selectSdkReady } from 'redux/selectors/settings';
import DrawerContent from './view'; import DrawerContent from './view';
const select = state => ({ export default connect()(DrawerContent);
balance: selectBalance(state),
channels: selectMyChannelClaims(state),
sdkReady: selectSdkReady(state),
unclaimedRewardAmount: selectUnclaimedRewardValue(state),
user: selectUser(state),
});
const perform = dispatch => ({
notify: data => dispatch(doToast(data)),
});
export default connect(select, perform)(DrawerContent);

View file

@ -1,227 +1,36 @@
import React from 'react'; import React from 'react';
import { DrawerItems, SafeAreaView } from 'react-navigation'; import { DrawerItems, SafeAreaView } from 'react-navigation';
import { Image, ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { ScrollView } from 'react-native';
import Button from 'component/button'; import Constants from 'constants';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import Icon from 'react-native-vector-icons/FontAwesome5';
import channelIconStyle from 'styles/channelIcon';
import discoverStyle from 'styles/discover'; import discoverStyle from 'styles/discover';
import { Lbryio } from 'lbryinc';
import { formatUsd } from 'utils/helper';
const groupedMenuItems = {
'Find content': [
{ icon: 'heart', solid: true, label: 'Following', route: Constants.DRAWER_ROUTE_SUBSCRIPTIONS },
{ icon: 'hashtag', label: 'Your Tags', route: Constants.DRAWER_ROUTE_DISCOVER },
{ icon: 'globe-americas', label: 'All Content', route: Constants.DRAWER_ROUTE_TRENDING },
],
'Your content': [
{ icon: 'at', label: 'Channels', route: Constants.DRAWER_ROUTE_CHANNEL_CREATOR },
{ icon: 'download', label: 'Library', route: Constants.DRAWER_ROUTE_MY_LBRY },
{ icon: 'cloud-upload-alt', label: 'Publishes', route: Constants.DRAWER_ROUTE_PUBLISHES },
{ icon: 'upload', label: 'New Publish', route: Constants.DRAWER_ROUTE_PUBLISH },
],
Wallet: [
{ icon: 'wallet', label: 'Wallet', route: Constants.DRAWER_ROUTE_WALLET },
{ icon: 'award', label: 'Rewards', route: Constants.DRAWER_ROUTE_REWARDS },
{ icon: 'user-friends', label: 'Invites', route: Constants.DRAWER_ROUTE_INVITES },
],
Settings: [
{ icon: 'cog', label: 'Settings', route: Constants.DRAWER_ROUTE_SETTINGS },
{ icon: 'info', label: 'About', route: Constants.DRAWER_ROUTE_ABOUT },
],
};
const groupNames = Object.keys(groupedMenuItems);
const routesRequiringSdkReady = [
Constants.DRAWER_ROUTE_CHANNEL_CREATOR,
Constants.DRAWER_ROUTE_MY_LBRY,
Constants.DRAWER_ROUTE_PUBLISHES,
Constants.DRAWER_ROUTE_PUBLISH,
Constants.DRAWER_ROUTE_WALLET,
Constants.DRAWER_ROUTE_REWARDS,
Constants.DRAWER_ROUTE_INVITES,
];
class DrawerContent extends React.PureComponent { class DrawerContent extends React.PureComponent {
state = {
usdExchangeRate: 0,
};
componentDidMount() {
Lbryio.getExchangeRates().then(rates => {
if (!isNaN(rates.LBC_USD)) {
this.setState({ usdExchangeRate: rates.LBC_USD });
}
});
}
getAvatarImageUrl = () => {
const { channels = [] } = this.props;
if (channels) {
// get the first channel thumbnail found. In the future, allow the user to select a default channel thumbnail?
for (let i = 0; i < channels.length; i++) {
if (channels[i].value && channels[i].value.thumbnail) {
return channels[i].value.thumbnail.url;
}
}
}
return null;
};
launchSignInFlow = () => {
// for now, sync flow (email, then password input) will be the default sign in flow
const { navigation } = this.props;
navigation.navigate({
routeName: 'Verification',
key: 'verification',
params: { syncFlow: true, signInFlow: true },
});
};
handleItemPress = routeName => {
const { navigation, notify, sdkReady } = this.props;
if (!sdkReady && routesRequiringSdkReady.includes(routeName)) {
if (navigation.closeDrawer) {
navigation.closeDrawer();
}
notify({
message: __('The background service is still initializing. Please try again when initialization is complete.'),
});
return;
}
navigation.navigate({ routeName: routeName });
};
render() { render() {
const { activeTintColor, balance, navigation, unclaimedRewardAmount, user, onItemPress } = this.props; const props = this.props;
const { state } = navigation; const { navigation, onItemPress } = props;
const activeItemKey = state.routes[state.index] ? state.routes[state.index].key : null;
const signedIn = user && user.has_verified_email;
const avatarImageUrl = this.getAvatarImageUrl();
return ( return (
<View style={discoverStyle.drawerContentArea}> <ScrollView>
{false && ( <SafeAreaView style={discoverStyle.drawerContentContainer} forceInset={{ top: 'always', horizontal: 'never' }}>
<View style={discoverStyle.signInContainer}> <DrawerItems
{!signedIn && ( {...props}
<Button onItemPress={route => {
style={discoverStyle.signInButton} const { routeName } = route.route;
theme={'light'} if (Constants.FULL_ROUTE_NAME_DISCOVER === routeName) {
text={__('Sign in')} navigation.navigate({ routeName: Constants.DRAWER_ROUTE_DISCOVER });
onPress={this.launchSignInFlow} return;
/> }
)}
{signedIn && (
<View style={discoverStyle.signedIn}>
<View style={discoverStyle.signedInAvatar}>
{avatarImageUrl && (
<Image
style={discoverStyle.signedInAvatarImage}
resizeMode={'cover'}
source={{ uri: avatarImageUrl }}
/>
)}
{!avatarImageUrl && (
<Text style={channelIconStyle.autothumbCharacter}>
{user.primary_email.substring(0, 1).toUpperCase()}
</Text>
)}
</View>
<Text style={discoverStyle.signedInEmail} numberOfLines={1}>
{user.primary_email}
</Text>
</View>
)}
</View>
)}
<ScrollView contentContainerStyle={discoverStyle.menuScrollContent}> if (Constants.FULL_ROUTE_NAME_WALLET === routeName) {
<SafeAreaView navigation.navigate({ routeName: Constants.DRAWER_ROUTE_WALLET });
style={discoverStyle.drawerContentContainer} return;
forceInset={{ top: 'always', horizontal: 'never' }} }
>
{!signedIn && (
<TouchableOpacity
accessible
accessibilityLabel={__('Sign In')}
onPress={this.launchSignInFlow}
delayPressIn={0}
style={[discoverStyle.signInMenuItem, discoverStyle.signInMenuItemButton]}
>
<Text style={discoverStyle.signInMenuItemButtonText}>{__('SIGN IN')}</Text>
</TouchableOpacity>
)}
{signedIn && ( onItemPress(route);
<View style={[discoverStyle.signInMenuItem, discoverStyle.signInMenuItemBorder]}> }}
<Text style={discoverStyle.signInMenuItemText} numberOfLines={1}> />
{user.primary_email.toUpperCase()} </SafeAreaView>
</Text> </ScrollView>
</View>
)}
{groupNames.map(groupName => {
const menuItems = groupedMenuItems[groupName];
return (
<View key={groupName} style={discoverStyle.menuGroup}>
{groupNames[3] !== groupName && (
<Text key={`${groupName}-title`} style={discoverStyle.menuGroupName}>
{__(groupName)}
</Text>
)}
{menuItems.map(item => {
const focused =
activeItemKey === item.route ||
(activeItemKey === Constants.FULL_ROUTE_NAME_DISCOVER &&
item.route === Constants.DRAWER_ROUTE_SUBSCRIPTIONS) ||
(activeItemKey === Constants.FULL_ROUTE_NAME_WALLET &&
item.route === Constants.DRAWER_ROUTE_WALLET);
return (
<TouchableOpacity
accessible
accessibilityLabel={__(item.label)}
style={[
discoverStyle.menuItemTouchArea,
focused ? discoverStyle.menuItemTouchAreaFocused : null,
]}
key={item.label}
onPress={() => this.handleItemPress(item.route)}
delayPressIn={0}
>
<View style={discoverStyle.menuItemIcon}>
<Icon
name={item.icon}
size={16}
solid={item.solid}
color={focused ? activeTintColor : null}
/>
</View>
<Text style={[discoverStyle.menuItem, focused ? discoverStyle.menuItemFocused : null]}>
{__(item.label)}
{item.label === 'Wallet' && this.state.usdExchangeRate > 0 && (
<Text> ({formatUsd(parseFloat(balance) * parseFloat(this.state.usdExchangeRate))})</Text>
)}
{item.label === 'Rewards' && this.state.usdExchangeRate > 0 && (
<Text>
{' '}
({formatUsd(parseFloat(unclaimedRewardAmount) * parseFloat(this.state.usdExchangeRate))})
</Text>
)}
</Text>
</TouchableOpacity>
);
})}
</View>
);
})}
</SafeAreaView>
</ScrollView>
</View>
); );
} }
} }

View file

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

View file

@ -1,26 +0,0 @@
import React from 'react';
import { NativeModules, Text, View, Image, TouchableOpacity } from 'react-native';
import Button from '../button';
import emptyStateStyle from 'styles/emptyState';
class EmptyStateView extends React.PureComponent {
render() {
const { message, buttonText, inner, onButtonPress } = this.props;
return (
<View
style={[emptyStateStyle.container, inner ? emptyStateStyle.innerContainer : emptyStateStyle.outerContainer]}
>
<Image style={emptyStateStyle.image} resizeMode={'stretch'} source={require('../../assets/gerbil-happy.png')} />
<Text style={emptyStateStyle.message}>{message}</Text>
{buttonText && (
<View style={emptyStateStyle.buttonContainer}>
<Button style={emptyStateStyle.button} text={buttonText} onPress={onButtonPress} />
</View>
)}
</View>
);
}
}
export default EmptyStateView;

View file

@ -11,6 +11,11 @@ class FileDownloadButton extends React.PureComponent {
} }
} }
componentWillReceiveProps(nextProps) {
//this.checkAvailability(nextProps.uri);
//this.restartDownload(nextProps);
}
restartDownload(props) { restartDownload(props) {
const { downloading, fileInfo, uri, restartDownload } = props; const { downloading, fileInfo, uri, restartDownload } = props;
@ -30,6 +35,7 @@ class FileDownloadButton extends React.PureComponent {
fileInfo, fileInfo,
downloading, downloading,
uri, uri,
purchaseUri,
costInfo, costInfo,
isPlayable, isPlayable,
isViewable, isViewable,
@ -39,27 +45,18 @@ class FileDownloadButton extends React.PureComponent {
doPause, doPause,
style, style,
openFile, openFile,
onFileActionPress,
onButtonLayout, onButtonLayout,
} = this.props; } = this.props;
if (fileInfo && fileInfo.download_path && fileInfo.completed) { if ((fileInfo && !fileInfo.stopped) || loading || downloading) {
return (
<TouchableOpacity
onLayout={onButtonLayout}
style={[style, fileDownloadButtonStyle.container]}
onPress={openFile}
>
<Text style={fileDownloadButtonStyle.text}>{isViewable ? __('View') : __('Open')}</Text>
</TouchableOpacity>
);
} else if ((fileInfo && !fileInfo.stopped) || loading || downloading) {
const progress = fileInfo && fileInfo.written_bytes ? (fileInfo.written_bytes / fileInfo.total_bytes) * 100 : 0, const progress = fileInfo && fileInfo.written_bytes ? (fileInfo.written_bytes / fileInfo.total_bytes) * 100 : 0,
label = fileInfo ? __('%progress%% complete', { progress: progress.toFixed(0) }) : __('Connecting...'); label = fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...';
return ( return (
<View style={[style, fileDownloadButtonStyle.container]}> <View style={[style, fileDownloadButtonStyle.container]}>
<View style={{ width: `${progress}%`, backgroundColor: '#ff0000', position: 'absolute', left: 0, top: 0 }} /> <View
style={{ width: `${progress}%`, backgroundColor: '#ff0000', position: 'absolute', left: 0, top: 0 }}
></View>
<Text style={fileDownloadButtonStyle.text}>{label}</Text> <Text style={fileDownloadButtonStyle.text}>{label}</Text>
</View> </View>
); );
@ -67,19 +64,43 @@ class FileDownloadButton extends React.PureComponent {
if (!costInfo) { if (!costInfo) {
return ( return (
<View style={[style, fileDownloadButtonStyle.container]}> <View style={[style, fileDownloadButtonStyle.container]}>
<Text style={fileDownloadButtonStyle.text}>{__('Fetching cost info...')}</Text> <Text style={fileDownloadButtonStyle.text}>Fetching cost info...</Text>
</View> </View>
); );
} }
return ( return (
<Button <Button
icon={isPlayable ? 'play' : null} icon={isPlayable ? 'play' : null}
text={isPlayable ? __('Play') : isViewable ? __('View') : __('Download')} text={isPlayable ? 'Play' : isViewable ? 'View' : 'Download'}
onLayout={onButtonLayout} onLayout={onButtonLayout}
style={[style, fileDownloadButtonStyle.container]} style={[style, fileDownloadButtonStyle.container]}
onPress={onFileActionPress} 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; return null;

View file

@ -8,23 +8,18 @@ import {
makeSelectTitleForUri, makeSelectTitleForUri,
makeSelectIsUriResolving, makeSelectIsUriResolving,
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
makeSelectShortUrlForUri,
} from 'lbry-redux'; } from 'lbry-redux';
import { doSetPlayerVisible } from 'redux/actions/drawer'; import { selectRewardContentClaimIds } from 'lbryinc';
import { selectBlackListedOutpoints, selectFilteredOutpoints, selectRewardContentClaimIds } from 'lbryinc';
import { selectShowNsfw } from 'redux/selectors/settings'; import { selectShowNsfw } from 'redux/selectors/settings';
import FileItem from './view'; import FileItem from './view';
const select = (state, props) => ({ const select = (state, props) => ({
blackListedOutpoints: selectBlackListedOutpoints(state),
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state),
filteredOutpoints: selectFilteredOutpoints(state),
metadata: makeSelectMetadataForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state), rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state), isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
obscureNsfw: !selectShowNsfw(state), obscureNsfw: !selectShowNsfw(state),
shortUrl: makeSelectShortUrlForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state), thumbnail: makeSelectThumbnailForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state), title: makeSelectTitleForUri(props.uri)(state),
nsfw: makeSelectClaimIsNsfw(props.uri)(state), nsfw: makeSelectClaimIsNsfw(props.uri)(state),
@ -32,7 +27,9 @@ const select = (state, props) => ({
const perform = dispatch => ({ const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)), resolveUri: uri => dispatch(doResolveUri(uri)),
setPlayerVisible: (visible, uri) => dispatch(doSetPlayerVisible(visible, uri)),
}); });
export default connect(select, perform)(FileItem); export default connect(
select,
perform
)(FileItem);

View file

@ -30,65 +30,39 @@ class FileItem extends React.PureComponent {
} }
navigateToFileUri = () => { navigateToFileUri = () => {
const { claim, navigation, uri, shortUrl } = this.props; const { navigation, uri } = this.props;
const normalizedUri = normalizeURI(uri); const normalizedUri = normalizeURI(uri);
if (NativeModules.Firebase) { if (NativeModules.Firebase) {
NativeModules.Firebase.track('explore_click', { uri: normalizedUri, short_url: shortUrl }); NativeModules.Firebase.track('explore_click', { uri: normalizedUri });
} }
navigateToUri(navigation, shortUrl || uri, null, false, claim ? claim.permanent_url : null); navigateToUri(navigation, normalizedUri);
}; };
render() { render() {
const { const {
blackListedOutpoints,
claim, claim,
title, title,
thumbnail, thumbnail,
fileInfo, fileInfo,
filteredOutpoints,
metadata, metadata,
isResolvingUri, isResolvingUri,
rewardedContentClaimIds, rewardedContentClaimIds,
style, style,
mediaStyle, mediaStyle,
navigation, navigation,
nsfw,
obscureNsfw,
showDetails, showDetails,
compactView, compactView,
setPlayerVisible,
titleBeforeThumbnail, titleBeforeThumbnail,
} = this.props; } = this.props;
if (claim && claim.value_type === 'channel') {
// don't display channels in the lists on the Your tags page
return null;
}
let shouldHide = false;
if (blackListedOutpoints || filteredOutpoints) {
const outpointsToHide = !blackListedOutpoints
? filteredOutpoints
: blackListedOutpoints.concat(filteredOutpoints);
shouldHide = outpointsToHide.some(
outpoint => outpoint && outpoint.txid === claim.txid && outpoint.nout === claim.nout,
);
}
if (shouldHide) {
// don't display blacklisted or filtered outpoints on the Your tags page
return null;
}
const uri = normalizeURI(this.props.uri); const uri = normalizeURI(this.props.uri);
const obscure = obscureNsfw && nsfw; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
const signingChannel = claim ? claim.signing_channel : null; const signingChannel = claim ? claim.signing_channel : null;
const channelName = signingChannel ? signingChannel.name : null; const channelName = signingChannel ? signingChannel.name : null;
const channelClaimId = signingChannel ? signingChannel.claim_id : null; const channelClaimId = signingChannel ? signingChannel.claim_id : null;
const fullChannelUri = channelClaimId ? `${channelName}#${channelClaimId}` : channelName; const fullChannelUri = channelClaimId ? `${channelName}#${channelClaimId}` : channelName;
const shortChannelUri = signingChannel ? signingChannel.short_url : null;
const height = claim ? claim.height : null; const height = claim ? claim.height : null;
const duration = claim && claim.value && claim.value.video ? claim.value.video.duration : null;
return ( return (
<View style={style}> <View style={style}>
@ -99,23 +73,19 @@ class FileItem extends React.PureComponent {
</Text> </Text>
)} )}
<FileItemMedia <FileItemMedia
duration={duration}
title={title} title={title}
thumbnail={thumbnail} thumbnail={thumbnail}
blurRadius={obscureNsfw ? 15 : 0}
resizeMode="cover" resizeMode="cover"
isResolvingUri={isResolvingUri} isResolvingUri={isResolvingUri}
style={mediaStyle} style={mediaStyle}
/> />
{!compactView && fileInfo && fileInfo.completed && fileInfo.download_path && ( {!compactView && fileInfo && fileInfo.completed && fileInfo.download_path && (
<Icon style={discoverStyle.downloadedIcon} solid color={Colors.NextLbryGreen} name={'folder'} size={16} /> <Icon style={discoverStyle.downloadedIcon} solid color={Colors.NextLbryGreen} name={'folder'} size={16} />
)} )}
{!compactView && (!fileInfo || !fileInfo.completed || !fileInfo.download_path) && ( {!compactView && (!fileInfo || !fileInfo.completed || !fileInfo.download_path) && (
<FilePrice <FilePrice uri={uri} style={discoverStyle.filePriceContainer} textStyle={discoverStyle.filePriceText} />
uri={uri}
style={discoverStyle.filePriceContainer}
iconStyle={discoverStyle.filePriceIcon}
textStyle={discoverStyle.filePriceText}
/>
)} )}
{!compactView && ( {!compactView && (
<View style={isRewardContent ? discoverStyle.rewardTitleContainer : null}> <View style={isRewardContent ? discoverStyle.rewardTitleContainer : null}>
@ -132,23 +102,17 @@ class FileItem extends React.PureComponent {
style={discoverStyle.channelName} style={discoverStyle.channelName}
text={channelName} text={channelName}
onPress={() => { onPress={() => {
navigateToUri( navigateToUri(navigation, normalizeURI(fullChannelUri));
navigation,
normalizeURI(shortChannelUri || fullChannelUri),
null,
false,
fullChannelUri,
setPlayerVisible,
);
}} }}
/> />
)} )}
{!channelName && !isResolvingUri && <Text style={discoverStyle.anonChannelName}>{__('Anonymous')}</Text>}
<DateTime style={discoverStyle.dateTime} textStyle={discoverStyle.dateTimeText} timeAgo uri={uri} /> <DateTime style={discoverStyle.dateTime} textStyle={discoverStyle.dateTimeText} timeAgo uri={uri} />
</View> </View>
)} )}
</TouchableOpacity> </TouchableOpacity>
{obscure && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />} {obscureNsfw && (
<NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />
)}
</View> </View>
); );
} }

View file

@ -2,26 +2,24 @@ import React from 'react';
import { ActivityIndicator, Image, Text, View } from 'react-native'; import { ActivityIndicator, Image, Text, View } from 'react-native';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import VideoDuration from 'component/videoDuration';
import autothumbStyle from 'styles/autothumb';
import fileItemMediaStyle from 'styles/fileItemMedia'; import fileItemMediaStyle from 'styles/fileItemMedia';
class FileItemMedia extends React.PureComponent { class FileItemMedia extends React.PureComponent {
static AUTO_THUMB_STYLES = [ static AUTO_THUMB_STYLES = [
autothumbStyle.autothumbPurple, fileItemMediaStyle.autothumbPurple,
autothumbStyle.autothumbRed, fileItemMediaStyle.autothumbRed,
autothumbStyle.autothumbPink, fileItemMediaStyle.autothumbPink,
autothumbStyle.autothumbIndigo, fileItemMediaStyle.autothumbIndigo,
autothumbStyle.autothumbBlue, fileItemMediaStyle.autothumbBlue,
autothumbStyle.autothumbLightBlue, fileItemMediaStyle.autothumbLightBlue,
autothumbStyle.autothumbCyan, fileItemMediaStyle.autothumbCyan,
autothumbStyle.autothumbTeal, fileItemMediaStyle.autothumbTeal,
autothumbStyle.autothumbGreen, fileItemMediaStyle.autothumbGreen,
autothumbStyle.autothumbYellow, fileItemMediaStyle.autothumbYellow,
autothumbStyle.autothumbOrange, fileItemMediaStyle.autothumbOrange,
]; ];
state = { state: {
imageLoadFailed: false, imageLoadFailed: false,
}; };
@ -50,7 +48,7 @@ class FileItemMedia extends React.PureComponent {
return false; return false;
} }
if (thumbnail.substring(0, 7) !== 'http://' && thumbnail.substring(0, 8) !== 'https://') { if (thumbnail.substring(0, 7) != 'http://' && thumbnail.substring(0, 8) != 'https://') {
return false; return false;
} }
@ -59,36 +57,37 @@ class FileItemMedia extends React.PureComponent {
render() { render() {
let style = this.props.style; let style = this.props.style;
const { duration, isResolvingUri, thumbnail, title, resizeMode } = this.props; const { blurRadius, isResolvingUri, thumbnail, title, resizeMode } = this.props;
const atStyle = this.state.autoThumbStyle; const atStyle = this.state.autoThumbStyle;
const hasDuration = !!duration;
if (this.isThumbnailValid(thumbnail) && !this.state.imageLoadFailed) { if (this.isThumbnailValid(thumbnail) && !this.state.imageLoadFailed) {
if (style == null) { if (style == null) {
style = fileItemMediaStyle.thumbnail; style = fileItemMediaStyle.thumbnail;
} }
return ( if (blurRadius > 0) {
<View style={style}> // No blur radius support in FastImage yet
<FastImage return (
<Image
source={{ uri: thumbnail }} source={{ uri: thumbnail }}
onError={() => this.setState({ imageLoadFailed: true })} blurRadius={blurRadius}
resizeMode={this.getFastImageResizeMode(resizeMode)} resizeMode={resizeMode ? resizeMode : 'cover'}
style={fileItemMediaStyle.image} style={style}
/> />
{duration > 0 && ( );
<VideoDuration }
duration={duration}
style={fileItemMediaStyle.duration} return (
textStyle={fileItemMediaStyle.durationText} <FastImage
/> source={{ uri: thumbnail }}
)} onError={() => this.setState({ imageLoadFailed: true })}
</View> resizeMode={this.getFastImageResizeMode(resizeMode)}
style={style}
/>
); );
} }
return ( return (
<View style={[style || fileItemMediaStyle.autothumb, atStyle]}> <View style={[style ? style : fileItemMediaStyle.autothumb, atStyle]}>
{isResolvingUri && ( {isResolvingUri && (
<View style={fileItemMediaStyle.resolving}> <View style={fileItemMediaStyle.resolving}>
<ActivityIndicator color={Colors.White} size={'large'} /> <ActivityIndicator color={Colors.White} size={'large'} />
@ -105,13 +104,6 @@ class FileItemMedia extends React.PureComponent {
.toUpperCase()} .toUpperCase()}
</Text> </Text>
)} )}
{duration > 0 && (
<VideoDuration
duration={duration}
style={fileItemMediaStyle.duration}
textStyle={fileItemMediaStyle.durationText}
/>
)}
</View> </View>
); );
} }

View file

@ -53,39 +53,39 @@ class FileList extends React.PureComponent<Props, State> {
dateNew: fileInfos => dateNew: fileInfos =>
this.props.sortByHeight this.props.sortByHeight
? fileInfos.slice().sort((fileInfo1, fileInfo2) => { ? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
if (fileInfo1.pending) { if (fileInfo1.pending) {
return -1; return -1;
} }
const height1 = this.props.claimsById[fileInfo1.claim_id] const height1 = this.props.claimsById[fileInfo1.claim_id]
? this.props.claimsById[fileInfo1.claim_id].height ? this.props.claimsById[fileInfo1.claim_id].height
: 0; : 0;
const height2 = this.props.claimsById[fileInfo2.claim_id] const height2 = this.props.claimsById[fileInfo2.claim_id]
? this.props.claimsById[fileInfo2.claim_id].height ? this.props.claimsById[fileInfo2.claim_id].height
: 0; : 0;
if (height1 > height2) { if (height1 > height2) {
return -1; return -1;
} else if (height1 < height2) { } else if (height1 < height2) {
return 1; return 1;
} }
return 0; return 0;
}) })
: [...fileInfos].reverse(), : [...fileInfos].reverse(),
dateOld: fileInfos => dateOld: fileInfos =>
this.props.sortByHeight this.props.sortByHeight
? fileInfos.slice().sort((fileInfo1, fileInfo2) => { ? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
const height1 = this.props.claimsById[fileInfo1.claim_id] const height1 = this.props.claimsById[fileInfo1.claim_id]
? this.props.claimsById[fileInfo1.claim_id].height ? this.props.claimsById[fileInfo1.claim_id].height
: 999999; : 999999;
const height2 = this.props.claimsById[fileInfo2.claim_id] const height2 = this.props.claimsById[fileInfo2.claim_id]
? this.props.claimsById[fileInfo2.claim_id].height ? this.props.claimsById[fileInfo2.claim_id].height
: 999999; : 999999;
if (height1 < height2) { if (height1 < height2) {
return -1; return -1;
} else if (height1 > height2) { } else if (height1 > height2) {
return 1; return 1;
} }
return 0; return 0;
}) })
: fileInfos, : fileInfos,
title: fileInfos => title: fileInfos =>
fileInfos.slice().sort((fileInfo1, fileInfo2) => { fileInfos.slice().sort((fileInfo1, fileInfo2) => {
@ -160,7 +160,7 @@ class FileList extends React.PureComponent<Props, State> {
// This is unfortunate // This is unfortunate
// https://github.com/lbryio/lbry/issues/1159 // https://github.com/lbryio/lbry/issues/1159
const name = claimName || claimNameDownloaded; const name = claimName || claimNameDownloaded;
uriParams.claimName = name; uriParams.contentName = name;
uriParams.claimId = claimId; uriParams.claimId = claimId;
const uri = buildURI(uriParams); const uri = buildURI(uriParams);
@ -179,7 +179,7 @@ class FileList extends React.PureComponent<Props, State> {
style={fileListStyle.fileItem} style={fileListStyle.fileItem}
uri={item} uri={item}
navigation={navigation} navigation={navigation}
showDetails showDetails={true}
compactView={false} compactView={false}
/> />
)} )}

View file

@ -5,13 +5,10 @@ import {
makeSelectMetadataForUri, makeSelectMetadataForUri,
makeSelectFileInfoForUri, makeSelectFileInfoForUri,
makeSelectIsUriResolving, makeSelectIsUriResolving,
makeSelectClaimIsNsfw,
makeSelectShortUrlForUri,
makeSelectTitleForUri, makeSelectTitleForUri,
makeSelectThumbnailForUri, makeSelectThumbnailForUri,
} from 'lbry-redux'; } from 'lbry-redux';
import { doSetPlayerVisible } from 'redux/actions/drawer'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectBlackListedOutpoints, selectFilteredOutpoints, selectRewardContentClaimIds } from 'lbryinc';
import { selectShowNsfw } from 'redux/selectors/settings'; import { selectShowNsfw } from 'redux/selectors/settings';
import FileListItem from './view'; import FileListItem from './view';
@ -22,18 +19,17 @@ const select = (state, props) => ({
filteredOutpoints: selectFilteredOutpoints(state), filteredOutpoints: selectFilteredOutpoints(state),
isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state), isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state),
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state), isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
obscureNsfw: !selectShowNsfw(state), obscureNsfw: !selectShowNsfw(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state),
shortUrl: makeSelectShortUrlForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state), title: makeSelectTitleForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state), thumbnail: makeSelectThumbnailForUri(props.uri)(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)), resolveUri: uri => dispatch(doResolveUri(uri)),
setPlayerVisible: (visible, uri) => dispatch(doSetPlayerVisible(visible, uri)),
}); });
export default connect(select, perform)(FileListItem); export default connect(
select,
perform
)(FileListItem);

View file

@ -1,15 +1,10 @@
import React from 'react'; import React from 'react';
import { normalizeURI, parseURI } from 'lbry-redux'; import { normalizeURI, parseURI } from 'lbry-redux';
import { ActivityIndicator, Platform, Text, TouchableOpacity, View } from 'react-native'; import { ActivityIndicator, Platform, Text, TouchableOpacity, View } from 'react-native';
import { navigateToUri, getDownloadProgress, getStorageForFileInfo } from 'utils/helper'; import { navigateToUri, formatBytes } from 'utils/helper';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
import ChannelIconItem from 'component/channelIconItem';
import channelIconStyle from 'styles/channelIcon';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import DateTime from 'component/dateTime'; import DateTime from 'component/dateTime';
import FastImage from 'react-native-fast-image';
import FileItemMedia from 'component/fileItemMedia'; import FileItemMedia from 'component/fileItemMedia';
import FilePrice from 'component/filePrice';
import Icon from 'react-native-vector-icons/FontAwesome5'; import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link'; import Link from 'component/link';
import NsfwOverlay from 'component/nsfwOverlay'; import NsfwOverlay from 'component/nsfwOverlay';
@ -17,9 +12,22 @@ import ProgressBar from 'component/progressBar';
import fileListStyle from 'styles/fileList'; import fileListStyle from 'styles/fileList';
class FileListItem extends React.PureComponent { class FileListItem extends React.PureComponent {
state = { getStorageForFileInfo = fileInfo => {
autoStyle: null, if (!fileInfo.completed) {
url: null, 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 => { getDownloadProgress = fileInfo => {
@ -27,41 +35,15 @@ class FileListItem extends React.PureComponent {
}; };
componentDidMount() { componentDidMount() {
const { claim, resolveUri, uri, batchResolve } = this.props; const { claim, resolveUri, uri } = this.props;
if (!claim && !batchResolve) { if (!claim) {
resolveUri(uri); resolveUri(uri);
} }
this.setState({
autoStyle:
ChannelIconItem.AUTO_THUMB_STYLES[Math.floor(Math.random() * ChannelIconItem.AUTO_THUMB_STYLES.length)],
});
}
componentDidUpdate() {
const { claim, resolveUri, uri, batchResolve } = this.props;
if (!claim && uri !== this.state.url && !batchResolve) {
this.setState({ url: uri }, () => resolveUri(uri));
}
} }
defaultOnPress = () => { defaultOnPress = () => {
const { autoplay, claim, featuredResult, navigation, uri, shortUrl } = this.props; const { navigation, uri } = this.props;
navigateToUri(navigation, uri);
if (featuredResult && !claim) {
navigation.navigate({ routeName: Constants.DRAWER_ROUTE_PUBLISH, params: { vanityUrl: uri.trim() } });
} else {
navigateToUri(navigation, shortUrl || uri, { autoplay }, false, claim ? claim.permanent_url : null);
}
};
onPressHandler = () => {
const { claim, onPress } = this.props;
if (onPress) {
onPress(claim);
} else {
this.defaultOnPress();
}
}; };
render() { render() {
@ -71,160 +53,53 @@ class FileListItem extends React.PureComponent {
fileInfo, fileInfo,
filteredOutpoints, filteredOutpoints,
metadata, metadata,
nsfw,
featuredResult, featuredResult,
isResolvingUri, isResolvingUri,
isDownloaded, isDownloaded,
style, style,
obscureNsfw,
onPress, onPress,
navigation, navigation,
rewardedContentClaimIds,
setPlayerVisible,
thumbnail, thumbnail,
hideChannel, hideChannel,
onLongPress,
selected,
title, title,
} = this.props; } = this.props;
const uri = normalizeURI(this.props.uri); const uri = normalizeURI(this.props.uri);
const obscure = obscureNsfw && nsfw; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
const isResolving = !fileInfo && isResolvingUri; const isResolving = !fileInfo && isResolvingUri;
const duration = claim && claim.value && claim.value.video ? claim.value.video.duration : null;
let name, let name, channel, height, channelClaimId, fullChannelUri, shouldHide, signingChannel;
channel,
height,
isRewardContent,
channelClaimId,
fullChannelUri,
repostUrl,
repostChannel,
repostChannelUrl,
shortChannelUri,
shouldHide,
signingChannel,
isRepost;
if (claim) { if (claim) {
name = claim.name; name = claim.name;
signingChannel = claim.signing_channel; signingChannel = claim.signing_channel;
channel = signingChannel ? signingChannel.name : null; channel = signingChannel ? signingChannel.name : null;
height = claim.height; height = claim.height;
isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
channelClaimId = signingChannel ? signingChannel.claim_id : null; channelClaimId = signingChannel ? signingChannel.claim_id : null;
fullChannelUri = channelClaimId ? `${channel}#${channelClaimId}` : channel; fullChannelUri = channelClaimId ? `${channel}#${channelClaimId}` : channel;
shortChannelUri = signingChannel ? signingChannel.short_url : null;
repostUrl = claim.repost_url;
repostChannelUrl = claim.repost_channel_url;
if (repostUrl) {
const { claimName: repostName, channelName } = parseURI(repostUrl);
repostChannel = channelName;
}
isRepost = !!repostUrl;
if (blackListedOutpoints || filteredOutpoints) { if (blackListedOutpoints || filteredOutpoints) {
const outpointsToHide = !blackListedOutpoints const outpointsToHide = blackListedOutpoints.concat(filteredOutpoints);
? filteredOutpoints shouldHide = outpointsToHide.some(outpoint => outpoint.txid === claim.txid && outpoint.nout === claim.nout);
: blackListedOutpoints.concat(filteredOutpoints);
shouldHide = outpointsToHide.some(
outpoint => outpoint && outpoint.txid === claim.txid && outpoint.nout === claim.nout,
);
} }
// TODO: hide channels on tag pages?
// shouldHide = 'channel' === claim.value_type;
} }
if (shouldHide || (!isResolvingUri && !claim && !featuredResult)) { if (shouldHide || (featuredResult && !isResolvingUri && !claim && !title && !name)) {
return null; return null;
} }
const actualHideChannel = !isRepost && hideChannel;
const isChannel = name && name.startsWith('@');
const hasThumbnail = !!thumbnail;
return ( return (
<View> <View style={style}>
{isRepost && ( <TouchableOpacity style={style} onPress={onPress || this.defaultOnPress}>
<View style={fileListStyle.repostInfo}> <FileItemMedia
<Icon name={'retweet'} size={14} style={fileListStyle.repostIcon} /> style={fileListStyle.thumbnail}
<Text style={fileListStyle.repostChannelName}> blurRadius={obscureNsfw ? 15 : 0}
<Link resizeMode="cover"
text={`@${repostChannel}`} title={title || name}
onPress={() => thumbnail={thumbnail}
navigateToUri(
navigation,
normalizeURI(repostChannelUrl || `@${repostChannel}`),
null,
false,
null,
false,
)
}
/>{' '}
reposted
</Text>
</View>
)}
<TouchableOpacity
style={[style, isChannel ? fileListStyle.channelContainer : null]}
onPress={this.onPressHandler}
onLongPress={() => {
if (onLongPress) {
onLongPress(claim);
}
}}
>
{!isChannel && (
<FileItemMedia
style={fileListStyle.thumbnail}
duration={duration}
resizeMode="cover"
title={title || name || normalizeURI(uri).substring(7)}
thumbnail={thumbnail}
/>
)}
{isChannel && (
<View style={fileListStyle.channelThumbnailView}>
<View style={[fileListStyle.channelThumbnailContainer, this.state.autoStyle]}>
{hasThumbnail && (
<FastImage
style={fileListStyle.channelThumbnail}
resizeMode={FastImage.resizeMode.cover}
source={{ uri: thumbnail }}
/>
)}
{!hasThumbnail && (
<Text style={channelIconStyle.autothumbCharacter}>
{title ? title.substring(0, 1).toUpperCase() : claim.name.substring(1, 2).toUpperCase()}
</Text>
)}
</View>
</View>
)}
{selected && (
<View style={fileListStyle.selectedOverlay}>
<Icon name={'check-circle'} solid color={Colors.NextLbryGreen} size={32} />
</View>
)}
{fileInfo && fileInfo.completed && fileInfo.download_path && (
<Icon
style={featuredResult ? fileListStyle.featuredDownloadedIcon : fileListStyle.downloadedIcon}
solid
color={Colors.NextLbryGreen}
name={'folder'}
size={16}
/>
)}
<FilePrice
uri={uri}
style={fileListStyle.filePriceContainer}
iconStyle={fileListStyle.filePriceIcon}
textStyle={fileListStyle.filePriceText}
/> />
{fileInfo && fileInfo.completed && fileInfo.download_path && (
<Icon style={fileListStyle.downloadedIcon} solid color={Colors.NextLbryGreen} name={'folder'} size={16} />
)}
<View style={fileListStyle.detailsContainer}> <View style={fileListStyle.detailsContainer}>
{featuredResult && ( {featuredResult && (
<Text style={fileListStyle.featuredUri} numberOfLines={1}> <Text style={fileListStyle.featuredUri} numberOfLines={1}>
@ -237,50 +112,30 @@ class FileListItem extends React.PureComponent {
{!title && !name && <Text style={fileListStyle.uri}>{uri}</Text>} {!title && !name && <Text style={fileListStyle.uri}>{uri}</Text>}
{!title && !name && ( {!title && !name && (
<View style={fileListStyle.row}> <View style={fileListStyle.row}>
<ActivityIndicator size={'small'} color={featuredResult ? Colors.White : Colors.NextLbryGreen} /> <ActivityIndicator size={'small'} color={featuredResult ? Colors.White : Colors.LbryGreen} />
</View> </View>
)} )}
</View> </View>
)} )}
{(title || name) && ( {(title || name) && (
<View style={fileListStyle.titleContainer}> <Text style={featuredResult ? fileListStyle.featuredTitle : fileListStyle.title}>
<Text {this.formatTitle(title) || this.formatTitle(name)}
style={featuredResult ? fileListStyle.featuredTitle : fileListStyle.title} </Text>
numberOfLines={actualHideChannel ? 4 : 3}
>
{title || name}
</Text>
{isRewardContent && <Icon style={fileListStyle.rewardIcon} name="award" size={12} />}
</View>
)} )}
{channel && !hideChannel && (
{featuredResult && !isResolving && !claim && (
<View style={fileListStyle.titleContainer}>
<Text style={fileListStyle.featuredTitle}>{__('Nothing here. Publish something!')}</Text>
</View>
)}
{(channel || isChannel) && !actualHideChannel && (
<Link <Link
style={fileListStyle.publisher} style={fileListStyle.publisher}
text={isChannel ? name : channel} text={channel}
onPress={() => { onPress={() => {
navigateToUri( navigateToUri(navigation, normalizeURI(fullChannelUri));
navigation,
normalizeURI(isChannel ? uri : shortChannelUri || fullChannelUri),
null,
false,
isChannel ? claim && claim.permanent_url : fullChannelUri,
setPlayerVisible,
);
}} }}
/> />
)} )}
<View style={fileListStyle.info}> <View style={fileListStyle.info}>
{fileInfo && !isNaN(fileInfo.written_bytes) && fileInfo.written_bytes > 0 && ( {fileInfo && !isNaN(fileInfo.written_bytes) && fileInfo.written_bytes > 0 && (
<Text style={fileListStyle.infoText}>{getStorageForFileInfo(fileInfo)}</Text> <Text style={fileListStyle.infoText}>{this.getStorageForFileInfo(fileInfo)}</Text>
)} )}
<DateTime style={fileListStyle.publishInfo} textStyle={fileListStyle.infoText} timeAgo uri={uri} /> <DateTime style={fileListStyle.publishInfo} textStyle={fileListStyle.infoText} timeAgo uri={uri} />
</View> </View>
@ -293,14 +148,16 @@ class FileListItem extends React.PureComponent {
color={Colors.NextLbryGreen} color={Colors.NextLbryGreen}
height={3} height={3}
style={fileListStyle.progress} style={fileListStyle.progress}
progress={getDownloadProgress(fileInfo)} progress={this.getDownloadProgress(fileInfo)}
/> />
)} )}
</View> </View>
)} )}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
{obscure && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />} {obscureNsfw && (
<NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />
)}
</View> </View>
); );
} }

View file

@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, View } from 'react-native'; import { Text, View } from 'react-native';
import { formatCredits, formatFullPrice } from 'lbry-redux'; import { formatCredits, formatFullPrice } from 'lbry-redux';
import Icon from 'react-native-vector-icons/FontAwesome5';
class CreditAmount extends React.PureComponent { class CreditAmount extends React.PureComponent {
static propTypes = { static propTypes = {
@ -43,16 +42,11 @@ class CreditAmount extends React.PureComponent {
let amountText; let amountText;
if (this.props.showFree && parseFloat(this.props.amount) === 0) { if (this.props.showFree && parseFloat(this.props.amount) === 0) {
amountText = __('FREE'); amountText = 'FREE';
} else { } else {
if (this.props.label) { if (this.props.label) {
const label = const label =
typeof this.props.label === 'string' typeof this.props.label === 'string' ? this.props.label : parseFloat(amount) == 1 ? 'credit' : 'credits';
? this.props.label
: parseFloat(amount) === 1
? __('credit')
: __('credits');
// TODO: handling singular / plural in other languages?
amountText = `${formattedAmount} ${label}`; amountText = `${formattedAmount} ${label}`;
} else { } else {
@ -63,27 +57,25 @@ class CreditAmount extends React.PureComponent {
} }
} }
return ( /*{this.props.isEstimate ? (
<Text style={style} numberOfLines={1}> <span
{amountText} className="credit-amount__estimate"
</Text> title={__('This is an estimate and does not include data fees')}
); >
*
</span>
) : null}*/
return <Text style={style}>{amountText}</Text>;
} }
} }
class FilePrice extends React.PureComponent { class FilePrice extends React.PureComponent {
componentWillMount() { componentWillMount() {
const { cost } = this.props; this.fetchCost(this.props);
if (isNaN(parseFloat(cost))) {
this.fetchCost(this.props);
}
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const { cost } = this.props; this.fetchCost(nextProps);
if (isNaN(parseFloat(cost))) {
this.fetchCost(nextProps);
}
} }
fetchCost(props) { fetchCost(props) {
@ -95,25 +87,30 @@ class FilePrice extends React.PureComponent {
} }
render() { render() {
const { cost, costInfo, look = 'indicator', showFullPrice = false, style, iconStyle, textStyle } = this.props; const { costInfo, look = 'indicator', showFullPrice = false, style, textStyle } = this.props;
const isEstimate = costInfo ? !costInfo.includesData : null; const isEstimate = costInfo ? !costInfo.includesData : null;
const amount = cost ? parseFloat(cost) : costInfo ? parseFloat(costInfo.cost) : 0;
if (isNaN(amount) || amount === 0) { if (!costInfo) {
return null; return (
<View style={style}>
<Text style={textStyle}>???</Text>
</View>
);
} }
return ( return (
<View style={style}> <View style={style}>
<Icon name="coins" size={9} style={iconStyle} />
<CreditAmount <CreditAmount
style={textStyle} style={textStyle}
label={false} label={false}
amount={amount} amount={parseFloat(costInfo.cost)}
isEstimate={isEstimate} isEstimate={isEstimate}
showFree showFree
showFullPrice={showFullPrice} showFullPrice={showFullPrice}
/> >
???
</CreditAmount>
</View> </View>
); );
} }

View file

@ -9,8 +9,8 @@ class FileRewardsDriver extends React.PureComponent<Props> {
return ( return (
<TouchableOpacity style={filePageStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}> <TouchableOpacity style={filePageStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}>
<Icon name="award" size={16} style={filePageStyle.rewardDriverIcon} /> <Icon name="award" size={16} style={filePageStyle.rewardIcon} />
<Text style={filePageStyle.rewardDriverText}>{__('Earn some credits to access this content.')}</Text> <Text style={filePageStyle.rewardDriverText}>Earn some credits to access this content.</Text>
</TouchableOpacity> </TouchableOpacity>
); );
} }

View file

@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { selectBalance } from 'lbry-redux'; import { selectBalance } from 'lbry-redux';
import { selectUnclaimedRewardValue } from 'lbryinc'; import { selectUnclaimedRewardValue } from 'lbryinc';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api import Constants from 'constants';
import FloatingWalletBalance from './view'; import FloatingWalletBalance from './view';
const select = state => ({ const select = state => ({
@ -11,4 +11,7 @@ const select = state => ({
rewardsNotInterested: makeSelectClientSetting(Constants.SETTING_REWARDS_NOT_INTERESTED)(state), rewardsNotInterested: makeSelectClientSetting(Constants.SETTING_REWARDS_NOT_INTERESTED)(state),
}); });
export default connect(select, null)(FloatingWalletBalance); export default connect(
select,
null
)(FloatingWalletBalance);

View file

@ -23,7 +23,7 @@ class FloatingWalletBalance extends React.PureComponent<Props> {
style={floatingButtonStyle.pendingContainer} style={floatingButtonStyle.pendingContainer}
onPress={() => navigation && navigation.navigate({ routeName: 'Rewards' })} onPress={() => navigation && navigation.navigate({ routeName: 'Rewards' })}
> >
<Icon name="award" size={14} style={floatingButtonStyle.rewardIcon} /> <Icon name="award" size={18} style={floatingButtonStyle.rewardIcon} />
<Text style={floatingButtonStyle.text}>{unclaimedRewardAmount}</Text> <Text style={floatingButtonStyle.text}>{unclaimedRewardAmount}</Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
@ -31,10 +31,9 @@ class FloatingWalletBalance extends React.PureComponent<Props> {
style={floatingButtonStyle.container} style={floatingButtonStyle.container}
onPress={() => navigation && navigation.navigate({ routeName: 'WalletStack' })} onPress={() => navigation && navigation.navigate({ routeName: 'WalletStack' })}
> >
<Icon name="coins" size={12} style={floatingButtonStyle.balanceIcon} />
{isNaN(balance) && <ActivityIndicator size="small" color={Colors.White} />} {isNaN(balance) && <ActivityIndicator size="small" color={Colors.White} />}
{(!isNaN(balance) || balance === 0) && ( {(!isNaN(balance) || balance === 0) && (
<Text style={floatingButtonStyle.text}>{formatCredits(parseFloat(balance), 1, true)}</Text> <Text style={floatingButtonStyle.text}>{formatCredits(parseFloat(balance), 2) + ' LBC'}</Text>
)} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View file

@ -36,7 +36,7 @@ class MediaPlayer extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
buffering: true, buffering: false,
backgroundPlayEnabled: false, backgroundPlayEnabled: false,
autoPaused: false, autoPaused: false,
rate: 1, rate: 1,
@ -45,9 +45,9 @@ class MediaPlayer extends React.PureComponent {
resizeMode: 'contain', resizeMode: 'contain',
duration: 0.0, duration: 0.0,
currentTime: 0.0, currentTime: 0.0,
paused: true, paused: !props.autoPlay,
fullscreenMode: false, fullscreenMode: false,
areControlsVisible: false, areControlsVisible: true,
controlsTimeout: -1, controlsTimeout: -1,
seekerOffset: 0, seekerOffset: 0,
seekerPosition: 0, seekerPosition: 0,
@ -87,9 +87,7 @@ class MediaPlayer extends React.PureComponent {
} }
onLoad = data => { onLoad = data => {
const { autoPlay } = this.props;
this.setState({ this.setState({
buffering: false,
duration: data.duration, duration: data.duration,
}); });
@ -102,17 +100,13 @@ class MediaPlayer extends React.PureComponent {
if (this.props.onMediaLoaded) { if (this.props.onMediaLoaded) {
this.props.onMediaLoaded(); this.props.onMediaLoaded();
} }
if (autoPlay) {
this.setState({ paused: false });
}
}; };
onProgress = data => { onProgress = data => {
const { savePosition, claim } = this.props; const { savePosition, claim } = this.props;
this.setState({ buffering: false, currentTime: data.currentTime }); this.setState({ buffering: false, currentTime: data.currentTime });
if (claim && data.currentTime > 0 && Math.floor(data.currentTime) % positionSaveInterval === 0) { if (data.currentTime > 0 && Math.floor(data.currentTime) % positionSaveInterval === 0) {
const { claim_id: claimId, txid, nout } = claim; const { claim_id: claimId, txid, nout } = claim;
savePosition(claimId, `${txid}:${nout}`, data.currentTime); savePosition(claimId, `${txid}:${nout}`, data.currentTime);
} }
@ -173,7 +167,7 @@ class MediaPlayer extends React.PureComponent {
togglePlay = () => { togglePlay = () => {
this.showPlayerControls(); this.showPlayerControls();
this.setState({ paused: !this.state.paused }, this.updateBackgroundMediaNotification); this.setState({ paused: !this.state.paused }, this.handlePausedState);
}; };
handlePausedState = () => { handlePausedState = () => {
@ -194,7 +188,7 @@ class MediaPlayer extends React.PureComponent {
}; };
onEnd = () => { onEnd = () => {
this.setState({ paused: true }, this.updateBackgroundMediaNotification); this.setState({ paused: true });
if (this.props.onPlaybackFinished) { if (this.props.onPlaybackFinished) {
this.props.onPlaybackFinished(); this.props.onPlaybackFinished();
} }
@ -333,10 +327,6 @@ class MediaPlayer extends React.PureComponent {
} }
}; };
onFocusChanged = evt => {
this.setState({ paused: !(this.state.paused && evt.hasAudioFocus) }, this.updateBackgroundMediaNotification);
};
onBuffer = () => { onBuffer = () => {
if (!this.state.paused) { if (!this.state.paused) {
this.setState({ buffering: true }, () => this.manualHidePlayerControls()); this.setState({ buffering: true }, () => this.manualHidePlayerControls());
@ -351,12 +341,6 @@ class MediaPlayer extends React.PureComponent {
this.setState({ paused: true }, this.updateBackgroundMediaNotification); this.setState({ paused: true }, this.updateBackgroundMediaNotification);
}; };
handleSeek = time => {
const { currentTime } = this.state;
const newTime = currentTime + time;
this.seekTo(newTime);
};
updateBackgroundMediaNotification = () => { updateBackgroundMediaNotification = () => {
this.handlePausedState(); this.handlePausedState();
const { backgroundPlayEnabled } = this.props; const { backgroundPlayEnabled } = this.props;
@ -378,26 +362,14 @@ class MediaPlayer extends React.PureComponent {
<Icon name={'arrow-left'} size={18} style={mediaPlayerStyle.backButtonIcon} /> <Icon name={'arrow-left'} size={18} style={mediaPlayerStyle.backButtonIcon} />
</TouchableOpacity> </TouchableOpacity>
<View style={mediaPlayerStyle.midControls}> <TouchableOpacity style={mediaPlayerStyle.playPauseButton} onPress={this.togglePlay}>
<TouchableOpacity style={mediaPlayerStyle.rewind10} onPress={() => this.handleSeek(-10)}> {this.state.paused && <Icon name="play" size={40} color="#ffffff" />}
<Icon name="undo" size={24} color={Colors.White} /> {!this.state.paused && <Icon name="pause" size={40} color="#ffffff" />}
<Text style={[mediaPlayerStyle.midControlText, mediaPlayerStyle.leftMidControlText]}>10</Text> </TouchableOpacity>
</TouchableOpacity>
<TouchableOpacity style={mediaPlayerStyle.playPauseButton} onPress={this.togglePlay}>
{this.state.paused && <Icon name="play" size={40} color={Colors.White} />}
{!this.state.paused && <Icon name="pause" size={40} color={Colors.White} />}
</TouchableOpacity>
<TouchableOpacity style={mediaPlayerStyle.forward10} onPress={() => this.handleSeek(10)}>
<Icon name="redo" size={24} color={Colors.White} />
<Text style={[mediaPlayerStyle.midControlText, mediaPlayerStyle.rightMidControlText]}>10</Text>
</TouchableOpacity>
</View>
<TouchableOpacity style={mediaPlayerStyle.toggleFullscreenButton} onPress={this.toggleFullscreenMode}> <TouchableOpacity style={mediaPlayerStyle.toggleFullscreenButton} onPress={this.toggleFullscreenMode}>
{this.state.fullscreenMode && <Icon name="compress" size={16} color={Colors.White} />} {this.state.fullscreenMode && <Icon name="compress" size={16} color="#ffffff" />}
{!this.state.fullscreenMode && <Icon name="expand" size={16} color={Colors.White} />} {!this.state.fullscreenMode && <Icon name="expand" size={16} color="#ffffff" />}
</TouchableOpacity> </TouchableOpacity>
<Text style={mediaPlayerStyle.elapsedDuration}>{this.formatTime(this.state.currentTime)}</Text> <Text style={mediaPlayerStyle.elapsedDuration}>{this.formatTime(this.state.currentTime)}</Text>
@ -446,13 +418,6 @@ class MediaPlayer extends React.PureComponent {
: mediaPlayerStyle.containedTrackingControls, : mediaPlayerStyle.containedTrackingControls,
]; ];
const seekerCircleStyle = [this.state.seeking ? mediaPlayerStyle.bigSeekerCircle : mediaPlayerStyle.seekerCircle];
if (!this.state.seeking) {
seekerCircleStyle.push(
this.state.fullscreenMode ? mediaPlayerStyle.seekerCircleTopFs : mediaPlayerStyle.seekerCircleTop,
);
}
return ( return (
<View style={styles} onLayout={onLayout}> <View style={styles} onLayout={onLayout}>
<Video <Video
@ -468,7 +433,6 @@ class MediaPlayer extends React.PureComponent {
}} }}
resizeMode={this.state.resizeMode} resizeMode={this.state.resizeMode}
playInBackground={this.state.backgroundPlayEnabled} playInBackground={this.state.backgroundPlayEnabled}
playWhenInactive={this.state.backgroundPlayEnabled}
style={mediaPlayerStyle.player} style={mediaPlayerStyle.player}
rate={this.state.rate} rate={this.state.rate}
volume={this.state.volume} volume={this.state.volume}
@ -479,7 +443,6 @@ class MediaPlayer extends React.PureComponent {
onEnd={this.onEnd} onEnd={this.onEnd}
onError={this.onError} onError={this.onError}
minLoadRetryCount={999} minLoadRetryCount={999}
onAudioFocusChanged={this.onFocusChanged}
/> />
{this.state.firstPlay && thumbnail && ( {this.state.firstPlay && thumbnail && (
@ -519,7 +482,7 @@ class MediaPlayer extends React.PureComponent {
]} ]}
{...this.seekResponder.panHandlers} {...this.seekResponder.panHandlers}
> >
<View style={seekerCircleStyle} /> <View style={this.state.seeking ? mediaPlayerStyle.bigSeekerCircle : mediaPlayerStyle.seekerCircle} />
</View> </View>
<TouchableOpacity <TouchableOpacity
style={[ style={[

View file

@ -46,7 +46,7 @@ export default class ModalPicker extends React.PureComponent {
onPress={() => onItemSelected(item)} onPress={() => onItemSelected(item)}
> >
{item.icon && <Icon style={modalPickerStyle.itemIcon} name={item.icon} size={16} />} {item.icon && <Icon style={modalPickerStyle.itemIcon} name={item.icon} size={16} />}
<Text style={modalPickerStyle.itemLabel}>{__(item.label)}</Text> <Text style={modalPickerStyle.itemLabel}>{item.label}</Text>
{selectedItem && selectedItem.name === item.name && ( {selectedItem && selectedItem.name === item.name && (
<Icon style={modalPickerStyle.itemSelected} name={'check'} color={Colors.LbryGreen} size={16} /> <Icon style={modalPickerStyle.itemSelected} name={'check'} color={Colors.LbryGreen} size={16} />
)} )}

View file

@ -1,28 +0,0 @@
import { connect } from 'react-redux';
import {
doClearRepostError,
doFetchChannelListMine,
doRepost,
doToast,
selectBalance,
selectMyChannelClaims,
selectRepostError,
selectRepostLoading,
} from 'lbry-redux';
import ModalRepostView from './view';
const select = state => ({
balance: selectBalance(state),
channels: selectMyChannelClaims(state),
reposting: selectRepostLoading(state),
error: selectRepostError(state),
});
const perform = dispatch => ({
fetchChannelListMine: () => dispatch(doFetchChannelListMine(1, 99999, true)),
notify: data => dispatch(doToast(data)),
repost: options => dispatch(doRepost(options)),
clearError: () => dispatch(doClearRepostError()),
});
export default connect(select, perform)(ModalRepostView);

View file

@ -1,204 +0,0 @@
import React from 'react';
import { ActivityIndicator, Alert, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { formatCredits, creditsToString } from 'lbry-redux';
import modalStyle from 'styles/modal';
import modalRepostStyle from 'styles/modalRepost';
import ChannelSelector from 'component/channelSelector';
import Button from 'component/button';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link';
import { logPublish } from 'utils/helper';
export default class ModalRepostView extends React.PureComponent {
depositAmountInput;
state = {
channelName: null,
creditsInputFocused: false,
depositAmount: '0.01',
repostName: null,
repostStarted: false,
showAdvanced: false,
};
componentDidMount() {
const { claim, fetchChannelListMine } = this.props;
const { name } = claim;
fetchChannelListMine();
this.setState({ repostName: name });
this.checkChannelSelection(this.props);
}
componentWillReceiveProps(nextProps) {
this.checkChannelSelection(nextProps);
const { notify } = this.props;
const { reposting, error } = nextProps;
if (this.state.repostStarted && !reposting && error) {
this.setState({ repostStarted: false });
notify({ message: error, isError: true });
}
}
checkChannelSelection = props => {
const { channels = [] } = props;
if (!this.state.channelName && channels && channels.length > 0) {
const firstChannel = channels[0];
this.setState({ channelName: firstChannel.name });
}
};
handleChannelChange = channelName => {
const { channels = [] } = this.props;
if (channels && channels.length > 0) {
const filtered = channels.filter(c => c.name === channelName);
if (filtered.length > 0) {
const channel = filtered[0];
this.setState({ channelName });
}
}
};
handleRepost = () => {
const { claim, balance, notify, repost, onRepostSuccessful, channels = [], clearError } = this.props;
const { depositAmount, repostName, channelName } = this.state;
if (parseInt(depositAmount, 10) > balance) {
notify({
message: 'Insufficient credits',
isError: true,
});
return;
}
clearError();
const channel = channels.filter(ch => ch.name === channelName)[0];
this.setState({ repostStarted: true }, () => {
repost({
name: repostName,
bid: creditsToString(depositAmount),
channel_id: channel.claim_id,
claim_id: claim.claim_id,
}).then(repostClaim => {
logPublish(repostClaim);
this.setState({ repostStarted: false });
notify({ message: __('The content was successfully reposted!') });
if (onRepostSuccessful) onRepostSuccessful();
});
});
};
render() {
const { balance, channels, reposting, title, onCancelPress, onOverlayPress } = this.props;
const canRepost = !!this.state.channelName && !!this.state.repostName;
const channelsLoaded = channels && channels.length > 0;
const processing = this.state.repostStarted || reposting || !channelsLoaded;
return (
<TouchableOpacity style={modalStyle.overlay} activeOpacity={1} onPress={onOverlayPress}>
<TouchableOpacity style={modalStyle.container} activeOpacity={1}>
<View
style={modalRepostStyle.container}
onLayout={() => {
if (this.tipAmountInput) {
this.tipAmountInput.focus();
}
}}
>
<Text style={modalRepostStyle.title} numberOfLines={1}>
{__('Repost %title%', { title })}
</Text>
<Text style={modalRepostStyle.infoText}>
{__('Repost your favorite content to help more people discover them!')}
</Text>
<Text style={modalRepostStyle.label}>{__('Channel to post on')}</Text>
<ChannelSelector
showAnonymous={false}
channelName={this.state.channelName}
onChannelChange={this.handleChannelChange}
/>
{this.state.showAdvanced && (
<View>
<Text style={modalRepostStyle.label}>{__('Name')}</Text>
<View style={modalRepostStyle.nameRow}>
<TextInput
editable={false}
value={this.state.channelName ? `lbry://${this.state.channelName}/` : ''}
style={modalRepostStyle.input}
/>
<TextInput
editable={canRepost}
value={this.state.repostName}
underlineColorAndroid={Colors.NextLbryGreen}
selectTextOnFocus
onChangeText={value => this.setState({ repostName: value })}
style={modalRepostStyle.input}
/>
</View>
<Text style={modalRepostStyle.label}>{__('Deposit')}</Text>
<View style={modalRepostStyle.row}>
<View style={modalRepostStyle.amountRow}>
<TextInput
editable={!this.state.repostStarted}
ref={ref => (this.depositAmountInput = ref)}
onChangeText={value => this.setState({ tipAmount: value })}
underlineColorAndroid={Colors.NextLbryGreen}
keyboardType={'numeric'}
onFocus={() => this.setState({ creditsInputFocused: true })}
onBlur={() => this.setState({ creditsInputFocused: false })}
placeholder={'0'}
value={this.state.depositAmount}
selectTextOnFocus
style={modalRepostStyle.depositAmountInput}
/>
<Text style={modalRepostStyle.currency}>LBC</Text>
<View style={modalRepostStyle.balance}>
{this.state.creditsInputFocused && <Icon name="coins" size={12} />}
{this.state.creditsInputFocused && (
<Text style={modalRepostStyle.balanceText}>{formatCredits(parseFloat(balance), 1, true)}</Text>
)}
</View>
</View>
</View>
</View>
)}
<View style={modalRepostStyle.buttonRow}>
{!processing && (
<Link
style={modalRepostStyle.cancelLink}
text={__('Cancel')}
onPress={() => {
if (onCancelPress) onCancelPress();
}}
/>
)}
{processing && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />}
<View style={modalRepostStyle.rightButtonRow}>
<Link
style={modalRepostStyle.advancedLink}
text={this.state.showAdvanced ? __('Hide advanced') : __('Show advanced')}
onPress={() => {
this.setState({ showAdvanced: !this.state.showAdvanced });
}}
/>
<Button
text={__('Repost')}
style={modalRepostStyle.button}
disabled={!canRepost || this.state.repostStarted || reposting}
onPress={this.handleRepost}
/>
</View>
</View>
</View>
</TouchableOpacity>
</TouchableOpacity>
);
}
}

View file

@ -1,9 +0,0 @@
import { connect } from 'react-redux';
import { selectFetchingClaimSearch } from 'lbry-redux';
import ModalSuggestedSubscriptions from './view';
const select = state => ({
loadingSuggested: selectFetchingClaimSearch(state),
});
export default connect(select)(ModalSuggestedSubscriptions);

View file

@ -1,29 +0,0 @@
import React from 'react';
import { ActivityIndicator, ScrollView, Text, TouchableOpacity, View } from 'react-native';
import modalStyle from 'styles/modal';
import subscriptionsStyle from 'styles/subscriptions';
import Button from 'component/button';
import Colors from 'styles/colors';
import SuggestedSubscriptionsGrid from 'component/suggestedSubscriptionsGrid';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import Icon from 'react-native-vector-icons/FontAwesome5';
export default class ModalSuggestedSubcriptions extends React.PureComponent {
render() {
const { loadingSuggested, navigation, onDonePress, onOverlayPress } = this.props;
return (
<TouchableOpacity style={modalStyle.overlay} activeOpacity={1} onPress={onOverlayPress}>
<TouchableOpacity style={[modalStyle.container, subscriptionsStyle.modalContainer]} activeOpacity={1}>
<SuggestedSubscriptionsGrid inModal navigation={navigation} />
<View style={modalStyle.wideButtons}>
<Button style={modalStyle.wideDoneButton} text={__('Done')} onPress={onDonePress} />
{loadingSuggested && (
<ActivityIndicator size="small" color={Colors.White} style={subscriptionsStyle.modalLoading} />
)}
</View>
</TouchableOpacity>
</TouchableOpacity>
);
}
}

View file

@ -8,8 +8,7 @@ import Icon from 'react-native-vector-icons/FontAwesome5';
import Tag from 'component/tag'; import Tag from 'component/tag';
import TagSearch from 'component/tagSearch'; import TagSearch from 'component/tagSearch';
import modalTagSelectorStyle from 'styles/modalTagSelector'; import modalTagSelectorStyle from 'styles/modalTagSelector';
import { __ } from 'utils/helper';
const minimumTags = 2;
export default class ModalTagSelector extends React.PureComponent { export default class ModalTagSelector extends React.PureComponent {
handleAddTag = tag => { handleAddTag = tag => {
@ -24,9 +23,6 @@ export default class ModalTagSelector extends React.PureComponent {
} }
this.props.doToggleTagFollow(tag); this.props.doToggleTagFollow(tag);
if (window.persistor) {
window.persistor.flush();
}
NativeModules.Firebase.track('tag_follow', { tag }); NativeModules.Firebase.track('tag_follow', { tag });
}; };
@ -35,16 +31,7 @@ export default class ModalTagSelector extends React.PureComponent {
return; return;
} }
const { followedTags, doToast } = this.props;
if (followedTags.length <= minimumTags) {
doToast({ message: __(`You can follow a minimum of ${minimumTags} tags.`) });
return;
}
this.props.doToggleTagFollow(tag); this.props.doToggleTagFollow(tag);
if (window.persistor) {
window.persistor.flush();
}
NativeModules.Firebase.track('tag_unfollow', { tag }); NativeModules.Firebase.track('tag_unfollow', { tag });
}; };
@ -54,9 +41,9 @@ export default class ModalTagSelector extends React.PureComponent {
return ( return (
<TouchableOpacity style={modalTagSelectorStyle.overlay} activeOpacity={1} onPress={onOverlayPress}> <TouchableOpacity style={modalTagSelectorStyle.overlay} activeOpacity={1} onPress={onOverlayPress}>
<TouchableOpacity style={modalTagSelectorStyle.container} activeOpacity={1}> <View style={modalTagSelectorStyle.container}>
<View style={modalTagSelectorStyle.titleRow}> <View style={modalTagSelectorStyle.titleRow}>
<Text style={modalTagSelectorStyle.title}>{__('Customize your tags')}</Text> <Text style={modalTagSelectorStyle.title}>Customize your tags</Text>
</View> </View>
<View style={modalTagSelectorStyle.tagList}> <View style={modalTagSelectorStyle.tagList}>
{tags && {tags &&
@ -72,9 +59,9 @@ export default class ModalTagSelector extends React.PureComponent {
</View> </View>
<TagSearch handleAddTag={this.handleAddTag} selectedTags={tags} /> <TagSearch handleAddTag={this.handleAddTag} selectedTags={tags} />
<View style={modalTagSelectorStyle.buttons}> <View style={modalTagSelectorStyle.buttons}>
<Button style={modalTagSelectorStyle.doneButton} text={__('Done')} onPress={onDonePress} /> <Button style={modalTagSelectorStyle.doneButton} text={'Done'} onPress={onDonePress} />
</View> </View>
</TouchableOpacity> </View>
</TouchableOpacity> </TouchableOpacity>
); );
} }

View file

@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { doSendTip, doToast, selectBalance } from 'lbry-redux';
import { selectSdkReady } from 'redux/selectors/settings';
import ModalTipView from './view';
const select = state => ({
balance: selectBalance(state),
sdkReady: selectSdkReady(state),
});
const perform = dispatch => ({
notify: data => dispatch(doToast(data)),
sendTip: (amount, claimId, isSupport, successCallback, errorCallback) =>
dispatch(doSendTip(amount, claimId, isSupport, successCallback, errorCallback)),
});
export default connect(select, perform)(ModalTipView);

View file

@ -1,157 +0,0 @@
import React from 'react';
import { ActivityIndicator, Alert, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { formatCredits } from 'lbry-redux';
import modalStyle from 'styles/modal';
import modalTipStyle from 'styles/modalTip';
import Button from 'component/button';
import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link';
export default class ModalTipView extends React.PureComponent {
tipAmountInput = null;
state = {
creditsInputFocused: false,
sendTipStarted: false,
tipAmount: null,
};
handleSendTip = () => {
const { claim, balance, notify, onSendTipFailed, onSendTipSuccessful, sdkReady, sendTip } = this.props;
const { tipAmount } = this.state;
if (!sdkReady) {
notify({
message: __(
'The background service is still initializing. You can still explore and watch content during the initialization process.',
),
});
return;
}
if (tipAmount > balance) {
notify({
message: 'Insufficient credits',
isError: true,
});
return;
}
const amount = parseInt(tipAmount, 10);
const message =
amount === 1
? __('Please confirm you want to tip %amount% credit', { amount })
: __('Please confirm you want to tip %amount% credits', { amount });
Alert.alert(
__('Send Tip'),
message,
[
{ text: __('No') },
{
text: __('Yes'),
onPress: () => {
this.setState({ sendTipStarted: true }, () =>
sendTip(
tipAmount,
claim.claim_id,
false,
() => {
// success
this.setState({ tipAmount: null, sendTipStarted: false });
if (onSendTipSuccessful) onSendTipSuccessful();
},
() => {
// error
if (onSendTipFailed) onSendTipFailed();
},
),
);
},
},
],
{ cancelable: true },
);
};
render() {
const { balance, channelName, contentName, onCancelPress, onOverlayPress } = this.props;
const canSendTip = this.state.tipAmount > 0;
return (
<TouchableOpacity style={modalStyle.overlay} activeOpacity={1} onPress={onOverlayPress}>
<TouchableOpacity style={modalStyle.container} activeOpacity={1}>
<View
style={modalTipStyle.container}
onLayout={() => {
if (this.tipAmountInput) {
this.tipAmountInput.focus();
}
}}
>
<Text style={modalTipStyle.title} numberOfLines={1}>
{channelName ? __('Send a tip to %channel%', { channel: channelName }) : __('Send a tip')}
</Text>
<View style={modalTipStyle.row}>
<View style={modalTipStyle.amountRow}>
<TextInput
editable={!this.state.sendTipStarted}
ref={ref => (this.tipAmountInput = ref)}
onChangeText={value => this.setState({ tipAmount: value })}
underlineColorAndroid={Colors.NextLbryGreen}
keyboardType={'numeric'}
onFocus={() => this.setState({ creditsInputFocused: true })}
onBlur={() => this.setState({ creditsInputFocused: false })}
placeholder={'0'}
value={this.state.tipAmount}
selectTextOnFocus
style={modalTipStyle.tipAmountInput}
/>
<Text style={modalTipStyle.currency}>LBC</Text>
<View style={modalTipStyle.balance}>
{this.state.creditsInputFocused && <Icon name="coins" size={12} />}
{this.state.creditsInputFocused && (
<Text style={modalTipStyle.balanceText}>{formatCredits(parseFloat(balance), 1, true)}</Text>
)}
</View>
</View>
{this.state.sendTipStarted && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />}
</View>
<View style={modalTipStyle.info}>
<Text style={modalTipStyle.infoText}>
{__(
'This will appear as a tip for %content%, which will boost its ability to be discovered while active.',
{ content: contentName },
)}{' '}
<Link
style={modalTipStyle.learnMoreLink}
text={__('Learn more.')}
href={'https://lbry.com/faq/tipping'}
/>
</Text>
</View>
<View style={modalTipStyle.buttonRow}>
<Link
style={modalTipStyle.cancelTipLink}
text={__('Cancel')}
onPress={() => {
if (onCancelPress) onCancelPress();
}}
/>
<Button
text={__('Send')}
style={modalTipStyle.button}
disabled={!canSendTip || this.state.sendTipStarted}
onPress={this.handleSendTip}
/>
</View>
</View>
</TouchableOpacity>
</TouchableOpacity>
);
}
}

View file

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

View file

@ -4,21 +4,17 @@ import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5'; import Icon from 'react-native-vector-icons/FontAwesome5';
import publishStyle from 'styles/publish'; import publishStyle from 'styles/publish';
class PublishRewardsDriver extends React.PureComponent<Props> { class PublishRewadsDriver extends React.PureComponent<Props> {
render() { render() {
const { navigation } = this.props; const { navigation } = this.props;
return ( return (
<TouchableOpacity style={publishStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}> <TouchableOpacity style={publishStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}>
<Icon name="award" size={16} style={publishStyle.rewardIcon} /> <Icon name="award" size={16} style={publishStyle.rewardIcon} />
<Text style={publishStyle.rewardDriverText}> <Text style={publishStyle.rewardDriverText}>Earn some credits to be able to publish your content.</Text>
{__('Publishing requires credits.')}
{'\n'}
{__('Tap here to get some for free.')}
</Text>
</TouchableOpacity> </TouchableOpacity>
); );
} }
} }
export default PublishRewardsDriver; export default PublishRewadsDriver;

View file

@ -1,35 +1,25 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
doResolveUris,
doResolvedSearch,
makeSelectClaimForUri, makeSelectClaimForUri,
makeSelectResolvedRecommendedContentForUri, makeSelectRecommendedContentForUri,
selectResolvingUris, makeSelectTitleForUri,
selectIsSearching, selectIsSearching,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectShowNsfw } from 'redux/selectors/settings'; import { doNativeSearch } from 'redux/actions/performance';
import RelatedContent from './view'; import RelatedContent from './view';
const RESULT_SIZE = 16;
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
recommendedContent: makeSelectRecommendedContentForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state),
isSearching: selectIsSearching(state), isSearching: selectIsSearching(state),
recommendedContent: makeSelectResolvedRecommendedContentForUri(
props.uri,
RESULT_SIZE,
props.claimId,
props.claimName,
props.title,
)(state),
resolvingUris: selectResolvingUris(state),
showNsfwContent: selectShowNsfw(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
resolveUris: uris => dispatch(doResolveUris(uris)), search: query => dispatch(doNativeSearch(query, 20, undefined, true)),
searchRecommended: (query, claimId, nsfw) =>
dispatch(doResolvedSearch(query, RESULT_SIZE, undefined, true, { related_to: claimId }, nsfw)),
}); });
export default connect(select, perform)(RelatedContent); export default connect(
select,
perform
)(RelatedContent);

View file

@ -1,45 +1,68 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator, FlatList, Text, View } from 'react-native'; import { ActivityIndicator, FlatList, Text, View } from 'react-native';
import { normalizeURI } from 'lbry-redux';
import { navigateToUri } from 'utils/helper'; import { navigateToUri } from 'utils/helper';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
import FileListItem from 'component/fileListItem'; import FileListItem from 'component/fileListItem';
import ClaimResultItem from 'component/claimResultItem';
import fileListStyle from 'styles/fileList'; import fileListStyle from 'styles/fileList';
import relatedContentStyle from 'styles/relatedContent'; import relatedContentStyle from 'styles/relatedContent';
export default class RelatedContent extends React.PureComponent { export default class RelatedContent extends React.PureComponent {
constructor() {
super();
this.didSearch = undefined;
}
componentDidMount() { componentDidMount() {
const { title, claimId, searchRecommended, showNsfwContent } = this.props; this.getRecommendedContent();
if (title && claimId) { }
searchRecommended(title, claimId, showNsfwContent);
componentDidUpdate(prevProps) {
const { claim, uri } = this.props;
if (uri !== prevProps.uri) {
this.didSearch = false;
}
if (claim && !this.didSearch) {
this.getRecommendedContent();
} }
} }
shouldComponentUpdate(nextProps, nextState) { getRecommendedContent() {
const { isSearching, recommendedContent } = nextProps; const { search, title } = this.props;
return isSearching || (!isSearching && recommendedContent && recommendedContent.length > 0);
if (title) {
search(title);
this.didSearch = true;
}
} }
didSearch;
render() { render() {
const { isSearching, recommendedContent, navigation, urlOpenHandler, uri, fullUri } = this.props; const { recommendedContent, isSearching, navigation } = this.props;
if (!isSearching && (!recommendedContent || recommendedContent.length === 0)) {
return null;
}
return ( return (
<View style={relatedContentStyle.container}> <View style={relatedContentStyle.container}>
<Text style={relatedContentStyle.title}>{__('Related Content')}</Text> <Text style={relatedContentStyle.title}>Related Content</Text>
{isSearching && <ActivityIndicator size={'small'} color={Colors.NextLbryGreen} />}
{recommendedContent && {recommendedContent &&
recommendedContent.map(result => ( recommendedContent.map(recommendedUri => (
<ClaimResultItem <FileListItem
style={fileListStyle.item} style={fileListStyle.item}
uri={result ? normalizeURI(`${result.name}#${result.claimId}`) : null} key={recommendedUri}
urlOpenHandler={urlOpenHandler} uri={recommendedUri}
key={result.claimId}
result={result}
navigation={navigation} navigation={navigation}
autoplay onPress={() => navigateToUri(navigation, recommendedUri, { autoplay: true })}
/> />
))} ))}
{isSearching && (
<ActivityIndicator size="small" color={Colors.NextLbryGreen} style={relatedContentStyle.loading} />
)}
</View> </View>
); );
} }

View file

@ -1,11 +1,10 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native'; import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
import { formatUsd } from 'utils/helper'; import Colors from '../../styles/colors';
import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5'; import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link'; import Link from '../link';
import rewardStyle from 'styles/reward'; import rewardStyle from '../../styles/reward';
type Props = { type Props = {
canClaim: boolean, canClaim: boolean,
@ -14,7 +13,6 @@ type Props = {
id: string, id: string,
reward_title: string, reward_title: string,
reward_amount: number, reward_amount: number,
reward_range?: string,
transaction_id: string, transaction_id: string,
created_at: string, created_at: string,
reward_description: string, reward_description: string,
@ -35,7 +33,7 @@ class RewardCard extends React.PureComponent<Props> {
notify({ message: errorMessage }); notify({ message: errorMessage });
clearError(reward); clearError(reward);
} else { } else {
notify({ message: __('Reward successfully claimed!') }); notify({ message: 'Reward successfully claimed!' });
} }
this.setState({ claimStarted: false }); this.setState({ claimStarted: false });
} }
@ -48,7 +46,7 @@ class RewardCard extends React.PureComponent<Props> {
if (showVerification) { if (showVerification) {
showVerification(); showVerification();
} }
notify({ message: __('Unfortunately, you are not eligible to claim this reward at this time.') }); notify({ message: 'Unfortunately, you are not eligible to claim this reward at this time.' });
return; return;
} }
@ -57,23 +55,8 @@ class RewardCard extends React.PureComponent<Props> {
}); });
}; };
getDisplayAmount = () => {
const { reward } = this.props;
if (reward) {
const claimed = !!reward.transaction_id;
if (!claimed && reward.reward_range && reward.reward_range.includes('-')) {
return reward.reward_range.split('-')[1];
} else if (reward.reward_amount > 0) {
return reward.reward_amount;
}
}
// unknown amount which normally shouldn't happen
return '?';
};
render() { render() {
const { canClaim, isPending, onClaimPress, reward, usdExchangeRate } = this.props; const { canClaim, isPending, onClaimPress, reward } = this.props;
const claimed = !!reward.transaction_id; const claimed = !!reward.transaction_id;
return ( return (
@ -113,21 +96,13 @@ class RewardCard extends React.PureComponent<Props> {
style={rewardStyle.link} style={rewardStyle.link}
href={`https://explorer.lbry.com/tx/${reward.transaction_id}`} href={`https://explorer.lbry.com/tx/${reward.transaction_id}`}
text={reward.transaction_id.substring(0, 7)} text={reward.transaction_id.substring(0, 7)}
error={__('The transaction URL could not be opened')} error={'The transaction URL could not be opened'}
/> />
)} )}
</View> </View>
<View style={rewardStyle.rightCol}> <View style={rewardStyle.rightCol}>
{reward.reward_range && reward.reward_range.indexOf('-') > -1 && ( <Text style={rewardStyle.rewardAmount}>{reward.reward_amount}</Text>
<Text style={rewardStyle.rightColHeader}>{__('up to')}</Text>
)}
<Text style={rewardStyle.rewardAmount}>{this.getDisplayAmount()}</Text>
<Text style={rewardStyle.rewardCurrency}>LBC</Text> <Text style={rewardStyle.rewardCurrency}>LBC</Text>
{usdExchangeRate > 0 && (
<Text style={rewardStyle.rewardUsd}>
&asymp;{formatUsd(parseFloat(this.getDisplayAmount()) * parseFloat(usdExchangeRate))}
</Text>
)}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
); );

View file

@ -1,13 +1,12 @@
import React from 'react'; import React from 'react';
import { Linking, NativeModules, Text, TouchableOpacity, View } from 'react-native'; import { NativeModules, Text, TouchableOpacity, View } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
import Button from 'component/button'; import Button from 'component/button';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api import Constants from 'constants';
import Link from 'component/link'; import Link from 'component/link';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
import Icon from 'react-native-vector-icons/FontAwesome5'; import Icon from 'react-native-vector-icons/FontAwesome5';
import rewardStyle from 'styles/reward'; import rewardStyle from 'styles/reward';
import { formatUsd } from '../../utils/helper';
class RewardEnrolment extends React.Component { class RewardEnrolment extends React.Component {
componentDidMount() { componentDidMount() {
@ -25,47 +24,26 @@ class RewardEnrolment extends React.Component {
navigation.navigate({ routeName: 'Verification', key: 'verification', params: { syncFlow: false } }); navigation.navigate({ routeName: 'Verification', key: 'verification', params: { syncFlow: false } });
}; };
onLearnMorePressed = () => {
Linking.openURL('https://lbry.com/faq/earn-credits');
};
render() { render() {
const { unclaimedRewardAmount, usdExchangeRate } = this.props; const { fetching, navigation, unclaimedRewardAmount, user } = this.props;
return ( return (
<View style={rewardStyle.enrollContainer}> <View style={rewardStyle.enrollContainer} onPress>
<View style={rewardStyle.summaryRow}> <View style={rewardStyle.summaryRow}>
<Icon name="award" size={36} color={Colors.White} /> <Icon name="award" size={36} color={Colors.White} />
<Text style={rewardStyle.summaryText}> <Text style={rewardStyle.summaryText}>{unclaimedRewardAmount} unclaimed credits</Text>
{unclaimedRewardAmount === 1 && __('%amount% available credit', { amount: unclaimedRewardAmount })}
{unclaimedRewardAmount !== 1 && __('%amount% available credits', { amount: unclaimedRewardAmount })}
</Text>
</View> </View>
<View style={rewardStyle.onboarding}> <View style={rewardStyle.onboarding}>
<Text style={rewardStyle.enrollDescText}> <Text style={rewardStyle.enrollDescText}>
{__('LBRY credits allow you to publish or purchase content.')} LBRY credits allow you to purchase content, publish content, and influence the network. You can start
{'\n\n'} earning credits by watching videos on LBRY.
{__('You can obtain free credits worth %amount% after you provide an email address.', {
amount: formatUsd(parseFloat(unclaimedRewardAmount) * parseFloat(usdExchangeRate)),
})}
{'\n\n'}
<Link style={rewardStyle.learnMoreLink} text={__('Learn more')} onPress={this.onLearnMorePressed} />.
</Text> </Text>
</View> </View>
<View style={rewardStyle.buttonRow}> <View style={rewardStyle.buttonRow}>
<Link <Link style={rewardStyle.notInterestedLink} text={'Not interested'} onPress={this.onNotInterestedPressed} />
style={rewardStyle.notInterestedLink} <Button style={rewardStyle.enrollButton} theme={'light'} text={'Enroll'} onPress={this.onEnrollPressed} />
text={__('Not interested')}
onPress={this.onNotInterestedPressed}
/>
<Button
style={rewardStyle.enrollButton}
theme={'light'}
text={__('Get started')}
onPress={this.onEnrollPressed}
/>
</View> </View>
</View> </View>
); );

View file

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

View file

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

View file

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

View file

@ -1,15 +0,0 @@
import { ActivityIndicator, Text, View } from 'react-native';
import React from 'react';
import discoverStyle from 'styles/discover';
import Colors from 'styles/colors';
export default class SdkLoadingStatus extends React.PureComponent {
render() {
return (
<View style={discoverStyle.sdkLoading}>
<ActivityIndicator color={Colors.White} size={'small'} />
<Text style={discoverStyle.sdkLoadingText}>{__('The LBRY background service is initializing...')}</Text>
</View>
);
}
}

View file

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

View file

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

View file

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { normalizeURI, parseURI } from 'lbry-redux'; import { normalizeURI, parseURI } from 'lbry-redux';
import { ActivityIndicator, Platform, Switch, Text, TouchableOpacity, View } from 'react-native'; import { ActivityIndicator, Platform, Switch, Text, TouchableOpacity, View } from 'react-native';
import { formatBytes } from 'utils/helper'; import { formatBytes } from '../../utils/helper';
import Colors from 'styles/colors'; import Colors from '../../styles/colors';
import storageStatsStyle from 'styles/storageStats'; import storageStatsStyle from '../../styles/storageStats';
class StorageStatsCard extends React.PureComponent { class StorageStatsCard extends React.PureComponent {
state = { state = {
@ -65,8 +65,7 @@ class StorageStatsCard extends React.PureComponent {
} }
render() { render() {
const { fileInfos } = this.props; if (this.state.totalBytes == 0) {
if (fileInfos.length === 0 || this.state.totalBytes === 0) {
return null; return null;
} }
@ -75,10 +74,10 @@ class StorageStatsCard extends React.PureComponent {
<View style={[storageStatsStyle.row, storageStatsStyle.totalSizeContainer]}> <View style={[storageStatsStyle.row, storageStatsStyle.totalSizeContainer]}>
<View style={storageStatsStyle.summary}> <View style={storageStatsStyle.summary}>
<Text style={storageStatsStyle.totalSize}>{formatBytes(this.state.totalBytes, 2)}</Text> <Text style={storageStatsStyle.totalSize}>{formatBytes(this.state.totalBytes, 2)}</Text>
<Text style={storageStatsStyle.annotation}>{__('used')}</Text> <Text style={storageStatsStyle.annotation}>used</Text>
</View> </View>
<View style={[storageStatsStyle.row, storageStatsStyle.toggleStatsContainer]}> <View style={[storageStatsStyle.row, storageStatsStyle.toggleStatsContainer]}>
<Text style={storageStatsStyle.statsText}>{__('Stats')}</Text> <Text style={storageStatsStyle.statsText}>Stats</Text>
<Switch <Switch
style={storageStatsStyle.statsToggle} style={storageStatsStyle.statsToggle}
value={this.state.showStats} value={this.state.showStats}
@ -98,28 +97,28 @@ class StorageStatsCard extends React.PureComponent {
{this.state.totalAudioBytes > 0 && ( {this.state.totalAudioBytes > 0 && (
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}> <View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
<View style={[storageStatsStyle.legendBox, storageStatsStyle.audioDistribution]} /> <View style={[storageStatsStyle.legendBox, storageStatsStyle.audioDistribution]} />
<Text style={storageStatsStyle.legendText}>{__('Audio')}</Text> <Text style={storageStatsStyle.legendText}>Audio</Text>
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalAudioBytes, 2)}</Text> <Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalAudioBytes, 2)}</Text>
</View> </View>
)} )}
{this.state.totalImageBytes > 0 && ( {this.state.totalImageBytes > 0 && (
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}> <View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
<View style={[storageStatsStyle.legendBox, storageStatsStyle.imageDistribution]} /> <View style={[storageStatsStyle.legendBox, storageStatsStyle.imageDistribution]} />
<Text style={storageStatsStyle.legendText}>{__('Images')}</Text> <Text style={storageStatsStyle.legendText}>Images</Text>
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalImageBytes, 2)}</Text> <Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalImageBytes, 2)}</Text>
</View> </View>
)} )}
{this.state.totalVideoBytes > 0 && ( {this.state.totalVideoBytes > 0 && (
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}> <View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
<View style={[storageStatsStyle.legendBox, storageStatsStyle.videoDistribution]} /> <View style={[storageStatsStyle.legendBox, storageStatsStyle.videoDistribution]} />
<Text style={storageStatsStyle.legendText}>{__('Videos')}</Text> <Text style={storageStatsStyle.legendText}>Videos</Text>
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalVideoBytes, 2)}</Text> <Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalVideoBytes, 2)}</Text>
</View> </View>
)} )}
{this.state.totalOtherBytes > 0 && ( {this.state.totalOtherBytes > 0 && (
<View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}> <View style={[storageStatsStyle.row, storageStatsStyle.legendItem]}>
<View style={[storageStatsStyle.legendBox, storageStatsStyle.otherDistribution]} /> <View style={[storageStatsStyle.legendBox, storageStatsStyle.otherDistribution]} />
<Text style={storageStatsStyle.legendText}>{__('Other')}</Text> <Text style={storageStatsStyle.legendText}>Other</Text>
<Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalOtherBytes, 2)}</Text> <Text style={storageStatsStyle.legendSize}>{formatBytes(this.state.totalOtherBytes, 2)}</Text>
</View> </View>
)} )}

View file

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

View file

@ -1,36 +0,0 @@
import React from 'react';
import { normalizeURI, parseURI } from 'lbry-redux';
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Button from 'component/button';
import Colors from 'styles/colors';
class SubscribeButtonOverlay extends React.PureComponent {
handlePress = () => {
const { claim, isSubscribed, doChannelSubscribe, doChannelUnsubscribe, uri } = this.props;
if (!claim) {
return;
}
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
const { name: claimName } = claim;
subscriptionHandler({
channelName: claimName,
uri: normalizeURI(uri),
});
};
render() {
const { uri, isSubscribed, style } = this.props;
let styles = style.length ? style : [style];
return (
<TouchableOpacity style={styles} opacity={0.7} onPress={this.handlePress}>
{isSubscribed && <Icon name={'heart'} size={20} solid color={Colors.Red} />}
{!isSubscribed && <Icon name={'heart'} size={20} color={Colors.Red} />}
</TouchableOpacity>
);
}
}
export default SubscribeButtonOverlay;

View file

@ -5,27 +5,17 @@ import Button from 'component/button';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
class SubscribeNotificationButton extends React.PureComponent { class SubscribeNotificationButton extends React.PureComponent {
handlePress = () => { render() {
const { const {
uri,
name, name,
doChannelSubscriptionEnableNotifications, doChannelSubscriptionEnableNotifications,
doChannelSubscriptionDisableNotifications, doChannelSubscriptionDisableNotifications,
doToast, doToast,
enabledChannelNotifications, enabledChannelNotifications,
isSubscribed,
style,
} = this.props; } = this.props;
const shouldNotify = enabledChannelNotifications.indexOf(name) > -1;
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.' });
}
};
render() {
const { enabledChannelNotifications, name, uri, isSubscribed, style } = this.props;
if (!isSubscribed) { if (!isSubscribed) {
return null; return null;
@ -41,14 +31,23 @@ class SubscribeNotificationButton extends React.PureComponent {
} }
const shouldNotify = enabledChannelNotifications.indexOf(name) > -1; const shouldNotify = enabledChannelNotifications.indexOf(name) > -1;
const { claimName } = parseURI(uri);
return ( return (
<Button <Button
style={styles} style={styles}
theme={'light'} theme={'light'}
icon={shouldNotify ? 'bell-slash' : 'bell'} icon={shouldNotify ? 'bell-slash' : 'bell'}
solid solid={true}
onPress={this.handlePress} 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.' });
}
}}
/> />
); );
} }

View file

@ -6,7 +6,6 @@ import {
makeSelectTitleForUri, makeSelectTitleForUri,
makeSelectIsUriResolving, makeSelectIsUriResolving,
} from 'lbry-redux'; } from 'lbry-redux';
import { doChannelSubscribe, doChannelUnsubscribe, makeSelectIsSubscribed } from 'lbryinc';
import SuggestedSubscriptionItem from './view'; import SuggestedSubscriptionItem from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -14,13 +13,13 @@ const select = (state, props) => ({
title: makeSelectTitleForUri(props.uri)(state), title: makeSelectTitleForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state), isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)), resolveUri: uri => dispatch(doResolveUri(uri)),
subscribe: subscription => doChannelSubscribe(subscription),
unsubscribe: subscription => doChannelUnsubscribe(subscription),
}); });
export default connect(select, perform)(SuggestedSubscriptionItem); export default connect(
select,
perform
)(SuggestedSubscriptionItem);

View file

@ -1,99 +1,67 @@
import React from 'react'; import React from 'react';
import { buildURI, normalizeURI } from 'lbry-redux'; import { buildURI, normalizeURI } from 'lbry-redux';
import { ActivityIndicator, FlatList, Image, Text, TouchableOpacity, View } from 'react-native'; import { ActivityIndicator, FlatList, Image, Text, View } from 'react-native';
import { navigateToUri } from 'utils/helper';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
import ChannelIconItem from 'component/channelIconItem';
import channelIconStyle from 'styles/channelIcon';
import discoverStyle from 'styles/discover'; import discoverStyle from 'styles/discover';
import FileItem from 'component/fileItem'; import FileItem from 'component/fileItem';
import SubscribeButtonOverlay from 'component/subscribeButtonOverlay'; import SubscribeButton from 'component/subscribeButton';
import subscriptionsStyle from 'styles/subscriptions'; import subscriptionsStyle from 'styles/subscriptions';
import Link from 'component/link';
import Tag from 'component/tag'; import Tag from 'component/tag';
class SuggestedSubscriptionItem extends React.PureComponent { class SuggestedSubscriptionItem extends React.PureComponent {
state = {
autoStyle: null,
};
componentDidMount() { componentDidMount() {
const { claim, uri, resolveUri } = this.props; const { claim, uri, resolveUri } = this.props;
if (!claim) { if (!claim) {
resolveUri(uri); resolveUri(uri);
} }
this.setState({
autoStyle:
ChannelIconItem.AUTO_THUMB_STYLES[Math.floor(Math.random() * ChannelIconItem.AUTO_THUMB_STYLES.length)],
});
} }
render() { render() {
const { claim, isResolvingUri, navigation, thumbnail, title, uri } = this.props; const { claim, isResolvingUri, navigation, thumbnail, title, uri } = this.props;
let tags;
let shortUrl, tags; if (claim && claim.value) {
if (claim) { tags = claim.value.tags;
shortUrl = claim.short_url;
if (claim.value) {
tags = claim.value.tags;
}
} }
const hasThumbnail = !!thumbnail;
if (isResolvingUri) { if (isResolvingUri) {
return ( return (
<View style={subscriptionsStyle.itemLoadingContainer}> <View style={subscriptionsStyle.itemLoadingContainer}>
<ActivityIndicator size={'small'} color={Colors.NextLbryGreen} /> <ActivityIndicator size={'small'} color={Colors.LbryGreen} />
</View> </View>
); );
} }
return ( return (
<TouchableOpacity style={subscriptionsStyle.suggestedItem}> <View style={subscriptionsStyle.suggestedItem}>
<View style={[subscriptionsStyle.suggestedItemThumbnailContainer, this.state.autoStyle]}> <View style={subscriptionsStyle.suggestedItemThumbnailContainer}>
{hasThumbnail && ( <Image
<Image style={subscriptionsStyle.suggestedItemThumbnail} resizeMode={'cover'} source={{ uri: thumbnail }} /> style={subscriptionsStyle.suggestedItemThumbnail}
)} resizeMode={'cover'}
{!hasThumbnail && ( source={thumbnail ? { uri: thumbnail } : require('../../assets/default_avatar.jpg')}
<Text style={channelIconStyle.autothumbCharacter}> />
{title ? title.substring(0, 1).toUpperCase() : claim ? claim.name.substring(1, 2).toUpperCase() : ''}
</Text>
)}
</View> </View>
<View style={subscriptionsStyle.suggestedItemDetails}> <View style={subscriptionsStyle.suggestedItemDetails}>
<Text style={subscriptionsStyle.suggestedItemTitle} numberOfLines={2}> {title && (
{title || claim.name} <Text style={subscriptionsStyle.suggestedItemTitle} numberOfLines={1}>
{title}
</Text>
)}
<Text style={subscriptionsStyle.suggestedItemName} numberOfLines={1}>
{claim && claim.name}
</Text> </Text>
{tags && ( {tags && (
<View style={subscriptionsStyle.suggestedItemTagList}> <View style={subscriptionsStyle.suggestedItemTagList}>
{tags && {tags &&
tags tags
.slice(0, 1) .slice(0, 3)
.map(tag => ( .map(tag => <Tag style={subscriptionsStyle.tag} key={tag} name={tag} navigation={navigation} />)}
<Tag
numberOfLines={1}
onPress={this.handleItemPress}
style={subscriptionsStyle.tag}
key={tag}
name={tag}
navigation={navigation}
truncate
/>
))}
</View> </View>
)} )}
</View> </View>
{claim && ( <SubscribeButton style={subscriptionsStyle.suggestedItemSubscribe} uri={normalizeURI(uri)} />
<SubscribeButtonOverlay </View>
claim={claim}
style={subscriptionsStyle.suggestedItemSubscribeOverlay}
uri={normalizeURI(claim.permanent_url)}
/>
)}
</TouchableOpacity>
); );
} }
} }

View file

@ -1,19 +1,20 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doClaimSearch, selectFetchingClaimSearch, selectClaimSearchByQuery, selectFollowedTags } from 'lbry-redux'; import { doClaimSearch, selectFetchingClaimSearch, selectLastClaimSearchUris, selectFollowedTags } from 'lbry-redux';
import { selectSuggestedChannels, selectIsFetchingSuggested } from 'lbryinc'; import { selectSuggestedChannels, selectIsFetchingSuggested } from 'lbryinc';
import { selectShowNsfw } from 'redux/selectors/settings';
import SuggestedSubscriptions from './view'; import SuggestedSubscriptions from './view';
const select = state => ({ const select = state => ({
followedTags: selectFollowedTags(state), followedTags: selectFollowedTags(state),
suggested: selectSuggestedChannels(state), suggested: selectSuggestedChannels(state),
loading: selectIsFetchingSuggested(state) || selectFetchingClaimSearch(state), loading: selectIsFetchingSuggested(state) || selectFetchingClaimSearch(state),
claimSearchByQuery: selectClaimSearchByQuery(state), claimSearchUris: selectLastClaimSearchUris(state),
showNsfwContent: selectShowNsfw(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
claimSearch: options => dispatch(doClaimSearch(options)), claimSearch: options => dispatch(doClaimSearch(10, options)),
}); });
export default connect(select, perform)(SuggestedSubscriptions); export default connect(
select,
perform
)(SuggestedSubscriptions);

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator, SectionList, Text, View } from 'react-native'; import { ActivityIndicator, FlatList, SectionList, Text, View } from 'react-native';
import { MATURE_TAGS, createNormalizedClaimSearchKey, normalizeURI } from 'lbry-redux'; import { normalizeURI } from 'lbry-redux';
import { navigateToUri } from 'utils/helper'; import { __, navigateToUri } from 'utils/helper';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import SuggestedSubscriptionItem from 'component/suggestedSubscriptionItem'; import SuggestedSubscriptionItem from 'component/suggestedSubscriptionItem';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
@ -11,45 +11,34 @@ import Link from 'component/link';
import _ from 'lodash'; import _ from 'lodash';
class SuggestedSubscriptions extends React.PureComponent { class SuggestedSubscriptions extends React.PureComponent {
state = {
options: {},
};
componentDidMount() { componentDidMount() {
const { claimSearch, followedTags, showNsfwContent } = this.props; const { claimSearch, followedTags } = this.props;
const options = { const options = {
any_tags: _.shuffle(followedTags.map(tag => tag.name)).slice(0, 3), any_tags: _.shuffle(followedTags.map(tag => tag.name)).slice(0, 2),
page: 1, page: 1,
no_totals: true, no_totals: true,
claim_type: 'channel', claim_type: 'channel',
}; };
if (!showNsfwContent) {
options.not_tags = MATURE_TAGS;
}
this.setState({ options });
claimSearch(options); claimSearch(options);
} }
buildSections = () => { buildSections = () => {
const { suggested, claimSearchByQuery } = this.props; const { suggested, claimSearchUris } = this.props;
const claimSearchKey = createNormalizedClaimSearchKey(this.state.options);
const claimSearchUris = claimSearchByQuery[claimSearchKey];
const suggestedUris = suggested ? suggested.map(suggested => suggested.uri) : []; const suggestedUris = suggested ? suggested.map(suggested => suggested.uri) : [];
return [ return [
{ {
title: __('Suggested channels'), title: __('You might like'),
data: claimSearchUris ? claimSearchUris.filter(uri => !suggestedUris.includes(uri)) : [], data: suggestedUris,
}, },
{ {
title: __('You might also like'), title: __('Tags you follow'),
data: _.shuffle(suggestedUris), data: claimSearchUris ? claimSearchUris.filter(uri => !suggestedUris.includes(uri)) : [],
}, },
]; ];
}; };
render() { render() {
const { suggested, inModal, loading, navigation } = this.props; const { suggested, loading, navigation } = this.props;
if (loading) { if (loading) {
return ( return (
@ -61,10 +50,8 @@ class SuggestedSubscriptions extends React.PureComponent {
return ( return (
<SectionList <SectionList
style={inModal ? subscriptionsStyle.modalScrollContainer : subscriptionsStyle.scrollContainer} style={subscriptionsStyle.scrollContainer}
contentContainerStyle={ contentContainerStyle={subscriptionsStyle.suggestedScrollPadding}
inModal ? subscriptionsStyle.modalSuggestedScrollContent : subscriptionsStyle.suggestedScrollContent
}
renderItem={({ item, index, section }) => ( renderItem={({ item, index, section }) => (
<SuggestedSubscriptionItem key={item} uri={normalizeURI(item)} navigation={navigation} /> <SuggestedSubscriptionItem key={item} uri={normalizeURI(item)} navigation={navigation} />
)} )}

View file

@ -1,27 +0,0 @@
import { connect } from 'react-redux';
import {
doClaimSearch,
selectFetchingClaimSearch,
selectClaimSearchByQuery,
selectClaimSearchByQueryLastPageReached,
selectFollowedTags,
} from 'lbry-redux';
import { selectSubscriptions, selectSuggestedChannels, selectIsFetchingSuggested } from 'lbryinc';
import { selectShowNsfw } from 'redux/selectors/settings';
import SuggestedSubscriptionsGrid from './view';
const select = state => ({
followedTags: selectFollowedTags(state),
subscriptions: selectSubscriptions(state),
suggested: selectSuggestedChannels(state),
loading: selectIsFetchingSuggested(state) || selectFetchingClaimSearch(state),
claimSearchByQuery: selectClaimSearchByQuery(state),
lastPageReached: selectClaimSearchByQueryLastPageReached(state),
showNsfwContent: selectShowNsfw(state),
});
const perform = dispatch => ({
claimSearch: options => dispatch(doClaimSearch(options)),
});
export default connect(select, perform)(SuggestedSubscriptionsGrid);

View file

@ -1,111 +0,0 @@
import React from 'react';
import { ActivityIndicator, SectionList, Text, View } from 'react-native';
import { MATURE_TAGS, createNormalizedClaimSearchKey, normalizeURI } from 'lbry-redux';
import { navigateToUri } from 'utils/helper';
import { FlatGrid } from 'react-native-super-grid';
import SubscribeButton from 'component/subscribeButton';
import SuggestedSubscriptionItem from 'component/suggestedSubscriptionItem';
import Colors from 'styles/colors';
import Constants from 'constants'; // eslint-disable-line node/no-deprecated-api
import discoverStyle from 'styles/discover';
import subscriptionsStyle from 'styles/subscriptions';
import Link from 'component/link';
import _ from 'lodash';
const suggestedPageSize = 24;
const softLimit = 2400;
class SuggestedSubscriptionsGrid extends React.PureComponent {
state = {
currentPage: 1,
options: {},
// maintain a local state of subscriptions so that changes don't affect the search
subscriptionIds: [],
};
buildClaimSearchOptions() {
const { showNsfwContent } = this.props;
const { currentPage } = this.state;
const options = {
no_totals: true,
page: currentPage,
page_size: suggestedPageSize,
claim_type: 'channel',
order_by: [Constants.ORDER_BY_EFFECTIVE_AMOUNT],
};
if (!showNsfwContent) {
options.not_tags = MATURE_TAGS;
}
if (this.state.subscriptionIds.length > 0) {
options.not_channel_ids = this.state.subscriptionIds;
}
return options;
}
doClaimSearch() {
const { claimSearch } = this.props;
const options = this.buildClaimSearchOptions();
claimSearch(options);
}
handleVerticalEndReached = () => {
// fetch more content
const { claimSearchByQuery, lastPageReached } = this.props;
const options = this.buildClaimSearchOptions();
const claimSearchKey = createNormalizedClaimSearchKey(options);
const uris = claimSearchByQuery[claimSearchKey];
if (
lastPageReached[claimSearchKey] ||
(uris.length > 0 && uris.length < suggestedPageSize) || uris.length >= softLimit
) {
return;
}
this.setState({ currentPage: this.state.currentPage + 1 }, () => this.doClaimSearch());
};
componentDidMount() {
const { claimSearch, followedTags, showNsfwContent, subscriptions } = this.props;
if (subscriptions && subscriptions.length > 0) {
this.setState(
{
subscriptionIds: subscriptions.map(subscription => subscription.uri.split('#')[1]),
},
() => this.doClaimSearch(),
);
} else {
this.doClaimSearch();
}
}
render() {
const { claimSearchByQuery, inModal, navigation } = this.props;
const options = this.buildClaimSearchOptions();
const claimSearchKey = createNormalizedClaimSearchKey(options);
const claimSearchUris = claimSearchByQuery[claimSearchKey];
return (
<FlatGrid
initialNumToRender={24}
maxToRenderPerBatch={48}
removeClippedSubviews
itemDimension={120}
spacing={1}
items={claimSearchUris}
style={inModal ? subscriptionsStyle.modalScrollContainer : subscriptionsStyle.scrollContainer}
contentContainerStyle={
inModal ? subscriptionsStyle.modalSuggestedScrollContent : subscriptionsStyle.suggestedScrollContent
}
renderItem={({ item, index }) => (
<SuggestedSubscriptionItem key={item} uri={normalizeURI(item)} navigation={navigation} />
)}
onEndReached={this.handleVerticalEndReached}
onEndReachedThreshold={0.2}
/>
);
}
}
export default SuggestedSubscriptionsGrid;

View file

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native'; import { Text, TouchableOpacity, View } from 'react-native';
import { MATURE_TAGS } from 'lbry-redux';
import { formatTagName } from 'utils/helper'; import { formatTagName } from 'utils/helper';
import tagStyle from 'styles/tag'; import tagStyle from 'styles/tag';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
@ -30,7 +29,7 @@ export default class Tag extends React.PureComponent {
}; };
render() { render() {
const { name, numberOfLines, onPress, style, type, truncate } = this.props; const { name, onPress, style, type } = this.props;
let styles = []; let styles = [];
if (style) { if (style) {
@ -42,7 +41,7 @@ export default class Tag extends React.PureComponent {
} }
styles.push({ styles.push({
backgroundColor: MATURE_TAGS.includes(name) ? Colors.TagGrape : Colors.TagGreen, backgroundColor: Colors.TagGreen,
borderRadius: 8, borderRadius: 8,
marginBottom: 4, marginBottom: 4,
}); });
@ -50,9 +49,7 @@ export default class Tag extends React.PureComponent {
return ( return (
<TouchableOpacity style={styles} onPress={onPress || this.onPressDefault}> <TouchableOpacity style={styles} onPress={onPress || this.onPressDefault}>
<View style={tagStyle.content}> <View style={tagStyle.content}>
<Text style={tagStyle.text} numberOfLines={numberOfLines}> <Text style={tagStyle.text}>{formatTagName(name)}</Text>
{truncate ? formatTagName(name) : name}
</Text>
{type && <Icon style={tagStyle.icon} name={type === 'add' ? 'plus' : 'times'} size={8} />} {type && <Icon style={tagStyle.icon} name={type === 'add' ? 'plus' : 'times'} size={8} />}
</View> </View>
</TouchableOpacity> </TouchableOpacity>

View file

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { KeyboardAvoidingView, Text, TextInput, TouchableOpacity, View } from 'react-native'; import { KeyboardAvoidingView, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { MATURE_TAGS } from 'lbry-redux';
import Tag from 'component/tag'; import Tag from 'component/tag';
import tagStyle from 'styles/tag'; import tagStyle from 'styles/tag';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
@ -67,14 +66,13 @@ export default class TagSearch extends React.PureComponent {
}; };
render() { render() {
const { editable, name, style, type, selectedTags = [], showNsfwTags } = this.props; const { name, style, type, selectedTags = [] } = this.props;
return ( return (
<View> <View>
<TextInput <TextInput
editable={editable}
style={tagStyle.searchInput} style={tagStyle.searchInput}
placeholder={__('Search for more tags')} placeholder={'Search for more tags'}
underlineColorAndroid={Colors.NextLbryGreen} underlineColorAndroid={Colors.NextLbryGreen}
value={this.state.tag} value={this.state.tag}
numberOfLines={1} numberOfLines={1}
@ -87,22 +85,6 @@ export default class TagSearch extends React.PureComponent {
))} ))}
</View> </View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
{showNsfwTags && (
<View style={tagStyle.nsfwTagsContainer}>
<Text style={tagStyle.nsfwTagsTitle}>{__('Mature tags')}</Text>
<View style={tagStyle.tagResultsList}>
{MATURE_TAGS.map(tag => (
<Tag
key={tag}
name={tag}
style={tagStyle.tag}
type="add"
onAddPress={name => this.onAddTagPress(name)}
/>
))}
</View>
</View>
)}
</View> </View>
); );
} }

View file

@ -2,10 +2,10 @@
import React from 'react'; import React from 'react';
import { Text, View, Linking } from 'react-native'; import { Text, View, Linking } from 'react-native';
import { buildURI, formatCredits } from 'lbry-redux'; import { buildURI, formatCredits } from 'lbry-redux';
import { navigateToUri } from 'utils/helper'; import { navigateToUri } from '../../../utils/helper';
import Link from 'component/link'; import Link from '../../link';
import moment from 'moment'; import moment from 'moment';
import transactionListStyle from 'styles/transactionList'; import transactionListStyle from '../../../styles/transactionList';
class TransactionListItem extends React.PureComponent { class TransactionListItem extends React.PureComponent {
capitalize(string: string) { capitalize(string: string) {
@ -19,12 +19,11 @@ class TransactionListItem extends React.PureComponent {
return ( return (
<View style={transactionListStyle.listItem}> <View style={transactionListStyle.listItem}>
<View style={[transactionListStyle.row, transactionListStyle.topRow]}> <View style={[transactionListStyle.row, transactionListStyle.topRow]}>
<View style={[transactionListStyle.col, transactionListStyle.leftCol]}> <View style={transactionListStyle.col}>
<Text style={transactionListStyle.text}>{this.capitalize(type)}</Text> <Text style={transactionListStyle.text}>{this.capitalize(type)}</Text>
{name && claimId && ( {name && claimId && (
<Link <Link
style={transactionListStyle.link} style={transactionListStyle.link}
numberOfLines={1}
onPress={() => navigateToUri(navigation, buildURI({ claimName: name, claimId }))} onPress={() => navigateToUri(navigation, buildURI({ claimName: name, claimId }))}
text={name} text={name}
/> />
@ -38,19 +37,19 @@ class TransactionListItem extends React.PureComponent {
</View> </View>
</View> </View>
<View style={transactionListStyle.row}> <View style={transactionListStyle.row}>
<View style={[transactionListStyle.col, transactionListStyle.leftCol]}> <View style={transactionListStyle.col}>
<Link <Link
style={transactionListStyle.smallLink} style={transactionListStyle.smallLink}
text={txid.substring(0, 8)} text={txid.substring(0, 8)}
href={`https://explorer.lbry.com/tx/${txid}`} href={`https://explorer.lbry.com/tx/${txid}`}
error={__('The transaction URL could not be opened')} error={'The transaction URL could not be opened'}
/> />
</View> </View>
<View style={transactionListStyle.col}> <View style={transactionListStyle.col}>
{date ? ( {date ? (
<Text style={transactionListStyle.smallText}>{moment(date).format('MMM D')}</Text> <Text style={transactionListStyle.smallText}>{moment(date).format('MMM D')}</Text>
) : ( ) : (
<Text style={transactionListStyle.smallText}>{__('Pending')}</Text> <Text style={transactionListStyle.smallText}>Pending</Text>
)} )}
</View> </View>
</View> </View>

View file

@ -47,7 +47,7 @@ class TransactionList extends React.PureComponent {
return ( return (
<View> <View>
{!transactionList.length && ( {!transactionList.length && (
<Text style={transactionListStyle.noTransactions}>{emptyMessage || __('No transactions to list.')}</Text> <Text style={transactionListStyle.noTransactions}>{emptyMessage || 'No transactions to list.'}</Text>
)} )}
{!!transactionList.length && ( {!!transactionList.length && (

View file

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

View file

@ -4,11 +4,8 @@ import {
selectSearchState as selectSearch, selectSearchState as selectSearch,
selectSearchValue, selectSearchValue,
selectSearchSuggestions, selectSearchSuggestions,
SETTINGS,
} from 'lbry-redux'; } from 'lbry-redux';
import { doSetPlayerVisible } from 'redux/actions/drawer';
import { selectCurrentRoute } from 'redux/selectors/drawer'; import { selectCurrentRoute } from 'redux/selectors/drawer';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import UriBar from './view'; import UriBar from './view';
const select = state => { const select = state => {
@ -19,16 +16,14 @@ const select = state => {
query: selectSearchValue(state), query: selectSearchValue(state),
currentRoute: selectCurrentRoute(state), currentRoute: selectCurrentRoute(state),
suggestions: selectSearchSuggestions(state), suggestions: selectSearchSuggestions(state),
showUriBarSuggestions: makeSelectClientSetting(SETTINGS.SHOW_URI_BAR_SUGGESTIONS)(state),
}; };
}; };
const perform = dispatch => ({ const perform = dispatch => ({
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)), updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)),
setPlayerVisible: (visible, uri) => dispatch(doSetPlayerVisible(visible, uri)),
}); });
export default connect( export default connect(
select, select,
perform, perform
)(UriBar); )(UriBar);

View file

@ -20,10 +20,6 @@ class UriBarItem extends React.PureComponent {
icon = <Icon name="search" size={18} />; icon = <Icon name="search" size={18} />;
break; break;
case SEARCH_TYPES.TAG:
icon = <Icon name="hashtag" size={18} />;
break;
case SEARCH_TYPES.FILE: case SEARCH_TYPES.FILE:
default: default:
icon = <Icon name="file" size={18} />; icon = <Icon name="file" size={18} />;
@ -41,7 +37,6 @@ class UriBarItem extends React.PureComponent {
{type === SEARCH_TYPES.SEARCH && `Search for '${value}'`} {type === SEARCH_TYPES.SEARCH && `Search for '${value}'`}
{type === SEARCH_TYPES.CHANNEL && `View the @${shorthand} channel`} {type === SEARCH_TYPES.CHANNEL && `View the @${shorthand} channel`}
{type === SEARCH_TYPES.FILE && `View content at ${value}`} {type === SEARCH_TYPES.FILE && `View content at ${value}`}
{type === SEARCH_TYPES.TAG && `Explore the '${value}' tag`}
</Text> </Text>
</View> </View>
</TouchableOpacity> </TouchableOpacity>

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