Merge pull request #127 from lbryio/redux

Redux
This commit is contained in:
Jeremy Kauffman 2017-05-21 17:13:42 -04:00 committed by GitHub
commit a95ad63c7e
151 changed files with 6648 additions and 3833 deletions

2
.gitignore vendored
View file

@ -14,3 +14,5 @@ dist
*.pyc
.#*
build/daemon.zip

View file

@ -8,16 +8,19 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased]
### Added
*
*
* Entire app re-written to use Redux as state store. Far saner and faster. Will also increase productivity moving forward.
* Channel page shows content published in channel.
* URI handling. Clicking lbry:// links should open the app and appropriate URI on all OSes.
* File cards have an icon indicating you posses that file.
* Download directory setting now uses a proper dialog.
* Movie player automatically shows if the file has already been downloaded.
### Changed
*
*
* Plyr replaces mediaelement as the movie player.
### Fixed
* Error modals now display full screen properly
*
* Publisher indicator on show pages and file cards/tiles will now always show the proper channel name.
* Performance improvements related to avoiding duplicate fetches.
### Deprecated
*

View file

@ -1,5 +1,11 @@
const {app, BrowserWindow, ipcMain} = require('electron');
const url = require('url');
const isDebug = process.env.NODE_ENV === 'development'
if (isDebug) {
require('electron-debug')({showDevTools: true});
}
const path = require('path');
const jayson = require('jayson');
const semver = require('semver');
@ -80,7 +86,7 @@ function checkForNewVersion(callback) {
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});
win.webContents.send('version-info-received', {remoteVersion, localVersion, upgradeAvailable});
}
}
})
@ -89,7 +95,7 @@ function checkForNewVersion(callback) {
req.on('error', (err) => {
console.log('Failed to get current version from GitHub. Error:', err);
if (win) {
win.webContents.send('version-info-received', null);
win.webContents.send('version-info-received', null);
}
});
}
@ -138,7 +144,9 @@ function createWindow () {
win = new BrowserWindow({backgroundColor: '#155B4A', minWidth: 800, minHeight: 600 }) //$color-primary
win.maximize()
// win.webContents.openDevTools();
if (isDebug) {
win.webContents.openDevTools();
}
win.loadURL(`file://${__dirname}/dist/index.html`)
if (openUri) { // We stored and received a URI that an external app requested before we had a window object
win.webContents.on('did-finish-load', () => {

1
lbry

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

@ -1 +0,0 @@
Subproject commit 5c2441fa13e39ba7280292519041e14ec696d753

1
lbryum

@ -1 +0,0 @@
Subproject commit 950b95aa7e45a2c15b269d807f6ff8e16bae4304

View file

@ -53,7 +53,7 @@
},
"devDependencies": {
"electron": "^1.4.15",
"electron-builder": "^11.7.0"
},
"dependencies": {}
"electron-builder": "^11.7.0",
"electron-debug": "^1.1.0"
}
}

2
ui/dist/index.html vendored
View file

@ -7,7 +7,7 @@
<link href='https://fonts.googleapis.com/css?family=Raleway:600,300' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600italic,600' rel='stylesheet' type='text/css'>
<link href="./css/all.css" rel="stylesheet" type="text/css" media="screen,print" />
<link href="./js/mediaelement/mediaelementplayer.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="https://cdn.plyr.io/2.0.12/plyr.css">
<link rel="icon" type="image/png" href="./img/fav/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="./img/fav/favicon-194x194.png" sizes="194x194">
<link rel="icon" type="image/png" href="./img/fav/favicon-96x96.png" sizes="96x96">

212
ui/js/actions/app.js Normal file
View file

@ -0,0 +1,212 @@
import * as types from 'constants/action_types'
import lbry from 'lbry'
import {
selectUpdateUrl,
selectUpgradeDownloadDir,
selectUpgradeDownloadItem,
selectUpgradeFilename,
selectPageTitle,
} from 'selectors/app'
const {remote, ipcRenderer, shell} = require('electron');
const path = require('path');
const app = require('electron').remote.app;
const {download} = remote.require('electron-dl');
const fs = remote.require('fs');
const queryStringFromParams = (params) => {
return Object
.keys(params)
.map(key => `${key}=${params[key]}`)
.join('&')
}
export function doNavigate(path, params = {}) {
return function(dispatch, getState) {
let url = path
if (params)
url = `${url}?${queryStringFromParams(params)}`
dispatch(doChangePath(url))
const state = getState()
const pageTitle = selectPageTitle(state)
dispatch(doHistoryPush(params, pageTitle, url))
}
}
export function doChangePath(path) {
return function(dispatch, getState) {
dispatch({
type: types.CHANGE_PATH,
data: {
path,
}
})
}
}
export function doHistoryBack() {
return function(dispatch, getState) {
history.back()
}
}
export function doHistoryPush(params, title, relativeUrl) {
return function(dispatch, getState) {
let pathParts = window.location.pathname.split('/')
pathParts[pathParts.length - 1] = relativeUrl.replace(/^\//, '')
const url = pathParts.join('/')
title += " - LBRY"
history.pushState(params, title, url)
window.document.title = title
}
}
export function doOpenModal(modal) {
return {
type: types.OPEN_MODAL,
data: {
modal
}
}
}
export function doCloseModal() {
return {
type: types.CLOSE_MODAL,
}
}
export function doUpdateDownloadProgress(percent) {
return {
type: types.UPGRADE_DOWNLOAD_PROGRESSED,
data: {
percent: percent
}
}
}
export function doSkipUpgrade() {
return {
type: types.SKIP_UPGRADE
}
}
export function doStartUpgrade() {
return function(dispatch, getState) {
const state = getState()
const upgradeDownloadPath = selectUpgradeDownloadDir(state)
ipcRenderer.send('upgrade', upgradeDownloadPath)
}
}
export function doDownloadUpgrade() {
return function(dispatch, getState) {
const state = getState()
// Make a new directory within temp directory so the filename is guaranteed to be available
const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep);
const upgradeFilename = selectUpgradeFilename(state)
let options = {
onProgress: (p) => dispatch(doUpdateDownloadProgress(Math.round(p * 100))),
directory: dir,
};
download(remote.getCurrentWindow(), selectUpdateUrl(state), options)
.then(downloadItem => {
/**
* TODO: get the download path directly from the download object. It should just be
* downloadItem.getSavePath(), but the copy on the main process is being garbage collected
* too soon.
*/
const _upgradeDownloadItem = downloadItem;
const _upgradeDownloadPath = path.join(dir, upgradeFilename);
dispatch({
type: types.UPGRADE_DOWNLOAD_COMPLETED,
data: {
dir,
downloadItem
}
})
});
dispatch({
type: types.UPGRADE_DOWNLOAD_STARTED
})
dispatch({
type: types.OPEN_MODAL,
data: {
modal: 'downloading'
}
})
}
}
export function doCancelUpgrade() {
return function(dispatch, getState) {
const state = getState()
const upgradeDownloadItem = selectUpgradeDownloadItem(state)
if (upgradeDownloadItem) {
/*
* Right now the remote reference to the download item gets garbage collected as soon as the
* the download is over (maybe even earlier), so trying to cancel a finished download may
* throw an error.
*/
try {
upgradeDownloadItem.cancel();
} catch (err) {
console.error(err)
// Do nothing
}
}
dispatch({ type: types.UPGRADE_CANCELLED })
}
}
export function doCheckUpgradeAvailable() {
return function(dispatch, getState) {
const state = getState()
lbry.getVersionInfo().then(({remoteVersion, upgradeAvailable}) => {
if (upgradeAvailable) {
dispatch({
type: types.UPDATE_VERSION,
data: {
version: remoteVersion,
}
})
dispatch({
type: types.OPEN_MODAL,
data: {
modal: 'upgrade'
}
})
}
});
}
}
export function doAlertError(errorList) {
return function(dispatch, getState) {
const state = getState()
dispatch({
type: types.OPEN_MODAL,
data: {
modal: 'error',
error: errorList
}
})
}
}
export function doDaemonReady() {
return {
type: types.DAEMON_READY
}
}

View file

@ -0,0 +1,29 @@
import * as types from 'constants/action_types'
import lbry from 'lbry'
import {
selectFetchingAvailability
} from 'selectors/availability'
export function doFetchAvailability(uri) {
return function(dispatch, getState) {
const state = getState()
const alreadyFetching = !!selectFetchingAvailability(state)[uri]
if (!alreadyFetching) {
dispatch({
type: types.FETCH_AVAILABILITY_STARTED,
data: {uri}
})
lbry.get_availability({uri}).then((availability) => {
dispatch({
type: types.FETCH_AVAILABILITY_COMPLETED,
data: {
availability,
uri,
}
})
})
}
}
}

291
ui/js/actions/content.js Normal file
View file

@ -0,0 +1,291 @@
import * as types from 'constants/action_types'
import lbry from 'lbry'
import lbryio from 'lbryio'
import lbryuri from 'lbryuri'
import rewards from 'rewards'
import {
selectBalance,
} from 'selectors/wallet'
import {
selectFileInfoForUri,
selectUrisDownloading,
} from 'selectors/file_info'
import {
selectResolvingUris
} from 'selectors/content'
import {
selectCostInfoForUri,
} from 'selectors/cost_info'
import {
selectClaimsByUri,
} from 'selectors/claims'
import {
doOpenModal,
} from 'actions/app'
export function doResolveUri(uri) {
return function(dispatch, getState) {
uri = lbryuri.normalize(uri)
const state = getState()
const alreadyResolving = selectResolvingUris(state).indexOf(uri) !== -1
if (!alreadyResolving) {
dispatch({
type: types.RESOLVE_URI_STARTED,
data: { uri }
})
lbry.resolve({ uri }).then((resolutionInfo) => {
const {
claim,
certificate,
} = resolutionInfo ? resolutionInfo : { claim : null, certificate: null }
dispatch({
type: types.RESOLVE_URI_COMPLETED,
data: {
uri,
claim,
certificate,
}
})
})
}
}
}
export function doCancelResolveUri(uri) {
return function(dispatch, getState) {
lbry.cancelResolve({ uri })
dispatch({
type: types.RESOLVE_URI_CANCELED,
data: { uri }
})
}
}
export function doFetchFeaturedUris() {
return function(dispatch, getState) {
const state = getState()
dispatch({
type: types.FETCH_FEATURED_CONTENT_STARTED,
})
const success = ({ Categories, Uris }) => {
let featuredUris = {}
Categories.forEach((category) => {
if (Uris[category] && Uris[category].length) {
featuredUris[category] = Uris[category]
}
})
dispatch({
type: types.FETCH_FEATURED_CONTENT_COMPLETED,
data: {
categories: Categories,
uris: featuredUris,
}
})
}
const failure = () => {
dispatch({
type: types.FETCH_FEATURED_CONTENT_COMPLETED,
data: {
categories: [],
uris: {}
}
})
}
lbryio.call('discover', 'list', { version: "early-access" } )
.then(success, failure)
}
}
export function doUpdateLoadStatus(uri, outpoint) {
return function(dispatch, getState) {
const state = getState()
lbry.file_list({
outpoint: outpoint,
full_status: true,
}).then(([fileInfo]) => {
if(!fileInfo || fileInfo.written_bytes == 0) {
// download hasn't started yet
setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)) }, 250)
} else if (fileInfo.completed) {
// TODO this isn't going to get called if they reload the client before
// the download finished
rewards.claimNextPurchaseReward()
dispatch({
type: types.DOWNLOADING_COMPLETED,
data: {
uri,
outpoint,
fileInfo,
}
})
} else {
// ready to play
const {
total_bytes,
written_bytes,
} = fileInfo
const progress = (written_bytes / total_bytes) * 100
dispatch({
type: types.DOWNLOADING_PROGRESSED,
data: {
uri,
outpoint,
fileInfo,
progress,
}
})
setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)) }, 250)
}
})
}
}
export function doDownloadFile(uri, streamInfo) {
return function(dispatch, getState) {
const state = getState()
lbry.file_list({ outpoint: streamInfo.outpoint, full_status: true }).then(([fileInfo]) => {
dispatch({
type: types.DOWNLOADING_STARTED,
data: {
uri,
outpoint: streamInfo.outpoint,
fileInfo,
}
})
})
lbryio.call('file', 'view', {
uri: uri,
outpoint: streamInfo.outpoint,
claimId: streamInfo.claim_id,
}).catch(() => {})
dispatch(doUpdateLoadStatus(uri, streamInfo.outpoint))
}
}
export function doLoadVideo(uri) {
return function(dispatch, getState) {
const state = getState()
dispatch({
type: types.LOADING_VIDEO_STARTED,
data: {
uri
}
})
lbry.get({ uri }).then(streamInfo => {
const timeout = streamInfo === null ||
typeof streamInfo !== 'object' ||
streamInfo.error == 'Timeout'
if(timeout) {
dispatch({
type: types.LOADING_VIDEO_FAILED,
data: { uri }
})
dispatch(doOpenModal('timedOut'))
} else {
dispatch(doDownloadFile(uri, streamInfo))
}
})
}
}
export function doPurchaseUri(uri) {
return function(dispatch, getState) {
const state = getState()
const balance = selectBalance(state)
const fileInfo = selectFileInfoForUri(state, { uri })
const costInfo = selectCostInfoForUri(state, { uri })
const downloadingByUri = selectUrisDownloading(state)
const alreadyDownloading = !!downloadingByUri[uri]
const { cost } = costInfo
// BUG if you delete a file from the file system system you're going to be
// asked to pay for it again. We need to check if the file is in the blobs
// here and then dispatch doLoadVideo() which will reconstruct it again from
// the blobs. Or perhaps there's another way to see if a file was already
// purchased?
// we already fully downloaded the file
if (fileInfo && fileInfo.completed) {
return Promise.resolve()
}
// we are already downloading the file
if (alreadyDownloading) {
return Promise.resolve()
}
// the file is free or we have partially downloaded it
if (cost <= 0.01 || (fileInfo && fileInfo.download_directory)) {
dispatch(doLoadVideo(uri))
return Promise.resolve()
}
if (cost > balance) {
dispatch(doOpenModal('notEnoughCredits'))
} else {
dispatch(doOpenModal('affirmPurchase'))
}
return Promise.resolve()
}
}
export function doFetchClaimsByChannel(uri) {
return function(dispatch, getState) {
dispatch({
type: types.FETCH_CHANNEL_CLAIMS_STARTED,
data: { uri }
})
lbry.resolve({ uri }).then((resolutionInfo) => {
const {
claims_in_channel,
} = resolutionInfo ? resolutionInfo : { claims_in_channel: [] }
dispatch({
type: types.FETCH_CHANNEL_CLAIMS_COMPLETED,
data: {
uri,
claims: claims_in_channel
}
})
})
}
}
export function doClaimListMine() {
return function(dispatch, getState) {
dispatch({
type: types.CLAIM_LIST_MINE_STARTED
})
lbry.claim_list_mine().then((claims) => {
dispatch({
type: types.CLAIM_LIST_MINE_COMPLETED,
data: {
claims
}
})
})
}
}

View file

@ -0,0 +1,77 @@
import * as types from 'constants/action_types'
import lbry from 'lbry'
import lbryio from 'lbryio'
import {
doResolveUri
} from 'actions/content'
import {
selectResolvingUris,
} from 'selectors/content'
import {
selectClaimsByUri
} from 'selectors/claims'
import {
selectSettingsIsGenerous
} from 'selectors/settings'
export function doFetchCostInfoForUri(uri) {
return function(dispatch, getState) {
const state = getState(),
claim = selectClaimsByUri(state)[uri],
isResolving = selectResolvingUris(state).indexOf(uri) !== -1,
isGenerous = selectSettingsIsGenerous(state)
if (claim === null) { //claim doesn't exist, nothing to fetch a cost for
return
}
if (!claim) {
setTimeout(() => {
dispatch(doFetchCostInfoForUri(uri))
}, 1000)
if (!isResolving) {
dispatch(doResolveUri(uri))
}
return
}
function begin() {
dispatch({
type: types.FETCH_COST_INFO_STARTED,
data: {
uri,
}
})
}
function resolve(costInfo) {
dispatch({
type: types.FETCH_COST_INFO_COMPLETED,
data: {
uri,
costInfo,
}
})
}
if (isGenerous && claim) {
let cost
const fee = claim.value.stream.metadata.fee;
if (fee === undefined ) {
resolve({ cost: 0, includesData: true })
} else if (fee.currency == 'LBC') {
resolve({ cost: fee.amount, includesData: true })
} else {
begin()
lbryio.getExchangeRates().then(({lbc_usd}) => {
resolve({ cost: fee.amount / lbc_usd, includesData: true })
});
}
} else {
begin()
lbry.getCostInfo(uri).then(resolve)
}
}
}

121
ui/js/actions/file_info.js Normal file
View file

@ -0,0 +1,121 @@
import * as types from 'constants/action_types'
import lbry from 'lbry'
import {
doClaimListMine
} from 'actions/content'
import {
selectClaimsByUri,
selectClaimListMineIsPending,
} from 'selectors/claims'
import {
selectFileListIsPending,
selectAllFileInfos,
selectUrisLoading,
} from 'selectors/file_info'
import {
doCloseModal,
} from 'actions/app'
const {
shell,
} = require('electron')
export function doFetchFileInfo(uri) {
return function(dispatch, getState) {
const state = getState()
const claim = selectClaimsByUri(state)[uri]
const outpoint = claim ? `${claim.txid}:${claim.nout}` : null
const alreadyFetching = !!selectUrisLoading(state)[uri]
if (!alreadyFetching) {
dispatch({
type: types.FETCH_FILE_INFO_STARTED,
data: {
outpoint,
}
})
lbry.file_list({outpoint: outpoint, full_status: true}).then(fileInfos => {
dispatch({
type: types.FETCH_FILE_INFO_COMPLETED,
data: {
outpoint,
fileInfo: fileInfos && fileInfos.length ? fileInfos[0] : null,
}
})
})
}
}
}
export function doFileList() {
return function(dispatch, getState) {
const state = getState()
const isPending = selectFileListIsPending(state)
if (!isPending) {
dispatch({
type: types.FILE_LIST_STARTED,
})
lbry.file_list().then((fileInfos) => {
dispatch({
type: types.FILE_LIST_COMPLETED,
data: {
fileInfos,
}
})
})
}
}
}
export function doOpenFileInShell(fileInfo) {
return function(dispatch, getState) {
shell.openItem(fileInfo.download_path)
}
}
export function doOpenFileInFolder(fileInfo) {
return function(dispatch, getState) {
shell.showItemInFolder(fileInfo.download_path)
}
}
export function doDeleteFile(outpoint, deleteFromComputer) {
return function(dispatch, getState) {
dispatch({
type: types.FILE_DELETE,
data: {
outpoint
}
})
lbry.file_delete({
outpoint: outpoint,
delete_target_file: deleteFromComputer,
})
dispatch(doCloseModal())
}
}
export function doFetchFileInfosAndPublishedClaims() {
return function(dispatch, getState) {
const state = getState(),
isClaimListMinePending = selectClaimListMineIsPending(state),
isFileInfoListPending = selectFileListIsPending(state)
if (isClaimListMinePending === undefined) {
dispatch(doClaimListMine())
}
if (isFileInfoListPending === undefined) {
dispatch(doFileList())
}
}
}

36
ui/js/actions/rewards.js Normal file
View file

@ -0,0 +1,36 @@
import * as types from 'constants/action_types'
import lbry from 'lbry'
import lbryio from 'lbryio';
import rewards from 'rewards'
export function doFetchRewards() {
return function(dispatch, getState) {
const state = getState()
dispatch({
type: types.FETCH_REWARDS_STARTED,
})
lbryio.call('reward', 'list', {}).then(function(userRewards) {
dispatch({
type: types.FETCH_REWARDS_COMPLETED,
data: { userRewards }
})
});
}
}
export function doClaimReward(rewardType) {
return function(dispatch, getState) {
try {
rewards.claimReward(rewards[rewardType])
dispatch({
type: types.REWARD_CLAIMED,
data: {
reward: rewards[rewardType]
}
})
} catch(err) {
}
}
}

56
ui/js/actions/search.js Normal file
View file

@ -0,0 +1,56 @@
import * as types from 'constants/action_types'
import lbryuri from 'lbryuri'
import lighthouse from 'lighthouse'
import {
doResolveUri,
} from 'actions/content'
import {
doNavigate,
doHistoryPush
} from 'actions/app'
import {
selectCurrentPage,
} from 'selectors/app'
export function doSearch(query) {
return function(dispatch, getState) {
const state = getState()
const page = selectCurrentPage(state)
if (!query) {
return dispatch({
type: types.SEARCH_CANCELLED,
})
}
dispatch({
type: types.SEARCH_STARTED,
data: { query }
})
if(page != 'search') {
dispatch(doNavigate('search', { query: query }))
} else {
dispatch(doHistoryPush({ query }, "Search for " + query, '/search'))
}
lighthouse.search(query).then(results => {
results.forEach(result => {
const uri = lbryuri.build({
channelName: result.channel_name,
contentName: result.name,
claimId: result.channel_id || result.claim_id,
})
dispatch(doResolveUri(uri))
})
dispatch({
type: types.SEARCH_COMPLETED,
data: {
query,
results,
}
})
})
}
}

31
ui/js/actions/settings.js Normal file
View file

@ -0,0 +1,31 @@
import * as types from 'constants/action_types'
import lbry from 'lbry'
export function doFetchDaemonSettings() {
return function(dispatch, getState) {
lbry.get_settings().then((settings) => {
dispatch({
type: types.DAEMON_SETTINGS_RECEIVED,
data: {
settings
}
})
})
}
}
export function doSetDaemonSetting(key, value) {
return function(dispatch, getState) {
let settings = {};
settings[key] = value;
lbry.settings_set(settings).then(settings)
lbry.get_settings().then((settings) => {
dispatch({
type: types.DAEMON_SETTINGS_RECEIVED,
data: {
settings
}
})
})
}
}

