Seed Support #56

Closed
ocnios wants to merge 173 commits from master into build
100 changed files with 4694 additions and 3423 deletions

24
.appveyor.yml Normal file
View file

@ -0,0 +1,24 @@
# Test against the latest version of this Node.js version
environment:
nodejs_version: 7
GH_TOKEN:
secure: LiI5jyuHUw6XbH4kC3gP1HX4P/v4rwD/gCNtaFhQu2AvJz1/1wALkp5ECnIxRySN
key_pass:
secure: u6DydPcdrUJlxGL9uc7yQRYG8+5rY6aAEE9nfCSzFyNzZlX9NniOp8Uh5ZKQqX7bGEngLI6ipbLfiJvn0XFnhbn2iTkOuMqOXVJVOehvwlQ=
pfx_key:
secure: 1mwqyRy7hDqDjDK+TIAoaXyXzpNgwruFNA6TPkinUcVM7A+NLD33RQLnfnwVy+R5ovD2pUfhQ6+N0Fqebv6tZh436LIEsock+6IOdpgFwrg=
# find with: Get-Childitem Path "C:\Program Files (x86)\Microsoft SDKs\Windows\" -Include *signtool* -File -Recurse -ErrorAction SilentlyContinue
SIGNTOOL_PATH: C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\signtool.exe
skip_branch_with_pr: true
clone_folder: C:\projects\lbry-app
build_script:
- ps: build\build.ps1
test: off
artifacts:
- path: dist\*.exe
name: LBRY

View file

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.9.2rc15 current_version = 0.10.0
commit = True commit = True
tag = True tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((?P<release>[a-z]+)(?P<candidate>\d+))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((?P<release>[a-z]+)(?P<candidate>\d+))?

1
.gitignore vendored
View file

@ -6,6 +6,7 @@ dist
/app/node_modules /app/node_modules
/build/venv /build/venv
/lbry-app-venv /lbry-app-venv
/lbry-venv
/daemon/build /daemon/build
/daemon/venv /daemon/venv
/daemon/requirements.txt /daemon/requirements.txt

6
.gitmodules vendored
View file

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

View file

@ -8,6 +8,29 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased] ## [Unreleased]
### Added ### Added
*
*
### Changed
*
*
### Fixed
* Error modals now display full screen properly
*
### Deprecated
*
*
### Removed
*
*
## [0.10.0] - 2017-05-04
### Added
* The UI has been overhauled to use an omnibar and drop the sidebar.
* The app is much more responsive switching pages. It no longer reloads the entire page and all assets on each page change. * The app is much more responsive switching pages. It no longer reloads the entire page and all assets on each page change.
* lbry.js now offers a subscription model for wallet balance similar to file info. * lbry.js now offers a subscription model for wallet balance similar to file info.
* Fixed file info subscribes not being unsubscribed in unmount. * Fixed file info subscribes not being unsubscribed in unmount.
@ -16,16 +39,24 @@ Web UI version numbers should always match the corresponding version of LBRY App
* New-style API calls return promises instead of using callbacks * New-style API calls return promises instead of using callbacks
* Wherever possible, use outpoints for unique IDs instead of names or SD hashes * Wherever possible, use outpoints for unique IDs instead of names or SD hashes
* New publishes now display immediately in My Files, even before they hit the lbrynet file manager. * New publishes now display immediately in My Files, even before they hit the lbrynet file manager.
* New welcome flow for new users
* Redesigned UI for Discover
* Handle more of price calculations at the daemon layer to improve page load time
* Add special support for building channel claims in lbryuri module
* Enable windows code signing of binary
### Changed ### Changed
* Update process now easier and more reliable * Update process now easier and more reliable
* Updated search to be compatible with new Lighthouse servers
* Cleaned up shutdown logic * Cleaned up shutdown logic
* * Support lbry v0.10 API signatures
### Fixed ### Fixed
* Fix Watch page and progress bars for new API changes * Fix Watch page and progress bars for new API changes
*
*
## [0.9.0rc15] - 2017-03-09 ## [0.9.0rc15] - 2017-03-09
### Added ### Added

View file

