Compare commits

..

No commits in common. "master" and "0.6.0" have entirely different histories.

1945 changed files with 199362 additions and 62982 deletions

View file

@ -1,32 +0,0 @@
## PR Checklist
<!-- For the checkbox formatting to work properly, make sure there are no spaces on either side of the "x" -->
Please check all that apply to this PR using "x":
- [ ] I have checked that this PR is not a duplicate of an existing PR (open, closed or merged)
- [ ] I have checked that this PR does not introduce a breaking change
- [ ] This PR introduces breaking changes and I have provided a detailed explanation below
## PR Type
What kind of change does this PR introduce?
- [ ] Bugfix
- [ ] Feature
- [ ] Code style update (formatting)
- [ ] Refactoring (no functional changes)
- [ ] Documentation changes
- [ ] Other - Please describe:
## Fixes
Issue Number:
## What is the current behavior?
## What is the new behavior?
## Other information
<!-- If this PR contains a breaking change, please describe the impact and solution strategy for existing applications below. -->

View file

@ -5,7 +5,7 @@ To make it possible for us to help you, please fill out below information carefu
Before reporting any issues, please make sure that you're using the latest version.
We are also available on live chat at https://chat.lbry.com
We are also available on live chat at https://chat.lbry.io
-->

View file

@ -1 +0,0 @@

76
.gitignore vendored
View file

@ -1,69 +1,11 @@
# 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/
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
app/google-services.json
app/twitter.properties
.buildozer
app/node_modules/
bin
buildozer.spec
build.log
recipes/**/*.pyc
src/main/assets/index.android.bundle
src/main/assets/index.android.bundle.meta
*.log
.vagrant
*.hprof
app/build
bin
app/debuglib

View file

@ -1,75 +0,0 @@
stages:
- build
- deploy
- release
build apk:
stage: build
image: lbry/android-base:platform-28
before_script:
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
- echo 'deb https://gitsecret.jfrog.io/artifactory/git-secret-deb git-secret main' >> /etc/apt/sources.list
- wget -qO - 'https://gitsecret.jfrog.io/artifactory/api/gpg/key/public' | apt-key add -
- apt-get -y update && apt-get -y install build-essential ca-certificates curl git gpg-agent openjdk-8-jdk software-properties-common wget zipalign git-secret
- git secret reveal
- chmod u+x $CI_PROJECT_DIR/gradlew
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1)
artifacts:
paths:
- bin/browser-*-release__arm.apk
- bin/browser-*-release__arm64.apk
expire_in: 1 week
script:
- export PATH=/usr/bin:$PATH
- export ANDROID_SDK_ROOT=~/.buildozer/android/platform/android-sdk-23
- chmod u+x ./release.sh
- ./release.sh
- cp bin/browser-$BUILD_VERSION-release__arm.apk /dev/null
- cp bin/browser-$BUILD_VERSION-release__arm64.apk /dev/null
deploy build.lbry.io:
image: python:stretch
stage: deploy
dependencies:
- build apk
before_script:
- apt-get -y update && apt-get -y install apt-transport-https
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
- echo 'deb https://gitsecret.jfrog.io/artifactory/git-secret-deb git-secret main' >> /etc/apt/sources.list
- wget -qO - 'https://gitsecret.jfrog.io/artifactory/api/gpg/key/public' | apt-key add -
- apt-get -y update && apt-get -y install openjdk-8-jdk git git-secret
- pip install awscli
- chmod u+x $CI_PROJECT_DIR/gradlew
- git secret reveal
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1)
- export BUILD_APK_FILENAME__32=browser-$BUILD_VERSION-release__arm.apk
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
script:
- aws s3 cp bin/$BUILD_APK_FILENAME__64 s3://build.lbry.io/android/build-${CI_PIPELINE_IID}_commit-${CI_COMMIT_SHA:0:7}/$BUILD_APK_FILENAME__64
- aws s3 cp bin/$BUILD_APK_FILENAME__32 s3://build.lbry.io/android/build-${CI_PIPELINE_IID}_commit-${CI_COMMIT_SHA:0:7}/$BUILD_APK_FILENAME__32
- aws s3 cp bin/$BUILD_APK_FILENAME__64 s3://build.lbry.io/android/push.apk
release apk:
image: python:stretch
stage: release
only:
- tags
dependencies:
- build apk
before_script:
- apt-get -y update && apt-get -y install apt-transport-https
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
- echo 'deb https://gitsecret.jfrog.io/artifactory/git-secret-deb git-secret main' >> /etc/apt/sources.list
- wget -qO - 'https://gitsecret.jfrog.io/artifactory/api/gpg/key/public' | apt-key add -
- apt-get -y update && apt-get -y install openjdk-8-jdk git git-secret
- pip install awscli githubrelease
- git secret reveal
- chmod u+x $CI_PROJECT_DIR/gradlew
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1)
- export BUILD_APK_FILENAME__32=browser-$BUILD_VERSION-release__arm.apk
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
script:
- githubrelease release lbryio/lbry-android create $CI_COMMIT_TAG --publish bin/$BUILD_APK_FILENAME__64 bin/$BUILD_APK_FILENAME__32
- githubrelease release lbryio/lbry-android edit $CI_COMMIT_TAG --draft
- aws s3 cp bin/$BUILD_APK_FILENAME__64 s3://build.lbry.io/android/latest.apk

1
.gitmodules vendored
View file

@ -1 +0,0 @@

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,3 +0,0 @@
lbry-android.keystore:0d958c531870694624cc877ea98ca1c583485f8ebbb3a5acca58b1930c190d65
app/google-services.json:896a0bee8294a36d061f10fa926129d8a780528b34d0a2f03113400c4246d67c
app/twitter.properties:01212d70712f2041efb5c814bf30ecbf6f72e1ca5179c7647c4f8cbd995dd033

94
.travis.yml Normal file
View file