125
ui/js/actions/wallet.js Normal file
View file

@ -0,0 +1,125 @@
import * as types from 'constants/action_types'
import lbry from 'lbry'
import {
selectDraftTransaction,
selectDraftTransactionAmount,
selectBalance,
} from 'selectors/wallet'
import {
doOpenModal,
} from 'actions/app'
export function doUpdateBalance(balance) {
return {
type: types.UPDATE_BALANCE,
data: {
balance: balance
}
}
}
export function doFetchTransactions() {
return function(dispatch, getState) {
dispatch({
type: types.FETCH_TRANSACTIONS_STARTED
})
lbry.call('get_transaction_history', {}, (results) => {
dispatch({
type: types.FETCH_TRANSACTIONS_COMPLETED,
data: {
transactions: results
}
})
})
}
}
export function doGetNewAddress() {
return function(dispatch, getState) {
dispatch({
type: types.GET_NEW_ADDRESS_STARTED
})
lbry.wallet_new_address().then(function(address) {
localStorage.setItem('wallet_address', address);
dispatch({
type: types.GET_NEW_ADDRESS_COMPLETED,
data: { address }
})
})
}
}
export function doCheckAddressIsMine(address) {
return function(dispatch, getState) {
dispatch({
type: types.CHECK_ADDRESS_IS_MINE_STARTED
})
lbry.checkAddressIsMine(address, (isMine) => {
if (!isMine) dispatch(doGetNewAddress())
dispatch({
type: types.CHECK_ADDRESS_IS_MINE_COMPLETED
})
})
}
}
export function doSendDraftTransaction() {
return function(dispatch, getState) {
const state = getState()
const draftTx = selectDraftTransaction(state)
const balance = selectBalance(state)
const amount = selectDraftTransactionAmount(state)
if (balance - amount < 1) {
return dispatch(doOpenModal('insufficientBalance'))
}
dispatch({
type: types.SEND_TRANSACTION_STARTED,
})
const successCallback = (results) => {
if(results === true) {
dispatch({
type: types.SEND_TRANSACTION_COMPLETED,
})
dispatch(doOpenModal('transactionSuccessful'))
}
else {
dispatch({
type: types.SEND_TRANSACTION_FAILED,
data: { error: results }
})
dispatch(doOpenModal('transactionFailed'))
}
}
const errorCallback = (error) => {
dispatch({
type: types.SEND_TRANSACTION_FAILED,
data: { error: error.message }
})
dispatch(doOpenModal('transactionFailed'))
}
lbry.sendToAddress(draftTx.amount, draftTx.address, successCallback, errorCallback);
}
}
export function doSetDraftTransactionAmount(amount) {
return {
type: types.SET_DRAFT_TRANSACTION_AMOUNT,
data: { amount }
}
}
export function doSetDraftTransactionAddress(address) {
return {
type: types.SET_DRAFT_TRANSACTION_ADDRESS,
data: { address }
}
}

View file

@ -1,335 +1,18 @@
import React from 'react';
import {Line} from 'rc-progress';
import store from 'store.js';
import lbry from './lbry.js';
import SettingsPage from './page/settings.js';
import HelpPage from './page/help.js';
import WatchPage from './page/watch.js';
import ReportPage from './page/report.js';
import StartPage from './page/start.js';
import RewardsPage from './page/rewards.js';
import RewardPage from './page/reward.js';
import WalletPage from './page/wallet.js';
import ShowPage from './page/show.js';
import PublishPage from './page/publish.js';
import SearchPage from './page/search.js';
import DiscoverPage from './page/discover.js';
import DeveloperPage from './page/developer.js';
import lbryuri from './lbryuri.js';
import {FileListDownloaded, FileListPublished} from './page/file-list.js';
import Header from './component/header.js';
import {Modal, ExpandableModal} from './component/modal.js';
import {Link} from './component/link.js';
const {remote, ipcRenderer, shell} = require('electron');
const {download} = remote.require('electron-dl');
const path = require('path');
const app = require('electron').remote.app;
const fs = remote.require('fs');
var App = React.createClass({
_error_key_labels: {
connectionString: 'API connection string',
method: 'Method',
params: 'Parameters',
code: 'Error code',
message: 'Error message',
data: 'Error data',
},
_fullScreenPages: ['watch'],
_storeHistoryOfNextRender: false,
_upgradeDownloadItem: null,
_isMounted: false,
_version: null,
getUpdateUrl: function() {
switch (process.platform) {
case 'darwin':
return 'https://lbry.io/get/lbry.dmg';
case 'linux':
return 'https://lbry.io/get/lbry.deb';
case 'win32':
return 'https://lbry.io/get/lbry.exe';
default:
throw 'Unknown platform';
}
},
// Hard code the filenames as a temporary workaround, because
// electron-dl throws errors when you try to get the filename
getUpgradeFilename: function() {
switch (process.platform) {
case 'darwin':
return `LBRY-${this._version}.dmg`;
case 'linux':
return `LBRY_${this._version}_amd64.deb`;
case 'windows':
return `LBRY.Setup.${this._version}.exe`;
default:
throw 'Unknown platform';
}
},
getViewingPageAndArgs: function(address) {
// For now, routes are in format ?page or ?page=args
let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/);
return {
viewingPage: viewingPage,
pageArgs: pageArgs === undefined ? null : decodeURIComponent(pageArgs)
};
},
getInitialState: function() {
return Object.assign(this.getViewingPageAndArgs(window.location.search), {
viewingPage: 'discover',
appUrl: null,
errorInfo: null,
modal: null,
downloadProgress: null,
downloadComplete: false,
});
},
componentWillMount: function() {
if ('openUri' in this.props) { // A URI was requested by an external app
this.showUri(this.props.openUri);
}
window.addEventListener("popstate", this.onHistoryPop);
document.addEventListener('unhandledError', (event) => {
this.alertError(event.detail);
});
ipcRenderer.on('open-uri-requested', (event, uri) => {
this.showUri(uri);
});
//open links in external browser and skip full redraw on changing page
document.addEventListener('click', (event) => {
var target = event.target;
while (target && target !== document) {
if (target.matches('a[href^="http"]')) {
event.preventDefault();
shell.openExternal(target.href);
return;
}
if (target.matches('a[href^="?"]')) {
event.preventDefault();
if (this._isMounted) {
let appUrl = target.getAttribute('href');
this._storeHistoryOfNextRender = true;
this.setState(Object.assign({}, this.getViewingPageAndArgs(appUrl), { appUrl: appUrl }));
document.body.scrollTop = 0;
}
}
target = target.parentNode;
}
});
if (!sessionStorage.getItem('upgradeSkipped')) {
lbry.getVersionInfo().then(({remoteVersion, upgradeAvailable}) => {
if (upgradeAvailable) {
this._version = remoteVersion;
this.setState({
modal: 'upgrade',
});
}
});
}
},
closeModal: function() {
this.setState({
modal: null,
});
},
componentDidMount: function() {
this._isMounted = true;
},
componentWillReceiveProps: function(nextProps) {
if ('openUri' in nextProps && (!('openUri' in this.props) || nextProps.openUri != this.props.openUri)) {
this.showUri(nextProps.openUri);
}
},
componentWillUnmount: function() {
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
});
},
showUri: function(uri) {
this._storeHistoryOfNextRender = true;
this.setState({
address: uri,
appUrl: "?show=" + encodeURIComponent(uri),
viewingPage: "show",
pageArgs: uri,
})
},
handleUpgradeClicked: function() {
// Make a new directory within temp directory so the filename is guaranteed to be available
const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep);
let options = {
onProgress: (p) => this.setState({downloadProgress: Math.round(p * 100)}),
directory: dir,
};
download(remote.getCurrentWindow(), this.getUpdateUrl(), options)
.then(downloadItem => {
/**
* TODO: get the download path directly from the download object. It should just be
* downloadItem.getSavePath(), but the copy on the main process is being garbage collected
* too soon.
*/
this._upgradeDownloadItem = downloadItem;
this._upgradeDownloadPath = path.join(dir, this.getUpgradeFilename());
this.setState({
downloadComplete: true
});
});
this.setState({modal: 'downloading'});
},
handleStartUpgradeClicked: function() {
ipcRenderer.send('upgrade', this._upgradeDownloadPath);
},
cancelUpgrade: function() {
if (this._upgradeDownloadItem) {
/*
* Right now the remote reference to the download item gets garbage collected as soon as the
* the download is over (maybe even earlier), so trying to cancel a finished download may
* throw an error.
*/
try {
this._upgradeDownloadItem.cancel();
} catch (err) {
// Do nothing
}
}
this.setState({
downloadProgress: null,
downloadComplete: false,
modal: null,
});
},
handleSkipClicked: function() {
sessionStorage.setItem('upgradeSkipped', true);
this.setState({
modal: null,
});
},
alertError: function(error) {
var errorInfoList = [];
for (let key of Object.keys(error)) {
let val = typeof error[key] == 'string' ? error[key] : JSON.stringify(error[key]);
let label = this._error_key_labels[key];
errorInfoList.push(<li key={key}><strong>{label}</strong>: <code>{val}</code></li>);
}
this.setState({
modal: 'error',
errorInfo: <ul className="error-modal__error-list">{errorInfoList}</ul>,
});
},
getContentAndAddress: function()
{
switch(this.state.viewingPage)
{
case 'search':
return [this.state.pageArgs ? this.state.pageArgs : "Search", 'icon-search', <SearchPage query={this.state.pageArgs} />];
case 'settings':
return ["Settings", "icon-gear", <SettingsPage />];
case 'help':
return ["Help", "icon-question", <HelpPage />];
case 'report':
return ['Report an Issue', 'icon-file', <ReportPage />];
case 'downloaded':
return ["Downloads & Purchases", "icon-folder", <FileListDownloaded />];
case 'published':
return ["Publishes", "icon-folder", <FileListPublished />];
case 'start':
return ["Start", "icon-file", <StartPage />];
case 'rewards':
return ["Rewards", "icon-bank", <RewardsPage />];
case 'wallet':
case 'send':
case 'receive':
return [this.state.viewingPage.charAt(0).toUpperCase() + this.state.viewingPage.slice(1), "icon-bank", <WalletPage viewingPage={this.state.viewingPage} />]
case 'show':
return [lbryuri.normalize(this.state.pageArgs), "icon-file", <ShowPage uri={this.state.pageArgs} />];
case 'publish':
return ["Publish", "icon-upload", <PublishPage />];
case 'developer':
return ["Developer", "icon-file", <DeveloperPage />];
case 'discover':
default:
return ["Home", "icon-home", <DiscoverPage />];
}
},
render: function() {
let [address, wunderBarIcon, mainContent] = this.getContentAndAddress();
lbry.setTitle(address);
if (this._storeHistoryOfNextRender) {
this._storeHistoryOfNextRender = false;
history.pushState({}, document.title, this.state.appUrl);
}
return (
this._fullScreenPages.includes(this.state.viewingPage) ?
mainContent :
<div id="window">
<Header onSearch={this.onSearch} onSubmit={this.showUri} address={address} wunderBarIcon={wunderBarIcon} viewingPage={this.state.viewingPage} />
<div id="main-content">
{mainContent}
</div>
<Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available"
type="confirm" confirmButtonLabel="Upgrade" abortButtonLabel="Skip"
onConfirmed={this.handleUpgradeClicked} onAborted={this.handleSkipClicked}>
Your version of LBRY is out of date and may be unreliable or insecure.
</Modal>
<Modal isOpen={this.state.modal == 'downloading'} contentLabel="Downloading Update" type="custom">
Downloading Update{this.state.downloadProgress ? `: ${this.state.downloadProgress}%` : null}
<Line percent={this.state.downloadProgress} strokeWidth="4"/>
{this.state.downloadComplete ? (
<div>
<br />
<p>Click "Begin Upgrade" to start the upgrade process.</p>
<p>The app will close, and you will be prompted to install the latest version of LBRY.</p>
<p>After the install is complete, please reopen the app.</p>
</div>
) : null }
<div className="modal__buttons">
{this.state.downloadComplete
? <Link button="primary" label="Begin Upgrade" className="modal__button" onClick={this.handleStartUpgradeClicked} />
: null}
<Link button="alt" label="Cancel" className="modal__button" onClick={this.cancelUpgrade} />
</div>
</Modal>
<ExpandableModal isOpen={this.state.modal == 'error'} contentLabel="Error" className="error-modal"
overlayClassName="modal-overlay error-modal-overlay" onConfirmed={this.closeModal}
extraContent={this.state.errorInfo}>
<h3 className="modal__header">Error</h3>
<div className="error-modal__content">
<div><img className="error-modal__warning-symbol" src={lbry.imagePath('warning.png')} /></div>
<p>We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.</p>
</div>
</ExpandableModal>
</div>
);
const env = process.env.NODE_ENV || 'production';
const config = require(`./config/${env}`);
const logs = [];
const app = {
env: env,
config: config,
store: store,
logs: logs,
log: function(message) {
console.log(message);
logs.push(message);
}
});
}
export default App;
global.app = app;
module.exports = app;

View file

@ -0,0 +1,24 @@
import React from 'react';
import { connect } from 'react-redux'
import {
selectCurrentModal,
} from 'selectors/app'
import {
doCheckUpgradeAvailable,
} from 'actions/app'
import {
doUpdateBalance,
} from 'actions/wallet'
import App from './view'
const select = (state) => ({
modal: selectCurrentModal(state),
})
const perform = (dispatch) => ({
checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()),
updateBalance: (balance) => dispatch(doUpdateBalance(balance))
})
export default connect(select, perform)(App)

View file

@ -0,0 +1,42 @@
import React from 'react'
import Router from 'component/router'
import Header from 'component/header';
import ErrorModal from 'component/errorModal'
import DownloadingModal from 'component/downloadingModal'
import UpgradeModal from 'component/upgradeModal'
import lbry from 'lbry'
import {Line} from 'rc-progress'
class App extends React.Component {
componentWillMount() {
document.addEventListener('unhandledError', (event) => {
this.props.alertError(event.detail);
});
if (!this.props.upgradeSkipped) {
this.props.checkUpgradeAvailable()
}
lbry.balanceSubscribe((balance) => {
this.props.updateBalance(balance)
})
}
render() {
const {
modal,
} = this.props
return <div id="window">
<Header />
<div id="main-content">
<Router />
</div>
{modal == 'upgrade' && <UpgradeModal />}
{modal == 'downloading' && <DownloadingModal />}
{modal == 'error' && <ErrorModal />}
</div>
}
}
export default App

View file