@ -45,4 +45,4 @@ to create distributable packages, which is run by calling:
### Development on Windows ### Development on Windows
This project has currently only been worked on in Linux and macOS. If you are on Windows, you can This project has currently only been worked on in Linux and macOS. If you are on Windows, you can
checkout out the build steps in [appveyor.yml](https://github.com/lbryio/lbry-app/blob/master/appveyor.yml) and probably figure out something from there. checkout out the build steps in [appveyor.yml](https://github.com/lbryio/lbry-app/blob/master/.appveyor.yml) and probably figure out something from there.

View file

@ -1,11 +1,18 @@
const {app, BrowserWindow, ipcMain} = require('electron'); const {app, BrowserWindow, ipcMain} = require('electron');
const url = require('url');
const path = require('path'); const path = require('path');
const jayson = require('jayson'); const jayson = require('jayson');
const semver = require('semver');
const https = require('https');
// tree-kill has better cross-platform handling of // tree-kill has better cross-platform handling of
// killing a process. child-process.kill was unreliable // killing a process. child-process.kill was unreliable
const kill = require('tree-kill'); const kill = require('tree-kill');
const child_process = require('child_process'); const child_process = require('child_process');
const assert = require('assert'); const assert = require('assert');
const {version: localVersion} = require(app.getAppPath() + '/package.json');
const VERSION_CHECK_INTERVAL = 30 * 60 * 1000;
const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest';
let client = jayson.client.http('http://localhost:5279/lbryapi'); let client = jayson.client.http('http://localhost:5279/lbryapi');
@ -17,12 +24,58 @@ let daemonSubprocess;
// This is set to true right before we try to shut the daemon subprocess -- // This is set to true right before we try to shut the daemon subprocess --
// if it dies when we didn't ask it to shut down, we want to alert the user. // if it dies when we didn't ask it to shut down, we want to alert the user.
let daemonSubprocessKillRequested = false; let daemonStopRequested = false;
// When a quit is attempted, we cancel the quit, do some preparations, then // When a quit is attempted, we cancel the quit, do some preparations, then
// this is set to true and app.quit() is called again to quit for real. // this is set to true and app.quit() is called again to quit for real.
let readyToQuit = false; let readyToQuit = false;
function checkForNewVersion(callback) {
function formatRc(ver) {
// Adds dash if needed to make RC suffix semver friendly
return ver.replace(/([^-])rc/, '$1-rc');
}
let result = '';
const opts = {
headers: {
'User-Agent': `LBRY/${localVersion}`,
}
};
const req = https.get(Object.assign(opts, url.parse(LATEST_RELEASE_API_URL)), (res) => {
res.on('data', (data) => {
result += data;
});
res.on('end', () => {
console.log('Local version:', localVersion);
const tagName = JSON.parse(result).tag_name;
const [_, remoteVersion] = tagName.match(/^v([\d.]+(?:-?rc\d+)?)$/);
if (!remoteVersion) {
console.log('Malformed remote version string:', tagName);
if (win) {
win.webContents.send('version-info-received', null);
}
} else {
console.log('Remote version:', remoteVersion);
const upgradeAvailable = semver.gt(formatRc(remoteVersion), formatRc(localVersion));
console.log(upgradeAvailable ? 'Upgrade available' : 'No upgrade available');
if (win) {
win.webContents.send('version-info-received', {remoteVersion, localVersion, upgradeAvailable});
}
}
})
});
req.on('error', (err) => {
console.log('Failed to get current version from GitHub. Error:', err);
if (win) {
win.webContents.send('version-info-received', null);
}
});
}
ipcMain.on('version-info-requested', checkForNewVersion);
/* /*
* Replacement for Electron's shell.openItem. The Electron version doesn't * Replacement for Electron's shell.openItem. The Electron version doesn't
* reliably work from the main process, and we need to be able to run it * reliably work from the main process, and we need to be able to run it
@ -62,9 +115,9 @@ function getPidsForProcessName(name) {
} }
function createWindow () { function createWindow () {
win = new BrowserWindow({backgroundColor: '#155B4A'}) //$color-primary win = new BrowserWindow({backgroundColor: '#155B4A', minWidth: 800, minHeight: 600 }) //$color-primary
win.maximize() win.maximize()
//win.webContents.openDevTools() // win.webContents.openDevTools();
win.loadURL(`file://${__dirname}/dist/index.html`) win.loadURL(`file://${__dirname}/dist/index.html`)
win.on('closed', () => { win.on('closed', () => {
win = null win = null
@ -74,8 +127,8 @@ function createWindow () {
function handleDaemonSubprocessExited() { function handleDaemonSubprocessExited() {
console.log('The daemon has exited.'); console.log('The daemon has exited.');
daemonSubprocess = null; daemonSubprocess = null;
if (!daemonSubprocessKillRequested) { if (!daemonStopRequested) {
// We didn't stop the daemon subprocess on purpose, so display a // We didn't request to stop the daemon, so display a
// warning and schedule a quit. // warning and schedule a quit.
// //
// TODO: maybe it would be better to restart the daemon? // TODO: maybe it would be better to restart the daemon?
@ -107,7 +160,6 @@ function launchDaemon() {
daemonSubprocess.stdout.on('data', (buf) => {console.log(String(buf).trim());}); daemonSubprocess.stdout.on('data', (buf) => {console.log(String(buf).trim());});
daemonSubprocess.stderr.on('data', (buf) => {console.log(String(buf).trim());}); daemonSubprocess.stderr.on('data', (buf) => {console.log(String(buf).trim());});
daemonSubprocess.on('exit', handleDaemonSubprocessExited); daemonSubprocess.on('exit', handleDaemonSubprocessExited);
console.log('lbrynet daemon has launched')
} }
/* /*
@ -209,31 +261,27 @@ app.on('activate', () => {
// When a quit is attempted, this is called. It attempts to shutdown the daemon, // When a quit is attempted, this is called. It attempts to shutdown the daemon,
// then calls quitNow() to quit for real. // then calls quitNow() to quit for real.
function shutdownDaemonAndQuit(evenIfNotStartedByApp = false) { function shutdownDaemonAndQuit(evenIfNotStartedByApp = false) {
if (daemonSubprocess) { function doShutdown() {
console.log('Killing lbrynet-daemon process'); console.log('Asking daemon to shut down down');
daemonSubprocessKillRequested = true; daemonStopRequested = true;
kill(daemonSubprocess.pid, undefined, (err) => {
console.log('Killed lbrynet-daemon process');
quitNow();
});
} else if (evenIfNotStartedByApp) {
console.log('Stopping lbrynet-daemon, even though app did not start it');
client.request('daemon_stop', [], (err, res) => { client.request('daemon_stop', [], (err, res) => {
if (err) { if (err) {
// We could get an error because the daemon is already stopped (good) console.log(`received error when stopping lbrynet-daemon. Error message: ${err.message}\n`);
// or because it's running but not responding properly (bad). console.log('You will need to manually kill the daemon.');
// So try to force kill any daemons that are still running.
console.log(`received error when stopping lbrynet-daemon. Error message: ${err.message}`);
forceKillAllDaemonsAndQuit();
} else { } else {
console.log('Successfully stopped daemon via RPC call.') console.log('Successfully stopped daemon via RPC call.')
quitNow(); quitNow();
} }
}); });
} else { }
if (daemonSubprocess) {
doShutdown();
} else if (!evenIfNotStartedByApp) {
console.log('Not killing lbrynet-daemon because app did not start it'); console.log('Not killing lbrynet-daemon because app did not start it');
quitNow(); quitNow();
} else {
doShutdown();
} }
// Is it safe to start the installer before the daemon finishes running? // Is it safe to start the installer before the daemon finishes running?

View file

@ -24,6 +24,9 @@ const baseTemplate = [
{ {
role: 'paste', role: 'paste',
}, },
{
role: 'selectall',
},
] ]
}, },
{ {

View file

@ -1,6 +1,6 @@
{ {
"name": "LBRY", "name": "LBRY",
"version": "0.9.2rc15", "version": "0.10.0",
"main": "main.js", "main": "main.js",
"description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.",
"author": { "author": {
@ -12,6 +12,7 @@
"install": "^0.8.7", "install": "^0.8.7",
"jayson": "^2.0.2", "jayson": "^2.0.2",
"npm": "^4.2.0", "npm": "^4.2.0",
"semver": "^5.3.0",
"tree-kill": "^1.1.0" "tree-kill": "^1.1.0"
} }
} }

View file

@ -1,55 +0,0 @@
# Test against the latest version of this Node.js version
environment:
nodejs_version: "6"
GH_TOKEN:
secure: LiI5jyuHUw6XbH4kC3gP1HX4P/v4rwD/gCNtaFhQu2AvJz1/1wALkp5ECnIxRySN
skip_branch_with_pr: true
clone_folder: C:\projects\lbry-electron
# Install scripts. (runs after repo cloning)
install:
# needed to deal with submodules
- git submodule update --init --recursive
- python build\set_version.py
- python build\set_build.py
# Get the latest stable version of Node.js or io.js
- ps: Install-Product node $env:nodejs_version
# install modules
- npm install
- cd app
- npm install
- cd ..
# create daemon and cli executable
- cd daemon
- ps: .\build.ps1
- cd ..
# build ui
- cd ui
- npm install
- node_modules\.bin\node-sass --output dist\css --sourcemap=none scss\
- node_modules\.bin\webpack
- ps: Copy-Item dist ..\app\ -recurse
- cd ..
# copy executables into ui
- ps: Copy-Item daemon\dist\lbrynet-daemon.exe app\dist
- ps: Copy-Item daemon\dist\lbrynet-cli.exe app\dist
build_script:
# build electron app
- node_modules\.bin\build -p never
# for debugging, see what was built
- python build\zip_daemon.py
- dir dist
- pip install -r build\requirements.txt
- python build\release_on_tag.py
test: off
artifacts:
- path: dist\*.exe
name: LBRY
- path: dist\*.zip
name: lbrynet-daemon

1
build/DAEMON_URL Normal file
View file

@ -0,0 +1 @@
https://github.com/lbryio/lbry/releases/download/v0.10.3rc1/lbrynet-daemon-v0.10.3rc1-OSNAME.zip

40
build/build.ps1 Normal file
View file

@ -0,0 +1,40 @@
pip install -r build\requirements.txt
python build\set_version.py
# Get the latest stable version of Node.js or io.js
Install-Product node $env:nodejs_version
# install node modules
npm install
cd app
npm install
cd ..
# build ui
cd ui
npm install
node_modules\.bin\node-sass --output dist\css --sourcemap=none scss\
node_modules\.bin\webpack
Copy-Item dist ..\app\ -recurse
cd ..
# get daemon and cli executable
$daemon_url = (Get-Content build\DAEMON_URL -Raw).replace("OSNAME", "windows")
Invoke-WebRequest -Uri $daemon_url -OutFile daemon.zip
Expand-Archive daemon.zip -DestinationPath app\dist\
dir app\dist\ # verify that daemon binary is there
rm daemon.zip
# build electron app
node_modules\.bin\build -p never
$binary_name = Get-ChildItem -Path dist -Filter '*.exe' -Name
$new_name = $binary_name -replace '^LBRY Setup (.*)\.exe$', 'LBRY_$1.exe'
Rename-Item -Path "dist\$binary_name" -NewName $new_name
dir dist # verify that binary was built/named correctly
# sign binary
nuget install secure-file -ExcludeVersion
secure-file\tools\secure-file -decrypt build\lbry2.pfx.enc -secret "$env:pfx_key"
& ${env:SIGNTOOL_PATH} sign /f build\lbry2.pfx /p "$env:key_pass" /tr http://tsa.starfieldtech.com /td SHA256 /fd SHA256 dist\*.exe
python build\release_on_tag.py

View file

@ -7,7 +7,18 @@ ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
cd "$ROOT" cd "$ROOT"
BUILD_DIR="$ROOT/build" BUILD_DIR="$ROOT/build"
LINUX=false
OSX=false
if [ "$(uname)" == "Darwin" ]; then if [ "$(uname)" == "Darwin" ]; then
OSX=true
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
LINUX=true
else
echo "Platform detection failed"
exit 1
fi
if $OSX; then
ICON="$BUILD_DIR/icon.icns" ICON="$BUILD_DIR/icon.icns"
else else
ICON="$BUILD_DIR/icons/lbry48.png" ICON="$BUILD_DIR/icons/lbry48.png"
@ -32,7 +43,6 @@ if [ "$FULL_BUILD" == "true" ]; then
set -u set -u
pip install -r "$BUILD_DIR/requirements.txt" pip install -r "$BUILD_DIR/requirements.txt"
python "$BUILD_DIR/set_version.py" python "$BUILD_DIR/set_version.py"
python "$BUILD_DIR/set_build.py"
fi fi
[ -d "$ROOT/dist" ] && rm -rf "$ROOT/dist" [ -d "$ROOT/dist" ] && rm -rf "$ROOT/dist"
@ -62,24 +72,15 @@ npm install
# daemon and cli # # daemon and cli #
#################### ####################
( if $OSX; then
cd "$ROOT/daemon" OSNAME="macos"
else
# copy requirements from lbry, but remove lbryum (we'll add it back in below) OSNAME="linux"
grep -v lbryum "$ROOT/lbry/requirements.txt" > requirements.txt fi
# for electron, we install lbryum and lbry using submodules DAEMON_URL="$(cat "$BUILD_DIR/DAEMON_URL" | sed "s/OSNAME/${OSNAME}/")"
echo "../lbryum" >> requirements.txt wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip"
echo "../lbry" >> requirements.txt unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/"
# also add pyinstaller rm "$BUILD_DIR/daemon.zip"
echo "PyInstaller==3.2.1" >> requirements.txt
pip install -r requirements.txt
pyinstaller -y daemon.onefile.spec
pyinstaller -y cli.onefile.spec
mv dist/lbrynet-daemon dist/lbrynet-cli "$ROOT/app/dist/"
)
python "$BUILD_DIR/zip_daemon.py"
################### ###################
# Build the app # # Build the app #
@ -91,12 +92,18 @@ python "$BUILD_DIR/zip_daemon.py"
) )
if [ "$FULL_BUILD" == "true" ]; then if [ "$FULL_BUILD" == "true" ]; then
if [ "$(uname)" == "Darwin" ]; then if $OSX; then
security unlock-keychain -p ${KEYCHAIN_PASSWORD} osx-build.keychain security unlock-keychain -p ${KEYCHAIN_PASSWORD} osx-build.keychain
fi fi
node_modules/.bin/build -p never node_modules/.bin/build -p never
if $OSX; then
binary_name=$(find "$ROOT/dist" -iname "*dmg")
new_name=$(basename "$binary_name" | sed 's/-/_/')
mv "$binary_name" "$(dirname "$binary_name")/$new_name"
fi
# electron-build has a publish feature, but I had a hard time getting # electron-build has a publish feature, but I had a hard time getting
# it to reliably work and it also seemed difficult to configure. Not proud of # it to reliably work and it also seemed difficult to configure. Not proud of
# this, but it seemed better to write my own. # this, but it seemed better to write my own.

View file

@ -1,8 +1,5 @@
import argparse
import datetime import datetime
import re import re
import sys
CHANGELOG_START_RE = re.compile(r'^\#\# \[Unreleased\]') CHANGELOG_START_RE = re.compile(r'^\#\# \[Unreleased\]')
CHANGELOG_END_RE = re.compile(r'^\#\# \[.*\] - \d{4}-\d{2}-\d{2}') CHANGELOG_END_RE = re.compile(r'^\#\# \[.*\] - \d{4}-\d{2}-\d{2}')
@ -14,84 +11,67 @@ EMPTY_RE = re.compile(r'^\w*\*\w*$')
ENTRY_RE = re.compile(r'\* (.*)') ENTRY_RE = re.compile(r'\* (.*)')
VALID_SECTIONS = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security'] VALID_SECTIONS = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security']
# allocate some entries to cut-down on merge conflicts # allocate some entries to cut-down on merge conflicts
TEMPLATE = """### Added TEMPLATE = """### Added
* *
* *
*
### Changed ### Changed
* *
* *
*
### Fixed ### Fixed
* *
* *
### Deprecated
*
*
### Removed
*
* *
""" """
def main(): class Changelog(object):
print "i am broken" def __init__(self, path):
return 1 self.path = path
parser = argparse.ArgumentParser() self.start = []
parser.add_argument('changelog') self.unreleased = []
parser.add_argument('version') self.rest = []
args = parser.parse_args() self._parse()
bump(changelog, version)
def _parse(self):
def bump(changelog, version): with open(self.path) as fp:
with open(changelog) as fp:
lines = fp.readlines() lines = fp.readlines()
start = []
unreleased = []
rest = []
unreleased_start_found = False unreleased_start_found = False
unreleased_end_found = False unreleased_end_found = False
for line in lines: for line in lines:
if not unreleased_start_found: if not unreleased_start_found:
start.append(line) self.start.append(line)
if CHANGELOG_START_RE.search(line): if CHANGELOG_START_RE.search(line):
unreleased_start_found = True unreleased_start_found = True
continue continue
if unreleased_end_found: if unreleased_end_found:
rest.append(line) self.rest.append(line)
continue continue
if CHANGELOG_END_RE.search(line): if CHANGELOG_END_RE.search(line):
rest.append(line) self.rest.append(line)
unreleased_end_found = True unreleased_end_found = True
continue continue
if CHANGELOG_ERROR_RE.search(line): if CHANGELOG_ERROR_RE.search(line):
raise Exception( raise Exception(
'Failed to parse {}: {}'.format(changelog, 'unexpected section header found')) 'Failed to parse {}: {}'.format(self.path, 'unexpected section header found'))
unreleased.append(line) self.unreleased.append(line)
today = datetime.datetime.today() self.unreleased = self._normalize_section(self.unreleased)
header = '## [{}] - {}\n'.format(version, today.strftime('%Y-%m-%d'))
released = normalize(unreleased)
if not released:
# If we don't have anything in the Unreleased section, then leave the
# changelog as it is and return None
return
changelog_data = ( @staticmethod
''.join(start) + def _normalize_section(lines):
TEMPLATE +
header +
'\n'.join(released) + '\n\n'
+ ''.join(rest)
)
with open(changelog, 'w') as fp:
fp.write(changelog_data)
return '\n'.join(released) + '\n\n'
def normalize(lines):
"""Parse a changelog entry and output a normalized form""" """Parse a changelog entry and output a normalized form"""
sections = {} sections = {}
current_section_name = None current_section_name = None
@ -124,8 +104,26 @@ def normalize(lines):
output.append('### {}'.format(section)) output.append('### {}'.format(section))
for entry in sections[section]: for entry in sections[section]:
output.append(' * {}'.format(entry)) output.append(' * {}'.format(entry))
output.append("\n")
return output return output
def get_unreleased(self):
return '\n'.join(self.unreleased) if self.unreleased else None
if __name__ == '__main__': def bump(self, version):
sys.exit(main()) if not self.unreleased:
return
today = datetime.datetime.today()
header = "## [{}] - {}\n\n".format(version, today.strftime('%Y-%m-%d'))
changelog_data = (
''.join(self.start) +
TEMPLATE +
header +
'\n'.join(self.unreleased) + '\n\n'
+ ''.join(self.rest)
)
with open(self.path, 'w') as fp:
fp.write(changelog_data)

View file

@ -1,13 +0,0 @@
#!/bin/bash
# https://github.com/lbryio/lbry-app/commit/4386102ba3bf8c731a075797756111d73c31a47a
# https://github.com/lbryio/lbry-app/commit/a3a376922298b94615f7514ca59988b73a522f7f
# Appveyor and Teamcity struggle with SSH urls in submodules, so we use HTTPS
# But locally, SSH urls are way better since they dont require a password
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "DIR"
git config submodule.lbry.url git@github.com:lbryio/lbry.git
git config submodule.lbryum.url git@github.com:lbryio/lbryum.git

BIN
build/lbry2.pfx.enc Normal file

Binary file not shown.

View file

@ -72,7 +72,6 @@ if ! cmd_exists pip; then
fi fi
if $LINUX && [ "$(pip list --format=columns | grep setuptools | wc -l)" -ge 1 ]; then if $LINUX && [ "$(pip list --format=columns | grep setuptools | wc -l)" -ge 1 ]; then
#$INSTALL python-setuptools
$SUDO pip install setuptools $SUDO pip install setuptools
fi fi
@ -88,3 +87,14 @@ if ! cmd_exists node; then
brew install node brew install node
fi fi
fi fi
if ! cmd_exists unzip; then
if $LINUX; then
$INSTALL unzip
elif $OSX; then
echo "unzip required"
exit 1
# not sure this works, but OSX should come with unzip
# brew install unzip
fi
fi

View file

@ -1,13 +1,12 @@
"""Trigger a release. """Bump version and create Github release
This script is to be run locally (not on a build server). This script should be run locally, not on a build server.
""" """
import argparse import argparse
import contextlib import contextlib
import logging
import os import os
import re import re
import string import requests
import subprocess import subprocess
import sys import sys
@ -16,122 +15,137 @@ import github
import changelog import changelog
# TODO: ask bumpversion for these ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
LBRY_PARTS = ('major', 'minor', 'patch', 'release', 'candidate') DAEMON_URL_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'DAEMON_URL')
LBRYUM_PARTS = ('major', 'minor', 'patch')
def main(): def main():
parser = argparse.ArgumentParser() bumpversion_parts = get_bumpversion_parts()
parser.add_argument(
"lbry_part", help="part of lbry version to bump",
choices=LBRY_PARTS
)
parser.add_argument(
"--skip-lbryum", help="skip bumping lbryum, even if there are changes",
action="store_true",
)
parser.add_argument(
"--lbryum-part", help="part of lbryum version to bump",
choices=LBRYUM_PARTS
)
parser.add_argument(
"--last-release",
help=("manually set the last release version. The default is to query and parse the"
" value from the release page.")
)
parser.add_argument(
"--skip-sanity-checks", action="store_true")
parser.add_argument(
"--require-changelog", action="store_true",
help=("Set this flag to raise an exception if a submodules has changes without a"
" corresponding changelog entry. The default is to log a warning")
)
parser.add_argument(
"--skip-push", action="store_true",
help="Set to not push changes to remote repo"
)
parser = argparse.ArgumentParser()
parser.add_argument("part", choices=bumpversion_parts, help="part of version to bump")
parser.add_argument("--skip-sanity-checks", action="store_true")
parser.add_argument("--skip-push", action="store_true")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--confirm", action="store_true")
args = parser.parse_args() args = parser.parse_args()
base = git.Repo(os.getcwd()) if args.dry_run:
print "DRY RUN. Nothing will be committed/pushed."
repo = Repo('lbry-app', args.part, ROOT)
branch = 'master' branch = 'master'
print 'Current version: {}'.format(repo.current_version)
print 'New version: {}'.format(repo.new_version)
with open(DAEMON_URL_FILE, 'r') as f:
daemon_url_template = f.read().strip()
daemon_version = re.search('/(?P<version>v[^/]+)', daemon_url_template)
print 'Daemon version: {} ({})'.format(
daemon_version.group('version'), daemon_url_template)
if not args.confirm and not confirm():
print "Aborting"
return 1
if not args.skip_sanity_checks: if not args.skip_sanity_checks:
run_sanity_checks(base, branch) run_sanity_checks(repo, branch)
repo.assert_new_tag_is_absent()
base_repo = Repo('lbry-app', args.lbry_part, os.getcwd()) is_rc = re.search('\drc\d+$', repo.new_version) is not None
base_repo.assert_new_tag_is_absent() # only have a release message for real releases, not for RCs
release_msg = '' if is_rc else repo.get_unreleased_changelog()
last_release = args.last_release or base_repo.get_last_tag() if args.dry_run:
logging.info('Last release: %s', last_release) print "rc: " + ("yes" if is_rc else "no")
print "release message: \n" + (release_msg or " NO MESSAGE FOR RCs")
return
gh_token = get_gh_token() gh_token = get_gh_token()
auth = github.Github(gh_token) auth = github.Github(gh_token)
github_repo = auth.get_repo('lbryio/lbry-app') github_repo = auth.get_repo('lbryio/lbry-app')
names = ['lbryum', 'lbry'] if not is_rc:
repos = {name: Repo(name, get_part(args, name)) for name in names} repo.bump_changelog()
changelogs = {}
for repo in repos.values():
logging.info('Processing repo: %s', repo.name)
repo.checkout(branch)
last_submodule_hash = base_repo.get_submodule_hash(last_release, repo.name)
if repo.has_changes_from_revision(last_submodule_hash):
if repo.name == 'lbryum':
if args.skip_lbryum:
continue
if not repo.part:
repo.part = get_lbryum_part()
entry = repo.get_changelog_entry()
if entry:
changelogs[repo.name] = entry.strip()
repo.add_changelog()
else:
msg = 'Changelog entry is missing for {}'.format(repo.name)
if args.require_changelog:
raise Exception(msg)
else:
logging.warning(msg)
else:
logging.warning('Submodule %s has no changes.', repo.name)
if repo.name == 'lbryum':
# The other repos have their version track each other so need to bump
# them even if there aren't any changes, but lbryum should only be
# bumped if it has changes
continue
# bumpversion will fail if there is already the tag we want in the repo
repo.assert_new_tag_is_absent()
repo.bumpversion() repo.bumpversion()
release_msg = get_release_msg(changelogs, names) new_tag = repo.get_new_tag()
github_repo.create_git_release(new_tag, new_tag, release_msg, draft=True, prerelease=is_rc)
for name in names: if args.skip_push:
base.git.add(name) print (
'Skipping push; you will have to reset and delete tags if '
base_repo.bumpversion() 'you want to run this script again.'
current_tag = base.git.describe() )
is_rc = re.match('\drc\d+$', current_tag) is not None
github_repo.create_git_release(current_tag, current_tag, release_msg, draft=True,
prerelease=is_rc)
no_change_msg = ('No change since the last release. This release is simply a placeholder'
' so that LBRY and LBRY App track the same version')
lbrynet_daemon_release_msg = changelogs.get('lbry', no_change_msg)
auth.get_repo('lbryio/lbry').create_git_release(
current_tag, current_tag, lbrynet_daemon_release_msg, draft=True)
if not args.skip_push:
for repo in repos.values():
repo.git.push(follow_tags=True)
base.git.push(follow_tags=True, recurse_submodules='check')
else: else:
logging.info('Skipping push; you will have to reset and delete tags if ' repo.git_repo.git.push(follow_tags=True, recurse_submodules='check')
'you want to run this script again. Take a look at reset.sh; '
'it probably does what you want.')
class Repo(object):
def __init__(self, name, part, directory):
self.name = name
self.part = part
if not self.part:
raise Exception('Part required')
self.directory = directory
self.git_repo = git.Repo(self.directory)
self._bumped = False
self.current_version = self._get_current_version()
self.new_version = self._get_new_version()
self._changelog = changelog.Changelog(os.path.join(self.directory, 'CHANGELOG.md'))
def get_new_tag(self):
return 'v' + self.new_version
def get_unreleased_changelog(self):
return self._changelog.get_unreleased()
def bump_changelog(self):
self._changelog.bump(self.new_version)
with pushd(self.directory):
self.git_repo.git.add(os.path.basename(self._changelog.path))
def _get_current_version(self):
with pushd(self.directory):
output = subprocess.check_output(
['bumpversion', '--dry-run', '--list', '--allow-dirty', self.part])
return re.search('^current_version=(.*)$', output, re.M).group(1)
def _get_new_version(self):
with pushd(self.directory):
output = subprocess.check_output(
['bumpversion', '--dry-run', '--list', '--allow-dirty', self.part])
return re.search('^new_version=(.*)$', output, re.M).group(1)
def bumpversion(self):
if self._bumped:
raise Exception('Cowardly refusing to bump a repo twice')
with pushd(self.directory):
subprocess.check_call(['bumpversion', '--allow-dirty', self.part])
self._bumped = True
def assert_new_tag_is_absent(self):
new_tag = self.get_new_tag()
tags = self.git_repo.git.tag()
if new_tag in tags.split('\n'):
raise Exception('Tag {} is already present in repo {}.'.format(new_tag, self.name))
def is_behind(self, branch):
self.git_repo.remotes.origin.fetch()
rev_list = '{branch}...origin/{branch}'.format(branch=branch)
commits_behind = self.git_repo.git.rev_list(rev_list, right_only=True, count=True)
commits_behind = int(commits_behind)
return commits_behind > 0
def get_bumpversion_parts():
with pushd(ROOT):
output = subprocess.check_output([
'bumpversion', '--dry-run', '--list', '--allow-dirty', 'fake-part',
])
parse_line = re.search('^parse=(.*)$', output, re.M).group(1)
return tuple(re.findall('<([^>]+)>', parse_line))
def get_gh_token(): def get_gh_token():
@ -148,131 +162,72 @@ in the future"""
return raw_input('token: ').strip() return raw_input('token: ').strip()
def get_lbryum_part(): def confirm():
print """The lbryum repo has changes but you didn't specify how to bump the try:
version. Please enter one of: {}""".format(', '.join(LBRYUM_PARTS)) return raw_input('Is this what you want? [y/N] ').strip().lower() == 'y'
while True: except KeyboardInterrupt:
part = raw_input('part: ').strip() return False
if part in LBRYUM_PARTS:
return part
print 'Invalid part. Enter one of: {}'.format(', '.join(LBRYUM_PARTS))
def get_release_msg(changelogs, names): def run_sanity_checks(repo, branch):
lines = [] if repo.git_repo.is_dirty():
for name in names:
entry = changelogs.get(name)
if not entry:
continue
lines.append('## {}\n'.format(name))
lines.append('{}\n'.format(entry))
return '\n'.join(lines)
def run_sanity_checks(base, branch):
if base.is_dirty():
print 'Cowardly refusing to release a dirty repo' print 'Cowardly refusing to release a dirty repo'
sys.exit(1) sys.exit(1)
if base.active_branch.name != branch: if repo.git_repo.active_branch.name != branch:
print 'Cowardly refusing to release when not on the {} branch'.format(branch) print 'Cowardly refusing to release when not on the {} branch'.format(branch)
sys.exit(1) sys.exit(1)
if is_behind(base, branch): if repo.is_behind(branch):
print 'Cowardly refusing to release when behind origin' print 'Cowardly refusing to release when behind origin'
sys.exit(1) sys.exit(1)
check_bumpversion() if not is_custom_bumpversion_version():
print (
'Install LBRY\'s fork of bumpversion: '
def is_behind(base, branch): 'pip install -U git+https://github.com/lbryio/bumpversion.git'
base.remotes.origin.fetch() )
rev_list = '{branch}...origin/{branch}'.format(branch=branch) sys.exit(1)
commits_behind = base.git.rev_list(rev_list, right_only=True, count=True) if not check_daemon_urls():
commits_behind = int(commits_behind)
return commits_behind > 0
def check_bumpversion():
def require_new_version():
print 'Install bumpversion: pip install -U git+https://github.com/lbryio/bumpversion.git'
sys.exit(1) sys.exit(1)
def check_daemon_urls():
success = True
with open(DAEMON_URL_FILE, 'r') as f:
daemon_url_template = f.read().strip()
if "OSNAME" not in daemon_url_template:
print "Daemon URL must include the string 'OSNAME'"
return False
for osname in ('linux', 'macos', 'windows'):
if not check_url(daemon_url_template.replace('OSNAME', osname)):
success = False
print "Daemon URL for " + osname + " does not work"
return success
def check_url(url):
url = url.strip()
r = requests.head(url)
if r.status_code >= 400:
return False
elif r.status_code >= 300:
new_location = r.headers.get('Location').strip()
if new_location == url:
# self-loop
return False
if "github-cloud.s3.amazonaws.com/releases" in new_location:
# HEAD doesnt work on s3 links, so assume its good
return True
return check_url(new_location)
return True
def is_custom_bumpversion_version():
try: try:
output = subprocess.check_output(['bumpversion', '-v'], stderr=subprocess.STDOUT) output = subprocess.check_output(['bumpversion', '-v'], stderr=subprocess.STDOUT).strip()
output = output.strip() if output == 'bumpversion 0.5.4-lbry':
if output != 'bumpversion 0.5.4-lbry': return True
require_new_version() except (subprocess.CalledProcessError, OSError):
except (subprocess.CalledProcessError, OSError) as err: pass
require_new_version() return False
def get_part(args, name):
return getattr(args, name + '_part') or args.lbry_part
class Repo(object):
def __init__(self, name, part, directory=None):
self.name = name
self.part = part
self.directory = directory or os.path.join(os.getcwd(), name)
self.git_repo = git.Repo(self.directory)
self.saved_commit = None
self._bumped = False
def get_last_tag(self):
return string.split(self.git_repo.git.describe(tags=True), '-')[0]
def get_submodule_hash(self, revision, submodule_path):
line = getattr(self.git_repo.git, 'ls-tree')(revision, submodule_path)
return string.split(line)[2] if line else None
def has_changes_from_revision(self, revision):
commit = str(self.git_repo.commit())
logging.info('%s =? %s', commit, revision)
return commit != revision
def save_commit(self):
self.saved_commit = self.git_repo.commit()
logging.info('Saved ', self.git_repo.commit(), self.saved_commit)
def checkout(self, branch):
self.git_repo.git.checkout(branch)
self.git_repo.git.pull(rebase=True)
def get_changelog_entry(self):
filename = os.path.join(self.directory, 'CHANGELOG.md')
return changelog.bump(filename, self.new_version())
def add_changelog(self):
with pushd(self.directory):
self.git_repo.git.add('CHANGELOG.md')
def new_version(self):
if self._bumped:
raise Exception('Cannot calculate a new version on an already bumped repo')
if not self.part:
raise Exception('Cannot calculate a new version without a part')
with pushd(self.directory):
output = subprocess.check_output(
['bumpversion', '--dry-run', '--list', '--allow-dirty', self.part])
return re.search('^new_version=(.*)$', output, re.M).group(1)
def bumpversion(self):
if self._bumped:
raise Exception('Cowardly refusing to bump a repo twice')
if not self.part:
raise Exception('Cannot bump version for {}: no part specified'.format(repo.name))
with pushd(self.directory):
subprocess.check_call(['bumpversion', '--allow-dirty', self.part])
self._bumped = True
def assert_new_tag_is_absent(self):
new_tag = 'v' + self.new_version()
tags = self.git_repo.git.tag()
if new_tag in tags.split('\n'):
raise Exception('Tag {} is already present in repo {}.'.format(new_tag, self.name))
@property
def git(self):
return self.git_repo.git
@contextlib.contextmanager @contextlib.contextmanager
@ -284,10 +239,4 @@ def pushd(new_dir):
if __name__ == '__main__': if __name__ == '__main__':
logging.basicConfig(
format="%(asctime)s %(levelname)-8s %(name)s:%(lineno)d: %(message)s",
level='INFO'
)
sys.exit(main()) sys.exit(main())
else:
log = logging.getLogger('__name__')

View file

@ -1,6 +1,5 @@
import glob import glob
import json import json
import logging
import os import os
import platform import platform
import subprocess import subprocess
@ -10,15 +9,13 @@ import github
import requests import requests
import uritemplate import uritemplate
from lbrynet.core import log_support
def main(): def main():
try: try:
current_tag = subprocess.check_output( current_tag = subprocess.check_output(
['git', 'describe', '--exact-match', 'HEAD']).strip() ['git', 'describe', '--exact-match', 'HEAD']).strip()
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
log.info('Stopping as we are not currently on a tag') print 'Stopping as we are not currently on a tag'
return return
if 'GH_TOKEN' not in os.environ: if 'GH_TOKEN' not in os.environ:
@ -27,20 +24,15 @@ def main():
gh_token = os.environ['GH_TOKEN'] gh_token = os.environ['GH_TOKEN']
auth = github.Github(gh_token) auth = github.Github(gh_token)
app_repo = auth.get_repo('lbryio/lbry-app') repo = auth.get_repo('lbryio/lbry-app')
daemon_repo = auth.get_repo('lbryio/lbry')
if not check_repo_has_tag(app_repo, current_tag): if not check_repo_has_tag(repo, current_tag):
log.info('Tag %s is not in repo %s', current_tag, app_repo) print 'Tag {} is not in repo {}'.format(current_tag, repo)
# TODO: maybe this should be an error # TODO: maybe this should be an error
return return
# daemon = get_daemon_artifact()
# release = get_release(daemon_repo, current_tag)
# upload_asset(release, daemon, gh_token)
app = get_app_artifact() app = get_app_artifact()
release = get_release(app_repo, current_tag) release = get_release(repo, current_tag)
upload_asset(release, app, gh_token) upload_asset(release, app, gh_token)
@ -60,21 +52,18 @@ def get_release(current_repo, current_tag):
def get_app_artifact(): def get_app_artifact():
this_dir = os.path.dirname(os.path.realpath(__file__))
system = platform.system() system = platform.system()
if system == 'Darwin': if system == 'Darwin':
return glob.glob('dist/mac/LBRY*.dmg')[0] return glob.glob(this_dir + '/../dist/mac/LBRY*.dmg')[0]
elif system == 'Linux': elif system == 'Linux':
return glob.glob('dist/LBRY*.deb')[0] return glob.glob(this_dir + '/../dist/LBRY*.deb')[0]
elif system == 'Windows': elif system == 'Windows':
return glob.glob('dist/LBRY*.exe')[0] return glob.glob(this_dir + '/../dist/LBRY*.exe')[0]
else: else:
raise Exception("I don't know about any artifact on {}".format(system)) raise Exception("I don't know about any artifact on {}".format(system))
def get_daemon_artifact():
return glob.glob('dist/*.zip')[0]
def upload_asset(release, asset_to_upload, token): def upload_asset(release, asset_to_upload, token):
basename = os.path.basename(asset_to_upload) basename = os.path.basename(asset_to_upload)
if is_asset_already_uploaded(release, basename): if is_asset_already_uploaded(release, basename):
@ -84,30 +73,26 @@ def upload_asset(release, asset_to_upload, token):
try: try:
return _upload_asset(release, asset_to_upload, token, _curl_uploader) return _upload_asset(release, asset_to_upload, token, _curl_uploader)
except Exception: except Exception:
log.exception('Failed to upload') print 'Failed uploading on attempt {}'.format(count + 1)
count += 1 count += 1
def _upload_asset(release, asset_to_upload, token, uploader): def _upload_asset(release, asset_to_upload, token, uploader):
basename = os.path.basename(asset_to_upload) basename = os.path.basename(asset_to_upload)
upload_uri = uritemplate.expand( upload_uri = uritemplate.expand(release.upload_url, {'name': basename})
release.upload_url,
{'name': basename}
)
output = uploader(upload_uri, asset_to_upload, token) output = uploader(upload_uri, asset_to_upload, token)
if 'errors' in output: if 'errors' in output:
raise Exception(output) raise Exception(output)
else: else:
log.info('Successfully uploaded to %s', output['browser_download_url']) print 'Successfully uploaded to {}'.format(output['browser_download_url'])
# requests doesn't work on windows / linux / osx. # requests doesn't work on windows / linux / osx.
def _requests_uploader(upload_uri, asset_to_upload, token): def _requests_uploader(upload_uri, asset_to_upload, token):
log.info('Using requests to upload %s to %s', asset_to_upload, upload_uri) print 'Using requests to upload {} to {}'.format(asset_to_upload, upload_uri)
with open(asset_to_upload, 'rb') as f: with open(asset_to_upload, 'rb') as f:
response = requests.post(upload_uri, data=f, auth=('', token)) response = requests.post(upload_uri, data=f, auth=('', token))
output = response.json() return response.json()
return output
# curl -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}' http://localhost:3000/api/login # curl -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}' http://localhost:3000/api/login
@ -118,7 +103,7 @@ def _curl_uploader(upload_uri, asset_to_upload, token):
# half a day trying to debug before deciding to switch to curl. # half a day trying to debug before deciding to switch to curl.
# #
# TODO: actually set the content type # TODO: actually set the content type
log.info('Using curl to upload %s to %s', asset_to_upload, upload_uri) print 'Using curl to upload {} to {}'.format(asset_to_upload, upload_uri)
cmd = [ cmd = [
'curl', 'curl',
'-sS', '-sS',
@ -141,21 +126,16 @@ def _curl_uploader(upload_uri, asset_to_upload, token):
print stderr print stderr
print 'stdout from curl:' print 'stdout from curl:'
print stdout print stdout
output = json.loads(stdout) return json.loads(stdout)
return output
def is_asset_already_uploaded(release, basename): def is_asset_already_uploaded(release, basename):
for asset in release.raw_data['assets']: for asset in release.raw_data['assets']:
if asset['name'] == basename: if asset['name'] == basename:
log.info('File %s has already been uploaded to %s', basename, release.tag_name) print 'File {} has already been uploaded to {}'.format(basename, release.tag_name)
return True return True
return False return False
if __name__ == '__main__': if __name__ == '__main__':
log = logging.getLogger('release-on-tag')
log_support.configure_console(level='INFO')
sys.exit(main()) sys.exit(main())
else:
log = logging.getLogger(__name__)

View file

@ -1,21 +0,0 @@
#!/bin/bash
set -euxo pipefail
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
cd "$ROOT"
(
cd lbry
git tag -d $(git describe)
git reset --hard origin/master
)
(
cd lbryum
git tag -d $(git describe)
git reset --hard origin/master
)
git tag -d $(git describe)
git reset --hard HEAD~1

View file

@ -1,28 +0,0 @@
"""Set the build version to be 'dev', 'qa', 'rc', 'release'"""
import os.path
import re
import subprocess
import sys
def main():
build = get_build()
with open(os.path.join('lbry', 'lbrynet', 'build_type.py'), 'w') as f:
f.write('BUILD = "{}"'.format(build))
def get_build():
try:
tag = subprocess.check_output(['git', 'describe', '--exact-match']).strip()
if re.match('v\d+\.\d+\.\d+rc\d+', tag):
return 'rc'
else:
return 'release'
except subprocess.CalledProcessError:
# if the build doesn't have a tag
return 'qa'
if __name__ == '__main__':
sys.exit(main())

View file

@ -1,58 +1,20 @@
"""Set the package version to the output of `git describe`""" """Set the package version to the output of `git describe`"""
import argparse from __future__ import print_function
import json
import os.path import os.path
import re
import subprocess
import sys import sys
import fileinput
def main(): def main():
parser = argparse.ArgumentParser() filename = os.path.abspath(
parser.add_argument('--version', help="defaults to the output of `git describe`") os.path.join(os.path.abspath(__file__), '..', '..', 'ui', 'js', 'lbryio.js'))
args = parser.parse_args() for line in fileinput.input(filename, inplace=True):
if args.version: if line.startswith(' enabled: false'):
version = args.version print(' enabled: true')
else: else:
tag = subprocess.check_output(['git', 'describe']).strip() print(line, end='')
try:
version = get_version_from_tag(tag)
except InvalidVersionTag:
# this should be an error but its easier to handle here
# than in the calling scripts.
print 'Tag cannot be converted to a version, Exitting'
return
set_version(version)
class InvalidVersionTag(Exception):
pass
def get_version_from_tag(tag):
match = re.match('v([\d.]+)', tag)
if match:
return match.group(1)
else:
raise InvalidVersionTag('Failed to parse version from tag {}'.format(tag))
def set_version(version):
package_file = os.path.join('app', 'package.json')
with open(package_file) as fp:
package_data = json.load(fp)
package_data['version'] = version
with open(package_file, 'w') as fp:
json.dump(package_data, fp, indent=2, separators=(',', ': '))
with open(os.path.join('lbry', 'lbrynet', '__init__.py'), 'w') as fp:
fp.write(LBRYNET_TEMPLATE.format(version=version))
LBRYNET_TEMPLATE = """
__version__ = "{version}"
version = tuple(__version__.split('.'))
"""
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -1,28 +0,0 @@
import os
import platform
import subprocess
import sys
import zipfile
def main():
tag = subprocess.check_output(['git', 'describe']).strip()
zipfilename = 'lbrynet-daemon-{}-{}.zip'.format(tag, get_system_label())
full_filename = os.path.join('dist', zipfilename)
executables = ['lbrynet-daemon', 'lbrynet-cli']
ext = '.exe' if platform.system() == 'Windows' else ''
with zipfile.ZipFile(full_filename, 'w') as myzip:
for executable in executables:
myzip.write(os.path.join('app', 'dist', executable + ext), executable + ext)
def get_system_label():
system = platform.system()
if system == 'Darwin':
return 'macos'
else:
return system.lower()
if __name__ == '__main__':
sys.exit(main())

View file

@ -1,28 +0,0 @@
$env:Path += ";C:\MinGW\bin\"
$env:Path += ";C:\Program Files (x86)\Windows Kits\10\bin\x86\"
gcc --version
mingw32-make --version
# build/install miniupnpc manually
tar zxf miniupnpc-1.9.tar.gz
cd miniupnpc-1.9
mingw32-make.exe -f Makefile.mingw
python.exe setupmingw32.py build --compiler=mingw32
python.exe setupmingw32.py install
cd ..\
Remove-Item -Recurse -Force miniupnpc-1.9
# copy requirements from lbry, but remove lbryum (we'll add it back in below) and gmpy and miniupnpc (installed manually)
Get-Content ..\lbry\requirements.txt | Select-String -Pattern 'lbryum|gmpy|miniupnpc' -NotMatch | Out-File requirements.txt
# add in gmpy wheel
Add-Content requirements.txt "./gmpy-1.17-cp27-none-win32.whl"
# for electron, we install lbryum and lbry using submodules
Add-Content requirements.txt "../lbryum"
Add-Content requirements.txt "../lbry"
pip.exe install pyinstaller
pip.exe install -r requirements.txt
pyinstaller -y daemon.onefile.spec
pyinstaller -y cli.onefile.spec

View file

@ -1,58 +0,0 @@
# -*- mode: python -*-
import platform
import os
cwd = os.getcwd()
if os.path.basename(cwd) != 'daemon':
raise Exception('The build needs to be run from the same directory as the spec file')
repo_base = os.path.abspath(os.path.join(cwd, '..'))
system = platform.system()
if system == 'Darwin':
icns = os.path.join(repo_base, 'build', 'icon.icns')
elif system == 'Linux':
icns = os.path.join(repo_base, 'build', 'icons', '256x256.png')
elif system == 'Windows':
icns = os.path.join(repo_base, 'build', 'icons', 'lbry256.ico')
else:
print 'Warning: System {} has no icons'.format(system)
icns = None
block_cipher = None
a = Analysis(
['cli.py'],
pathex=[cwd],
binaries=None,
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher
)
pyz = PYZ(
a.pure,
a.zipped_data,
cipher=block_cipher
)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='lbrynet-cli',
debug=False,
strip=False,
upx=True,
console=True,
icon=icns
)

View file

@ -1,7 +0,0 @@
from lbrynet.lbrynet_daemon import DaemonCLI
import logging
logging.basicConfig()
if __name__ == '__main__':
DaemonCLI.main()

View file

@ -1,77 +0,0 @@
# -*- mode: python -*-
import platform
import os
import lbryum
cwd = os.getcwd()
if os.path.basename(cwd) != 'daemon':
raise Exception('The build needs to be run from the same directory as the spec file')
repo_base = os.path.abspath(os.path.join(cwd, '..'))
system = platform.system()
if system == 'Darwin':
icns = os.path.join(repo_base, 'build', 'icon.icns')
elif system == 'Linux':
icns = os.path.join(repo_base, 'build', 'icons', '256x256.png')
elif system == 'Windows':
icns = os.path.join(repo_base, 'build', 'icons', 'lbry256.ico')
else:
print 'Warning: System {} has no icons'.format(system)
icns = None
block_cipher = None
languages = (
'chinese_simplified.txt', 'japanese.txt', 'spanish.txt',
'english.txt', 'portuguese.txt'
)
datas = [
(
os.path.join(os.path.dirname(lbryum.__file__), 'wordlist', language),
'lbryum/wordlist'
)
for language in languages
]
a = Analysis(
['daemon.py'],
pathex=[cwd],
binaries=None,
datas=datas,
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher
)
pyz = PYZ(
a.pure, a.zipped_data,
cipher=block_cipher
)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='lbrynet-daemon',
debug=False,
strip=False,
upx=True,
console=True,
icon=icns
)

View file

@ -1,4 +0,0 @@
from lbrynet.lbrynet_daemon import DaemonControl
if __name__ == '__main__':
DaemonControl.start()

Binary file not shown.

Binary file not shown.

1
lbry

@ -1 +0,0 @@
Subproject commit 043e2d0ab96030468d53d02e311fd848f35c2dc1

1
lbryum

@ -1 +0,0 @@
Subproject commit 121bda3963ee94f0c9c027813c55b71b38219739

View file

@ -42,5 +42,6 @@
"devDependencies": { "devDependencies": {
"electron": "^1.4.15", "electron": "^1.4.15",
"electron-builder": "^11.7.0" "electron-builder": "^11.7.0"
} },
"dependencies": {}
} }

View file

@ -7,16 +7,16 @@ import HelpPage from './page/help.js';
import WatchPage from './page/watch.js'; import WatchPage from './page/watch.js';
import ReportPage from './page/report.js'; import ReportPage from './page/report.js';
import StartPage from './page/start.js'; import StartPage from './page/start.js';
import ClaimCodePage from './page/claim_code.js'; import RewardsPage from './page/rewards.js';
import ReferralPage from './page/referral.js'; import RewardPage from './page/reward.js';
import WalletPage from './page/wallet.js'; import WalletPage from './page/wallet.js';
import DetailPage from './page/show.js'; import ShowPage from './page/show.js';
import PublishPage from './page/publish.js'; import PublishPage from './page/publish.js';
import SearchPage from './page/search.js';
import DiscoverPage from './page/discover.js'; import DiscoverPage from './page/discover.js';
import SplashScreen from './component/splash.js';
import DeveloperPage from './page/developer.js'; import DeveloperPage from './page/developer.js';
import lbryuri from './lbryuri.js';
import {FileListDownloaded, FileListPublished} from './page/file-list.js'; import {FileListDownloaded, FileListPublished} from './page/file-list.js';
import Drawer from './component/drawer.js';
import Header from './component/header.js'; import Header from './component/header.js';
import {Modal, ExpandableModal} from './component/modal.js'; import {Modal, ExpandableModal} from './component/modal.js';
import {Link} from './component/link.js'; import {Link} from './component/link.js';
@ -38,17 +38,12 @@ var App = React.createClass({
message: 'Error message', message: 'Error message',
data: 'Error data', data: 'Error data',
}, },
_fullScreenPages: ['watch'],
_storeHistoryOfNextRender: false,
_upgradeDownloadItem: null, _upgradeDownloadItem: null,
_isMounted: false, _isMounted: false,
_version: null, _version: null,
// Temporary workaround since electron-dl throws errors when you try to get the filename
getDefaultProps: function() {
return {
address: window.location.search
};
},
getUpdateUrl: function() { getUpdateUrl: function() {
switch (process.platform) { switch (process.platform) {
case 'darwin': case 'darwin':
@ -80,15 +75,13 @@ var App = React.createClass({
let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/); let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/);
return { return {
viewingPage: viewingPage, viewingPage: viewingPage,
pageArgs: pageArgs === undefined ? null : pageArgs pageArgs: pageArgs === undefined ? null : decodeURIComponent(pageArgs)
}; };
}, },
getInitialState: function() { getInitialState: function() {
var match, param, val, viewingPage, pageArgs, return Object.assign(this.getViewingPageAndArgs(window.location.search), {
drawerOpenRaw = sessionStorage.getItem('drawerOpen'); viewingPage: 'discover',
appUrl: null,
return Object.assign(this.getViewingPageAndArgs(this.props.address), {
drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true,
errorInfo: null, errorInfo: null,
modal: null, modal: null,
downloadProgress: null, downloadProgress: null,
@ -96,6 +89,8 @@ var App = React.createClass({
}); });
}, },
componentWillMount: function() { componentWillMount: function() {
window.addEventListener("popstate", this.onHistoryPop);
document.addEventListener('unhandledError', (event) => { document.addEventListener('unhandledError', (event) => {
this.alertError(event.detail); this.alertError(event.detail);
}); });
@ -112,7 +107,10 @@ var App = React.createClass({
if (target.matches('a[href^="?"]')) { if (target.matches('a[href^="?"]')) {
event.preventDefault(); event.preventDefault();
if (this._isMounted) { if (this._isMounted) {
this.setState(this.getViewingPageAndArgs(target.getAttribute('href'))); let appUrl = target.getAttribute('href');
this._storeHistoryOfNextRender = true;
this.setState(Object.assign({}, this.getViewingPageAndArgs(appUrl), { appUrl: appUrl }));
document.body.scrollTop = 0;
} }
} }
target = target.parentNode; target = target.parentNode;
@ -120,28 +118,16 @@ var App = React.createClass({
}); });
if (!sessionStorage.getItem('upgradeSkipped')) { if (!sessionStorage.getItem('upgradeSkipped')) {
lbry.checkNewVersionAvailable(({isAvailable}) => { lbry.getVersionInfo().then(({remoteVersion, upgradeAvailable}) => {
if (!isAvailable) { if (upgradeAvailable) {
return; this._version = remoteVersion;
}
lbry.getVersionInfo((versionInfo) => {
this._version = versionInfo.lbrynet_version;
this.setState({ this.setState({
modal: 'upgrade', modal: 'upgrade',
}); });
}); }
}); });
} }
}, },
openDrawer: function() {
sessionStorage.setItem('drawerOpen', true);
this.setState({ drawerOpen: true });
},
closeDrawer: function() {
sessionStorage.setItem('drawerOpen', false);
this.setState({ drawerOpen: false });
},
closeModal: function() { closeModal: function() {
this.setState({ this.setState({
modal: null, modal: null,
@ -152,6 +138,28 @@ var App = React.createClass({
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
this._isMounted = false; this._isMounted = false;
window.removeEventListener("popstate", this.onHistoryPop);
},
onHistoryPop: function() {
this.setState(this.getViewingPageAndArgs(location.search));
},
onSearch: function(term) {
this._storeHistoryOfNextRender = true;
const isShow = term.startsWith('lbry://');
this.setState({
viewingPage: isShow ? "show" : "search",
appUrl: (isShow ? "?show=" : "?search=") + encodeURIComponent(term),
pageArgs: term
});
},
onSubmit: function(uri) {
this._storeHistoryOfNextRender = true;
this.setState({
address: uri,
appUrl: "?show=" + encodeURIComponent(uri),
viewingPage: "show",
pageArgs: uri
})
}, },
handleUpgradeClicked: function() { handleUpgradeClicked: function() {
// Make a new directory within temp directory so the filename is guaranteed to be available // Make a new directory within temp directory so the filename is guaranteed to be available
@ -205,12 +213,6 @@ var App = React.createClass({
modal: null, modal: null,
}); });
}, },
onSearch: function(term) {
this.setState({
viewingPage: 'discover',
pageArgs: term
});
},
alertError: function(error) { alertError: function(error) {
var errorInfoList = []; var errorInfoList = [];
for (let key of Object.keys(error)) { for (let key of Object.keys(error)) {
@ -224,81 +226,57 @@ var App = React.createClass({
errorInfo: <ul className="error-modal__error-list">{errorInfoList}</ul>, errorInfo: <ul className="error-modal__error-list">{errorInfoList}</ul>,
}); });
}, },
getHeaderLinks: function() getContentAndAddress: function()
{
switch(this.state.viewingPage)
{
case 'wallet':
case 'send':
case 'receive':
case 'claim':
case 'referral':
return {
'?wallet' : 'Overview',
'?send' : 'Send',
'?receive' : 'Receive',
'?claim' : 'Claim Beta Code',
'?referral' : 'Check Referral Credit',
};
case 'downloaded':
case 'published':
return {
'?downloaded': 'Downloaded',
'?published': 'Published',
};
default:
return null;
}
},
getMainContent: function()
{ {
switch(this.state.viewingPage) switch(this.state.viewingPage)
{ {
case 'search':
return [this.state.pageArgs ? this.state.pageArgs : "Search", 'icon-search', <SearchPage query={this.state.pageArgs} />];
case 'settings': case 'settings':
return <SettingsPage />; return ["Settings", "icon-gear", <SettingsPage />];
case 'help': case 'help':
return <HelpPage />; return ["Help", "icon-question", <HelpPage />];
case 'watch':
return <WatchPage name={this.state.pageArgs} />;
case 'report': case 'report':
return <ReportPage />; return ['Report an Issue', 'icon-file', <ReportPage />];
case 'downloaded': case 'downloaded':
return <FileListDownloaded />; return ["Downloads & Purchases", "icon-folder", <FileListDownloaded />];
case 'published': case 'published':
return <FileListPublished />; return ["Publishes", "icon-folder", <FileListPublished />];
case 'start': case 'start':
return <StartPage />; return ["Start", "icon-file", <StartPage />];
case 'claim': case 'rewards':
return <ClaimCodePage />; return ["Rewards", "icon-bank", <RewardsPage />];
case 'referral':
return <ReferralPage />;
case 'wallet': case 'wallet':
case 'send': case 'send':
case 'receive': case 'receive':
return <WalletPage viewingPage={this.state.viewingPage} />; return [this.state.viewingPage.charAt(0).toUpperCase() + this.state.viewingPage.slice(1), "icon-bank", <WalletPage viewingPage={this.state.viewingPage} />]
case 'show': case 'show':
return <DetailPage name={this.state.pageArgs} />; return [lbryuri.normalize(this.state.pageArgs), "icon-file", <ShowPage uri={this.state.pageArgs} />];
case 'publish': case 'publish':
return <PublishPage />; return ["Publish", "icon-upload", <PublishPage />];
case 'developer': case 'developer':
return <DeveloperPage />; return ["Developer", "icon-file", <DeveloperPage />];
case 'discover': case 'discover':
default: default:
return <DiscoverPage {... this.state.pageArgs !== null ? {query: this.state.pageArgs} : {} } />; return ["Home", "icon-home", <DiscoverPage />];
} }
}, },
render: function() { render: function() {
var mainContent = this.getMainContent(), let [address, wunderBarIcon, mainContent] = this.getContentAndAddress();
headerLinks = this.getHeaderLinks(),
searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : ''; lbry.setTitle(address);
if (this._storeHistoryOfNextRender) {
this._storeHistoryOfNextRender = false;
history.pushState({}, document.title, this.state.appUrl);
}
return ( return (
this.state.viewingPage == 'watch' ? this._fullScreenPages.includes(this.state.viewingPage) ?
mainContent : mainContent :
<div id="window" className={ this.state.drawerOpen ? 'drawer-open' : 'drawer-closed' }> <div id="window">
<Drawer onCloseDrawer={this.closeDrawer} viewingPage={this.state.viewingPage} /> <Header onSearch={this.onSearch} onSubmit={this.onSubmit} address={address} wunderBarIcon={wunderBarIcon} viewingPage={this.state.viewingPage} />
<div id="main-content" className={ headerLinks ? 'with-sub-nav' : 'no-sub-nav' }> <div id="main-content">
<Header onOpenDrawer={this.openDrawer} initialQuery={searchQuery} onSearch={this.onSearch} links={headerLinks} viewingPage={this.state.viewingPage} />
{mainContent} {mainContent}
</div> </div>
<Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available" <Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available"
@ -325,7 +303,7 @@ var App = React.createClass({
</div> </div>
</Modal> </Modal>
<ExpandableModal isOpen={this.state.modal == 'error'} contentLabel="Error" className="error-modal" <ExpandableModal isOpen={this.state.modal == 'error'} contentLabel="Error" className="error-modal"
overlayClassName="error-modal-overlay" onConfirmed={this.closeModal} overlayClassName="modal-overlay error-modal-overlay" onConfirmed={this.closeModal}
extraContent={this.state.errorInfo}> extraContent={this.state.errorInfo}>
<h3 className="modal__header">Error</h3> <h3 className="modal__header">Error</h3>

301
ui/js/component/auth.js Normal file
View file

@ -0,0 +1,301 @@
import React from "react";
import lbryio from "../lbryio.js";
import Modal from "./modal.js";
import ModalPage from "./modal-page.js";
import {Link, RewardLink} from "../component/link.js";
import {FormRow} from "../component/form.js";
import {CreditAmount, Address} from "../component/common.js";
import {getLocal, getSession, setSession, setLocal} from '../utils.js';
const SubmitEmailStage = React.createClass({
getInitialState: function() {
return {
rewardType: null,
email: '',
submitting: false
};
},
handleEmailChanged: function(event) {
this.setState({
email: event.target.value,
});
},
onEmailSaved: function(email) {
this.props.setStage("confirm", { email: email })
},
handleSubmit: function(event) {
event.preventDefault();
this.setState({
submitting: true,
});
lbryio.call('user_email', 'new', {email: this.state.email}, 'post').then(() => {
this.onEmailSaved(this.state.email);
}, (error) => {
if (error.xhr && error.xhr.status == 409) {
this.onEmailSaved(this.state.email);
return;
} else if (this._emailRow) {
this._emailRow.showError(error.message)
}
this.setState({ submitting: false });
});
},
render: function() {
return (
<section>
<form onSubmit={this.handleSubmit}>
<FormRow ref={(ref) => { this._emailRow = ref }} type="text" label="Email" placeholder="scrwvwls@lbry.io"
name="email" value={this.state.email}
onChange={this.handleEmailChanged} />
<div className="form-row-submit">
<Link button="primary" label="Next" disabled={this.state.submitting} onClick={this.handleSubmit} />
</div>
</form>
</section>
);
}
});
const ConfirmEmailStage = React.createClass({
getInitialState: function() {
return {
rewardType: null,
code: '',
submitting: false,
errorMessage: null,
};
},
handleCodeChanged: function(event) {
this.setState({
code: event.target.value,
});
},
handleSubmit: function(event) {
event.preventDefault();
this.setState({
submitting: true,
});
const onSubmitError = (error) => {
if (this._codeRow) {
this._codeRow.showError(error.message)
}
this.setState({ submitting: false });
};
lbryio.call('user_email', 'confirm', {verification_token: this.state.code, email: this.props.email}, 'post').then((userEmail) => {
if (userEmail.IsVerified) {
this.props.setStage("welcome")
} else {
onSubmitError(new Error("Your email is still not verified.")) //shouldn't happen?
}
}, onSubmitError);
},
render: function() {
return (
<section>
<form onSubmit={this.handleSubmit}>
<FormRow label="Verification Code" ref={(ref) => { this._codeRow = ref }} type="text"
name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={this.handleCodeChanged}
helper="A verification code is required to access this version."/>
<div className="form-row-submit form-row-submit--with-footer">
<Link button="primary" label="Verify" disabled={this.state.submitting} onClick={this.handleSubmit} />
</div>
<div className="form-field__helper">
No code? <Link onClick={() => { this.props.setStage("nocode")}} label="Click here" />.
</div>
</form>
</section>
);
}
});
const WelcomeStage = React.createClass({
propTypes: {
endAuth: React.PropTypes.func,
},
getInitialState: function() {
return {
hasReward: false,
rewardAmount: null,
}
},
onRewardClaim: function(reward) {
this.setState({
hasReward: true,
rewardAmount: reward.amount
})
},
render: function() {
return (
!this.state.hasReward ?
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY" {...this.props}>
<section>
<h3 className="modal__header">Welcome to LBRY.</h3>
<p>Using LBRY is like dating a centaur. Totally normal up top, and <em>way different</em> underneath.</p>
<p>Up top, LBRY is similar to popular media sites.</p>
<p>Below, LBRY is controlled by users -- you -- via blockchain and decentralization.</p>
<p>Thank you for making content freedom possible! Here's a nickel, kid.</p>
<div style={{textAlign: "center", marginBottom: "12px"}}>
<RewardLink type="new_user" button="primary" onRewardClaim={this.onRewardClaim} onRewardFailure={() => this.props.setStage(null)} onConfirmed={() => { this.props.setStage(null) }} />
</div>
</section>
</Modal> :
<Modal type="alert" overlayClassName="modal-overlay modal-overlay--clear" isOpen={true} contentLabel="Welcome to LBRY" {...this.props} onConfirmed={() => { this.props.setStage(null) }}>
<section>
<h3 className="modal__header">About Your Reward</h3>
<p>You earned a reward of <CreditAmount amount={this.state.rewardAmount} label={false} /> LBRY credits, or <em>LBC</em>.</p>
<p>This reward will show in your Wallet momentarily, probably while you are reading this message.</p>
<p>LBC is used to compensate creators, to publish, and to have say in how the network works.</p>
<p>No need to understand it all just yet! Try watching or downloading something next.</p>
<p>Finally, know that LBRY is a beta and that it earns the name.</p>
</section>
</Modal>
);
}
});
const ErrorStage = React.createClass({
render: function() {
return (
<section>
<p>An error was encountered that we cannot continue from.</p>
<p>At least we're earning the name beta.</p>
{ this.props.errorText ? <p>Message: {this.props.errorText}</p> : '' }
<Link button="alt" label="Try Reload" onClick={() => { window.location.reload() } } />
</section>
);
}
});
const PendingStage = React.createClass({
render: function() {
return (
<section>
<p>Preparing for first access <span className="busy-indicator"></span></p>
</section>
);
}
});
const CodeRequiredStage = React.createClass({
_balanceSubscribeId: null,
getInitialState: function() {
return {
balance: 0,
address: getLocal('wallet_address')
}
},
componentWillMount: function() {
this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
this.setState({
balance: balance
});
})
if (!this.state.address) {
lbry.getUnusedAddress((address) => {
setLocal('wallet_address', address);
this.setState({ address: address });
});
}
},
componentWillUnmount: function() {
if (this._balanceSubscribeId) {
lbry.balanceUnsubscribe(this._balanceSubscribeId)
}
},
render: function() {
const disabled = this.state.balance < 1;
return (
<div>
<section className="section-spaced">
<p>Access to LBRY is restricted as we build and scale the network.</p>
<p>There are two ways in:</p>
<h3>Own LBRY Credits</h3>
<p>If you own at least 1 LBC, you can get in right now.</p>
<p style={{ textAlign: "center"}}><Link onClick={() => { setLocal('auth_bypassed', true); this.props.setStage(null); }}
disabled={disabled} label="Let Me In" button={ disabled ? "alt" : "primary" } /></p>
<p>Your balance is <CreditAmount amount={this.state.balance} />. To increase your balance, send credits to this address:</p>
<p><Address address={ this.state.address ? this.state.address : "Generating Address..." } /></p>
<p>If you don't understand how to send credits, then...</p>
</section>
<section>
<h3>Wait For A Code</h3>
<p>If you provide your email, you'll automatically receive a notification when the system is open.</p>
<p><Link onClick={() => { this.props.setStage("email"); }} label="Return" /></p>
</section>
</div>
);
}
});
export const AuthOverlay = React.createClass({
_stages: {
pending: PendingStage,
error: ErrorStage,
nocode: CodeRequiredStage,
email: SubmitEmailStage,
confirm: ConfirmEmailStage,
welcome: WelcomeStage
},
getInitialState: function() {
return {
stage: "pending",
stageProps: {}
};
},
setStage: function(stage, stageProps = {}) {
this.setState({
stage: stage,
stageProps: stageProps
})
},
componentWillMount: function() {
lbryio.authenticate().then((user) => {
if (!user.HasVerifiedEmail) {
if (getLocal('auth_bypassed')) {
this.setStage(null)
} else {
this.setStage("email", {})
}
} else {
lbryio.call('reward', 'list', {}).then((userRewards) => {
userRewards.filter(function(reward) {
return reward.RewardType == "new_user" && reward.TransactionID;
}).length ?
this.setStage(null) :
this.setStage("welcome")
});
}
}).catch((err) => {
this.setStage("error", { errorText: err.message })
document.dispatchEvent(new CustomEvent('unhandledError', {
detail: {
message: err.message,
data: err.stack
}
}));
})
},
render: function() {
if (!this.state.stage) {
return null;
}
const StageContent = this._stages[this.state.stage];
return (
this.state.stage != "welcome" ?
<ModalPage className="modal-page--full" isOpen={true} contentLabel="Authentication" {...this.props}>
<h1>LBRY Early Access</h1>
<StageContent {...this.state.stageProps} setStage={this.setStage} />
</ModalPage> :
<StageContent setStage={this.setStage} {...this.state.stageProps} />
);
}
});

View file

@ -1,31 +1,29 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import uri from '../uri.js'; import lbryuri from '../lbryuri.js';
import {Icon} from './common.js'; import {Icon} from './common.js';
const ChannelIndicator = React.createClass({ const UriIndicator = React.createClass({
propTypes: { propTypes: {
uri: React.PropTypes.string.isRequired, uri: React.PropTypes.string.isRequired,
claimInfo: React.PropTypes.object.isRequired, hasSignature: React.PropTypes.bool.isRequired,
signatureIsValid: React.PropTypes.bool,
}, },
render: function() { render: function() {
const {name, has_signature, signature_is_valid} = this.props.claimInfo;
if (!has_signature) {
return null;
}
const uriObj = uri.parseLbryUri(this.props.uri); const uriObj = lbryuri.parse(this.props.uri);
if (!uriObj.isChannel) {
return null; if (!this.props.hasSignature || !uriObj.isChannel) {
return <span className="empty">Anonymous</span>;
} }
const channelUriObj = Object.assign({}, uriObj); const channelUriObj = Object.assign({}, uriObj);
delete channelUriObj.path; delete channelUriObj.path;
const channelUri = uri.buildLbryUri(channelUriObj, false); delete channelUriObj.contentName;
const channelUri = lbryuri.build(channelUriObj, false);
let icon, modifier; let icon, modifier;
if (!signature_is_valid) { if (this.props.signatureIsValid) {
icon = 'icon-check-circle';
modifier = 'valid'; modifier = 'valid';
} else { } else {
icon = 'icon-times-circle'; icon = 'icon-times-circle';
@ -33,11 +31,13 @@ const ChannelIndicator = React.createClass({
} }
return ( return (
<span> <span>
by <strong>{channelUri}</strong> {' '} {channelUri} {' '}
<Icon icon={icon} className={`channel-indicator__icon channel-indicator__icon--${modifier}`} /> { !this.props.signatureIsValid ?
<Icon icon={icon} className={`channel-indicator__icon channel-indicator__icon--${modifier}`} /> :
'' }
</span> </span>
); );
} }
}); });
export default ChannelIndicator; export default UriIndicator;

View file

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import $clamp from 'clamp-js-main';
//component/icon.js //component/icon.js
export let Icon = React.createClass({ export let Icon = React.createClass({
@ -19,29 +18,15 @@ export let Icon = React.createClass({
export let TruncatedText = React.createClass({ export let TruncatedText = React.createClass({
propTypes: { propTypes: {
lines: React.PropTypes.number, lines: React.PropTypes.number
height: React.PropTypes.string,
auto: React.PropTypes.bool,
}, },
getDefaultProps: function() { getDefaultProps: function() {
return { return {
lines: null, lines: null,
height: null,
auto: true,
} }
}, },
componentDidMount: function() {
// Manually round up the line height, because clamp.js doesn't like fractional-pixel line heights.
// Need to work directly on the style object because setting the style prop doesn't update internal styles right away.
this.refs.span.style.lineHeight = Math.ceil(parseFloat(getComputedStyle(this.refs.span).lineHeight)) + 'px';
$clamp(this.refs.span, {
clamp: this.props.lines || this.props.height || 'auto',
});
},
render: function() { render: function() {
return <span ref="span" className="truncated-text">{this.props.children}</span>; return <span className="truncated-text" style={{ WebkitLineClamp: this.props.lines }}>{this.props.children}</span>;
} }
}); });
@ -54,45 +39,109 @@ export let BusyMessage = React.createClass({
} }
}); });
var creditAmountStyle = {
color: '#216C2A',
fontWeight: 'bold',
fontSize: '0.8em'
}, estimateStyle = {
fontSize: '0.8em',
color: '#aaa',
};
export let CurrencySymbol = React.createClass({ export let CurrencySymbol = React.createClass({
render: function() { return <span>LBC</span>; } render: function() { return <span>LBC</span>; }
}); });
export let CreditAmount = React.createClass({ export let CreditAmount = React.createClass({
propTypes: { propTypes: {
amount: React.PropTypes.number, amount: React.PropTypes.number.isRequired,
precision: React.PropTypes.number precision: React.PropTypes.number,
isEstimate: React.PropTypes.bool,
label: React.PropTypes.bool,
showFree: React.PropTypes.bool,
look: React.PropTypes.oneOf(['indicator', 'plain']),
},
getDefaultProps: function() {
return {
precision: 1,
label: true,
showFree: false,
look: 'indicator',
}
}, },
render: function() { render: function() {
var formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision ? this.props.precision : 1); const formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision);
let amountText;
if (this.props.showFree && parseFloat(formattedAmount) == 0) {
amountText = 'free';
} else if (this.props.label) {
amountText = formattedAmount + (parseFloat(formattedAmount) == 1 ? ' credit' : ' credits');
} else {
amountText = formattedAmount;
}
return ( return (
<span className="credit-amount"> <span className={`credit-amount credit-amount--${this.props.look}`}>
<span style={creditAmountStyle}>{formattedAmount} {parseFloat(formattedAmount) == 1.0 ? 'credit' : 'credits'}</span> <span>
{ this.props.isEstimate ? <span style={estimateStyle}> (est)</span> : null } {amountText}
</span>
{ this.props.isEstimate ? <span className="credit-amount__estimate" title="This is an estimate and does not include data fees">*</span> : null }
</span> </span>
); );
} }
}); });
export let FilePrice = React.createClass({
_isMounted: false,
propTypes: {
uri: React.PropTypes.string.isRequired,
look: React.PropTypes.oneOf(['indicator', 'plain']),
},
getDefaultProps: function() {
return {
look: 'indicator',
}
},
componentWillMount: function() {
this.setState({
cost: null,
isEstimate: null,
});
},
componentDidMount: function() {
this._isMounted = true;
lbry.getCostInfo(this.props.uri).then(({cost, includesData}) => {
if (this._isMounted) {
this.setState({
cost: cost,
isEstimate: !includesData,
});
}
}, (err) => {
// If we get an error looking up cost information, do nothing
});
},
componentWillUnmount: function() {
this._isMounted = false;
},
render: function() {
if (this.state.cost === null) {
return <span className={`credit-amount credit-amount--${this.props.look}`}>???</span>;
}
return <CreditAmount label={false} amount={this.state.cost} isEstimate={this.state.isEstimate} showFree={true} />
}
});
var addressStyle = { var addressStyle = {
fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace', fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace',
}; };
export let Address = React.createClass({ export let Address = React.createClass({
_inputElem: null,
propTypes: { propTypes: {
address: React.PropTypes.string, address: React.PropTypes.string,
}, },
render: function() { render: function() {
return ( return (
<span style={addressStyle}>{this.props.address}</span> <input className="input-copyable" type="text" ref={(input) => { this._inputElem = input; }}
onFocus={() => { this._inputElem.select(); }} style={addressStyle} readOnly="readonly" value={this.props.address}></input>
); );
} }
}); });
@ -131,6 +180,9 @@ export let Thumbnail = React.createClass({
this._isMounted = false; this._isMounted = false;
}, },
render: function() { render: function() {
return <img ref="img" onError={this.handleError} {... this.props} src={this.state.imageUri} /> const className = this.props.className ? this.props.className : '',
otherProps = Object.assign({}, this.props)
delete otherProps.className;
return <img ref="img" onError={this.handleError} {...otherProps} className={className} src={this.state.imageUri} />
}, },
}); });

View file

@ -1,67 +0,0 @@
import lbry from '../lbry.js';
import React from 'react';
import {Link} from './link.js';
var DrawerItem = React.createClass({
getDefaultProps: function() {
return {
subPages: [],
};
},
render: function() {
var isSelected = (this.props.viewingPage == this.props.href.substr(1) ||
this.props.subPages.indexOf(this.props.viewingPage) != -1);
return <Link {...this.props} className={ 'drawer-item ' + (isSelected ? 'drawer-item-selected' : '') } />
}
});
var drawerImageStyle = { //@TODO: remove this, img should be properly scaled once size is settled
height: '36px'
};
var Drawer = React.createClass({
_balanceSubscribeId: null,
handleLogoClicked: function(event) {
if ((event.ctrlKey || event.metaKey) && event.shiftKey) {
window.location.href = '?developer'
event.preventDefault();
}
},
getInitialState: function() {
return {
balance: 0,
};
},
componentDidMount: function() {
this._balanceSubscribeId = lbry.balanceSubscribe(function(balance) {
this.setState({
balance: balance
});
}.bind(this));
},
componentWillUnmount: function() {
if (this._balanceSubscribeId) {
lbry.balanceUnsubscribe(this._balanceSubscribeId)
}
},
render: function() {
return (
<nav id="drawer">
<div id="drawer-handle">
<Link title="Close" onClick={this.props.onCloseDrawer} icon="icon-bars" className="close-drawer-link"/>
<a href="?discover" onMouseUp={this.handleLogoClicked}><img src={lbry.imagePath("lbry-dark-1600x528.png")} style={drawerImageStyle}/></a>
</div>
<DrawerItem href='?discover' viewingPage={this.props.viewingPage} label="Discover" icon="icon-search" />
<DrawerItem href='?publish' viewingPage={this.props.viewingPage} label="Publish" icon="icon-upload" />
<DrawerItem href='?downloaded' subPages={['published']} viewingPage={this.props.viewingPage} label="My Files" icon='icon-cloud-download' />
<DrawerItem href="?wallet" subPages={['send', 'receive', 'claim', 'referral']} viewingPage={this.props.viewingPage} label="My Wallet" badge={lbry.formatCredits(this.state.balance) } icon="icon-bank" />
<DrawerItem href='?settings' viewingPage={this.props.viewingPage} label="Settings" icon='icon-gear' />
<DrawerItem href='?help' viewingPage={this.props.viewingPage} label="Help" icon='icon-question-circle' />
</nav>
);
}
});
export default Drawer;

View file

@ -1,76 +1,24 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lbryuri from '../lbryuri.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import {Icon} from '../component/common.js'; import {Icon, FilePrice} from '../component/common.js';
import Modal from './modal.js'; import {Modal} from './modal.js';
import FormField from './form.js'; import {FormField} from './form.js';
import {ToolTip} from '../component/tooltip.js'; import {ToolTip} from '../component/tooltip.js';
import {DropDownMenu, DropDownMenuItem} from './menu.js'; import {DropDownMenu, DropDownMenuItem} from './menu.js';
const {shell} = require('electron'); const {shell} = require('electron');
let WatchLink = React.createClass({
propTypes: {
streamName: React.PropTypes.string,
downloadStarted: React.PropTypes.bool,
},
startVideo: function() {
window.location = '?watch=' + this.props.streamName;
},
handleClick: function() {
this.setState({
loading: true,
});
if (this.props.downloadStarted) {
this.startVideo();
} else {
lbry.getCostInfoForName(this.props.streamName, ({cost}) => {
lbry.getBalance((balance) => {
if (cost > balance) {
this.setState({
modal: 'notEnoughCredits',
loading: false,
});
} else {
this.startVideo();
}
});
});
}
},
getInitialState: function() {
return {
modal: null,
loading: false,
};
},
closeModal: function() {
this.setState({
modal: null,
});
},
render: function() {
return (
<div className="button-set-item">
<Link button="primary" disabled={this.state.loading} label="Watch" icon="icon-play" onClick={this.handleClick} />
<Modal contentLabel="Not enough credits" isOpen={this.state.modal == 'notEnoughCredits'} onConfirmed={this.closeModal}>
You don't have enough LBRY credits to pay for this stream.
</Modal>
</div>
);
}
});
let FileActionsRow = React.createClass({ let FileActionsRow = React.createClass({
_isMounted: false, _isMounted: false,
_fileInfoSubscribeId: null, _fileInfoSubscribeId: null,
propTypes: { propTypes: {
streamName: React.PropTypes.string, uri: React.PropTypes.string,
outpoint: React.PropTypes.string.isRequired, outpoint: React.PropTypes.string.isRequired,
metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]), metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]),
contentType: React.PropTypes.string, contentType: React.PropTypes.string.isRequired,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
@ -79,7 +27,7 @@ let FileActionsRow = React.createClass({
menuOpen: false, menuOpen: false,
deleteChecked: false, deleteChecked: false,
attemptingDownload: false, attemptingDownload: false,
attemptingRemove: false attemptingRemove: false,
} }
}, },
onFileInfoUpdate: function(fileInfo) { onFileInfoUpdate: function(fileInfo) {
@ -95,15 +43,15 @@ let FileActionsRow = React.createClass({
attemptingDownload: true, attemptingDownload: true,
attemptingRemove: false attemptingRemove: false
}); });
lbry.getCostInfoForName(this.props.streamName, ({cost}) => { lbry.getCostInfo(this.props.uri).then(({cost}) => {
lbry.getBalance((balance) => { lbry.getBalance((balance) => {
if (cost > balance) { if (cost > balance) {
this.setState({ this.setState({
modal: 'notEnoughCredits', modal: 'notEnoughCredits',
attemptingDownload: false, attemptingDownload: false,
}); });
} else { } else if (this.state.affirmedPurchase) {
lbry.getStream(this.props.streamName, (streamInfo) => { lbry.get({uri: this.props.uri}).then((streamInfo) => {
if (streamInfo === null || typeof streamInfo !== 'object') { if (streamInfo === null || typeof streamInfo !== 'object') {
this.setState({ this.setState({
modal: 'timedOut', modal: 'timedOut',
@ -111,6 +59,11 @@ let FileActionsRow = React.createClass({
}); });
} }
}); });
} else {
this.setState({
attemptingDownload: false,
modal: 'affirmPurchase'
})
} }
}); });
}); });
@ -153,6 +106,13 @@ let FileActionsRow = React.createClass({
attemptingDownload: false attemptingDownload: false
}); });
}, },
onAffirmPurchase: function() {
this.setState({
affirmedPurchase: true,
modal: null
});
this.tryDownload();
},
openMenu: function() { openMenu: function() {
this.setState({ this.setState({
menuOpen: !this.state.menuOpen, menuOpen: !this.state.menuOpen,
@ -196,11 +156,10 @@ let FileActionsRow = React.createClass({
linkBlock = <Link label="Open" button="text" icon="icon-folder-open" onClick={this.onOpenClick} />; linkBlock = <Link label="Open" button="text" icon="icon-folder-open" onClick={this.onOpenClick} />;
} }
const uri = lbryuri.normalize(this.props.uri);
const title = this.props.metadata ? this.props.metadata.title : uri;
return ( return (
<div> <div>
{this.props.contentType && this.props.contentType.startsWith('video/')
? <WatchLink streamName={this.props.streamName} downloadStarted={!!this.state.fileInfo} />
: null}
{this.state.fileInfo !== null || this.state.fileInfo.isMine {this.state.fileInfo !== null || this.state.fileInfo.isMine
? linkBlock ? linkBlock
: null} : null}
@ -209,18 +168,22 @@ let FileActionsRow = React.createClass({
<DropDownMenuItem key={0} onClick={this.handleRevealClicked} label={openInFolderMessage} /> <DropDownMenuItem key={0} onClick={this.handleRevealClicked} label={openInFolderMessage} />
<DropDownMenuItem key={1} onClick={this.handleRemoveClicked} label="Remove..." /> <DropDownMenuItem key={1} onClick={this.handleRemoveClicked} label="Remove..." />
</DropDownMenu> : '' } </DropDownMenu> : '' }
<Modal type="confirm" isOpen={this.state.modal == 'affirmPurchase'}
contentLabel="Confirm Purchase" onConfirmed={this.onAffirmPurchase} onAborted={this.closeModal}>
Are you sure you'd like to buy <strong>{title}</strong> for <strong><FilePrice uri={uri} metadata={this.props.metadata} label={false} look="plain" /></strong> credits?
</Modal>
<Modal isOpen={this.state.modal == 'notEnoughCredits'} contentLabel="Not enough credits" <Modal isOpen={this.state.modal == 'notEnoughCredits'} contentLabel="Not enough credits"
onConfirmed={this.closeModal}> onConfirmed={this.closeModal}>
You don't have enough LBRY credits to pay for this stream. You don't have enough LBRY credits to pay for this stream.
</Modal> </Modal>
<Modal isOpen={this.state.modal == 'timedOut'} contentLabel="Download failed" <Modal isOpen={this.state.modal == 'timedOut'} contentLabel="Download failed"
onConfirmed={this.closeModal}> onConfirmed={this.closeModal}>
LBRY was unable to download the stream <strong>lbry://{this.props.streamName}</strong>. LBRY was unable to download the stream <strong>{uri}</strong>.
</Modal> </Modal>
<Modal isOpen={this.state.modal == 'confirmRemove'} contentLabel="Not enough credits" <Modal isOpen={this.state.modal == 'confirmRemove'} contentLabel="Not enough credits"
type="confirm" confirmButtonLabel="Remove" onConfirmed={this.handleRemoveConfirmed} type="confirm" confirmButtonLabel="Remove" onConfirmed={this.handleRemoveConfirmed}
onAborted={this.closeModal}> onAborted={this.closeModal}>
<p>Are you sure you'd like to remove <cite>{this.props.metadata.title}</cite> from LBRY?</p> <p>Are you sure you'd like to remove <cite>{title}</cite> from LBRY?</p>
<label><FormField type="checkbox" checked={this.state.deleteChecked} onClick={this.handleDeleteCheckboxClicked} /> Delete this file from my computer</label> <label><FormField type="checkbox" checked={this.state.deleteChecked} onClick={this.handleDeleteCheckboxClicked} /> Delete this file from my computer</label>
</Modal> </Modal>
@ -234,7 +197,7 @@ export let FileActions = React.createClass({
_fileInfoSubscribeId: null, _fileInfoSubscribeId: null,
propTypes: { propTypes: {
streamName: React.PropTypes.string, uri: React.PropTypes.string,
outpoint: React.PropTypes.string.isRequired, outpoint: React.PropTypes.string.isRequired,
metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]), metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]),
contentType: React.PropTypes.string, contentType: React.PropTypes.string,
@ -261,7 +224,8 @@ export let FileActions = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this._isMounted = true; this._isMounted = true;
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate); this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
lbry.getStreamAvailability(this.props.uri, (availability) => {
lbry.get_availability({uri: this.props.uri}, (availability) => {
if (this._isMounted) { if (this._isMounted) {
this.setState({ this.setState({
available: availability > 0, available: availability > 0,
@ -291,10 +255,10 @@ export let FileActions = React.createClass({
return (<section className="file-actions"> return (<section className="file-actions">
{ {
fileInfo || this.state.available || this.state.forceShowActions fileInfo || this.state.available || this.state.forceShowActions
? <FileActionsRow outpoint={this.props.outpoint} metadata={this.props.metadata} streamName={this.props.streamName} ? <FileActionsRow outpoint={this.props.outpoint} metadata={this.props.metadata} uri={this.props.uri}
contentType={this.props.contentType} /> contentType={this.props.contentType} />
: <div> : <div>
<div className="button-set-item empty">This file is not currently available.</div> <div className="button-set-item empty">Content unavailable.</div>
<ToolTip label="Why?" <ToolTip label="Why?"
body="The content on LBRY is hosted by its users. It appears there are no users connected that have this file at the moment." body="The content on LBRY is hosted by its users. It appears there are no users connected that have this file at the moment."
className="button-set-item" /> className="button-set-item" />

View file

@ -1,67 +1,23 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lbryuri from '../lbryuri.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import {FileActions} from '../component/file-actions.js'; import {FileActions} from '../component/file-actions.js';
import {Thumbnail, TruncatedText, CreditAmount} from '../component/common.js'; import {BusyMessage, TruncatedText, FilePrice} from '../component/common.js';
import ChannelIndicator from '../component/channel-indicator.js'; import UriIndicator from '../component/channel-indicator.js';
let FilePrice = React.createClass({
_isMounted: false,
propTypes: {
uri: React.PropTypes.string
},
getInitialState: function() {
return {
cost: null,
costIncludesData: null,
}
},
componentDidMount: function() {
this._isMounted = true;
lbry.getCostInfoForName(this.props.uri, ({cost, includesData}) => {
if (this._isMounted) {
this.setState({
cost: cost,
costIncludesData: includesData,
});
}
}, () => {
// If we get an error looking up cost information, do nothing
});
},
componentWillUnmount: function() {
this._isMounted = false;
},
render: function() {
if (this.state.cost === null)
{
return null;
}
return (
<span className="file-tile__cost">
<CreditAmount amount={this.state.cost} isEstimate={!this.state.costIncludesData}/>
</span>
);
}
});
/*should be merged into FileTile once FileTile is refactored to take a single id*/ /*should be merged into FileTile once FileTile is refactored to take a single id*/
export let FileTileStream = React.createClass({ export let FileTileStream = React.createClass({
_fileInfoSubscribeId: null, _fileInfoSubscribeId: null,
_isMounted: null, _isMounted: null,
_metadata: null,
propTypes: { propTypes: {
uri: React.PropTypes.string, uri: React.PropTypes.string,
claimInfo: React.PropTypes.object, metadata: React.PropTypes.object,
contentType: React.PropTypes.string.isRequired,
outpoint: React.PropTypes.string, outpoint: React.PropTypes.string,
hasSignature: React.PropTypes.bool,
signatureIsValid: React.PropTypes.bool,
hideOnRemove: React.PropTypes.bool, hideOnRemove: React.PropTypes.bool,
hidePrice: React.PropTypes.bool, hidePrice: React.PropTypes.bool,
obscureNsfw: React.PropTypes.bool obscureNsfw: React.PropTypes.bool
@ -70,20 +26,15 @@ export let FileTileStream = React.createClass({
return { return {
showNsfwHelp: false, showNsfwHelp: false,
isHidden: false, isHidden: false,
available: null,
} }
}, },
getDefaultProps: function() { getDefaultProps: function() {
return { return {
obscureNsfw: !lbry.getClientSetting('showNsfw'), obscureNsfw: !lbry.getClientSetting('showNsfw'),
hidePrice: false hidePrice: false,
hasSignature: false,
} }
}, },
componentWillMount: function() {
const {value: {stream: {metadata, source: {contentType}}}} = this.props.claimInfo;
this._metadata = metadata;
this._contentType = contentType;
},
componentDidMount: function() { componentDidMount: function() {
this._isMounted = true; this._isMounted = true;
if (this.props.hideOnRemove) { if (this.props.hideOnRemove) {
@ -103,7 +54,7 @@ export let FileTileStream = React.createClass({
} }
}, },
handleMouseOver: function() { handleMouseOver: function() {
if (this.props.obscureNsfw && this.props.metadata && this._metadata.nsfw) { if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
this.setState({ this.setState({
showNsfwHelp: true, showNsfwHelp: true,
}); });
@ -121,39 +72,37 @@ export let FileTileStream = React.createClass({
return null; return null;
} }
const metadata = this._metadata; const uri = lbryuri.normalize(this.props.uri);
const isConfirmed = typeof metadata == 'object'; const metadata = this.props.metadata;
const title = isConfirmed ? metadata.title : ('lbry://' + this.props.uri); const isConfirmed = !!metadata;
const title = isConfirmed ? metadata.title : uri;
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw; const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
const primaryUrl = "?show=" + uri;
return ( return (
<section className={ 'file-tile card ' + (obscureNsfw ? 'card-obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}> <section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className={"row-fluid card-content file-tile__row"}> <a href={primaryUrl} className="card__link">
<div className="span3"> <div className={"card__inner file-tile__row"}>
<a href={'?show=' + this.props.uri}><Thumbnail className="file-tile__thumbnail" src={metadata.thumbnail} alt={'Photo for ' + (title || this.props.uri)} /></a> <div className="card__media"
style={{ backgroundImage: "url('" + (metadata && metadata.thumbnail ? metadata.thumbnail : lbry.imagePath('default-thumb.svg')) + "')" }}>
</div> </div>
<div className="span9"> <div className="file-tile__content">
<div className="card__title-primary">
{ !this.props.hidePrice { !this.props.hidePrice
? <FilePrice uri={this.props.uri} /> ? <FilePrice uri={this.props.uri} />
: null} : null}
<div className="meta"><a href={'?show=' + this.props.uri}>{'lbry://' + this.props.uri}</a></div> <div className="meta">{uri}</div>
<h3 className="file-tile__title"> <h3><TruncatedText lines={1}>{title}</TruncatedText></h3>
<a href={'?show=' + this.props.uri}> </div>
<TruncatedText lines={1}> <div className="card__content card__subtext">
{title}
</TruncatedText>
</a>
</h3>
<ChannelIndicator uri={this.props.uri} claimInfo={this.props.claimInfo} />
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this._contentType} />
<p className="file-tile__description">
<TruncatedText lines={3}> <TruncatedText lines={3}>
{isConfirmed {isConfirmed
? metadata.description ? metadata.description
: <span className="empty">This file is pending confirmation.</span>} : <span className="empty">This file is pending confirmation.</span>}
</TruncatedText> </TruncatedText>
</p>
</div> </div>
</div> </div>
</div>
</a>
{this.state.showNsfwHelp {this.state.showNsfwHelp
? <div className='card-overlay'> ? <div className='card-overlay'>
<p> <p>
@ -167,12 +116,113 @@ export let FileTileStream = React.createClass({
} }
}); });
export let FileCardStream = React.createClass({
_fileInfoSubscribeId: null,
_isMounted: null,
_metadata: null,
propTypes: {
uri: React.PropTypes.string,
claimInfo: React.PropTypes.object,
outpoint: React.PropTypes.string,
hideOnRemove: React.PropTypes.bool,
hidePrice: React.PropTypes.bool,
obscureNsfw: React.PropTypes.bool
},
getInitialState: function() {
return {
showNsfwHelp: false,
isHidden: false,
}
},
getDefaultProps: function() {
return {
obscureNsfw: !lbry.getClientSetting('showNsfw'),
hidePrice: false,
hasSignature: false,
}
},
componentDidMount: function() {
this._isMounted = true;
if (this.props.hideOnRemove) {
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
}
},
componentWillUnmount: function() {
if (this._fileInfoSubscribeId) {
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
}
},
onFileInfoUpdate: function(fileInfo) {
if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
this.setState({
isHidden: true
});
}
},
handleMouseOver: function() {
this.setState({
hovered: true,
});
},
handleMouseOut: function() {
this.setState({
hovered: false,
});
},
render: function() {
if (this.state.isHidden) {
return null;
}
const uri = lbryuri.normalize(this.props.uri);
const metadata = this.props.metadata;
const isConfirmed = !!metadata;
const title = isConfirmed ? metadata.title : uri;
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
const primaryUrl = '?show=' + uri;
return (
<section className={ 'card card--small card--link ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className="card__inner">
<a href={primaryUrl} className="card__link">
<div className="card__title-identity">
<h5 title={title}><TruncatedText lines={1}>{title}</TruncatedText></h5>
<div className="card__subtitle">
{ !this.props.hidePrice ? <span style={{float: "right"}}><FilePrice uri={this.props.uri} metadata={metadata} /></span> : null}
<UriIndicator uri={uri} metadata={metadata} contentType={this.props.contentType}
hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />
</div>
</div>
<div className="card__media" style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}></div>
<div className="card__content card__subtext card__subtext--two-lines">
<TruncatedText lines={2}>
{isConfirmed
? metadata.description
: <span className="empty">This file is pending confirmation.</span>}
</TruncatedText>
</div>
</a>
{this.state.showNsfwHelp && this.state.hovered
? <div className='card-overlay'>
<p>
This content is Not Safe For Work.
To view adult content, please change your <Link className="button-text" href="?settings" label="Settings" />.
</p>
</div>
: null}
</div>
</section>
);
}
});
export let FileTile = React.createClass({ export let FileTile = React.createClass({
_isMounted: false, _isMounted: false,
_isResolvePending: false,
propTypes: { propTypes: {
uri: React.PropTypes.string.isRequired, uri: React.PropTypes.string.isRequired,
available: React.PropTypes.bool,
}, },
getInitialState: function() { getInitialState: function() {
@ -181,30 +231,54 @@ export let FileTile = React.createClass({
claimInfo: null claimInfo: null
} }
}, },
resolve: function(uri) {
componentDidMount: function() { this._isResolvePending = true;
this._isMounted = true; lbry.resolve({uri: uri}).then((resolutionInfo) => {
this._isResolvePending = false;
lbry.resolve({uri: this.props.uri}).then(({claim: claimInfo}) => { if (this._isMounted && resolutionInfo && resolutionInfo.claim && resolutionInfo.claim.value &&
const {value: {stream: {metadata}}, txid, nout} = claimInfo; resolutionInfo.claim.value.stream && resolutionInfo.claim.value.stream.metadata) {
if (this._isMounted && claimInfo.value.stream.metadata) {
// In case of a failed lookup, metadata will be null, in which case the component will never display // In case of a failed lookup, metadata will be null, in which case the component will never display
this.setState({ this.setState({
outpoint: txid + ':' + nout, claimInfo: resolutionInfo.claim,
claimInfo: claimInfo,
}); });
} }
}); });
}, },
componentWillReceiveProps: function(nextProps) {
if (nextProps.uri != this.props.uri) {
this.setState(this.getInitialState());
this.resolve(nextProps.uri);
}
},
componentDidMount: function() {
this._isMounted = true;
this.resolve(this.props.uri);
},
componentWillUnmount: function() { componentWillUnmount: function() {
this._isMounted = false; this._isMounted = false;
}, },
render: function() { render: function() {
if (!this.state.claimInfo || !this.state.outpoint) { if (!this.state.claimInfo) {
if (this.props.displayStyle == 'card') {
return <FileCardStream outpoint={null} metadata={{title: this.props.uri, description: "Loading..."}} contentType={null} hidePrice={true}
hasSignature={false} signatureIsValid={false} uri={this.props.uri} />
}
if (this.props.showEmpty)
{
return this._isResolvePending ?
<BusyMessage message="Loading magic decentralized data" /> :
<div className="empty">{lbryuri.normalize(this.props.uri)} is unclaimed. <Link label="Put something here" href="?publish" /></div>;
}
return null; return null;
} }
return <FileTileStream outpoint={this.state.outpoint} claimInfo={this.state.claimInfo} const {txid, nout, has_signature, signature_is_valid,
{... this.props} uri={this.props.uri}/>; value: {stream: {metadata, source: {contentType}}}} = this.state.claimInfo;
return this.props.displayStyle == 'card' ?
<FileCardStream outpoint={txid + ':' + nout} metadata={metadata} contentType={contentType}
hasSignature={has_signature} signatureIsValid={signature_is_valid} {... this.props}/> :
<FileTileStream outpoint={txid + ':' + nout} metadata={metadata} contentType={contentType}
hasSignature={has_signature} signatureIsValid={signature_is_valid} {... this.props} />;
} }
}); });

View file

@ -1,28 +1,32 @@
import React from 'react'; import React from 'react';
import {Icon} from './common.js'; import {Icon} from './common.js';
var requiredFieldWarningStyle = { var formFieldCounter = 0,
color: '#cc0000', formFieldNestedLabelTypes = ['radio', 'checkbox'];
transition: 'opacity 400ms ease-in',
};
var FormField = React.createClass({ function formFieldId() {
return "form-field-" + (++formFieldCounter);
}
export let FormField = React.createClass({
_fieldRequiredText: 'This field is required', _fieldRequiredText: 'This field is required',
_type: null, _type: null,
_element: null, _element: null,
propTypes: { propTypes: {
type: React.PropTypes.string.isRequired, type: React.PropTypes.string.isRequired,
hidden: React.PropTypes.bool, prefix: React.PropTypes.string,
postfix: React.PropTypes.string,
hasError: React.PropTypes.bool
}, },
getInitialState: function() { getInitialState: function() {
return { return {
adviceState: 'hidden', isError: null,
adviceText: null, errorMessage: null,
} }
}, },
componentWillMount: function() { componentWillMount: function() {
if (['text', 'radio', 'checkbox', 'file'].includes(this.props.type)) { if (['text', 'number', 'radio', 'checkbox', 'file'].includes(this.props.type)) {
this._element = 'input'; this._element = 'input';
this._type = this.props.type; this._type = this.props.type;
} else if (this.props.type == 'text-number') { } else if (this.props.type == 'text-number') {
@ -33,25 +37,11 @@ var FormField = React.createClass({
this._element = this.props.type; this._element = this.props.type;
} }
}, },
showAdvice: function(text) { showError: function(text) {
this.setState({ this.setState({
adviceState: 'shown', isError: true,
adviceText: text, errorMessage: text,
}); });
setTimeout(() => {
this.setState({
adviceState: 'fading',
});
setTimeout(() => {
this.setState({
adviceState: 'hidden',
});
}, 450);
}, 5000);
},
warnRequired: function() {
this.showAdvice(this._fieldRequiredText);
}, },
focus: function() { focus: function() {
this.refs.field.focus(); this.refs.field.focus();
@ -60,7 +50,8 @@ var FormField = React.createClass({
if (this.props.type == 'checkbox') { if (this.props.type == 'checkbox') {
return this.refs.field.checked; return this.refs.field.checked;
} else if (this.props.type == 'file') { } else if (this.props.type == 'file') {
return this.refs.field.files[0].path; return this.refs.field.files.length && this.refs.field.files[0].path ?
this.refs.field.files[0].path : null;
} else { } else {
return this.refs.field.value; return this.refs.field.value;
} }
@ -70,45 +61,94 @@ var FormField = React.createClass({
}, },
render: function() { render: function() {
// Pass all unhandled props to the field element // Pass all unhandled props to the field element
const otherProps = Object.assign({}, this.props); const otherProps = Object.assign({}, this.props),
delete otherProps.type; isError = this.state.isError !== null ? this.state.isError : this.props.hasError,
delete otherProps.hidden; elementId = this.props.id ? this.props.id : formFieldId(),
renderElementInsideLabel = this.props.label && formFieldNestedLabelTypes.includes(this.props.type);
return ( delete otherProps.type;
!this.props.hidden delete otherProps.label;
? <div className="form-field-container"> delete otherProps.hasError;
<this._element type={this._type} className="form-field" name={this.props.name} ref="field" placeholder={this.props.placeholder} delete otherProps.className;
className={'form-field--' + this.props.type + ' ' + (this.props.className || '')} delete otherProps.postfix;
delete otherProps.prefix;
const element = <this._element id={elementId} type={this._type} name={this.props.name} ref="field" placeholder={this.props.placeholder}
className={'form-field__input form-field__input-' + this.props.type + ' ' + (this.props.className || '') + (isError ? 'form-field__input--error' : '')}
{...otherProps}> {...otherProps}>
{this.props.children} {this.props.children}
</this._element> </this._element>;
<FormFieldAdvice field={this.refs.field} state={this.state.adviceState}>{this.state.adviceText}</FormFieldAdvice>
</div>
: null
);
}
});
var FormFieldAdvice = React.createClass({ return <div className="form-field">
{ this.props.prefix ? <span className="form-field__prefix">{this.props.prefix}</span> : '' }
{ renderElementInsideLabel ?
<label htmlFor={elementId} className={"form-field__label " + (isError ? 'form-field__label--error' : '')}>
{element}
{this.props.label}
</label> :
element }
{ this.props.postfix ? <span className="form-field__postfix">{this.props.postfix}</span> : '' }
{ isError && this.state.errorMessage ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
</div>
}
})
export let FormRow = React.createClass({
_fieldRequiredText: 'This field is required',
propTypes: { propTypes: {
state: React.PropTypes.string.isRequired, label: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element])
// helper: React.PropTypes.html,
},
getInitialState: function() {
return {
isError: false,
errorMessage: null,
}
},
showError: function(text) {
this.setState({
isError: true,
errorMessage: text,
});
},
showRequiredError: function() {
this.showError(this._fieldRequiredText);
},
clearError: function(text) {
this.setState({
isError: false,
errorMessage: ''
});
},
getValue: function() {
return this.refs.field.getValue();
},
getSelectedElement: function() {
return this.refs.field.getSelectedElement();
},
focus: function() {
this.refs.field.focus();
}, },
render: function() { render: function() {
return ( const fieldProps = Object.assign({}, this.props),
this.props.state != 'hidden' elementId = formFieldId(),
? <div className="form-field-advice-container"> renderLabelInFormField = formFieldNestedLabelTypes.includes(this.props.type);
<div className={'form-field-advice' + (this.props.state == 'fading' ? ' form-field-advice--fading' : '')}>
<Icon icon="icon-caret-up" className="form-field-advice__arrow" />
<div className="form-field-advice__content-container">
<span className="form-field-advice__content">
{this.props.children}
</span>
</div>
</div>
</div>
: null
);
}
});
export default FormField; if (!renderLabelInFormField) {
delete fieldProps.label;
}
delete fieldProps.helper;
return <div className="form-row">
{ this.props.label && !renderLabelInFormField ?
<div className={"form-row__label-row " + (this.props.labelPrefix ? "form-row__label-row--prefix" : "") }>
<label htmlFor={elementId} className={"form-field__label " + (this.state.isError ? 'form-field__label--error' : '')}>
{this.props.label}
</label>
</div> : '' }
<FormField ref="field" hasError={this.state.isError} {...fieldProps} />
{ !this.state.isError && this.props.helper ? <div className="form-field__helper">{this.props.helper}</div> : '' }
{ this.state.isError ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
</div>
}
})

View file

@ -1,74 +1,198 @@
import React from 'react'; import React from 'react';
import lbryuri from '../lbryuri.js';
import {Link} from './link.js'; import {Link} from './link.js';
import {Icon, CreditAmount} from './common.js';
var Header = React.createClass({ var Header = React.createClass({
_balanceSubscribeId: null,
_isMounted: false,
propTypes: {
onSearch: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired
},
getInitialState: function() { getInitialState: function() {
return { return {
title: "LBRY", balance: 0
isScrolled: false
}; };
}, },
componentWillMount: function() {
new MutationObserver(function(mutations) {
this.setState({ title: mutations[0].target.textContent });
}.bind(this)).observe(
document.querySelector('title'),
{ subtree: true, characterData: true, childList: true }
);
},
componentDidMount: function() { componentDidMount: function() {
document.addEventListener('scroll', this.handleScroll); this._isMounted = true;
}, this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
componentWillUnmount: function() { if (this._isMounted) {
document.removeEventListener('scroll', this.handleScroll); this.setState({balance: balance});
if (this.userTypingTimer)
{
clearTimeout(this.userTypingTimer);
} }
},
handleScroll: function() {
this.setState({
isScrolled: document.body.scrollTop > 0
}); });
}, },
onQueryChange: function(event) { componentWillUnmount: function() {
this._isMounted = false;
if (this.userTypingTimer) if (this._balanceSubscribeId) {
{ lbry.balanceUnsubscribe(this._balanceSubscribeId)
clearTimeout(this.userTypingTimer);
} }
//@TODO: Switch to React.js timing
var searchTerm = event.target.value;
this.userTypingTimer = setTimeout(() => {
this.props.onSearch(searchTerm);
}, 800); // 800ms delay, tweak for faster/slower
}, },
render: function() { render: function() {
return ( return <header id="header">
<header id="header" className={ (this.state.isScrolled ? 'header-scrolled' : 'header-unscrolled') + ' ' + (this.props.links ? 'header-with-subnav' : 'header-no-subnav') }> <div className="header__item">
<div className="header-top-bar"> <Link onClick={() => { lbry.back() }} button="alt button--flat" icon="icon-arrow-left" />
<Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" />
<h1>{ this.state.title }</h1>
<div className="header-search">
<input type="search" onChange={this.onQueryChange} defaultValue={this.props.initialQuery}
placeholder="Find movies, music, games, and more"/>
</div> </div>
<div className="header__item">
<Link href="?discover" button="alt button--flat" icon="icon-home" />
</div>
<div className="header__item header__item--wunderbar">
<WunderBar address={this.props.address} icon={this.props.wunderBarIcon}
onSearch={this.props.onSearch} onSubmit={this.props.onSubmit} viewingPage={this.props.viewingPage} />
</div>
<div className="header__item">
<Link href="?wallet" button="text" icon="icon-bank" label={lbry.formatCredits(this.state.balance, 1)} ></Link>
</div>
<div className="header__item">
<Link button="primary button--flat" href="?publish" icon="icon-upload" label="Publish" />
</div>
<div className="header__item">
<Link button="alt button--flat" href="?downloaded" icon="icon-folder" />
</div>
<div className="header__item">
<Link button="alt button--flat" href="?settings" icon="icon-gear" />
</div> </div>
{
this.props.links ?
<SubHeader links={this.props.links} viewingPage={this.props.viewingPage} /> :
''
}
</header> </header>
);
} }
}); });
var SubHeader = React.createClass({ class WunderBar extends React.PureComponent {
static propTypes = {
onSearch: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired
}
constructor(props) {
super(props);
this._userTypingTimer = null;
this._input = null;
this._stateBeforeSearch = null;
this._resetOnNextBlur = true;
this.onChange = this.onChange.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.onReceiveRef = this.onReceiveRef.bind(this);
this.state = {
address: this.props.address,
icon: this.props.icon
};
}
componentWillUnmount() {
if (this.userTypingTimer) {
clearTimeout(this._userTypingTimer);
}
}
onChange(event) {
if (this._userTypingTimer)
{
clearTimeout(this._userTypingTimer);
}
this.setState({ address: event.target.value })
let searchTerm = event.target.value;
this._userTypingTimer = setTimeout(() => {
this._resetOnNextBlur = false;
this.props.onSearch(searchTerm);
}, 800); // 800ms delay, tweak for faster/slower
}
componentWillReceiveProps(nextProps) {
if (nextProps.viewingPage !== this.props.viewingPage || nextProps.address != this.props.address) {
this.setState({ address: nextProps.address, icon: nextProps.icon });
}
}
onFocus() {
this._stateBeforeSearch = this.state;
let newState = {
icon: "icon-search",
isActive: true
}
this._focusPending = true;
//below is hacking, improved when we have proper routing
if (!this.state.address.startsWith('lbry://') && this.state.icon !== "icon-search") //onFocus, if they are not on an exact URL or a search page, clear the bar
{
newState.address = '';
}
this.setState(newState);
}
onBlur() {
let commonState = {isActive: false};
if (this._resetOnNextBlur) {
this.setState(Object.assign({}, this._stateBeforeSearch, commonState));
this._input.value = this.state.address;
} else {
this._resetOnNextBlur = true;
this._stateBeforeSearch = this.state;
this.setState(commonState);
}
}
componentDidUpdate() {
this._input.value = this.state.address;
if (this._input && this._focusPending) {
this._input.select();
this._focusPending = false;
}
}
onKeyPress(event) {
if (event.charCode == 13 && this._input.value) {
let uri = null,
method = "onSubmit";
this._resetOnNextBlur = false;
clearTimeout(this._userTypingTimer);
try {
uri = lbryuri.normalize(this._input.value);
this.setState({ value: uri });
} catch (error) { //then it's not a valid URL, so let's search
uri = this._input.value;
method = "onSearch";
}
this.props[method](uri);
this._input.blur();
}
}
onReceiveRef(ref) {
this._input = ref;
}
render() {
return (
<div className={'wunderbar' + (this.state.isActive ? ' wunderbar--active' : '')}>
{this.state.icon ? <Icon fixed icon={this.state.icon} /> : '' }
<input className="wunderbar__input" type="search" placeholder="Type a LBRY address or search term"
ref={this.onReceiveRef}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChange={this.onChange}
onKeyPress={this.onKeyPress}
value={this.state.address}
placeholder="Find movies, music, games, and more" />
</div>
);
}
}
export let SubHeader = React.createClass({
render: function() { render: function() {
var links = [], let links = [],
viewingUrl = '?' + this.props.viewingPage; viewingUrl = '?' + this.props.viewingPage;
for (let link of Object.keys(this.props.links)) { for (let link of Object.keys(this.props.links)) {
@ -79,7 +203,7 @@ var SubHeader = React.createClass({
); );
} }
return ( return (
<nav className="sub-header"> <nav className={'sub-header' + (this.props.modifier ? ' sub-header--' + this.props.modifier : '')}>
{links} {links}
</nav> </nav>
); );

View file

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import {Icon} from './common.js'; import {Icon} from './common.js';
import Modal from '../component/modal.js';
import rewards from '../rewards.js';
export let Link = React.createClass({ export let Link = React.createClass({
propTypes: { propTypes: {
@ -39,7 +41,7 @@ export let Link = React.createClass({
content = ( content = (
<span {... 'button' in this.props ? {className: 'button__content'} : {}}> <span {... 'button' in this.props ? {className: 'button__content'} : {}}>
{'icon' in this.props ? <Icon icon={this.props.icon} fixed={true} /> : null} {'icon' in this.props ? <Icon icon={this.props.icon} fixed={true} /> : null}
{<span className="link-label">{this.props.label}</span>} {this.props.label ? <span className="link-label">{this.props.label}</span> : null}
{'badge' in this.props ? <span className="badge">{this.props.badge}</span> : null} {'badge' in this.props ? <span className="badge">{this.props.badge}</span> : null}
</span> </span>
); );
@ -53,3 +55,79 @@ export let Link = React.createClass({
); );
} }
}); });
export let RewardLink = React.createClass({
propTypes: {
type: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool,
onRewardClaim: React.PropTypes.func,
onRewardFailure: React.PropTypes.func
},
refreshClaimable: function() {
switch(this.props.type) {
case 'new_user':
this.setState({ claimable: true });
return;
case 'first_publish':
lbry.claim_list_mine().then((list) => {
this.setState({
claimable: list.length > 0
})
});
return;
}
},
componentWillMount: function() {
this.refreshClaimable();
},
getInitialState: function() {
return {
claimable: true,
pending: false,
errorMessage: null
}
},
claimReward: function() {
this.setState({
pending: true
})
rewards.claimReward(this.props.type).then((reward) => {
this.setState({
pending: false,
errorMessage: null
})
if (this.props.onRewardClaim) {
this.props.onRewardClaim(reward);
}
}).catch((error) => {
this.setState({
errorMessage: error.message,
pending: false
})
})
},
clearError: function() {
if (this.props.onRewardFailure) {
this.props.onRewardFailure()
}
this.setState({
errorMessage: null
})
},
render: function() {
return (
<div className="reward-link">
{this.props.claimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <Link button={this.props.button ? this.props.button : 'alt'} disabled={this.state.pending || !this.state.claimable }
label={ this.state.pending ? "Claiming..." : "Claim Reward"} onClick={this.claimReward} />}
{this.state.errorMessage ?
<Modal isOpen={true} contentLabel="Reward Claim Error" className="error-modal" onConfirmed={this.clearError}>
{this.state.errorMessage}
</Modal>
: ''}
</div>
);
}
});

View file

@ -9,9 +9,6 @@ var LoadScreen = React.createClass({
details: React.PropTypes.string, details: React.PropTypes.string,
isWarning: React.PropTypes.bool, isWarning: React.PropTypes.bool,
}, },
handleCancelClick: function() {
history.back();
},
getDefaultProps: function() { getDefaultProps: function() {
return { return {
isWarning: false, isWarning: false,
@ -34,9 +31,6 @@ var LoadScreen = React.createClass({
<BusyMessage message={this.props.message} /> <BusyMessage message={this.props.message} />
</h3> </h3>
{this.props.isWarning ? <Icon icon="icon-warning" /> : null} <span className={'load-screen__details ' + (this.props.isWarning ? 'load-screen__details--warning' : '')}>{this.props.details}</span> {this.props.isWarning ? <Icon icon="icon-warning" /> : null} <span className={'load-screen__details ' + (this.props.isWarning ? 'load-screen__details--warning' : '')}>{this.props.details}</span>
{window.history.length > 1
? <div><Link label="Cancel" onClick={this.handleCancelClick} className='load-screen__cancel-link button-text' /></div>
: null}
</div> </div>
</div> </div>
); );

View file

@ -0,0 +1,18 @@
import React from 'react';
import ReactModal from 'react-modal';
export const ModalPage = React.createClass({
render: function() {
return (
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
className={(this.props.className || '') + ' modal-page'}
overlayClassName="modal-overlay">
<div className="modal-page__content">
{this.props.children}
</div>
</ReactModal>
);
}
});
export default ModalPage;

View file

@ -6,6 +6,7 @@ import {Link} from './link.js';
export const Modal = React.createClass({ export const Modal = React.createClass({
propTypes: { propTypes: {
type: React.PropTypes.oneOf(['alert', 'confirm', 'custom']), type: React.PropTypes.oneOf(['alert', 'confirm', 'custom']),
overlay: React.PropTypes.bool,
onConfirmed: React.PropTypes.func, onConfirmed: React.PropTypes.func,
onAborted: React.PropTypes.func, onAborted: React.PropTypes.func,
confirmButtonLabel: React.PropTypes.string, confirmButtonLabel: React.PropTypes.string,
@ -16,6 +17,7 @@ export const Modal = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
type: 'alert', type: 'alert',
overlay: true,
confirmButtonLabel: 'OK', confirmButtonLabel: 'OK',
abortButtonLabel: 'Cancel', abortButtonLabel: 'Cancel',
confirmButtonDisabled: false, confirmButtonDisabled: false,
@ -26,7 +28,7 @@ export const Modal = React.createClass({
return ( return (
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props} <ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
className={(this.props.className || '') + ' modal'} className={(this.props.className || '') + ' modal'}
overlayClassName={(this.props.overlayClassName || '') + ' modal-overlay'}> overlayClassName={![null, undefined, ""].includes(this.props.overlayClassName) ? this.props.overlayClassName : 'modal-overlay'}>
<div> <div>
{this.props.children} {this.props.children}
</div> </div>

21
ui/js/component/notice.js Normal file
View file

@ -0,0 +1,21 @@
import React from 'react';
export const Notice = React.createClass({
propTypes: {
isError: React.PropTypes.bool,
},
getDefaultProps: function() {
return {
isError: false,
};
},
render: function() {
return (
<section className={'notice ' + (this.props.isError ? 'notice--error ' : '') + (this.props.className || '')}>
{this.props.children}
</section>
);
},
});
export default Notice;

View file

@ -0,0 +1,57 @@
import React from 'react';
import lbry from '../lbry.js';
export const SnackBar = React.createClass({
_displayTime: 5, // in seconds
_hideTimeout: null,
getInitialState: function() {
return {
snacks: []
}
},
handleSnackReceived: function(event) {
// if (this._hideTimeout) {
// clearTimeout(this._hideTimeout);
// }
let snacks = this.state.snacks;
snacks.push(event.detail);
this.setState({ snacks: snacks});
},
componentWillMount: function() {
document.addEventListener('globalNotice', this.handleSnackReceived);
},
componentWillUnmount: function() {
document.removeEventListener('globalNotice', this.handleSnackReceived);
},
render: function() {
if (!this.state.snacks.length) {
this._hideTimeout = null; //should be unmounting anyway, but be safe?
return null;
}
let snack = this.state.snacks[0];
if (this._hideTimeout === null) {
this._hideTimeout = setTimeout(() => {
this._hideTimeout = null;
let snacks = this.state.snacks;
snacks.shift();
this.setState({ snacks: snacks });
}, this._displayTime * 1000);
}
return (
<div className="snack-bar">
{snack.message}
{snack.linkText && snack.linkTarget ?
<a className="snack-bar__action" href={snack.linkTarget}>{snack.linkText}</a> : ''}
</div>
);
},
});
export default SnackBar;

View file

@ -13,11 +13,12 @@ var SplashScreen = React.createClass({
isLagging: false, isLagging: false,
} }
}, },
updateStatus: function(was_lagging=false) { updateStatus: function() {
lbry.getDaemonStatus(this._updateStatusCallback); lbry.status().then(this._updateStatusCallback);
}, },
_updateStatusCallback: function(status) { _updateStatusCallback: function(status) {
if (status.code == 'started') { const startupStatus = status.startup_status
if (startupStatus.code == 'started') {
// Wait until we are able to resolve a name before declaring // Wait until we are able to resolve a name before declaring
// that we are done. // that we are done.
// TODO: This is a hack, and the logic should live in the daemon // TODO: This is a hack, and the logic should live in the daemon
@ -27,27 +28,35 @@ var SplashScreen = React.createClass({
isLagging: false isLagging: false
}); });
lbry.resolveName('one', () => { lbry.resolve({uri: 'lbry://one'}).then(() => {
window.sessionStorage.setItem('loaded', 'y') window.sessionStorage.setItem('loaded', 'y')
this.props.onLoadDone(); this.props.onLoadDone();
}); });
return; return;
} }
this.setState({ this.setState({
details: status.message + (status.is_lagging ? '' : '...'), details: startupStatus.message + (startupStatus.is_lagging ? '' : '...'),
isLagging: status.is_lagging, isLagging: startupStatus.is_lagging,
}); });
setTimeout(() => { setTimeout(() => {
this.updateStatus(status.is_lagging); this.updateStatus();
}, 500); }, 500);
}, },
componentDidMount: function() { componentDidMount: function() {
lbry.connect((connected) => { lbry.connect().then((isConnected) => {
if (isConnected) {
this.updateStatus(); this.updateStatus();
}); } else {
this.setState({
isLagging: true,
message: "Failed to connect to LBRY",
details: "LBRY was unable to start and connect properly."
})
}
})
}, },
render: function() { render: function() {
return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />; return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />
} }
}); });

View file

@ -1,32 +1,46 @@
import lbryio from './lbryio.js';
import lighthouse from './lighthouse.js'; import lighthouse from './lighthouse.js';
import jsonrpc from './jsonrpc.js'; import jsonrpc from './jsonrpc.js';
import {getLocal, setLocal} from './utils.js'; import lbryuri from './lbryuri.js';
import {getLocal, getSession, setSession, setLocal} from './utils.js';
const {remote} = require('electron'); const {remote, ipcRenderer} = require('electron');
const menu = remote.require('./menu/main-menu'); const menu = remote.require('./menu/main-menu');
/** /**
* Records a publish attempt in local storage. Returns a dictionary with all the data needed to * Records a publish attempt in local storage. Returns a dictionary with all the data needed to
* needed to make a dummy claim or file info object. * needed to make a dummy claim or file info object.
*/ */
function savePendingPublish(name) { function savePendingPublish({name, channel_name}) {
let uri;
if (channel_name) {
uri = lbryuri.build({name: channel_name, path: name}, false);
} else {
uri = lbryuri.build({name: name}, false);
}
const pendingPublishes = getLocal('pendingPublishes') || []; const pendingPublishes = getLocal('pendingPublishes') || [];
const newPendingPublish = { const newPendingPublish = {
claim_id: 'pending_claim_' + name, name, channel_name,
txid: 'pending_' + name, claim_id: 'pending_claim_' + uri,
txid: 'pending_' + uri,
nout: 0, nout: 0,
outpoint: 'pending_' + name + ':0', outpoint: 'pending_' + uri + ':0',
name: name,
time: Date.now(), time: Date.now(),
}; };
setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]); setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]);
return newPendingPublish; return newPendingPublish;
} }
function removePendingPublish({name, outpoint}) { /**
setLocal('pendingPublishes', getPendingPublishes().filter( * If there is a pending publish with the given name or outpoint, remove it.
(pub) => pub.name != name && pub.outpoint != outpoint * A channel name may also be provided along with name.
)); */
function removePendingPublishIfNeeded({name, channel_name, outpoint}) {
function pubMatches(pub) {
return pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name));
}
setLocal('pendingPublishes', getPendingPublishes().filter(pub => !pubMatches(pub)));
} }
/** /**
@ -35,61 +49,30 @@ function removePendingPublish({name, outpoint}) {
*/ */
function getPendingPublishes() { function getPendingPublishes() {
const pendingPublishes = getLocal('pendingPublishes') || []; const pendingPublishes = getLocal('pendingPublishes') || [];
const newPendingPublishes = pendingPublishes.filter(pub => Date.now() - pub.time <= lbry.pendingPublishTimeout);
const newPendingPublishes = [];
for (let pendingPublish of pendingPublishes) {
if (Date.now() - pendingPublish.time <= lbry.pendingPublishTimeout) {
newPendingPublishes.push(pendingPublish);
}
}
setLocal('pendingPublishes', newPendingPublishes); setLocal('pendingPublishes', newPendingPublishes);
return newPendingPublishes return newPendingPublishes;
} }
/** /**
* Gets a pending publish attempt by its name or (fake) outpoint. If none is found (or one is found * Gets a pending publish attempt by its name or (fake) outpoint. A channel name can also be
* but it has timed out), returns null. * provided along withe the name. If no pending publish is found, returns null.
*/ */
function getPendingPublish({name, outpoint}) { function getPendingPublish({name, channel_name, outpoint}) {
const pendingPublishes = getPendingPublishes(); const pendingPublishes = getPendingPublishes();
const pendingPublishIndex = pendingPublishes.findIndex( return pendingPublishes.find(
({name: itemName, outpoint: itemOutpoint}) => itemName == name || itemOutpoint == outpoint pub => pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name))
); ) || null;
const pendingPublish = pendingPublishes[pendingPublishIndex];
if (pendingPublishIndex == -1) {
return null;
} else if (Date.now() - pendingPublish.time > lbry.pendingPublishTimeout) {
// Pending publish timed out, so remove it from the stored list and don't match
const newPendingPublishes = pendingPublishes.slice();
newPendingPublishes.splice(pendingPublishIndex, 1);
setLocal('pendingPublishes', newPendingPublishes);
return null;
} else {
return pendingPublish;
}
} }
function pendingPublishToDummyClaim({name, outpoint, claim_id, txid, nout}) { function pendingPublishToDummyClaim({channel_name, name, outpoint, claim_id, txid, nout}) {
return { return {name, outpoint, claim_id, txid, nout, channel_name};
name: name,
outpoint: outpoint,
claim_id: claim_id,
txid: txid,
nout: nout,
};
} }
function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) { function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) {
return { return {name, outpoint, claim_id, metadata: null};
name: name,
outpoint: outpoint,
claim_id: claim_id,
metadata: "Attempting publication",
};
} }
window.pptdfi = pendingPublishToDummyFileInfo;
let lbry = { let lbry = {
isConnected: false, isConnected: false,
@ -112,33 +95,55 @@ let lbry = {
}; };
lbry.call = function (method, params, callback, errorCallback, connectFailedCallback) { lbry.call = function (method, params, callback, errorCallback, connectFailedCallback) {
jsonrpc.call(lbry.daemonConnectionString, method, [params], callback, errorCallback, connectFailedCallback); jsonrpc.call(lbry.daemonConnectionString, method, params, callback, errorCallback, connectFailedCallback);
} }
//core //core
lbry.connect = function(callback) lbry._connectPromise = null;
{ lbry.connect = function() {
if (lbry._connectPromise === null) {
lbry._connectPromise = new Promise((resolve, reject) => {
// Check every half second to see if the daemon is accepting connections // Check every half second to see if the daemon is accepting connections
// Once this returns True, can call getDaemonStatus to see where function checkDaemonStarted(tryNum = 0) {
// we are in the startup process
function checkDaemonStarted(tryNum=0) {
lbry.isDaemonAcceptingConnections(function (runningStatus) { lbry.isDaemonAcceptingConnections(function (runningStatus) {
if (runningStatus) { if (runningStatus) {
lbry.isConnected = true; resolve(true);
callback(true); }
} else { else {
if (tryNum <= 600) { // Move # of tries into constant or config option if (tryNum <= 600) { // Move # of tries into constant or config option
setTimeout(function () { setTimeout(function () {
checkDaemonStarted(tryNum + 1); checkDaemonStarted(tryNum + 1);
}, 500); }, tryNum < 100 ? 200 : 1000);
} else { }
callback(false); else {
reject(new Error("Unable to connect to LBRY"));
} }
} }
}); });
} }
checkDaemonStarted(); checkDaemonStarted();
});
}
return lbry._connectPromise;
}
//kill this but still better than document.title =, which this replaced
lbry.setTitle = function(title) {
document.title = title + " - LBRY";
}
//kill this with proper routing
lbry.back = function() {
if (window.history.length > 1) {
window.history.back();
} else {
window.location.href = "?discover";
}
} }
lbry.isDaemonAcceptingConnections = function (callback) { lbry.isDaemonAcceptingConnections = function (callback) {
@ -146,10 +151,6 @@ lbry.isDaemonAcceptingConnections = function (callback) {
lbry.call('status', {}, () => callback(true), null, () => callback(false)) lbry.call('status', {}, () => callback(true), null, () => callback(false))
}; };
lbry.getDaemonStatus = function (callback) {
lbry.call('daemon_status', {}, callback);
};
lbry.checkFirstRun = function(callback) { lbry.checkFirstRun = function(callback) {
lbry.call('is_first_run', {}, callback); lbry.call('is_first_run', {}, callback);
} }
@ -189,23 +190,6 @@ lbry.sendToAddress = function(amount, address, callback, errorCallback) {
lbry.call("send_amount_to_address", { "amount" : amount, "address": address }, callback, errorCallback); lbry.call("send_amount_to_address", { "amount" : amount, "address": address }, callback, errorCallback);
} }
lbry.resolveName = function(name, callback) {
if (!name) {
throw new Error(`Name required.`);
}
lbry.call('resolve_name', { 'name': name }, callback, () => {
// For now, assume any error means the name was not resolved
callback(null);
});
}
lbry.getStream = function(name, callback) {
if (!name) {
throw new Error(`Name required.`);
}
lbry.call('get', { 'name': name }, callback);
};
lbry.getClaimInfo = function(name, callback) { lbry.getClaimInfo = function(name, callback) {
if (!name) { if (!name) {
throw new Error(`Name required.`); throw new Error(`Name required.`);
@ -219,23 +203,6 @@ lbry.getMyClaim = function(name, callback) {
}); });
} }
lbry.getKeyFee = function(name, callback, errorCallback) {
if (!name) {
throw new Error(`Name required.`);
}
lbry.call('stream_cost_estimate', { name: name }, callback, errorCallback);
}
lbry.getTotalCost = function(name, size, callback, errorCallback) {
if (!name) {
throw new Error(`Name required.`);
}
lbry.call('stream_cost_estimate', {
name: name,
size: size,
}, callback, errorCallback);
}
lbry.getPeersForBlobHash = function(blobHash, callback) { lbry.getPeersForBlobHash = function(blobHash, callback) {
let timedOut = false; let timedOut = false;
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
@ -251,16 +218,8 @@ lbry.getPeersForBlobHash = function(blobHash, callback) {
}); });
} }
lbry.getStreamAvailability = function(name, callback, errorCallback) { /**
if (!name) { * Takes a LBRY URI; will first try and calculate a total cost using
throw new Error(`Name required.`);
}
lbry.call('get_availability', {name: name}, callback, errorCallback);
}
lbry.getCostInfoForName = function(name, callback, errorCallback) {
/**
* Takes a LBRY name; will first try and calculate a total cost using
* Lighthouse. If Lighthouse can't be reached, it just retrives the * Lighthouse. If Lighthouse can't be reached, it just retrives the
* key fee. * key fee.
* *
@ -269,70 +228,77 @@ lbry.getCostInfoForName = function(name, callback, errorCallback) {
* - includes_data: Boolean; indicates whether or not the data fee info * - includes_data: Boolean; indicates whether or not the data fee info
* from Lighthouse is included. * from Lighthouse is included.
*/ */
if (!name) { lbry.costPromiseCache = {}
throw new Error(`Name required.`); lbry.getCostInfo = function(uri) {
if (lbry.costPromiseCache[uri] === undefined) {
lbry.costPromiseCache[uri] = new Promise((resolve, reject) => {
const COST_INFO_CACHE_KEY = 'cost_info_cache';
let costInfoCache = getSession(COST_INFO_CACHE_KEY, {})
function cacheAndResolve(cost, includesData) {
costInfoCache[uri] = {cost, includesData};
setSession(COST_INFO_CACHE_KEY, costInfoCache);
resolve({cost, includesData});
} }
function getCostWithData(name, size, callback, errorCallback) { if (!uri) {
lbry.getTotalCost(name, size, (cost) => { return reject(new Error(`URI required.`));
callback({
cost: cost,
includesData: true,
});
}, errorCallback);
} }
function getCostNoData(name, callback, errorCallback) { if (costInfoCache[uri] && costInfoCache[uri].cost) {
lbry.getKeyFee(name, (cost) => { return resolve(costInfoCache[uri])
callback({ }
cost: cost,
includesData: false, function getCost(uri, size) {
lbry.stream_cost_estimate({uri, ... size !== null ? {size} : {}}).then((cost) => {
cacheAndResolve(cost, size !== null);
}, reject);
}
function getCostGenerous(uri) {
// If generous is on, the calculation is simple enough that we might as well do it here in the front end
lbry.resolve({uri: uri}).then((resolutionInfo) => {
const fee = resolutionInfo.claim.value.stream.metadata.fee;
if (fee === undefined) {
cacheAndResolve(0, true);
} else if (fee.currency == 'LBC') {
cacheAndResolve(fee.amount, true);
} else {
lbryio.getExchangeRates().then(({lbc_usd}) => {
cacheAndResolve(fee.amount / lbc_usd, true);
}); });
}, errorCallback); }
});
}
const uriObj = lbryuri.parse(uri);
const name = uriObj.path || uriObj.name;
lbry.settings_get({allow_cached: true}).then(({is_generous_host}) => {
if (is_generous_host) {
return getCostGenerous(uri);
} }
lighthouse.get_size_for_name(name).then((size) => { lighthouse.get_size_for_name(name).then((size) => {
getCostWithData(name, size, callback, errorCallback); if (size) {
getCost(name, size);
}
else {
getCost(name, null);
}
}, () => { }, () => {
getCostNoData(name, callback, errorCallback); getCost(name, null);
}); });
}
lbry.getFeaturedDiscoverNames = function(callback) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', 'https://api.lbry.io/discover/list', true);
xhr.onload = () => {
if (xhr.status === 200) {
var responseData = JSON.parse(xhr.responseText);
if (responseData.data) //new signature, once api.lbry.io is updated
{
resolve(responseData.data);
}
else
{
resolve(responseData);
}
} else {
reject(Error('Failed to fetch featured names.'));
}
};
xhr.send();
}); });
});
}
return lbry.costPromiseCache[uri];
} }
lbry.getMyClaims = function(callback) { lbry.getMyClaims = function(callback) {
lbry.call('get_name_claims', {}, callback); lbry.call('get_name_claims', {}, callback);
} }
lbry.startFile = function(name, callback) {
lbry.call('start_lbry_file', { name: name }, callback);
}
lbry.stopFile = function(name, callback) {
lbry.call('stop_lbry_file', { name: name }, callback);
}
lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) { lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) {
this._removedFiles.push(outpoint); this._removedFiles.push(outpoint);
this._updateFileInfoSubscribers(outpoint); this._updateFileInfoSubscribers(outpoint);
@ -393,12 +359,13 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall
returnedPending = true; returnedPending = true;
if (publishedCallback) { if (publishedCallback) {
savePendingPublish(params.name); savePendingPublish({name: params.name, channel_name: params.channel_name});
publishedCallback(true); publishedCallback(true);
} }
if (fileListedCallback) { if (fileListedCallback) {
savePendingPublish(params.name); const {name, channel_name} = params;
savePendingPublish({name: params.name, channel_name: params.channel_name});
fileListedCallback(true); fileListedCallback(true);
} }
}, 2000); }, 2000);
@ -408,44 +375,6 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall
//}); //});
} }
lbry.getVersionInfo = function(callback) {
lbry.call('version', {}, callback);
};
lbry.checkNewVersionAvailable = function(callback) {
lbry.call('version', {}, function(versionInfo) {
var ver = versionInfo.lbrynet_version.split('.');
var maj = parseInt(ver[0]),
min = parseInt(ver[1]),
patch = parseInt(ver[2]);
var remoteVer = versionInfo.remote_lbrynet.split('.');
var remoteMaj = parseInt(remoteVer[0]),
remoteMin = parseInt(remoteVer[1]),
remotePatch = parseInt(remoteVer[2]);
if (maj < remoteMaj) {
var newVersionAvailable = true;
} else if (maj == remoteMaj) {
if (min < remoteMin) {
var newVersionAvailable = true;
} else if (min == remoteMin) {
var newVersionAvailable = (patch < remotePatch);
} else {
var newVersionAvailable = false;
}
} else {
var newVersionAvailable = false;
}
callback(newVersionAvailable);
}, function(err) {
if (err.fault == 'NoSuchFunction') {
// Really old daemon that can't report a version
callback(true);
}
});
}
lbry.getClientSettings = function() { lbry.getClientSettings = function() {
var outSettings = {}; var outSettings = {};
@ -458,6 +387,10 @@ lbry.getClientSettings = function() {
lbry.getClientSetting = function(setting) { lbry.getClientSetting = function(setting) {
var localStorageVal = localStorage.getItem('setting_' + setting); var localStorageVal = localStorage.getItem('setting_' + setting);
if (setting == 'showDeveloperMenu')
{
return true;
}
return (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal)); return (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal));
} }
@ -550,7 +483,7 @@ lbry.stop = function(callback) {
lbry.fileInfo = {}; lbry.fileInfo = {};
lbry._subscribeIdCount = 0; lbry._subscribeIdCount = 0;
lbry._fileInfoSubscribeCallbacks = {}; lbry._fileInfoSubscribeCallbacks = {};
lbry._fileInfoSubscribeInterval = 5000; lbry._fileInfoSubscribeInterval = 500000;
lbry._balanceSubscribeCallbacks = {}; lbry._balanceSubscribeCallbacks = {};
lbry._balanceSubscribeInterval = 5000; lbry._balanceSubscribeInterval = 5000;
lbry._removedFiles = []; lbry._removedFiles = [];
@ -560,8 +493,9 @@ lbry._updateClaimOwnershipCache = function(claimId) {
lbry.getMyClaims((claimInfos) => { lbry.getMyClaims((claimInfos) => {
lbry._claimIdOwnershipCache[claimId] = !!claimInfos.reduce(function(match, claimInfo) { lbry._claimIdOwnershipCache[claimId] = !!claimInfos.reduce(function(match, claimInfo) {
return match || claimInfo.claim_id == claimId; return match || claimInfo.claim_id == claimId;
}, false);
}); });
});
}; };
lbry._updateFileInfoSubscribers = function(outpoint) { lbry._updateFileInfoSubscribers = function(outpoint) {
@ -612,6 +546,7 @@ lbry.fileInfoUnsubscribe = function(outpoint, subscribeId) {
delete lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId]; delete lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId];
} }
lbry._balanceUpdateInterval = null;
lbry._updateBalanceSubscribers = function() { lbry._updateBalanceSubscribers = function() {
lbry.get_balance().then(function(balance) { lbry.get_balance().then(function(balance) {
for (let callback of Object.values(lbry._balanceSubscribeCallbacks)) { for (let callback of Object.values(lbry._balanceSubscribeCallbacks)) {
@ -619,8 +554,8 @@ lbry._updateBalanceSubscribers = function() {
} }
}); });
if (Object.keys(lbry._balanceSubscribeCallbacks).length) { if (!lbry._balanceUpdateInterval && Object.keys(lbry._balanceSubscribeCallbacks).length) {
setTimeout(() => { lbry._balanceUpdateInterval = setInterval(() => {
lbry._updateBalanceSubscribers(); lbry._updateBalanceSubscribers();
}, lbry._balanceSubscribeInterval); }, lbry._balanceSubscribeInterval);
} }
@ -635,6 +570,9 @@ lbry.balanceSubscribe = function(callback) {
lbry.balanceUnsubscribe = function(subscribeId) { lbry.balanceUnsubscribe = function(subscribeId) {
delete lbry._balanceSubscribeCallbacks[subscribeId]; delete lbry._balanceSubscribeCallbacks[subscribeId];
if (lbry._balanceUpdateInterval && !Object.keys(lbry._balanceSubscribeCallbacks).length) {
clearInterval(lbry._balanceUpdateInterval)
}
} }
lbry.showMenuIfNeeded = function() { lbry.showMenuIfNeeded = function() {
@ -646,6 +584,14 @@ lbry.showMenuIfNeeded = function() {
sessionStorage.setItem('menuShown', chosenMenu); sessionStorage.setItem('menuShown', chosenMenu);
}; };
lbry.getVersionInfo = function() {
return new Promise((resolve, reject) => {
ipcRenderer.once('version-info-received', (event, versionInfo) => { resolve(versionInfo) });
ipcRenderer.send('version-info-requested');
});
}
/** /**
* Wrappers for API methods to simulate missing or future behavior. Unlike the old-style stubs, * Wrappers for API methods to simulate missing or future behavior. Unlike the old-style stubs,
* these are designed to be transparent wrappers around the corresponding API methods. * these are designed to be transparent wrappers around the corresponding API methods.
@ -657,14 +603,14 @@ lbry.showMenuIfNeeded = function() {
*/ */
lbry.file_list = function(params={}) { lbry.file_list = function(params={}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const {name, outpoint} = params; const {name, channel_name, outpoint} = params;
/** /**
* If we're searching by outpoint, check first to see if there's a matching pending publish. * If we're searching by outpoint, check first to see if there's a matching pending publish.
* Pending publishes use their own faux outpoints that are always unique, so we don't need * Pending publishes use their own faux outpoints that are always unique, so we don't need
* to check if there's a real file. * to check if there's a real file.
*/ */
if (outpoint !== undefined) { if (outpoint) {
const pendingPublish = getPendingPublish({outpoint}); const pendingPublish = getPendingPublish({outpoint});
if (pendingPublish) { if (pendingPublish) {
resolve([pendingPublishToDummyFileInfo(pendingPublish)]); resolve([pendingPublishToDummyFileInfo(pendingPublish)]);
@ -673,14 +619,8 @@ lbry.file_list = function(params={}) {
} }
lbry.call('file_list', params, (fileInfos) => { lbry.call('file_list', params, (fileInfos) => {
// Remove any pending publications that are now listed in the file manager removePendingPublishIfNeeded({name, channel_name, outpoint});
const pendingPublishes = getPendingPublishes();
for (let {name: itemName} of fileInfos) {
if (pendingPublishes.find(() => name == itemName)) {
removePendingPublish({name: name});
}
}
const dummyFileInfos = getPendingPublishes().map(pendingPublishToDummyFileInfo); const dummyFileInfos = getPendingPublishes().map(pendingPublishToDummyFileInfo);
resolve([...fileInfos, ...dummyFileInfos]); resolve([...fileInfos, ...dummyFileInfos]);
}, reject, reject); }, reject, reject);
@ -690,16 +630,60 @@ lbry.file_list = function(params={}) {
lbry.claim_list_mine = function(params={}) { lbry.claim_list_mine = function(params={}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lbry.call('claim_list_mine', params, (claims) => { lbry.call('claim_list_mine', params, (claims) => {
// Filter out pending publishes when the name is already in the file manager for (let {name, channel_name, txid, nout} of claims) {
const dummyClaims = getPendingPublishes().filter( removePendingPublishIfNeeded({name, channel_name, outpoint: txid + ':' + nout});
(pub) => !claims.find(({name}) => name == pub.name) }
).map(pendingPublishToDummyClaim);
const dummyClaims = getPendingPublishes().map(pendingPublishToDummyClaim);
resolve([...claims, ...dummyClaims]); resolve([...claims, ...dummyClaims]);
}, reject, reject); }, reject, reject)
}); });
} }
lbry.resolve = function(params={}) {
const claimCacheKey = 'resolve_claim_cache',
claimCache = getSession(claimCacheKey, {})
return new Promise((resolve, reject) => {
if (!params.uri) {
throw "Resolve has hacked cache on top of it that requires a URI"
}
if (params.uri && claimCache[params.uri] !== undefined) {
resolve(claimCache[params.uri]);
} else {
lbry.call('resolve', params, function(data) {
claimCache[params.uri] = data;
setSession(claimCacheKey, claimCache)
resolve(data)
}, reject)
}
});
}
// Adds caching.
lbry.settings_get = function(params={}) {
return new Promise((resolve, reject) => {
if (params.allow_cached) {
const cached = getSession('settings');
if (cached) {
return resolve(cached);
}
}
lbry.call('settings_get', {}, (settings) => {
setSession('settings', settings);
resolve(settings);
});
});
}
// lbry.get = function(params={}) {
// return function(params={}) {
// return new Promise((resolve, reject) => {
// jsonrpc.call(lbry.daemonConnectionString, "get", params, resolve, reject, reject);
// });
// };
// }
lbry = new Proxy(lbry, { lbry = new Proxy(lbry, {
get: function(target, name) { get: function(target, name) {
if (name in target) { if (name in target) {
@ -708,7 +692,7 @@ lbry = new Proxy(lbry, {
return function(params={}) { return function(params={}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
jsonrpc.call(lbry.daemonConnectionString, name, [params], resolve, reject, reject); jsonrpc.call(lbry.daemonConnectionString, name, params, resolve, reject, reject);
}); });
}; };
} }

159
ui/js/lbryio.js Normal file
View file

@ -0,0 +1,159 @@
import {getLocal, getSession, setSession, setLocal} from './utils.js';
import lbry from './lbry.js';
const querystring = require('querystring');
const lbryio = {
_accessToken: getLocal('accessToken'),
_authenticationPromise: null,
_user : null,
enabled: false
};
const CONNECTION_STRING = process.env.LBRY_APP_API_URL ? process.env.LBRY_APP_API_URL : 'https://api.lbry.io/';
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
lbryio.getExchangeRates = function() {
return new Promise((resolve, reject) => {
const cached = getSession('exchangeRateCache');
if (!cached || Date.now() - cached.time > EXCHANGE_RATE_TIMEOUT) {
lbryio.call('lbc', 'exchange_rate', {}, 'get', true).then(({lbc_usd, lbc_btc, btc_usd}) => {
const rates = {lbc_usd, lbc_btc, btc_usd};
setSession('exchangeRateCache', {
rates: rates,
time: Date.now(),
});
resolve(rates);
});
} else {
resolve(cached.rates);
}
});
}
lbryio.call = function(resource, action, params={}, method='get', evenIfDisabled=false) { // evenIfDisabled is just for development, when we may have some calls working and some not
return new Promise((resolve, reject) => {
if (!lbryio.enabled && !evenIfDisabled && (resource != 'discover' || action != 'list')) {
reject(new Error("LBRY internal API is disabled"))
return
}
const xhr = new XMLHttpRequest;
xhr.addEventListener('error', function (event) {
reject(new Error("Something went wrong making an internal API call."));
});
xhr.addEventListener('timeout', function() {
reject(new Error('XMLHttpRequest connection timed out'));
});
xhr.addEventListener('load', function() {
const response = JSON.parse(xhr.responseText);
if (!response.success) {
if (reject) {
let error = new Error(response.error);
error.xhr = xhr;
reject(error);
} else {
document.dispatchEvent(new CustomEvent('unhandledError', {
detail: {
connectionString: connectionString,
method: action,
params: params,
message: response.error.message,
... response.error.data ? {data: response.error.data} : {},
}
}));
}
} else {
resolve(response.data);
}
});
// For social media auth:
//const accessToken = localStorage.getItem('accessToken');
//const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}};
// Temp app ID based auth:
const fullParams = {app_id: lbryio.getAccessToken(), ...params};
if (method == 'get') {
xhr.open('get', CONNECTION_STRING + resource + '/' + action + '?' + querystring.stringify(fullParams), true);
xhr.send();
} else if (method == 'post') {
xhr.open('post', CONNECTION_STRING + resource + '/' + action, true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send(querystring.stringify(fullParams));
}
});
};
lbryio.getAccessToken = () => {
return getLocal('accessToken');
}
lbryio.setAccessToken = (token) => {
setLocal('accessToken', token)
}
lbryio.authenticate = function() {
if (!lbryio.enabled) {
return new Promise((resolve, reject) => {
resolve({
ID: 1,
HasVerifiedEmail: true
})
})
}
if (lbryio._authenticationPromise === null) {
lbryio._authenticationPromise = new Promise((resolve, reject) => {
lbry.status().then((response) => {
let installation_id = response.installation_id;
function setCurrentUser() {
lbryio.call('user', 'me').then((data) => {
lbryio.user = data
resolve(data)
}).catch(function(err) {
lbryio.setAccessToken(null);
if (!getSession('reloadedOnFailedAuth')) {
setSession('reloadedOnFailedAuth', true)
window.location.reload();
} else {
reject(err);
}
})
}
if (!lbryio.getAccessToken()) {
lbryio.call('user', 'new', {
language: 'en',
app_id: installation_id,
}, 'post').then(function(responseData) {
if (!responseData.ID) {
reject(new Error("Received invalid authentication response."));
}
lbryio.setAccessToken(installation_id)
setCurrentUser()
}).catch(function(error) {
/*
until we have better error code format, assume all errors are duplicate application id
if we're wrong, this will be caught by later attempts to make a valid call
*/
lbryio.setAccessToken(installation_id)
setCurrentUser()
})
} else {
setCurrentUser()
}
}).catch(reject);
});
}
return lbryio._authenticationPromise;
}
export default lbryio;

169
ui/js/lbryuri.js Normal file
View file

@ -0,0 +1,169 @@
const CHANNEL_NAME_MIN_LEN = 4;
const CLAIM_ID_MAX_LEN = 40;
const lbryuri = {};
/**
* Parses a LBRY name into its component parts. Throws errors with user-friendly
* messages for invalid names.
*
* N.B. that "name" indicates the value in the name position of the URI. For
* claims for channel content, this will actually be the channel name, and
* the content name is in the path (e.g. lbry://@channel/content)
*
* In most situations, you'll want to use the contentName and channelName keys
* and ignore the name key.
*
* Returns a dictionary with keys:
* - name (string): The value in the "name" position in the URI. Note that this
* could be either content name or channel name; see above.
* - path (string, if persent)
* - claimSequence (int, if present)
* - bidPosition (int, if present)
* - claimId (string, if present)
* - isChannel (boolean)
* - contentName (string): For anon claims, the name; for channel claims, the path
* - channelName (string, if present): Channel name without @
*/
lbryuri.parse = function(uri, requireProto=false) {
// Break into components. Empty sub-matches are converted to null
const componentsRegex = new RegExp(
'^((?:lbry:\/\/)?)' + // protocol
'([^:$#/]*)' + // name (stops at the first separator or end)
'([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end)
'(/?)(.*)' // path separator, path
);
const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(uri).slice(1).map(match => match || null);
let contentName;
// Validate protocol
if (requireProto && !proto) {
throw new Error('LBRY URIs must include a protocol prefix (lbry://).');
}
// Validate and process name
if (!name) {
throw new Error('URI does not include name.');
}
const isChannel = name.startsWith('@');
const channelName = isChannel ? name.slice(1) : name;
if (isChannel) {
if (!channelName) {
throw new Error('No channel name after @.');
}
if (channelName.length < CHANNEL_NAME_MIN_LEN) {
throw new Error(`Channel names must be at least ${CHANNEL_NAME_MIN_LEN} characters.`);
}
contentName = path;
}
const nameBadChars = (channelName || name).match(/[^A-Za-z0-9-]/g);
if (nameBadChars) {
throw new Error(`Invalid character${nameBadChars.length == 1 ? '' : 's'} in name: ${nameBadChars.join(', ')}.`);
}
// Validate and process modifier (claim ID, bid position or claim sequence)
let claimId, claimSequence, bidPosition;
if (modSep) {
if (!modVal) {
throw new Error(`No modifier provided after separator ${modSep}.`);
}
if (modSep == '#') {
claimId = modVal;
} else if (modSep == ':') {
claimSequence = modVal;
} else if (modSep == '$') {
bidPosition = modVal;
}
}
if (claimId && (claimId.length > CLAIM_ID_MAX_LEN || !claimId.match(/^[0-9a-f]+$/))) {
throw new Error(`Invalid claim ID ${claimId}.`);
}
if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) {
throw new Error('Claim sequence must be a number.');
}
if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) {
throw new Error('Bid position must be a number.');
}
// Validate and process path
if (path) {
if (!isChannel) {
throw new Error('Only channel URIs may have a path.');
}
const pathBadChars = path.match(/[^A-Za-z0-9-]/g);
if (pathBadChars) {
throw new Error(`Invalid character${count == 1 ? '' : 's'} in path: ${nameBadChars.join(', ')}`);
}
contentName = path;
} else if (pathSep) {
throw new Error('No path provided after /');
}
return {
name, path, isChannel,
... contentName ? {contentName} : {},
... channelName ? {channelName} : {},
... claimSequence ? {claimSequence: parseInt(claimSequence)} : {},
... bidPosition ? {bidPosition: parseInt(bidPosition)} : {},
... claimId ? {claimId} : {},
... path ? {path} : {},
};
}
/**
* Takes an object in the same format returned by lbryuri.parse() and builds a URI.
*
* The channelName key will accept names with or without the @ prefix.
*/
lbryuri.build = function(uriObj, includeProto=true, allowExtraProps=false) {
let {name, claimId, claimSequence, bidPosition, path, contentName, channelName} = uriObj;
if (channelName) {
const channelNameFormatted = channelName.startsWith('@') ? channelName : '@' + channelName;
if (!name) {
name = channelNameFormatted;
} else if (name !== channelNameFormatted) {
throw new Error('Received a channel content URI, but name and channelName do not match. "name" represents the value in the name position of the URI (lbry://name...), which for channel content will be the channel name. In most cases, to construct a channel URI you should just pass channelName and contentName.');
}
}
if (contentName) {
if (!name) {
name = contentName;
} else if (!path) {
path = contentName;
}
if (path && path !== contentName) {
throw new Error('path and contentName do not match. Only one is required; most likely you wanted contentName.');
}
}
return (includeProto ? 'lbry://' : '') + name +
(claimId ? `#${claimId}` : '') +
(claimSequence ? `:${claimSequence}` : '') +
(bidPosition ? `\$${bidPosition}` : '') +
(path ? `/${path}` : '');
}
/* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just
* consists of adding the lbry:// prefix if needed) */
lbryuri.normalize= function(uri) {
const {name, path, bidPosition, claimSequence, claimId} = lbryuri.parse(uri);
return lbryuri.build({name, path, claimSequence, bidPosition, claimId});
}
window.lbryuri = lbryuri;
export default lbryuri;

View file

@ -1,12 +1,12 @@
import lbry from './lbry.js'; import lbry from './lbry.js';
import jsonrpc from './jsonrpc.js'; import jsonrpc from './jsonrpc.js';
const queryTimeout = 5000; const queryTimeout = 3000;
const maxQueryTries = 5; const maxQueryTries = 2;
const defaultServers = [ const defaultServers = [
'http://lighthouse4.lbry.io:50005', 'http://lighthouse7.lbry.io:50005',
'http://lighthouse5.lbry.io:50005', 'http://lighthouse8.lbry.io:50005',
'http://lighthouse6.lbry.io:50005', 'http://lighthouse9.lbry.io:50005',
]; ];
const path = '/'; const path = '/';
@ -20,12 +20,9 @@ function getServers() {
} }
function call(method, params, callback, errorCallback) { function call(method, params, callback, errorCallback) {
if (connectTryNum > maxQueryTries) { if (connectTryNum >= maxQueryTries) {
if (connectFailedCallback) { errorCallback(new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`));
connectFailedCallback(); return;
} else {
throw new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`);
}
} }
/** /**
@ -48,7 +45,7 @@ function call(method, params, callback, errorCallback) {
}, () => { }, () => {
connectTryNum++; connectTryNum++;
call(method, params, callback, errorCallback); call(method, params, callback, errorCallback);
}); }, queryTimeout);
} }
const lighthouse = new Proxy({}, { const lighthouse = new Proxy({}, {

View file

@ -1,9 +1,12 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import lbry from './lbry.js'; import lbry from './lbry.js';
import lbryio from './lbryio.js';
import lighthouse from './lighthouse.js'; import lighthouse from './lighthouse.js';
import App from './app.js'; import App from './app.js';
import SplashScreen from './component/splash.js'; import SplashScreen from './component/splash.js';
import SnackBar from './component/snack-bar.js';
import {AuthOverlay} from './component/auth.js';
const {remote} = require('electron'); const {remote} = require('electron');
const contextMenu = remote.require('./menu/context-menu'); const contextMenu = remote.require('./menu/context-menu');
@ -16,31 +19,24 @@ window.addEventListener('contextmenu', (event) => {
event.preventDefault(); event.preventDefault();
}); });
var init = function() { let init = function() {
window.lbry = lbry; window.lbry = lbry;
window.lighthouse = lighthouse; window.lighthouse = lighthouse;
let canvas = document.getElementById('canvas');
lbry.connect().then(function(isConnected) {
lbryio.authenticate() //start auth process as soon as soon as we can get an install ID
})
function onDaemonReady() {
window.sessionStorage.setItem('loaded', 'y'); //once we've made it here once per session, we don't need to show splash again
ReactDOM.render(<div>{ lbryio.enabled ? <AuthOverlay/> : '' }<App /><SnackBar /></div>, canvas)
}
var canvas = document.getElementById('canvas');
if (window.sessionStorage.getItem('loaded') == 'y') { if (window.sessionStorage.getItem('loaded') == 'y') {
ReactDOM.render(<App/>, canvas) onDaemonReady();
} else { } else {
ReactDOM.render( ReactDOM.render(<SplashScreen message="Connecting" onLoadDone={onDaemonReady} />, canvas);
<SplashScreen message="Connecting" onLoadDone={function() {
// Redirect to the claim code page if needed. Find somewhere better for this logic
if (!localStorage.getItem('claimCodeDone') && window.location.search == '' || window.location.search == '?' || window.location.search == 'discover') {
lbry.getBalance((balance) => {
if (balance <= 0) {
window.location.href = '?claim';
} else {
ReactDOM.render(<App/>, canvas);
}
});
} else {
ReactDOM.render(<App/>, canvas);
}
}}/>,
canvas
);
} }
}; };

View file

@ -1,158 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import Modal from '../component/modal.js';
import {Link} from '../component/link.js';
var claimCodeContentStyle = {
display: 'inline-block',
textAlign: 'left',
width: '600px',
}, claimCodeLabelStyle = {
display: 'inline-block',
cursor: 'default',
width: '130px',
textAlign: 'right',
marginRight: '6px',
};
var ClaimCodePage = React.createClass({
getInitialState: function() {
return {
submitting: false,
modal: null,
referralCredits: null,
activationCredits: null,
failureReason: null,
}
},
handleSubmit: function(event) {
if (typeof event !== 'undefined') {
event.preventDefault();
}
if (!this.refs.code.value) {
this.setState({
modal: 'missingCode',
});
return;
} else if (!this.refs.email.value) {
this.setState({
modal: 'missingEmail',
});
return;
}
this.setState({
submitting: true,
});
lbry.getUnusedAddress((address) => {
var code = this.refs.code.value;
var email = this.refs.email.value;
var xhr = new XMLHttpRequest;
xhr.addEventListener('load', () => {
var response = JSON.parse(xhr.responseText);
if (response.success) {
this.setState({
modal: 'codeRedeemed',
referralCredits: response.referralCredits,
activationCredits: response.activationCredits,
});
} else {
this.setState({
submitting: false,
modal: 'codeRedeemFailed',
failureReason: response.reason,
});
}
});
xhr.addEventListener('error', () => {
this.setState({
submitting: false,
modal: 'couldNotConnect',
});
});
xhr.open('POST', 'https://invites.lbry.io', true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send('code=' + encodeURIComponent(code) + '&address=' + encodeURIComponent(address) +
'&email=' + encodeURIComponent(email));
});
},
handleSkip: function() {
this.setState({
modal: 'skipped',
});
},
handleFinished: function() {
localStorage.setItem('claimCodeDone', true);
window.location = '?home';
},
closeModal: function() {
this.setState({
modal: null,
});
},
render: function() {
return (
<main>
<form onSubmit={this.handleSubmit}>
<div className="card">
<h2>Claim your beta invitation code</h2>
<section style={claimCodeContentStyle}>
<p>Thanks for beta testing LBRY! Enter your invitation code and email address below to receive your initial
LBRY credits.</p>
<p>You will be added to our mailing list (if you're not already on it) and will be eligible for future rewards for beta testers.</p>
</section>
<section>
<section><label style={claimCodeLabelStyle} htmlFor="code">Invitation code</label><input name="code" ref="code" /></section>
<section><label style={claimCodeLabelStyle} htmlFor="email">Email</label><input name="email" ref="email" /></section>
</section>
<section>
<Link button="primary" label={this.state.submitting ? "Submitting..." : "Submit"}
disabled={this.state.submitting} onClick={this.handleSubmit} />
<Link button="alt" label="Skip" disabled={this.state.submitting} onClick={this.handleSkip} />
<input type='submit' className='hidden' />
</section>
</div>
</form>
<Modal isOpen={this.state.modal == 'missingCode'} contentLabel="Invitation code required"
onConfirmed={this.closeModal}>
Please enter an invitation code or choose "Skip."
</Modal>
<Modal isOpen={this.state.modal == 'missingEmail'} contentLabel="Email required"
onConfirmed={this.closeModal}>
Please enter an email address or choose "Skip."
</Modal>
<Modal isOpen={this.state.modal == 'codeRedeemFailed'} contentLabel="Failed to redeem code"
onConfirmed={this.closeModal}>
{this.state.failureReason}
</Modal>
<Modal isOpen={this.state.modal == 'codeRedeemed'} contentLabel="Code redeemed"
onConfirmed={this.handleFinished}>
Your invite code has been redeemed. { ' ' }
{this.state.referralCredits > 0
? `You have also earned ${referralCredits} credits from referrals. A total of ${activationCredits + referralCredits}
will be added to your balance shortly.`
: (this.state.activationCredits > 0
? `${this.state.activationCredits} credits will be added to your balance shortly.`
: 'The credits will be added to your balance shortly.')}
</Modal>
<Modal isOpen={this.state.modal == 'skipped'} contentLabel="Welcome to LBRY"
onConfirmed={this.handleFinished}>
Welcome to LBRY! You can visit the Wallet page to redeem an invite code at any time.
</Modal>
<Modal isOpen={this.state.modal == 'couldNotConnect'} contentLabel="Could not connect"
onConfirmed={this.closeModal}>
<p>LBRY couldn't connect to our servers to confirm your invitation code. Please check your internet connection.</p>
If you continue to have problems, you can still browse LBRY and visit the Settings page to redeem your code later.
</Modal>
</main>
);
}
});
export default ClaimCodePage;

View file

@ -1,6 +1,6 @@
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import React from 'react'; import React from 'react';
import FormField from '../component/form.js'; import {FormField} from '../component/form.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
const fs = require('fs'); const fs = require('fs');

View file

@ -1,168 +1,62 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbryio from '../lbryio.js';
import lighthouse from '../lighthouse.js'; import {FileTile, FileTileStream} from '../component/file-tile.js';
import {FileTile} from '../component/file-tile.js';
import {Link} from '../component/link.js';
import {ToolTip} from '../component/tooltip.js'; import {ToolTip} from '../component/tooltip.js';
import {BusyMessage} from '../component/common.js';
var fetchResultsStyle = { const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' +
color: '#888',
textAlign: 'center',
fontSize: '1.2em'
};
var SearchActive = React.createClass({
render: function() {
return (
<div style={fetchResultsStyle}>
<BusyMessage message="Looking up the Dewey Decimals" />
</div>
);
}
});
var searchNoResultsStyle = {
textAlign: 'center'
}, searchNoResultsMessageStyle = {
fontStyle: 'italic',
marginRight: '5px'
};
var SearchNoResults = React.createClass({
render: function() {
return (
<section style={searchNoResultsStyle}>
<span style={searchNoResultsMessageStyle}>No one has checked anything in for {this.props.query} yet.</span>
<Link label="Be the first" href="?publish" />
</section>
);
}
});
var SearchResults = React.createClass({
render: function() {
var rows = [],
seenNames = {}; //fix this when the search API returns claim IDs
this.props.results.forEach(function({name, value}) {
if (!seenNames[name]) {
seenNames[name] = name;
rows.push(
<FileTile key={name} uri={name} sdHash={value.sources.lbry_sd_hash} />
);
}
});
return (
<div>{rows}</div>
);
}
});
var featuredContentLegendStyle = {
fontSize: '12px',
color: '#aaa',
verticalAlign: '15%',
};
var FeaturedContent = React.createClass({
getInitialState: function() {
return {
featuredNames: [],
};
},
componentWillMount: function() {
lbry.getFeaturedDiscoverNames().then((featuredNames) => {
this.setState({ featuredNames: featuredNames });
});
},
render: function() {
const toolTipText = ('Community Content is a public space where anyone can share content with the ' +
'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' + 'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
'"five" to put your content here!'); '"five" to put your content here!');
return ( let FeaturedCategory = React.createClass({
<div className="row-fluid"> render: function() {
<div className="span6"> return (<div className="card-row card-row--small">
<h3>Featured Content</h3> { this.props.category ?
{ this.state.featuredNames.map((name) => { return <FileTile key={name} uri={name} /> }) } <h3 className="card-row__header">{this.props.category}
</div> { this.props.category.match(/^community/i) ?
<div className="span6"> <ToolTip label="What's this?" body={communityCategoryToolTipText} className="tooltip--header"/>
<h3> : '' }</h3>
Community Content : '' }
<ToolTip label="What's this?" body={toolTipText} className="tooltip--header"/> { this.props.names.map((name) => { return <FileTile key={name} displayStyle="card" uri={name} /> }) }
</h3> </div>)
<FileTile uri="one" />
<FileTile uri="two" />
<FileTile uri="three" />
<FileTile uri="four" />
<FileTile uri="five" />
</div>
</div>
);
} }
}); })
var DiscoverPage = React.createClass({
userTypingTimer: null,
componentDidUpdate: function() {
if (this.props.query != this.state.query)
{
this.handleSearchChanged(this.props.query);
}
},
componentWillReceiveProps: function(nextProps, nextState) {
if (nextProps.query != nextState.query)
{
this.handleSearchChanged(nextProps.query);
}
},
handleSearchChanged: function(query) {
this.setState({
searching: true,
query: query,
});
lighthouse.search(query).then(this.searchCallback);
},
componentWillMount: function() {
document.title = "Discover";
if (this.props.query) {
// Rendering with a query already typed
this.handleSearchChanged(this.props.query);
}
},
let DiscoverPage = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
results: [], featuredUris: {},
query: this.props.query, failed: false
searching: ('query' in this.props) && (this.props.query.length > 0)
}; };
}, },
componentWillMount: function() {
searchCallback: function(results) { lbryio.call('discover', 'list', { version: "early-access" } ).then(({Categories, Uris}) => {
if (this.state.searching) //could have canceled while results were pending, in which case nothing to do let featuredUris = {}
{ Categories.forEach((category) => {
this.setState({ if (Uris[category] && Uris[category].length) {
results: results, featuredUris[category] = Uris[category]
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
});
} }
})
this.setState({ featuredUris: featuredUris });
}, () => {
this.setState({
failed: true
})
});
}, },
render: function() { render: function() {
return ( return <main>{
<main> this.state.failed ?
{ this.state.searching ? <SearchActive /> : null } <div className="empty">Failed to load landing content.</div> :
{ !this.state.searching && this.props.query && this.state.results.length ? <SearchResults results={this.state.results} /> : null } <div>
{ !this.state.searching && this.props.query && !this.state.results.length ? <SearchNoResults query={this.props.query} /> : null } {
{ !this.props.query && !this.state.searching ? <FeaturedContent /> : null } Object.keys(this.state.featuredUris).map((category) => {
</main> return this.state.featuredUris[category].length ?
); <FeaturedCategory key={category} category={category} names={this.state.featuredUris[category]} /> :
'';
})
}
</div>
}</main>;
} }
}); });

View file

@ -1,11 +1,24 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lbryuri from '../lbryuri.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import FormField from '../component/form.js'; import {FormField} from '../component/form.js';
import {SubHeader} from '../component/header.js';
import {FileTileStream} from '../component/file-tile.js'; import {FileTileStream} from '../component/file-tile.js';
import rewards from '../rewards.js';
import lbryio from '../lbryio.js';
import {BusyMessage, Thumbnail} from '../component/common.js'; import {BusyMessage, Thumbnail} from '../component/common.js';
export let FileListNav = React.createClass({
render: function() {
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
'?downloaded': 'Downloaded',
'?published': 'Published',
}} />;
}
});
export let FileListDownloaded = React.createClass({ export let FileListDownloaded = React.createClass({
_isMounted: false, _isMounted: false,
@ -16,7 +29,6 @@ export let FileListDownloaded = React.createClass({
}, },
componentDidMount: function() { componentDidMount: function() {
this._isMounted = true; this._isMounted = true;
document.title = "Downloaded Files";
lbry.claim_list_mine().then((myClaimInfos) => { lbry.claim_list_mine().then((myClaimInfos) => {
if (!this._isMounted) { return; } if (!this._isMounted) { return; }
@ -31,27 +43,25 @@ export let FileListDownloaded = React.createClass({
}); });
}); });
}, },
componentWillUnmount: function() {
this._isMounted = false;
},
render: function() { render: function() {
let content = "";
if (this.state.fileInfos === null) { if (this.state.fileInfos === null) {
return ( content = <BusyMessage message="Loading" />;
<main className="page">
<BusyMessage message="Loading" />
</main>
);
} else if (!this.state.fileInfos.length) { } else if (!this.state.fileInfos.length) {
return ( content = <span>You haven't downloaded anything from LBRY yet. Go <Link href="?discover" label="search for your first download" />!</span>;
<main className="page">
<span>You haven't downloaded anything from LBRY yet. Go <Link href="?discover" label="search for your first download" />!</span>
</main>
);
} else { } else {
content = <FileList fileInfos={this.state.fileInfos} hidePrices={true} />;
}
return ( return (
<main className="page"> <main className="main--single-column">
<FileList fileInfos={this.state.fileInfos} hidePrices={true} /> <FileListNav viewingPage="downloaded" />
{content}
</main> </main>
); );
} }
}
}); });
export let FileListPublished = React.createClass({ export let FileListPublished = React.createClass({
@ -62,9 +72,22 @@ export let FileListPublished = React.createClass({
fileInfos: null, fileInfos: null,
}; };
}, },
_requestPublishReward: function() {
lbryio.call('reward', 'list', {}).then(function(userRewards) {
//already rewarded
if (userRewards.filter(function (reward) {
return reward.RewardType == rewards.TYPE_FIRST_PUBLISH && reward.TransactionID;
}).length) {
return;
}
else {
rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
}
}, () => {});
},
componentDidMount: function () { componentDidMount: function () {
this._isMounted = true; this._isMounted = true;
document.title = "Published Files"; this._requestPublishReward();
lbry.claim_list_mine().then((claimInfos) => { lbry.claim_list_mine().then((claimInfos) => {
if (!this._isMounted) { return; } if (!this._isMounted) { return; }
@ -79,29 +102,27 @@ export let FileListPublished = React.createClass({
}); });
}); });
}, },
componentWillUnmount: function() {
this._isMounted = false;
},
render: function () { render: function () {
let content = null;
if (this.state.fileInfos === null) { if (this.state.fileInfos === null) {
return ( content = <BusyMessage message="Loading" />;
<main className="page">
<BusyMessage message="Loading" />
</main>
);
} }
else if (!this.state.fileInfos.length) { else if (!this.state.fileInfos.length) {
return ( content = <span>You haven't published anything to LBRY yet. Try <Link href="?publish" label="publishing" />!</span>;
<main className="page">
<span>You haven't published anything to LBRY yet.</span> Try <Link href="?publish" label="publishing" />!
</main>
);
} }
else { else {
content = <FileList fileInfos={this.state.fileInfos} />;
}
return ( return (
<main className="page"> <main className="main--single-column">
<FileList fileInfos={this.state.fileInfos} /> <FileListNav viewingPage="published" />
{content}
</main> </main>
); );
} }
}
}); });
export let FileList = React.createClass({ export let FileList = React.createClass({
@ -160,14 +181,24 @@ export let FileList = React.createClass({
seenUris = {}; seenUris = {};
const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos); const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos);
for (let {name, outpoint, metadata} of fileInfosSorted) { for (let {outpoint, name, channel_name, metadata, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) {
if (!metadata || seenUris[name]) { if (seenUris[name] || !claim_id) {
continue; continue;
} }
let streamMetadata;
if (metadata) {
streamMetadata = metadata.stream.metadata;
} else {
streamMetadata = null;
}
const uri = lbryuri.build({contentName: name, channelName: channel_name});
seenUris[name] = true; seenUris[name] = true;
content.push(<FileTileStream key={outpoint} outpoint={outpoint} name={name} hideOnRemove={true} content.push(<FileTileStream key={outpoint} outpoint={outpoint} uri={uri} hideOnRemove={true}
hidePrice={this.props.hidePrices} metadata={metadata} />); hidePrice={this.props.hidePrices} metadata={streamMetadata} contentType={mime_type}
hasSignature={has_signature} signatureIsValid={signature_is_valid} />);
} }
return ( return (

View file

@ -3,6 +3,7 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import {SettingsNav} from './settings.js';
import {version as uiVersion} from 'json!../../package.json'; import {version as uiVersion} from 'json!../../package.json';
var HelpPage = React.createClass({ var HelpPage = React.createClass({
@ -24,9 +25,6 @@ var HelpPage = React.createClass({
}); });
}); });
}, },
componentDidMount: function() {
document.title = "Help";
},
render: function() { render: function() {
let ver, osName, platform, newVerLink; let ver, osName, platform, newVerLink;
if (this.state.versionInfo) { if (this.state.versionInfo) {
@ -49,30 +47,42 @@ var HelpPage = React.createClass({
} }
return ( return (
<main className="page"> <main className="main--single-column">
<SettingsNav viewingPage="help" />
<section className="card"> <section className="card">
<div className="card__title-primary">
<h3>Read the FAQ</h3> <h3>Read the FAQ</h3>
</div>
<div className="card__content">
<p>Our FAQ answers many common questions.</p> <p>Our FAQ answers many common questions.</p>
<p><Link href="https://lbry.io/faq" label="Read the FAQ" icon="icon-question" button="alt"/></p> <p><Link href="https://lbry.io/faq" label="Read the FAQ" icon="icon-question" button="alt"/></p>
</div>
</section> </section>
<section className="card"> <section className="card">
<div className="card__title-primary">
<h3>Get Live Help</h3> <h3>Get Live Help</h3>
</div>
<div className="card__content">
<p> <p>
Live help is available most hours in the <strong>#help</strong> channel of our Slack chat room. Live help is available most hours in the <strong>#help</strong> channel of our Slack chat room.
</p> </p>
<p> <p>
<Link button="alt" label="Join Our Slack" icon="icon-slack" href="https://slack.lbry.io" /> <Link button="alt" label="Join Our Slack" icon="icon-slack" href="https://slack.lbry.io" />
</p> </p>
</div>
</section> </section>
<section className="card"> <section className="card">
<h3>Report a Bug</h3> <div className="card__title-primary"><h3>Report a Bug</h3></div>
<div className="card__content">
<p>Did you find something wrong?</p> <p>Did you find something wrong?</p>
<p><Link href="?report" label="Submit a Bug Report" icon="icon-bug" button="alt" /></p> <p><Link href="?report" label="Submit a Bug Report" icon="icon-bug" button="alt" /></p>
<div className="meta">Thanks! LBRY is made by its users.</div> <div className="meta">Thanks! LBRY is made by its users.</div>
</div>
</section> </section>
{!ver ? null : {!ver ? null :
<section className="card"> <section className="card">
<h3>About</h3> <div className="card__title-primary"><h3>About</h3></div>
<div className="card__content">
{ver.lbrynet_update_available || ver.lbryum_update_available ? {ver.lbrynet_update_available || ver.lbryum_update_available ?
<p>A newer version of LBRY is available. <Link href={newVerLink} label={`Download LBRY ${ver.remote_lbrynet} now!`} /></p> <p>A newer version of LBRY is available. <Link href={newVerLink} label={`Download LBRY ${ver.remote_lbrynet} now!`} /></p>
: <p>Your copy of LBRY is up to date.</p> : <p>Your copy of LBRY is up to date.</p>
@ -101,6 +111,7 @@ var HelpPage = React.createClass({
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div>
</section> </section>
} }
</main> </main>

View file

@ -1,16 +1,19 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import FormField from '../component/form.js'; import {FormField, FormRow} from '../component/form.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import rewards from '../rewards.js';
import lbryio from '../lbryio.js';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
var PublishPage = React.createClass({ var PublishPage = React.createClass({
_requiredFields: ['name', 'bid', 'meta_title', 'meta_author', 'meta_license', 'meta_description'], _requiredFields: ['meta_title', 'name', 'bid', 'tos_agree'],
_updateChannelList: function(channel) { _updateChannelList: function(channel) {
// Calls API to update displayed list of channels. If a channel name is provided, will select // Calls API to update displayed list of channels. If a channel name is provided, will select
// that channel at the same time (used immediately after creating a channel) // that channel at the same time (used immediately after creating a channel)
lbry.channel_list_mine().then((channels) => { lbry.channel_list_mine().then((channels) => {
rewards.claimReward(rewards.TYPE_FIRST_CHANNEL).then(() => {}, () => {})
this.setState({ this.setState({
channels: channels, channels: channels,
... channel ? {channel} : {} ... channel ? {channel} : {}
@ -26,20 +29,24 @@ var PublishPage = React.createClass({
submitting: true, submitting: true,
}); });
var checkFields = this._requiredFields.slice(); let checkFields = this._requiredFields;
if (!this.state.myClaimExists) { if (!this.state.myClaimExists) {
checkFields.push('file'); checkFields.unshift('file');
} }
var missingFieldFound = false; let missingFieldFound = false;
for (let fieldName of checkFields) { for (let fieldName of checkFields) {
var field = this.refs[fieldName]; const field = this.refs[fieldName];
if (field.getValue() === '') { if (field) {
field.warnRequired(); if (field.getValue() === '' || field.getValue() === false) {
field.showRequiredError();
if (!missingFieldFound) { if (!missingFieldFound) {
field.focus(); field.focus();
missingFieldFound = true; missingFieldFound = true;
} }
} else {
field.clearError();
}
} }
} }
@ -60,14 +67,16 @@ var PublishPage = React.createClass({
var metadata = {}; var metadata = {};
} }
for (let metaField of ['title', 'author', 'description', 'thumbnail', 'license', 'license_url', 'language', 'nsfw']) { for (let metaField of ['title', 'description', 'thumbnail', 'license', 'license_url', 'language']) {
var value = this.refs['meta_' + metaField].getValue(); var value = this.refs['meta_' + metaField].getValue();
if (value !== '') { if (value !== '') {
metadata[metaField] = value; metadata[metaField] = value;
} }
} }
var licenseUrl = this.refs.meta_license_url.getValue(); metadata.nsfw = Boolean(parseInt(!!this.refs.meta_nsfw.getValue()));
const licenseUrl = this.refs.meta_license_url.getValue();
if (licenseUrl) { if (licenseUrl) {
metadata.license_url = licenseUrl; metadata.license_url = licenseUrl;
} }
@ -110,17 +119,18 @@ var PublishPage = React.createClass({
channels: null, channels: null,
rawName: '', rawName: '',
name: '', name: '',
bid: '', bid: 10,
hasFile: false,
feeAmount: '', feeAmount: '',
feeCurrency: 'USD', feeCurrency: 'USD',
channel: 'anonymous', channel: 'anonymous',
newChannelName: '@', newChannelName: '@',
newChannelBid: '', newChannelBid: 10,
nameResolved: false, nameResolved: null,
myClaimExists: null,
topClaimValue: 0.0, topClaimValue: 0.0,
myClaimValue: 0.0, myClaimValue: 0.0,
myClaimMetadata: null, myClaimMetadata: null,
myClaimExists: null,
copyrightNotice: '', copyrightNotice: '',
otherLicenseDescription: '', otherLicenseDescription: '',
otherLicenseUrl: '', otherLicenseUrl: '',
@ -138,7 +148,7 @@ var PublishPage = React.createClass({
}); });
}, },
handlePublishStartedConfirmed: function() { handlePublishStartedConfirmed: function() {
window.location = "?published"; window.location.href = "?published";
}, },
handlePublishError: function(error) { handlePublishError: function(error) {
this.setState({ this.setState({
@ -161,57 +171,63 @@ var PublishPage = React.createClass({
} }
if (!lbry.nameIsValid(rawName, false)) { if (!lbry.nameIsValid(rawName, false)) {
this.refs.name.showAdvice('LBRY names must contain only letters, numbers and dashes.'); this.refs.name.showError('LBRY names must contain only letters, numbers and dashes.');
return; return;
} }
const name = rawName.toLowerCase();
this.setState({ this.setState({
rawName: rawName, rawName: rawName,
name: name,
nameResolved: null,
myClaimExists: null,
}); });
var name = rawName.toLowerCase(); lbry.getMyClaim(name, (myClaimInfo) => {
if (name != this.state.name) {
lbry.resolveName(name, (info) => {
if (name != this.refs.name.getValue().toLowerCase()) {
// A new name has been typed already, so bail // A new name has been typed already, so bail
return; return;
} }
if (!info) {
this.setState({ this.setState({
name: name, myClaimExists: !!myClaimInfo,
nameResolved: false,
myClaimExists: false,
}); });
} else { lbry.resolve({uri: name}).then((claimInfo) => {
lbry.getMyClaim(name, (myClaimInfo) => { if (name != this.state.name) {
lbry.getClaimInfo(name, (claimInfo) => {
if (name != this.refs.name.getValue()) {
return; return;
} }
const topClaimIsMine = (myClaimInfo && myClaimInfo.amount >= claimInfo.amount); if (!claimInfo) {
this.setState({
nameResolved: false,
});
} else {
const topClaimIsMine = (myClaimInfo && myClaimInfo.claim.amount >= claimInfo.claim.amount);
const newState = { const newState = {
name: name,
nameResolved: true, nameResolved: true,
topClaimValue: parseFloat(claimInfo.amount), topClaimValue: parseFloat(claimInfo.claim.amount),
myClaimExists: !!myClaimInfo, myClaimExists: !!myClaimInfo,
myClaimValue: myClaimInfo ? parseFloat(myClaimInfo.amount) : null, myClaimValue: myClaimInfo ? parseFloat(myClaimInfo.claim.amount) : null,
myClaimMetadata: myClaimInfo ? myClaimInfo.value : null, myClaimMetadata: myClaimInfo ? myClaimInfo.value : null,
topClaimIsMine: topClaimIsMine, topClaimIsMine: topClaimIsMine,
}; };
if (topClaimIsMine) { if (topClaimIsMine) {
newState.bid = myClaimInfo.amount; newState.bid = myClaimInfo.claim.amount;
} else if (this.state.myClaimMetadata) { } else if (this.state.myClaimMetadata) {
// Just changed away from a name we have a claim on, so clear pre-fill // Just changed away from a name we have a claim on, so clear pre-fill
newState.bid = ''; newState.bid = '';
} }
this.setState(newState); this.setState(newState);
});
});
} }
}, () => { // Assume an error means the name is available
this.setState({
name: name,
nameResolved: false,
myClaimExists: false,
});
});
}); });
}, },
handleBidChange: function(event) { handleBidChange: function(event) {
@ -234,7 +250,7 @@ var PublishPage = React.createClass({
isFee: feeEnabled isFee: feeEnabled
}); });
}, },
handeLicenseChange: function(event) { handleLicenseChange: function(event) {
var licenseType = event.target.options[event.target.selectedIndex].getAttribute('data-license-type'); var licenseType = event.target.options[event.target.selectedIndex].getAttribute('data-license-type');
var newState = { var newState = {
copyrightChosen: licenseType == 'copyright', copyrightChosen: licenseType == 'copyright',
@ -242,8 +258,7 @@ var PublishPage = React.createClass({
}; };
if (licenseType == 'copyright') { if (licenseType == 'copyright') {
var author = this.refs.meta_author.getValue(); newState.copyrightNotice = 'All rights reserved.'
newState.copyrightNotice = 'Copyright ' + (new Date().getFullYear()) + (author ? ' ' + author : '');
} }
this.setState(newState); this.setState(newState);
@ -274,8 +289,10 @@ var PublishPage = React.createClass({
const newChannelName = (event.target.value.startsWith('@') ? event.target.value : '@' + event.target.value); const newChannelName = (event.target.value.startsWith('@') ? event.target.value : '@' + event.target.value);
if (newChannelName.length > 1 && !lbry.nameIsValid(newChannelName.substr(1), false)) { if (newChannelName.length > 1 && !lbry.nameIsValid(newChannelName.substr(1), false)) {
this.refs.newChannelName.showAdvice('LBRY channel names must contain only letters, numbers and dashes.'); this.refs.newChannelName.showError('LBRY channel names must contain only letters, numbers and dashes.');
return; return;
} else {
this.refs.newChannelName.clearError()
} }
this.setState({ this.setState({
@ -287,9 +304,14 @@ var PublishPage = React.createClass({
newChannelBid: event.target.value, newChannelBid: event.target.value,
}); });
}, },
handleTOSChange: function(event) {
this.setState({
TOSAgreed: event.target.checked,
});
},
handleCreateChannelClick: function (event) { handleCreateChannelClick: function (event) {
if (this.state.newChannelName.length < 5) { if (this.state.newChannelName.length < 5) {
this.refs.newChannelName.showAdvice('LBRY channel names must be at least 4 characters in length.'); this.refs.newChannelName.showError('LBRY channel names must be at least 4 characters in length.');
return; return;
} }
@ -308,7 +330,7 @@ var PublishPage = React.createClass({
}, 5000); }, 5000);
}, (error) => { }, (error) => {
// TODO: better error handling // TODO: better error handling
this.refs.newChannelName.showAdvice('Unable to create channel due to an internal error.'); this.refs.newChannelName.showError('Unable to create channel due to an internal error.');
this.setState({ this.setState({
creatingChannel: false, creatingChannel: false,
}); });
@ -326,138 +348,68 @@ var PublishPage = React.createClass({
componentWillMount: function() { componentWillMount: function() {
this._updateChannelList(); this._updateChannelList();
}, },
componentDidMount: function() {
document.title = "Publish";
},
componentDidUpdate: function() { componentDidUpdate: function() {
}, },
// Also getting a type warning here too onFileChange: function() {
if (this.refs.file.getValue()) {
this.setState({ hasFile: true })
} else {
this.setState({ hasFile: false })
}
},
getNameBidHelpText: function() {
if (!this.state.name) {
return "Select a URL for this publish.";
} else if (this.state.nameResolved === false) {
return "This URL is unused.";
} else if (this.state.myClaimExists) {
return "You have already used this URL. Publishing to it again will update your previous publish."
} else if (this.state.topClaimValue) {
return <span>A deposit of at least <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit ' : 'credits '}
is required to win <strong>{this.state.name}</strong>. However, you can still get a permanent URL for any amount.</span>
} else {
return '';
}
},
closeModal: function() {
this.setState({
modal: null,
});
},
render: function() { render: function() {
if (this.state.channels === null) { if (this.state.channels === null) {
return null; return null;
} }
const lbcInputHelp = "This LBC remains yours and the deposit can be undone at any time."
return ( return (
<main ref="page"> <main className="main--single-column">
<form onSubmit={this.handleSubmit}> <form onSubmit={this.handleSubmit}>
<section className="card"> <section className="card">
<h4>LBRY Name</h4> <div className="card__title-primary">
<div className="form-row"> <h4>Content</h4>
lbry://<FormField type="text" ref="name" value={this.state.rawName} onChange={this.handleNameChange} /> <div className="card__subtitle">
{ What are you publishing?
(!this.state.name ? '' :
(! this.state.nameResolved ? <em> The name <strong>{this.state.name}</strong> is available.</em>
: (this.state.myClaimExists ? <em> You already have a claim on the name <strong>{this.state.name}</strong>. You can use this page to update your claim.</em>
: <em> The name <strong>{this.state.name}</strong> is currently claimed for <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'}.</em>)))
}
<div className="help">What LBRY name would you like to claim for this file?</div>
</div>
</section>
<section className="card">
<h4>Channel</h4>
<div className="form-row">
<FormField type="select" onChange={this.handleChannelChange} value={this.state.channel}>
<option key="anonymous" value="anonymous">Anonymous</option>
{this.state.channels.map(({name}) => <option key={name} value={name}>{name}</option>)}
<option key="new" value="new">New channel...</option>
</FormField>
{this.state.channel == 'new'
? <section>
<label>Name <FormField type="text" onChange={this.handleNewChannelNameChange} ref={newChannelName => { this.refs.newChannelName = newChannelName }}
value={this.state.newChannelName} /></label>
<label>Bid amount <FormField type="text-number" onChange={this.handleNewChannelBidChange} value={this.state.newChannelBid} /> LBC</label>
<Link button="primary" label={!this.state.creatingChannel ? 'Create channel' : 'Creating channel...'} onClick={this.handleCreateChannelClick} disabled={this.state.creatingChannel} />
</section>
: null}
<div className="help">What channel would you like to publish this file under?</div>
</div>
</section>
<section className="card">
<h4>Choose File</h4>
<FormField name="file" ref="file" type="file" />
{ this.state.myClaimExists ? <div className="help">If you don't choose a file, the file from your existing claim will be used.</div> : null }
</section>
<section className="card">
<h4>Bid Amount</h4>
<div className="form-row">
Credits <FormField ref="bid" type="text-number" onChange={this.handleBidChange} value={this.state.bid} placeholder={this.state.nameResolved ? this.state.topClaimValue + 10 : 100} />
<div className="help">How much would you like to bid for this name?
{ !this.state.nameResolved ? <span> Since this name is not currently resolved, you may bid as low as you want, but higher bids help prevent others from claiming your name.</span>
: (this.state.topClaimIsMine ? <span> You currently control this name with a bid of <strong>{this.state.myClaimValue}</strong> {this.state.myClaimValue == 1 ? 'credit' : 'credits'}.</span>
: (this.state.myClaimExists ? <span> You have a non-winning bid on this name for <strong>{this.state.myClaimValue}</strong> {this.state.myClaimValue == 1 ? 'credit' : 'credits'}.
To control this name, you'll need to increase your bid to more than <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'}.</span>
: <span> You must bid over <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'} to claim this name.</span>)) }
</div> </div>
</div> </div>
</section> <div className="card__content">
<FormRow name="file" label="File" ref="file" type="file" onChange={this.onFileChange}
<section className="card"> helper={this.state.myClaimExists ? "If you don't choose a file, the file from your existing claim will be used." : null}/>
<h4>Fee</h4>
<div className="form-row">
<label>
<FormField type="radio" onChange={ () => { this.handleFeePrefChange(false) } } checked={!this.state.isFee} /> No fee
</label>
<label>
<FormField type="radio" onChange={ () => { this.handleFeePrefChange(true) } } checked={this.state.isFee} /> { !this.state.isFee ? 'Choose fee...' : 'Fee ' }
<span className={!this.state.isFee ? 'hidden' : ''}>
<FormField type="text-number" onChange={this.handleFeeAmountChange} /> <FormField type="select" onChange={this.handleFeeCurrencyChange}>
<option value="USD">US Dollars</option>
<option value="LBC">LBRY credits</option>
</FormField>
</span>
</label>
<div className="help">
<p>How much would you like to charge for this file?</p>
If you choose to price this content in dollars, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.
</div> </div>
{ !this.state.hasFile ? '' :
<div>
<div className="card__content">
<FormRow label="Title" type="text" ref="meta_title" name="title" placeholder="Titular Title" />
</div> </div>
</section> <div className="card__content">
<FormRow type="text" label="Thumbnail URL" ref="meta_thumbnail" name="thumbnail" placeholder="http://spee.ch/mylogo" />
<section className="card">
<h4>Your Content</h4>
<div className="form-row">
<label htmlFor="title">Title</label><FormField type="text" ref="meta_title" name="title" placeholder="My Show, Episode 1" />
</div> </div>
<div className="form-row"> <div className="card__content">
<label htmlFor="author">Author</label><FormField type="text" ref="meta_author" name="author" placeholder="My Company, Inc." /> <FormRow label="Description" type="textarea" ref="meta_description" name="description" placeholder="Description of your content" />
</div> </div>
<div className="form-row"> <div className="card__content">
<label htmlFor="license">License</label><FormField type="select" ref="meta_license" name="license" onChange={this.handeLicenseChange}> <FormRow label="Language" type="select" defaultValue="en" ref="meta_language" name="language">
<option data-url="https://creativecommons.org/licenses/by/4.0/legalcode">Creative Commons Attribution 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode">Creative Commons Attribution-ShareAlike 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode">Creative Commons Attribution-NoDerivatives 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode">Creative Commons Attribution-NonCommercial 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode">Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International</option>
<option>Public Domain</option>
<option data-license-type="copyright" {... this.state.copyrightChosen ? {value: this.state.copyrightNotice} : {}}>Copyrighted...</option>
<option data-license-type="other" {... this.state.otherLicenseChosen ? {value: this.state.otherLicenseDescription} : {}}>Other...</option>
</FormField>
<FormField type="hidden" ref="meta_license_url" name="license_url" value={this.getLicenseUrl()} />
</div>
{this.state.copyrightChosen
? <div className="form-row">
<label htmlFor="copyright-notice" value={this.state.copyrightNotice}>Copyright notice</label><FormField type="text" name="copyright-notice" value={this.state.copyrightNotice} onChange={this.handleCopyrightNoticeChange} />
</div>
: null}
{this.state.otherLicenseChosen
? <div className="form-row">
<label htmlFor="other-license-description">License description</label><FormField type="text" name="other-license-description" onChange={this.handleOtherLicenseDescriptionChange} />
</div>
: null}
{this.state.otherLicenseChosen
? <div className="form-row">
<label htmlFor="other-license-url">License URL</label> <FormField type="text" name="other-license-url" onChange={this.handleOtherLicenseUrlChange} />
</div>
: null}
<div className="form-row">
<label htmlFor="language">Language</label> <FormField type="select" defaultValue="en" ref="meta_language" name="language">
<option value="en">English</option> <option value="en">English</option>
<option value="zh">Chinese</option> <option value="zh">Chinese</option>
<option value="fr">French</option> <option value="fr">French</option>
@ -465,28 +417,138 @@ var PublishPage = React.createClass({
<option value="jp">Japanese</option> <option value="jp">Japanese</option>
<option value="ru">Russian</option> <option value="ru">Russian</option>
<option value="es">Spanish</option> <option value="es">Spanish</option>
</FormRow>
</div>
<div className="card__content">
<FormRow type="select" label="Maturity" defaultValue="en" ref="meta_nsfw" name="nsfw">
{/* <option value=""></option> */}
<option value="0">All Ages</option>
<option value="1">Adults Only</option>
</FormRow>
</div>
</div>}
</section>
<section className="card">
<div className="card__title-primary">
<h4>Access</h4>
<div className="card__subtitle">
How much does this content cost?
</div>
</div>
<div className="card__content">
<div className="form-row__label-row">
<label className="form-row__label">Price</label>
</div>
<FormRow label="Free" type="radio" name="isFree" value="1" onChange={ () => { this.handleFeePrefChange(false) } } defaultChecked={!this.state.isFee} />
<FormField type="radio" name="isFree" label={!this.state.isFee ? 'Choose price...' : 'Price ' }
onChange={ () => { this.handleFeePrefChange(true) } } defaultChecked={this.state.isFee} />
<span className={!this.state.isFee ? 'hidden' : ''}>
<FormField type="number" className="form-field__input--inline" step="0.01" placeholder="1.00" onChange={this.handleFeeAmountChange} /> <FormField type="select" onChange={this.handleFeeCurrencyChange}>
<option value="USD">US Dollars</option>
<option value="LBC">LBRY credits</option>
</FormField> </FormField>
</div> </span>
<div className="form-row"> { this.state.isFee ?
<label htmlFor="description">Description</label> <FormField type="textarea" ref="meta_description" name="description" placeholder="Description of your content" /> <div className="form-field__helper">
</div> If you choose to price this content in dollars, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.
<div className="form-row"> </div> : '' }
<label><FormField type="checkbox" ref="meta_nsfw" name="nsfw" placeholder="Description of your content" /> Not Safe For Work</label> <FormRow label="License" type="select" ref="meta_license" name="license" onChange={this.handleLicenseChange}>
<option></option>
<option>Public Domain</option>
<option data-url="https://creativecommons.org/licenses/by/4.0/legalcode">Creative Commons Attribution 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode">Creative Commons Attribution-ShareAlike 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode">Creative Commons Attribution-NoDerivatives 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode">Creative Commons Attribution-NonCommercial 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode">Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International</option>
<option data-license-type="copyright" {... this.state.copyrightChosen ? {value: this.state.copyrightNotice} : {}}>Copyrighted...</option>
<option data-license-type="other" {... this.state.otherLicenseChosen ? {value: this.state.otherLicenseDescription} : {}}>Other...</option>
</FormRow>
<FormField type="hidden" ref="meta_license_url" name="license_url" value={this.getLicenseUrl()} />
{this.state.copyrightChosen
? <FormRow label="Copyright notice" type="text" name="copyright-notice"
value={this.state.copyrightNotice} onChange={this.handleCopyrightNoticeChange} />
: null}
{this.state.otherLicenseChosen ?
<FormRow label="License description" type="text" name="other-license-description" onChange={this.handleOtherLicenseDescriptionChange} />
: null}
{this.state.otherLicenseChosen ?
<FormRow label="License URL" type="text" name="other-license-url" onChange={this.handleOtherLicenseUrlChange} />
: null}
</div> </div>
</section> </section>
<section className="card">
<div className="card__title-primary">
<h4>Identity</h4>
<div className="card__subtitle">
Who created this content?
</div>
</div>
<div className="card__content">
<FormRow type="select" tabIndex="1" onChange={this.handleChannelChange} value={this.state.channel}>
<option key="anonymous" value="anonymous">Anonymous</option>
{this.state.channels.map(({name}) => <option key={name} value={name}>{name}</option>)}
<option key="new" value="new">New identity...</option>
</FormRow>
</div>
{this.state.channel == 'new' ?
<div className="card__content">
<FormRow label="Name" type="text" onChange={this.handleNewChannelNameChange} ref={newChannelName => { this.refs.newChannelName = newChannelName }}
value={this.state.newChannelName} />
<FormRow label="Deposit"
postfix="LBC"
step="0.01"
type="number"
helper={lbcInputHelp}
onChange={this.handleNewChannelBidChange}
value={this.state.newChannelBid} />
<div className="form-row-submit">
<Link button="primary" label={!this.state.creatingChannel ? 'Create identity' : 'Creating identity...'} onClick={this.handleCreateChannelClick} disabled={this.state.creatingChannel} />
</div>
</div>
: null}
</section>
<section className="card"> <section className="card">
<h4>Additional Content Information (Optional)</h4> <div className="card__title-primary">
<div className="form-row"> <h4>Address</h4>
<label htmlFor="meta_thumbnail">Thumbnail URL</label> <FormField type="text" ref="meta_thumbnail" name="thumbnail" placeholder="http://mycompany.com/images/ep_1.jpg" /> <div className="card__subtitle">Where should this content permanently reside? <Link label="Read more" href="https://lbry.io/faq/naming" />.</div>
</div>
<div className="card__content">
<FormRow prefix="lbry://" type="text" ref="name" placeholder="myname" value={this.state.rawName} onChange={this.handleNameChange}
helper={this.getNameBidHelpText()} />
</div>
{ this.state.rawName ?
<div className="card__content">
<FormRow ref="bid"
type="number"
step="0.01"
label="Deposit"
postfix="LBC"
onChange={this.handleBidChange}
value={this.state.bid}
placeholder={this.state.nameResolved ? this.state.topClaimValue + 10 : 100}
helper={lbcInputHelp} />
</div> : '' }
</section>
<section className="card">
<div className="card__title-primary">
<h4>Terms of Service</h4>
</div>
<div className="card__content">
<FormRow label={
<span>I agree to the <Link href="https://www.lbry.io/termsofservice" label="LBRY terms of service" checked={this.state.TOSAgreed} /></span>
} type="checkbox" name="tos_agree" ref={(field) => { this.refs.tos_agree = field }} onChange={this.handleTOSChange} />
</div> </div>
</section> </section>
<div className="card-series-submit"> <div className="card-series-submit">
<Link button="primary" label={!this.state.submitting ? 'Publish' : 'Publishing...'} onClick={this.handleSubmit} disabled={this.state.submitting} /> <Link button="primary" label={!this.state.submitting ? 'Publish' : 'Publishing...'} onClick={this.handleSubmit} disabled={this.state.submitting} />
<Link button="cancel" onClick={window.history.back} label="Cancel" /> <Link button="cancel" onClick={lbry.back} label="Cancel" />
<input type="submit" className="hidden" /> <input type="submit" className="hidden" />
</div> </div>
</form> </form>
@ -494,7 +556,7 @@ var PublishPage = React.createClass({
<Modal isOpen={this.state.modal == 'publishStarted'} contentLabel="File published" <Modal isOpen={this.state.modal == 'publishStarted'} contentLabel="File published"
onConfirmed={this.handlePublishStartedConfirmed}> onConfirmed={this.handlePublishStartedConfirmed}>
<p>Your file has been published to LBRY at the address <code>lbry://{this.state.name}</code>!</p> <p>Your file has been published to LBRY at the address <code>lbry://{this.state.name}</code>!</p>
You will now be taken to your My Files page, where your newly published file will be listed. The file will take a few minutes to appear for other LBRY users; until then it will be listed as "pending." <p>The file will take a few minutes to appear for other LBRY users. Until then it will be listed as "pending" under your published files.</p>
</Modal> </Modal>
<Modal isOpen={this.state.modal == 'error'} contentLabel="Error publishing file" <Modal isOpen={this.state.modal == 'error'} contentLabel="Error publishing file"
onConfirmed={this.closeModal}> onConfirmed={this.closeModal}>

View file

@ -1,130 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import {Link} from '../component/link.js';
import Modal from '../component/modal.js';
var referralCodeContentStyle = {
display: 'inline-block',
textAlign: 'left',
width: '600px',
}, referralCodeLabelStyle = {
display: 'inline-block',
cursor: 'default',
width: '130px',
textAlign: 'right',
marginRight: '6px',
};
var ReferralPage = React.createClass({
getInitialState: function() {
return {
submitting: false,
modal: null,
referralCredits: null,
failureReason: null,
}
},
handleSubmit: function(event) {
if (typeof event !== 'undefined') {
event.preventDefault();
}
if (!this.refs.code.value) {
this.setState({
modal: 'missingCode',
});
} else if (!this.refs.email.value) {
this.setState({
modal: 'missingEmail',
});
}
this.setState({
submitting: true,
});
lbry.getUnusedAddress((address) => {
var code = this.refs.code.value;
var email = this.refs.email.value;
var xhr = new XMLHttpRequest;
xhr.addEventListener('load', () => {
var response = JSON.parse(xhr.responseText);
if (response.success) {
this.setState({
modal: 'referralInfo',
referralCredits: response.referralCredits,
});
} else {
this.setState({
submitting: false,
modal: 'lookupFailed',
failureReason: response.reason,
});
}
});
xhr.addEventListener('error', () => {
this.setState({
submitting: false,
modal: 'couldNotConnect',
});
});
xhr.open('POST', 'https://invites.lbry.io/check', true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send('code=' + encodeURIComponent(code) + '&address=' + encodeURIComponent(address) +
'&email=' + encodeURIComponent(email));
});
},
closeModal: function() {
this.setState({
modal: null,
});
},
handleFinished: function() {
localStorage.setItem('claimCodeDone', true);
window.location = '?home';
},
render: function() {
return (
<main>
<form onSubmit={this.handleSubmit}>
<div className="card">
<h2>Check your referral credits</h2>
<section style={referralCodeContentStyle}>
<p>Have you referred others to LBRY? Enter your referral code and email address below to check how many credits you've earned!</p>
<p>As a reminder, your referral code is the same as your LBRY invitation code.</p>
</section>
<section>
<section><label style={referralCodeLabelStyle} htmlFor="code">Referral code</label><input name="code" ref="code" /></section>
<section><label style={referralCodeLabelStyle} htmlFor="email">Email</label><input name="email" ref="email" /></section>
</section>
<section>
<Link button="primary" label={this.state.submitting ? "Submitting..." : "Submit"}
disabled={this.state.submitting} onClick={this.handleSubmit} />
<input type='submit' className='hidden' />
</section>
</div>
</form>
<Modal isOpen={this.state.modal == 'referralInfo'} contentLabel="Credit earnings"
onConfirmed={this.handleFinished}>
{this.state.referralCredits > 0
? `You have earned ${response.referralCredits} credits from referrals. We will credit your account shortly. Thanks!`
: 'You have not earned any new referral credits since the last time you checked. Please check back in a week or two.'}
</Modal>
<Modal isOpen={this.state.modal == 'lookupFailed'} contentLabel={this.state.failureReason}
onConfirmed={this.closeModal}>
{this.state.failureReason}
</Modal>
<Modal isOpen={this.state.modal == 'couldNotConnect'} contentLabel="Couldn't confirm referral code"
onConfirmed={this.closeModal}>
LBRY couldn't connect to our servers to confirm your referral code. Please check your internet connection.
</Modal>
</main>
);
}
});
export default ReferralPage;

View file

@ -18,9 +18,6 @@ var ReportPage = React.createClass({
this._messageArea.value = ''; this._messageArea.value = '';
} }
}, },
componentDidMount: function() {
document.title = "Report an Issue";
},
closeModal: function() { closeModal: function() {
this.setState({ this.setState({
modal: null, modal: null,
@ -34,7 +31,7 @@ var ReportPage = React.createClass({
}, },
render: function() { render: function() {
return ( return (
<main className="page"> <main className="main--single-column">
<section className="card"> <section className="card">
<h3>Report an Issue</h3> <h3>Report an Issue</h3>
<p>Please describe the problem you experienced and any information you think might be useful to us. Links to screenshots are great!</p> <p>Please describe the problem you experienced and any information you think might be useful to us. Links to screenshots are great!</p>

126
ui/js/page/reward.js Normal file
View file

@ -0,0 +1,126 @@
import React from 'react';
import lbryio from '../lbryio.js';
import {Link} from '../component/link.js';
import Notice from '../component/notice.js';
import {CreditAmount} from '../component/common.js';
//
// const {shell} = require('electron');
// const querystring = require('querystring');
//
// const GITHUB_CLIENT_ID = '6baf581d32bad60519';
//
// const LinkGithubReward = React.createClass({
// propTypes: {
// onClaimed: React.PropTypes.func,
// },
// _launchLinkPage: function() {
// /* const githubAuthParams = {
// client_id: GITHUB_CLIENT_ID,
// redirect_uri: 'https://lbry.io/',
// scope: 'user:email,public_repo',
// allow_signup: false,
// }
// shell.openExternal('https://github.com/login/oauth/authorize?' + querystring.stringify(githubAuthParams)); */
// shell.openExternal('https://lbry.io');
// },
// handleConfirmClicked: function() {
// this.setState({
// confirming: true,
// });
//
// lbry.get_new_address().then((address) => {
// lbryio.call('reward', 'new', {
// reward_type: 'new_developer',
// access_token: '**access token here**',
// wallet_address: address,
// }, 'post').then((response) => {
// console.log('response:', response);
//
// this.props.onClaimed(); // This will trigger another API call to show that we succeeded
//
// this.setState({
// confirming: false,
// error: null,
// });
// }, (error) => {
// console.log('failed with error:', error);
// this.setState({
// confirming: false,
// error: error,
// });
// });
// });
// },
// getInitialState: function() {
// return {
// confirming: false,
// error: null,
// };
// },
// render: function() {
// return (
// <section>
// <p><Link button="alt" label="Link with GitHub" onClick={this._launchLinkPage} /></p>
// <section className="reward-page__details">
// <p>This will open a browser window where you can authorize GitHub to link your account to LBRY. This will record your email (no spam) and star the LBRY repo.</p>
// <p>Once you're finished, you may confirm you've linked the account to receive your reward.</p>
// </section>
// {this.state.error
// ? <Notice isError>
// {this.state.error.message}
// </Notice>
// : null}
//
// <Link button="primary" label={this.state.confirming ? 'Confirming...' : 'Confirm'}
// onClick={this.handleConfirmClicked} />
// </section>
// );
// }
// });
//
// const RewardPage = React.createClass({
// propTypes: {
// name: React.PropTypes.string.isRequired,
// },
// _getRewardType: function() {
// lbryio.call('reward_type', 'get', this.props.name).then((rewardType) => {
// this.setState({
// rewardType: rewardType,
// });
// });
// },
// getInitialState: function() {
// return {
// rewardType: null,
// };
// },
// componentWillMount: function() {
// this._getRewardType();
// },
// render: function() {
// if (!this.state.rewardType) {
// return null;
// }
//
// let Reward;
// if (this.props.name == 'link_github') {
// Reward = LinkGithubReward;
// }
//
// const {title, description, value} = this.state.rewardType;
// return (
// <main>
// <section className="card">
// <h2>{title}</h2>
// <CreditAmount amount={value} />
// <p>{this.state.rewardType.claimed
// ? <span class="empty">This reward has been claimed.</span>
// : description}</p>
// <Reward onClaimed={this._getRewardType} />
// </section>
// </main>
// );
// }
// });
//
// export default RewardPage;

74
ui/js/page/rewards.js Normal file
View file

@ -0,0 +1,74 @@
import React from 'react';
import lbry from '../lbry.js';
import lbryio from '../lbryio.js';
import {CreditAmount, Icon} from '../component/common.js';
import rewards from '../rewards.js';
import Modal from '../component/modal.js';
import {WalletNav} from './wallet.js';
import {RewardLink} from '../component/link.js';
const RewardTile = React.createClass({
propTypes: {
type: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool.isRequired,
value: React.PropTypes.number.isRequired,
onRewardClaim: React.PropTypes.func
},
render: function() {
return (
<section className="card">
<div className="card__inner">
<div className="card__title-primary">
<CreditAmount amount={this.props.value} />
<h3>{this.props.title}</h3>
</div>
<div className="card__actions">
{this.props.claimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <RewardLink {...this.props} />}
</div>
<div className="card__content">{this.props.description}</div>
</div>
</section>
);
}
});
var RewardsPage = React.createClass({
componentWillMount: function() {
this.loadRewards()
},
getInitialState: function() {
return {
userRewards: null,
failed: null
};
},
loadRewards: function() {
lbryio.call('reward', 'list', {}).then((userRewards) => {
this.setState({
userRewards: userRewards,
});
}, () => {
this.setState({failed: true })
});
},
render: function() {
return (
<main className="main--single-column">
<WalletNav viewingPage="rewards"/>
<div>
{!this.state.userRewards
? (this.state.failed ? <div className="empty">Failed to load rewards.</div> : '')
: this.state.userRewards.map(({RewardType, RewardTitle, RewardDescription, TransactionID, RewardAmount}) => {
return <RewardTile key={RewardType} onRewardClaim={this.loadRewards} type={RewardType} title={RewardTitle} description={RewardDescription} claimed={!!TransactionID} value={RewardAmount} />;
})}
</div>
</main>
);
}
});
export default RewardsPage;

165
ui/js/page/search.js Normal file
View file

@ -0,0 +1,165 @@
import React from 'react';
import lbry from '../lbry.js';
import lbryio from '../lbryio.js';
import lbryuri from '../lbryuri.js';
import lighthouse from '../lighthouse.js';
import {FileTile, FileTileStream} from '../component/file-tile.js';
import {Link} from '../component/link.js';
import {ToolTip} from '../component/tooltip.js';
import {BusyMessage} from '../component/common.js';
var SearchNoResults = React.createClass({
render: function() {
return <section>
<span className="empty">
No one has checked anything in for {this.props.query} yet.
<Link label="Be the first" href="?publish" />
</span>
</section>;
}
});
var SearchResultList = React.createClass({
render: function() {
var rows = [],
seenNames = {}; //fix this when the search API returns claim IDs
for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) {
const uri = lbryuri.build({
channelName: channel_name,
contentName: name,
claimId: channel_id || claim_id,
});
rows.push(
<FileTileStream key={uri} uri={uri} outpoint={txid + ':' + nout} metadata={claim.stream.metadata} contentType={claim.stream.source.contentType} />
);
}
return (
<div>{rows}</div>
);
}
});
let SearchResults = React.createClass({
propTypes: {
query: React.PropTypes.string.isRequired
},
_isMounted: false,
search: function(term) {
lighthouse.search(term).then(this.searchCallback);
if (!this.state.searching) {
this.setState({ searching: true })
}
},
componentWillMount: function () {
this._isMounted = true;
this.search(this.props.query);
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.query != this.props.query) {
this.search(nextProps.query);
}
},
componentWillUnmount: function () {
this._isMounted = false;
},
getInitialState: function () {
return {
results: [],
searching: true
};
},
searchCallback: function (results) {
if (this._isMounted) //could have canceled while results were pending, in which case nothing to do
{
this.setState({
results: results,
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
});
}
},
render: function () {
return this.state.searching ?
<BusyMessage message="Looking up the Dewey Decimals" /> :
(this.state.results && this.state.results.length ?
<SearchResultList results={this.state.results} /> :
<SearchNoResults query={this.props.query} />);
}
});
let SearchPage = React.createClass({
_isMounted: false,
propTypes: {
query: React.PropTypes.string.isRequired
},
isValidUri: function(query) {
try {
lbryuri.parse(query);
return true;
} catch (e) {
return false;
}
},
componentWillMount: function() {
this._isMounted = true;
lighthouse.search(this.props.query).then(this.searchCallback);
},
componentWillUnmount: function() {
this._isMounted = false;
},
getInitialState: function() {
return {
results: [],
searching: true
};
},
searchCallback: function(results) {
if (this._isMounted) //could have canceled while results were pending, in which case nothing to do
{
this.setState({
results: results,
searching: false //multiple searches can be out, we're only done if we receive one we actually care about
});
}
},
render: function() {
return (
<main className="main--single-column">
{ this.isValidUri(this.props.query) ?
<section className="section-spaced">
<h3 className="card-row__header">
Exact URL
<ToolTip label="?" body="This is the resolution of a LBRY URL and not controlled by LBRY Inc." className="tooltip--header"/>
</h3>
<FileTile uri={this.props.query} showEmpty={true} />
</section> : '' }
<section className="section-spaced">
<h3 className="card-row__header">
Search Results for {this.props.query}
<ToolTip label="?" body="These search results are provided by LBRY Inc." className="tooltip--header"/>
</h3>
<SearchResults query={this.props.query} />
</section>
</main>
);
}
});
export default SearchPage;

View file

@ -1,52 +1,63 @@
import React from 'react'; import React from 'react';
import {FormField, FormRow} from '../component/form.js';
import {SubHeader} from '../component/header.js';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
var settingsRadioOptionStyles = { export let SettingsNav = React.createClass({
display: 'block', render: function() {
marginLeft: '13px' return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
}, settingsCheckBoxOptionStyles = { '?settings': 'Settings',
display: 'block', '?help' : 'Help'
marginLeft: '13px' }} />;
}, settingsNumberFieldStyles = { }
width: '40px' });
}, downloadDirectoryLabelStyles = {
fontSize: '.9em',
marginLeft: '13px'
}, downloadDirectoryFieldStyles= {
width: '300px'
};
var SettingsPage = React.createClass({ var SettingsPage = React.createClass({
_onSettingSaveSuccess: function() {
// This is bad.
// document.dispatchEvent(new CustomEvent('globalNotice', {
// detail: {
// message: "Settings saved",
// },
// }))
},
setDaemonSetting: function(name, value) {
lbry.setDaemonSetting(name, value, this._onSettingSaveSuccess)
},
setClientSetting: function(name, value) {
lbry.setClientSetting(name, value)
this._onSettingSaveSuccess()
},
onRunOnStartChange: function (event) { onRunOnStartChange: function (event) {
lbry.setDaemonSetting('run_on_startup', event.target.checked); this.setDaemonSetting('run_on_startup', event.target.checked);
}, },
onShareDataChange: function (event) { onShareDataChange: function (event) {
lbry.setDaemonSetting('share_debug_info', event.target.checked); this.setDaemonSetting('share_usage_data', event.target.checked);
}, },
onDownloadDirChange: function(event) { onDownloadDirChange: function(event) {
lbry.setDaemonSetting('download_directory', event.target.value); this.setDaemonSetting('download_directory', event.target.value);
}, },
onMaxUploadPrefChange: function(isLimited) { onMaxUploadPrefChange: function(isLimited) {
if (!isLimited) { if (!isLimited) {
lbry.setDaemonSetting('max_upload', 0.0); this.setDaemonSetting('max_upload', 0.0);
} }
this.setState({ this.setState({
isMaxUpload: isLimited isMaxUpload: isLimited
}); });
}, },
onMaxUploadFieldChange: function(event) { onMaxUploadFieldChange: function(event) {
lbry.setDaemonSetting('max_upload', Number(event.target.value)); this.setDaemonSetting('max_upload', Number(event.target.value));
}, },
onMaxDownloadPrefChange: function(isLimited) { onMaxDownloadPrefChange: function(isLimited) {
if (!isLimited) { if (!isLimited) {
lbry.setDaemonSetting('max_download', 0.0); this.setDaemonSetting('max_download', 0.0);
} }
this.setState({ this.setState({
isMaxDownload: isLimited isMaxDownload: isLimited
}); });
}, },
onMaxDownloadFieldChange: function(event) { onMaxDownloadFieldChange: function(event) {
lbry.setDaemonSetting('max_download', Number(event.target.value)); this.setDaemonSetting('max_download', Number(event.target.value));
}, },
getInitialState: function() { getInitialState: function() {
return { return {
@ -55,100 +66,144 @@ var SettingsPage = React.createClass({
showUnavailable: lbry.getClientSetting('showUnavailable'), showUnavailable: lbry.getClientSetting('showUnavailable'),
} }
}, },
componentDidMount: function() {
document.title = "Settings";
},
componentWillMount: function() { componentWillMount: function() {
lbry.getDaemonSettings(function(settings) { lbry.getDaemonSettings((settings) => {
this.setState({ this.setState({
daemonSettings: settings, daemonSettings: settings,
isMaxUpload: settings.max_upload != 0, isMaxUpload: settings.max_upload != 0,
isMaxDownload: settings.max_download != 0 isMaxDownload: settings.max_download != 0
}); });
}.bind(this)); });
}, },
onShowNsfwChange: function(event) { onShowNsfwChange: function(event) {
lbry.setClientSetting('showNsfw', event.target.checked); lbry.setClientSetting('showNsfw', event.target.checked);
}, },
onShowUnavailableChange: function(event) { onShowUnavailableChange: function(event) {
lbry.setClientSetting('showUnavailable', event.target.checked);
}, },
render: function() { render: function() {
if (!this.state.daemonSettings) { if (!this.state.daemonSettings) {
return null; return null;
} }
/*
return (
<main>
<section className="card"> <section className="card">
<div className="card__content">
<h3>Run on Startup</h3> <h3>Run on Startup</h3>
<label style={settingsCheckBoxOptionStyles}> </div>
<input type="checkbox" onChange={this.onRunOnStartChange} defaultChecked={this.state.daemonSettings.run_on_startup} /> Run LBRY automatically when I start my computer <div className="card__content">
</label> <FormRow type="checkbox"
onChange={this.onRunOnStartChange}
defaultChecked={this.state.daemonSettings.run_on_startup}
label="Run LBRY automatically when I start my computer" />
</div>
</section> </section>
*/
return (
<main className="main--single-column">
<SettingsNav viewingPage="settings" />
<section className="card"> <section className="card">
<div className="card__content">
<h3>Download Directory</h3> <h3>Download Directory</h3>
<div className="help">Where would you like the files you download from LBRY to be saved?</div> </div>
<input style={downloadDirectoryFieldStyles} type="text" name="download_directory" defaultValue={this.state.daemonSettings.download_directory} onChange={this.onDownloadDirChange}/> <div className="card__content">
<FormRow type="text"
name="download_directory"
defaultValue={this.state.daemonSettings.download_directory}
helper="LBRY downloads will be saved here."
onChange={this.onDownloadDirChange} />
</div>
</section> </section>
<section className="card"> <section className="card">
<div className="card__content">
<h3>Bandwidth Limits</h3> <h3>Bandwidth Limits</h3>
<div className="form-row">
<h4>Max Upload</h4>
<label style={settingsRadioOptionStyles}>
<input type="radio" name="max_upload_pref" onChange={this.onMaxUploadPrefChange.bind(this, false)} defaultChecked={!this.state.isMaxUpload}/> Unlimited
</label>
<label style={settingsRadioOptionStyles}>
<input type="radio" name="max_upload_pref" onChange={this.onMaxUploadPrefChange.bind(this, true)} defaultChecked={this.state.isMaxUpload}/> { this.state.isMaxUpload ? 'Up to' : 'Choose limit...' }
<span className={ this.state.isMaxUpload ? '' : 'hidden'}> <input type="number" min="0" step=".5" defaultValue={this.state.daemonSettings.max_upload} style={settingsNumberFieldStyles} onChange={this.onMaxUploadFieldChange}/> MB/s</span>
</label>
</div> </div>
<div className="card__content">
<div className="form-row__label-row"><div className="form-field__label">Max Upload</div></div>
<FormRow type="radio"
name="max_upload_pref"
onChange={() => { this.onMaxUploadPrefChange(false) }}
defaultChecked={!this.state.isMaxUpload}
label="Unlimited" />
<div className="form-row"> <div className="form-row">
<h4>Max Download</h4> <FormField type="radio"
<label style={settingsRadioOptionStyles}> name="max_upload_pref"
<input type="radio" name="max_download_pref" onChange={this.onMaxDownloadPrefChange.bind(this, false)} defaultChecked={!this.state.isMaxDownload}/> Unlimited onChange={() => { this.onMaxUploadPrefChange(true) }}
</label> defaultChecked={this.state.isMaxUpload}
<label style={settingsRadioOptionStyles}> label={ this.state.isMaxUpload ? 'Up to' : 'Choose limit...' } />
<input type="radio" name="max_download_pref" onChange={this.onMaxDownloadPrefChange.bind(this, true)} defaultChecked={this.state.isMaxDownload}/> { this.state.isMaxDownload ? 'Up to' : 'Choose limit...' } { this.state.isMaxUpload ?
<span className={ this.state.isMaxDownload ? '' : 'hidden'}> <input type="number" min="0" step=".5" defaultValue={this.state.daemonSettings.max_download} style={settingsNumberFieldStyles} onChange={this.onMaxDownloadFieldChange}/> MB/s</span> <FormField type="number"
</label> min="0"
step=".5"
defaultValue={this.state.daemonSettings.max_upload}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxUploadFieldChange}
/>
: ''
}
{ this.state.isMaxUpload ? <span className="form-field__label">MB/s</span> : '' }
</div>
</div>
<div className="card__content">
<div className="form-row__label-row"><div className="form-field__label">Max Download</div></div>
<FormRow label="Unlimited"
type="radio"
name="max_download_pref"
onChange={() => { this.onMaxDownloadPrefChange(false) }}
defaultChecked={!this.state.isMaxDownload} />
<div className="form-row">
<FormField type="radio"
name="max_download_pref"
onChange={() => { this.onMaxDownloadPrefChange(true) }}
defaultChecked={this.state.isMaxDownload}
label={ this.state.isMaxDownload ? 'Up to' : 'Choose limit...' } />
{ this.state.isMaxDownload ?
<FormField type="number"
min="0"
step=".5"
defaultValue={this.state.daemonSettings.max_download}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxDownloadFieldChange}
/>
: ''
}
{ this.state.isMaxDownload ? <span className="form-field__label">MB/s</span> : '' }
</div>
</div> </div>
</section> </section>
<section className="card"> <section className="card">
<div className="card__content">
<h3>Content</h3> <h3>Content</h3>
<div className="form-row">
<label style={settingsCheckBoxOptionStyles}>
<input type="checkbox" onChange={this.onShowNsfwChange} defaultChecked={this.state.showNsfw} /> Show NSFW content
</label>
<div className="help">
NSFW content may include nudity, intense sexuality, profanity, or other adult content.
By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction.
</div> </div>
<div className="card__content">
<FormRow type="checkbox"
onChange={this.onShowUnavailableChange}
defaultChecked={this.state.showUnavailable}
label="Show unavailable content in search results" />
</div>
<div className="card__content">
<FormRow label="Show NSFW content" type="checkbox"
onChange={this.onShowNsfwChange} defaultChecked={this.state.showNsfw}
helper="NSFW content may include nudity, intense sexuality, profanity, or other adult content. By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction. " />
</div> </div>
</section> </section>
<section className="card"> <section className="card">
<h3>Search</h3> <div className="card__content">
<div className="form-row">
<div className="help">
Would you like search results to include items that are not currently available for download?
</div>
<label style={settingsCheckBoxOptionStyles}>
<input type="checkbox" onChange={this.onShowUnavailableChange} defaultChecked={this.state.showUnavailable} />
Show unavailable content in search results
</label>
</div>
</section>
<section className="card">
<h3>Share Diagnostic Data</h3> <h3>Share Diagnostic Data</h3>
<label style={settingsCheckBoxOptionStyles}> </div>
<input type="checkbox" onChange={this.onShareDataChange} defaultChecked={this.state.daemonSettings.share_debug_info} /> <div className="card__content">
Help make LBRY better by contributing diagnostic data about my usage <FormRow type="checkbox"
</label> onChange={this.onShareDataChange}
defaultChecked={this.state.daemonSettings.share_usage_data}
label="Help make LBRY better by contributing diagnostic data about my usage" />
</div>
</section> </section>
</main> </main>
); );
} }
}); });
export default SettingsPage; export default SettingsPage;

View file

@ -1,55 +1,32 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lighthouse from '../lighthouse.js'; import lighthouse from '../lighthouse.js';
import {CreditAmount, Thumbnail} from '../component/common.js'; import lbryuri from '../lbryuri.js';
import {Video} from '../page/watch.js'
import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js';
import {FileActions} from '../component/file-actions.js'; import {FileActions} from '../component/file-actions.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import UriIndicator from '../component/channel-indicator.js';
var formatItemImgStyle = {
maxWidth: '100%',
maxHeight: '100%',
display: 'block',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: '5px',
};
var FormatItem = React.createClass({ var FormatItem = React.createClass({
propTypes: { propTypes: {
claimInfo: React.PropTypes.object, metadata: React.PropTypes.object,
cost: React.PropTypes.number, contentType: React.PropTypes.string,
name: React.PropTypes.string, uri: React.PropTypes.string,
outpoint: React.PropTypes.string, outpoint: React.PropTypes.string,
costIncludesData: React.PropTypes.bool,
}, },
render: function() { render: function() {
var claimInfo = this.props.claimInfo; const {author, language, license} = this.props.metadata;
var thumbnail = claimInfo.thumbnail;
var title = claimInfo.title; if (!this.props.contentType && [author, language, license].filter((val) => {return !!val; }).length === 0) {
var description = claimInfo.description; return null;
var author = claimInfo.author; }
var language = claimInfo.language;
var license = claimInfo.license;
var fileContentType = (claimInfo.content_type || claimInfo['content-type']);
var mediaType = lbry.getMediaType(fileContentType);
var costIncludesData = this.props.costIncludesData;
var cost = this.props.cost || 0.0;
return ( return (
<div className="row-fluid">
<div className="span4">
<Thumbnail src={thumbnail} alt={'Photo for ' + title} style={formatItemImgStyle} />
</div>
<div className="span8">
<p>{description}</p>
<section>
<table className="table-standard"> <table className="table-standard">
<tbody> <tbody>
<tr> <tr>
<td>Content-Type</td><td>{fileContentType}</td> <td>Content-Type</td><td>{this.props.contentType}</td>
</tr>
<tr>
<td>Cost</td><td><CreditAmount amount={cost} isEstimate={!costIncludesData}/></td>
</tr> </tr>
<tr> <tr>
<td>Author</td><td>{author}</td> <td>Author</td><td>{author}</td>
@ -62,105 +39,246 @@ var FormatItem = React.createClass({
</tr> </tr>
</tbody> </tbody>
</table> </table>
</section>
<FileActions streamName={this.props.name} outpoint={this.props.outpoint} metadata={claimInfo} />
<section>
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
</section>
</div>
</div>
); );
} }
}); });
var FormatsSection = React.createClass({ let ChannelPage = React.createClass({
propTypes: {
claimInfo: React.PropTypes.object,
cost: React.PropTypes.number,
name: React.PropTypes.string,
costIncludesData: React.PropTypes.bool,
},
render: function() { render: function() {
var name = this.props.name; return <main className="main--single-column">
var format = this.props.claimInfo; <section className="card">
var title = format.title; <div className="card__inner">
<div className="card__title-identity"><h1>{this.props.title}</h1></div>
if(format == null) </div>
{ <div className="card__content">
return ( <p>
<div> This channel page is a stub.
<h2>Sorry, no results found for "{name}".</h2> </p>
</div>); </div>
} </section>
</main>
return (
<div>
<div className="meta">lbry://{name}</div>
<h2>{title}</h2>
{/* In future, anticipate multiple formats, just a guess at what it could look like
// var formats = this.props.claimInfo.formats
// return (<tbody>{formats.map(function(format,i){ */}
<FormatItem claimInfo={format} cost={this.props.cost} name={this.props.name} costIncludesData={this.props.costIncludesData} />
{/* })}</tbody>); */}
</div>);
} }
}); });
var DetailPage = React.createClass({ let FilePage = React.createClass({
_isMounted: false,
propTypes: { propTypes: {
name: React.PropTypes.string, uri: React.PropTypes.string,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
metadata: null,
cost: null, cost: null,
costIncludesData: null, costIncludesData: null,
nameLookupComplete: null, isDownloaded: null,
}; };
}, },
componentWillUnmount: function() {
this._isMounted = false;
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.outpoint != this.props.outpoint || nextProps.uri != this.props.uri) {
this.loadCostAndFileState(nextProps.uri, nextProps.outpoint);
}
},
componentWillMount: function() { componentWillMount: function() {
document.title = 'lbry://' + this.props.name; this._isMounted = true;
this.loadCostAndFileState(this.props.uri, this.props.outpoint);
},
lbry.claim_show({name: this.props.name}, ({name, txid, nout, value}) => { loadCostAndFileState: function(uri, outpoint) {
lbry.file_list({outpoint: outpoint}).then((fileInfo) => {
if (this._isMounted) {
this.setState({ this.setState({
outpoint: txid + ':' + nout, isDownloaded: fileInfo.length > 0,
metadata: value,
nameLookupComplete: true,
}); });
}
}); });
lbry.getCostInfoForName(this.props.name, ({cost, includesData}) => { lbry.getCostInfo(uri).then(({cost, includesData}) => {
if (this._isMounted) {
this.setState({ this.setState({
cost: cost, cost: cost,
costIncludesData: includesData, costIncludesData: includesData,
}); });
}
}); });
}, },
render: function() {
if (this.state.metadata == null) {
return null;
}
const name = this.props.name; render: function() {
const costIncludesData = this.state.costIncludesData; const metadata = this.props.metadata,
const metadata = this.state.metadata; title = metadata ? this.props.metadata.title : this.props.uri,
const cost = this.state.cost; uriIndicator = <UriIndicator uri={this.props.uri} hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />;
const outpoint = this.state.outpoint;
return ( return (
<main> <main className="main--single-column">
<section className="card"> <section className="show-page-media">
{this.state.nameLookupComplete ? ( { this.props.contentType && this.props.contentType.startsWith('video/') ?
<FormatsSection name={name} outpoint={outpoint} claimInfo={metadata} cost={cost} costIncludesData={costIncludesData} /> <Video className="video-embedded" uri={this.props.uri} metadata={metadata} outpoint={this.props.outpoint} /> :
) : ( (metadata ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) }
<div>
<h2>No content</h2>
There is no content available at the name <strong>lbry://{this.props.name}</strong>. If you reached this page from a link within the LBRY interface, please <Link href="?report" label="report a bug" />. Thanks!
</div>
)}
</section> </section>
</main>); <section className="card">
<div className="card__inner">
<div className="card__title-identity">
{this.state.isDownloaded === false
? <span style={{float: "right"}}><FilePrice uri={this.props.uri} metadata={metadata} /></span>
: null}
<h1>{title}</h1>
<div className="card__subtitle">
{ this.props.channelUri ?
<Link href={"?show=" + this.props.channelUri }>{uriIndicator}</Link> :
uriIndicator}
</div>
<div className="card__actions">
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} />
</div>
</div>
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
{metadata.description}
</div>
</div>
{ metadata ?
<div className="card__content">
<FormatItem metadata={metadata} contentType={this.state.contentType} cost={this.state.cost} uri={this.props.uri} outpoint={this.props.outpoint} costIncludesData={this.state.costIncludesData} />
</div> : '' }
<div className="card__content">
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
</div>
</section>
</main>
);
} }
}); });
export default DetailPage; let ShowPage = React.createClass({
_uri: null,
_isMounted: false,
propTypes: {
uri: React.PropTypes.string,
},
getInitialState: function() {
return {
outpoint: null,
metadata: null,
contentType: null,
hasSignature: false,
claimType: null,
signatureIsValid: false,
cost: null,
costIncludesData: null,
uriLookupComplete: null,
isFailed: false,
};
},
componentWillUnmount: function() {
this._isMounted = false;
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.uri != this.props.uri) {
this.setState(this.getInitialState());
this.loadUri(nextProps.uri);
}
},
componentWillMount: function() {
this._isMounted = true;
this.loadUri(this.props.uri);
},
loadUri: function(uri) {
this._uri = lbryuri.normalize(uri);
lbry.resolve({uri: this._uri}).then((resolveData) => {
const isChannel = resolveData && resolveData.claims_in_channel;
if (!this._isMounted) {
return;
}
if (resolveData) {
let newState = { uriLookupComplete: true }
if (!isChannel) {
let {claim: {txid: txid, nout: nout, has_signature: has_signature, signature_is_valid: signature_is_valid, value: {stream: {metadata: metadata, source: {contentType: contentType}}}}} = resolveData;
Object.assign(newState, {
claimType: "file",
metadata: metadata,
outpoint: txid + ':' + nout,
hasSignature: has_signature,
signatureIsValid: signature_is_valid,
contentType: contentType
});
lbry.setTitle(metadata.title ? metadata.title : this._uri)
} else {
let {certificate: {txid: txid, nout: nout, has_signature: has_signature}} = resolveData;
Object.assign(newState, {
claimType: "channel",
outpoint: txid + ':' + nout,
txid: txid,
metadata: {
title:resolveData.certificate.name
}
});
}
this.setState(newState);
} else {
this.setState(Object.assign({}, this.getInitialState(), {
uriLookupComplete: true,
isFailed: true
}));
}
});
},
render: function() {
const metadata = this.state.metadata,
title = metadata ? this.state.metadata.title : this._uri;
let innerContent = "";
if (!this.state.uriLookupComplete || this.state.isFailed) {
innerContent = <section className="card">
<div className="card__inner">
<div className="card__title-identity"><h1>{title}</h1></div>
</div>
<div className="card__content">
{ this.state.uriLookupComplete ?
<p>This location is not yet in use. { ' ' }<Link href="?publish" label="Put something here" />.</p> :
<BusyMessage message="Loading magic decentralized data..." />
}
</div>
</section>;
} else if (this.state.claimType == "channel") {
innerContent = <ChannelPage title={this._uri} />
} else {
let channelUriObj = lbryuri.parse(this._uri)
delete channelUriObj.path;
delete channelUriObj.contentName;
const channelUri = this.state.signatureIsValid && this.state.hasSignature && channelUriObj.isChannel ? lbryuri.build(channelUriObj, false) : null;
innerContent = <FilePage
uri={this._uri}
channelUri={channelUri}
outpoint={this.state.outpoint}
metadata={metadata}
contentType={this.state.contentType}
hasSignature={this.state.hasSignature}
signatureIsValid={this.state.signatureIsValid}
/>;
}
return <main className="main--single-column">{innerContent}</main>;
}
});
export default ShowPage;

View file

@ -5,12 +5,9 @@ var StartPage = React.createClass({
componentWillMount: function() { componentWillMount: function() {
lbry.stop(); lbry.stop();
}, },
componentDidMount: function() {
document.title = "LBRY is Closed";
},
render: function() { render: function() {
return ( return (
<main className="page"> <main className="main--single-column">
<h3>LBRY is Closed</h3> <h3>LBRY is Closed</h3>
<Link href="lbry://lbry" label="Click here to start LBRY" /> <Link href="lbry://lbry" label="Click here to start LBRY" />
</main> </main>

View file

@ -2,12 +2,10 @@ import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
import {SubHeader} from '../component/header.js';
import {FormField, FormRow} from '../component/form.js';
import {Address, BusyMessage, CreditAmount} from '../component/common.js'; import {Address, BusyMessage, CreditAmount} from '../component/common.js';
var addressRefreshButtonStyle = {
fontSize: '11pt',
};
var AddressSection = React.createClass({ var AddressSection = React.createClass({
_refreshAddress: function(event) { _refreshAddress: function(event) {
if (typeof event !== 'undefined') { if (typeof event !== 'undefined') {
@ -60,12 +58,20 @@ var AddressSection = React.createClass({
render: function() { render: function() {
return ( return (
<section className="card"> <section className="card">
<div className="card__title-primary">
<h3>Wallet Address</h3> <h3>Wallet Address</h3>
<Address address={this.state.address} /> <Link text="Get new address" icon='icon-refresh' onClick={this._getNewAddress} style={addressRefreshButtonStyle} /> </div>
<input type='submit' className='hidden' /> <div className="card__content">
<Address address={this.state.address} />
</div>
<div className="card__actions">
<Link label="Get New Address" button="primary" icon='icon-refresh' onClick={this._getNewAddress} />
</div>
<div className="card__content">
<div className="help"> <div className="help">
<p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p> <p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p>
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. <p>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.</p>
</div>
</div> </div>
</section> </section>
); );
@ -143,26 +149,25 @@ var SendToAddressSection = React.createClass({
return ( return (
<section className="card"> <section className="card">
<form onSubmit={this.handleSubmit}> <form onSubmit={this.handleSubmit}>
<div className="card__title-primary">
<h3>Send Credits</h3> <h3>Send Credits</h3>
<div className="form-row">
<label htmlFor="amount">Amount</label>
<input id="amount" type="text" size="10" onChange={this.setAmount}></input>
</div> </div>
<div className="form-row"> <div className="card__content">
<label htmlFor="address">Recipient address</label> <FormRow label="Amount" postfix="LBC" step="0.01" type="number" placeholder="1.23" size="10" onChange={this.setAmount} />
<input id="address" type="text" size="60" onChange={this.setAddress}></input>
</div> </div>
<div className="form-row form-row-submit"> <div className="card__content">
<FormRow label="Recipient Address" placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs" type="text" size="60" onChange={this.setAddress} />
</div>
<div className="card__actions card__actions--form-submit">
<Link button="primary" label="Send" onClick={this.handleSubmit} disabled={!(parseFloat(this.state.amount) > 0.0) || this.state.address == ""} /> <Link button="primary" label="Send" onClick={this.handleSubmit} disabled={!(parseFloat(this.state.amount) > 0.0) || this.state.address == ""} />
<input type='submit' className='hidden' /> <input type='submit' className='hidden' />
</div> </div>
{ {
this.state.results ? this.state.results ?
<div className="form-row"> <div className="card__content">
<h4>Results</h4> <h4>Results</h4>
{this.state.results} {this.state.results}
</div> </div> : ''
: ''
} }
</form> </form>
<Modal isOpen={this.state.modal === 'insufficientBalance'} contentLabel="Insufficient balance" <Modal isOpen={this.state.modal === 'insufficientBalance'} contentLabel="Insufficient balance"
@ -231,7 +236,10 @@ var TransactionList = React.createClass({
} }
return ( return (
<section className="card"> <section className="card">
<div className="card__title-primary">
<h3>Transaction History</h3> <h3>Transaction History</h3>
</div>
<div className="card__content">
{ this.state.transactionItems === null ? <BusyMessage message="Loading transactions" /> : '' } { this.state.transactionItems === null ? <BusyMessage message="Loading transactions" /> : '' }
{ this.state.transactionItems && rows.length === 0 ? <div className="empty">You have no transactions.</div> : '' } { this.state.transactionItems && rows.length === 0 ? <div className="empty">You have no transactions.</div> : '' }
{ this.state.transactionItems && rows.length > 0 ? { this.state.transactionItems && rows.length > 0 ?
@ -250,11 +258,22 @@ var TransactionList = React.createClass({
</table> </table>
: '' : ''
} }
</div>
</section> </section>
); );
} }
}); });
export let WalletNav = React.createClass({
render: function() {
return <SubHeader modifier="constrained" viewingPage={this.props.viewingPage} links={{
'?wallet': 'Overview',
'?send': 'Send',
'?receive': 'Receive',
'?rewards': 'Rewards'
}} />;
}
});
var WalletPage = React.createClass({ var WalletPage = React.createClass({
_balanceSubscribeId: null, _balanceSubscribeId: null,
@ -262,9 +281,6 @@ var WalletPage = React.createClass({
propTypes: { propTypes: {
viewingPage: React.PropTypes.string, viewingPage: React.PropTypes.string,
}, },
componentDidMount: function() {
document.title = "My Wallet";
},
/* /*
Below should be refactored so that balance is shared all of wallet page. Or even broader? Below should be refactored so that balance is shared all of wallet page. Or even broader?
What is the proper React pattern for sharing a global state like balance? What is the proper React pattern for sharing a global state like balance?
@ -288,11 +304,16 @@ var WalletPage = React.createClass({
}, },
render: function() { render: function() {
return ( return (
<main className="page"> <main className="main--single-column">
<WalletNav viewingPage={this.props.viewingPage} />
<section className="card"> <section className="card">
<div className="card__title-primary">
<h3>Balance</h3> <h3>Balance</h3>
</div>
<div className="card__content">
{ this.state.balance === null ? <BusyMessage message="Checking balance" /> : ''} { this.state.balance === null ? <BusyMessage message="Checking balance" /> : ''}
{ this.state.balance !== null ? <CreditAmount amount={this.state.balance} precision={8} /> : '' } { this.state.balance !== null ? <CreditAmount amount={this.state.balance} precision={8} /> : '' }
</div>
</section> </section>
{ this.props.viewingPage === 'wallet' ? <TransactionList /> : '' } { this.props.viewingPage === 'wallet' ? <TransactionList /> : '' }
{ this.props.viewingPage === 'send' ? <SendToAddressSection /> : '' } { this.props.viewingPage === 'send' ? <SendToAddressSection /> : '' }

View file

@ -1,39 +1,136 @@
import React from 'react'; import React from 'react';
import {Icon} from '../component/common.js'; import {Icon, Thumbnail, FilePrice} from '../component/common.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import Modal from '../component/modal.js';
import lbryio from '../lbryio.js';
import rewards from '../rewards.js';
import LoadScreen from '../component/load_screen.js' import LoadScreen from '../component/load_screen.js'
const fs = require('fs'); const fs = require('fs');
const VideoStream = require('videostream'); const VideoStream = require('videostream');
export let WatchLink = React.createClass({
propTypes: {
uri: React.PropTypes.string,
metadata: React.PropTypes.object,
downloadStarted: React.PropTypes.bool,
onGet: React.PropTypes.func,
},
getInitialState: function() {
affirmedPurchase: false
},
play: function() {
lbry.get({uri: this.props.uri}).then((streamInfo) => {
if (streamInfo === null || typeof streamInfo !== 'object') {
this.setState({
modal: 'timedOut',
attemptingDownload: false,
});
}
var WatchPage = React.createClass({ lbryio.call('file', 'view', {
uri: this.props.uri,
outpoint: streamInfo.outpoint,
claimId: streamInfo.claim_id
}).catch(() => {})
});
if (this.props.onGet) {
this.props.onGet()
}
},
onWatchClick: function() {
this.setState({
loading: true
});
lbry.getCostInfo(this.props.uri).then(({cost}) => {
lbry.getBalance((balance) => {
if (cost > balance) {
this.setState({
modal: 'notEnoughCredits',
attemptingDownload: false,
});
} else if (cost <= 0.01) {
this.play()
} else {
lbry.file_list({outpoint: this.props.outpoint}).then((fileInfo) => {
if (fileInfo) { // Already downloaded
this.play();
} else {
this.setState({
modal: 'affirmPurchase'
});
}
});
}
});
});
},
getInitialState: function() {
return {
modal: null,
loading: false,
};
},
closeModal: function() {
this.setState({
loading: false,
modal: null,
});
},
render: function() {
return (<div>
<Link button={ this.props.button ? this.props.button : null }
disabled={this.state.loading}
label={this.props.label ? this.props.label : ""}
className={this.props.className}
icon="icon-play"
onClick={this.onWatchClick} />
<Modal contentLabel="Not enough credits" isOpen={this.state.modal == 'notEnoughCredits'} onConfirmed={this.closeModal}>
You don't have enough LBRY credits to pay for this stream.
</Modal>
<Modal type="confirm" isOpen={this.state.modal == 'affirmPurchase'}
contentLabel="Confirm Purchase" onConfirmed={this.play} onAborted={this.closeModal}>
Are you sure you'd like to buy <strong>{this.props.metadata.title}</strong> for <strong><FilePrice uri={this.props.uri} metadata={this.props.metadata} label={false} look="plain" /></strong> credits?
</Modal>
</div>);
}
});
export let Video = React.createClass({
_isMounted: false, _isMounted: false,
_controlsHideDelay: 3000, // Note: this needs to be shorter than the built-in delay in Electron, or Electron will hide the controls before us _controlsHideDelay: 3000, // Note: this needs to be shorter than the built-in delay in Electron, or Electron will hide the controls before us
_controlsHideTimeout: null, _controlsHideTimeout: null,
_outpoint: null,
propTypes: { propTypes: {
name: React.PropTypes.string, uri: React.PropTypes.string.isRequired,
metadata: React.PropTypes.object,
outpoint: React.PropTypes.string,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
downloadStarted: false, downloadStarted: false,
readyToPlay: false, readyToPlay: false,
loadStatusMessage: "Requesting stream", isPlaying: false,
isPurchased: false,
loadStatusMessage: "Requesting stream... it may sit here for like 15-20 seconds in a really awkward way... we're working on it",
mimeType: null, mimeType: null,
controlsShown: false, controlsShown: false,
}; };
}, },
componentDidMount: function() { onGet: function() {
lbry.get({name: this.props.name}).then((fileInfo) => { lbry.get({uri: this.props.uri}).then((fileInfo) => {
this._outpoint = fileInfo.outpoint;
this.updateLoadStatus(); this.updateLoadStatus();
}); });
this.setState({
isPlaying: true
})
}, },
handleBackClicked: function() { componentDidMount: function() {
history.back(); if (this.props.autoplay) {
this.start()
}
}, },
handleMouseMove: function() { handleMouseMove: function() {
if (this._controlsTimeout) { if (this._controlsTimeout) {
@ -68,7 +165,7 @@ var WatchPage = React.createClass({
}, },
updateLoadStatus: function() { updateLoadStatus: function() {
lbry.file_list({ lbry.file_list({
outpoint: this._outpoint, outpoint: this.props.outpoint,
full_status: true, full_status: true,
}).then(([status]) => { }).then(([status]) => {
if (!status || status.written_bytes == 0) { if (!status || status.written_bytes == 0) {
@ -93,6 +190,9 @@ var WatchPage = React.createClass({
return fs.createReadStream(status.download_path, opts) return fs.createReadStream(status.download_path, opts)
} }
}; };
rewards.claimNextPurchaseReward()
var elem = this.refs.video; var elem = this.refs.video;
var videostream = VideoStream(mediaFile, elem); var videostream = VideoStream(mediaFile, elem);
elem.play(); elem.play();
@ -101,26 +201,15 @@ var WatchPage = React.createClass({
}, },
render: function() { render: function() {
return ( return (
!this.state.readyToPlay <div className={"video " + this.props.className + (this.state.isPlaying && this.state.readyToPlay ? " video--active" : " video--hidden")}>{
? <LoadScreen message={'Loading video...'} details={this.state.loadStatusMessage} /> this.state.isPlaying ?
: <main className="video full-screen" onMouseMove={this.handleMouseMove} onMouseLeave={this.handleMouseLeave}> !this.state.readyToPlay ?
<video controls width="100%" height="100%" id="video" ref="video"></video> <span>this is the world's worst loading screen and we shipped our software with it anyway... <br/><br/>{this.state.loadStatusMessage}</span> :
{this.state.controlsShown <video controls id="video" ref="video"></video> :
? <div className="video__overlay"> <div className="video__cover" style={{backgroundImage: 'url("' + this.props.metadata.thumbnail + '")'}}>
<div className="video__back"> <WatchLink className="video__play-button" uri={this.props.uri} metadata={this.props.metadata} outpoint={this.props.outpoint} onGet={this.onGet} icon="icon-play"></WatchLink>
<Link icon="icon-arrow-circle-o-left" className="video__back-link" onClick={this.handleBackClicked}/>
<div className="video__back-label">
<Icon icon="icon-caret-left" className="video__back-label-arrow" />
<div className="video__back-label-content">
Back to LBRY
</div> </div>
</div> }</div>
</div>
</div>
: null}
</main>
); );
} }
}); })
export default WatchPage;

124
ui/js/rewards.js Normal file
View file

@ -0,0 +1,124 @@
import lbry from './lbry.js';
import lbryio from './lbryio.js';
function rewardMessage(type, amount) {
return {
new_developer: `You earned ${amount} for registering as a new developer.`,
new_user: `You earned ${amount} LBC new user reward.`,
confirm_email: `You earned ${amount} LBC for verifying your email address.`,
new_channel: `You earned ${amount} LBC for creating a publisher identity.`,
first_stream: `You earned ${amount} LBC for streaming your first video.`,
many_downloads: `You earned ${amount} LBC for downloading some of the things.`,
first_publish: `You earned ${amount} LBC for making your first publication.`,
}[type];
}
const rewards = {};
rewards.TYPE_NEW_DEVELOPER = "new_developer",
rewards.TYPE_NEW_USER = "new_user",
rewards.TYPE_CONFIRM_EMAIL = "confirm_email",
rewards.TYPE_FIRST_CHANNEL = "new_channel",
rewards.TYPE_FIRST_STREAM = "first_stream",
rewards.TYPE_MANY_DOWNLOADS = "many_downloads",
rewards.TYPE_FIRST_PUBLISH = "first_publish";
rewards.claimReward = function (type) {
function requestReward(resolve, reject, params) {
if (!lbryio.enabled) {
reject(new Error("Rewards are not enabled."))
return;
}
lbryio.call('reward', 'new', params, 'post').then(({RewardAmount}) => {
const
message = rewardMessage(type, RewardAmount),
result = {
type: type,
amount: RewardAmount,
message: message
};
// Display global notice
document.dispatchEvent(new CustomEvent('globalNotice', {
detail: {
message: message,
linkText: "Show All",
linkTarget: "?rewards",
isError: false,
},
}));
// Add more events here to display other places
resolve(result);
}, reject);
}
return new Promise((resolve, reject) => {
lbry.wallet_new_address().then((address) => {
const params = {
reward_type: type,
wallet_address: address,
};
switch (type) {
case rewards.TYPE_FIRST_CHANNEL:
lbry.claim_list_mine().then(function(claims) {
let claim = claims.find(function(claim) {
return claim.name.length && claim.name[0] == '@' && claim.txid.length
})
if (claim) {
params.transaction_id = claim.txid;
requestReward(resolve, reject, params)
} else {
reject(new Error("Please create a channel identity first."))
}
}).catch(reject)
break;
case rewards.TYPE_FIRST_PUBLISH:
lbry.claim_list_mine().then((claims) => {
let claim = claims.find(function(claim) {
return claim.name.length && claim.name[0] != '@' && claim.txid.length
})
if (claim) {
params.transaction_id = claim.txid
requestReward(resolve, reject, params)
} else {
reject(claims.length ?
new Error("Please publish something and wait for confirmation by the network to claim this reward.") :
new Error("Please publish something to claim this reward."))
}
}).catch(reject)
break;
case rewards.TYPE_FIRST_STREAM:
case rewards.TYPE_NEW_USER:
default:
requestReward(resolve, reject, params);
}
});
});
}
rewards.claimNextPurchaseReward = function() {
let types = {}
types[rewards.TYPE_FIRST_STREAM] = false
types[rewards.TYPE_MANY_DOWNLOADS] = false
lbryio.call('reward', 'list', {}).then((userRewards) => {
userRewards.forEach((reward) => {
if (types[reward.RewardType] === false && reward.TransactionID) {
types[reward.RewardType] = true
}
})
let unclaimedType = Object.keys(types).find((type) => {
return types[type] === false;
})
if (unclaimedType) {
rewards.claimReward(unclaimedType);
}
}, () => { });
}
export default rewards;

View file

@ -1,118 +0,0 @@
const CHANNEL_NAME_MIN_LEN = 4;
const CLAIM_ID_MAX_LEN = 40;
const uri = {};
/**
* Parses a LBRY name into its component parts. Throws errors with user-friendly
* messages for invalid names.
*
* Returns a dictionary with keys:
* - name (string)
* - properName (string; strips off @ for channels)
* - isChannel (boolean)
* - claimSequence (int, if present)
* - bidPosition (int, if present)
* - claimId (string, if present)
* - path (string, if persent)
*/
uri.parseLbryUri = function(lbryUri, requireProto=false) {
// Break into components. Empty sub-matches are converted to null
const componentsRegex = new RegExp(
'^((?:lbry:\/\/)?)' + // protocol
'([^:$#/]*)' + // name (stops at the first separator or end)
'([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end)
'(/?)(.*)' // path separator, path
);
const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(lbryUri).slice(1).map(match => match || null);
// Validate protocol
if (requireProto && !proto) {
throw new Error('LBRY URIs must include a protocol prefix (lbry://).');
}
// Validate and process name
if (!name) {
throw new Error('URI does not include name.');
}
const isChannel = name[0] == '@';
const properName = isChannel ? name.substr(1) : name;
if (isChannel) {
if (!properName) {
throw new Error('No channel name after @.');
}
if (properName.length < CHANNEL_NAME_MIN_LEN) {
throw new Error(`Channel names must be at least ${CHANNEL_NAME_MIN_LEN} characters.`);
}
}
const nameBadChars = properName.match(/[^A-Za-z0-9-]/g);
if (nameBadChars) {
throw new Error(`Invalid character${nameBadChars.length == 1 ? '' : 's'} in name: ${nameBadChars.join(', ')}.`);
}
// Validate and process modifier (claim ID, bid position or claim sequence)
let claimId, claimSequence, bidPosition;
if (modSep) {
if (!modVal) {
throw new Error(`No modifier provided after separator ${modSep}.`);
}
if (modSep == '#') {
claimId = modVal;
} else if (modSep == ':') {
claimSequence = modVal;
} else if (modSep == '$') {
bidPosition = modVal;
}
}
if (claimId && (claimId.length > CLAIM_ID_MAX_LEN || !claimId.match(/^[0-9a-f]+$/))) {
throw new Error(`Invalid claim ID ${claimId}.`);
}
if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]+$/)) {
throw new Error('Bid position must be a number.');
}
if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]+$/)) {
throw new Error('Claim sequence must be a number.');
}
// Validate path
if (path) {
if (!isChannel) {
throw new Error('Only channel URIs may have a path.');
}
const pathBadChars = path.match(/[^A-Za-z0-9-]/g);
if (pathBadChars) {
throw new Error(`Invalid character${count == 1 ? '' : 's'} in path: ${nameBadChars.join(', ')}`);
}
} else if (pathSep) {
throw new Error('No path provided after /');
}
return {
name, properName, isChannel,
... claimSequence ? {claimSequence: parseInt(claimSequence)} : {},
... bidPosition ? {bidPosition: parseInt(bidPosition)} : {},
... claimId ? {claimId} : {},
... path ? {path} : {},
};
}
uri.buildLbryUri = function(uriObj, includeProto=true) {
const {name, claimId, claimSequence, bidPosition, path} = uriObj;
return (includeProto ? 'lbry://' : '') + name +
(claimId ? `#${claimId}` : '') +
(claimSequence ? `:${claimSequence}` : '') +
(bidPosition ? `\$${bidPosition}` : '') +
(path ? `/${path}` : '');
}
export default uri;

View file

@ -2,9 +2,9 @@
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value * Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
* is not set yet. * is not set yet.
*/ */
export function getLocal(key) { export function getLocal(key, fallback=undefined) {
const itemRaw = localStorage.getItem(key); const itemRaw = localStorage.getItem(key);
return itemRaw === null ? undefined : JSON.parse(itemRaw); return itemRaw === null ? fallback : JSON.parse(itemRaw);
} }
/** /**
@ -13,3 +13,19 @@ export function getLocal(key) {
export function setLocal(key, value) { export function setLocal(key, value) {
localStorage.setItem(key, JSON.stringify(value)); localStorage.setItem(key, JSON.stringify(value));
} }
/**
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
* is not set yet.
*/
export function getSession(key, fallback=undefined) {
const itemRaw = sessionStorage.getItem(key);
return itemRaw === null ? fallback : JSON.parse(itemRaw);
}
/**
* Thin wrapper around localStorage.setItem(). Converts value to JSON.
*/
export function setSession(key, value) {
sessionStorage.setItem(key, JSON.stringify(value));
}

View file

@ -1,6 +1,6 @@
{ {
"name": "lbry-web-ui", "name": "lbry-web-ui",
"version": "0.9.2rc15", "version": "0.10.0",
"description": "LBRY UI", "description": "LBRY UI",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
@ -21,7 +21,6 @@
"babel-cli": "^6.11.4", "babel-cli": "^6.11.4",
"babel-preset-es2015": "^6.13.2", "babel-preset-es2015": "^6.13.2",
"babel-preset-react": "^6.11.1", "babel-preset-react": "^6.11.1",
"clamp-js-main": "^0.11.1",
"mediaelement": "^2.23.4", "mediaelement": "^2.23.4",
"node-sass": "^3.8.0", "node-sass": "^3.8.0",
"rc-progress": "^2.0.6", "rc-progress": "^2.0.6",

View file

@ -1,246 +0,0 @@
@import "global";
html
{
height: 100%;
font-size: $font-size;
}
body
{
font-family: 'Source Sans Pro', sans-serif;
line-height: $font-line-height;
}
$drawer-width: 240px;
#drawer
{
width: $drawer-width;
position: fixed;
min-height: 100vh;
left: 0;
top: 0;
background: $color-bg;
z-index: 3;
.drawer-item
{
display: block;
padding: $spacing-vertical / 2;
font-size: 1.2em;
height: $spacing-vertical * 1.5;
.icon
{
margin-right: 6px;
}
.link-label
{
line-height: $spacing-vertical * 1.5;
}
.badge
{
float: right;
background: $color-money;
display: inline-block;
padding: 2px;
color: white;
margin-top: $spacing-vertical * 0.25 - 2;
border-radius: 2px;
}
}
.drawer-item-selected
{
background: $color-canvas;
color: $color-primary;
}
}
#drawer-handle
{
padding: $spacing-vertical / 2;
max-height: $height-header - $spacing-vertical;
text-align: center;
}
#window.drawer-closed
{
#drawer { display: none }
}
#window.drawer-open
{
#main-content { margin-left: $drawer-width; }
.open-drawer-link { display: none }
#header { padding-left: $drawer-width + $spacing-vertical / 2; }
}
#header
{
background: $color-primary;
color: white;
&.header-no-subnav {
height: $height-header;
}
&.header-with-subnav {
height: $height-header * 2;
}
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 2;
box-sizing: border-box;
h1 { font-size: 1.8em; line-height: $height-header - $spacing-vertical; display: inline-block; float: left; }
&.header-scrolled
{
box-shadow: $default-box-shadow;
}
}
.header-top-bar
{
padding: $spacing-vertical / 2;
}
.header-search
{
margin-left: 60px;
text-align: center;
input[type="search"] {
background: rgba(255, 255, 255, 0.3);
color: white;
width: 400px;
@include placeholder-color(#e8e8e8);
}
}
nav.sub-header
{
background: $color-primary;
text-transform: uppercase;
padding: $spacing-vertical / 2;
> a
{
$sub-header-selected-underline-height: 2px;
display: inline-block;
margin: 0 15px;
padding: 0 5px;
line-height: $height-header - $spacing-vertical - $sub-header-selected-underline-height;
color: #e8e8e8;
&:first-child
{
margin-left: 0;
}
&:last-child
{
margin-right: 0;
}
&.sub-header-selected
{
border-bottom: $sub-header-selected-underline-height solid #fff;
color: #fff;
}
&:hover
{
color: #fff;
}
}
}
#main-content
{
background: $color-canvas;
&.no-sub-nav
{
min-height: calc(100vh - 60px); //should be -$height-header, but I'm dumb I guess? It wouldn't work
main { margin-top: $height-header; }
}
&.with-sub-nav
{
min-height: calc(100vh - 120px); //should be -$height-header, but I'm dumb I guess? It wouldn't work
main { margin-top: $height-header * 2; }
}
main
{
padding: $spacing-vertical;
}
h2
{
margin-bottom: $spacing-vertical;
}
h3, h4
{
margin-bottom: $spacing-vertical / 2;
margin-top: $spacing-vertical;
&:first-child
{
margin-top: 0;
}
}
.meta
{
+ h2, + h3, + h4
{
margin-top: 0;
}
}
}
$header-icon-size: 1.5em;
.open-drawer-link, .close-drawer-link
{
display: inline-block;
font-size: $header-icon-size;
padding: 2px 6px 0 6px;
float: left;
}
.close-lbry-link
{
font-size: $header-icon-size;
float: right;
padding: 0 6px 0 18px;
}
.card {
margin-left: auto;
margin-right: auto;
max-width: 800px;
padding: $spacing-vertical;
background: $color-bg;
box-shadow: $default-box-shadow;
border-radius: 2px;
}
.card-obscured
{
position: relative;
}
.card-obscured .card-content {
-webkit-filter: blur($blur-intensity);
-moz-filter: blur($blur-intensity);
-o-filter: blur($blur-intensity);
-ms-filter: blur($blur-intensity);
filter: blur($blur-intensity);
}
.card-overlay {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
padding: 20px;
background-color: rgba(128, 128, 128, 0.8);
color: #fff;
display: flex;
align-items: center;
font-weight: 600;
}
.card-series-submit
{
margin-left: auto;
margin-right: auto;
max-width: 800px;
padding: $spacing-vertical / 2;
}
.full-screen
{
width: 100%;
height: 100%;
}

View file

@ -6,17 +6,20 @@ $padding-button: 12px;
$padding-text-link: 4px; $padding-text-link: 4px;
$color-primary: #155B4A; $color-primary: #155B4A;
$color-primary-light: saturate(lighten($color-primary, 50%), 20%);
$color-light-alt: hsl(hue($color-primary), 15, 85); $color-light-alt: hsl(hue($color-primary), 15, 85);
$color-text-dark: #000; $color-text-dark: #000;
$color-black-transparent: rgba(32,32,32,0.9);
$color-help: rgba(0,0,0,.6); $color-help: rgba(0,0,0,.6);
$color-notice: #921010; $color-notice: #8a6d3b;
$color-warning: #ffffff; $color-error: #a94442;
$color-load-screen-text: #c3c3c3; $color-load-screen-text: #c3c3c3;
$color-canvas: #f5f5f5; $color-canvas: #f5f5f5;
$color-bg: #ffffff; $color-bg: #ffffff;
$color-bg-alt: #D9D9D9; $color-bg-alt: #D9D9D9;
$color-money: #216C2A; $color-money: #216C2A;
$color-meta-light: #505050; $color-meta-light: #505050;
$color-form-border: rgba(160,160,160,.5);
$font-size: 16px; $font-size: 16px;
$font-line-height: 1.3333; $font-line-height: 1.3333;
@ -25,10 +28,16 @@ $mobile-width-threshold: 801px;
$max-content-width: 1000px; $max-content-width: 1000px;
$max-text-width: 660px; $max-text-width: 660px;
$width-page-constrained: 800px;
$height-header: $spacing-vertical * 2.5; $height-header: $spacing-vertical * 2.5;
$height-button: $spacing-vertical * 1.5; $height-button: $spacing-vertical * 1.5;
$height-video-embedded: $width-page-constrained * 9 / 16;
$default-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); $box-shadow-layer: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
$box-shadow-focus: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12);
$transition-standard: .225s ease;
$blur-intensity: 8px; $blur-intensity: 8px;
@ -152,3 +161,34 @@ $blur-intensity: 8px;
height:1px; height:1px;
overflow:hidden; overflow:hidden;
} }
@mixin text-link($color: $color-primary, $hover-opacity: 0.70) {
.icon
{
&:first-child {
padding-right: 5px;
}
&:last-child:not(:only-child) {
padding-left: 5px;
}
}
&:not(.no-underline) {
text-decoration: underline;
.icon {
text-decoration: none;
}
}
&:hover
{
opacity: $hover-opacity;
transition: opacity $transition-standard;
text-decoration: underline;
.icon {
text-decoration: none;
}
}
color: $color;
cursor: pointer;
}

View file

@ -1,87 +0,0 @@
@import "global";
$gutter_fluid: 4;
[class*="span"] {
min-height: 1px;
max-width: 100%;
}
.span12 { width: 100%; }
.span11 { width: 91.666%; }
.span10 { width: 83.333%; }
.span9 { width: 75%; }
.span8 { width: 66.666%; }
.span7 { width: 58.333%; }
.span6 { width: 50%; }
.span5 { width: 41.666%; }
.span4 { width: 33.333%; }
.span3 { width: 25%; }
.span2 { width: 16.666%; }
.span1 { width: 8.333%; }
.row-fluid {
width: 100%;
> [class*="span"] {
float: left;
width: 100%;
margin-left: 1% * $gutter_fluid;
&:first-child
{
margin-left: 0;
}
}
$column_width: (100% - $gutter_fluid * 11) / 12;
> .span12 { width: $column_width * 12 + $gutter_fluid * 11; }
> .span11 { width: $column_width * 11 + $gutter_fluid * 10; }
> .span10 { width: $column_width * 10 + $gutter_fluid * 9; }
> .span9 { width: $column_width * 9 + $gutter_fluid * 8; }
> .span8 { width: $column_width * 8 + $gutter_fluid * 7; }
> .span7 { width: $column_width * 7 + $gutter_fluid * 6; }
> .span6 { width: $column_width * 6 + $gutter_fluid * 5; }
> .span5 { width: $column_width * 5 + $gutter_fluid * 4; }
> .span4 { width: $column_width * 4 + $gutter_fluid * 3; }
> .span3 { width: $column_width * 3 + $gutter_fluid * 2; }
> .span2 { width: $column_width * 2 + $gutter_fluid * 1; }
> .span1 { width: $column_width; }
}
.tile-fluid {
width: 100%;
> [class*="span"] {
float: left;
}
}
.column-fluid {
@include display-flex();
flex-wrap: wrap;
> [class*="span"] {
@include display-flex();
@include flex(1 0 auto);
overflow: hidden;
justify-content: center;
}
}
.row-fluid, .tile-fluid {
@include clearfix();
}
@media (max-width: $mobile-width-threshold) {
.row-fluid, .tile-fluid, .column-fluid {
width: 100%;
}
.pull-left, .pull-right
{
float: none;
}
[class*="span"] {
float: none !important;
width: 100% !important;
margin-left: 0 !important;
display: block !important;
}
}

View file

@ -1,35 +1,51 @@
@import "global"; @import "global";
@mixin text-link($color: $color-primary, $hover-opacity: 0.70) { html
{
height: 100%;
font-size: $font-size;
}
body
{
font-family: 'Source Sans Pro', sans-serif;
line-height: $font-line-height;
}
.icon #window
{
min-height: 100vh;
background: $color-canvas;
}
.badge
{
background: $color-money;
display: inline-block;
padding: 2px;
color: white;
border-radius: 2px;
}
.credit-amount--indicator
{
font-weight: bold;
color: $color-money;
}
#main-content
{
padding: $spacing-vertical;
margin-top: $height-header;
display: flex;
flex-direction: column;
main {
margin-left: auto;
margin-right: auto;
max-width: 100%;
}
main.main--single-column
{ {
&:first-child { width: $width-page-constrained;
padding-right: 5px;
} }
&:last-child:not(:only-child) {
padding-left: 5px;
}
}
&:not(.no-underline) {
text-decoration: underline;
.icon {
text-decoration: none;
}
}
&:hover
{
opacity: $hover-opacity;
transition: opacity .225s ease;
text-decoration: underline;
.icon {
text-decoration: none;
}
}
color: $color;
cursor: pointer;
} }
.icon-fixed-width { .icon-fixed-width {
@ -38,26 +54,6 @@
text-align: center; text-align: center;
} }
section
{
margin-bottom: $spacing-vertical;
&:last-child
{
margin-bottom: 0;
}
&:only-child {
/* If it's an only child, assume it's part of a React layout that will handle the last child condition on its own */
margin-bottom: $spacing-vertical;
}
}
main h1 {
font-size: 2.0em;
margin-bottom: $spacing-vertical;
margin-top: $spacing-vertical*2;
font-family: 'Raleway', sans-serif;
}
h2 { h2 {
font-size: 1.75em; font-size: 1.75em;
} }
@ -76,11 +72,6 @@ sup, sub {
sup { top: -0.4em; } sup { top: -0.4em; }
sub { top: 0.4em; } sub { top: 0.4em; }
label {
cursor: default;
display: block;
}
code { code {
font: 0.8em Consolas, 'Lucida Console', 'Source Sans', monospace; font: 0.8em Consolas, 'Lucida Console', 'Source Sans', monospace;
background-color: #eee; background-color: #eee;
@ -104,25 +95,11 @@ p
opacity: 0.7; opacity: 0.7;
} }
input[type="text"], input[type="search"], textarea
{
@include placeholder {
color: lighten($color-text-dark, 60%);
}
border: 2px solid rgba(160,160,160,.5);
padding-left: 5px;
padding-right: 5px;
box-sizing: border-box;
-webkit-appearance: none;
}
input[type="text"], input[type="search"]
{
line-height: $spacing-vertical - 4;
height: $spacing-vertical * 1.5;
}
.truncated-text { .truncated-text {
display: inline-block; //display: inline-block;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
} }
.busy-indicator .busy-indicator
@ -144,75 +121,6 @@ input[type="text"], input[type="search"]
} }
} }
.button-set-item {
position: relative;
display: inline-block;
+ .button-set-item
{
margin-left: $padding-button;
}
}
.button-block, .faux-button-block
{
display: inline-block;
height: $height-button;
line-height: $height-button;
text-decoration: none;
border: 0 none;
text-align: center;
border-radius: 2px;
text-transform: uppercase;
.icon
{
top: 0em;
}
.icon:first-child
{
padding-right: 5px;
}
.icon:last-child
{
padding-left: 5px;
}
}
.button-block
{
cursor: pointer;
}
.button__content {
margin: 0 $padding-button;
}
.button-primary
{
color: white;
background-color: $color-primary;
box-shadow: $default-box-shadow;
}
.button-alt
{
background-color: $color-bg-alt;
box-shadow: $default-box-shadow;
}
.button-text
{
@include text-link();
display: inline-block;
.button__content {
margin: 0 $padding-text-link;
}
}
.button-text-help
{
@include text-link(#aaa);
font-size: 0.8em;
}
.icon:only-child { .icon:only-child {
position: relative; position: relative;
top: 0.16em; top: 0.16em;
@ -235,169 +143,17 @@ input[type="text"], input[type="search"]
font-style: italic; font-style: italic;
} }
.form-row
{
+ .form-row
{
margin-top: $spacing-vertical / 2;
}
.help
{
margin-top: $spacing-vertical / 2;
}
+ .form-row-submit
{
margin-top: $spacing-vertical;
}
}
.form-field-container {
display: inline-block;
}
.form-field--text {
width: 330px;
}
.form-field--text-number {
width: 50px;
}
.form-field-advice-container {
position: relative;
}
.form-field-advice {
position: absolute;
top: 0px;
left: 0px;
display: flex;
flex-direction: column;
white-space: nowrap;
transition: opacity 400ms ease-in;
}
.form-field-advice--fading {
opacity: 0;
}
.form-field-advice__arrow {
text-align: left;
padding-left: 18px;
font-size: 22px;
line-height: 0.3;
color: darken($color-primary, 5%);
}
.form-field-advice__content-container {
display: inline-block;
}
.form-field-advice__content {
display: inline-block;
padding: 5px;
border-radius: 2px;
background-color: darken($color-primary, 5%);
color: #fff;
}
.form-field-label {
width: 118px;
text-align: right;
vertical-align: top;
display: inline-block;
}
.sort-section { .sort-section {
display: block; display: block;
margin-bottom: 5px; margin-bottom: $spacing-vertical * 2/3;
text-align: right; text-align: right;
line-height: 1;
font-size: 0.85em; font-size: 0.85em;
color: $color-help; color: $color-help;
} }
.modal-overlay { section.section-spaced {
position: fixed; margin-bottom: $spacing-vertical;
display: flex;
justify-content: center;
align-items: center;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: rgba(255, 255, 255, 0.74902);
z-index: 9999;
}
.modal {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
border-radius: 4px;
outline: none;
padding: 36px;
max-width: 250px;
}
.modal__header {
margin-bottom: 5px;
text-align: center;
}
.modal__buttons {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 15px;
}
.modal__button {
margin: 0px 6px;
}
.error-modal-overlay {
background: rgba(#000, .88);
}
.error-modal__content {
display: flex;
padding: 0px 8px 10px 10px;
}
.error-modal__warning-symbol {
margin-top: 6px;
margin-right: 7px;
}
.download-started-modal__file-path {
word-break: break-all;
}
.error-modal {
max-width: none;
width: 400px;
}
.error-modal__error-list { /*shitty hack/temp fix for long errors making modals unusable*/
border: 1px solid #eee;
padding: 8px;
list-style: none;
max-height: 400px;
max-width: 400px;
overflow-y: hidden;
} }

View file

@ -25,12 +25,6 @@
transform: translate(0, 0); transform: translate(0, 0);
} }
.icon-mega
{
font-size: 200px;
line-height: 1;
}
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
readers do not read off random characters that represent icons */ readers do not read off random characters that represent icons */
.icon-glass:before { .icon-glass:before {

View file

@ -3,10 +3,14 @@ body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fiel
margin:0; margin:0;
padding:0; padding:0;
} }
input:focus, textarea:focus :focus
{ {
outline: 0; outline: 0;
} }
input::-webkit-search-cancel-button {
/* Remove default */
-webkit-appearance: none;
}
table table
{ {
border-collapse: collapse; border-collapse: collapse;
@ -30,6 +34,7 @@ input, textarea, select
font-family:inherit; font-family:inherit;
font-size:inherit; font-size:inherit;
font-weight:inherit; font-weight:inherit;
border: 0 none;
} }
img { img {
width: auto\9; width: auto\9;

View file

@ -1,15 +1,24 @@
@import "_reset"; @import "_reset";
@import "_grid";
@import "_icons"; @import "_icons";
@import "_mediaelement"; @import "_mediaelement";
@import "_canvas";
@import "_gui"; @import "_gui";
@import "component/_table"; @import "component/_table";
@import "component/_button.scss";
@import "component/_card.scss";
@import "component/_file-actions.scss"; @import "component/_file-actions.scss";
@import "component/_file-tile.scss"; @import "component/_file-tile.scss";
@import "component/_form-field.scss";
@import "component/_header.scss";
@import "component/_menu.scss"; @import "component/_menu.scss";
@import "component/_tooltip.scss"; @import "component/_tooltip.scss";
@import "component/_load-screen.scss"; @import "component/_load-screen.scss";
@import "component/_channel-indicator.scss"; @import "component/_channel-indicator.scss";
@import "component/_notice.scss";
@import "component/_modal.scss";
@import "component/_modal-page.scss";
@import "component/_snack-bar.scss";
@import "component/_video.scss";
@import "page/_developer.scss"; @import "page/_developer.scss";
@import "page/_watch.scss"; @import "page/_watch.scss";
@import "page/_reward.scss";
@import "page/_show.scss";

View file

@ -0,0 +1,87 @@
@import "../global";
$button-focus-shift: 12%;
.button-set-item {
position: relative;
display: inline-block;
+ .button-set-item
{
margin-left: $padding-button;
}
}
.button-block, .faux-button-block
{
display: inline-block;
height: $height-button;
line-height: $height-button;
text-decoration: none;
border: 0 none;
text-align: center;
border-radius: 2px;
text-transform: uppercase;
.icon
{
top: 0em;
}
.icon:first-child
{
padding-right: 5px;
}
.icon:last-child
{
padding-left: 5px;
}
.icon:only-child
{
padding-left: 0;
padding-right: 0;
}
}
.button-block
{
cursor: pointer;
}
.button__content {
margin: 0 $padding-button;
}
.button-primary
{
$color-button-text: white;
color: darken($color-button-text, $button-focus-shift * 0.5);
background-color: $color-primary;
box-shadow: $box-shadow-layer;
&:focus {
color: $color-button-text;
//box-shadow: $box-shadow-focus;
background-color: mix(black, $color-primary, $button-focus-shift)
}
}
.button-alt
{
background-color: $color-bg-alt;
box-shadow: $box-shadow-layer;
}
.button-text
{
@include text-link();
display: inline-block;
.button__content {
margin: 0 $padding-text-link;
}
}
.button-text-help
{
@include text-link(#aaa);
font-size: 0.8em;
}
.button--flat
{
box-shadow: none !important;
}

View file

@ -0,0 +1,151 @@
@import "../global";
$padding-card-horizontal: $spacing-vertical * 2/3;
.card {
margin-left: auto;
margin-right: auto;
max-width: $width-page-constrained;
background: $color-bg;
box-shadow: $box-shadow-layer;
border-radius: 2px;
margin-bottom: $spacing-vertical * 2/3;
overflow: auto;
}
.card--obscured
{
position: relative;
}
.card--obscured .card__inner {
-webkit-filter: blur($blur-intensity);
-moz-filter: blur($blur-intensity);
-o-filter: blur($blur-intensity);
-ms-filter: blur($blur-intensity);
filter: blur($blur-intensity);
}
.card__title-primary {
padding: 0 $padding-card-horizontal;
margin-top: $spacing-vertical;
}
.card__title-identity {
padding: 0 $padding-card-horizontal;
margin-top: $spacing-vertical * 1/3;
margin-bottom: $spacing-vertical * 1/3;
}
.card__actions {
padding: 0 $padding-card-horizontal;
}
.card__actions {
margin-top: $spacing-vertical * 2/3;
}
.card__actions--bottom {
margin-top: $spacing-vertical * 1/3;
margin-bottom: $spacing-vertical * 1/3;
}
.card__actions--form-submit {
margin-top: $spacing-vertical;
margin-bottom: $spacing-vertical * 2/3;
}
.card__content {
margin-top: $spacing-vertical * 2/3;
margin-bottom: $spacing-vertical * 2/3;
padding: 0 $padding-card-horizontal;
}
.card__subtext {
color: #444;
margin-top: 12px;
font-size: 0.9em;
margin-top: $spacing-vertical * 2/3;
margin-bottom: $spacing-vertical * 2/3;
padding: 0 $padding-card-horizontal;
}
.card__subtext--allow-newlines {
white-space: pre-wrap;
}
.card__subtext--two-lines {
height: $font-size * 0.9 * $font-line-height * 2;
}
.card-overlay {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
padding: 20px;
background-color: rgba(128, 128, 128, 0.8);
color: #fff;
display: flex;
align-items: center;
font-weight: 600;
}
$card-link-scaling: 1.1;
.card__link {
display: block;
}
.card--link:hover {
position: relative;
z-index: 1;
box-shadow: $box-shadow-focus;
transform: scale($card-link-scaling);
transform-origin: 50% 50%;
overflow-x: visible;
overflow-y: visible;
}
.card__media {
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
}
$width-card-small: $spacing-vertical * 12;
$height-card-small: $spacing-vertical * 15;
.card--small {
width: $width-card-small;
overflow-x: hidden;
white-space: normal;
}
.card--small .card__media {
height: $width-card-small * 9 / 16;
}
.card__subtitle {
color: $color-help;
font-size: 0.85em;
line-height: $font-line-height * 1 / 0.85;
}
.card-series-submit
{
margin-left: auto;
margin-right: auto;
max-width: $width-page-constrained;
padding: $spacing-vertical / 2;
}
.card-row {
> .card {
vertical-align: top;
display: inline-block;
margin-right: $spacing-vertical / 3;
}
+ .card-row {
margin-top: $spacing-vertical * 1/3;
}
}
.card-row--small {
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
/*hacky way to give space for hover */
padding-left: 20px;
margin-left: -20px;
padding-right: 20px;
margin-right: -20px;
}
.card-row__header {
margin-bottom: $spacing-vertical / 3;
}

View file

@ -1,5 +1,5 @@
@import "../global"; @import "../global";
.channel-indicator__icon--invalid { .channel-indicator__icon--invalid {
color: #b01c2e; color: $color-error;
} }

View file

@ -1,31 +1,26 @@
@import "../global"; @import "../global";
$height-file-tile: $spacing-vertical * 6;
.file-tile__row { .file-tile__row {
height: $spacing-vertical * 7; overflow: hidden;
} height: $height-file-tile;
.credit-amount {
.file-tile__row--unavailable {
opacity: 0.5;
}
.file-tile__thumbnail {
max-width: 100%;
max-height: $spacing-vertical * 7;
display: block;
margin-left: auto;
margin-right: auto;
}
.file-tile__title {
font-weight: bold;
}
.file-tile__cost {
float: right; float: right;
} }
//also a hack
.file-tile__description { .card__media {
color: #444; height: $height-file-tile;
margin-top: 12px; max-width: $height-file-tile;
font-size: 0.9em; width: $height-file-tile;
margin-right: $spacing-vertical / 2;
float: left;
}
//basically everything here is a hack now
.file-tile__content {
padding-top: $spacing-vertical * 1/3;
margin-left: $height-file-tile + $spacing-vertical / 2;
}
.card__title-primary {
margin-top: 0;
}
} }

View file

@ -0,0 +1,158 @@
@import "../global";
$width-input-border: 2px;
$width-input-text: 330px;
.form-row-submit
{
margin-top: $spacing-vertical;
}
.form-row-submit--with-footer
{
margin-bottom: $spacing-vertical;
}
.form-row__label-row {
margin-top: $spacing-vertical * 5/6;
margin-bottom: $spacing-vertical * 1/6;
line-height: 1;
font-size: 0.9 * $font-size;
}
.form-row__label-row--prefix {
float: left;
margin-right: 5px;
}
input[type="text"].input-copyable {
border: 1px solid $color-form-border;
line-height: 1;
padding-top: $spacing-vertical * 1/3;
padding-bottom: $spacing-vertical * 1/3;
width: $width-input-text;
padding-left: 5px;
padding-right: 5px;
width: 100%;
&:focus { border-color: black; }
}
.form-field {
display: inline-block;
input[type="checkbox"],
input[type="radio"] {
cursor: pointer;
}
select {
transition: outline $transition-standard;
cursor: pointer;
box-sizing: border-box;
padding-left: 5px;
padding-right: 5px;
height: $spacing-vertical;
&:focus {
outline: $width-input-border solid $color-primary;
}
}
textarea,
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="search"],
input[type="date"] {
@include placeholder {
color: lighten($color-text-dark, 60%);
}
transition: all $transition-standard;
cursor: pointer;
padding-left: 1px;
padding-right: 1px;
box-sizing: border-box;
-webkit-appearance: none;
&[readonly] {
background-color: #bbb;
}
}
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="search"],
input[type="date"] {
border-bottom: $width-input-border solid $color-form-border;
line-height: 1;
padding-top: $spacing-vertical * 1/3;
padding-bottom: $spacing-vertical * 1/3;
&.form-field__input--error {
border-color: $color-error;
}
&.form-field__input--inline {
padding-top: 0;
padding-bottom: 0;
border-bottom-width: 1px;
margin-left: 8px;
margin-right: 8px;
}
}
textarea:focus,
input[type="text"]:focus,
input[type="password"]:focus,
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="date"]:focus {
border-color: $color-primary;
}
textarea {
border: $width-input-border solid $color-form-border;
}
}
.form-field__label {
&[for] { cursor: pointer; }
> input[type="checkbox"], input[type="radio"] {
margin-right: 6px;
}
}
.form-field__label--error {
color: $color-error;
}
.form-field__input-text {
width: $width-input-text;
}
.form-field__prefix {
margin-right: 4px;
}
.form-field__postfix {
margin-left: 4px;
}
.form-field__input-number {
width: 70px;
text-align: right;
}
.form-field__input-textarea {
width: $width-input-text;
}
.form-field__error, .form-field__helper {
margin-top: $spacing-vertical * 1/3;
font-size: 0.8em;
transition: opacity $transition-standard;
}
.form-field__error {
color: $color-error;
}
.form-field__helper {
color: $color-help;
}

View file

@ -0,0 +1,94 @@
@import "../global";
$color-header: #666;
$color-header-active: darken($color-header, 20%);
#header
{
color: $color-header;
background: #fff;
display: flex;
position: fixed;
box-shadow: $box-shadow-layer;
top: 0;
left: 0;
width: 100%;
z-index: 2;
padding: $spacing-vertical / 2;
box-sizing: border-box;
}
.header__item {
flex: 0 0 content;
padding-left: $spacing-vertical / 4;
padding-right: $spacing-vertical / 4;
}
.header__item--wunderbar {
flex-grow: 1;
}
.wunderbar
{
position: relative;
.icon {
position: absolute;
left: 10px;
top: $spacing-vertical / 2 - 4px; //hacked
}
}
.wunderbar--active .icon-search { color: $color-primary; }
.wunderbar__input {
background: rgba(255, 255, 255, 0.7);
width: 100%;
color: $color-header;
height: $spacing-vertical * 1.5;
line-height: $spacing-vertical * 1.5;
padding-left: 38px;
padding-right: 5px;
border: 1px solid $color-text-dark;
@include border-radius(2px);
border: 1px solid #ccc;
&:focus {
color: $color-header-active;
box-shadow: $box-shadow-focus;
border-color: $color-primary;
}
}
nav.sub-header
{
text-transform: uppercase;
padding: 0 0 $spacing-vertical;
&.sub-header--constrained {
max-width: $width-page-constrained;
margin-left: auto;
margin-right: auto;
}
> a
{
$sub-header-selected-underline-height: 2px;
display: inline-block;
margin: 0 15px;
padding: 0 5px;
line-height: $height-header - $spacing-vertical - $sub-header-selected-underline-height;
color: $color-header;
&:first-child
{
margin-left: 0;
}
&:last-child
{
margin-right: 0;
}
&.sub-header-selected
{
border-bottom: $sub-header-selected-underline-height solid $color-header-active;
color: $color-header-active;
}
&:hover
{
color: $color-header-active;
}
}
}

View file

@ -23,7 +23,7 @@
} }
.load-screen__details--warning { .load-screen__details--warning {
color: $color-warning; color: white;
} }
.load-screen__cancel-link { .load-screen__cancel-link {

View file

@ -10,7 +10,7 @@ $border-radius-menu: 2px;
position: absolute; position: absolute;
white-space: nowrap; white-space: nowrap;
background-color: white; background-color: white;
box-shadow: $default-box-shadow; box-shadow: $box-shadow-layer;
border-radius: $border-radius-menu; border-radius: $border-radius-menu;
padding-top: ($spacing-vertical / 5) 0px; padding-top: ($spacing-vertical / 5) 0px;
z-index: 1; z-index: 1;

View file

@ -0,0 +1,54 @@
@import "../global";
.modal-page {
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
}
.modal-page--full {
left: 0;
right: 0;
top: 0;
bottom: 0;
.modal-page__content {
max-width: 500px;
}
}
/*
.modal-page {
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
border-radius: 4px;
outline: none;
padding: 36px;
top: 25px;
left: 25px;
right: 25px;
bottom: 25px;
}
*/
.modal-page__content {
h1, h2 {
margin-bottom: $spacing-vertical / 2;
}
h3, h4 {
margin-bottom: $spacing-vertical / 4;
}
}

View file

@ -0,0 +1,81 @@
@import "../global";
.modal-overlay {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: rgba(255, 255, 255, 0.74902);
z-index: 9999;
}
.modal-overlay--clear {
background-color: transparent;
}
.modal {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
border-radius: 4px;
padding: $spacing-vertical;
box-shadow: $box-shadow-layer;
max-width: 400px;
}
.modal__header {
margin-bottom: 5px;
text-align: center;
}
.modal__buttons {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 15px;
}
.modal__button {
margin: 0px 6px;
}
.error-modal-overlay {
background: rgba(#000, .88);
}
.error-modal__content {
display: flex;
padding: 0px 8px 10px 10px;
}
.error-modal__warning-symbol {
margin-top: 6px;
margin-right: 7px;
}
.download-started-modal__file-path {
word-break: break-all;
}
.error-modal {
max-width: none;
width: 400px;
}
.error-modal__error-list { /*shitty hack/temp fix for long errors making modals unusable*/
border: 1px solid #eee;
padding: 8px;
list-style: none;
max-height: 400px;
max-width: 400px;
overflow-y: hidden;
}

View file

@ -0,0 +1,18 @@
@import "../global";
.notice {
padding: 10px 20px;
border: 1px solid #000;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
border-radius: 5px;
color: #468847;
background-color: #dff0d8;
border-color: #d6e9c6;
}
.notice--error {
color: #b94a48;
background-color: #f2dede;
border-color: #eed3d7;
}

View file

@ -0,0 +1,42 @@
@import "../global";
$padding-snack-horizontal: $spacing-vertical;
.snack-bar {
$height-snack: $spacing-vertical * 2;
$padding-snack-vertical: $spacing-vertical / 4;
line-height: $height-snack - $padding-snack-vertical * 2;
padding: $padding-snack-vertical $padding-snack-horizontal;
position: fixed;
top: $spacing-vertical;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
min-width: 300px;
max-width: 500px;
background: $color-black-transparent;
color: #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 2px;
transition: all $transition-standard;
z-index: 10000; /*hack to get it over react modal */
}
.snack-bar__action {
display: inline-block;
text-transform: uppercase;
color: $color-primary-light;
margin: 0px 0px 0px $padding-snack-horizontal;
min-width: min-content;
&:hover {
text-decoration: underline;
}
}

View file

@ -15,6 +15,7 @@
z-index: 1; z-index: 1;
left: 50%; left: 50%;
margin-left: $tooltip-body-width * -1 / 2; margin-left: $tooltip-body-width * -1 / 2;
white-space: normal;
box-sizing: border-box; box-sizing: border-box;
padding: $spacing-vertical / 2; padding: $spacing-vertical / 2;
@ -24,7 +25,7 @@
background-color: $color-bg; background-color: $color-bg;
font-size: $font-size * 7/8; font-size: $font-size * 7/8;
line-height: $font-line-height; line-height: $font-line-height;
box-shadow: $default-box-shadow; box-shadow: $box-shadow-layer;
} }
.tooltip--header .tooltip__link { .tooltip--header .tooltip__link {

View file

@ -0,0 +1,56 @@
video {
object-fit: contain;
box-sizing: border-box;
max-height: 100%;
max-width: 100%;
}
.video {
background: #000;
color: white;
}
.video-embedded {
max-width: $width-page-constrained;
max-height: $height-video-embedded;
height: $height-video-embedded;
video {
height: 100%;
}
&.video--hidden {
height: $height-video-embedded;
}
&.video--active {
/*background: none;*/
}
}
.video__cover {
text-align: center;
height: 100%;
width: 100%;
background-size: auto 100%;
background-position: center center;
background-repeat: no-repeat;
position: relative;
.video__play-button { @include absolute-center(); }
}
.video__play-button {
position: absolute;
width: 100%;
height: 100%;
cursor: pointer;
display: none;
font-size: $spacing-vertical * 3;
color: white;
z-index: 1;
background: $color-black-transparent;
opacity: 0.6;
left: 0;
top: 0;
&:hover {
opacity: 1;
transition: opacity $transition-standard;
}
}

View file

@ -0,0 +1,5 @@
@import "../global";
.reward-page__details {
background-color: lighten($color-canvas, 1.5%);
}

9
ui/scss/page/_show.scss Normal file
View file

@ -0,0 +1,9 @@
@import "../global";
.show-page-media {
text-align: center;
margin-bottom: $spacing-vertical;
img {
max-width: 100%;
}
}

View file

@ -1,6 +1,3 @@
.video {
background: #000;
}
.video__overlay { .video__overlay {
position: absolute; position: absolute;
@ -23,7 +20,7 @@
} }
.video__back-label { .video__back-label {
opacity: 0; opacity: 0.5;
transition: opacity 100ms ease-in; transition: opacity 100ms ease-in;
} }