@ -0,0 +1,94 @@
sudo: required
dist: xenial
language: python
python:
- '3.7'
install:
- deactivate
- export PATH=/usr/bin:$PATH
- export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
- sudo dpkg --add-architecture i386
- sudo add-apt-repository ppa:deadsnakes/ppa -y
- sudo apt-get -qq update
- sudo apt-get -qq install build-essential python3.7 python3.7-dev python3.7-venv python3-pip ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386
- sudo pip install --upgrade cython==0.28.1 pip setuptools
- wget -q https://nodejs.org/dist/v8.11.1/node-v8.11.1-linux-x64.tar.xz
- tar -xf node-v8.11.1-linux-x64.tar.xz
- sudo ln -s $TRAVIS_BUILD_DIR/node-v8.11.1-linux-x64/bin/node /usr/bin/node
- sudo ln -s $TRAVIS_BUILD_DIR/node-v8.11.1-linux-x64/bin/npm /usr/bin/npm
- git clone https://github.com/lbryio/buildozer.git
- cd app
- npm config set registry="http://registry.npmjs.org/"
- npm install
- sudo npm install -g react-native-cli
- sudo ln -s $TRAVIS_BUILD_DIR/node-v8.11.1-linux-x64/bin/react-native /usr/bin/react-native
- cd ..
- cd buildozer
- sudo python setup.py install
- cd ..
- mv buildozer.spec.travis buildozer.spec
- mkdir -p cd ~/.buildozer/android/platform/
- wget -q 'https://dist.testnet.lbry.tech/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P ~/.buildozer/android/
- wget -q 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P ~/.buildozer/android/platform/
- wget -q 'https://dl.google.com/android/repository/platform-27_r01.zip' -P ~/.buildozer/android/platform/
- wget -q 'https://dl.google.com/android/repository/build-tools_r26.0.1-linux.zip' -P ~/.buildozer/android/platform/
- tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C ~/.buildozer/android/
- cp -f $TRAVIS_BUILD_DIR/scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
- rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
- ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
- tar -xf ~/.buildozer/android/platform/android-sdk_r23-linux.tgz -C ~/.buildozer/android/platform/
- mv ~/.buildozer/android/platform/android-sdk-linux ~/.buildozer/android/platform/android-sdk-23
- unzip -qq ~/.buildozer/android/platform/platform-27_r01.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms
- mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-8.1.0 ~/.buildozer/android/platform/android-sdk-23/platforms/android-27
- mkdir -p ~/.buildozer/android/platform/android-sdk-23/build-tools
- unzip -qq ~/.buildozer/android/platform/build-tools_r26.0.1-linux.zip -d ~/.buildozer/android/platform/android-sdk-23/build-tools
- mv ~/.buildozer/android/platform/android-sdk-23/build-tools/android-8.0.0 ~/.buildozer/android/platform/android-sdk-23/build-tools/26.0.1
- mkdir -p ~/.buildozer/android/platform/android-sdk-23/licenses
- echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
script:
- "./release.sh | grep -Fv -e 'working:' -e 'copy' -e 'Compiling' --line-buffered"
- cp $TRAVIS_BUILD_DIR/bin/*.apk /dev/null
before_deploy:
- cd $TRAVIS_BUILD_DIR/bin
- export BUILD_VERSION=$(cat ../src/main/python/main.py | grep --color=never -oP '([0-9]+\.?)+')
- cp browser-$BUILD_VERSION-release.apk latest.apk
deploy:
- provider: releases
api_key:
secure: m+FYX7vHZoiLSHHiJ2d3y8Fm4qSRoIVjEei+5BV17awiow/U8UKvy/5J1n8qfBdq+dpst5z58pTHCKWPbJz84C3z/posJ5mwEcOAaD/kxSAMHbtlaPW90pRWHUu3aW86UM/ggqtljE9Qz8KS/9a0xNUDfcXLkLgxuxgwodMcacEulAAc9TIOCUeR3IFI+KN0ptTCVahCu2JN8DCHKomaR+yKZHdo/9v9XCAcvmImSDu9nUDLH3+A7xQeRpPJqSspk1dadgdXP76kU8t3OKsYuM7DS5AoKvMIc9lZot4UYYKAx7/zavbzeEmqnyskULgsmV8/UDI1AV9U7uFBdrR6dSjISA1k6EHnCgqzasF+lp0hz5iE/0yPxlE9Z1kLW9gZgxSJtjr6Kv2uqAjHYYmpkjtTwHPwBugRM7PWMTxHNcPwkIHpBSRkXjpyDjkWd/LY4X866Y1g2BdIhbGshjy/9Fb2vnYxNZW6drLHn+wWeHJ41Vfgtg1cn01yZGJqgIkcTkhzNL6Bi++y8EBJXDr4L870s336SpbqRuIrO/C16ZFB+XnOg4Ty50Fk5zkbySMHII58iWqSyDYWNvhqo9zU9jn1XQQeok12129Y4t9TUOcJRbxhQ+511lCmVcFIkWHsXDK2QSZ7TeMK5GQUA8OvcNe+WLCJaQ/YD7OZvwlPTvc=
file_glob: true
file: browser-*-release.apk
overwrite: true
skip_cleanup: true
on:
tags: true
- provider: s3
access_key_id:
secure: qEZZ73DWBn9+M2pS4VwsyX8YZjZOENIMP/eoU1A9Vbn155oZpbUaJ7k+4cAXqmBm0WBMKZDqpzCRSGehLAxHFH5rjkj9gLSgd8fY9cveABHkl50HeuxxNsQM+ytk9sCtP8bWOqf7rT/iCgM1soyF2pYmfHM3tU9l0fWK8oZ7pFRIg91hXxUvhvYY2u6B9D1NqSN9T3xtrwEkVjvmkmyKMLCtoIdyB7QdeQciFdGFhZC9DYJVRLxa3BlzZ1T8Qv4MCyIxPjxIugNvVR64VgGjdBdq0BEIyoRqbeIvtqQjlnne4DfJFeCmbDrSva1wDP1UyFoxsRhiWQ/jXXgIyN2CisI6QRD0J+a2LgmbmtkUzhRMuVQJmBrIauulOzcowwRV2J4TtUaAJK9iSHT9D3RLzpazCOnjvJZV9CK5w252Vs5eHnisCSCQk8Ozox96Sg6XW580NEXfkYoGzXLSGiy9zrZs813blUjssEY+jIQmJEby80C3guK7/G4lzthv57psqBWcYd0tFR+vTestS+EFlC02ToUngJhW7I7lPA2G2yrJ5319jFxUSniijb1n72TQthnbqTBahepvKvuG1iWZnCKxS5sWkutFoqEcpQyhXdf7QdR/VrOr8N5xrhK1B8dCYZM6h8eMZbBvOLH49+N6L9jiJz5x+Lk32wcssv1oOgI=
secret_access_key:
secure: lPygaaJdjFgWY4GcXUXC4Oc5op/TE85Md8lX2bzW19058lbcqYSdM0WySQCxoU/4rlM4Q0N8du0qQ3kZXDpP9XSqvFTVnTGTuB4yghUR1yXcpt6u3JOeOX+YAc3wyQ/pmod6VGO0n8pm8hBVsSFXufdBTjD2W+tNrDoa8RYnlWrt7BbICGltB7PcqYh1Qw6S1wDyZt8I4B5JHDhyJmX6FT5KfOb5cJyynpxlKUstUfy1rh81KuGkEcuEVOLg1s7HE1/IUkVIgezAuCrMHjc86qbNcHULJMFCVYntvvs07+tctrPxA/cfS24WkW7smyij+gdZAZWNNgkIDCuwqpex1v1nKn56mC8xXyUl4CnSCuubQtqUBzTmd4T5sF7trTtpVr9NInwy+4mUoCpz2UKZekTjZkqpzCAuC/cBVWE1/k3wsNat6dGyc9QnKXBqLVhuwYsCOteqLW50ToMMMW0ccDV6FXodwZmrunGd5wIX+UgZkf4l32vzKUxHtIupfYbsjylcPc3VO0OzMMKP/3sYLAN6QntVDFc30k1uqqpgJN4t0nV7vvjMI+b0Qr+o7GzUV2d+QulQXOySJgB2pH0kV1EoPAJ8KbqDOy8KgCJl0YIbOaz14+SiRQhotJ2hrLdtsvyVYXMX+d/CKHJSWa2MQq+jD7lMCwVaGg82PFN1gI4=
bucket: "build.lbry.io"
upload-dir: android
region: us-east-1
overwrite: true
skip_cleanup: true
on:
tags: true
- provider: s3
access_key_id:
secure: qEZZ73DWBn9+M2pS4VwsyX8YZjZOENIMP/eoU1A9Vbn155oZpbUaJ7k+4cAXqmBm0WBMKZDqpzCRSGehLAxHFH5rjkj9gLSgd8fY9cveABHkl50HeuxxNsQM+ytk9sCtP8bWOqf7rT/iCgM1soyF2pYmfHM3tU9l0fWK8oZ7pFRIg91hXxUvhvYY2u6B9D1NqSN9T3xtrwEkVjvmkmyKMLCtoIdyB7QdeQciFdGFhZC9DYJVRLxa3BlzZ1T8Qv4MCyIxPjxIugNvVR64VgGjdBdq0BEIyoRqbeIvtqQjlnne4DfJFeCmbDrSva1wDP1UyFoxsRhiWQ/jXXgIyN2CisI6QRD0J+a2LgmbmtkUzhRMuVQJmBrIauulOzcowwRV2J4TtUaAJK9iSHT9D3RLzpazCOnjvJZV9CK5w252Vs5eHnisCSCQk8Ozox96Sg6XW580NEXfkYoGzXLSGiy9zrZs813blUjssEY+jIQmJEby80C3guK7/G4lzthv57psqBWcYd0tFR+vTestS+EFlC02ToUngJhW7I7lPA2G2yrJ5319jFxUSniijb1n72TQthnbqTBahepvKvuG1iWZnCKxS5sWkutFoqEcpQyhXdf7QdR/VrOr8N5xrhK1B8dCYZM6h8eMZbBvOLH49+N6L9jiJz5x+Lk32wcssv1oOgI=
secret_access_key:
secure: lPygaaJdjFgWY4GcXUXC4Oc5op/TE85Md8lX2bzW19058lbcqYSdM0WySQCxoU/4rlM4Q0N8du0qQ3kZXDpP9XSqvFTVnTGTuB4yghUR1yXcpt6u3JOeOX+YAc3wyQ/pmod6VGO0n8pm8hBVsSFXufdBTjD2W+tNrDoa8RYnlWrt7BbICGltB7PcqYh1Qw6S1wDyZt8I4B5JHDhyJmX6FT5KfOb5cJyynpxlKUstUfy1rh81KuGkEcuEVOLg1s7HE1/IUkVIgezAuCrMHjc86qbNcHULJMFCVYntvvs07+tctrPxA/cfS24WkW7smyij+gdZAZWNNgkIDCuwqpex1v1nKn56mC8xXyUl4CnSCuubQtqUBzTmd4T5sF7trTtpVr9NInwy+4mUoCpz2UKZekTjZkqpzCAuC/cBVWE1/k3wsNat6dGyc9QnKXBqLVhuwYsCOteqLW50ToMMMW0ccDV6FXodwZmrunGd5wIX+UgZkf4l32vzKUxHtIupfYbsjylcPc3VO0OzMMKP/3sYLAN6QntVDFc30k1uqqpgJN4t0nV7vvjMI+b0Qr+o7GzUV2d+QulQXOySJgB2pH0kV1EoPAJ8KbqDOy8KgCJl0YIbOaz14+SiRQhotJ2hrLdtsvyVYXMX+d/CKHJSWa2MQq+jD7lMCwVaGg82PFN1gI4=
bucket: "build.lbry.io"
upload-dir: "android/build-${TRAVIS_BUILD_NUMBER}_commit-${TRAVIS_COMMIT:0:7}"
region: us-east-1
overwrite: true
skip_cleanup: true
on:
all_branches: true
env:
global:
- secure: GS3Cp1QXiX8UPye3kdk2A2f3iFRr02sHKpY+RE+Zvx3Q7GDmhDuepHKzx6Hq5Os5fZN9Y/Bdds+XH+vLIRtT6XsWR7AONPhSifVY3XB5/2F+lDcZ538W8P8GZvXejpY4VecMUWHoWbuyt0s3PpaGXZJcHp8ir+CUJ0NUmU3I9w449pqj9/de2LHtG3qKH1lG0Xz58iOC0mmEeH451cQv3dDw851ihA4ak9vCTV1KKuMJUcv+2u6PxXGVX0mrJLEssjL6ze6G5iZUB4PM1vUpe3HqcVw8CSOa8O79BQxoB00qyA3WD+LpZDPpI0wh6gmBsR/2nCFyMJndJr3CjyB6lHdK7PgBoK0CJjszKawiZqg74O9DOjzTJTO2v9bnkfPrNxu4/3D/tbDg+whY8k5oV1sgDue9KAo/2aEEO0LGlKP4W3Qqt/lzRKsfpMVrMTdCNKJ8rG/wUFWw8ehOCmAsJaQ1saDOZDMNPLLuYpxFgmXFqWV5ThbUHgEJVj+G7qt6CMEussKvuZJoJZx24Pdk5Prr7ENzTyPmE5gk4b8WNfVNleOEC09xu5tFk2yOdzF1dawKsa1Mog6gImirTQ/INC/3BANdKoG9/cLJEIt9boJaFDXE1dpqoLVzoez9znHKOGSAU/1PaH3thjVnbUyO5z24PpPZ12zM3+3P8DbI454=
before_install:
- openssl aes-256-cbc -K $encrypted_b4c9b905b12e_key -iv $encrypted_b4c9b905b12e_iv
-in lbry-android.keystore.enc -out lbry-android.keystore -d

117
BUILD.md Normal file
View file

@ -0,0 +1,117 @@
## Linux Build Instructions
This app has currently only been built on Ubuntu 14.04, 16.04, 17.10, and 18.04, but these instructions, or an analog of them, should work on most Linux or macOS environments.
### Install Prerequisites
#### Requirements
* JDK 1.8
* Android SDK
* Crystax Android NDK
* Buildozer
* Node.js
* npm
* yarn
#### apt Packages
Based on the quick-start instructions at http://buildozer.readthedocs.io/en/latest/installation.html
```
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install autoconf autogen build-essential curl libtool libffi-dev python python-pip python-openssl python3.7 python3.7-dev python3-pip ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386
```
Alternatively, the JDK available from http://www.oracle.com/technetwork/java/javase/downloads/index.html can be installed instead of the `openjdk-8-jdk` package.
#### Install Cython and Setuptools
```
sudo pip install --upgrade cython==0.28.1 setuptools
```
#### Install buildozer
A forked version of `buildozer` needs to be installed in order to copy the React Native UI source files into the corresponding directories.
```
git clone https://github.com/lbryio/buildozer.git
cd buildozer
sudo python2.7 setup.py install
```
#### Create buildozer.spec
Assuming `lbry-android` as the current working folder:
* Copy `buildozer.spec.sample` to `buildozer.spec` in the `lbry-android` folder. Running `buildozer init` instead will create a new `buildozer.spec` file.
* Update `buildozer.spec` settings to match your environment. The basic recommended settings are outlined below.
| Setting | Description |
|:------------------- |:-----------------------------|
| title | application title |
| package.name | package name (e.g. browser) |
| package.domain | package domain (e.g. io.lbry) |
| source.dir | the location of the application main.py |
| version | application version |
| requirements | the Python module requirements for building the application |
| services | list of Android background services and their corresponding Python entry points |
| android.permissions | Android manifest permissions required by the application. This should be set to `INTERNET` at the very least to enable internet connectivity |
| android.api | Android API version (Should be at least 23 for Gradle build support) |
| android.sdk | Android SDK version (Should be at least 23 for Gradle build support) |
| android.ndk | Android NDK version (not required when using crystax Android NDK) |
| android.ndk_path | Android NDK path. This should be set to the crystax Android NDK path) |
| android.sdk_path | Android SDK path. This should be set to the path where the Android SDK is manually set up (if not set up in the `.buildozer` path). |
| p4a.source_dir | Path to the python-for-android repository folder. Currently set to the included `p4a` folder |
| p4a.local_recipes | Path to a folder containing python_for_android recipes to be used in the build. The included `recipes` folder includes recipes for a successful build |
#### Setup Android SDK for buildozer
Download the Android SDK, platform and build tools archives.
* Android API 23 SDK - https://dl.google.com/android/android-sdk_r23-linux.tgz
* Android API 27 platform - https://dl.google.com/android/repository/platform-27_r01.zip
* Android build tools 26.0.1 - https://dl.google.com/android/repository/build-tools_r26.0.1-linux.
Create the `.buildozer` path (and the `android` sub-path) in your home folder if it doesn't already exist.
`mkdir ~/.buildozer`
`mkdir ~/.buildozer/android`
Extract the API 23 SDK to the `~/.buildozer/android` path and rename the extracted folder.
```
tar -xf android-sdk_r23-linux.tgz ~/.buildozer/android/platform/
mv ~/.buildozer/android/platform/android-sdk-linux ~/.buildozer/android/platform/android-sdk-23
```
Extract the API 27 platform archive into the `android-sdk-23` folder and rename the extracted folder.
```
unzip platform-27_r01.zip -d ~/.buildozer/android/platform/android-sdk-23/platforms
mv ~/.buildozer/android/platform/android-sdk-23/platforms/android-8.1.0 ~/.buildozer/android/platform/android-sdk-23/platforms/android-27
```
Extract the build tools 26.0.1 build tools into the `android-sdk-23` folder and rename the extracted folder.
```
mkdir -p ~/.buildozer/android/platform/android-sdk-23/build-tools
unzip ~/.buildozer/android/platform/build-tools_r26.0.1-linux.zip -d ~/.buildozer/android/platform/android-sdk-23/build-tools
mv ~/.buildozer/android/platform/android-sdk-23/build-tools/android-8.0.0 ~/.buildozer/android/platform/android-sdk-23/build-tools/26.0.1
```
Finally, create the Android SDK license file. This prevents being prompted to accept the SDK license during the build process.
```
mkdir -p ~/.buildozer/android/platform/android-sdk-23/licenses
echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > ~/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
```
#### Setup Crystax Android NDK for buildozer
* Download the Crystax Android NDK from https://us.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz and extract. Remember to update `android.ndk_path` in your `buildozer.spec` to the path of the extracted Crystax NDK archive.
* Copy `build-target-python.sh` from the `scripts` folder in the cloned `lbry-android` repository to the `crystax-ndk-10.3.2/build/tools/` folder.
* Delete the `android-9` folder in `crystax-ndk-10.3.2/platforms`, and create a symbolic link named `android-9` to the `android-21` folder.
#### Build and Deploy
Run `npm i` in the `lbry-android/app` folder to install the necessary modules required by the React Native user interface.
Run `./build.sh` in `lbry-android` to build the APK. The output can be found in the `bin` subdirectory.
To build and deploy, you can run `./deploy.sh`. This requires a connected device or running Android emulator.
#### Development
If you already installed `Android SDK` and `adb`
* Run `adb reverse tcp:8081 tcp:8081`
* Then go to the `lbry-android/app` folder and run `npm start`
Note: You need to have your device connected with USB debugging.
Once the bundler is ready, run the LBRY Browser app on your device and then shake the device violently until you see the React Native dev menu. You can enable "Live Reloading" and "Hot Reloading" from this menu, so any changes you make to the React Native code will be visible as you save. This will only reload React Native Javascript files. Native Java code needs to be redeployed by running the command `./deploy.sh`

View file

@ -1,6 +1,6 @@
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:

View file

@ -1,12 +1,9 @@
# LBRY Android
[![pipeline status](https://ci.lbry.tech/lbry/lbry-android/badges/master/pipeline.svg)](https://ci.lbry.tech/lbry/lbry-android/commits/master)
[![GitHub license](https://img.shields.io/github/license/lbryio/lbry-android)](https://github.com/lbryio/lbry-android/blob/master/LICENSE)
[![Build Status](https://travis-ci.org/lbryio/lbry-android.svg?branch=master)](https://travis-ci.org/lbryio/lbry-android)
An Android browser and wallet for the [LBRY](https://lbry.com) network.
<img src="https://spee.ch/@lbry:3f/android-08-homepage.gif" alt="LBRY Android GIF" width="384px" />
An Android browser and wallet for the [LBRY](https://lbry.io) network. This app bundles [lbrynet-daemon](https://github.com/lbryio/lbry) as a background service with a UI layer built with React Native. The APK is built using buildozer and the Gradle build tool.
<img src="https://spee.ch/8/lbry-android.png" alt="LBRY Android Screenshot" width="384px" />
## Installation
The minimum supported Android version is 5.0 Lollipop. There are two ways to install:
@ -15,22 +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.
## 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
Clone the repository and open the project in Android Studio. Android Studio will automatically run the initial build process.
Create file 'twitter.properties' in app/ folder with the following content:
```
twitterConsumerKey=XXXXXX
twitterConsumerSecret=XXXXXX
```
Copy the file 'google-services.sample.json' to 'google-services.json' in the app/ folder.
Click the Sync button and when process finishes, the Run button to launch the app on your simulator or connected debugging device after the build process is complete.
The app is built from source via [Buildozer](https://github.com/kivy/buildozer). After cloning the repository, copy `buildozer.spec.sample` to `buildozer.spec` and modify this file as necessary for your environment. Please see [BUILD.md](BUILD.md) for detailed build instructions.
## Contributing
Contributions to this project are welcome, encouraged, and compensated. For more details, see https://lbry.io/faq/contributing
@ -39,7 +24,7 @@ Contributions to this project are welcome, encouraged, and compensated. For more
This project is MIT licensed. For the full license, see [LICENSE](LICENSE).
## Security
We take security seriously. Please contact security@lbry.com regarding any security issues. Our PGP key is [here](https://lbry.com/faq/pgp-key) if you need it.
We take security seriously. Please contact security@lbry.io regarding any security issues. Our PGP key is [here](https://keybase.io/lbry/key.asc) if you need it.
## Contact
The primary contact for this project is [@akinwale](https://github.com/akinwale) (akinwale@lbry.com)
The primary contact for this project is [@akinwale](https://github.com/akinwale) (akinwale@lbry.io)

100
Vagrantfile vendored Normal file
View file

@ -0,0 +1,100 @@
echoed=false
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/bionic64"
#config.disksize.size = "20GB"
config.vm.provider "virtualbox" do |v|
host = RbConfig::CONFIG['host_os']
# Give VM 1/4 system memory & access to all cpu cores on the host
if host =~ /darwin/
cpus = `sysctl -n hw.ncpu`.to_i
# sysctl returns Bytes and we need to convert to MB
mem = `sysctl -n hw.memsize`.to_i / 1024 / 1024 / 4
elsif host =~ /linux/
cpus = `nproc`.to_i
# meminfo shows KB and we need to convert to MB
mem = `grep 'MemTotal' /proc/meminfo | sed -e 's/MemTotal://' -e 's/ kB//'`.to_i / 1024 / 4
else
cpus = `wmic cpu get NumberOfCores`.split("\n")[2].to_i
mem = `wmic OS get TotalVisibleMemorySize`.split("\n")[2].to_i / 1024 /4
end
mem = mem / 1024 / 4
mem = [mem, 2048].max # Minimum 2048
if echoed === false
echoed=true
puts("Memory", mem)
puts("CPUs", cpus)
end
#v.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/home_vagrant_lbry-android", "1"]
#v.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/vagrant", "1"]
v.customize ["modifyvm", :id, "--memory", mem]
v.customize ["modifyvm", :id, "--cpus", cpus]
end
config.vm.synced_folder "./", "/home/vagrant/lbry-android"
config.vm.provision "shell", inline: <<-SHELL
dpkg --add-architecture i386
apt-get update
apt-get install -y libssl-dev
apt-get install -y python3.6 python3.6-dev python3-pip autoconf libffi-dev pkg-config libtool build-essential ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 m4 libc6-dev-i386 python-pip
pip install -f --upgrade setuptools pyopenssl
git clone https://github.com/lbryio/buildozer.git
cd buildozer
python2.7 setup.py install
cd ../
rm -rf ./buildozer
# Install additonal buildozer dependencies
sudo apt-get install cython
# Install node
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
sudo apt-get install -y nodejs
export HOME=/home/vagrant
cp $HOME/lbry-android/buildozer.spec.vagrant $HOME/lbry-android/buildozer.spec
mkdir -p cd $HOME/.buildozer/android/platform/
wget -q 'https://us.crystax.net/download/crystax-ndk-10.3.2-linux-x86_64.tar.xz' -P $HOME/.buildozer/android/
wget -q 'https://dl.google.com/android/android-sdk_r23-linux.tgz' -P $HOME/.buildozer/android/platform/
wget -q 'https://dl.google.com/android/repository/platform-27_r01.zip' -P $HOME/.buildozer/android/platform/
wget -q 'https://dl.google.com/android/repository/build-tools_r26.0.1-linux.zip' -P $HOME/.buildozer/android/platform/
tar -xf ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz -C $HOME/.buildozer/android/
rm $HOME/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz
ln -s $HOME/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 $HOME/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
cp -f $HOME/lbry-android/scripts/build-target-python.sh $HOME/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
rm -rf $HOME/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
tar -xf $HOME/.buildozer/android/platform/android-sdk_r23-linux.tgz -C $HOME/.buildozer/android/platform/
rm $HOME/.buildozer/android/platform/android-sdk_r23-linux.tgz
mv $HOME/.buildozer/android/platform/android-sdk-linux $HOME/.buildozer/android/platform/android-sdk-23
unzip -qq $HOME/.buildozer/android/platform/android-23_r02.zip -d $HOME/.buildozer/android/platform/android-sdk-23/platforms
rm $HOME/.buildozer/android/platform/platform-27_r01.zip
mv $HOME/.buildozer/android/platform/android-sdk-23/platforms/android-8.1.0 $HOME/.buildozer/android/platform/android-sdk-23/platforms/android-27
mkdir -p $HOME/.buildozer/android/platform/android-sdk-23/build-tools
unzip -qq $HOME/.buildozer/android/platform/build-tools_r26.0.1-linux.zip -d $HOME/.buildozer/android/platform/android-sdk-23/build-tools
rm $HOME/.buildozer/android/platform/build-tools_r26.0.1-linux.zip
mv $HOME/.buildozer/android/platform/android-sdk-23/build-tools/android-8.0.0 $HOME/.buildozer/android/platform/android-sdk-23/build-tools/26.0.1
mkdir -p $HOME/.buildozer/android/platform/android-sdk-23/licenses
rm -rf $HOME/.buildozer/android/platform/android-sdk-23/tools
# https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
wget -q https://dl.google.com/android/repository/tools_r25.2.5-linux.zip
unzip -o -q ./tools_r25.2.5-linux.zip -d $HOME/.buildozer/android/platform/android-sdk-23/
rm sdk-tools-linux-3859397.zip
echo $'\nd56f5187479451eabf01fb78af6dfcb131a6481e' > $HOME/.buildozer/android/platform/android-sdk-23/licenses/android-sdk-license
sudo chown -r vagrant $HOME
echo "Installing React Native via NPM..."
sudo npm install -g react-native-cli
SHELL
end

8
app/.babelrc Normal file
View file

@ -0,0 +1,8 @@
{
"presets": ["react-native"],
"plugins": [
["module-resolver", {
root: ["./src"],
}],
]
}

1
app/.gitignore vendored
View file

@ -1 +0,0 @@
/build

View file

@ -1,144 +0,0 @@
import com.google.gms.googleservices.GoogleServicesPlugin
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
flavorDimensions "default"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "io.lbry.browser"
minSdkVersion 21
targetSdkVersion 29
versionCode 1701
versionName "0.17.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'lib/x86_64/darwin/libscrypt.dylib'
}
productFlavors {
__32bit {
versionCode android.defaultConfig.versionCode * 10 + 1
ndk {
abiFilter "armeabi-v7a"
}
}
__64bit {
versionCode android.defaultConfig.versionCode * 10 + 2
ndk {
abiFilter "arm64-v8a"
}
}
}
buildTypes {
debug {
Properties twitterProps = new Properties()
twitterProps.load(project.file('twitter.properties').newDataInputStream())
resValue "string", "TWITTER_CONSUMER_KEY", "\"${twitterProps.getProperty("twitterConsumerKey")}\""
resValue "string", "TWITTER_CONSUMER_SECRET", "\"${twitterProps.getProperty("twitterConsumerSecret")}\""
}
release {
Properties twitterProps = new Properties()
twitterProps.load(project.file('twitter.properties').newDataInputStream())
resValue "string", "TWITTER_CONSUMER_KEY", "\"${twitterProps.getProperty("twitterConsumerKey")}\""
resValue "string", "TWITTER_CONSUMER_SECRET", "\"${twitterProps.getProperty("twitterConsumerSecret")}\""
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
task printVersionName {
doLast {
println android.defaultConfig.versionName
}
}
configurations {
all {
exclude module: 'httpclient'
exclude module: 'commons-logging'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.3.0-alpha01'
implementation "androidx.cardview:cardview:1.0.0"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment:2.3.1'
implementation 'androidx.navigation:navigation-ui:2.3.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.webkit:webkit:1.4.0-rc01'
implementation 'androidx.camera:camera-core:1.0.0-beta03'
implementation 'androidx.camera:camera-camera2:1.0.0-beta03'
implementation 'androidx.camera:camera-lifecycle:1.0.0-beta03'
implementation 'androidx.camera:camera-view:1.0.0-alpha10'
implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'com.squareup.okhttp3:okhttp:4.4.1'
implementation 'com.google.firebase:firebase-analytics:18.0.0'
implementation 'com.google.android.gms:play-services-base:17.5.0'
implementation 'com.google.firebase:firebase-messaging:21.0.0'
implementation 'com.google.oauth-client:google-oauth-client:1.30.4'
implementation 'com.android.billingclient:billing:3.0.2'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.2'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.2'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.2'
implementation 'com.google.android.exoplayer:extension-cast:2.12.2'
implementation 'com.google.android.exoplayer:extension-mediasession:2.12.2'
implementation 'com.google.android:flexbox:2.0.1'
implementation 'com.hbb20:ccp:2.3.8'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.atlassian.commonmark:commonmark:0.14.0'
implementation 'com.arthenica:mobile-ffmpeg-full-gpl:4.3.1.LTS'
implementation 'commons-codec:commons-codec:1.15'
implementation 'org.bitcoinj:bitcoinj-tools:0.14.7'
implementation 'org.java-websocket:Java-WebSocket:1.5.1'
implementation ('com.journeyapps:zxing-android-embedded:4.1.0') { transitive = false }
implementation 'com.google.zxing:core:3.3.0'
compileOnly 'org.projectlombok:lombok:1.18.10'
annotationProcessor 'org.projectlombok:lombok:1.18.10'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
__32bitImplementation 'io.lbry:lbrysdk32:0.102.0'
__64bitImplementation 'io.lbry:lbrysdk64:0.102.0'
//__64bitImplementation(name: 'lbrysdk', ext: 'aar')
}
apply plugin: 'com.google.gms.google-services'
GoogleServicesPlugin.config.disableVersionCheck = true

2
app/bundle.sh Executable file
View file

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

Binary file not shown.

View file

@ -1,41 +0,0 @@
{
"project_info": {
"project_number": "861521963586",
"firebase_url": "https://lbry-mobile-builds-debug.firebaseio.com",
"project_id": "lbry-mobile-builds-debug",
"storage_bucket": "lbry-mobile-builds-debug.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:861521963586:android:592958d248940ab2",
"android_client_info": {
"package_name": "io.lbry.browser"
}
},
"oauth_client": [
{
"client_id": "861521963586-60cmvg5nmnrqkrc11a7bpmpv5ra2d50q.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyC7A3BYcIdZP9-Q-VNHoexYJWgZA7WzsPI"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "861521963586-60cmvg5nmnrqkrc11a7bpmpv5ra2d50q.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

3
app/index.js Normal file
View file

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

7172
app/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

43
app/package.json Normal file
View file

@ -0,0 +1,43 @@
{
"name": "LBRYApp",
"version": "0.0.1",
"private": "true",
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
},
"dependencies": {
"base-64": "^0.1.0",
"@expo/vector-icons": "^8.1.0",
"lbry-redux": "lbryio/lbry-redux#old-search",
"lbryinc": "lbryio/lbryinc",
"lodash": ">=4.17.11",
"merge": ">=1.2.1",
"moment": "^2.22.1",
"react": "16.2.0",
"react-native": "0.55.3",
"react-native-country-picker-modal": "^0.6.2",
"react-native-exception-handler": "2.9.0",
"react-native-fast-image": "^5.0.3",
"react-native-fetch-blob": "^0.10.8",
"react-native-image-zoom-viewer": "^2.2.5",
"react-native-phone-input": "lbryio/react-native-phone-input",
"react-native-vector-icons": "^5.0.0",
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
"react-navigation": "^2.18.3",
"react-navigation-redux-helpers": "^2.0.9",
"react-redux": "^5.0.3",
"redux": "^3.6.0",
"redux-logger": "3.0.6",
"redux-persist": "^4.8.0",
"redux-persist-filesystem-storage": "^1.2.0",
"redux-persist-transform-compress": "^4.2.0",
"redux-persist-transform-filter": "0.0.10",
"redux-thunk": "^2.2.0"
},
"devDependencies": {
"babel-preset-env": "^1.6.1",
"babel-preset-stage-2": "^6.18.0",
"babel-plugin-module-resolver": "^3.1.1",
"flow-babel-webpack-plugin": "^1.1.1"
}
}

View file

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -1,27 +0,0 @@
package io.lbry.browser;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("io.lbry.browser", appContext.getPackageName());
}
}

View file

@ -1,12 +0,0 @@
package io.lbry.browser.utils;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@SmallTest
public class HelperTest {
}

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,24 @@
import { connect } from 'react-redux';
import {
doUserEmailNew,
doUserResendVerificationEmail,
selectEmailNewErrorMessage,
selectEmailNewIsPending,
selectEmailToVerify,
} from 'lbryinc';
import { doToast } from 'lbry-redux';
import EmailRewardSubcard from './view';
const select = state => ({
emailToVerify: selectEmailToVerify(state),
emailNewErrorMessage: selectEmailNewErrorMessage(state),
emailNewPending: selectEmailNewIsPending(state)
});
const perform = dispatch => ({
addUserEmail: email => dispatch(doUserEmailNew(email)),
notify: data => dispatch(doToast(data)),
resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email))
});
export default connect(select, perform)(EmailRewardSubcard);

View file

@ -0,0 +1,105 @@
// @flow
import React from 'react';
import {
ActivityIndicator,
AsyncStorage,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Button from '../button';
import Colors from '../../styles/colors';
import Constants from '../../constants';
import Link from '../link';
import rewardStyle from '../../styles/reward';
class EmailRewardSubcard extends React.PureComponent {
state = {
email: null,
emailAlreadySet: false,
previousEmail: null,
verfiyStarted: false
};
componentDidMount() {
const { emailToVerify } = this.props;
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => {
if (email && email.trim().length > 0) {
this.setState({ email, emailAlreadySet: true, previousEmail: email });
} else {
this.setState({ email: emailToVerify, previousEmail: emailToVerify });
}
});
}
componentWillReceiveProps(nextProps) {
const { emailNewErrorMessage, emailNewPending } = nextProps;
const { notify } = this.props;
if (this.state.verifyStarted && !emailNewPending) {
if (emailNewErrorMessage) {
notify({ message: String(emailNewErrorMessage), isError: true });
this.setState({ verifyStarted: false });
} else {
notify({ message: 'Please follow the instructions in the email sent to your address to continue.' });
AsyncStorage.setItem(Constants.KEY_EMAIL_VERIFY_PENDING, 'true');
}
}
}
handleChangeText = (text) => {
// save the value to the state email
this.setState({ email: text });
AsyncStorage.setItem(Constants.KEY_FIRST_RUN_EMAIL, text);
}
onSendVerificationPressed = () => {
if (this.state.verifyStarted) {
return;
}
const { addUserEmail, notify, resendVerificationEmail } = this.props;
const { email } = this.state;
if (!email || email.trim().length === 0 || email.indexOf('@') === -1) {
return notify({
message: 'Please provide a valid email address to continue.',
});
}
this.setState({ verifyStarted: true });
if (this.state.emailAlreadySet && this.state.previousEmail === email) {
// resend verification email if there was one previously set (and it wasn't changed)
resendVerificationEmail(email);
AsyncStorage.setItem(Constants.KEY_EMAIL_VERIFY_PENDING, 'true');
notify({ message: 'Please follow the instructions in the email sent to your address to continue.' });
return;
}
addUserEmail(email);
}
render() {
const { emailNewPending } = this.props;
return (
<View style={rewardStyle.subcard}>
<Text style={rewardStyle.subtitle}>Pending action: Verify Email</Text>
<Text style={rewardStyle.subcardText}>Please provide an email address to verify. If you received a link previously, please follow the instructions in the email to complete verification.</Text>
<TextInput style={rewardStyle.subcardTextInput}
placeholder="you@example.com"
underlineColorAndroid="transparent"
value={this.state.email}
onChangeText={text => this.handleChangeText(text)} />
{!this.state.verifyStarted && <Button style={rewardStyle.actionButton}
text={"Send verification email"}
onPress={this.onSendVerificationPressed} />}
{this.state.verifyStarted && emailNewPending &&
<ActivityIndicator size={"small"} color={Colors.LbryGreen} style={rewardStyle.loading} />}
</View>
);
}
};
export default EmailRewardSubcard;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import { SETTINGS } from 'lbry-redux';
import { makeSelectClientSetting } from '../../redux/selectors/settings';
import MediaPlayer from './view';
const select = state => ({
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
});
export default connect(select, null)(MediaPlayer);

View file

@ -0,0 +1,401 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import {
DeviceEventEmitter,
NativeModules,
PanResponder,
Text,
View,
ScrollView,
TouchableOpacity
} from 'react-native';
import FastImage from 'react-native-fast-image'
import Video from 'react-native-video';
import Icon from 'react-native-vector-icons/FontAwesome5';
import FileItemMedia from 'component/fileItemMedia';
import mediaPlayerStyle from 'styles/mediaPlayer';
class MediaPlayer extends React.PureComponent {
static ControlsTimeout = 3000;
seekResponder = null;
seekerWidth = 0;
trackingOffset = 0;
tracking = null;
video = null;
constructor(props) {
super(props);
this.state = {
encodedFilePath: null,
rate: 1,
volume: 1,
muted: false,
resizeMode: 'contain',
duration: 0.0,
currentTime: 0.0,
paused: !props.autoPlay,
fullscreenMode: false,
areControlsVisible: true,
controlsTimeout: -1,
seekerOffset: 0,
seekerPosition: 0,
firstPlay: true,
seekTimeout: -1
};
}
formatTime(time) {
let str = '';
let minutes = 0, hours = 0, seconds = parseInt(time, 10);
if (seconds > 60) {
minutes = parseInt(seconds / 60, 10);
seconds = seconds % 60;
if (minutes > 60) {
hours = parseInt(minutes / 60, 10);
minutes = minutes % 60;
}
str = (hours > 0 ? this.pad(hours) + ':' : '') + this.pad(minutes) + ':' + this.pad(seconds);
} else {
str = '00:' + this.pad(seconds);
}
return str;
}
pad(value) {
if (value < 10) {
return '0' + String(value);
}
return value;
}
onLoad = (data) => {
this.setState({
duration: data.duration
});
if (this.props.onMediaLoaded) {
this.props.onMediaLoaded();
}
}
onProgress = (data) => {
this.setState({ currentTime: data.currentTime });
if (!this.state.seeking) {
this.setSeekerPosition(this.calculateSeekerPosition());
}
if (this.state.firstPlay) {
if (this.props.onPlaybackStarted) {
this.props.onPlaybackStarted();
}
this.setState({ firstPlay: false });
this.hidePlayerControls();
}
}
clearControlsTimeout = () => {
if (this.state.controlsTimeout > -1) {
clearTimeout(this.state.controlsTimeout)
}
}
showPlayerControls = () => {
this.clearControlsTimeout();
if (!this.state.areControlsVisible) {
this.setState({ areControlsVisible: true });
}
this.hidePlayerControls();
}
manualHidePlayerControls = () => {
this.clearControlsTimeout();
this.setState({ areControlsVisible: false });
}
hidePlayerControls() {
const player = this;
let timeout = setTimeout(() => {
player.setState({ areControlsVisible: false });
}, MediaPlayer.ControlsTimeout);
player.setState({ controlsTimeout: timeout });
}
togglePlayerControls = () => {
if (this.state.areControlsVisible) {
this.manualHidePlayerControls();
} else {
this.showPlayerControls();
}
}
togglePlay = () => {
this.showPlayerControls();
this.setState({ paused: !this.state.paused });
}
toggleFullscreenMode = () => {
this.showPlayerControls();
const { onFullscreenToggled } = this.props;
this.setState({ fullscreenMode: !this.state.fullscreenMode }, () => {
if (onFullscreenToggled) {
onFullscreenToggled(this.state.fullscreenMode);
}
});
}
onEnd = () => {
this.setState({ paused: true });
if (this.props.onPlaybackFinished) {
this.props.onPlaybackFinished();
}
this.video.seek(0);
}
setSeekerPosition(position = 0) {
position = this.checkSeekerPosition(position);
this.setState({ seekerPosition: position });
if (!this.state.seeking) {
this.setState({ seekerOffset: position });
}
}
checkSeekerPosition(val = 0) {
if (val < 0) {
val = 0;
} else if (val >= this.seekerWidth) {
return this.seekerWidth;
}
return val;
}
seekTo(time = 0) {
if (time > this.state.duration) {
return;
}
this.video.seek(time);
this.setState({ currentTime: time });
}
initSeeker() {
this.seekResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
this.clearControlsTimeout();
if (this.state.seekTimeout > 0) {
clearTimeout(this.state.seekTimeout);
}
this.setState({ seeking: true });
},
onPanResponderMove: (evt, gestureState) => {
const position = this.state.seekerOffset + gestureState.dx;
this.setSeekerPosition(position);
},
onPanResponderRelease: (evt, gestureState) => {
const time = this.getCurrentTimeForSeekerPosition();
if (time >= this.state.duration) {
this.setState({ paused: true });
this.onEnd();
} else {
this.seekTo(time);
this.setState({ seekTimeout: setTimeout(() => { this.setState({ seeking: false }); }, 100) });
}
this.hidePlayerControls();
}
});
}
getTrackingOffset() {
return this.state.fullscreenMode ? this.trackingOffset : 0;
}
getCurrentTimeForSeekerPosition() {
return this.state.duration * (this.state.seekerPosition / this.seekerWidth);
}
calculateSeekerPosition() {
return this.seekerWidth * this.getCurrentTimePercentage();
}
getCurrentTimePercentage() {
if (this.state.currentTime > 0) {
return parseFloat(this.state.currentTime) / parseFloat(this.state.duration);
}
return 0;
};
componentWillMount() {
this.initSeeker();
}
componentDidMount() {
const { assignPlayer } = this.props;
if (assignPlayer) {
assignPlayer(this);
}
this.setSeekerPosition(this.calculateSeekerPosition());
DeviceEventEmitter.addListener('onBackgroundPlayPressed', this.play);
DeviceEventEmitter.addListener('onBackgroundPausePressed', this.pause);
}
componentWillUnmount() {
DeviceEventEmitter.removeListener('onBackgroundPlayPressed', this.play);
DeviceEventEmitter.removeListener('onBackgroundPausePressed', this.pause);
this.clearControlsTimeout();
this.setState({ paused: true, fullscreenMode: false });
const { onFullscreenToggled } = this.props;
if (onFullscreenToggled) {
onFullscreenToggled(false);
}
}
play = () => {
this.setState({ paused: false }, this.updateBackgroundMediaNotification);
}
pause = () => {
this.setState({ paused: true }, this.updateBackgroundMediaNotification);
}
updateBackgroundMediaNotification = () => {
const { backgroundPlayEnabled } = this.props;
if (backgroundPlayEnabled) {
if (NativeModules.BackgroundMedia && window.currentMediaInfo) {
const { title, channel, uri } = window.currentMediaInfo;
NativeModules.BackgroundMedia.showPlaybackNotification(title, channel, uri, this.state.paused);
}
}
}
renderPlayerControls() {
if (this.state.areControlsVisible) {
return (
<View style={mediaPlayerStyle.playerControlsContainer}>
<TouchableOpacity style={mediaPlayerStyle.playPauseButton}
onPress={this.togglePlay}>
{this.state.paused && <Icon name="play" size={32} color="#ffffff" />}
{!this.state.paused && <Icon name="pause" size={32} color="#ffffff" />}
</TouchableOpacity>
<TouchableOpacity style={mediaPlayerStyle.toggleFullscreenButton} onPress={this.toggleFullscreenMode}>
{this.state.fullscreenMode && <Icon name="compress" size={16} color="#ffffff" />}
{!this.state.fullscreenMode && <Icon name="expand" size={16} color="#ffffff" />}
</TouchableOpacity>
<Text style={mediaPlayerStyle.elapsedDuration}>{this.formatTime(this.state.currentTime)}</Text>
<Text style={mediaPlayerStyle.totalDuration}>{this.formatTime(this.state.duration)}</Text>
</View>
);
}
return null;
}
getEncodedDownloadPath = (fileInfo) => {
if (this.state.encodedFilePath) {
return this.state.encodedFilePath;
}
const { file_name: fileName } = fileInfo;
const encodedFileName = encodeURIComponent(fileName).replace(/!/g, '%21');
const encodedFilePath = fileInfo.download_path.replace(fileName, encodedFileName);
this.setState({ encodedFilePath });
return encodedFilePath;
}
onSeekerTouchAreaPressed = (evt) => {
if (evt && evt.nativeEvent) {
const newSeekerPosition = evt.nativeEvent.locationX;
if (!isNaN(newSeekerPosition)) {
const time = this.state.duration * (newSeekerPosition / this.seekerWidth);
this.setSeekerPosition(newSeekerPosition);
this.seekTo(time);
}
}
}
render() {
const { backgroundPlayEnabled, fileInfo, thumbnail, onLayout, style } = this.props;
const completedWidth = this.getCurrentTimePercentage() * this.seekerWidth;
const remainingWidth = this.seekerWidth - completedWidth;
let styles = [this.state.fullscreenMode ? mediaPlayerStyle.fullscreenContainer : mediaPlayerStyle.container];
if (style) {
if (style.length) {
styles = styles.concat(style);
} else {
styles.push(style);
}
}
const trackingStyle = [mediaPlayerStyle.trackingControls, this.state.fullscreenMode ?
mediaPlayerStyle.fullscreenTrackingControls : mediaPlayerStyle.containedTrackingControls];
return (
<View style={styles} onLayout={onLayout}>
<Video source={{ uri: 'file:///' + this.getEncodedDownloadPath(fileInfo) }}
ref={(ref: Video) => { this.video = ref; }}
resizeMode={this.state.resizeMode}
playInBackground={backgroundPlayEnabled}
style={mediaPlayerStyle.player}
rate={this.state.rate}
volume={this.state.volume}
paused={this.state.paused}
onLoad={this.onLoad}
onProgress={this.onProgress}
onEnd={this.onEnd}
/>
{this.state.firstPlay && thumbnail &&
<FastImage
source={{uri: thumbnail}}
resizeMode={FastImage.resizeMode.cover}
style={mediaPlayerStyle.playerThumbnail}
/>}
<TouchableOpacity style={mediaPlayerStyle.playerControls} onPress={this.togglePlayerControls}>
{this.renderPlayerControls()}
</TouchableOpacity>
{(!this.state.fullscreenMode || (this.state.fullscreenMode && this.state.areControlsVisible)) &&
<View style={trackingStyle} onLayout={(evt) => {
this.trackingOffset = evt.nativeEvent.layout.x;
this.seekerWidth = evt.nativeEvent.layout.width;
}}>
<View style={mediaPlayerStyle.progress}>
<View style={[mediaPlayerStyle.innerProgressCompleted, { width: completedWidth }]} />
<View style={[mediaPlayerStyle.innerProgressRemaining, { width: remainingWidth }]} />
</View>
</View>}
{this.state.areControlsVisible &&
<View style={{ left: this.getTrackingOffset(), width: this.seekerWidth }}>
<View style={[mediaPlayerStyle.seekerHandle,
(this.state.fullscreenMode ? mediaPlayerStyle.seekerHandleFs : mediaPlayerStyle.seekerHandleContained),
{ left: this.state.seekerPosition }]} { ...this.seekResponder.panHandlers }>
<View style={this.state.seeking ? mediaPlayerStyle.bigSeekerCircle : mediaPlayerStyle.seekerCircle} />
</View>
<TouchableOpacity
style={[mediaPlayerStyle.seekerTouchArea,
(this.state.fullscreenMode ? mediaPlayerStyle.seekerTouchAreaFs : mediaPlayerStyle.seekerTouchAreaContained)]}
onPress={this.onSeekerTouchAreaPressed} />
</View>}
</View>
);
}
}
export default MediaPlayer;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,28 @@
import { connect } from 'react-redux';
import { doToast } from 'lbry-redux';
import {
doUserPhoneNew,
doUserPhoneVerify,
selectPhoneNewErrorMessage,
selectPhoneNewIsPending,
selectPhoneToVerify,
selectPhoneVerifyIsPending,
selectPhoneVerifyErrorMessage
} from 'lbryinc';
import PhoneNumberRewardSubcard from './view';
const select = state => ({
phoneVerifyErrorMessage: selectPhoneVerifyErrorMessage(state),
phoneVerifyIsPending: selectPhoneVerifyIsPending(state),
phone: selectPhoneToVerify(state),
phoneNewErrorMessage: selectPhoneNewErrorMessage(state),
phoneNewIsPending: selectPhoneNewIsPending(state),
});
const perform = dispatch => ({
addUserPhone: (phone, country_code) => dispatch(doUserPhoneNew(phone, country_code)),
verifyPhone: (verificationCode) => dispatch(doUserPhoneVerify(verificationCode)),
notify: data => dispatch(doToast(data)),
});
export default connect(select, perform)(PhoneNumberRewardSubcard);

View file

@ -0,0 +1,235 @@
// @flow
import React from 'react';
import {
ActivityIndicator,
AsyncStorage,
DeviceEventEmitter,
NativeModules,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import Button from 'component/button';
import Colors from 'styles/colors';
import Constants from 'constants';
import CountryPicker from 'react-native-country-picker-modal';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Link from 'component/link';
import PhoneInput from 'react-native-phone-input';
import rewardStyle from 'styles/reward';
class PhoneNumberRewardSubcard extends React.PureComponent {
phoneInput = null;
picker = null;
constructor(props) {
super(props);
this.state = {
canReceiveSms: false,
cca2: 'US',
codeVerifyStarted: false,
codeVerifySuccessful: false,
countryCode: null,
newPhoneAdded: false,
number: null,
phoneVerifyFailed: false,
verificationCode: null,
};
}
componentDidMount() {
//DeviceEventEmitter.addListener('onReceiveSmsPermissionGranted', this.receiveSmsPermissionGranted);
DeviceEventEmitter.addListener('onVerificationCodeReceived', this.receiveVerificationCode);
const { phone } = this.props;
if (phone && String(phone).trim().length > 0) {
this.setState({ newPhoneAdded: true });
}
/*if (NativeModules.UtilityModule) {
NativeModules.UtilityModule.canReceiveSms().then(canReceiveSms => this.setState({ canReceiveSms }));
}*/
}
componentWillUnmount() {
//DeviceEventEmitter.removeListener('onReceiveSmsPermissionGranted', this.receiveSmsPermissionGranted);
DeviceEventEmitter.removeListener('onVerificationCodeReceived', this.receiveVerificationCode);
}
componentDidUpdate(prevProps) {
const {
phoneVerifyIsPending,
phoneVerifyErrorMessage,
notify,
phoneNewErrorMessage,
phoneNewIsPending,
onPhoneVerifySuccessful
} = this.props;
if (!phoneNewIsPending && (phoneNewIsPending !== prevProps.phoneNewIsPending)) {
if (phoneNewErrorMessage) {
notify({ message: String(phoneNewErrorMessage) });
this.setState({ phoneVerifyFailed: true });
} else {
this.setState({ newPhoneAdded: true, phoneVerifyFailed: false });
}
}
if (!phoneVerifyIsPending && (phoneVerifyIsPending !== prevProps.phoneVerifyIsPending)) {
if (phoneVerifyErrorMessage) {
notify({ message: String(phoneVerifyErrorMessage) });
this.setState({ codeVerifyStarted: false, phoneVerifyFailed: true });
} else {
notify({ message: 'Your phone number was successfully verified.' });
this.setState({ codeVerifySuccessful: true, phoneVerifyFailed: false });
if (onPhoneVerifySuccessful) {
onPhoneVerifySuccessful();
}
}
}
}
receiveVerificationCode = (evt) => {
if (!this.state.newPhoneAdded || this.state.codeVerifySuccessful) {
return;
}
const { verifyPhone } = this.props;
this.setState({ codeVerifyStarted: true });
verifyPhone(evt.code);
}
onSendTextPressed = () => {
const { addUserPhone, notify } = this.props;
if (!this.phoneInput.isValidNumber()) {
return notify({
message: 'Please provide a valid telephone number.',
});
}
this.setState({ phoneVerifyFailed: false });
const countryCode = this.phoneInput.getCountryCode();
const number = this.phoneInput.getValue().replace('+' + countryCode, '');
this.setState({ countryCode, number });
addUserPhone(number, countryCode);
}
onVerifyPressed = () => {
if (this.state.codeVerifyStarted) {
return;
}
const { verifyPhone } = this.props;
this.setState({ codeVerifyStarted: true, phoneVerifyFailed: false });
verifyPhone(this.state.verificationCode);
}
onPressFlag = () => {
if (this.picker) {
this.picker.openModal();
}
}
selectCountry(country) {
this.phoneInput.selectCountry(country.cca2.toLowerCase());
this.setState({ cca2: country.cca2 });
}
handleChangeText = (text) => {
this.setState({ verificationCode: text });
};
render() {
const {
phoneVerifyIsPending,
phoneVerifyErrorMessage,
phone,
phoneErrorMessage,
phoneNewIsPending
} = this.props;
if (this.state.codeVerifySuccessful) {
return null;
}
return (
<View style={rewardStyle.subcard}>
<Text style={rewardStyle.subtitle}>Pending action: Verify Phone Number</Text>
<View style={rewardStyle.phoneVerificationContainer}>
{!this.state.newPhoneAdded &&
<View>
<Text style={[rewardStyle.bottomMarginMedium, rewardStyle.subcardText]}>Please enter your phone number to continue.</Text>
<PhoneInput
ref={(ref) => { this.phoneInput = ref; }}
style={StyleSheet.flatten(rewardStyle.phoneInput)}
textProps={{ placeholder: '(phone number)' }}
textStyle={StyleSheet.flatten(rewardStyle.phoneInputText)}
onPressFlag={this.onPressFlag} />
{!phoneNewIsPending &&
<Button
style={[rewardStyle.actionButton, rewardStyle.topMarginMedium]}
text={"Send verification text"}
onPress={this.onSendTextPressed} />}
{phoneNewIsPending &&
<ActivityIndicator
style={[rewardStyle.loading, rewardStyle.topMarginMedium]}
size="small"
color={Colors.LbryGreen} />}
</View>}
{this.state.newPhoneAdded &&
<View>
{!phoneVerifyIsPending && !this.codeVerifyStarted &&
<View>
<Text style={[rewardStyle.bottomMarginSmall, rewardStyle.subcardText]}>
Please enter the verification code.
</Text>
<TextInput
style={rewardStyle.verificationCodeInput}
keyboardType="numeric"
placeholder="0000"
underlineColorAndroid="transparent"
value={this.state.verificationCode}
onChangeText={text => this.handleChangeText(text)}
/>
<Button
style={[rewardStyle.actionButton, rewardStyle.topMarginSmall ]}
text={"Verify"}
onPress={this.onVerifyPressed} />
</View>
}
{phoneVerifyIsPending &&
<View>
<Text style={rewardStyle.subcardText}>Verifying your phone number...</Text>
<ActivityIndicator
color={Colors.LbryGreen}
size="small"
style={[rewardStyle.loading, rewardStyle.topMarginMedium]} />
</View>}
</View>
}
{this.state.phoneVerifyFailed &&
<View style={rewardStyle.failureFootnote}>
<Text style={rewardStyle.subcardText}>
Sorry, we were unable to verify your phone number. Please go to <Link style={rewardStyle.textLink} href="http://chat.lbry.io" text="chat.lbry.io" /> for manual verification if this keeps happening.
</Text>
</View>}
</View>
<CountryPicker
ref={(picker) => { this.picker = picker; }}
cca2={this.state.cca2}
filterable={true}
onChange={value => this.selectCountry(value)}
showCallingCode={true}
translation="eng">
<View />
</CountryPicker>
</View>
);
}
};
export default PhoneNumberRewardSubcard;

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,17 @@
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,71 @@
import React from 'react';
import { AsyncStorage, NativeModules, Text, TouchableOpacity, View } from 'react-native';
import Button from '../../component/button';
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.',
});
}
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={() => {
navigation.navigate('Rewards');
}}>
<Text style={rewardStyle.summaryText}>
You have {unclaimedRewardAmount} LBC in unclaimed rewards. You have {this.state.actionsLeft} action{this.state.actionsLeft === 1 ? '' : 's'} left to claim your first reward. Tap here to continue.
</Text>
<Button style={rewardStyle.dismissButton} theme={"light"} text={"Dismiss"} onPress={this.onDismissPressed} />
</TouchableOpacity>
);
}
}
export default RewardSummary;