@ -2,29 +2,35 @@ 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 Link from "component/link"
import {RewardLink} from 'component/reward-link';
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 {
class SubmitEmailStage extends React.Component {
constructor(props) {
super(props);
this.state = {
rewardType: null,
email: '',
submitting: false
};
},
handleEmailChanged: function(event) {
}
handleEmailChanged(event) {
this.setState({
email: event.target.value,
});
},
onEmailSaved: function(email) {
}
onEmailSaved(email) {
this.props.setStage("confirm", { email: email })
},
handleSubmit: function(event) {
}
handleSubmit(event) {
event.preventDefault();
this.setState({
@ -41,38 +47,43 @@ const SubmitEmailStage = React.createClass({
}
this.setState({ submitting: false });
});
},
render: function() {
}
render() {
return (
<section>
<form onSubmit={this.handleSubmit}>
<form onSubmit={(event) => { this.handleSubmit(event) }}>
<FormRow ref={(ref) => { this._emailRow = ref }} type="text" label="Email" placeholder="scrwvwls@lbry.io"
name="email" value={this.state.email}
onChange={this.handleEmailChanged} />
onChange={(event) => { this.handleEmailChanged(event) }} />
<div className="form-row-submit">
<Link button="primary" label="Next" disabled={this.state.submitting} onClick={this.handleSubmit} />
<Link button="primary" label="Next" disabled={this.state.submitting} onClick={(event) => { this.handleSubmit(event) }} />
</div>
</form>
</section>
);
}
});
}
const ConfirmEmailStage = React.createClass({
getInitialState: function() {
return {
class ConfirmEmailStage extends React.Component {
constructor(props) {
super(props);
this.state = {
rewardType: null,
code: '',
submitting: false,
errorMessage: null,
};
},
handleCodeChanged: function(event) {
}
handleCodeChanged(event) {
this.setState({
code: event.target.value,
});
},
handleSubmit: function(event) {
}
handleSubmit(event) {
event.preventDefault();
this.setState({
submitting: true,
@ -92,16 +103,17 @@ const ConfirmEmailStage = React.createClass({
onSubmitError(new Error("Your email is still not verified.")) //shouldn't happen?
}
}, onSubmitError);
},
render: function() {
}
render() {
return (
<section>
<form onSubmit={this.handleSubmit}>
<form onSubmit={(event) => { this.handleSubmit(event) }}>
<FormRow label="Verification Code" ref={(ref) => { this._codeRow = ref }} type="text"
name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={this.handleCodeChanged}
name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={(event) => { this.handleCodeChanged(event) }}
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} />
<Link button="primary" label="Verify" disabled={this.state.submitting} onClick={(event) => { this.handleSubmit(event)}} />
</div>
<div className="form-field__helper">
No code? <Link onClick={() => { this.props.setStage("nocode")}} label="Click here" />.
@ -110,25 +122,30 @@ const ConfirmEmailStage = React.createClass({
</section>
);
}
});
}
const WelcomeStage = React.createClass({
propTypes: {
class WelcomeStage extends React.Component {
static propTypes = {
endAuth: React.PropTypes.func,
},
getInitialState: function() {
return {
}
constructor(props) {
super(props);
this.state = {
hasReward: false,
rewardAmount: null,
}
},
onRewardClaim: function(reward) {
};
}
onRewardClaim(reward) {
this.setState({
hasReward: true,
rewardAmount: reward.amount
})
},
render: function() {
}
render() {
return (
!this.state.hasReward ?
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY" {...this.props}>
@ -139,7 +156,7 @@ const WelcomeStage = React.createClass({
<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) }} />
<RewardLink type="new_user" button="primary" onRewardClaim={(event) => { this.onRewardClaim(event) }} onRewardFailure={() => this.props.setStage(null)} onConfirmed={() => { this.props.setStage(null) }} />
</div>
</section>
</Modal> :
@ -155,43 +172,37 @@ const WelcomeStage = React.createClass({
</Modal>
);
}
});
}
const ErrorStage = (props) => {
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: {props.errorText}</p> : '' }
<Link button="alt" label="Try Reload" onClick={() => { window.location.reload() } } />
</section>
}
const PendingStage = (props) => {
return <section>
<p>Preparing for first access <span className="busy-indicator"></span></p>
</section>
}
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>
);
}
});
class CodeRequiredStage extends React.Component {
constructor(props) {
super(props);
const PendingStage = React.createClass({
render: function() {
return (
<section>
<p>Preparing for first access <span className="busy-indicator"></span></p>
</section>
);
}
});
this._balanceSubscribeId = nullp
const CodeRequiredStage = React.createClass({
_balanceSubscribeId: null,
getInitialState: function() {
return {
this.state = {
balance: 0,
address: getLocal('wallet_address')
}
},
};
}
componentWillMount: function() {
componentWillMount() {
this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
this.setState({
balance: balance
@ -199,18 +210,20 @@ const CodeRequiredStage = React.createClass({
})
if (!this.state.address) {
lbry.getUnusedAddress((address) => {
lbry.wallet_unused_address().then((address) => {
setLocal('wallet_address', address);
this.setState({ address: address });
});
}
},
componentWillUnmount: function() {
}
componentWillUnmount() {
if (this._balanceSubscribeId) {
lbry.balanceUnsubscribe(this._balanceSubscribeId)
}
},
render: function() {
}
render() {
const disabled = this.state.balance < 1;
return (
<div>
@ -233,31 +246,36 @@ const CodeRequiredStage = React.createClass({
</div>
);
}
});
}
export const AuthOverlay = React.createClass({
_stages: {
pending: PendingStage,
error: ErrorStage,
nocode: CodeRequiredStage,
email: SubmitEmailStage,
confirm: ConfirmEmailStage,
welcome: WelcomeStage
},
getInitialState: function() {
return {
export class AuthOverlay extends React.Component {
constructor(props) {
super(props);
this._stages = {
pending: PendingStage,
error: ErrorStage,
nocode: CodeRequiredStage,
email: SubmitEmailStage,
confirm: ConfirmEmailStage,
welcome: WelcomeStage
}
this.state = {
stage: "pending",
stageProps: {}
};
},
setStage: function(stage, stageProps = {}) {
}
setStage(stage, stageProps = {}) {
this.setState({
stage: stage,
stageProps: stageProps
})
},
componentWillMount: function() {
}
componentWillMount() {
lbryio.authenticate().then((user) => {
if (!user.HasVerifiedEmail) {
if (getLocal('auth_bypassed')) {
@ -283,19 +301,25 @@ export const AuthOverlay = React.createClass({
}
}));
})
},
render: function() {
}
render() {
if (!this.state.stage) {
return null;
}
const StageContent = this._stages[this.state.stage];
if (!StageContent) {
return <span className="empty">Unknown authentication step.</span>
}
return (
this.state.stage != "welcome" ?
<ModalPage className="modal-page--full" isOpen={true} contentLabel="Authentication" {...this.props}>
<ModalPage className="modal-page--full" isOpen={true} contentLabel="Authentication">
<h1>LBRY Early Access</h1>
<StageContent {...this.state.stageProps} setStage={this.setStage} />
<StageContent {...this.state.stageProps} setStage={(stage, stageProps) => { this.setStage(stage, stageProps) }} />
</ModalPage> :
<StageContent setStage={this.setStage} {...this.state.stageProps} />
<StageContent setStage={(stage, stageProps) => { this.setStage(stage, stageProps) }} {...this.state.stageProps} />
);
}
});
}

View file

@ -1,43 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import lbryuri from '../lbryuri.js';
import {Icon} from './common.js';
const UriIndicator = React.createClass({
propTypes: {
uri: React.PropTypes.string.isRequired,
hasSignature: React.PropTypes.bool.isRequired,
signatureIsValid: React.PropTypes.bool,
},
render: function() {
const uriObj = lbryuri.parse(this.props.uri);
if (!this.props.hasSignature || !uriObj.isChannel) {
return <span className="empty">Anonymous</span>;
}
const channelUriObj = Object.assign({}, uriObj);
delete channelUriObj.path;
delete channelUriObj.contentName;
const channelUri = lbryuri.build(channelUriObj, false);
let icon, modifier;
if (this.props.signatureIsValid) {
modifier = 'valid';
} else {
icon = 'icon-times-circle';
modifier = 'invalid';
}
return (
<span>
{channelUri} {' '}
{ !this.props.signatureIsValid ?
<Icon icon={icon} className={`channel-indicator__icon channel-indicator__icon--${modifier}`} /> :
'' }
</span>
);
}
});
export default UriIndicator;

View file

@ -2,65 +2,69 @@ import React from 'react';
import lbry from '../lbry.js';
//component/icon.js
export let Icon = React.createClass({
propTypes: {
export class Icon extends React.Component {
static propTypes = {
icon: React.PropTypes.string.isRequired,
className: React.PropTypes.string,
fixed: React.PropTypes.bool,
},
render: function() {
const {fixed, className, ...other} = this.props;
}
render() {
const {fixed, className} = this.props;
const spanClassName = ('icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') +
this.props.icon + ' ' + (this.props.className || ''));
return <span className={spanClassName} {... other}></span>
return <span className={spanClassName}></span>
}
});
}
export let TruncatedText = React.createClass({
propTypes: {
lines: React.PropTypes.number
},
getDefaultProps: function() {
return {
lines: null,
}
},
render: function() {
export class TruncatedText extends React.Component {
static propTypes = {
lines: React.PropTypes.number,
}
static defaultProps = {
lines: null
}
render() {
return <span className="truncated-text" style={{ WebkitLineClamp: this.props.lines }}>{this.props.children}</span>;
}
});
}
export let BusyMessage = React.createClass({
propTypes: {
message: React.PropTypes.string
},
render: function() {
export class BusyMessage extends React.Component {
static propTypes = {
message: React.PropTypes.string,
}
render() {
return <span>{this.props.message} <span className="busy-indicator"></span></span>
}
});
}
export let CurrencySymbol = React.createClass({
render: function() { return <span>LBC</span>; }
});
export class CurrencySymbol extends React.Component {
render() {
return <span>LBC</span>;
}
}
export let CreditAmount = React.createClass({
propTypes: {
export class CreditAmount extends React.Component {
static propTypes = {
amount: React.PropTypes.number.isRequired,
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() {
}
static defaultProps = {
precision: 1,
label: true,
showFree: false,
look: 'indicator',
}
render() {
const formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision);
let amountText;
if (this.props.showFree && parseFloat(formattedAmount) == 0) {
@ -80,93 +84,56 @@ export let CreditAmount = React.createClass({
</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 = {
let addressStyle = {
fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace',
};
export let Address = React.createClass({
_inputElem: null,
propTypes: {
export class Address extends React.Component {
static propTypes = {
address: React.PropTypes.string,
},
render: function() {
}
constructor(props) {
super(props);
this._inputElem = null;
}
render() {
return (
<input className="input-copyable" type="text" ref={(input) => { this._inputElem = input; }}
onFocus={() => { this._inputElem.select(); }} style={addressStyle} readOnly="readonly" value={this.props.address}></input>
);
}
});
}
export let Thumbnail = React.createClass({
_defaultImageUri: lbry.imagePath('default-thumb.svg'),
_maxLoadTime: 10000,
_isMounted: false,
propTypes: {
export class Thumbnail extends React.Component {
static propTypes = {
src: React.PropTypes.string,
},
handleError: function() {
}
handleError() {
if (this.state.imageUrl != this._defaultImageUri) {
this.setState({
imageUri: this._defaultImageUri,
});
}
},
getInitialState: function() {
return {
}
constructor(props) {
super(props);
this._defaultImageUri = lbry.imagePath('default-thumb.svg')
this._maxLoadTime = 10000
this._isMounted = false
this.state = {
imageUri: this.props.src || this._defaultImageUri,
};
},
componentDidMount: function() {
}
componentDidMount() {
this._isMounted = true;
setTimeout(() => {
if (this._isMounted && !this.refs.img.complete) {
@ -175,14 +142,16 @@ export let Thumbnail = React.createClass({
});
}
}, this._maxLoadTime);
},
componentWillUnmount: function() {
}
componentWillUnmount() {
this._isMounted = false;
},
render: function() {
}
render() {
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} />
},
});
return <img ref="img" onError={() => { this.handleError() }} {...otherProps} className={className} src={this.state.imageUri} />
}
}

View file

@ -0,0 +1,25 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doStartUpgrade,
doCancelUpgrade,
} from 'actions/app'
import {
selectDownloadProgress,
selectDownloadComplete,
} from 'selectors/app'
import DownloadingModal from './view'
const select = (state) => ({
downloadProgress: selectDownloadProgress(state),
downloadComplete: selectDownloadComplete(state),
})
const perform = (dispatch) => ({
startUpgrade: () => dispatch(doStartUpgrade()),
cancelUpgrade: () => dispatch(doCancelUpgrade())
})
export default connect(select, perform)(DownloadingModal)

View file

@ -0,0 +1,40 @@
import React from 'react'
import {
Modal
} from 'component/modal'
import {Line} from 'rc-progress';
import Link from 'component/link'
class DownloadingModal extends React.Component {
render() {
const {
downloadProgress,
downloadComplete,
startUpgrade,
cancelUpgrade,
} = this.props
return (
<Modal isOpen={true} contentLabel="Downloading Update" type="custom">
Downloading Update{downloadProgress ? `: ${downloadProgress}%` : null}
<Line percent={downloadProgress} strokeWidth="4"/>
{downloadComplete ? (
<div>
<br />
<p>Click "Begin Upgrade" to start the upgrade process.</p>
<p>The app will close, and you will be prompted to install the latest version of LBRY.</p>
<p>After the install is complete, please reopen the app.</p>
</div>
) : null }
<div className="modal__buttons">
{downloadComplete
? <Link button="primary" label="Begin Upgrade" className="modal__button" onClick={startUpgrade} />
: null}
<Link button="alt" label="Cancel" className="modal__button" onClick={cancelUpgrade} />
</div>
</Modal>
)
}
}
export default DownloadingModal

View file

@ -0,0 +1,23 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
selectCurrentModal,
selectError,
} from 'selectors/app'
import {
doCloseModal,
} from 'actions/app'
import ErrorModal from './view'
const select = (state) => ({
modal: selectCurrentModal(state),
error: selectError(state),
})
const perform = (dispatch) => ({
closeModal: () => dispatch(doCloseModal())
})
export default connect(select, perform)(ErrorModal)

View file

@ -0,0 +1,50 @@
import React from 'react'
import lbry from 'lbry'
import {
ExpandableModal
} from 'component/modal'
class ErrorModal extends React.Component {
render() {
const {
modal,
closeModal,
error,
} = this.props
const _error_key_labels = {
connectionString: 'API connection string',
method: 'Method',
params: 'Parameters',
code: 'Error code',
message: 'Error message',
data: 'Error data',
}
const errorInfo = <ul className="error-modal__error-list"></ul>
const errorInfoList = []
for (let key of Object.keys(error)) {
let val = typeof error[key] == 'string' ? error[key] : JSON.stringify(error[key]);
let label = this._error_key_labels[key];
errorInfoList.push(<li key={key}><strong>{label}</strong>: <code>{val}</code></li>);
}
return(
<ExpandableModal
isOpen={modal == 'error'}
contentLabel="Error" className="error-modal"
overlayClassName="error-modal-overlay"
onConfirmed={closeModal}
extraContent={errorInfo}
>
<h3 className="modal__header">Error</h3>
<div className="error-modal__content">
<div><img className="error-modal__warning-symbol" src={lbry.imagePath('warning.png')} /></div>
<p>We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.</p>
</div>
</ExpandableModal>
)
}
}
export default ErrorModal

View file

@ -1,270 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import lbryuri from '../lbryuri.js';
import {Link} from '../component/link.js';
import {Icon, FilePrice} from '../component/common.js';
import {Modal} from './modal.js';
import {FormField} from './form.js';
import {ToolTip} from '../component/tooltip.js';
import {DropDownMenu, DropDownMenuItem} from './menu.js';
const {shell} = require('electron');
let FileActionsRow = React.createClass({
_isMounted: false,
_fileInfoSubscribeId: null,
propTypes: {
uri: React.PropTypes.string,
outpoint: React.PropTypes.string.isRequired,
metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]),
contentType: React.PropTypes.string.isRequired,
},
getInitialState: function() {
return {
fileInfo: null,
modal: null,
menuOpen: false,
deleteChecked: false,
attemptingDownload: false,
attemptingRemove: false,
}
},
onFileInfoUpdate: function(fileInfo) {
if (this._isMounted) {
this.setState({
fileInfo: fileInfo ? fileInfo : false,
attemptingDownload: fileInfo ? false : this.state.attemptingDownload
});
}
},
tryDownload: function() {
this.setState({
attemptingDownload: true,
attemptingRemove: false
});
lbry.getCostInfo(this.props.uri).then(({cost}) => {
lbry.getBalance((balance) => {
if (cost > balance) {
this.setState({
modal: 'notEnoughCredits',
attemptingDownload: false,
});
} else if (this.state.affirmedPurchase) {
lbry.get({uri: this.props.uri}).then((streamInfo) => {
if (streamInfo === null || typeof streamInfo !== 'object') {
this.setState({
modal: 'timedOut',
attemptingDownload: false,
});
}
});
} else {
this.setState({
attemptingDownload: false,
modal: 'affirmPurchase'
})
}
});
});
},
closeModal: function() {
this.setState({
modal: null,
})
},
onDownloadClick: function() {
if (!this.state.fileInfo && !this.state.attemptingDownload) {
this.tryDownload();
}
},
onOpenClick: function() {
if (this.state.fileInfo && this.state.fileInfo.download_path) {
shell.openItem(this.state.fileInfo.download_path);
}
},
handleDeleteCheckboxClicked: function(event) {
this.setState({
deleteChecked: event.target.checked,
});
},
handleRevealClicked: function() {
if (this.state.fileInfo && this.state.fileInfo.download_path) {
shell.showItemInFolder(this.state.fileInfo.download_path);
}
},
handleRemoveClicked: function() {
this.setState({
modal: 'confirmRemove',
});
},
handleRemoveConfirmed: function() {
lbry.removeFile(this.props.outpoint, this.state.deleteChecked);
this.setState({
modal: null,
fileInfo: false,
attemptingDownload: false
});
},
onAffirmPurchase: function() {
this.setState({
affirmedPurchase: true,
modal: null
});
this.tryDownload();
},
openMenu: function() {
this.setState({
menuOpen: !this.state.menuOpen,
});
},
componentDidMount: function() {
this._isMounted = true;
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
},
componentWillUnmount: function() {
this._isMounted = false;
if (this._fileInfoSubscribeId) {
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
}
},
render: function() {
if (this.state.fileInfo === null)
{
return null;
}
const openInFolderMessage = window.navigator.platform.startsWith('Mac') ? 'Open in Finder' : 'Open in Folder',
showMenu = !!this.state.fileInfo;
let linkBlock;
if (this.state.fileInfo === false && !this.state.attemptingDownload) {
linkBlock = <Link button="text" label="Download" icon="icon-download" onClick={this.onDownloadClick} />;
} else if (this.state.attemptingDownload || (!this.state.fileInfo.completed && !this.state.fileInfo.isMine)) {
const
progress = this.state.fileInfo ? this.state.fileInfo.written_bytes / this.state.fileInfo.total_bytes * 100 : 0,
label = this.state.fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...',
labelWithIcon = <span className="button__content"><Icon icon="icon-download" /><span>{label}</span></span>;
linkBlock = (
<div className="faux-button-block file-actions__download-status-bar button-set-item">
<div className="faux-button-block file-actions__download-status-bar-overlay" style={{ width: progress + '%' }}>{labelWithIcon}</div>
{labelWithIcon}
</div>
);
} else {
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 (
<div>
{this.state.fileInfo !== null || this.state.fileInfo.isMine
? linkBlock
: null}
{ showMenu ?
<DropDownMenu>
<DropDownMenuItem key={0} onClick={this.handleRevealClicked} label={openInFolderMessage} />
<DropDownMenuItem key={1} onClick={this.handleRemoveClicked} label="Remove..." />
</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"
onConfirmed={this.closeModal}>
You don't have enough LBRY credits to pay for this stream.
</Modal>
<Modal isOpen={this.state.modal == 'timedOut'} contentLabel="Download failed"
onConfirmed={this.closeModal}>
LBRY was unable to download the stream <strong>{uri}</strong>.
</Modal>
<Modal isOpen={this.state.modal == 'confirmRemove'} contentLabel="Not enough credits"
type="confirm" confirmButtonLabel="Remove" onConfirmed={this.handleRemoveConfirmed}
onAborted={this.closeModal}>
<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>
</Modal>
</div>
);
}
});
export let FileActions = React.createClass({
_isMounted: false,
_fileInfoSubscribeId: null,
propTypes: {
uri: React.PropTypes.string,
outpoint: React.PropTypes.string.isRequired,
metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]),
contentType: React.PropTypes.string,
},
getInitialState: function() {
return {
available: true,
forceShowActions: false,
fileInfo: null,
}
},
onShowFileActionsRowClicked: function() {
this.setState({
forceShowActions: true,
});
},
onFileInfoUpdate: function(fileInfo) {
if (this.isMounted) {
this.setState({
fileInfo: fileInfo,
});
}
},
componentDidMount: function() {
this._isMounted = true;
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
lbry.get_availability({uri: this.props.uri}, (availability) => {
if (this._isMounted) {
this.setState({
available: availability > 0,
});
}
}, () => {
// Take any error to mean the file is unavailable
if (this._isMounted) {
this.setState({
available: false,
});
}
});
},
componentWillUnmount: function() {
this._isMounted = false;
if (this._fileInfoSubscribeId) {
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
}
},
render: function() {
const fileInfo = this.state.fileInfo;
if (fileInfo === null) {
return null;
}
return (<section className="file-actions">
{
fileInfo || this.state.available || this.state.forceShowActions
? <FileActionsRow outpoint={this.props.outpoint} metadata={this.props.metadata} uri={this.props.uri}
contentType={this.props.contentType} />
: <div>
<div className="button-set-item empty">Content unavailable.</div>
<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."
className="button-set-item" />
<Link label="Try Anyway" onClick={this.onShowFileActionsRowClicked} className="button-text button-set-item" />
</div>
}
</section>);
}
});

View file

@ -0,0 +1,58 @@
import React from 'react';
const {remote} = require('electron');
class FileSelector extends React.Component {
static propTypes = {
type: React.PropTypes.oneOf(['file', 'directory']),
initPath: React.PropTypes.string,
onFileChosen: React.PropTypes.func,
}
static defaultProps = {
type: 'file',
}
componentWillMount() {
this.setState({
path: this.props.initPath || null,
});
}
handleButtonClick() {
remote.dialog.showOpenDialog({
properties: [this.props.type == 'file' ? 'openFile' : 'openDirectory'],
}, (paths) => {
if (!paths) { // User hit cancel, so do nothing
return;
}
const path = paths[0];
this.setState({
path: path,
});
if (this.props.onFileChosen) {
this.props.onFileChosen(path);
}
});
}
render() {
return (
<div className="file-selector">
<button type="button" className="file-selector__choose-button" onClick={() => this.handleButtonClick()}>
{this.props.type == 'file' ?
'Choose File' :
'Choose Directory'}
</button>
{' '}
<span className="file-selector__path">
{this.state.path ?
this.state.path :
'No File Chosen'}
</span>
</div>
);
}
};
export default FileSelector;

View file

@ -1,284 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import lbryuri from '../lbryuri.js';
import {Link} from '../component/link.js';
import {FileActions} from '../component/file-actions.js';
import {BusyMessage, TruncatedText, FilePrice} from '../component/common.js';
import UriIndicator from '../component/channel-indicator.js';
/*should be merged into FileTile once FileTile is refactored to take a single id*/
export let FileTileStream = React.createClass({
_fileInfoSubscribeId: null,
_isMounted: null,
propTypes: {
uri: React.PropTypes.string,
metadata: React.PropTypes.object,
contentType: React.PropTypes.string.isRequired,
outpoint: React.PropTypes.string,
hasSignature: React.PropTypes.bool,
signatureIsValid: React.PropTypes.bool,
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() {
if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
this.setState({
showNsfwHelp: true,
});
}
},
handleMouseOut: function() {
if (this.state.showNsfwHelp) {
this.setState({
showNsfwHelp: 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={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<a href={primaryUrl} className="card__link">
<div className={"card__inner file-tile__row"}>
<div className="card__media"
style={{ backgroundImage: "url('" + (metadata && metadata.thumbnail ? metadata.thumbnail : lbry.imagePath('default-thumb.svg')) + "')" }}>
</div>
<div className="file-tile__content">
<div className="card__title-primary">
{ !this.props.hidePrice
? <FilePrice uri={this.props.uri} />
: null}
<div className="meta">{uri}</div>
<h3><TruncatedText lines={1}>{title}</TruncatedText></h3>
</div>
<div className="card__content card__subtext">
<TruncatedText lines={3}>
{isConfirmed
? metadata.description
: <span className="empty">This file is pending confirmation.</span>}
</TruncatedText>
</div>
</div>
</div>
</a>
{this.state.showNsfwHelp
? <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}
</section>
);
}
});
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({
_isMounted: false,
_isResolvePending: false,
propTypes: {
uri: React.PropTypes.string.isRequired,
},
getInitialState: function() {
return {
outpoint: null,
claimInfo: null
}
},
resolve: function(uri) {
this._isResolvePending = true;
lbry.resolve({uri: uri}).then((resolutionInfo) => {
this._isResolvePending = false;
if (this._isMounted && resolutionInfo && resolutionInfo.claim && resolutionInfo.claim.value &&
resolutionInfo.claim.value.stream && resolutionInfo.claim.value.stream.metadata) {
// In case of a failed lookup, metadata will be null, in which case the component will never display
this.setState({
claimInfo: resolutionInfo.claim,
});
}
});
},
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() {
this._isMounted = false;
},
render: function() {
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;
}
const {txid, nout, has_signature, signature_is_valid,
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

@ -0,0 +1,68 @@
import React from 'react'
import {
connect,
} from 'react-redux'
import {
selectPlatform,
} from 'selectors/app'
import {
makeSelectFileInfoForUri,
makeSelectDownloadingForUri,
makeSelectLoadingForUri,
} from 'selectors/file_info'
import {
makeSelectIsAvailableForUri,
} from 'selectors/availability'
import {
selectCurrentModal,
} from 'selectors/app'
import {
doCloseModal,
doOpenModal,
doHistoryBack,
} from 'actions/app'
import {
doFetchAvailability
} from 'actions/availability'
import {
doOpenFileInShell,
doOpenFileInFolder,
doDeleteFile,
} from 'actions/file_info'
import {
doPurchaseUri,
doLoadVideo,
} from 'actions/content'
import FileActions from './view'
const makeSelect = () => {
const selectFileInfoForUri = makeSelectFileInfoForUri()
const selectIsAvailableForUri = makeSelectIsAvailableForUri()
const selectDownloadingForUri = makeSelectDownloadingForUri()
const select = (state, props) => ({
fileInfo: selectFileInfoForUri(state, props),
isAvailable: selectIsAvailableForUri(state, props),
platform: selectPlatform(state),
modal: selectCurrentModal(state),
downloading: selectDownloadingForUri(state, props),
})
return select
}
const perform = (dispatch) => ({
checkAvailability: (uri) => dispatch(doFetchAvailability(uri)),
closeModal: () => dispatch(doCloseModal()),
openInFolder: (fileInfo) => dispatch(doOpenFileInFolder(fileInfo)),
openInShell: (fileInfo) => dispatch(doOpenFileInShell(fileInfo)),
deleteFile: (fileInfo, deleteFromComputer) => {
dispatch(doHistoryBack())
dispatch(doDeleteFile(fileInfo, deleteFromComputer))
},
openModal: (modal) => dispatch(doOpenModal(modal)),
startDownload: (uri) => dispatch(doPurchaseUri(uri)),
loadVideo: (uri) => dispatch(doLoadVideo(uri))
})
export default connect(makeSelect, perform)(FileActions)

View file

@ -0,0 +1,147 @@
import React from 'react';
import {Icon,BusyMessage} from 'component/common';
import FilePrice from 'component/filePrice'
import {Modal} from 'component/modal';
import {FormField} from 'component/form';
import Link from 'component/link';
import {ToolTip} from 'component/tooltip';
import {DropDownMenu, DropDownMenuItem} from 'component/menu';
class FileActions extends React.Component {
constructor(props) {
super(props)
this.state = {
forceShowActions: false,
deleteChecked: false,
}
}
componentWillMount() {
this.checkAvailability(this.props.uri)
}
componentWillReceiveProps(nextProps) {
this.checkAvailability(nextProps.uri)
}
checkAvailability(uri) {
if (!this._uri || uri !== this._uri) {
this._uri = uri;
this.props.checkAvailability(uri)
}
}
onShowFileActionsRowClicked() {
this.setState({
forceShowActions: true,
});
}
handleDeleteCheckboxClicked(event) {
this.setState({
deleteChecked: event.target.checked,
})
}
onAffirmPurchase() {
this.props.closeModal()
this.props.loadVideo(this.props.uri)
}
render() {
const {
fileInfo,
isAvailable,
platform,
downloading,
uri,
deleteFile,
openInFolder,
openInShell,
modal,
openModal,
closeModal,
startDownload,
} = this.props
const deleteChecked = this.state.deleteChecked,
metadata = fileInfo ? fileInfo.metadata : null,
openInFolderMessage = platform.startsWith('Mac') ? 'Open in Finder' : 'Open in Folder',
showMenu = fileInfo && Object.keys(fileInfo).length > 0,
title = metadata ? metadata.title : uri;
let content
if (downloading) {
const
progress = (fileInfo && fileInfo.written_bytes) ? fileInfo.written_bytes / fileInfo.total_bytes * 100 : 0,
label = fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...',
labelWithIcon = <span className="button__content"><Icon icon="icon-download" /><span>{label}</span></span>;
content = <div className="faux-button-block file-actions__download-status-bar button-set-item">
<div className="faux-button-block file-actions__download-status-bar-overlay" style={{ width: progress + '%' }}>{labelWithIcon}</div>
{labelWithIcon}
</div>
} else if (!fileInfo && isAvailable === undefined) {
content = <BusyMessage message="Checking availability" />
} else if (!fileInfo && !isAvailable && !this.state.forceShowActions) {
content = <div>
<div className="button-set-item empty">Content unavailable.</div>
<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."
className="button-set-item" />
<Link label="Try Anyway" onClick={this.onShowFileActionsRowClicked.bind(this)} className="button-text button-set-item" />
</div>
} else if (fileInfo === null && !downloading) {
content = <Link button="text" label="Download" icon="icon-download" onClick={() => { startDownload(uri) } } />;
} else if (fileInfo && fileInfo.download_path) {
content = <Link label="Open" button="text" icon="icon-folder-open" onClick={() => openInShell(fileInfo)} />;
} else {
console.log('handle this case of file action props?');
console.log(this.props)
}
return (
<section className="file-actions">
{ content }
{ showMenu ?
<DropDownMenu>
<DropDownMenuItem key={0} onClick={() => openInFolder(fileInfo)} label={openInFolderMessage} />
<DropDownMenuItem key={1} onClick={() => openModal('confirmRemove')} label="Remove..." />
</DropDownMenu> : '' }
<Modal type="confirm" isOpen={modal == 'affirmPurchase'}
contentLabel="Confirm Purchase" onConfirmed={this.onAffirmPurchase.bind(this)} onAborted={this.props.closeModal}>
This will purchase <strong>{title}</strong> for <strong><FilePrice uri={uri} look="plain" /></strong> credits.
</Modal>
<Modal isOpen={modal == 'notEnoughCredits'} contentLabel="Not enough credits"
onConfirmed={closeModal}>
You don't have enough LBRY credits to pay for this stream.
</Modal>
<Modal isOpen={modal == 'timedOut'} contentLabel="Download failed"
onConfirmed={closeModal}>
LBRY was unable to download the stream <strong>{uri}</strong>.
</Modal>
<Modal isOpen={modal == 'confirmRemove'}
contentLabel="Not enough credits"
type="confirm"
confirmButtonLabel="Remove"
onConfirmed={() => deleteFile(fileInfo.outpoint, deleteChecked)}
onAborted={closeModal}>
<p>Are you sure you'd like to remove <cite>{title}</cite> from LBRY?</p>
<label><FormField type="checkbox" checked={deleteChecked} onClick={this.handleDeleteCheckboxClicked.bind(this)} /> Delete this file from my computer</label>
</Modal>
</section>
);
}
}
export default FileActions

View file

@ -0,0 +1,50 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doNavigate,
} from 'actions/app'
import {
doResolveUri,
doCancelResolveUri,
} from 'actions/content'
import {
selectObscureNsfw,
} from 'selectors/app'
import {
makeSelectClaimForUri,
makeSelectMetadataForUri,
} from 'selectors/claims'
import {
makeSelectFileInfoForUri,
} from 'selectors/file_info'
import {
makeSelectIsResolvingForUri,
} from 'selectors/content'
import FileCard from './view'
const makeSelect = () => {
const selectClaimForUri = makeSelectClaimForUri()
const selectFileInfoForUri = makeSelectFileInfoForUri()
const selectMetadataForUri = makeSelectMetadataForUri()
const selectResolvingUri = makeSelectIsResolvingForUri()
const select = (state, props) => ({
claim: selectClaimForUri(state, props),
fileInfo: selectFileInfoForUri(state, props),
obscureNsfw: selectObscureNsfw(state),
metadata: selectMetadataForUri(state, props),
isResolvingUri: selectResolvingUri(state, props),
})
return select
}
const perform = (dispatch) => ({
navigate: (path, params) => dispatch(doNavigate(path, params)),
resolveUri: (uri) => dispatch(doResolveUri(uri)),
cancelResolveUri: (uri) => dispatch(doCancelResolveUri(uri))
})
export default connect(makeSelect, perform)(FileCard)

View file

@ -0,0 +1,113 @@
import React from 'react';
import lbry from 'lbry.js';
import lbryuri from 'lbryuri.js';
import Link from 'component/link';
import {Thumbnail, TruncatedText, Icon} from 'component/common';
import FilePrice from 'component/filePrice'
import UriIndicator from 'component/uriIndicator';
class FileCard extends React.Component {
componentWillMount() {
this.resolve(this.props)
}
componentWillReceiveProps(nextProps) {
this.resolve(nextProps)
}
resolve(props) {
const {
isResolvingUri,
resolveUri,
claim,
uri,
} = props
if(!isResolvingUri && claim === undefined && uri) {
resolveUri(uri)
}
}
componentWillUnmount() {
const {
isResolvingUri,
cancelResolveUri,
uri
} = this.props
if (isResolvingUri) {
cancelResolveUri(uri)
}
}
handleMouseOver() {
this.setState({
hovered: true,
});
}
handleMouseOut() {
this.setState({
hovered: false,
});
}
render() {
const {
claim,
fileInfo,
metadata,
isResolvingUri,
navigate,
} = this.props
const uri = lbryuri.normalize(this.props.uri);
const title = !isResolvingUri && metadata && metadata.title ? metadata.title : uri;
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
let description = ""
if (isResolvingUri) {
description = "Loading..."
} else if (metadata && metadata.description) {
description = metadata.description
} else if (claim === null) {
description = 'This address contains no content.'
}
return (
<section className={ 'card card--small card--link ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver.bind(this)} onMouseLeave={this.handleMouseOut.bind(this)}>
<div className="card__inner">
<Link onClick={() => navigate('/show', { uri })} className="card__link">
<div className="card__title-identity">
<h5 title={title}><TruncatedText lines={1}>{title}</TruncatedText></h5>
<div className="card__subtitle">
<span style={{float: "right"}}>
<FilePrice uri={uri} />
{ fileInfo ? <span>{' '}<Icon fixed icon="icon-folder" /></span> : '' }
</span>
<UriIndicator uri={uri} />
</div>
</div>
{metadata && metadata.thumbnail &&
<div className="card__media" style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}></div>
}
<div className="card__content card__subtext card__subtext--two-lines">
<TruncatedText lines={2}>{description}</TruncatedText>
</div>
</Link>
{obscureNsfw && 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" onClick={() => navigate('settings')} label="Settings" />.
</p>
</div>
: null}
</div>
</section>
);
}
}
export default FileCard

View file

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

View file

@ -0,0 +1,93 @@
import React from 'react';
import lbry from 'lbry.js';
import lbryuri from 'lbryuri.js';
import Link from 'component/link';
import {FormField} from 'component/form.js';
import FileTile from 'component/fileTile';
import rewards from 'rewards.js';
import lbryio from 'lbryio.js';
import {BusyMessage, Thumbnail} from 'component/common.js';
class FileList extends React.Component {
constructor(props) {
super(props)
this.state = {
sortBy: 'date',
}
this._sortFunctions = {
date: function(fileInfos) {
return fileInfos.slice().reverse();
},
title: function(fileInfos) {
return fileInfos.slice().sort(function(fileInfo1, fileInfo2) {
const title1 = fileInfo1.metadata ? fileInfo1.metadata.stream.metadata.title.toLowerCase() : fileInfo1.name;
const title2 = fileInfo2.metadata ? fileInfo2.metadata.stream.metadata.title.toLowerCase() : fileInfo2.name;
if (title1 < title2) {
return -1;
} else if (title1 > title2) {
return 1;
} else {
return 0;
}
})
},
filename: function(fileInfos) {
return fileInfos.slice().sort(function({file_name: fileName1}, {file_name: fileName2}) {
const fileName1Lower = fileName1.toLowerCase();
const fileName2Lower = fileName2.toLowerCase();
if (fileName1Lower < fileName2Lower) {
return -1;
} else if (fileName2Lower > fileName1Lower) {
return 1;
} else {
return 0;
}
})
},
}
}
handleSortChanged(event) {
this.setState({
sortBy: event.target.value,
})
}
render() {
const {
handleSortChanged,
fetching,
fileInfos,
} = this.props
const {
sortBy,
} = this.state
const content = []
this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
const uri = lbryuri.build({
contentName: fileInfo.name,
channelName: fileInfo.channel_name,
})
content.push(<FileTile key={uri} uri={uri} hidePrice={true} showEmpty={this.props.fileTileShowEmpty} />)
})
return (
<section className="file-list__header">
{ fetching && <span className="busy-indicator"/> }
<span className='sort-section'>
Sort by { ' ' }
<FormField type="select" onChange={this.handleSortChanged.bind(this)}>
<option value="date">Date</option>
<option value="title">Title</option>
<option value="filename">File name</option>
</FormField>
</span>
{content}
</section>
)
}
}
export default FileList

View file

@ -0,0 +1,29 @@
import React from 'react'
import {
connect,
} from 'react-redux'
import {
doSearch,
} from 'actions/search'
import {
selectIsSearching,
selectCurrentSearchResults,
selectSearchQuery,
} from 'selectors/search'
import {
doNavigate,
} from 'actions/app'
import FileListSearch from './view'
const select = (state) => ({
isSearching: selectIsSearching(state),
query: selectSearchQuery(state),
results: selectCurrentSearchResults(state)
})
const perform = (dispatch) => ({
navigate: (path) => dispatch(doNavigate(path)),
search: (search) => dispatch(doSearch(search))
})
export default connect(select, perform)(FileListSearch)

View file

@ -0,0 +1,70 @@
import React from 'react';
import lbry from 'lbry';
import lbryio from 'lbryio';
import lbryuri from 'lbryuri';
import lighthouse from 'lighthouse';
import FileTile from 'component/fileTile'
import Link from 'component/link'
import {ToolTip} from 'component/tooltip.js';
import {BusyMessage} from 'component/common.js';
const SearchNoResults = (props) => {
const {
navigate,
query,
} = props
return <section>
<span className="empty">
No one has checked anything in for {query} yet. { ' ' }
<Link label="Be the first" onClick={() => navigate('/publish')} />
</span>
</section>;
}
const FileListSearchResults = (props) => {
const {
results,
} = props
const rows = [],
seenNames = {}; //fix this when the search API returns claim IDs
for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of results) {
const uri = lbryuri.build({
channelName: channel_name,
contentName: name,
claimId: channel_id || claim_id,
});
rows.push(
<FileTile key={uri} uri={uri} />
);
}
return (
<div>{rows}</div>
);
}
class FileListSearch extends React.Component{
componentWillMount() {
this.props.search(this.props.query)
}
render() {
const {
isSearching,
results
} = this.props
return (
isSearching ?
<BusyMessage message="Looking up the Dewey Decimals" /> :
(results && results.length) ?
<FileListSearchResults {...this.props} /> :
<SearchNoResults {...this.props} />
)
}
}
export default FileListSearch

View file

@ -0,0 +1,28 @@
import React from 'react'
import {
connect,
} from 'react-redux'
import {
doFetchCostInfoForUri,
} from 'actions/cost_info'
import {
makeSelectCostInfoForUri,
} from 'selectors/cost_info'
import FilePrice from './view'
const makeSelect = () => {
const selectCostInfoForUri = makeSelectCostInfoForUri()
const select = (state, props) => ({
costInfo: selectCostInfoForUri(state, props),
})
return select
}
const perform = (dispatch) => ({
fetchCostInfo: (uri) => dispatch(doFetchCostInfoForUri(uri)),
// cancelFetchCostInfo: (uri) => dispatch(doCancelFetchCostInfoForUri(uri))
})
export default connect(makeSelect, perform)(FilePrice)

View file

@ -0,0 +1,43 @@
import React from 'react'
import {
CreditAmount,
} from 'component/common'
class FilePrice extends React.Component{
componentWillMount() {
this.fetchCost(this.props)
}
componentWillReceiveProps(nextProps) {
this.fetchCost(nextProps)
}
fetchCost(props) {
const {
costInfo,
fetchCostInfo,
uri
} = props
if (costInfo === undefined) {
fetchCostInfo(uri)
}
}
render() {
const {
costInfo,
look = 'indicator',
} = this.props
const isEstimate = costInfo ? !costInfo.includesData : null
if (!costInfo) {
return <span className={`credit-amount credit-amount--${look}`}>???</span>;
}
return <CreditAmount label={false} amount={costInfo.cost} isEstimate={isEstimate} showFree={true} />
}
}
export default FilePrice

View file

@ -0,0 +1,48 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doNavigate,
} from 'actions/app'
import {
doResolveUri,
} from 'actions/content'
import {
makeSelectClaimForUri,
makeSelectMetadataForUri,
} from 'selectors/claims'
import {
makeSelectFileInfoForUri,
} from 'selectors/file_info'
import {
selectObscureNsfw,
} from 'selectors/app'
import {
makeSelectIsResolvingForUri,
} from 'selectors/content'
import FileTile from './view'
const makeSelect = () => {
const selectClaimForUri = makeSelectClaimForUri()
const selectFileInfoForUri = makeSelectFileInfoForUri()
const selectMetadataForUri = makeSelectMetadataForUri()
const selectResolvingUri = makeSelectIsResolvingForUri()
const select = (state, props) => ({
claim: selectClaimForUri(state, props),
fileInfo: selectFileInfoForUri(state, props),
obscureNsfw: selectObscureNsfw(state),
metadata: selectMetadataForUri(state, props),
isResolvingUri: selectResolvingUri(state, props),
})
return select
}
const perform = (dispatch) => ({
navigate: (path, params) => dispatch(doNavigate(path, params)),
resolveUri: (uri) => dispatch(doResolveUri(uri)),
})
export default connect(makeSelect, perform)(FileTile)

View file

@ -0,0 +1,116 @@
import React from 'react';
import lbry from 'lbry.js';
import lbryuri from 'lbryuri.js';
import Link from 'component/link';
import FileActions from 'component/fileActions';
import {Thumbnail, TruncatedText,} from 'component/common.js';
import FilePrice from 'component/filePrice'
import UriIndicator from 'component/uriIndicator';
class FileTile extends React.Component {
static SHOW_EMPTY_PUBLISH = "publish"
static SHOW_EMPTY_PENDING = "pending"
constructor(props) {
super(props)
this.state = {
showNsfwHelp: false,
}
}
componentDidMount() {
const {
isResolvingUri,
resolveUri,
claim,
uri,
} = this.props
if(!isResolvingUri && !claim && uri) {
resolveUri(uri)
}
}
handleMouseOver() {
if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
this.setState({
showNsfwHelp: true,
});
}
}
handleMouseOut() {
if (this.state.showNsfwHelp) {
this.setState({
showNsfwHelp: false,
});
}
}
render() {
const {
claim,
metadata,
isResolvingUri,
showEmpty,
navigate,
hidePrice,
} = this.props
const uri = lbryuri.normalize(this.props.uri);
const isClaimed = !!claim;
const isClaimable = lbryuri.isClaimable(uri)
const title = isClaimed && metadata && metadata.title ? metadata.title : uri;
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
let onClick = () => navigate('/show', { uri })
let description = ""
if (isClaimed) {
description = metadata && metadata.description
} else if (isResolvingUri) {
description = "Loading..."
} else if (showEmpty === FileTile.SHOW_EMPTY_PUBLISH) {
onClick = () => navigate('/publish', { })
description = <span className="empty">
This location is unused. { ' ' }
{ isClaimable && <span className="button-text">Put something here!</span> }
</span>
} else if (showEmpty === FileTile.SHOW_EMPTY_PENDING) {
description = <span className="empty">This file is pending confirmation.</span>
}
return (
<section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver.bind(this)} onMouseLeave={this.handleMouseOut.bind(this)}>
<Link onClick={onClick} className="card__link">
<div className={"card__inner file-tile__row"}>
<div className="card__media"
style={{ backgroundImage: "url('" + (metadata && metadata.thumbnail ? metadata.thumbnail : lbry.imagePath('default-thumb.svg')) + "')" }}>
</div>
<div className="file-tile__content">
<div className="card__title-primary">
{ !hidePrice ? <FilePrice uri={this.props.uri} /> : null}
<div className="meta">{uri}</div>
<h3><TruncatedText lines={1}>{title}</TruncatedText></h3>
</div>
<div className="card__content card__subtext">
<TruncatedText lines={3}>
{description}
</TruncatedText>
</div>
</div>
</div>
</Link>
{this.state.showNsfwHelp
? <div className='card-overlay'>
<p>
This content is Not Safe For Work.
To view adult content, please change your <Link className="button-text" onClick={() => navigate('/settings')} label="Settings" />.
</p>
</div>
: null}
</section>
);
}
}
export default FileTile

View file

@ -1,65 +1,95 @@
import React from 'react';
import FileSelector from './file-selector.js';
import {Icon} from './common.js';
var formFieldCounter = 0,
formFieldFileSelectorTypes = ['file', 'directory'],
formFieldNestedLabelTypes = ['radio', 'checkbox'];
function formFieldId() {
return "form-field-" + (++formFieldCounter);
}
export let FormField = React.createClass({
_fieldRequiredText: 'This field is required',
_type: null,
_element: null,
propTypes: {
export class FormField extends React.Component {
static propTypes = {
type: React.PropTypes.string.isRequired,
prefix: React.PropTypes.string,
postfix: React.PropTypes.string,
hasError: React.PropTypes.bool
},
getInitialState: function() {
return {
}
constructor(props) {
super(props);
this._fieldRequiredText = 'This field is required';
this._type = null;
this._element = null;
this.state = {
isError: null,
errorMessage: null,
}
},
componentWillMount: function() {
if (['text', 'number', 'radio', 'checkbox', 'file'].includes(this.props.type)) {
};
}
componentWillMount() {
if (['text', 'number', 'radio', 'checkbox'].includes(this.props.type)) {
this._element = 'input';
this._type = this.props.type;
} else if (this.props.type == 'text-number') {
this._element = 'input';
this._type = 'text';
} else if (formFieldFileSelectorTypes.includes(this.props.type)) {
this._element = 'input';
this._type = 'hidden';
} else {
// Non <input> field, e.g. <select>, <textarea>
this._element = this.props.type;
}
},
showError: function(text) {
}
componentDidMount() {
/**
* We have to add the webkitdirectory attribute here because React doesn't allow it in JSX
* https://github.com/facebook/react/issues/3468
*/
if (this.props.type == 'directory') {
this.refs.field.webkitdirectory = true;
}
}
handleFileChosen(path) {
this.refs.field.value = path;
if (this.props.onChange) { // Updating inputs programmatically doesn't generate an event, so we have to make our own
const event = new Event('change', {bubbles: true})
this.refs.field.dispatchEvent(event); // This alone won't generate a React event, but we use it to attach the field as a target
this.props.onChange(event);
}
}
showError(text) {
this.setState({
isError: true,
errorMessage: text,
});
},
focus: function() {
}
focus() {
this.refs.field.focus();
},
getValue: function() {
}
getValue() {
if (this.props.type == 'checkbox') {
return this.refs.field.checked;
} else if (this.props.type == 'file') {
return this.refs.field.files.length && this.refs.field.files[0].path ?
this.refs.field.files[0].path : null;
} else {
return this.refs.field.value;
}
},
getSelectedElement: function() {
}
getSelectedElement() {
return this.refs.field.options[this.refs.field.selectedIndex];
},
render: function() {
}
render() {
// Pass all unhandled props to the field element
const otherProps = Object.assign({}, this.props),
isError = this.state.isError !== null ? this.state.isError : this.props.hasError,
@ -87,49 +117,64 @@ export let FormField = React.createClass({
{this.props.label}
</label> :
element }
{ formFieldFileSelectorTypes.includes(this.props.type) ?
<FileSelector type={this.props.type} onFileChosen={this.handleFileChosen.bind(this)}
{... this.props.defaultValue ? {initPath: this.props.defaultValue} : {}} /> :
null }
{ 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: {
label: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element])
export class FormRow extends React.Component {
static propTypes = {
label: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element]),
// helper: React.PropTypes.html,
},
getInitialState: function() {
return {
}
constructor(props) {
super(props);
this._fieldRequiredText = 'This field is required';
this.state = {
isError: false,
errorMessage: null,
}
},
showError: function(text) {
};
}
showError(text) {
this.setState({
isError: true,
errorMessage: text,
});
},
showRequiredError: function() {
}
showRequiredError() {
this.showError(this._fieldRequiredText);
},
clearError: function(text) {
}
clearError(text) {
this.setState({
isError: false,
errorMessage: ''
});
},
getValue: function() {
}
getValue() {
return this.refs.field.getValue();
},
getSelectedElement: function() {
}
getSelectedElement() {
return this.refs.field.getSelectedElement();
},
focus: function() {
}
focus() {
this.refs.field.focus();
},
render: function() {
}
render() {
const fieldProps = Object.assign({}, this.props),
elementId = formFieldId(),
renderLabelInFormField = formFieldNestedLabelTypes.includes(this.props.type);
@ -151,4 +196,4 @@ export let FormRow = React.createClass({
{ this.state.isError ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
</div>
}
})
}

View file

@ -1,213 +0,0 @@
import React from 'react';
import lbryuri from '../lbryuri.js';
import {Link} from './link.js';
import {Icon, CreditAmount} from './common.js';
var Header = React.createClass({
_balanceSubscribeId: null,
_isMounted: false,
propTypes: {
onSearch: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {
balance: 0
};
},
componentDidMount: function() {
this._isMounted = true;
this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
if (this._isMounted) {
this.setState({balance: balance});
}
});
},
componentWillUnmount: function() {
this._isMounted = false;
if (this._balanceSubscribeId) {
lbry.balanceUnsubscribe(this._balanceSubscribeId)
}
},
render: function() {
return <header id="header">
<div className="header__item">
<Link onClick={() => { lbry.back() }} button="alt button--flat" icon="icon-arrow-left" />
</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>
</header>
}
});
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() {
let links = [],
viewingUrl = '?' + this.props.viewingPage;
for (let link of Object.keys(this.props.links)) {
links.push(
<a href={link} key={link} className={ viewingUrl == link ? 'sub-header-selected' : 'sub-header-unselected' }>
{this.props.links[link]}
</a>
);
}
return (
<nav className={'sub-header' + (this.props.modifier ? ' sub-header--' + this.props.modifier : '')}>
{links}
</nav>
);
}
});
export default Header;

View file

@ -0,0 +1,24 @@
import React from 'react'
import lbry from 'lbry'
import {
connect
} from 'react-redux'
import {
selectBalance
} from 'selectors/wallet'
import {
doNavigate,
doHistoryBack,
} from 'actions/app'
import Header from './view'
const select = (state) => ({
balance: lbry.formatCredits(selectBalance(state), 1)
})
const perform = (dispatch) => ({
navigate: (path) => dispatch(doNavigate(path)),
back: () => dispatch(doHistoryBack()),
})
export default connect(select, perform)(Header)

View file

@ -0,0 +1,37 @@
import React from 'react';
import Link from 'component/link';
import WunderBar from 'component/wunderbar';
export const Header = (props) => {
const {
balance,
back,
navigate
} = props
return <header id="header">
<div className="header__item">
<Link onClick={back} button="alt button--flat" icon="icon-arrow-left" />
</div>
<div className="header__item">
<Link onClick={() => navigate('/discover')} button="alt button--flat" icon="icon-home" />
</div>
<div className="header__item header__item--wunderbar">
<WunderBar/>
</div>
<div className="header__item">
<Link onClick={() => navigate('/wallet')} button="text" icon="icon-bank" label={balance} ></Link>
</div>
<div className="header__item">
<Link onClick={() => navigate('/publish')} button="primary button--flat" icon="icon-upload" label="Publish" />
</div>
<div className="header__item">
<Link onClick={() => navigate('/downloaded')} button="alt button--flat" icon="icon-folder" />
</div>
<div className="header__item">
<Link onClick={() => navigate('/settings')} button="alt button--flat" icon="icon-gear" />
</div>
</header>
}
export default Header;

View file

@ -1,133 +0,0 @@
import React from 'react';
import {Icon} from './common.js';
import Modal from '../component/modal.js';
import rewards from '../rewards.js';
export let Link = React.createClass({
propTypes: {
label: React.PropTypes.string,
icon: React.PropTypes.string,
button: React.PropTypes.string,
badge: React.PropTypes.string,
hidden: React.PropTypes.bool,
},
getDefaultProps: function() {
return {
hidden: false,
disabled: false,
};
},
handleClick: function(e) {
if (this.props.onClick) {
this.props.onClick(e);
}
},
render: function() {
if (this.props.hidden) {
return null;
}
/* The way the class name is generated here is a mess -- refactor */
const className = (this.props.className || '') +
(!this.props.className && !this.props.button ? 'button-text' : '') + // Non-button links get the same look as text buttons
(this.props.button ? ' button-block button-' + this.props.button + ' button-set-item' : '') +
(this.props.disabled ? ' disabled' : '');
let content;
if (this.props.children) { // Custom content
content = this.props.children;
} else {
content = (
<span {... 'button' in this.props ? {className: 'button__content'} : {}}>
{'icon' in this.props ? <Icon icon={this.props.icon} fixed={true} /> : null}
{this.props.label ? <span className="link-label">{this.props.label}</span> : null}
{'badge' in this.props ? <span className="badge">{this.props.badge}</span> : null}
</span>
);
}
return (
<a className={className} href={this.props.href || 'javascript:;'} title={this.props.title}
onClick={this.handleClick} {... 'style' in this.props ? {style: this.props.style} : {}}>
{content}
</a>
);
}
});
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

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

View file

@ -0,0 +1,100 @@
import React from 'react';
import {Icon} from 'component/common.js';
const Link = (props) => {
const {
href,
title,
onClick,
style,
label,
icon,
badge,
button,
hidden,
disabled,
children,
} = props
const className = (props.className || '') +
(!props.className && !props.button ? 'button-text' : '') + // Non-button links get the same look as text buttons
(props.button ? ' button-block button-' + props.button + ' button-set-item' : '') +
(props.disabled ? ' disabled' : '');
let content;
if (children) {
content = children
} else {
content = (
<span {... 'button' in props ? {className: 'button__content'} : {}}>
{'icon' in props ? <Icon icon={icon} fixed={true} /> : null}
{label ? <span className="link-label">{label}</span> : null}
{'badge' in props ? <span className="badge">{badge}</span> : null}
</span>
)
}
return (
<a className={className} href={href || 'javascript:;'} title={title}
onClick={onClick}
{... 'style' in props ? {style: style} : {}}>
{content}
</a>
);
}
export default Link
// export let Link = React.createClass({
// propTypes: {
// label: React.PropTypes.string,
// icon: React.PropTypes.string,
// button: React.PropTypes.string,
// badge: React.PropTypes.string,
// hidden: React.PropTypes.bool,
// },
// getDefaultProps: function() {
// return {
// hidden: false,
// disabled: false,
// };
// },
// handleClick: function(e) {
// if (this.props.onClick) {
// this.props.onClick(e);
// }
// },
// render: function() {
// if (this.props.hidden) {
// return null;
// }
// /* The way the class name is generated here is a mess -- refactor */
// const className = (this.props.className || '') +
// (!this.props.className && !this.props.button ? 'button-text' : '') + // Non-button links get the same look as text buttons
// (this.props.button ? ' button-block button-' + this.props.button + ' button-set-item' : '') +
// (this.props.disabled ? ' disabled' : '');
// let content;
// if (this.props.children) { // Custom content
// content = this.props.children;
// } else {
// content = (
// <span {... 'button' in this.props ? {className: 'button__content'} : {}}>
// {'icon' in this.props ? <Icon icon={this.props.icon} fixed={true} /> : null}
// {<span className="link-label">{this.props.label}</span>}
// {'badge' in this.props ? <span className="badge">{this.props.badge}</span> : null}
// </span>
// );
// }
// return (
// <a className={className} href={this.props.href || 'javascript:;'} title={this.props.title}
// onClick={(event) => { this.handleClick() }} {... 'style' in this.props ? {style: this.props.style} : {}}>
// {content}
// </a>
// );
// }
// });

View file

@ -1,27 +1,30 @@
import React from 'react';
import lbry from '../lbry.js';
import {BusyMessage, Icon} from './common.js';
import {Link} from '../component/link.js'
import Link from 'component/link'
var LoadScreen = React.createClass({
propTypes: {
class LoadScreen extends React.Component {
static propTypes = {
message: React.PropTypes.string.isRequired,
details: React.PropTypes.string,
isWarning: React.PropTypes.bool,
},
getDefaultProps: function() {
return {
isWarning: false,
}
},
getInitialState: function() {
return {
}
constructor(props) {
super(props);
this.state = {
message: null,
details: null,
isLagging: false,
}
},
render: function() {
};
}
static defaultProps = {
isWarning: false,
}
render() {
const imgSrc = lbry.imagePath('lbry-white-485x160.png');
return (
<div className="load-screen">
@ -35,7 +38,7 @@ var LoadScreen = React.createClass({
</div>
);
}
});
}
export default LoadScreen;

View file

@ -1,20 +1,20 @@
import React from 'react';
import {Icon} from './common.js';
import {Link} from '../component/link.js';
import Link from 'component/link';
export let DropDownMenuItem = React.createClass({
propTypes: {
export class DropDownMenuItem extends React.Component {
static propTypes = {
href: React.PropTypes.string,
label: React.PropTypes.string,
icon: React.PropTypes.string,
onClick: React.PropTypes.func,
},
getDefaultProps: function() {
return {
iconPosition: 'left',
}
},
render: function() {
}
static defaultProps = {
iconPosition: 'left',
}
render() {
var icon = (this.props.icon ? <Icon icon={this.props.icon} fixed /> : null);
return (
@ -26,23 +26,27 @@ export let DropDownMenuItem = React.createClass({
</a>
);
}
});
}
export let DropDownMenu = React.createClass({
_isWindowClickBound: false,
_menuDiv: null,
export class DropDownMenu extends React.Component {
constructor(props) {
super(props);
getInitialState: function() {
return {
this._isWindowClickBound = false;
this._menuDiv = null;
this.state = {
menuOpen: false,
};
},
componentWillUnmount: function() {
}
componentWillUnmount() {
if (this._isWindowClickBound) {
window.removeEventListener('click', this.handleWindowClick, false);
}
},
handleMenuIconClick: function(e) {
}
handleMenuIconClick(e) {
this.setState({
menuOpen: !this.state.menuOpen,
});
@ -52,35 +56,38 @@ export let DropDownMenu = React.createClass({
e.stopPropagation();
}
return false;
},
handleMenuClick: function(e) {
}
handleMenuClick(e) {
// Event bubbles up to the menu after a link is clicked
this.setState({
menuOpen: false,
});
},
handleWindowClick: function(e) {
}
handleWindowClick(e) {
if (this.state.menuOpen &&
(!this._menuDiv || !this._menuDiv.contains(e.target))) {
this.setState({
menuOpen: false
});
}
},
render: function() {
}
render() {
if (!this.state.menuOpen && this._isWindowClickBound) {
this._isWindowClickBound = false;
window.removeEventListener('click', this.handleWindowClick, false);
}
return (
<div className="menu-container">
<Link ref={(span) => this._menuButton = span} button="text" icon="icon-ellipsis-v" onClick={this.handleMenuIconClick} />
<Link ref={(span) => this._menuButton = span} button="text" icon="icon-ellipsis-v" onClick={(event) => { this.handleMenuIconClick(event) }} />
{this.state.menuOpen
? <div ref={(div) => this._menuDiv = div} className="menu" onClick={this.handleMenuClick}>
? <div ref={(div) => this._menuDiv = div} className="menu" onClick={(event) => { this.handleMenuClick(event) }}>
{this.props.children}
</div>
: null}
</div>
);
}
});
}

View file

@ -1,8 +1,8 @@
import React from 'react';
import ReactModal from 'react-modal';
export const ModalPage = React.createClass({
render: function() {
export class ModalPage extends React.Component {
render() {
return (
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
className={(this.props.className || '') + ' modal-page'}
@ -13,6 +13,6 @@ export const ModalPage = React.createClass({
</ReactModal>
);
}
});
}
export default ModalPage;
export default ModalPage

View file

@ -1,10 +1,10 @@
import React from 'react';
import ReactModal from 'react-modal';
import {Link} from './link.js';
import Link from 'component/link';
export const Modal = React.createClass({
propTypes: {
export class Modal extends React.Component {
static propTypes = {
type: React.PropTypes.oneOf(['alert', 'confirm', 'custom']),
overlay: React.PropTypes.bool,
onConfirmed: React.PropTypes.func,
@ -13,18 +13,18 @@ export const Modal = React.createClass({
abortButtonLabel: React.PropTypes.string,
confirmButtonDisabled: React.PropTypes.bool,
abortButtonDisabled: React.PropTypes.bool,
},
getDefaultProps: function() {
return {
type: 'alert',
overlay: true,
confirmButtonLabel: 'OK',
abortButtonLabel: 'Cancel',
confirmButtonDisabled: false,
abortButtonDisabled: false,
};
},
render: function() {
}
static defaultProps = {
type: 'alert',
overlay: true,
confirmButtonLabel: 'OK',
abortButtonLabel: 'Cancel',
confirmButtonDisabled: false,
abortButtonDisabled: false,
}
render() {
return (
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
className={(this.props.className || '') + ' modal'}
@ -43,31 +43,35 @@ export const Modal = React.createClass({
</ReactModal>
);
}
});
}
export const ExpandableModal = React.createClass({
propTypes: {
export class ExpandableModal extends React.Component {
static propTypes = {
expandButtonLabel: React.PropTypes.string,
extraContent: React.PropTypes.element,
},
getDefaultProps: function() {
return {
confirmButtonLabel: 'OK',
expandButtonLabel: 'Show More...',
hideButtonLabel: 'Show Less',
}
},
getInitialState: function() {
return {
}
static defaultProps = {
confirmButtonLabel: 'OK',
expandButtonLabel: 'Show More...',
hideButtonLabel: 'Show Less',
}
constructor(props) {
super(props);
this.state = {
expanded: false,
}
},
toggleExpanded: function() {
}
toggleExpanded() {
this.setState({
expanded: !this.state.expanded,
});
},
render: function() {
}
render() {
return (
<Modal type="custom" {... this.props}>
{this.props.children}
@ -77,11 +81,11 @@ export const ExpandableModal = React.createClass({
<div className="modal__buttons">
<Link button="primary" label={this.props.confirmButtonLabel} className="modal__button" onClick={this.props.onConfirmed} />
<Link button="alt" label={!this.state.expanded ? this.props.expandButtonLabel : this.props.hideButtonLabel}
className="modal__button" onClick={this.toggleExpanded} />
className="modal__button" onClick={() => { this.toggleExpanded() }} />
</div>
</Modal>
);
}
});
}
export default Modal;

View file

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

View file

@ -0,0 +1,91 @@
import React from 'react';
import lbry from 'lbry'
import {Icon} from 'component/common';
import Modal from 'component/modal';
import rewards from 'rewards';
import Link from 'component/link'
export class RewardLink extends React.Component {
static propTypes = {
type: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool,
onRewardClaim: React.PropTypes.func,
onRewardFailure: React.PropTypes.func
}
constructor(props) {
super(props);
this.state = {
claimable: true,
pending: false,
errorMessage: null
};
}
refreshClaimable() {
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() {
this.refreshClaimable();
}
claimReward() {
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() {
if (this.props.onRewardFailure) {
this.props.onRewardFailure()
}
this.setState({
errorMessage: null
})
}
render() {
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

@ -0,0 +1,14 @@
import React from 'react';
import { connect } from 'react-redux';
import Router from './view.jsx';
import {
selectCurrentPage,
selectCurrentParams,
} from 'selectors/app.js';
const select = (state) => ({
params: selectCurrentParams(state),
currentPage: selectCurrentPage(state)
})
export default connect(select, null)(Router);

View file

@ -0,0 +1,51 @@
import React from 'react';
import SettingsPage from 'page/settings';
import HelpPage from 'page/help';
import ReportPage from 'page/report.js';
import StartPage from 'page/start.js';
import WalletPage from 'page/wallet';
import ShowPage from 'page/showPage'
import PublishPage from 'page/publish';
import DiscoverPage from 'page/discover';
import SplashScreen from 'component/splash.js';
import DeveloperPage from 'page/developer.js';
import RewardsPage from 'page/rewards.js';
import FileListDownloaded from 'page/fileListDownloaded'
import FileListPublished from 'page/fileListPublished'
import ChannelPage from 'page/channel'
import SearchPage from 'page/search'
const route = (page, routesMap) => {
const component = routesMap[page]
return component
};
const Router = (props) => {
const {
currentPage,
params,
} = props;
return route(currentPage, {
'settings': <SettingsPage {...params} />,
'help': <HelpPage {...params} />,
'report': <ReportPage {...params} />,
'downloaded': <FileListDownloaded {...params} />,
'published': <FileListPublished {...params} />,
'start': <StartPage {...params} />,
'wallet': <WalletPage {...params} />,
'send': <WalletPage {...params} />,
'receive': <WalletPage {...params} />,
'show': <ShowPage {...params} />,
'channel': <ChannelPage {...params} />,
'publish': <PublishPage {...params} />,
'developer': <DeveloperPage {...params} />,
'discover': <DiscoverPage {...params} />,
'rewards': <RewardsPage {...params} />,
'search': <SearchPage {...params} />,
})
}
export default Router

View file

@ -1,18 +1,18 @@
import React from 'react';
import lbry from '../lbry.js';
export const SnackBar = React.createClass({
export class SnackBar extends React.Component {
constructor(props) {
super(props);
_displayTime: 5, // in seconds
this._displayTime = 5; // in seconds
this._hideTimeout = null;
_hideTimeout: null,
getInitialState: function() {
return {
this.state = {
snacks: []
}
},
handleSnackReceived: function(event) {
};
}
handleSnackReceived(event) {
// if (this._hideTimeout) {
// clearTimeout(this._hideTimeout);
// }
@ -20,14 +20,17 @@ export const SnackBar = React.createClass({
let snacks = this.state.snacks;
snacks.push(event.detail);
this.setState({ snacks: snacks});
},
componentWillMount: function() {
}
componentWillMount() {
document.addEventListener('globalNotice', this.handleSnackReceived);
},
componentWillUnmount: function() {
}
componentWillUnmount() {
document.removeEventListener('globalNotice', this.handleSnackReceived);
},
render: function() {
}
render() {
if (!this.state.snacks.length) {
this._hideTimeout = null; //should be unmounting anyway, but be safe?
return null;
@ -51,7 +54,7 @@ export const SnackBar = React.createClass({
<a className="snack-bar__action" href={snack.linkTarget}>{snack.linkText}</a> : ''}
</div>
);
},
});
}
}
export default SnackBar;

View file

@ -2,21 +2,26 @@ import React from 'react';
import lbry from '../lbry.js';
import LoadScreen from './load_screen.js';
var SplashScreen = React.createClass({
propTypes: {
export class SplashScreen extends React.Component {
static propTypes = {
message: React.PropTypes.string,
onLoadDone: React.PropTypes.func,
},
getInitialState: function() {
return {
}
constructor(props) {
super(props);
this.state = {
details: 'Starting daemon',
isLagging: false,
}
},
updateStatus: function() {
lbry.status().then(this._updateStatusCallback);
},
_updateStatusCallback: function(status) {
};
}
updateStatus() {
lbry.status().then((status) => { this._updateStatusCallback(status) });
}
_updateStatusCallback(status) {
const startupStatus = status.startup_status
if (startupStatus.code == 'started') {
// Wait until we are able to resolve a name before declaring
@ -29,7 +34,6 @@ var SplashScreen = React.createClass({
});
lbry.resolve({uri: 'lbry://one'}).then(() => {
window.sessionStorage.setItem('loaded', 'y')
this.props.onLoadDone();
});
return;
@ -41,8 +45,9 @@ var SplashScreen = React.createClass({
setTimeout(() => {
this.updateStatus();
}, 500);
},
componentDidMount: function() {
}
componentDidMount() {
lbry.connect().then((isConnected) => {
if (isConnected) {
this.updateStatus();
@ -54,10 +59,11 @@ var SplashScreen = React.createClass({
})
}
})
},
render: function() {
}
render() {
return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />
}
});
}
export default SplashScreen;

View file

@ -0,0 +1,23 @@
import React from 'react'
import {
connect,
} from 'react-redux'
import {
selectCurrentPage,
selectHeaderLinks,
} from 'selectors/app'
import {
doNavigate,
} from 'actions/app'
import SubHeader from './view'
const select = (state, props) => ({
currentPage: selectCurrentPage(state),
subLinks: selectHeaderLinks(state),
})
const perform = (dispatch) => ({
navigate: (path) => dispatch(doNavigate(path)),
})
export default connect(select, perform)(SubHeader)

View file

@ -0,0 +1,29 @@
import React from 'react'
import Link from 'component/link'
const SubHeader = (props) => {
const {
subLinks,
currentPage,
navigate,
modifier,
} = props
const links = []
for(let link of Object.keys(subLinks)) {
links.push(
<Link onClick={(event) => navigate(`/${link}`, event)} key={link} className={link == currentPage ? 'sub-header-selected' : 'sub-header-unselected' }>
{subLinks[link]}
</Link>
)
}
return (
<nav className={'sub-header' + (modifier ? ' sub-header--' + modifier : '')}>
{links}
</nav>
)
}
export default SubHeader

View file

@ -1,36 +1,44 @@
import React from 'react';
export let ToolTip = React.createClass({
propTypes: {
export class ToolTip extends React.Component {
static propTypes = {
body: React.PropTypes.string.isRequired,
label: React.PropTypes.string.isRequired
},
getInitialState: function() {
return {
}
constructor(props) {
super(props);
this.state = {
showTooltip: false,
};
},
handleClick: function() {
}
handleClick() {
this.setState({
showTooltip: !this.state.showTooltip,
});
},
handleTooltipMouseOut: function() {
}
handleTooltipMouseOut() {
this.setState({
showTooltip: false,
});
},
render: function() {
}
render() {
return (
<span className={'tooltip ' + (this.props.className || '')}>
<a className="tooltip__link" onClick={this.handleClick}>
<a className="tooltip__link" onClick={() => { this.handleClick() }}>
{this.props.label}
</a>
<div className={'tooltip__body ' + (this.state.showTooltip ? '' : ' hidden')}
onMouseOut={this.handleTooltipMouseOut}>
onMouseOut={() => { this.handleTooltipMouseOut() }}>
{this.props.body}
</div>
</span>
);
}
});
}
export default ToolTip

View file

@ -0,0 +1,25 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doFetchTransactions,
} from 'actions/wallet'
import {
selectBalance,
selectTransactionItems,
selectIsFetchingTransactions,
} from 'selectors/wallet'
import TransactionList from './view'
const select = (state) => ({
fetchingTransactions: selectIsFetchingTransactions(state),
transactionItems: selectTransactionItems(state),
})
const perform = (dispatch) => ({
fetchTransactions: () => dispatch(doFetchTransactions())
})
export default connect(select, perform)(TransactionList)

View file

@ -0,0 +1,65 @@
import React from 'react';
import {
Address,
BusyMessage,
CreditAmount
} from 'component/common';
class TransactionList extends React.Component{
componentWillMount() {
this.props.fetchTransactions()
}
render() {
const {
fetchingTransactions,
transactionItems,
} = this.props
const rows = []
if (transactionItems.length > 0) {
transactionItems.forEach(function (item) {
rows.push(
<tr key={item.id}>
<td>{ (item.amount > 0 ? '+' : '' ) + item.amount }</td>
<td>{ item.date ? item.date.toLocaleDateString() : <span className="empty">(Transaction pending)</span> }</td>
<td>{ item.date ? item.date.toLocaleTimeString() : <span className="empty">(Transaction pending)</span> }</td>
<td>
<a className="button-text" href={"https://explorer.lbry.io/#!/transaction?id="+item.id}>{item.id.substr(0, 7)}</a>
</td>
</tr>
);
});
}
return (
<section className="card">
<div className="card__title-primary">
<h3>Transaction History</h3>
</div>
<div className="card__content">
{ fetchingTransactions && <BusyMessage message="Loading transactions" /> }
{ !fetchingTransactions && rows.length === 0 ? <div className="empty">You have no transactions.</div> : '' }
{ rows.length > 0 ?
<table className="table-standard table-stretch">
<thead>
<tr>
<th>Amount</th>
<th>Date</th>
<th>Time</th>
<th>Transaction</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
: ''
}
</div>
</section>
)
}
}
export default TransactionList

View file

@ -0,0 +1,19 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doDownloadUpgrade,
doSkipUpgrade,
} from 'actions/app'
import UpgradeModal from './view'
const select = (state) => ({
})
const perform = (dispatch) => ({
downloadUpgrade: () => dispatch(doDownloadUpgrade()),
skipUpgrade: () => dispatch(doSkipUpgrade()),
})
export default connect(select, perform)(UpgradeModal)

View file

@ -0,0 +1,32 @@
import React from 'react'
import {
Modal
} from 'component/modal'
import {
downloadUpgrade,
skipUpgrade
} from 'actions/app'
class UpgradeModal extends React.Component {
render() {
const {
downloadUpgrade,
skipUpgrade
} = this.props
return (
<Modal
isOpen={true}
contentLabel="Update available"
type="confirm"
confirmButtonLabel="Upgrade"
abortButtonLabel="Skip"
onConfirmed={downloadUpgrade}
onAborted={skipUpgrade}>
Your version of LBRY is out of date and may be unreliable or insecure.
</Modal>
)
}
}
export default UpgradeModal

View file

@ -0,0 +1,31 @@
import React from 'react'
import lbryuri from 'lbryuri';
import {
connect,
} from 'react-redux'
import {
makeSelectIsResolvingForUri
} from 'selectors/content'
import {
makeSelectClaimForUri,
} from 'selectors/claims'
import UriIndicator from './view'
const makeSelect = () => {
const selectClaim = makeSelectClaimForUri(),
selectIsResolving = makeSelectIsResolvingForUri();
const select = (state, props) => ({
claim: selectClaim(state, props),
isResolvingUri: selectIsResolving(state, props),
uri: lbryuri.normalize(props.uri),
})
return select
}
const perform = (dispatch) => ({
resolveUri: (uri) => dispatch(doResolveUri(uri))
})
export default connect(makeSelect, perform)(UriIndicator)

View file

@ -0,0 +1,70 @@
import React from 'react';
import {Icon} from 'component/common';
class UriIndicator extends React.Component{
componentWillMount() {
this.resolve(this.props)
}
componentWillReceiveProps(nextProps) {
this.resolve(nextProps)
}
resolve(props) {
const {
isResolvingUri,
resolveUri,
claim,
uri,
} = props
if(!isResolvingUri && claim === undefined && uri) {
resolveUri(uri)
}
}
render() {
const {
claim,
uri,
isResolvingUri
} = this.props
if (isResolvingUri) {
return <span className="empty">Validating...</span>
}
if (!claim) {
return <span className="empty">Unused</span>
}
const {
channel_name: channelName,
has_signature: hasSignature,
signature_is_valid: signatureIsValid,
} = claim
if (!hasSignature || !channelName) {
return <span className="empty">Anonymous</span>;
}
let icon, modifier;
if (signatureIsValid) {
modifier = 'valid';
} else {
icon = 'icon-times-circle';
modifier = 'invalid';
}
return (
<span>
{channelName} {' '}
{ !signatureIsValid ?
<Icon icon={icon} className={`channel-indicator__icon channel-indicator__icon--${modifier}`} /> :
'' }
</span>
)
}
}
export default UriIndicator;

View file

@ -0,0 +1,54 @@
import React from 'react'
import {
connect,
} from 'react-redux'
import {
doCloseModal,
} from 'actions/app'
import {
selectCurrentModal,
} from 'selectors/app'
import {
doPurchaseUri,
doLoadVideo,
} from 'actions/content'
import {
makeSelectMetadataForUri
} from 'selectors/claims'
import {
makeSelectFileInfoForUri,
makeSelectLoadingForUri,
makeSelectDownloadingForUri,
} from 'selectors/file_info'
import {
makeSelectCostInfoForUri,
} from 'selectors/cost_info'
import Video from './view'
const makeSelect = () => {
const selectCostInfo = makeSelectCostInfoForUri()
const selectFileInfo = makeSelectFileInfoForUri()
const selectIsLoading = makeSelectLoadingForUri()
const selectIsDownloading = makeSelectDownloadingForUri()
const selectMetadata = makeSelectMetadataForUri()
const select = (state, props) => ({
costInfo: selectCostInfo(state, props),
fileInfo: selectFileInfo(state, props),
metadata: selectMetadata(state, props),
modal: selectCurrentModal(state),
isLoading: selectIsLoading(state, props),
isDownloading: selectIsDownloading(state, props),
})
return select
}
const perform = (dispatch) => ({
loadVideo: (uri) => dispatch(doLoadVideo(uri)),
purchaseUri: (uri) => dispatch(doPurchaseUri(uri)),
closeModal: () => dispatch(doCloseModal()),
})
export default connect(makeSelect, perform)(Video)

View file

@ -0,0 +1,151 @@
import React from 'react';
import FilePrice from 'component/filePrice'
import Link from 'component/link';
import Modal from 'component/modal';
class VideoPlayButton extends React.Component {
onPurchaseConfirmed() {
this.props.closeModal()
this.props.startPlaying()
this.props.loadVideo(this.props.uri)
}
onWatchClick() {
this.props.purchaseUri(this.props.uri).then(() => {
if (!this.props.modal) {
this.props.startPlaying()
}
})
}
render() {
const {
button,
label,
className,
metadata,
metadata: {
title,
},
uri,
modal,
closeModal,
isLoading,
costInfo,
fileInfo,
} = this.props
/*
title={
isLoading ? "Video is Loading" :
!costInfo ? "Waiting on cost info..." :
fileInfo === undefined ? "Waiting on file info..." : ""
}
*/
return (<div>
<Link button={ button ? button : null }
disabled={isLoading || fileInfo === undefined || (fileInfo === null && (!costInfo || costInfo.cost === undefined))}
label={label ? label : ""}
className="video__play-button"
icon="icon-play"
onClick={this.onWatchClick.bind(this)} />
{modal}
<Modal contentLabel="Not enough credits" isOpen={modal == 'notEnoughCredits'} onConfirmed={() => { this.closeModal() }}>
You don't have enough LBRY credits to pay for this stream.
</Modal>
<Modal
type="confirm"
isOpen={modal == 'affirmPurchase'}
contentLabel="Confirm Purchase"
onConfirmed={this.onPurchaseConfirmed.bind(this)}
onAborted={closeModal}>
This will purchase <strong>{title}</strong> for <strong><FilePrice uri={uri} look="plain" /></strong> credits.
</Modal>
<Modal
isOpen={modal == 'timedOut'} onConfirmed={() => { this.closeModal() }} contentLabel="Timed Out">
Sorry, your download timed out :(
</Modal>
</div>);
}
}
const plyr = require('plyr')
class Video extends React.Component {
constructor(props) {
super(props)
this.state = { isPlaying: false }
}
startPlaying() {
this.setState({
isPlaying: true
})
}
render() {
const {
metadata,
isLoading,
isDownloading,
fileInfo,
} = this.props
const {
isPlaying = false,
} = this.state
const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0
let loadStatusMessage = ''
if (isLoading) {
loadStatusMessage = "Requesting stream... it may sit here for like 15-20 seconds in a really awkward way... we're working on it"
} else if (isDownloading) {
loadStatusMessage = "Downloading stream... not long left now!"
}
return (
<div className={"video " + this.props.className + (isPlaying ? " video--active" : " video--hidden")}>{
isPlaying || isLoading ?
(!isReadyToPlay ?
<span>this is the world's worst loading screen and we shipped our software with it anyway... <br /><br />{loadStatusMessage}</span> :
<VideoPlayer poster={metadata.thumbnail} autoplay={isPlaying} downloadPath={fileInfo.download_path} />) :
<div className="video__cover" style={{backgroundImage: 'url("' + metadata.thumbnail + '")'}}>
<VideoPlayButton startPlaying={this.startPlaying.bind(this)} {...this.props} />
</div>
}</div>
);
}
}
class VideoPlayer extends React.Component {
componentDidMount() {
const elem = this.refs.video
const {
autoplay,
downloadPath,
contentType,
} = this.props
const players = plyr.setup(elem)
if (autoplay) {
players[0].play()
}
}
render() {
const {
downloadPath,
contentType,
poster,
} = this.props
return (
<video controls id="video" ref="video" style={{backgroundImage: "url('" + poster + "')"}} >
<source src={downloadPath} type={contentType} />
</video>
)
}
}
export default Video

View file

@ -0,0 +1,25 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doCheckAddressIsMine,
doGetNewAddress,
} from 'actions/wallet'
import {
selectReceiveAddress,
selectGettingNewAddress
} from 'selectors/wallet'
import WalletPage from './view'
const select = (state) => ({
receiveAddress: selectReceiveAddress(state),
gettingNewAddress: selectGettingNewAddress(state),
})
const perform = (dispatch) => ({
checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)),
getNewAddress: () => dispatch(doGetNewAddress()),
})
export default connect(select, perform)(WalletPage)

View file

@ -0,0 +1,41 @@
import React from 'react';
import Link from 'component/link';
import {
Address
} from 'component/common';
class WalletAddress extends React.Component {
componentWillMount() {
this.props.checkAddressIsMine(this.props.receiveAddress)
}
render() {
const {
receiveAddress,
getNewAddress,
gettingNewAddress,
} = this.props
return (
<section className="card">
<div className="card__title-primary">
<h3>Wallet Address</h3>
</div>
<div className="card__content">
<Address address={receiveAddress} />
</div>
<div className="card__actions">
<Link label="Get New Address" button="primary" icon='icon-refresh' onClick={getNewAddress} disabled={gettingNewAddress} />
</div>
<div className="card__content">
<div className="help">
<p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p>
<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>
</section>
);
}
}
export default WalletAddress

View file

@ -0,0 +1,36 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doCloseModal,
} from 'actions/app'
import {
doSendDraftTransaction,
doSetDraftTransactionAmount,
doSetDraftTransactionAddress,
} from 'actions/wallet'
import {
selectCurrentModal,
} from 'selectors/app'
import {
selectDraftTransactionAmount,
selectDraftTransactionAddress,
} from 'selectors/wallet'
import WalletSend from './view'
const select = (state) => ({
modal: selectCurrentModal(state),
address: selectDraftTransactionAddress(state),
amount: selectDraftTransactionAmount(state),
})
const perform = (dispatch) => ({
closeModal: () => dispatch(doCloseModal()),
sendToAddress: () => dispatch(doSendDraftTransaction()),
setAmount: (event) => dispatch(doSetDraftTransactionAmount(event.target.value)),
setAddress: (event) => dispatch(doSetDraftTransactionAddress(event.target.value)),
})
export default connect(select, perform)(WalletSend)

View file

@ -0,0 +1,49 @@
import React from 'react';
import Link from 'component/link';
import Modal from 'component/modal';
import {
FormRow
} from 'component/form';
const WalletSend = (props) => {
const {
sendToAddress,
closeModal,
modal,
setAmount,
setAddress,
amount,
address,
} = props
return (
<section className="card">
<form onSubmit={sendToAddress}>
<div className="card__title-primary">
<h3>Send Credits</h3>
</div>
<div className="card__content">
<FormRow label="Amount" postfix="LBC" step="0.01" type="number" placeholder="1.23" size="10" onChange={setAmount} value={amount} />
</div>
<div className="card__content">
<FormRow label="Recipient Address" placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs" type="text" size="60" onChange={setAddress} value={address} />
</div>
<div className="card__actions card__actions--form-submit">
<Link button="primary" label="Send" onClick={sendToAddress} disabled={!(parseFloat(amount) > 0.0) || !address} />
<input type='submit' className='hidden' />
</div>
</form>
{modal == 'insufficientBalance' && <Modal isOpen={true} contentLabel="Insufficient balance" onConfirmed={closeModal}>
Insufficient balance: after this transaction you would have less than 1 LBC in your wallet.
</Modal>}
{modal == 'transactionSuccessful' && <Modal isOpen={true} contentLabel="Transaction successful" onConfirmed={closeModal}>
Your transaction was successfully placed in the queue.
</Modal>}
{modal == 'transactionFailed' && <Modal isOpen={true} contentLabel="Transaction failed" onConfirmed={closeModal}>
Something went wrong:
</Modal>}
</section>
)
}
export default WalletSend

View file

@ -0,0 +1,28 @@
import React from 'react'
import {
connect
} from 'react-redux'
import lbryuri from 'lbryuri.js'
import {
selectWunderBarAddress,
selectWunderBarIcon
} from 'selectors/search'
import {
doSearch,
} from 'actions/search'
import {
doNavigate,
} from 'actions/app'
import Wunderbar from './view'
const select = (state) => ({
address: selectWunderBarAddress(state),
icon: selectWunderBarIcon(state)
})
const perform = (dispatch) => ({
onSearch: (query) => dispatch(doSearch(query)),
onSubmit: (query) => dispatch(doNavigate('/show', { uri: lbryuri.normalize(query) } ))
})
export default connect(select, perform)(Wunderbar)

View file

@ -0,0 +1,160 @@
import React from 'react';
import lbryuri from 'lbryuri.js';
import {Icon} from 'component/common.js';
class WunderBar extends React.PureComponent {
static TYPING_TIMEOUT = 800
static propTypes = {
onSearch: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired
}
constructor(props) {
super(props);
this._userTypingTimer = null;
this._isSearchDispatchPending = false;
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 })
this._isSearchDispatchPending = true;
let searchQuery = event.target.value;
this._userTypingTimer = setTimeout(() => {
const hasQuery = searchQuery.length === 0;
this._resetOnNextBlur = hasQuery;
this._isSearchDispatchPending = false;
if (searchQuery) {
this.props.onSearch(searchQuery);
}
}, WunderBar.TYPING_TIMEOUT); // 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() {
if (this._isSearchDispatchPending) {
setTimeout(() => {
this.onBlur();
}, WunderBar.TYPING_TIMEOUT + 1)
} else {
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() {
if (this._input) {
const start = this._input.selectionStart,
end = this._input.selectionEnd;
this._input.value = this.state.address; //this causes cursor to go to end of input
this._input.setSelectionRange(start, end);
if (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"
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 default WunderBar;

View file

@ -0,0 +1,2 @@
module.exports = {
}

View file

@ -0,0 +1,2 @@
module.exports = {
}

View file

@ -0,0 +1,66 @@
export const CHANGE_PATH = 'CHANGE_PATH'
export const OPEN_MODAL = 'OPEN_MODAL'
export const CLOSE_MODAL = 'CLOSE_MODAL'
export const HISTORY_BACK = 'HISTORY_BACK'
export const DAEMON_READY = 'DAEMON_READY'
// Upgrades
export const UPGRADE_CANCELLED = 'UPGRADE_CANCELLED'
export const DOWNLOAD_UPGRADE = 'DOWNLOAD_UPGRADE'
export const UPGRADE_DOWNLOAD_STARTED = 'UPGRADE_DOWNLOAD_STARTED'
export const UPGRADE_DOWNLOAD_COMPLETED = 'UPGRADE_DOWNLOAD_COMPLETED'
export const UPGRADE_DOWNLOAD_PROGRESSED = 'UPGRADE_DOWNLOAD_PROGRESSED'
export const CHECK_UPGRADE_AVAILABLE = 'CHECK_UPGRADE_AVAILABLE'
export const UPDATE_VERSION = 'UPDATE_VERSION'
export const SKIP_UPGRADE = 'SKIP_UPGRADE'
export const START_UPGRADE = 'START_UPGRADE'
// Wallet
export const GET_NEW_ADDRESS_STARTED = 'GET_NEW_ADDRESS_STARTED'
export const GET_NEW_ADDRESS_COMPLETED = 'GET_NEW_ADDRESS_COMPLETED'
export const FETCH_TRANSACTIONS_STARTED = 'FETCH_TRANSACTIONS_STARTED'
export const FETCH_TRANSACTIONS_COMPLETED = 'FETCH_TRANSACTIONS_COMPLETED'
export const UPDATE_BALANCE = 'UPDATE_BALANCE'
export const CHECK_ADDRESS_IS_MINE_STARTED = 'CHECK_ADDRESS_IS_MINE_STARTED'
export const CHECK_ADDRESS_IS_MINE_COMPLETED = 'CHECK_ADDRESS_IS_MINE_COMPLETED'
export const SET_DRAFT_TRANSACTION_AMOUNT = 'SET_DRAFT_TRANSACTION_AMOUNT'
export const SET_DRAFT_TRANSACTION_ADDRESS = 'SET_DRAFT_TRANSACTION_ADDRESS'
export const SEND_TRANSACTION_STARTED = 'SEND_TRANSACTION_STARTED'
export const SEND_TRANSACTION_COMPLETED = 'SEND_TRANSACTION_COMPLETED'
export const SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED'
// Content
export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED'
export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED'
export const RESOLVE_URI_STARTED = 'RESOLVE_URI_STARTED'
export const RESOLVE_URI_COMPLETED = 'RESOLVE_URI_COMPLETED'
export const RESOLVE_URI_CANCELED = 'RESOLVE_URI_CANCELED'
export const FETCH_CHANNEL_CLAIMS_STARTED = 'FETCH_CHANNEL_CLAIMS_STARTED'
export const FETCH_CHANNEL_CLAIMS_COMPLETED = 'FETCH_CHANNEL_CLAIMS_COMPLETED'
export const CLAIM_LIST_MINE_STARTED = 'CLAIM_LIST_MINE_STARTED'
export const CLAIM_LIST_MINE_COMPLETED = 'CLAIM_LIST_MINE_COMPLETED'
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED'
export const FILE_LIST_COMPLETED = 'FILE_LIST_COMPLETED'
export const FETCH_FILE_INFO_STARTED = 'FETCH_FILE_INFO_STARTED'
export const FETCH_FILE_INFO_COMPLETED = 'FETCH_FILE_INFO_COMPLETED'
export const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED'
export const FETCH_COST_INFO_COMPLETED = 'FETCH_COST_INFO_COMPLETED'
export const LOADING_VIDEO_STARTED = 'LOADING_VIDEO_STARTED'
export const LOADING_VIDEO_COMPLETED = 'LOADING_VIDEO_COMPLETED'
export const LOADING_VIDEO_FAILED = 'LOADING_VIDEO_FAILED'
export const DOWNLOADING_STARTED = 'DOWNLOADING_STARTED'
export const DOWNLOADING_PROGRESSED = 'DOWNLOADING_PROGRESSED'
export const DOWNLOADING_COMPLETED = 'DOWNLOADING_COMPLETED'
export const PLAY_VIDEO_STARTED = 'PLAY_VIDEO_STARTED'
export const FETCH_AVAILABILITY_STARTED = 'FETCH_AVAILABILITY_STARTED'
export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED'
export const FILE_DELETE = 'FILE_DELETE'
// Search
export const SEARCH_STARTED = 'SEARCH_STARTED'
export const SEARCH_COMPLETED = 'SEARCH_COMPLETED'
export const SEARCH_CANCELLED = 'SEARCH_CANCELLED'
// Settings
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED'

View file

@ -68,6 +68,8 @@ jsonrpc.call = function (connectionString, method, params, callback, errorCallba
}));
sessionStorage.setItem('JSONRPCCounter', counter + 1);
return xhr
};
export default jsonrpc;

View file

@ -7,6 +7,20 @@ import {getLocal, getSession, setSession, setLocal} from './utils.js';
const {remote, ipcRenderer} = require('electron');
const menu = remote.require('./menu/main-menu');
let lbry = {
isConnected: false,
daemonConnectionString: 'http://localhost:5279/lbryapi',
pendingPublishTimeout: 20 * 60 * 1000,
defaultClientSettings: {
showNsfw: false,
showUnavailable: true,
debug: false,
useCustomLighthouseServers: false,
customLighthouseServers: [],
showDeveloperMenu: false,
}
};
/**
* 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.
@ -40,14 +54,14 @@ function removePendingPublishIfNeeded({name, channel_name, outpoint}) {
return pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name));
}
setLocal('pendingPublishes', getPendingPublishes().filter(pub => !pubMatches(pub)));
setLocal('pendingPublishes', lbry.getPendingPublishes().filter(pub => !pubMatches(pub)));
}
/**
* Gets the current list of pending publish attempts. Filters out any that have timed out and
* removes them from the list.
*/
function getPendingPublishes() {
lbry.getPendingPublishes = function() {
const pendingPublishes = getLocal('pendingPublishes') || [];
const newPendingPublishes = pendingPublishes.filter(pub => Date.now() - pub.time <= lbry.pendingPublishTimeout);
setLocal('pendingPublishes', newPendingPublishes);
@ -59,7 +73,7 @@ function getPendingPublishes() {
* provided along withe the name. If no pending publish is found, returns null.
*/
function getPendingPublish({name, channel_name, outpoint}) {
const pendingPublishes = getPendingPublishes();
const pendingPublishes = lbry.getPendingPublishes();
return pendingPublishes.find(
pub => pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name))
) || null;
@ -72,30 +86,9 @@ function pendingPublishToDummyClaim({channel_name, name, outpoint, claim_id, txi
function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) {
return {name, outpoint, claim_id, metadata: null};
}
window.pptdfi = pendingPublishToDummyFileInfo;
let lbry = {
isConnected: false,
rootPath: '.',
daemonConnectionString: 'http://localhost:5279/lbryapi',
webUiUri: 'http://localhost:5279',
peerListTimeout: 6000,
pendingPublishTimeout: 20 * 60 * 1000,
colors: {
primary: '#155B4A'
},
defaultClientSettings: {
showNsfw: false,
showUnavailable: true,
debug: false,
useCustomLighthouseServers: false,
customLighthouseServers: [],
showDeveloperMenu: false,
}
};
lbry.call = function (method, params, callback, errorCallback, connectFailedCallback) {
jsonrpc.call(lbry.daemonConnectionString, method, params, callback, errorCallback, connectFailedCallback);
return jsonrpc.call(lbry.daemonConnectionString, method, params, callback, errorCallback, connectFailedCallback);
}
//core
@ -131,93 +124,19 @@ lbry.connect = function() {
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) {
// Returns true/false whether the daemon is at a point it will start returning status
lbry.call('status', {}, () => callback(true), null, () => callback(false))
};
lbry.checkFirstRun = function(callback) {
lbry.call('is_first_run', {}, callback);
}
lbry.getNewAddress = function(callback) {
lbry.call('wallet_new_address', {}, callback);
}
lbry.getUnusedAddress = function(callback) {
lbry.call('wallet_unused_address', {}, callback);
}
lbry.checkAddressIsMine = function(address, callback) {
lbry.call('address_is_mine', {address: address}, callback);
}
lbry.getDaemonSettings = function(callback) {
lbry.call('get_settings', {}, callback);
}
lbry.setDaemonSettings = function(settings, callback) {
lbry.call('set_settings', settings, callback);
}
lbry.setDaemonSetting = function(setting, value, callback) {
var setSettingsArgs = {};
setSettingsArgs[setting] = value;
lbry.call('set_settings', setSettingsArgs, callback)
}
lbry.getBalance = function(callback) {
lbry.call("wallet_balance", {}, callback);
}
lbry.sendToAddress = function(amount, address, callback, errorCallback) {
lbry.call("send_amount_to_address", { "amount" : amount, "address": address }, callback, errorCallback);
}
lbry.getClaimInfo = function(name, callback) {
if (!name) {
throw new Error(`Name required.`);
}
lbry.call('get_claim_info', { name: name }, callback);
}
lbry.getMyClaim = function(name, callback) {
lbry.call('claim_list_mine', {}, (claims) => {
callback(claims.find((claim) => claim.name == name) || null);
});
}
lbry.getPeersForBlobHash = function(blobHash, callback) {
let timedOut = false;
const timeout = setTimeout(() => {
timedOut = true;
callback([]);
}, lbry.peerListTimeout);
lbry.call('peer_list', { blob_hash: blobHash }, function(peers) {
if (!timedOut) {
clearTimeout(timeout);
callback(peers);
}
});
}
/**
* Takes a LBRY URI; will first try and calculate a total cost using
* Lighthouse. If Lighthouse can't be reached, it just retrives the
@ -255,80 +174,22 @@ lbry.getCostInfo = function(uri) {
}, 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);
});
}
});
}
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) => {
if (size) {
getCost(name, size);
}
lighthouse.get_size_for_name(name).then((size) => {
if (size) {
getCost(name, size);
}
else {
getCost(name, null);
}
}, () => {
else {
getCost(name, null);
});
});
}
})
});
}
return lbry.costPromiseCache[uri];
}
lbry.getMyClaims = function(callback) {
lbry.call('get_name_claims', {}, callback);
}
lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) {
this._removedFiles.push(outpoint);
this._updateFileInfoSubscribers(outpoint);
lbry.file_delete({
outpoint: outpoint,
delete_target_file: deleteTargetFile,
}).then(callback);
}
lbry.getFileInfoWhenListed = function(name, callback, timeoutCallback, tryNum=0) {
function scheduleNextCheckOrTimeout() {
if (timeoutCallback && tryNum > 200) {
timeoutCallback();
} else {
setTimeout(() => lbry.getFileInfoWhenListed(name, callback, timeoutCallback, tryNum + 1), 250);
}
}
// Calls callback with file info when it appears in the lbrynet file manager.
// If timeoutCallback is provided, it will be called if the file fails to appear.
lbry.file_list({name: name}).then(([fileInfo]) => {
if (fileInfo) {
callback(fileInfo);
} else {
scheduleNextCheckOrTimeout();
}
}, () => scheduleNextCheckOrTimeout());
}
/**
* Publishes a file. The optional fileListedCallback is called when the file becomes available in
* lbry.file_list() during the publish process.
@ -369,10 +230,6 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall
fileListedCallback(true);
}
}, 2000);
//lbry.getFileInfoWhenListed(params.name, function(fileInfo) {
// fileListedCallback(fileInfo);
//});
}
@ -427,29 +284,10 @@ lbry.formatName = function(name) {
return name;
}
lbry.nameIsValid = function(name, checkCase=true) {
const regexp = new RegExp('^[a-z0-9-]+$', checkCase ? '' : 'i');
return regexp.test(name);
}
lbry.loadJs = function(src, type, onload)
{
var lbryScriptTag = document.getElementById('lbry'),
newScriptTag = document.createElement('script'),
type = type || 'text/javascript';
newScriptTag.src = src;
newScriptTag.type = type;
if (onload)
{
newScriptTag.onload = onload;
}
lbryScriptTag.parentNode.insertBefore(newScriptTag, lbryScriptTag);
}
lbry.imagePath = function(file)
{
return lbry.rootPath + '/img/' + file;
return 'img/' + file;
}
lbry.getMediaType = function(contentType, fileName) {
@ -480,71 +318,9 @@ lbry.stop = function(callback) {
lbry.call('stop', {}, callback);
};
lbry.fileInfo = {};
lbry._subscribeIdCount = 0;
lbry._fileInfoSubscribeCallbacks = {};
lbry._fileInfoSubscribeInterval = 500000;
lbry._balanceSubscribeCallbacks = {};
lbry._balanceSubscribeInterval = 5000;
lbry._removedFiles = [];
lbry._claimIdOwnershipCache = {};
lbry._updateClaimOwnershipCache = function(claimId) {
lbry.getMyClaims((claimInfos) => {
lbry._claimIdOwnershipCache[claimId] = !!claimInfos.reduce(function(match, claimInfo) {
return match || claimInfo.claim_id == claimId;
}, false);
});
};
lbry._updateFileInfoSubscribers = function(outpoint) {
const callSubscribedCallbacks = (outpoint, fileInfo) => {
for (let callback of Object.values(this._fileInfoSubscribeCallbacks[outpoint])) {
callback(fileInfo);
}
}
if (lbry._removedFiles.includes(outpoint)) {
callSubscribedCallbacks(outpoint, false);
} else {
lbry.file_list({
outpoint: outpoint,
full_status: true,
}).then(([fileInfo]) => {
if (fileInfo) {
if (this._claimIdOwnershipCache[fileInfo.claim_id] === undefined) {
this._updateClaimOwnershipCache(fileInfo.claim_id);
}
fileInfo.isMine = !!this._claimIdOwnershipCache[fileInfo.claim_id];
}
callSubscribedCallbacks(outpoint, fileInfo);
});
}
if (Object.keys(this._fileInfoSubscribeCallbacks[outpoint]).length) {
setTimeout(() => {
this._updateFileInfoSubscribers(outpoint);
}, lbry._fileInfoSubscribeInterval);
}
}
lbry.fileInfoSubscribe = function(outpoint, callback) {
if (!lbry._fileInfoSubscribeCallbacks[outpoint])
{
lbry._fileInfoSubscribeCallbacks[outpoint] = {};
}
const subscribeId = ++lbry._subscribeIdCount;
lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId] = callback;
lbry._updateFileInfoSubscribers(outpoint);
return subscribeId;
}
lbry.fileInfoUnsubscribe = function(outpoint, subscribeId) {
delete lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId];
}
lbry._balanceUpdateInterval = null;
lbry._updateBalanceSubscribers = function() {
@ -621,7 +397,7 @@ lbry.file_list = function(params={}) {
lbry.call('file_list', params, (fileInfos) => {
removePendingPublishIfNeeded({name, channel_name, outpoint});
const dummyFileInfos = getPendingPublishes().map(pendingPublishToDummyFileInfo);
const dummyFileInfos = lbry.getPendingPublishes().map(pendingPublishToDummyFileInfo);
resolve([...fileInfos, ...dummyFileInfos]);
}, reject, reject);
});
@ -634,56 +410,41 @@ lbry.claim_list_mine = function(params={}) {
removePendingPublishIfNeeded({name, channel_name, outpoint: txid + ':' + nout});
}
const dummyClaims = getPendingPublishes().map(pendingPublishToDummyClaim);
const dummyClaims = lbry.getPendingPublishes().map(pendingPublishToDummyClaim);
resolve([...claims, ...dummyClaims]);
}, reject, reject)
});
}
const claimCacheKey = 'resolve_claim_cache';
lbry._claimCache = getSession(claimCacheKey, {});
lbry._resolveXhrs = {}
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]);
if (params.uri && lbry._claimCache[params.uri] !== undefined) {
resolve(lbry._claimCache[params.uri]);
} else {
lbry.call('resolve', params, function(data) {
claimCache[params.uri] = data;
setSession(claimCacheKey, claimCache)
lbry._resolveXhrs[params.uri] = lbry.call('resolve', params, function(data) {
if (data !== undefined) {
lbry._claimCache[params.uri] = data;
}
setSession(claimCacheKey, lbry._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.cancelResolve = function(params={}) {
const xhr = lbry._resolveXhrs[params.uri]
if (xhr && xhr.readyState > 0 && xhr.readyState < 4) {
xhr.abort()
}
}
// 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, {
get: function(target, name) {
if (name in target) {

View file

@ -7,28 +7,25 @@ const lbryio = {
_accessToken: getLocal('accessToken'),
_authenticationPromise: null,
_user : null,
enabled: false
enabled: true
};
const CONNECTION_STRING = process.env.LBRY_APP_API_URL ? process.env.LBRY_APP_API_URL : 'https://api.lbry.io/';
const CONNECTION_STRING = 'https://api.lbry.io/';
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
lbryio._exchangePromise = null;
lbryio._exchangeLastFetched = null;
lbryio.getExchangeRates = function() {
return new Promise((resolve, reject) => {
const cached = getSession('exchangeRateCache');
if (!cached || Date.now() - cached.time > EXCHANGE_RATE_TIMEOUT) {
if (!lbryio._exchangeLastFetched || Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT) {
lbryio._exchangePromise = new Promise((resolve, reject) => {
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);
}
});
}).catch(reject);
});
lbryio._exchangeLastFetched = Date.now();
}
return lbryio._exchangePromise;
}
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

View file

@ -165,5 +165,30 @@ lbryuri.normalize= function(uri) {
return lbryuri.build({name, path, claimSequence, bidPosition, claimId});
}
lbryuri.isValid = function(uri) {
let parts
try {
parts = lbryuri.parse(lbryuri.normalize(uri))
} catch (error) {
return false;
}
return parts && parts.name;
}
lbryuri.isValidName = function(name, checkCase=true) {
const regexp = new RegExp('^[a-z0-9-]+$', checkCase ? '' : 'i');
return regexp.test(name);
}
lbryuri.isClaimable = function(uri) {
let parts
try {
parts = lbryuri.parse(lbryuri.normalize(uri))
} catch (error) {
return false;
}
return parts && parts.name && !parts.claimId && !parts.bidPosition && !parts.claimSequence && !parts.isChannel && !parts.path;
}
window.lbryuri = lbryuri;
export default lbryuri;

View file

@ -3,13 +3,29 @@ import ReactDOM from 'react-dom';
import lbry from './lbry.js';
import lbryio from './lbryio.js';
import lighthouse from './lighthouse.js';
import App from './app.js';
import SplashScreen from './component/splash.js';
import SnackBar from './component/snack-bar.js';
import {AuthOverlay} from './component/auth.js';
import App from 'component/app/index.js';
import SplashScreen from 'component/splash.js';
import SnackBar from 'component/snack-bar.js';
import {AuthOverlay} from 'component/auth.js';
import { Provider } from 'react-redux';
import store from 'store.js';
import {
doChangePath,
doNavigate,
doDaemonReady,
doHistoryPush
} from 'actions/app'
import {
doFetchDaemonSettings
} from 'actions/settings'
import {
doFileList
} from 'actions/file_info'
import parseQueryParams from 'util/query_params'
const {remote, ipcRenderer} = require('electron');
const {remote, ipcRenderer, shell} = require('electron');
const contextMenu = remote.require('./menu/context-menu');
const app = require('./app')
lbry.showMenuIfNeeded();
@ -19,31 +35,51 @@ window.addEventListener('contextmenu', (event) => {
event.preventDefault();
});
let openUri = null;
window.addEventListener('popstate', (event, param) => {
const queryString = document.location.search
const pathParts = document.location.pathname.split('/')
const route = '/' + pathParts[pathParts.length - 1]
function onOpenUriRequested(event, uri) {
/**
* If an external app requests a URI while we're still on the splash screen, we store it to
* later pass into the App component.
*/
openUri = uri;
};
ipcRenderer.on('open-uri-requested', onOpenUriRequested);
if (route.match(/html$/)) return
console.log('title should be set here, but it is not in popstate? TODO')
let init = function() {
window.lbry = lbry;
window.lighthouse = lighthouse;
let canvas = document.getElementById('canvas');
app.store.dispatch(doChangePath(`${route}${queryString}`))
})
lbry.connect().then(function(isConnected) {
lbryio.authenticate() //start auth process as soon as soon as we can get an install ID
})
ipcRenderer.on('open-uri-requested', (event, uri) => {
console.log(event)
console.log(uri)
if (uri && uri.startsWith('lbry://')) {
console.log(uri)
doNavigate('/show', { uri })
}
});
document.addEventListener('click', (event) => {
var target = event.target;
while (target && target !== document) {
if (target.matches('a[href^="http"]')) {
event.preventDefault();
shell.openExternal(target.href);
return;
}
target = target.parentNode;
}
});
const initialState = app.store.getState();
var init = function() {
function onDaemonReady() {
app.store.dispatch(doDaemonReady())
window.sessionStorage.setItem('loaded', 'y'); //once we've made it here once per session, we don't need to show splash again
ipcRenderer.removeListener('open-uri-requested', onOpenUriRequested); // <App /> will handle listening for URI requests once it's loaded
ReactDOM.render(<div>{ lbryio.enabled ? <AuthOverlay/> : '' }<App {... openUri ? {openUri: openUri} : {}} /><SnackBar /></div>, canvas)
app.store.dispatch(doHistoryPush({}, "" +
"Discover", "/discover"))
app.store.dispatch(doFetchDaemonSettings())
app.store.dispatch(doFileList())
ReactDOM.render(<Provider store={store}><div>{ lbryio.enabled ? <AuthOverlay/> : '' }<App /><SnackBar /></div></Provider>, canvas)
}
if (window.sessionStorage.getItem('loaded') == 'y') {

View file

@ -0,0 +1,30 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doFetchClaimsByChannel
} from 'actions/content'
import {
makeSelectClaimForUri,
makeSelectClaimsInChannelForUri
} from 'selectors/claims'
import ChannelPage from './view'
const makeSelect = () => {
const selectClaim = makeSelectClaimForUri(),
selectClaimsInChannel = makeSelectClaimsInChannelForUri()
const select = (state, props) => ({
claim: selectClaim(state, props),
claimsInChannel: selectClaimsInChannel(state, props)
})
return select
}
const perform = (dispatch) => ({
fetchClaims: (uri) => dispatch(doFetchClaimsByChannel(uri))
})
export default connect(makeSelect, perform)(ChannelPage)

View file

@ -0,0 +1,54 @@
import React from 'react';
import lbryuri from 'lbryuri'
import {BusyMessage} from 'component/common'
import FileTile from 'component/fileTile'
class ChannelPage extends React.Component{
componentDidMount() {
this.fetchClaims(this.props)
}
componentWillReceiveProps(nextProps) {
this.fetchClaims(nextProps)
}
fetchClaims(props) {
if (props.claimsInChannel === undefined) {
props.fetchClaims(props.uri)
}
}
render() {
const {
claimsInChannel,
claim,
uri
} = this.props
let contentList
if (claimsInChannel === undefined) {
contentList = <BusyMessage message="Fetching content" />
} else if (claimsInChannel) {
contentList = claimsInChannel.length ?
claimsInChannel.map((claim) => <FileTile key={claim.claim_id} uri={lbryuri.build({name: claim.name, claimId: claim.claim_id})} />) :
<span className="empty">No content found.</span>
}
return <main className="main--single-column">
<section className="card">
<div className="card__inner">
<div className="card__title-identity"><h1>{uri}</h1></div>
</div>
<div className="card__content">
<p>
This channel page is a stub.
</p>
</div>
</section>
<h3 className="card-row__header">Published Content</h3>
{contentList}
</main>
}
}
export default ChannelPage;

View file

@ -1,39 +1,45 @@
import lbry from '../lbry.js';
import React from 'react';
import {FormField} from '../component/form.js';
import {Link} from '../component/link.js';
import Link from '../component/link';
const fs = require('fs');
const {ipcRenderer} = require('electron');
const DeveloperPage = React.createClass({
getInitialState: function() {
return {
class DeveloperPage extends React.Component {
constructor(props) {
super(props);
this.state = {
showDeveloperMenu: lbry.getClientSetting('showDeveloperMenu'),
useCustomLighthouseServers: lbry.getClientSetting('useCustomLighthouseServers'),
customLighthouseServers: lbry.getClientSetting('customLighthouseServers').join('\n'),
upgradePath: '',
};
},
handleShowDeveloperMenuChange: function(event) {
}
handleShowDeveloperMenuChange(event) {
lbry.setClientSetting('showDeveloperMenu', event.target.checked);
lbry.showMenuIfNeeded();
this.setState({
showDeveloperMenu: event.target.checked,
});
},
handleUseCustomLighthouseServersChange: function(event) {
}
handleUseCustomLighthouseServersChange(event) {
lbry.setClientSetting('useCustomLighthouseServers', event.target.checked);
this.setState({
useCustomLighthouseServers: event.target.checked,
});
},
handleUpgradeFileChange: function(event) {
}
handleUpgradeFileChange(event) {
this.setState({
upgradePath: event.target.files[0].path,
upgradePath: event.target.value,
});
},
handleForceUpgradeClick: function() {
}
handleForceUpgradeClick() {
let upgradeSent = false;
if (!this.state.upgradePath) {
alert('Please select a file to upgrade from');
@ -51,37 +57,38 @@ const DeveloperPage = React.createClass({
alert('Failed to start upgrade. Is "' + this.state.upgradePath + '" a valid path to the upgrade?');
}
}
},
render: function() {
}
render() {
return (
<main>
<section className="card">
<h3>Developer Settings</h3>
<div className="form-row">
<label><FormField type="checkbox" onChange={this.handleShowDeveloperMenuChange} checked={this.state.showDeveloperMenu} /> Show developer menu</label>
<label><FormField type="checkbox" onChange={(event) => { this.handleShowDeveloperMenuChange() }} checked={this.state.showDeveloperMenu} /> Show developer menu</label>
</div>
<div className="form-row">
<label><FormField type="checkbox" onChange={this.handleUseCustomLighthouseServersChange} checked={this.state.useCustomLighthouseServers} /> Use custom search servers</label>
<label><FormField type="checkbox" onChange={(event) => { this.handleUseCustomLighthouseServersChange() }} checked={this.state.useCustomLighthouseServers} /> Use custom search servers</label>
</div>
{this.state.useCustomLighthouseServers
? <div className="form-row">
<label>
Custom search servers (one per line)
<div><FormField type="textarea" className="developer-page__custom-lighthouse-servers" value={this.state.customLighthouseServers} onChange={this.handleCustomLighthouseServersChange} checked={this.state.debugMode} /></div>
<div><FormField type="textarea" className="developer-page__custom-lighthouse-servers" value={this.state.customLighthouseServers} onChange={(event) => { this.handleCustomLighthouseServersChange() }} checked={this.state.debugMode} /></div>
</label>
</div>
: null}
</section>
<section className="card">
<div className="form-row">
<FormField name="file" ref="file" type="file" onChange={this.handleUpgradeFileChange}/>
<FormField name="file" ref="file" type="file" onChange={(event) => { this.handleUpgradeFileChange() }}/>
&nbsp;
<Link label="Force Upgrade" button="alt" onClick={this.handleForceUpgradeClick} />
<Link label="Force Upgrade" button="alt" onClick={(event) => { this.handleForceUpgradeClick() }} />
</div>
</section>
</main>
);
}
});
}
export default DeveloperPage;

View file

@ -1,63 +0,0 @@
import React from 'react';
import lbryio from '../lbryio.js';
import {FileTile, FileTileStream} from '../component/file-tile.js';
import {ToolTip} from '../component/tooltip.js';
const communityCategoryToolTipText = ('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 ' +
'"five" to put your content here!');
let FeaturedCategory = React.createClass({
render: function() {
return (<div className="card-row card-row--small">
{ this.props.category ?
<h3 className="card-row__header">{this.props.category}
{ this.props.category.match(/^community/i) ?
<ToolTip label="What's this?" body={communityCategoryToolTipText} className="tooltip--header"/>
: '' }</h3>
: '' }
{ this.props.names.map((name) => { return <FileTile key={name} displayStyle="card" uri={name} /> }) }
</div>)
}
})
let DiscoverPage = React.createClass({
getInitialState: function() {
return {
featuredUris: {},
failed: false
};
},
componentWillMount: function() {
lbryio.call('discover', 'list', { version: "early-access" } ).then(({Categories, Uris}) => {
let featuredUris = {}
Categories.forEach((category) => {
if (Uris[category] && Uris[category].length) {
featuredUris[category] = Uris[category]
}
})
this.setState({ featuredUris: featuredUris });
}, () => {
this.setState({
failed: true
})
});
},
render: function() {
return <main>{
this.state.failed ?
<div className="empty">Failed to load landing content.</div> :
<div>
{
Object.keys(this.state.featuredUris).map((category) => {
return this.state.featuredUris[category].length ?
<FeaturedCategory key={category} category={category} names={this.state.featuredUris[category]} /> :
'';
})
}
</div>
}</main>;
}
});
export default DiscoverPage;

View file

@ -0,0 +1,23 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doFetchFeaturedUris,
} from 'actions/content'
import {
selectFeaturedUris,
selectFetchingFeaturedUris,
} from 'selectors/content'
import DiscoverPage from './view'
const select = (state) => ({
featuredUris: selectFeaturedUris(state),
fetchingFeaturedUris: selectFetchingFeaturedUris(state),
})
const perform = (dispatch) => ({
fetchFeaturedUris: () => dispatch(doFetchFeaturedUris())
})
export default connect(select, perform)(DiscoverPage)

View file

@ -0,0 +1,59 @@
import React from 'react';
import lbryio from 'lbryio.js';
import lbryuri from 'lbryuri'
import FileCard from 'component/fileCard';
import {BusyMessage} from 'component/common.js';
import ToolTip from 'component/tooltip.js';
const communityCategoryToolTipText = ('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 ' +
'"five" to put your content here!');
const FeaturedCategory = (props) => {
const {
category,
names,
} = props
return <div className="card-row card-row--small">
<h3 className="card-row__header">{category}
{category && category.match(/^community/i) && <ToolTip label="What's this?" body={communityCategoryToolTipText} className="tooltip--header" />}
</h3>
{names && names.map(name => <FileCard key={name} displayStyle="card" uri={lbryuri.normalize(name)} />)}
</div>
}
class DiscoverPage extends React.Component{
componentWillMount() {
this.props.fetchFeaturedUris()
}
render() {
const {
featuredUris,
fetchingFeaturedUris,
} = this.props
let content
if (fetchingFeaturedUris) {
content = <BusyMessage message="Fetching content" />
} else {
if (typeof featuredUris === "object") {
content = Object.keys(featuredUris).map(category => {
return featuredUris[category].length ?
<FeaturedCategory key={category} category={category} names={featuredUris[category]} /> :
'';
})
} else if (featuredUris !== undefined) {
content = <div className="empty">Failed to load landing content.</div>
}
}
return (
<main>{content}</main>
)
}
}
export default DiscoverPage;

View file

@ -1,218 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import lbryuri from '../lbryuri.js';
import {Link} from '../component/link.js';
import {FormField} from '../component/form.js';
import {SubHeader} from '../component/header.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';
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({
_isMounted: false,
getInitialState: function() {
return {
fileInfos: null,
};
},
componentDidMount: function() {
this._isMounted = true;
lbry.claim_list_mine().then((myClaimInfos) => {
if (!this._isMounted) { return; }
lbry.file_list().then((fileInfos) => {
if (!this._isMounted) { return; }
const myClaimOutpoints = myClaimInfos.map(({txid, nout}) => txid + ':' + nout);
this.setState({
fileInfos: fileInfos.filter(({outpoint}) => !myClaimOutpoints.includes(outpoint)),
});
});
});
},
componentWillUnmount: function() {
this._isMounted = false;
},
render: function() {
let content = "";
if (this.state.fileInfos === null) {
content = <BusyMessage message="Loading" />;
} else if (!this.state.fileInfos.length) {
content = <span>You haven't downloaded anything from LBRY yet. Go <Link href="?discover" label="search for your first download" />!</span>;
} else {
content = <FileList fileInfos={this.state.fileInfos} hidePrices={true} />;
}
return (
<main className="main--single-column">
<FileListNav viewingPage="downloaded" />
{content}
</main>
);
}
});
export let FileListPublished = React.createClass({
_isMounted: false,
getInitialState: function () {
return {
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 () {
this._isMounted = true;
this._requestPublishReward();
lbry.claim_list_mine().then((claimInfos) => {
if (!this._isMounted) { return; }
lbry.file_list().then((fileInfos) => {
if (!this._isMounted) { return; }
const myClaimOutpoints = claimInfos.map(({txid, nout}) => txid + ':' + nout);
this.setState({
fileInfos: fileInfos.filter(({outpoint}) => myClaimOutpoints.includes(outpoint)),
});
});
});
},
componentWillUnmount: function() {
this._isMounted = false;
},
render: function () {
let content = null;
if (this.state.fileInfos === null) {
content = <BusyMessage message="Loading" />;
}
else if (!this.state.fileInfos.length) {
content = <span>You haven't published anything to LBRY yet. Try <Link href="?publish" label="publishing" />!</span>;
}
else {
content = <FileList fileInfos={this.state.fileInfos} />;
}
return (
<main className="main--single-column">
<FileListNav viewingPage="published" />
{content}
</main>
);
}
});
export let FileList = React.createClass({
_sortFunctions: {
date: function(fileInfos) {
return fileInfos.slice().reverse();
},
title: function(fileInfos) {
return fileInfos.slice().sort(function(fileInfo1, fileInfo2) {
const title1 = fileInfo1.metadata ? fileInfo1.metadata.title.toLowerCase() : fileInfo1.name;
const title2 = fileInfo2.metadata ? fileInfo2.metadata.title.toLowerCase() : fileInfo2.name;
if (title1 < title2) {
return -1;
} else if (title1 > title2) {
return 1;
} else {
return 0;
}
});
},
filename: function(fileInfos) {
return fileInfos.slice().sort(function({file_name: fileName1}, {file_name: fileName2}) {
const fileName1Lower = fileName1.toLowerCase();
const fileName2Lower = fileName2.toLowerCase();
if (fileName1Lower < fileName2Lower) {
return -1;
} else if (fileName2Lower > fileName1Lower) {
return 1;
} else {
return 0;
}
});
},
},
propTypes: {
fileInfos: React.PropTypes.array.isRequired,
hidePrices: React.PropTypes.bool,
},
getDefaultProps: function() {
return {
hidePrices: false,
};
},
getInitialState: function() {
return {
sortBy: 'date',
};
},
handleSortChanged: function(event) {
this.setState({
sortBy: event.target.value,
});
},
render: function() {
var content = [],
seenUris = {};
const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos);
for (let {outpoint, name, channel_name, metadata, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) {
if (seenUris[name] || !claim_id) {
continue;
}
let streamMetadata;
if (metadata) {
streamMetadata = metadata.stream.metadata;
} else {
streamMetadata = null;
}
const uri = lbryuri.build({contentName: name, channelName: channel_name});
seenUris[name] = true;
content.push(<FileTileStream key={outpoint} outpoint={outpoint} uri={uri} hideOnRemove={true}
hidePrice={this.props.hidePrices} metadata={streamMetadata} contentType={mime_type}
hasSignature={has_signature} signatureIsValid={signature_is_valid} />);
}
return (
<section>
<span className='sort-section'>
Sort by { ' ' }
<FormField type="select" onChange={this.handleSortChanged}>
<option value="date">Date</option>
<option value="title">Title</option>
<option value="filename">File name</option>
</FormField>
</span>
{content}
</section>
);
}
});

View file

@ -0,0 +1,27 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doFetchFileInfosAndPublishedClaims,
} from 'actions/file_info'
import {
selectFileInfosDownloaded,
selectFileListDownloadedOrPublishedIsPending,
} from 'selectors/file_info'
import {
doNavigate,
} from 'actions/app'
import FileListDownloaded from './view'
const select = (state) => ({
fileInfos: selectFileInfosDownloaded(state),
isPending: selectFileListDownloadedOrPublishedIsPending(state),
})
const perform = (dispatch) => ({
navigate: (path) => dispatch(doNavigate(path)),
fetchFileInfosDownloaded: () => dispatch(doFetchFileInfosAndPublishedClaims()),
})
export default connect(select, perform)(FileListDownloaded)

View file

@ -0,0 +1,45 @@
import React from 'react';
import lbry from 'lbry.js';
import lbryuri from 'lbryuri.js';
import Link from 'component/link';
import {FormField} from 'component/form.js';
import {FileTile} from 'component/fileTile';
import rewards from 'rewards.js';
import lbryio from 'lbryio.js';
import {BusyMessage, Thumbnail} from 'component/common.js';
import FileList from 'component/fileList'
import SubHeader from 'component/subHeader'
class FileListDownloaded extends React.Component {
componentWillMount() {
this.props.fetchFileInfosDownloaded()
}
render() {
const {
fileInfos,
isPending,
navigate,
} = this.props
let content
if (fileInfos && fileInfos.length > 0) {
content = <FileList fileInfos={fileInfos} fetching={isPending} />
} else {
if (isPending) {
content = <BusyMessage message="Loading" />
} else {
content = <span>You haven't downloaded anything from LBRY yet. Go <Link onClick={() => navigate('/discover')} label="search for your first download" />!</span>
}
}
return (
<main className="main--single-column">
<SubHeader />
{content}
</main>
)
}
}
export default FileListDownloaded

View file

@ -0,0 +1,27 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doFetchFileInfosAndPublishedClaims,
} from 'actions/file_info'
import {
selectFileInfosPublished,
selectFileListDownloadedOrPublishedIsPending
} from 'selectors/file_info'
import {
doNavigate,
} from 'actions/app'
import FileListPublished from './view'
const select = (state) => ({
fileInfos: selectFileInfosPublished(state),
isPending: selectFileListDownloadedOrPublishedIsPending(state),
})
const perform = (dispatch) => ({
navigate: (path) => dispatch(doNavigate(path)),
fetchFileListPublished: () => dispatch(doFetchFileInfosAndPublishedClaims()),
})
export default connect(select, perform)(FileListPublished)

View file

@ -0,0 +1,67 @@
import React from 'react';
import lbry from 'lbry.js';
import lbryuri from 'lbryuri.js';
import Link from 'component/link';
import {FormField} from 'component/form.js';
import FileTile from 'component/fileTile';
import rewards from 'rewards.js';
import lbryio from 'lbryio.js';
import {BusyMessage, Thumbnail} from 'component/common.js';
import FileList from 'component/fileList'
import SubHeader from 'component/subHeader'
class FileListPublished extends React.Component {
componentWillMount() {
this.props.fetchFileListPublished()
}
componentDidUpdate() {
if(this.props.fileInfos.length > 0) this._requestPublishReward()
}
_requestPublishReward() {
// TODO this is throwing an error now
// Error: LBRY internal API is disabled
//
// 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(() => {})
// }
// })
}
render() {
const {
fileInfos,
isPending,
navigate,
} = this.props
let content
if (fileInfos && fileInfos.length > 0) {
content = <FileList fileInfos={fileInfos} fetching={isPending} fileTileShowEmpty={FileTile.SHOW_EMPTY_PENDING} />
} else {
if (isPending) {
content = <BusyMessage message="Loading" />
} else {
content = <span>You haven't downloaded anything from LBRY yet. Go <Link onClick={() => navigate('/discover')} label="search for your first download" />!</span>
}
}
return (
<main className="main--single-column">
<SubHeader />
{content}
</main>
)
}
}
export default FileListPublished

View file

@ -0,0 +1,47 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doNavigate,
} from 'actions/app'
import {
doFetchFileInfo,
} from 'actions/file_info'
import {
makeSelectFileInfoForUri,
} from 'selectors/file_info'
import {
makeSelectClaimForUri,
makeSelectContentTypeForUri,
makeSelectMetadataForUri,
} from 'selectors/claims'
import {
makeSelectCostInfoForUri,
} from 'selectors/cost_info'
import FilePage from './view'
const makeSelect = () => {
const selectClaim = makeSelectClaimForUri(),
selectContentType = makeSelectContentTypeForUri(),
selectFileInfo = makeSelectFileInfoForUri(),
selectCostInfo = makeSelectCostInfoForUri(),
selectMetadata = makeSelectMetadataForUri()
const select = (state, props) => ({
claim: selectClaim(state, props),
contentType: selectContentType(state, props),
costInfo: selectCostInfo(state, props),
metadata: selectMetadata(state, props),
fileInfo: selectFileInfo(state, props)
})
return select
}
const perform = (dispatch) => ({
navigate: (path, params) => dispatch(doNavigate(path, params)),
fetchFileInfo: (uri) => dispatch(doFetchFileInfo(uri))
})
export default connect(makeSelect, perform)(FilePage)

View file

@ -0,0 +1,128 @@
import React from 'react';
import lbry from 'lbry.js';
import lbryuri from 'lbryuri.js';
import Video from 'component/video'
import {
Thumbnail,
} from 'component/common';
import FilePrice from 'component/filePrice'
import FileActions from 'component/fileActions';
import Link from 'component/link';
import UriIndicator from 'component/uriIndicator';
const FormatItem = (props) => {
const {
contentType,
metadata: {
author,
language,
license,
}
} = props
const mediaType = lbry.getMediaType(contentType);
return (
<table className="table-standard">
<tbody>
<tr>
<td>Content-Type</td><td>{mediaType}</td>
</tr>
<tr>
<td>Author</td><td>{author}</td>
</tr>
<tr>
<td>Language</td><td>{language}</td>
</tr>
<tr>
<td>License</td><td>{license}</td>
</tr>
</tbody>
</table>
)
}
class FilePage extends React.Component{
componentDidMount() {
this.fetchFileInfo(this.props)
}
componentWillReceiveProps(nextProps) {
this.fetchFileInfo(nextProps)
}
fetchFileInfo(props) {
if (props.fileInfo === undefined) {
props.fetchFileInfo(props.uri)
}
}
render() {
const {
claim,
fileInfo,
metadata,
contentType,
uri,
} = this.props
if (!claim || !metadata) {
return <span className="empty">Empty claim or metadata info.</span>
}
const {
txid,
nout,
channel_name: channelName,
has_signature: hasSignature,
signature_is_valid: signatureIsValid,
value
} = claim
const outpoint = txid + ':' + nout
const title = metadata.title
const channelClaimId = claim.value && claim.value.publisherSignature ? claim.value.publisherSignature.certificateId : null;
const channelUri = signatureIsValid && hasSignature && channelName ? lbryuri.build({channelName, claimId: channelClaimId}, false) : null
const uriIndicator = <UriIndicator uri={uri} />
return (
<main className="main--single-column">
<section className="show-page-media">
{ contentType && contentType.startsWith('video/') ?
<Video className="video-embedded" uri={uri} /> :
(metadata && metadata.thumbnail ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) }
</section>
<section className="card">
<div className="card__inner">
<div className="card__title-identity">
{!fileInfo || fileInfo.written_bytes <= 0
? <span style={{float: "right"}}><FilePrice uri={lbryuri.normalize(uri)} /></span>
: null}<h1>{title}</h1>
<div className="card__subtitle">
{ channelUri ?
<Link onClick={() => this.props.navigate('/show', { uri: channelUri })}>{uriIndicator}</Link> :
uriIndicator}
</div>
<div className="card__actions">
<FileActions uri={uri} />
</div>
</div>
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
{metadata && metadata.description}
</div>
</div>
{ metadata ?
<div className="card__content">
<FormatItem metadata={metadata} contentType={contentType} />
</div> : '' }
<div className="card__content">
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
</div>
</section>
</main>
)
}
}
export default FilePage;

7
ui/js/page/help/index.js Normal file
View file

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

View file

@ -1,19 +1,21 @@
//@TODO: Customize advice based on OS
//@TODO: Customize advice based on OS
import React from 'react';
import lbry from '../lbry.js';
import {Link} from '../component/link.js';
import {SettingsNav} from './settings.js';
import {version as uiVersion} from 'json!../../package.json';
import lbry from 'lbry.js';
import Link from 'component/link';
import SubHeader from 'component/subHeader'
import {version as uiVersion} from 'json!../../../package.json';
var HelpPage = React.createClass({
getInitialState: function() {
return {
class HelpPage extends React.Component {
constructor(props) {
super(props);
this.state = {
versionInfo: null,
lbryId: null,
};
},
componentWillMount: function() {
}
componentWillMount() {
lbry.getVersionInfo((info) => {
this.setState({
versionInfo: info,
@ -24,8 +26,9 @@ var HelpPage = React.createClass({
lbryId: info.lbry_id,
});
});
},
render: function() {
}
render() {
let ver, osName, platform, newVerLink;
if (this.state.versionInfo) {
ver = this.state.versionInfo;
@ -48,7 +51,7 @@ var HelpPage = React.createClass({
return (
<main className="main--single-column">
<SettingsNav viewingPage="help" />
<SubHeader />
<section className="card">
<div className="card__title-primary">
<h3>Read the FAQ</h3>
@ -75,48 +78,48 @@ var HelpPage = React.createClass({
<div className="card__title-primary"><h3>Report a Bug</h3></div>
<div className="card__content">
<p>Did you find something wrong?</p>
<p><Link href="?report" label="Submit a Bug Report" icon="icon-bug" button="alt" /></p>
<p><Link onClick={() => navigate('report')} label="Submit a Bug Report" icon="icon-bug" button="alt" /></p>
<div className="meta">Thanks! LBRY is made by its users.</div>
</div>
</section>
{!ver ? null :
<section className="card">
<div className="card__title-primary"><h3>About</h3></div>
<div className="card__content">
{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>Your copy of LBRY is up to date.</p>
}
<table className="table-standard">
<tbody>
<tr>
<th>daemon (lbrynet)</th>
<td>{ver.lbrynet_version}</td>
</tr>
<tr>
<th>wallet (lbryum)</th>
<td>{ver.lbryum_version}</td>
</tr>
<tr>
<th>interface</th>
<td>{uiVersion}</td>
</tr>
<tr>
<th>Platform</th>
<td>{platform}</td>
</tr>
<tr>
<th>Installation ID</th>
<td>{this.state.lbryId}</td>
</tr>
</tbody>
</table>
</div>
</section>
<section className="card">
<div className="card__title-primary"><h3>About</h3></div>
<div className="card__content">
{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>Your copy of LBRY is up to date.</p>
}
<table className="table-standard">
<tbody>
<tr>
<th>daemon (lbrynet)</th>
<td>{ver.lbrynet_version}</td>
</tr>
<tr>
<th>wallet (lbryum)</th>
<td>{ver.lbryum_version}</td>
</tr>
<tr>
<th>interface</th>
<td>{uiVersion}</td>
</tr>
<tr>
<th>Platform</th>
<td>{platform}</td>
</tr>
<tr>
<th>Installation ID</th>
<td>{this.state.lbryId}</td>
</tr>
</tbody>
</table>
</div>
</section>
}
</main>
);
}
});
}
export default HelpPage;
export default HelpPage;

View file

@ -0,0 +1,23 @@
import React from 'react'
import {
connect,
} from 'react-redux'
import {
doNavigate,
doHistoryBack,
} from 'actions/app'
import {
selectMyClaims
} from 'selectors/claims'
import PublishPage from './view'
const select = (state) => ({
myClaims: selectMyClaims(state)
})
const perform = (dispatch) => ({
back: () => dispatch(doHistoryBack()),
navigate: (path) => dispatch(doNavigate(path)),
})
export default connect(select, perform)(PublishPage)

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