View file

@ -0,0 +1,16 @@
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.Mixpanel) {
NativeModules.Mixpanel.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,40 @@
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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,22 @@
import { connect } from 'react-redux';
import {
doToast,
doSendDraftTransaction,
selectDraftTransaction,
selectDraftTransactionError,
selectBalance
} from 'lbry-redux';
import WalletSend from './view';
const select = state => ({
balance: selectBalance(state),
draftTransaction: selectDraftTransaction(state),
transactionError: selectDraftTransactionError(state),
});
const perform = dispatch => ({
sendToAddress: (address, amount) => dispatch(doSendDraftTransaction(address, amount)),
notify: (data) => dispatch(doToast(data))
});
export default connect(select, perform)(WalletSend);

View file

@ -0,0 +1,125 @@
// @flow
import React from 'react';
import { regexAddress } from 'lbry-redux';
import { Alert, TextInput, Text, View } from 'react-native';
import Button from '../button';
import walletStyle from '../../styles/wallet';
type DraftTransaction = {
address: string,
amount: ?number, // So we can use a placeholder in the input
};
type Props = {
sendToAddress: (string, number) => void,
balance: number,
};
class WalletSend extends React.PureComponent<Props> {
amountInput = null;
state = {
amount: null,
address: null,
addressChanged: false,
addressValid: false
};
componentWillUpdate(nextProps) {
const { draftTransaction, transactionError } = nextProps;
if (transactionError && transactionError.trim().length > 0) {
this.setState({ address: draftTransaction.address, amount: draftTransaction.amount });
}
}
handleSend = () => {
const { balance, sendToAddress, notify } = this.props;
const { address, amount } = this.state;
if (address && !regexAddress.test(address)) {
notify({
message: 'The recipient address is not a valid LBRY address.',
});
return;
}
if (amount > balance) {
notify({
message: 'Insufficient credits',
});
return;
}
if (amount && address) {
// Show confirmation before send
Alert.alert(
'Send LBC',
`Are you sure you want to send ${amount} LBC to ${address}?`,
[
{ text: 'No' },
{ text: 'Yes', onPress: () => {
sendToAddress(address, parseFloat(amount));
this.setState({ address: null, amount: null });
}}
]);
}
}
handleAddressInputBlur = () => {
if (this.state.addressChanged && !this.state.addressValid) {
const { notify } = this.props;
notify({
message: 'The recipient address is not a valid LBRY address.',
});
}
}
handleAddressInputSubmit = () => {
if (this.amountInput) {
this.amountInput.focus();
}
}
render() {
const { balance } = this.props;
const canSend = this.state.address &&
this.state.amount > 0 &&
this.state.address.trim().length > 0;
return (
<View style={walletStyle.card}>
<Text style={walletStyle.title}>Send Credits</Text>
<Text style={walletStyle.text}>Recipient address</Text>
<View style={[walletStyle.row, walletStyle.bottomMarginMedium]}>
<TextInput onChangeText={value => this.setState({
address: value,
addressChanged: true,
addressValid: (value.trim().length == 0 || regexAddress.test(value))
})}
onBlur={this.handleAddressInputBlur}
onSubmitEditing={this.handleAddressInputSubmit}
placeholder={'bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs'}
value={this.state.address}
returnKeyType={'next'}
style={[walletStyle.input, walletStyle.addressInput, walletStyle.bottomMarginMedium]} />
</View>
<Text style={walletStyle.text}>Amount</Text>
<View style={walletStyle.row}>
<View style={walletStyle.amountRow}>
<TextInput ref={ref => this.amountInput = ref}
onChangeText={value => this.setState({amount: value})}
keyboardType={'numeric'}
value={this.state.amount}
style={[walletStyle.input, walletStyle.amountInput]} />
<Text style={[walletStyle.text, walletStyle.currency]}>LBC</Text>
</View>
<Button text={'Send'}
style={[walletStyle.button, walletStyle.sendButton]}
disabled={!canSend}
onPress={this.handleSend} />
</View>
</View>
);
}
}
export default WalletSend;

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