format with prettier

This commit is contained in:
Sean Yesmunt 2017-06-05 21:21:55 -07:00
parent 6f93834990
commit c8b949f020
124 changed files with 8463 additions and 7383 deletions

View file

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

View file

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

View file

@ -1,294 +1,288 @@
import * as types from 'constants/action_types' import * as types from 'constants/action_types';
import lbry from 'lbry' import lbry from 'lbry';
import lbryio from 'lbryio' import lbryio from 'lbryio';
import lbryuri from 'lbryuri' import lbryuri from 'lbryuri';
import rewards from 'rewards' import rewards from 'rewards';
import { selectBalance } from 'selectors/wallet';
import { import {
selectBalance, selectFileInfoForUri,
} from 'selectors/wallet' selectUrisDownloading
import { } from 'selectors/file_info';
selectFileInfoForUri, import { selectResolvingUris } from 'selectors/content';
selectUrisDownloading, import { selectCostInfoForUri } from 'selectors/cost_info';
} from 'selectors/file_info' import { selectClaimsByUri } from 'selectors/claims';
import { import { doOpenModal } from 'actions/app';
selectResolvingUris
} from 'selectors/content'
import {
selectCostInfoForUri,
} from 'selectors/cost_info'
import {
selectClaimsByUri,
} from 'selectors/claims'
import {
doOpenModal,
} from 'actions/app'
export function doResolveUri(uri) { export function doResolveUri(uri) {
return function(dispatch, getState) { return function(dispatch, getState) {
uri = lbryuri.normalize(uri);
uri = lbryuri.normalize(uri) const state = getState();
const alreadyResolving = selectResolvingUris(state).indexOf(uri) !== -1;
const state = getState() if (!alreadyResolving) {
const alreadyResolving = selectResolvingUris(state).indexOf(uri) !== -1 dispatch({
type: types.RESOLVE_URI_STARTED,
data: { uri }
});
if (!alreadyResolving) { lbry.resolve({ uri }).then(resolutionInfo => {
dispatch({ const { claim, certificate } = resolutionInfo
type: types.RESOLVE_URI_STARTED, ? resolutionInfo
data: { uri } : { claim: null, certificate: null };
})
lbry.resolve({ uri }).then((resolutionInfo) => { dispatch({
const { type: types.RESOLVE_URI_COMPLETED,
claim, data: {
certificate, uri,
} = resolutionInfo ? resolutionInfo : { claim : null, certificate: null } claim,
certificate
dispatch({ }
type: types.RESOLVE_URI_COMPLETED, });
data: { });
uri, }
claim, };
certificate,
}
})
})
}
}
} }
export function doCancelResolveUri(uri) { export function doCancelResolveUri(uri) {
return function(dispatch, getState) { return function(dispatch, getState) {
lbry.cancelResolve({ uri }) lbry.cancelResolve({ uri });
dispatch({ dispatch({
type: types.RESOLVE_URI_CANCELED, type: types.RESOLVE_URI_CANCELED,
data: { uri } data: { uri }
}) });
} };
} }
export function doFetchFeaturedUris() { export function doFetchFeaturedUris() {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState() const state = getState();
dispatch({ dispatch({
type: types.FETCH_FEATURED_CONTENT_STARTED, type: types.FETCH_FEATURED_CONTENT_STARTED
}) });
const success = ({ Categories, Uris }) => { const success = ({ Categories, Uris }) => {
let featuredUris = {};
let featuredUris = {} Categories.forEach(category => {
if (Uris[category] && Uris[category].length) {
featuredUris[category] = Uris[category];
}
});
Categories.forEach((category) => { dispatch({
if (Uris[category] && Uris[category].length) { type: types.FETCH_FEATURED_CONTENT_COMPLETED,
featuredUris[category] = Uris[category] data: {
} categories: Categories,
}) uris: featuredUris
}
});
};
dispatch({ const failure = () => {
type: types.FETCH_FEATURED_CONTENT_COMPLETED, dispatch({
data: { type: types.FETCH_FEATURED_CONTENT_COMPLETED,
categories: Categories, data: {
uris: featuredUris, categories: [],
} uris: {}
}) }
} });
};
const failure = () => { lbryio
dispatch({ .call('discover', 'list', { version: 'early-access' })
type: types.FETCH_FEATURED_CONTENT_COMPLETED, .then(success, failure);
data: { };
categories: [],
uris: {}
}
})
}
lbryio.call('discover', 'list', { version: "early-access" } )
.then(success, failure)
}
} }
export function doUpdateLoadStatus(uri, outpoint) { export function doUpdateLoadStatus(uri, outpoint) {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState() const state = getState();
lbry.file_list({ lbry
outpoint: outpoint, .file_list({
full_status: true, outpoint: outpoint,
}).then(([fileInfo]) => { full_status: true
if(!fileInfo || fileInfo.written_bytes == 0) { })
// download hasn't started yet .then(([fileInfo]) => {
setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)) }, 250) if (!fileInfo || fileInfo.written_bytes == 0) {
} else if (fileInfo.completed) { // download hasn't started yet
// TODO this isn't going to get called if they reload the client before setTimeout(() => {
// the download finished dispatch(doUpdateLoadStatus(uri, outpoint));
dispatch({ }, 250);
type: types.DOWNLOADING_COMPLETED, } else if (fileInfo.completed) {
data: { // TODO this isn't going to get called if they reload the client before
uri, // the download finished
outpoint, dispatch({
fileInfo, type: types.DOWNLOADING_COMPLETED,
} data: {
}) uri,
} else { outpoint,
// ready to play fileInfo
const { }
total_bytes, });
written_bytes, } else {
} = fileInfo // ready to play
const progress = (written_bytes / total_bytes) * 100 const { total_bytes, written_bytes } = fileInfo;
const progress = written_bytes / total_bytes * 100;
dispatch({ dispatch({
type: types.DOWNLOADING_PROGRESSED, type: types.DOWNLOADING_PROGRESSED,
data: { data: {
uri, uri,
outpoint, outpoint,
fileInfo, fileInfo,
progress, progress
} }
}) });
setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)) }, 250) setTimeout(() => {
} dispatch(doUpdateLoadStatus(uri, outpoint));
}) }, 250);
} }
});
};
} }
export function doDownloadFile(uri, streamInfo) { export function doDownloadFile(uri, streamInfo) {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState() const state = getState();
lbry.file_list({ outpoint: streamInfo.outpoint, full_status: true }).then(([fileInfo]) => { lbry
dispatch({ .file_list({ outpoint: streamInfo.outpoint, full_status: true })
type: types.DOWNLOADING_STARTED, .then(([fileInfo]) => {
data: { dispatch({
uri, type: types.DOWNLOADING_STARTED,
outpoint: streamInfo.outpoint, data: {
fileInfo, uri,
} outpoint: streamInfo.outpoint,
}) fileInfo
}
});
dispatch(doUpdateLoadStatus(uri, streamInfo.outpoint)) dispatch(doUpdateLoadStatus(uri, streamInfo.outpoint));
}) });
lbryio.call('file', 'view', { lbryio
uri: uri, .call('file', 'view', {
outpoint: streamInfo.outpoint, uri: uri,
claim_id: streamInfo.claim_id, outpoint: streamInfo.outpoint,
}).catch(() => {}) claim_id: streamInfo.claim_id
})
.catch(() => {});
rewards.claimEligiblePurchaseRewards() rewards.claimEligiblePurchaseRewards();
};
}
} }
export function doLoadVideo(uri) { export function doLoadVideo(uri) {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState() const state = getState();
dispatch({ dispatch({
type: types.LOADING_VIDEO_STARTED, type: types.LOADING_VIDEO_STARTED,
data: { data: {
uri uri
} }
}) });
lbry.get({ uri }).then(streamInfo => { lbry.get({ uri }).then(streamInfo => {
const timeout = streamInfo === null || const timeout =
typeof streamInfo !== 'object' || streamInfo === null ||
streamInfo.error == 'Timeout' typeof streamInfo !== 'object' ||
streamInfo.error == 'Timeout';
if(timeout) { if (timeout) {
dispatch({ dispatch({
type: types.LOADING_VIDEO_FAILED, type: types.LOADING_VIDEO_FAILED,
data: { uri } data: { uri }
}) });
dispatch(doOpenModal('timedOut')) dispatch(doOpenModal('timedOut'));
} else { } else {
dispatch(doDownloadFile(uri, streamInfo)) dispatch(doDownloadFile(uri, streamInfo));
} }
}) });
} };
} }
export function doPurchaseUri(uri, purchaseModalName) { export function doPurchaseUri(uri, purchaseModalName) {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState() const state = getState();
const balance = selectBalance(state) const balance = selectBalance(state);
const fileInfo = selectFileInfoForUri(state, { uri }) const fileInfo = selectFileInfoForUri(state, { uri });
const downloadingByUri = selectUrisDownloading(state) const downloadingByUri = selectUrisDownloading(state);
const alreadyDownloading = !!downloadingByUri[uri] const alreadyDownloading = !!downloadingByUri[uri];
// we already fully downloaded the file. // we already fully downloaded the file.
if (fileInfo && fileInfo.completed) { if (fileInfo && fileInfo.completed) {
// If written_bytes is false that means the user has deleted/moved the // If written_bytes is false that means the user has deleted/moved the
// file manually on their file system, so we need to dispatch a // file manually on their file system, so we need to dispatch a
// doLoadVideo action to reconstruct the file from the blobs // doLoadVideo action to reconstruct the file from the blobs
if (!fileInfo.written_bytes) dispatch(doLoadVideo(uri)) if (!fileInfo.written_bytes) dispatch(doLoadVideo(uri));
return Promise.resolve() return Promise.resolve();
} }
// we are already downloading the file // we are already downloading the file
if (alreadyDownloading) { if (alreadyDownloading) {
return Promise.resolve() return Promise.resolve();
} }
const costInfo = selectCostInfoForUri(state, { uri }) const costInfo = selectCostInfoForUri(state, { uri });
const { cost } = costInfo const { cost } = costInfo;
// the file is free or we have partially downloaded it // the file is free or we have partially downloaded it
if (cost <= 0.01 || (fileInfo && fileInfo.download_directory)) { if (cost <= 0.01 || (fileInfo && fileInfo.download_directory)) {
dispatch(doLoadVideo(uri)) dispatch(doLoadVideo(uri));
return Promise.resolve() return Promise.resolve();
} }
if (cost > balance) { if (cost > balance) {
dispatch(doOpenModal('notEnoughCredits')) dispatch(doOpenModal('notEnoughCredits'));
} else { } else {
dispatch(doOpenModal(purchaseModalName)) dispatch(doOpenModal(purchaseModalName));
} }
return Promise.resolve() return Promise.resolve();
} };
} }
export function doFetchClaimsByChannel(uri) { export function doFetchClaimsByChannel(uri) {
return function(dispatch, getState) { return function(dispatch, getState) {
dispatch({ dispatch({
type: types.FETCH_CHANNEL_CLAIMS_STARTED, type: types.FETCH_CHANNEL_CLAIMS_STARTED,
data: { uri } data: { uri }
}) });
lbry.resolve({ uri }).then((resolutionInfo) => { lbry.resolve({ uri }).then(resolutionInfo => {
const { const { claims_in_channel } = resolutionInfo
claims_in_channel, ? resolutionInfo
} = resolutionInfo ? resolutionInfo : { claims_in_channel: [] } : { claims_in_channel: [] };
dispatch({ dispatch({
type: types.FETCH_CHANNEL_CLAIMS_COMPLETED, type: types.FETCH_CHANNEL_CLAIMS_COMPLETED,
data: { data: {
uri, uri,
claims: claims_in_channel claims: claims_in_channel
} }
}) });
}) });
} };
} }
export function doFetchClaimListMine() { export function doFetchClaimListMine() {
return function(dispatch, getState) { return function(dispatch, getState) {
dispatch({ dispatch({
type: types.FETCH_CLAIM_LIST_MINE_STARTED type: types.FETCH_CLAIM_LIST_MINE_STARTED
}) });
lbry.claim_list_mine().then(claims => {
lbry.claim_list_mine().then((claims) => { dispatch({
dispatch({ type: types.FETCH_CLAIM_LIST_MINE_COMPLETED,
type: types.FETCH_CLAIM_LIST_MINE_COMPLETED, data: {
data: { claims
claims }
} });
}) });
}) };
}
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,20 +3,26 @@ import lbry from './lbry.js';
const env = ENV; const env = ENV;
const config = require(`./config/${env}`); const config = require(`./config/${env}`);
const language = lbry.getClientSetting('language') ? lbry.getClientSetting('language') : 'en'; const language = lbry.getClientSetting('language')
const i18n = require('y18n')({directory: 'app/locales', updateFiles: false, locale: language}); ? lbry.getClientSetting('language')
: 'en';
const i18n = require('y18n')({
directory: 'app/locales',
updateFiles: false,
locale: language
});
const logs = []; const logs = [];
const app = { const app = {
env: env, env: env,
config: config, config: config,
store: store, store: store,
i18n: i18n, i18n: i18n,
logs: logs, logs: logs,
log: function(message) { log: function(message) {
console.log(message); console.log(message);
logs.push(message); logs.push(message);
} }
} };
window.__ = i18n.__; window.__ = i18n.__;
window.__n = i18n.__n; window.__n = i18n.__n;

View file

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

View file

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

View file

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

View file

@ -3,155 +3,202 @@ import lbry from '../lbry.js';
//component/icon.js //component/icon.js
export class Icon extends React.Component { export class Icon extends React.Component {
static propTypes = { static propTypes = {
icon: React.PropTypes.string.isRequired, icon: React.PropTypes.string.isRequired,
className: React.PropTypes.string, className: React.PropTypes.string,
fixed: React.PropTypes.bool, fixed: React.PropTypes.bool
} };
render() { render() {
const {fixed, className} = this.props; const { fixed, className } = this.props;
const spanClassName = ('icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') + const spanClassName =
this.props.icon + ' ' + (this.props.className || '')); 'icon ' +
return <span className={spanClassName}></span> ('fixed' in this.props ? 'icon-fixed-width ' : '') +
} this.props.icon +
' ' +
(this.props.className || '');
return <span className={spanClassName} />;
}
} }
export class TruncatedText extends React.Component { export class TruncatedText extends React.Component {
static propTypes = { static propTypes = {
lines: React.PropTypes.number, lines: React.PropTypes.number
} };
static defaultProps = { static defaultProps = {
lines: null lines: null
} };
render() { render() {
return <span className="truncated-text" style={{ WebkitLineClamp: this.props.lines }}>{this.props.children}</span>; return (
} <span
className="truncated-text"
style={{ WebkitLineClamp: this.props.lines }}
>
{this.props.children}
</span>
);
}
} }
export class BusyMessage extends React.Component { export class BusyMessage extends React.Component {
static propTypes = { static propTypes = {
message: React.PropTypes.string, message: React.PropTypes.string
} };
render() { render() {
return <span>{this.props.message} <span className="busy-indicator"></span></span> return (
} <span>{this.props.message} <span className="busy-indicator" /></span>
);
}
} }
export class CurrencySymbol extends React.Component { export class CurrencySymbol extends React.Component {
render() { render() {
return <span>LBC</span>; return <span>LBC</span>;
} }
} }
export class CreditAmount extends React.Component { export class CreditAmount extends React.Component {
static propTypes = { static propTypes = {
amount: React.PropTypes.number.isRequired, amount: React.PropTypes.number.isRequired,
precision: React.PropTypes.number, precision: React.PropTypes.number,
isEstimate: React.PropTypes.bool, isEstimate: React.PropTypes.bool,
label: React.PropTypes.bool, label: React.PropTypes.bool,
showFree: React.PropTypes.bool, showFree: React.PropTypes.bool,
look: React.PropTypes.oneOf(['indicator', 'plain']), look: React.PropTypes.oneOf(['indicator', 'plain'])
} };
static defaultProps = { static defaultProps = {
precision: 1, precision: 1,
label: true, label: true,
showFree: false, showFree: false,
look: 'indicator', look: 'indicator'
} };
render() { render() {
const formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision); const formattedAmount = lbry.formatCredits(
let amountText; this.props.amount,
if (this.props.showFree && parseFloat(formattedAmount) == 0) { this.props.precision
amountText = __('free'); );
} else if (this.props.label) { let amountText;
amountText = formattedAmount + ' ' + (parseFloat(formattedAmount) == 1 ? __('credit') : __('credits')); if (this.props.showFree && parseFloat(formattedAmount) == 0) {
} else { amountText = __('free');
amountText = formattedAmount; } else if (this.props.label) {
} amountText =
formattedAmount +
' ' +
(parseFloat(formattedAmount) == 1 ? __('credit') : __('credits'));
} else {
amountText = formattedAmount;
}
return ( return (
<span className={`credit-amount credit-amount--${this.props.look}`}> <span className={`credit-amount credit-amount--${this.props.look}`}>
<span> <span>
{amountText} {amountText}
</span> </span>
{ this.props.isEstimate ? <span className="credit-amount__estimate" title={__("This is an estimate and does not include data fees")}>*</span> : null } {this.props.isEstimate
</span> ? <span
); className="credit-amount__estimate"
} title={__('This is an estimate and does not include data fees')}
>
*
</span>
: null}
</span>
);
}
} }
let addressStyle = { let addressStyle = {
fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace', fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace'
}; };
export class Address extends React.Component { export class Address extends React.Component {
static propTypes = { static propTypes = {
address: React.PropTypes.string, address: React.PropTypes.string
} };
constructor(props) { constructor(props) {
super(props); super(props);
this._inputElem = null; this._inputElem = null;
} }
render() { render() {
return ( return (
<input className="input-copyable" type="text" ref={(input) => { this._inputElem = input; }} <input
onFocus={() => { this._inputElem.select(); }} style={addressStyle} readOnly="readonly" value={this.props.address}></input> className="input-copyable"
); type="text"
} ref={input => {
this._inputElem = input;
}}
onFocus={() => {
this._inputElem.select();
}}
style={addressStyle}
readOnly="readonly"
value={this.props.address}
/>
);
}
} }
export class Thumbnail extends React.Component { export class Thumbnail extends React.Component {
static propTypes = { static propTypes = {
src: React.PropTypes.string, src: React.PropTypes.string
} };
handleError() { handleError() {
if (this.state.imageUrl != this._defaultImageUri) { if (this.state.imageUrl != this._defaultImageUri) {
this.setState({ this.setState({
imageUri: this._defaultImageUri, imageUri: this._defaultImageUri
}); });
} }
} }
constructor(props) { constructor(props) {
super(props); super(props);
this._defaultImageUri = lbry.imagePath('default-thumb.svg') this._defaultImageUri = lbry.imagePath('default-thumb.svg');
this._maxLoadTime = 10000 this._maxLoadTime = 10000;
this._isMounted = false this._isMounted = false;
this.state = { this.state = {
imageUri: this.props.src || this._defaultImageUri, imageUri: this.props.src || this._defaultImageUri
}; };
} }
componentDidMount() { componentDidMount() {
this._isMounted = true; this._isMounted = true;
setTimeout(() => { setTimeout(() => {
if (this._isMounted && !this.refs.img.complete) { if (this._isMounted && !this.refs.img.complete) {
this.setState({ this.setState({
imageUri: this._defaultImageUri, imageUri: this._defaultImageUri
}); });
} }
}, this._maxLoadTime); }, this._maxLoadTime);
} }
componentWillUnmount() { componentWillUnmount() {
this._isMounted = false; this._isMounted = false;
} }
render() { render() {
const className = this.props.className ? this.props.className : '', const className = this.props.className ? this.props.className : '',
otherProps = Object.assign({}, this.props) otherProps = Object.assign({}, this.props);
delete otherProps.className; 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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,151 +1,222 @@
import React from 'react'; import React from 'react';
import {Icon,BusyMessage} from 'component/common'; import { Icon, BusyMessage } from 'component/common';
import FilePrice from 'component/filePrice' import FilePrice from 'component/filePrice';
import {Modal} from 'component/modal'; import { Modal } from 'component/modal';
import {FormField} from 'component/form'; import { FormField } from 'component/form';
import Link from 'component/link'; import Link from 'component/link';
import {ToolTip} from 'component/tooltip'; import { ToolTip } from 'component/tooltip';
import {DropDownMenu, DropDownMenuItem} from 'component/menu'; import { DropDownMenu, DropDownMenuItem } from 'component/menu';
class FileActions extends React.Component { class FileActions extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props);
this.state = { this.state = {
forceShowActions: false, forceShowActions: false,
deleteChecked: false, deleteChecked: false
} };
} }
componentWillMount() { componentWillMount() {
this.checkAvailability(this.props.uri) this.checkAvailability(this.props.uri);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.checkAvailability(nextProps.uri) this.checkAvailability(nextProps.uri);
} }
checkAvailability(uri) { checkAvailability(uri) {
if (!this._uri || uri !== this._uri) { if (!this._uri || uri !== this._uri) {
this._uri = uri; this._uri = uri;
this.props.checkAvailability(uri) this.props.checkAvailability(uri);
} }
} }
onShowFileActionsRowClicked() { onShowFileActionsRowClicked() {
this.setState({ this.setState({
forceShowActions: true, forceShowActions: true
}); });
} }
handleDeleteCheckboxClicked(event) { handleDeleteCheckboxClicked(event) {
this.setState({ this.setState({
deleteChecked: event.target.checked, deleteChecked: event.target.checked
}) });
} }
onAffirmPurchase() { onAffirmPurchase() {
this.props.closeModal() this.props.closeModal();
this.props.loadVideo(this.props.uri) this.props.loadVideo(this.props.uri);
} }
render() { render() {
const { const {
fileInfo, fileInfo,
isAvailable, isAvailable,
platform, platform,
downloading, downloading,
uri, uri,
deleteFile, deleteFile,
openInFolder, openInFolder,
openInShell, openInShell,
modal, modal,
openModal, openModal,
closeModal, closeModal,
startDownload, startDownload,
costInfo, costInfo,
loading, loading
} = this.props } = this.props;
const deleteChecked = this.state.deleteChecked, const deleteChecked = this.state.deleteChecked,
metadata = fileInfo ? fileInfo.metadata : null, metadata = fileInfo ? fileInfo.metadata : null,
openInFolderMessage = platform.startsWith('Mac') ? __('Open in Finder') : __('Open in Folder'), openInFolderMessage = platform.startsWith('Mac')
showMenu = fileInfo && Object.keys(fileInfo).length > 0, ? __('Open in Finder')
title = metadata ? metadata.title : uri; : __('Open in Folder'),
showMenu = fileInfo && Object.keys(fileInfo).length > 0,
title = metadata ? metadata.title : uri;
let content let content;
if (loading || downloading) { if (loading || 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>
);
const content = (
progress = (fileInfo && fileInfo.written_bytes) ? fileInfo.written_bytes / fileInfo.total_bytes * 100 : 0, <div className="faux-button-block file-actions__download-status-bar button-set-item">
label = fileInfo ? progress.toFixed(0) + __('% complete') : __('Connecting...'), <div
labelWithIcon = <span className="button__content"><Icon icon="icon-download" /><span>{label}</span></span>; 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) {
if (!costInfo) {
content = <BusyMessage message={__('Fetching cost info')} />;
} else {
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?');
}
content = <div className="faux-button-block file-actions__download-status-bar button-set-item"> return (
<div className="faux-button-block file-actions__download-status-bar-overlay" style={{ width: progress + '%' }}>{labelWithIcon}</div> <section className="file-actions">
{labelWithIcon} {content}
</div> {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={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>
} else if (!fileInfo && isAvailable === undefined) { <label>
<FormField
content = <BusyMessage message={__("Checking availability")} /> type="checkbox"
checked={deleteChecked}
} else if (!fileInfo && !isAvailable && !this.state.forceShowActions) { onClick={this.handleDeleteCheckboxClicked.bind(this)}
/>
content = <div> {' '}{__('Delete this file from my computer')}
<div className="button-set-item empty">{__("Content unavailable.")}</div> </label>
<ToolTip label={__("Why?")} </Modal>
body={__("The content on LBRY is hosted by its users. It appears there are no users connected that have this file at the moment.")} </section>
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) {
if (!costInfo) {
content = <BusyMessage message={__("Fetching cost info")} />
} else {
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?');
}
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={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 export default FileActions;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,74 +3,74 @@ import lbry from 'lbry';
import lbryio from 'lbryio'; import lbryio from 'lbryio';
import lbryuri from 'lbryuri'; import lbryuri from 'lbryuri';
import lighthouse from 'lighthouse'; import lighthouse from 'lighthouse';
import FileTile from 'component/fileTile' import FileTile from 'component/fileTile';
import Link from 'component/link' import Link from 'component/link';
import {ToolTip} from 'component/tooltip.js'; import { ToolTip } from 'component/tooltip.js';
import {BusyMessage} from 'component/common.js'; import { BusyMessage } from 'component/common.js';
const SearchNoResults = (props) => { const SearchNoResults = props => {
const { const { navigate, query } = props;
navigate,
query,
} = props
return <section> return (
<span className="empty"> <section>
{__("No one has checked anything in for %s yet."), query} { ' ' } <span className="empty">
<Link label={__("Be the first")} onClick={() => navigate('/publish')} /> {(__('No one has checked anything in for %s yet.'), query)} {' '}
</span> <Link label={__('Be the first')} onClick={() => navigate('/publish')} />
</section>; </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 (
<div>
{isSearching &&
!results &&
<BusyMessage message={__('Looking up the Dewey Decimals')} />}
{isSearching &&
results &&
<BusyMessage message={__('Refreshing the Dewey Decimals')} />}
{results && !!results.length
? <FileListSearchResults {...this.props} />
: <SearchNoResults {...this.props} />}
</div>
);
}
} }
const FileListSearchResults = (props) => { export default FileListSearch;
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 (
<div>
{isSearching && !results &&
<BusyMessage message={__("Looking up the Dewey Decimals")} />}
{isSearching && results &&
<BusyMessage message={__("Refreshing the Dewey Decimals")} />}
{(results && !!results.length) ?
<FileListSearchResults {...this.props} /> :
<SearchNoResults {...this.props} />}
</div>
)
}
}
export default FileListSearch

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,47 +1,53 @@
import React from 'react'; import React from 'react';
import {Icon} from 'component/common.js'; import { Icon } from 'component/common.js';
const Link = (props) => { const Link = props => {
const { const {
href, href,
title, title,
onClick, onClick,
style, style,
label, label,
icon, icon,
badge, badge,
button, button,
hidden, hidden,
disabled, disabled,
children, children
} = props } = props;
const className = (props.className || '') + const className =
(!props.className && !props.button ? 'button-text' : '') + // Non-button links get the same look as text buttons (props.className || '') +
(props.button ? ' button-block button-' + props.button + ' button-set-item' : '') + (!props.className && !props.button ? 'button-text' : '') + // Non-button links get the same look as text buttons
(props.disabled ? ' disabled' : ''); (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>
);
}
let content; return (
if (children) { <a
content = children className={className}
} else { href={href || 'javascript:;'}
content = ( title={title}
<span {... 'button' in props ? {className: 'button__content'} : {}}> onClick={onClick}
{'icon' in props ? <Icon icon={icon} fixed={true} /> : null} {...('style' in props ? { style: style } : {})}
{label ? <span className="link-label">{label}</span> : null} >
{'badge' in props ? <span className="badge">{badge}</span> : null} {content}
</span> </a>
) );
} };
return ( export default Link;
<a className={className} href={href || 'javascript:;'} title={title}
onClick={onClick}
{... 'style' in props ? {style: style} : {}}>
{content}
</a>
);
}
export default Link

View file

@ -1,46 +1,54 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import {BusyMessage, Icon} from './common.js'; import { BusyMessage, Icon } from './common.js';
import Link from 'component/link' import Link from 'component/link';
class LoadScreen extends React.Component { class LoadScreen extends React.Component {
static propTypes = { static propTypes = {
message: React.PropTypes.string.isRequired, message: React.PropTypes.string.isRequired,
details: React.PropTypes.string, details: React.PropTypes.string,
isWarning: React.PropTypes.bool, isWarning: React.PropTypes.bool
} };
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
message: null, message: null,
details: null, details: null,
isLagging: false, isLagging: false
}; };
} }
static defaultProps = { static defaultProps = {
isWarning: false, isWarning: false
} };
render() { render() {
const imgSrc = lbry.imagePath('lbry-white-485x160.png'); const imgSrc = lbry.imagePath('lbry-white-485x160.png');
return ( return (
<div className="load-screen"> <div className="load-screen">
<img src={imgSrc} alt="LBRY"/> <img src={imgSrc} alt="LBRY" />
<div className="load-screen__message"> <div className="load-screen__message">
<h3> <h3>
{!this.props.isWarning ? {!this.props.isWarning
<BusyMessage message={this.props.message} /> : ? <BusyMessage message={this.props.message} />
<span><Icon icon="icon-warning" />{' ' + this.props.message}</span> } : <span>
</h3> <Icon icon="icon-warning" />{' ' + this.props.message}
<span className={'load-screen__details ' + (this.props.isWarning ? 'load-screen__details--warning' : '')}>{this.props.details}</span> </span>}
</div> </h3>
</div> <span
); className={
} 'load-screen__details ' +
(this.props.isWarning ? 'load-screen__details--warning' : '')
}
>
{this.props.details}
</span>
</div>
</div>
);
}
} }
export default LoadScreen; export default LoadScreen;

View file

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

View file

@ -2,17 +2,20 @@ import React from 'react';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
export class ModalPage extends React.Component { export class ModalPage extends React.Component {
render() { render() {
return ( return (
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props} <ReactModal
className={(this.props.className || '') + ' modal-page'} onCloseRequested={this.props.onAborted || this.props.onConfirmed}
overlayClassName="modal-overlay"> {...this.props}
<div className="modal-page__content"> className={(this.props.className || '') + ' modal-page'}
{this.props.children} overlayClassName="modal-overlay"
</div> >
</ReactModal> <div className="modal-page__content">
); {this.props.children}
} </div>
</ReactModal>
);
}
} }
export default ModalPage export default ModalPage;

View file

@ -1,91 +1,123 @@
import React from 'react'; import React from 'react';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
import Link from 'component/link'; import Link from 'component/link';
import app from '../app.js' import app from '../app.js';
export class Modal extends React.Component { export class Modal extends React.Component {
static propTypes = { static propTypes = {
type: React.PropTypes.oneOf(['alert', 'confirm', 'custom']), type: React.PropTypes.oneOf(['alert', 'confirm', 'custom']),
overlay: React.PropTypes.bool, overlay: React.PropTypes.bool,
onConfirmed: React.PropTypes.func, onConfirmed: React.PropTypes.func,
onAborted: React.PropTypes.func, onAborted: React.PropTypes.func,
confirmButtonLabel: React.PropTypes.string, confirmButtonLabel: React.PropTypes.string,
abortButtonLabel: React.PropTypes.string, abortButtonLabel: React.PropTypes.string,
confirmButtonDisabled: React.PropTypes.bool, confirmButtonDisabled: React.PropTypes.bool,
abortButtonDisabled: React.PropTypes.bool, abortButtonDisabled: React.PropTypes.bool
} };
static defaultProps = { static defaultProps = {
type: 'alert', type: 'alert',
overlay: true, overlay: true,
confirmButtonLabel: app.i18n.__('OK'), confirmButtonLabel: app.i18n.__('OK'),
abortButtonLabel: app.i18n.__('Cancel'), abortButtonLabel: app.i18n.__('Cancel'),
confirmButtonDisabled: false, confirmButtonDisabled: false,
abortButtonDisabled: false, abortButtonDisabled: false
} };
render() { render() {
return ( return (
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props} <ReactModal
className={(this.props.className || '') + ' modal'} onCloseRequested={this.props.onAborted || this.props.onConfirmed}
overlayClassName={![null, undefined, ""].includes(this.props.overlayClassName) ? this.props.overlayClassName : 'modal-overlay'}> {...this.props}
<div> className={(this.props.className || '') + ' modal'}
{this.props.children} overlayClassName={
</div> ![null, undefined, ''].includes(this.props.overlayClassName)
{this.props.type == 'custom' // custom modals define their own buttons ? this.props.overlayClassName
? null : 'modal-overlay'
: <div className="modal__buttons"> }
<Link button="primary" label={this.props.confirmButtonLabel} className="modal__button" disabled={this.props.confirmButtonDisabled} onClick={this.props.onConfirmed} /> >
{this.props.type == 'confirm' <div>
? <Link button="alt" label={this.props.abortButtonLabel} className="modal__button" disabled={this.props.abortButtonDisabled} onClick={this.props.onAborted} /> {this.props.children}
: null} </div>
</div>} {this.props.type == 'custom' // custom modals define their own buttons
</ReactModal> ? null
); : <div className="modal__buttons">
} <Link
button="primary"
label={this.props.confirmButtonLabel}
className="modal__button"
disabled={this.props.confirmButtonDisabled}
onClick={this.props.onConfirmed}
/>
{this.props.type == 'confirm'
? <Link
button="alt"
label={this.props.abortButtonLabel}
className="modal__button"
disabled={this.props.abortButtonDisabled}
onClick={this.props.onAborted}
/>
: null}
</div>}
</ReactModal>
);
}
} }
export class ExpandableModal extends React.Component { export class ExpandableModal extends React.Component {
static propTypes = { static propTypes = {
expandButtonLabel: React.PropTypes.string, expandButtonLabel: React.PropTypes.string,
extraContent: React.PropTypes.element, extraContent: React.PropTypes.element
} };
static defaultProps = { static defaultProps = {
confirmButtonLabel: app.i18n.__('OK'), confirmButtonLabel: app.i18n.__('OK'),
expandButtonLabel: app.i18n.__('Show More...'), expandButtonLabel: app.i18n.__('Show More...'),
hideButtonLabel: app.i18n.__('Show Less'), hideButtonLabel: app.i18n.__('Show Less')
} };
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
expanded: false, expanded: false
} };
} }
toggleExpanded() { toggleExpanded() {
this.setState({ this.setState({
expanded: !this.state.expanded, expanded: !this.state.expanded
}); });
} }
render() { render() {
return ( return (
<Modal type="custom" {... this.props}> <Modal type="custom" {...this.props}>
{this.props.children} {this.props.children}
{this.state.expanded {this.state.expanded ? this.props.extraContent : null}
? this.props.extraContent <div className="modal__buttons">
: null} <Link
<div className="modal__buttons"> button="primary"
<Link button="primary" label={this.props.confirmButtonLabel} className="modal__button" onClick={this.props.onConfirmed} /> label={this.props.confirmButtonLabel}
<Link button="alt" label={!this.state.expanded ? this.props.expandButtonLabel : this.props.hideButtonLabel} className="modal__button"
className="modal__button" onClick={() => { this.toggleExpanded() }} /> onClick={this.props.onConfirmed}
</div> />
</Modal> <Link
); button="alt"
} label={
!this.state.expanded
? this.props.expandButtonLabel
: this.props.hideButtonLabel
}
className="modal__button"
onClick={() => {
this.toggleExpanded();
}}
/>
</div>
</Modal>
);
}
} }
export default Modal; export default Modal;

View file

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

View file

@ -1,91 +1,109 @@
import React from 'react'; import React from 'react';
import lbry from 'lbry' import lbry from 'lbry';
import {Icon} from 'component/common'; import { Icon } from 'component/common';
import Modal from 'component/modal'; import Modal from 'component/modal';
import rewards from 'rewards'; import rewards from 'rewards';
import Link from 'component/link' import Link from 'component/link';
export class RewardLink extends React.Component { export class RewardLink extends React.Component {
static propTypes = { static propTypes = {
type: React.PropTypes.string.isRequired, type: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool, claimed: React.PropTypes.bool,
onRewardClaim: React.PropTypes.func, onRewardClaim: React.PropTypes.func,
onRewardFailure: React.PropTypes.func onRewardFailure: React.PropTypes.func
} };
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
claimable: true, claimable: true,
pending: false, pending: false,
errorMessage: null errorMessage: null
}; };
} }
refreshClaimable() { refreshClaimable() {
switch(this.props.type) { switch (this.props.type) {
case 'new_user': case 'new_user':
this.setState({ claimable: true }); this.setState({ claimable: true });
return; return;
case 'first_publish': case 'first_publish':
lbry.claim_list_mine().then((list) => { lbry.claim_list_mine().then(list => {
this.setState({ this.setState({
claimable: list.length > 0 claimable: list.length > 0
}) });
}); });
return; return;
} }
} }
componentWillMount() { componentWillMount() {
this.refreshClaimable(); this.refreshClaimable();
} }
claimReward() { claimReward() {
this.setState({ this.setState({
pending: true pending: true
}) });
rewards.claimReward(this.props.type).then((reward) => { rewards
this.setState({ .claimReward(this.props.type)
pending: false, .then(reward => {
errorMessage: null this.setState({
}) pending: false,
if (this.props.onRewardClaim) { errorMessage: null
this.props.onRewardClaim(reward); });
} if (this.props.onRewardClaim) {
}).catch((error) => { this.props.onRewardClaim(reward);
this.setState({ }
errorMessage: error.message, })
pending: false .catch(error => {
}) this.setState({
}) errorMessage: error.message,
} pending: false
});
});
}
clearError() { clearError() {
if (this.props.onRewardFailure) { if (this.props.onRewardFailure) {
this.props.onRewardFailure() this.props.onRewardFailure();
} }
this.setState({ this.setState({
errorMessage: null errorMessage: null
}) });
} }
render() { render() {
return ( return (
<div className="reward-link"> <div className="reward-link">
{this.props.claimed {this.props.claimed
? <span><Icon icon="icon-check" /> {__("Reward claimed.")}</span> ? <span><Icon icon="icon-check" /> {__('Reward claimed.')}</span>
: <Link button={this.props.button ? this.props.button : 'alt'} disabled={this.state.pending || !this.state.claimable } : <Link
label={ this.state.pending ? __("Claiming...") : __("Claim Reward")} onClick={() => { this.claimReward() }} />} button={this.props.button ? this.props.button : 'alt'}
{this.state.errorMessage ? disabled={this.state.pending || !this.state.claimable}
<Modal isOpen={true} contentLabel={__("Reward Claim Error")} className="error-modal" onConfirmed={() => { this.clearError() }}> label={
{this.state.errorMessage} this.state.pending ? __('Claiming...') : __('Claim Reward')
</Modal> }
: ''} onClick={() => {
</div> 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

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

View file

@ -4,48 +4,44 @@ import HelpPage from 'page/help';
import ReportPage from 'page/report.js'; import ReportPage from 'page/report.js';
import StartPage from 'page/start.js'; import StartPage from 'page/start.js';
import WalletPage from 'page/wallet'; import WalletPage from 'page/wallet';
import ShowPage from 'page/showPage' import ShowPage from 'page/showPage';
import PublishPage from 'page/publish'; import PublishPage from 'page/publish';
import DiscoverPage from 'page/discover'; import DiscoverPage from 'page/discover';
import SplashScreen from 'component/splash.js'; import SplashScreen from 'component/splash.js';
import DeveloperPage from 'page/developer.js'; import DeveloperPage from 'page/developer.js';
import RewardsPage from 'page/rewards.js'; import RewardsPage from 'page/rewards.js';
import FileListDownloaded from 'page/fileListDownloaded' import FileListDownloaded from 'page/fileListDownloaded';
import FileListPublished from 'page/fileListPublished' import FileListPublished from 'page/fileListPublished';
import ChannelPage from 'page/channel' import ChannelPage from 'page/channel';
import SearchPage from 'page/search' import SearchPage from 'page/search';
const route = (page, routesMap) => { const route = (page, routesMap) => {
const component = routesMap[page] const component = routesMap[page];
return component return component;
}; };
const Router = props => {
const { currentPage, params } = props;
const Router = (props) => { return route(currentPage, {
const { settings: <SettingsPage {...params} />,
currentPage, help: <HelpPage {...params} />,
params, report: <ReportPage {...params} />,
} = props; 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} />
});
};
return route(currentPage, { export default Router;
'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,23 +1,16 @@
import React from 'react' import React from 'react';
import { import { connect } from 'react-redux';
connect, import { doNavigate, doRemoveSnackBarSnack } from 'actions/app';
} from 'react-redux' import { selectSnackBarSnacks } from 'selectors/app';
import { import SnackBar from './view';
doNavigate,
doRemoveSnackBarSnack,
} from 'actions/app'
import {
selectSnackBarSnacks,
} from 'selectors/app'
import SnackBar from './view'
const perform = (dispatch) => ({ const perform = dispatch => ({
navigate: (path) => dispatch(doNavigate(path)), navigate: path => dispatch(doNavigate(path)),
removeSnack: () => dispatch(doRemoveSnackBarSnack()), removeSnack: () => dispatch(doRemoveSnackBarSnack())
}) });
const select = (state) => ({ const select = state => ({
snacks: selectSnackBarSnacks(state), snacks: selectSnackBarSnacks(state)
}) });
export default connect(select, perform)(SnackBar) export default connect(select, perform)(SnackBar);

View file

@ -1,49 +1,45 @@
import React from 'react'; import React from 'react';
import Link from 'component/link' import Link from 'component/link';
class SnackBar extends React.Component { class SnackBar extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this._displayTime = 5; // in seconds this._displayTime = 5; // in seconds
this._hideTimeout = null; this._hideTimeout = null;
} }
render() { render() {
const { const { navigate, snacks, removeSnack } = this.props;
navigate,
snacks,
removeSnack,
} = this.props
if (!snacks.length) { if (!snacks.length) {
this._hideTimeout = null; //should be unmounting anyway, but be safe? this._hideTimeout = null; //should be unmounting anyway, but be safe?
return null; return null;
} }
const snack = snacks[0]; const snack = snacks[0];
const { const { message, linkText, linkTarget } = snack;
message,
linkText,
linkTarget,
} = snack
if (this._hideTimeout === null) { if (this._hideTimeout === null) {
this._hideTimeout = setTimeout(() => { this._hideTimeout = setTimeout(() => {
this._hideTimeout = null; this._hideTimeout = null;
removeSnack() removeSnack();
}, this._displayTime * 1000); }, this._displayTime * 1000);
} }
return ( return (
<div className="snack-bar"> <div className="snack-bar">
{message} {message}
{linkText && linkTarget && {linkText &&
<Link onClick={() => navigate(linkTarget)} className="snack-bar__action" label={linkText} /> linkTarget &&
} <Link
</div> onClick={() => navigate(linkTarget)}
); className="snack-bar__action"
} label={linkText}
/>}
</div>
);
}
} }
export default SnackBar; export default SnackBar;

View file

@ -3,67 +3,80 @@ import lbry from '../lbry.js';
import LoadScreen from './load_screen.js'; import LoadScreen from './load_screen.js';
export class SplashScreen extends React.Component { export class SplashScreen extends React.Component {
static propTypes = { static propTypes = {
message: React.PropTypes.string, message: React.PropTypes.string,
onLoadDone: React.PropTypes.func, onLoadDone: React.PropTypes.func
} };
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
details: __('Starting daemon'), details: __('Starting daemon'),
message: __("Connecting"), message: __('Connecting'),
isLagging: false, isLagging: false
}; };
} }
updateStatus() { updateStatus() {
lbry.status().then((status) => { this._updateStatusCallback(status) }); lbry.status().then(status => {
} this._updateStatusCallback(status);
});
}
_updateStatusCallback(status) { _updateStatusCallback(status) {
const startupStatus = status.startup_status const startupStatus = status.startup_status;
if (startupStatus.code == 'started') { if (startupStatus.code == 'started') {
// Wait until we are able to resolve a name before declaring // Wait until we are able to resolve a name before declaring
// that we are done. // that we are done.
// TODO: This is a hack, and the logic should live in the daemon // TODO: This is a hack, and the logic should live in the daemon
// to give us a better sense of when we are actually started // to give us a better sense of when we are actually started
this.setState({ this.setState({
message: __("Testing Network"), message: __('Testing Network'),
details: __("Waiting for name resolution"), details: __('Waiting for name resolution'),
isLagging: false isLagging: false
}); });
lbry.resolve({uri: "lbry://one"}).then(() => { lbry.resolve({ uri: 'lbry://one' }).then(() => {
this.props.onLoadDone(); this.props.onLoadDone();
}); });
return; return;
} }
this.setState({ this.setState({
details: startupStatus.message + (startupStatus.is_lagging ? '' : '...'), details: startupStatus.message + (startupStatus.is_lagging ? '' : '...'),
isLagging: startupStatus.is_lagging, isLagging: startupStatus.is_lagging
}); });
setTimeout(() => { setTimeout(() => {
this.updateStatus(); this.updateStatus();
}, 500); }, 500);
} }
componentDidMount() { componentDidMount() {
lbry.connect() lbry
.then(() => { this.updateStatus() }) .connect()
.catch(() => { .then(() => {
this.setState({ this.updateStatus();
isLagging: true, })
message: __("Connection Failure"), .catch(() => {
details: __("Try closing all LBRY processes and starting again. If this still happpens, your anti-virus software or firewall may be preventing LBRY from connecting. Contact hello@lbry.io if you think this is a software bug.") this.setState({
}) isLagging: true,
}) message: __('Connection Failure'),
} details: __(
'Try closing all LBRY processes and starting again. If this still happpens, your anti-virus software or firewall may be preventing LBRY from connecting. Contact hello@lbry.io if you think this is a software bug.'
)
});
});
}
render() { render() {
return <LoadScreen message={this.state.message} details={this.state.details} isWarning={this.state.isLagging} /> return (
} <LoadScreen
message={this.state.message}
details={this.state.details}
isWarning={this.state.isLagging}
/>
);
}
} }
export default SplashScreen; export default SplashScreen;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,46 +1,42 @@
import React from 'react'; import React from 'react';
import FilePrice from 'component/filePrice' import FilePrice from 'component/filePrice';
import Link from 'component/link'; import Link from 'component/link';
import Modal from 'component/modal'; import Modal from 'component/modal';
import lbry from 'lbry' import lbry from 'lbry';
import { import { Thumbnail } from 'component/common';
Thumbnail,
} from 'component/common'
class VideoPlayButton extends React.Component { class VideoPlayButton extends React.Component {
onPurchaseConfirmed() { onPurchaseConfirmed() {
this.props.closeModal() this.props.closeModal();
this.props.startPlaying() this.props.startPlaying();
this.props.loadVideo(this.props.uri) this.props.loadVideo(this.props.uri);
} }
onWatchClick() { onWatchClick() {
this.props.purchaseUri(this.props.uri).then(() => { this.props.purchaseUri(this.props.uri).then(() => {
if (!this.props.modal) { if (!this.props.modal) {
this.props.startPlaying() this.props.startPlaying();
} }
}) });
} }
render() { render() {
const { const {
button, button,
label, label,
className, className,
metadata, metadata,
metadata: { metadata: { title },
title, uri,
}, modal,
uri, closeModal,
modal, isLoading,
closeModal, costInfo,
isLoading, fileInfo,
costInfo, mediaType
fileInfo, } = this.props;
mediaType,
} = this.props
/* /*
title={ title={
isLoading ? "Video is Loading" : isLoading ? "Video is Loading" :
!costInfo ? "Waiting on cost info..." : !costInfo ? "Waiting on cost info..." :
@ -48,135 +44,171 @@ class VideoPlayButton extends React.Component {
} }
*/ */
const disabled = isLoading || fileInfo === undefined || (fileInfo === null && (!costInfo || costInfo.cost === undefined)) const disabled =
const icon = ["audio", "video"].indexOf(mediaType) !== -1 ? "icon-play" : "icon-folder-o" isLoading ||
fileInfo === undefined ||
(fileInfo === null && (!costInfo || costInfo.cost === undefined));
const icon = ['audio', 'video'].indexOf(mediaType) !== -1
? 'icon-play'
: 'icon-folder-o';
return (<div> return (
<Link button={ button ? button : null } <div>
disabled={disabled} <Link
label={label ? label : ""} button={button ? button : null}
className="video__play-button" disabled={disabled}
icon={icon} label={label ? label : ''}
onClick={this.onWatchClick.bind(this)} /> className="video__play-button"
<Modal contentLabel={__("Not enough credits")} isOpen={modal == 'notEnoughCredits'} onConfirmed={closeModal}> icon={icon}
{__("You don't have enough LBRY credits to pay for this stream.")} onClick={this.onWatchClick.bind(this)}
</Modal> />
<Modal <Modal
type="confirm" contentLabel={__('Not enough credits')}
isOpen={modal == 'affirmPurchaseAndPlay'} isOpen={modal == 'notEnoughCredits'}
contentLabel={__("Confirm Purchase")} onConfirmed={closeModal}
onConfirmed={this.onPurchaseConfirmed.bind(this)} >
onAborted={closeModal}> {__("You don't have enough LBRY credits to pay for this stream.")}
{__("This will purchase")} <strong>{title}</strong> {__("for")} <strong><FilePrice uri={uri} look="plain" /></strong> {__("credits")}. </Modal>
</Modal> <Modal
<Modal type="confirm"
isOpen={modal == 'timedOut'} onConfirmed={closeModal} contentLabel={__("Timed Out")}> isOpen={modal == 'affirmPurchaseAndPlay'}
{__("Sorry, your download timed out :(")} contentLabel={__('Confirm Purchase')}
</Modal> onConfirmed={this.onPurchaseConfirmed.bind(this)}
</div>); onAborted={closeModal}
} >
{__('This will purchase')} <strong>{title}</strong> {__('for')}
{' '}<strong><FilePrice uri={uri} look="plain" /></strong>
{' '}{__('credits')}.
</Modal>
<Modal
isOpen={modal == 'timedOut'}
onConfirmed={closeModal}
contentLabel={__('Timed Out')}
>
{__('Sorry, your download timed out :(')}
</Modal>
</div>
);
}
} }
class Video extends React.Component { class Video extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props);
this.state = { isPlaying: false } this.state = { isPlaying: false };
} }
startPlaying() { startPlaying() {
this.setState({ this.setState({
isPlaying: true isPlaying: true
}) });
} }
render() { render() {
const { const {
metadata, metadata,
isLoading, isLoading,
isDownloading, isDownloading,
fileInfo, fileInfo,
contentType, contentType
} = this.props } = this.props;
const { const { isPlaying = false } = this.state;
isPlaying = false,
} = this.state
const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0 const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0;
const mediaType = lbry.getMediaType(contentType, fileInfo && fileInfo.file_name) const mediaType = lbry.getMediaType(
contentType,
fileInfo && fileInfo.file_name
);
let loadStatusMessage = '' let loadStatusMessage = '';
if(fileInfo && fileInfo.completed && !fileInfo.written_bytes) { if (fileInfo && fileInfo.completed && !fileInfo.written_bytes) {
loadStatusMessage = __("It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds.") loadStatusMessage = __(
} else if (isLoading) { "It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds."
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) { } else if (isLoading) {
loadStatusMessage = __("Downloading stream... not long left now!") 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!');
}
let klassName = "" let klassName = '';
if (isLoading || isDownloading) klassName += "video-embedded video" if (isLoading || isDownloading) klassName += 'video-embedded video';
if (mediaType === "video") { if (mediaType === 'video') {
klassName += "video-embedded video" klassName += 'video-embedded video';
klassName += isPlaying ? " video--active" : " video--hidden" klassName += isPlaying ? ' video--active' : ' video--hidden';
} else if (mediaType === "application") { } else if (mediaType === 'application') {
klassName += "video-embedded" klassName += 'video-embedded';
} else { } else {
if (!isPlaying) klassName += "video-embedded" if (!isPlaying) klassName += 'video-embedded';
} }
const poster = metadata.thumbnail const poster = metadata.thumbnail;
return ( return (
<div className={klassName}>{ <div className={klassName}>
isPlaying ? {isPlaying
(!isReadyToPlay ? ? !isReadyToPlay
<span>{__("this is the world's worst loading screen and we shipped our software with it anyway...")} <br /><br />{loadStatusMessage}</span> : ? <span>
<VideoPlayer filename={fileInfo.file_name} poster={poster} downloadPath={fileInfo.download_path} mediaType={mediaType} poster={poster} />) : {__(
<div className="video__cover" style={{backgroundImage: 'url("' + metadata.thumbnail + '")'}}> "this is the world's worst loading screen and we shipped our software with it anyway..."
<VideoPlayButton startPlaying={this.startPlaying.bind(this)} {...this.props} mediaType={mediaType} /> )}
</div> {' '}<br /><br />{loadStatusMessage}
}</div> </span>
); : <VideoPlayer
} filename={fileInfo.file_name}
poster={poster}
downloadPath={fileInfo.download_path}
mediaType={mediaType}
poster={poster}
/>
: <div
className="video__cover"
style={{ backgroundImage: 'url("' + metadata.thumbnail + '")' }}
>
<VideoPlayButton
startPlaying={this.startPlaying.bind(this)}
{...this.props}
mediaType={mediaType}
/>
</div>}
</div>
);
}
} }
const from = require('from2') const from = require('from2');
const player = require('render-media') const player = require('render-media');
const fs = require('fs') const fs = require('fs');
class VideoPlayer extends React.Component { class VideoPlayer extends React.Component {
componentDidMount() { componentDidMount() {
const elem = this.refs.media const elem = this.refs.media;
const { const { downloadPath, filename } = this.props;
downloadPath, const file = {
filename, name: filename,
} = this.props createReadStream: opts => {
const file = { return fs.createReadStream(downloadPath, opts);
name: filename, }
createReadStream: (opts) => { };
return fs.createReadStream(downloadPath, opts) player.append(file, elem, {
} autoplay: true,
} controls: true
player.append(file, elem, { });
autoplay: true, }
controls: true,
})
}
render() { render() {
const { const { downloadPath, mediaType, poster } = this.props;
downloadPath,
mediaType,
poster,
} = this.props
return ( return (
<div> <div>
{["audio", "application"].indexOf(mediaType) !== -1 && <Thumbnail src={poster} className="video-embedded" />} {['audio', 'application'].indexOf(mediaType) !== -1 &&
<div ref="media" /> <Thumbnail src={poster} className="video-embedded" />}
</div> <div ref="media" />
) </div>
} );
}
} }
export default Video export default Video;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,160 +1,170 @@
import React from 'react'; import React from 'react';
import lbryuri from 'lbryuri.js'; import lbryuri from 'lbryuri.js';
import {Icon} from 'component/common.js'; import { Icon } from 'component/common.js';
class WunderBar extends React.PureComponent { class WunderBar extends React.PureComponent {
static TYPING_TIMEOUT = 800 static TYPING_TIMEOUT = 800;
static propTypes = { static propTypes = {
onSearch: React.PropTypes.func.isRequired, onSearch: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired onSubmit: React.PropTypes.func.isRequired
} };
constructor(props) { constructor(props) {
super(props); super(props);
this._userTypingTimer = null; this._userTypingTimer = null;
this._isSearchDispatchPending = false; this._isSearchDispatchPending = false;
this._input = null; this._input = null;
this._stateBeforeSearch = null; this._stateBeforeSearch = null;
this._resetOnNextBlur = true; this._resetOnNextBlur = true;
this.onChange = this.onChange.bind(this); this.onChange = this.onChange.bind(this);
this.onFocus = this.onFocus.bind(this); this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this); this.onBlur = this.onBlur.bind(this);
this.onKeyPress = this.onKeyPress.bind(this); this.onKeyPress = this.onKeyPress.bind(this);
this.onReceiveRef = this.onReceiveRef.bind(this); this.onReceiveRef = this.onReceiveRef.bind(this);
this.state = { this.state = {
address: this.props.address, address: this.props.address,
icon: this.props.icon icon: this.props.icon
}; };
} }
componentWillUnmount() { componentWillUnmount() {
if (this.userTypingTimer) { if (this.userTypingTimer) {
clearTimeout(this._userTypingTimer); clearTimeout(this._userTypingTimer);
} }
} }
onChange(event) { onChange(event) {
if (this._userTypingTimer) {
clearTimeout(this._userTypingTimer);
}
if (this._userTypingTimer) this.setState({ address: event.target.value });
{
clearTimeout(this._userTypingTimer);
}
this.setState({ address: event.target.value }) this._isSearchDispatchPending = true;
this._isSearchDispatchPending = true; let searchQuery = event.target.value;
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
}
this._userTypingTimer = setTimeout(() => { componentWillReceiveProps(nextProps) {
const hasQuery = searchQuery.length === 0; if (
this._resetOnNextBlur = hasQuery; nextProps.viewingPage !== this.props.viewingPage ||
this._isSearchDispatchPending = false; nextProps.address != this.props.address
if (searchQuery) { ) {
this.props.onSearch(searchQuery); this.setState({ address: nextProps.address, icon: nextProps.icon });
} }
}, WunderBar.TYPING_TIMEOUT); // 800ms delay, tweak for faster/slower }
}
componentWillReceiveProps(nextProps) { onFocus() {
if (nextProps.viewingPage !== this.props.viewingPage || nextProps.address != this.props.address) { this._stateBeforeSearch = this.state;
this.setState({ address: nextProps.address, icon: nextProps.icon }); let newState = {
} icon: 'icon-search',
} isActive: true
};
onFocus() { this._focusPending = true;
this._stateBeforeSearch = this.state; //below is hacking, improved when we have proper routing
let newState = { if (
icon: "icon-search", !this.state.address.startsWith('lbry://') &&
isActive: true 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);
}
this._focusPending = true; onBlur() {
//below is hacking, improved when we have proper routing if (this._isSearchDispatchPending) {
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 setTimeout(() => {
{ this.onBlur();
newState.address = ''; }, WunderBar.TYPING_TIMEOUT + 1);
} } else {
this.setState(newState); 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);
}
}
}
onBlur() { componentDidUpdate() {
if (this._isSearchDispatchPending) { if (this._input) {
setTimeout(() => { const start = this._input.selectionStart,
this.onBlur(); end = this._input.selectionEnd;
}, 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() { this._input.value = this.state.address; //this causes cursor to go to end of input
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);
this._input.setSelectionRange(start, end); if (this._focusPending) {
this._input.select();
this._focusPending = false;
}
}
}
if (this._focusPending) { onKeyPress(event) {
this._input.select(); if (event.charCode == 13 && this._input.value) {
this._focusPending = false; let uri = null,
} method = 'onSubmit';
}
}
onKeyPress(event) { this._resetOnNextBlur = false;
if (event.charCode == 13 && this._input.value) { clearTimeout(this._userTypingTimer);
let uri = null, try {
method = "onSubmit"; 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._resetOnNextBlur = false; this.props[method](uri);
clearTimeout(this._userTypingTimer); this._input.blur();
}
}
try { onReceiveRef(ref) {
uri = lbryuri.normalize(this._input.value); this._input = ref;
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); render() {
this._input.blur(); return (
} <div
} className={
'wunderbar' + (this.state.isActive ? ' wunderbar--active' : '')
onReceiveRef(ref) { }
this._input = ref; >
} {this.state.icon ? <Icon fixed icon={this.state.icon} /> : ''}
<input
render() { className="wunderbar__input"
return ( type="search"
<div className={'wunderbar' + (this.state.isActive ? ' wunderbar--active' : '')}> ref={this.onReceiveRef}
{this.state.icon ? <Icon fixed icon={this.state.icon} /> : '' } onFocus={this.onFocus}
<input className="wunderbar__input" type="search" onBlur={this.onBlur}
ref={this.onReceiveRef} onChange={this.onChange}
onFocus={this.onFocus} onKeyPress={this.onKeyPress}
onBlur={this.onBlur} value={this.state.address}
onChange={this.onChange} placeholder={__('Find movies, music, games, and more')}
onKeyPress={this.onKeyPress} />
value={this.state.address} </div>
placeholder={__("Find movies, music, games, and more")} /> );
</div> }
);
}
} }
export default WunderBar; export default WunderBar;

View file

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

View file

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

View file

@ -1,68 +1,71 @@
export const CHANGE_PATH = 'CHANGE_PATH' export const CHANGE_PATH = 'CHANGE_PATH';
export const OPEN_MODAL = 'OPEN_MODAL' export const OPEN_MODAL = 'OPEN_MODAL';
export const CLOSE_MODAL = 'CLOSE_MODAL' export const CLOSE_MODAL = 'CLOSE_MODAL';
export const HISTORY_BACK = 'HISTORY_BACK' export const HISTORY_BACK = 'HISTORY_BACK';
export const SHOW_SNACKBAR = 'SHOW_SNACKBAR' export const SHOW_SNACKBAR = 'SHOW_SNACKBAR';
export const REMOVE_SNACKBAR_SNACK = 'REMOVE_SNACKBAR_SNACK' export const REMOVE_SNACKBAR_SNACK = 'REMOVE_SNACKBAR_SNACK';
export const DAEMON_READY = 'DAEMON_READY' export const DAEMON_READY = 'DAEMON_READY';
// Upgrades // Upgrades
export const UPGRADE_CANCELLED = 'UPGRADE_CANCELLED' export const UPGRADE_CANCELLED = 'UPGRADE_CANCELLED';
export const DOWNLOAD_UPGRADE = 'DOWNLOAD_UPGRADE' export const DOWNLOAD_UPGRADE = 'DOWNLOAD_UPGRADE';
export const UPGRADE_DOWNLOAD_STARTED = 'UPGRADE_DOWNLOAD_STARTED' export const UPGRADE_DOWNLOAD_STARTED = 'UPGRADE_DOWNLOAD_STARTED';
export const UPGRADE_DOWNLOAD_COMPLETED = 'UPGRADE_DOWNLOAD_COMPLETED' export const UPGRADE_DOWNLOAD_COMPLETED = 'UPGRADE_DOWNLOAD_COMPLETED';
export const UPGRADE_DOWNLOAD_PROGRESSED = 'UPGRADE_DOWNLOAD_PROGRESSED' export const UPGRADE_DOWNLOAD_PROGRESSED = 'UPGRADE_DOWNLOAD_PROGRESSED';
export const CHECK_UPGRADE_AVAILABLE = 'CHECK_UPGRADE_AVAILABLE' export const CHECK_UPGRADE_AVAILABLE = 'CHECK_UPGRADE_AVAILABLE';
export const UPDATE_VERSION = 'UPDATE_VERSION' export const UPDATE_VERSION = 'UPDATE_VERSION';
export const SKIP_UPGRADE = 'SKIP_UPGRADE' export const SKIP_UPGRADE = 'SKIP_UPGRADE';
export const START_UPGRADE = 'START_UPGRADE' export const START_UPGRADE = 'START_UPGRADE';
// Wallet // Wallet
export const GET_NEW_ADDRESS_STARTED = 'GET_NEW_ADDRESS_STARTED' export const GET_NEW_ADDRESS_STARTED = 'GET_NEW_ADDRESS_STARTED';
export const GET_NEW_ADDRESS_COMPLETED = 'GET_NEW_ADDRESS_COMPLETED' export const GET_NEW_ADDRESS_COMPLETED = 'GET_NEW_ADDRESS_COMPLETED';
export const FETCH_TRANSACTIONS_STARTED = 'FETCH_TRANSACTIONS_STARTED' export const FETCH_TRANSACTIONS_STARTED = 'FETCH_TRANSACTIONS_STARTED';
export const FETCH_TRANSACTIONS_COMPLETED = 'FETCH_TRANSACTIONS_COMPLETED' export const FETCH_TRANSACTIONS_COMPLETED = 'FETCH_TRANSACTIONS_COMPLETED';
export const UPDATE_BALANCE = 'UPDATE_BALANCE' export const UPDATE_BALANCE = 'UPDATE_BALANCE';
export const CHECK_ADDRESS_IS_MINE_STARTED = 'CHECK_ADDRESS_IS_MINE_STARTED' 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 CHECK_ADDRESS_IS_MINE_COMPLETED =
export const SET_DRAFT_TRANSACTION_AMOUNT = 'SET_DRAFT_TRANSACTION_AMOUNT' 'CHECK_ADDRESS_IS_MINE_COMPLETED';
export const SET_DRAFT_TRANSACTION_ADDRESS = 'SET_DRAFT_TRANSACTION_ADDRESS' export const SET_DRAFT_TRANSACTION_AMOUNT = 'SET_DRAFT_TRANSACTION_AMOUNT';
export const SEND_TRANSACTION_STARTED = 'SEND_TRANSACTION_STARTED' export const SET_DRAFT_TRANSACTION_ADDRESS = 'SET_DRAFT_TRANSACTION_ADDRESS';
export const SEND_TRANSACTION_COMPLETED = 'SEND_TRANSACTION_COMPLETED' export const SEND_TRANSACTION_STARTED = 'SEND_TRANSACTION_STARTED';
export const SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED' export const SEND_TRANSACTION_COMPLETED = 'SEND_TRANSACTION_COMPLETED';
export const SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED';
// Content // Content
export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED' export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED';
export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED' export const FETCH_FEATURED_CONTENT_COMPLETED =
export const RESOLVE_URI_STARTED = 'RESOLVE_URI_STARTED' 'FETCH_FEATURED_CONTENT_COMPLETED';
export const RESOLVE_URI_COMPLETED = 'RESOLVE_URI_COMPLETED' export const RESOLVE_URI_STARTED = 'RESOLVE_URI_STARTED';
export const RESOLVE_URI_CANCELED = 'RESOLVE_URI_CANCELED' export const RESOLVE_URI_COMPLETED = 'RESOLVE_URI_COMPLETED';
export const FETCH_CHANNEL_CLAIMS_STARTED = 'FETCH_CHANNEL_CLAIMS_STARTED' export const RESOLVE_URI_CANCELED = 'RESOLVE_URI_CANCELED';
export const FETCH_CHANNEL_CLAIMS_COMPLETED = 'FETCH_CHANNEL_CLAIMS_COMPLETED' export const FETCH_CHANNEL_CLAIMS_STARTED = 'FETCH_CHANNEL_CLAIMS_STARTED';
export const FETCH_CLAIM_LIST_MINE_STARTED = 'FETCH_CLAIM_LIST_MINE_STARTED' export const FETCH_CHANNEL_CLAIMS_COMPLETED = 'FETCH_CHANNEL_CLAIMS_COMPLETED';
export const FETCH_CLAIM_LIST_MINE_COMPLETED = 'FETCH_CLAIM_LIST_MINE_COMPLETED' export const FETCH_CLAIM_LIST_MINE_STARTED = 'FETCH_CLAIM_LIST_MINE_STARTED';
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED' export const FETCH_CLAIM_LIST_MINE_COMPLETED =
export const FILE_LIST_COMPLETED = 'FILE_LIST_COMPLETED' 'FETCH_CLAIM_LIST_MINE_COMPLETED';
export const FETCH_FILE_INFO_STARTED = 'FETCH_FILE_INFO_STARTED' export const FILE_LIST_STARTED = 'FILE_LIST_STARTED';
export const FETCH_FILE_INFO_COMPLETED = 'FETCH_FILE_INFO_COMPLETED' export const FILE_LIST_COMPLETED = 'FILE_LIST_COMPLETED';
export const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED' export const FETCH_FILE_INFO_STARTED = 'FETCH_FILE_INFO_STARTED';
export const FETCH_COST_INFO_COMPLETED = 'FETCH_COST_INFO_COMPLETED' export const FETCH_FILE_INFO_COMPLETED = 'FETCH_FILE_INFO_COMPLETED';
export const LOADING_VIDEO_STARTED = 'LOADING_VIDEO_STARTED' export const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED';
export const LOADING_VIDEO_COMPLETED = 'LOADING_VIDEO_COMPLETED' export const FETCH_COST_INFO_COMPLETED = 'FETCH_COST_INFO_COMPLETED';
export const LOADING_VIDEO_FAILED = 'LOADING_VIDEO_FAILED' export const LOADING_VIDEO_STARTED = 'LOADING_VIDEO_STARTED';
export const DOWNLOADING_STARTED = 'DOWNLOADING_STARTED' export const LOADING_VIDEO_COMPLETED = 'LOADING_VIDEO_COMPLETED';
export const DOWNLOADING_PROGRESSED = 'DOWNLOADING_PROGRESSED' export const LOADING_VIDEO_FAILED = 'LOADING_VIDEO_FAILED';
export const DOWNLOADING_COMPLETED = 'DOWNLOADING_COMPLETED' export const DOWNLOADING_STARTED = 'DOWNLOADING_STARTED';
export const PLAY_VIDEO_STARTED = 'PLAY_VIDEO_STARTED' export const DOWNLOADING_PROGRESSED = 'DOWNLOADING_PROGRESSED';
export const FETCH_AVAILABILITY_STARTED = 'FETCH_AVAILABILITY_STARTED' export const DOWNLOADING_COMPLETED = 'DOWNLOADING_COMPLETED';
export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED' export const PLAY_VIDEO_STARTED = 'PLAY_VIDEO_STARTED';
export const FILE_DELETE = 'FILE_DELETE' export const FETCH_AVAILABILITY_STARTED = 'FETCH_AVAILABILITY_STARTED';
export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED';
export const FILE_DELETE = 'FILE_DELETE';
// Search // Search
export const SEARCH_STARTED = 'SEARCH_STARTED' export const SEARCH_STARTED = 'SEARCH_STARTED';
export const SEARCH_COMPLETED = 'SEARCH_COMPLETED' export const SEARCH_COMPLETED = 'SEARCH_COMPLETED';
export const SEARCH_CANCELLED = 'SEARCH_CANCELLED' export const SEARCH_CANCELLED = 'SEARCH_CANCELLED';
// Settings // Settings
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED' export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';

View file

@ -1,75 +1,87 @@
const jsonrpc = {}; const jsonrpc = {};
jsonrpc.call = function (connectionString, method, params, callback, errorCallback, connectFailedCallback, timeout) { jsonrpc.call = function(
var xhr = new XMLHttpRequest; connectionString,
if (typeof connectFailedCallback !== 'undefined') { method,
if (timeout) { params,
xhr.timeout = timeout; callback,
} errorCallback,
connectFailedCallback,
timeout
) {
var xhr = new XMLHttpRequest();
if (typeof connectFailedCallback !== 'undefined') {
if (timeout) {
xhr.timeout = timeout;
}
xhr.addEventListener('error', function (e) { xhr.addEventListener('error', function(e) {
connectFailedCallback(e); connectFailedCallback(e);
}); });
xhr.addEventListener('timeout', function() { xhr.addEventListener('timeout', function() {
connectFailedCallback(new Error(__('XMLHttpRequest connection timed out'))); connectFailedCallback(
}) new Error(__('XMLHttpRequest connection timed out'))
} );
xhr.addEventListener('load', function() { });
var response = JSON.parse(xhr.responseText); }
xhr.addEventListener('load', function() {
var response = JSON.parse(xhr.responseText);
if (response.error) { if (response.error) {
if (errorCallback) { if (errorCallback) {
errorCallback(response.error); errorCallback(response.error);
} else { } else {
var errorEvent = new CustomEvent('unhandledError', { var errorEvent = new CustomEvent('unhandledError', {
detail: { detail: {
connectionString: connectionString, connectionString: connectionString,
method: method, method: method,
params: params, params: params,
code: response.error.code, code: response.error.code,
message: response.error.message, message: response.error.message,
data: response.error.data data: response.error.data
} }
}); });
document.dispatchEvent(errorEvent) document.dispatchEvent(errorEvent);
} }
} else if (callback) { } else if (callback) {
callback(response.result); callback(response.result);
} }
}); });
if (connectFailedCallback) { if (connectFailedCallback) {
xhr.addEventListener('error', function (event) { xhr.addEventListener('error', function(event) {
connectFailedCallback(event); connectFailedCallback(event);
}); });
} else { } else {
xhr.addEventListener('error', function (event) { xhr.addEventListener('error', function(event) {
var errorEvent = new CustomEvent('unhandledError', { var errorEvent = new CustomEvent('unhandledError', {
detail: { detail: {
connectionString: connectionString, connectionString: connectionString,
method: method, method: method,
params: params, params: params,
code: xhr.status, code: xhr.status,
message: __('Connection to API server failed') message: __('Connection to API server failed')
} }
}); });
document.dispatchEvent(errorEvent); document.dispatchEvent(errorEvent);
}); });
} }
const counter = parseInt(sessionStorage.getItem('JSONRPCCounter') || 0); const counter = parseInt(sessionStorage.getItem('JSONRPCCounter') || 0);
xhr.open('POST', connectionString, true); xhr.open('POST', connectionString, true);
xhr.send(JSON.stringify({ xhr.send(
'jsonrpc': '2.0', JSON.stringify({
'method': method, jsonrpc: '2.0',
'params': params, method: method,
'id': counter, params: params,
})); id: counter
})
);
sessionStorage.setItem('JSONRPCCounter', counter + 1); sessionStorage.setItem('JSONRPCCounter', counter + 1);
return xhr return xhr;
}; };
export default jsonrpc; export default jsonrpc;

View file

@ -2,60 +2,68 @@ import lbryio from './lbryio.js';
import lighthouse from './lighthouse.js'; import lighthouse from './lighthouse.js';
import jsonrpc from './jsonrpc.js'; import jsonrpc from './jsonrpc.js';
import lbryuri from './lbryuri.js'; import lbryuri from './lbryuri.js';
import {getLocal, getSession, setSession, setLocal} from './utils.js'; import { getLocal, getSession, setSession, setLocal } from './utils.js';
const {remote, ipcRenderer} = require('electron'); const { remote, ipcRenderer } = require('electron');
const menu = remote.require('./menu/main-menu'); const menu = remote.require('./menu/main-menu');
let lbry = { let lbry = {
isConnected: false, isConnected: false,
daemonConnectionString: 'http://localhost:5279/lbryapi', daemonConnectionString: 'http://localhost:5279/lbryapi',
pendingPublishTimeout: 20 * 60 * 1000, pendingPublishTimeout: 20 * 60 * 1000,
defaultClientSettings: { defaultClientSettings: {
showNsfw: false, showNsfw: false,
showUnavailable: true, showUnavailable: true,
debug: false, debug: false,
useCustomLighthouseServers: false, useCustomLighthouseServers: false,
customLighthouseServers: [], customLighthouseServers: [],
showDeveloperMenu: false, showDeveloperMenu: false,
language: 'en', language: 'en'
} }
}; };
/** /**
* Records a publish attempt in local storage. Returns a dictionary with all the data needed to * Records a publish attempt in local storage. Returns a dictionary with all the data needed to
* needed to make a dummy claim or file info object. * needed to make a dummy claim or file info object.
*/ */
function savePendingPublish({name, channel_name}) { function savePendingPublish({ name, channel_name }) {
let uri; let uri;
if (channel_name) { if (channel_name) {
uri = lbryuri.build({name: channel_name, path: name}, false); uri = lbryuri.build({ name: channel_name, path: name }, false);
} else { } else {
uri = lbryuri.build({name: name}, false); uri = lbryuri.build({ name: name }, false);
} }
const pendingPublishes = getLocal('pendingPublishes') || []; const pendingPublishes = getLocal('pendingPublishes') || [];
const newPendingPublish = { const newPendingPublish = {
name, channel_name, name,
claim_id: 'pending_claim_' + uri, channel_name,
txid: 'pending_' + uri, claim_id: 'pending_claim_' + uri,
nout: 0, txid: 'pending_' + uri,
outpoint: 'pending_' + uri + ':0', nout: 0,
time: Date.now(), outpoint: 'pending_' + uri + ':0',
}; time: Date.now()
setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]); };
return newPendingPublish; setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]);
return newPendingPublish;
} }
/** /**
* If there is a pending publish with the given name or outpoint, remove it. * If there is a pending publish with the given name or outpoint, remove it.
* A channel name may also be provided along with name. * A channel name may also be provided along with name.
*/ */
function removePendingPublishIfNeeded({name, channel_name, outpoint}) { function removePendingPublishIfNeeded({ name, channel_name, outpoint }) {
function pubMatches(pub) { function pubMatches(pub) {
return pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name)); return (
} pub.outpoint === outpoint ||
(pub.name === name &&
(!channel_name || pub.channel_name === channel_name))
);
}
setLocal('pendingPublishes', lbry.getPendingPublishes().filter(pub => !pubMatches(pub))); setLocal(
'pendingPublishes',
lbry.getPendingPublishes().filter(pub => !pubMatches(pub))
);
} }
/** /**
@ -63,74 +71,111 @@ function removePendingPublishIfNeeded({name, channel_name, outpoint}) {
* removes them from the list. * removes them from the list.
*/ */
lbry.getPendingPublishes = function() { lbry.getPendingPublishes = function() {
const pendingPublishes = getLocal('pendingPublishes') || []; const pendingPublishes = getLocal('pendingPublishes') || [];
const newPendingPublishes = pendingPublishes.filter(pub => Date.now() - pub.time <= lbry.pendingPublishTimeout); const newPendingPublishes = pendingPublishes.filter(
setLocal('pendingPublishes', newPendingPublishes); pub => Date.now() - pub.time <= lbry.pendingPublishTimeout
return newPendingPublishes; );
} setLocal('pendingPublishes', newPendingPublishes);
return newPendingPublishes;
};
/** /**
* Gets a pending publish attempt by its name or (fake) outpoint. A channel name can also be * Gets a pending publish attempt by its name or (fake) outpoint. A channel name can also be
* provided along withe the name. If no pending publish is found, returns null. * provided along withe the name. If no pending publish is found, returns null.
*/ */
function getPendingPublish({name, channel_name, outpoint}) { function getPendingPublish({ name, channel_name, outpoint }) {
const pendingPublishes = lbry.getPendingPublishes(); const pendingPublishes = lbry.getPendingPublishes();
return pendingPublishes.find( return (
pub => pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name)) pendingPublishes.find(
) || null; pub =>
pub.outpoint === outpoint ||
(pub.name === name &&
(!channel_name || pub.channel_name === channel_name))
) || null
);
} }
function pendingPublishToDummyClaim({channel_name, name, outpoint, claim_id, txid, nout}) { function pendingPublishToDummyClaim({
return {name, outpoint, claim_id, txid, nout, channel_name}; channel_name,
name,
outpoint,
claim_id,
txid,
nout
}) {
return { name, outpoint, claim_id, txid, nout, channel_name };
} }
function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) { function pendingPublishToDummyFileInfo({ name, outpoint, claim_id }) {
return {name, outpoint, claim_id, metadata: null}; return { name, outpoint, claim_id, metadata: null };
} }
lbry.call = function (method, params, callback, errorCallback, connectFailedCallback) { lbry.call = function(
return jsonrpc.call(lbry.daemonConnectionString, method, params, callback, errorCallback, connectFailedCallback); method,
} params,
callback,
errorCallback,
connectFailedCallback
) {
return jsonrpc.call(
lbry.daemonConnectionString,
method,
params,
callback,
errorCallback,
connectFailedCallback
);
};
//core //core
lbry._connectPromise = null; lbry._connectPromise = null;
lbry.connect = function() { lbry.connect = function() {
if (lbry._connectPromise === null) { if (lbry._connectPromise === null) {
lbry._connectPromise = new Promise((resolve, reject) => { lbry._connectPromise = new Promise((resolve, reject) => {
let tryNum = 0;
let tryNum = 0 function checkDaemonStartedFailed() {
if (tryNum <= 100) {
// Move # of tries into constant or config option
setTimeout(() => {
tryNum++;
checkDaemonStarted();
}, tryNum < 50 ? 400 : 1000);
} else {
reject(new Error('Unable to connect to LBRY'));
}
}
function checkDaemonStartedFailed() { // Check every half second to see if the daemon is accepting connections
if (tryNum <= 100) { // Move # of tries into constant or config option function checkDaemonStarted() {
setTimeout(() => { lbry.call(
tryNum++ 'status',
checkDaemonStarted(); {},
}, tryNum < 50 ? 400 : 1000); resolve,
} checkDaemonStartedFailed,
else { checkDaemonStartedFailed
reject(new Error("Unable to connect to LBRY")); );
} }
}
// Check every half second to see if the daemon is accepting connections checkDaemonStarted();
function checkDaemonStarted() { });
lbry.call('status', {}, resolve, checkDaemonStartedFailed, checkDaemonStartedFailed) }
}
checkDaemonStarted(); return lbry._connectPromise;
}); };
}
return lbry._connectPromise;
}
lbry.checkAddressIsMine = function(address, callback) { lbry.checkAddressIsMine = function(address, callback) {
lbry.call('wallet_is_address_mine', {address: address}, callback); lbry.call('wallet_is_address_mine', { address: address }, callback);
} };
lbry.sendToAddress = function(amount, address, callback, errorCallback) { lbry.sendToAddress = function(amount, address, callback, errorCallback) {
lbry.call("send_amount_to_address", { "amount" : amount, "address": address }, callback, errorCallback); lbry.call(
} 'send_amount_to_address',
{ amount: amount, address: address },
callback,
errorCallback
);
};
/** /**
* Takes a LBRY URI; will first try and calculate a total cost using * Takes a LBRY URI; will first try and calculate a total cost using
@ -142,48 +187,49 @@ lbry.sendToAddress = function(amount, address, callback, errorCallback) {
* - includes_data: Boolean; indicates whether or not the data fee info * - includes_data: Boolean; indicates whether or not the data fee info
* from Lighthouse is included. * from Lighthouse is included.
*/ */
lbry.costPromiseCache = {} lbry.costPromiseCache = {};
lbry.getCostInfo = function(uri) { lbry.getCostInfo = function(uri) {
if (lbry.costPromiseCache[uri] === undefined) { if (lbry.costPromiseCache[uri] === undefined) {
lbry.costPromiseCache[uri] = new Promise((resolve, reject) => { lbry.costPromiseCache[uri] = new Promise((resolve, reject) => {
const COST_INFO_CACHE_KEY = 'cost_info_cache'; const COST_INFO_CACHE_KEY = 'cost_info_cache';
let costInfoCache = getSession(COST_INFO_CACHE_KEY, {}) let costInfoCache = getSession(COST_INFO_CACHE_KEY, {});
function cacheAndResolve(cost, includesData) { function cacheAndResolve(cost, includesData) {
costInfoCache[uri] = {cost, includesData}; costInfoCache[uri] = { cost, includesData };
setSession(COST_INFO_CACHE_KEY, costInfoCache); setSession(COST_INFO_CACHE_KEY, costInfoCache);
resolve({cost, includesData}); resolve({ cost, includesData });
} }
if (!uri) { if (!uri) {
return reject(new Error(`URI required.`)); return reject(new Error(`URI required.`));
} }
if (costInfoCache[uri] && costInfoCache[uri].cost) { if (costInfoCache[uri] && costInfoCache[uri].cost) {
return resolve(costInfoCache[uri]) return resolve(costInfoCache[uri]);
} }
function getCost(uri, size) { function getCost(uri, size) {
lbry.stream_cost_estimate({uri, ... size !== null ? {size} : {}}).then((cost) => { lbry
cacheAndResolve(cost, size !== null); .stream_cost_estimate({ uri, ...(size !== null ? { size } : {}) })
}, reject); .then(cost => {
} cacheAndResolve(cost, size !== null);
}, reject);
}
const uriObj = lbryuri.parse(uri); const uriObj = lbryuri.parse(uri);
const name = uriObj.path || uriObj.name; const name = uriObj.path || uriObj.name;
lighthouse.get_size_for_name(name).then((size) => { lighthouse.get_size_for_name(name).then(size => {
if (size) { if (size) {
getCost(name, size); getCost(name, size);
} } else {
else { getCost(name, null);
getCost(name, null); }
} });
}) });
}); }
} return lbry.costPromiseCache[uri];
return lbry.costPromiseCache[uri]; };
}
/** /**
* Publishes a file. The optional fileListedCallback is called when the file becomes available in * Publishes a file. The optional fileListedCallback is called when the file becomes available in
@ -192,125 +238,144 @@ lbry.getCostInfo = function(uri) {
* This currently includes a work-around to cache the file in local storage so that the pending * This currently includes a work-around to cache the file in local storage so that the pending
* publish can appear in the UI immediately. * publish can appear in the UI immediately.
*/ */
lbry.publish = function(params, fileListedCallback, publishedCallback, errorCallback) { lbry.publish = function(
lbry.call('publish', params, (result) => { params,
if (returnedPending) { fileListedCallback,
return; publishedCallback,
} errorCallback
) {
lbry.call(
'publish',
params,
result => {
if (returnedPending) {
return;
}
clearTimeout(returnPendingTimeout); clearTimeout(returnPendingTimeout);
publishedCallback(result); publishedCallback(result);
}, (err) => { },
if (returnedPending) { err => {
return; if (returnedPending) {
} return;
}
clearTimeout(returnPendingTimeout); clearTimeout(returnPendingTimeout);
errorCallback(err); errorCallback(err);
}); }
);
let returnedPending = false; let returnedPending = false;
// Give a short grace period in case publish() returns right away or (more likely) gives an error // Give a short grace period in case publish() returns right away or (more likely) gives an error
const returnPendingTimeout = setTimeout(() => { const returnPendingTimeout = setTimeout(() => {
returnedPending = true; returnedPending = true;
if (publishedCallback) { if (publishedCallback) {
savePendingPublish({name: params.name, channel_name: params.channel_name}); savePendingPublish({
publishedCallback(true); name: params.name,
} channel_name: params.channel_name
});
if (fileListedCallback) { publishedCallback(true);
const {name, channel_name} = params; }
savePendingPublish({name: params.name, channel_name: params.channel_name});
fileListedCallback(true);
}
}, 2000);
}
if (fileListedCallback) {
const { name, channel_name } = params;
savePendingPublish({
name: params.name,
channel_name: params.channel_name
});
fileListedCallback(true);
}
}, 2000);
};
lbry.getClientSettings = function() { lbry.getClientSettings = function() {
var outSettings = {}; var outSettings = {};
for (let setting of Object.keys(lbry.defaultClientSettings)) { for (let setting of Object.keys(lbry.defaultClientSettings)) {
var localStorageVal = localStorage.getItem('setting_' + setting); var localStorageVal = localStorage.getItem('setting_' + setting);
outSettings[setting] = (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal)); outSettings[setting] = localStorageVal === null
} ? lbry.defaultClientSettings[setting]
return outSettings; : JSON.parse(localStorageVal);
} }
return outSettings;
};
lbry.getClientSetting = function(setting) { lbry.getClientSetting = function(setting) {
var localStorageVal = localStorage.getItem('setting_' + setting); var localStorageVal = localStorage.getItem('setting_' + setting);
if (setting == 'showDeveloperMenu') if (setting == 'showDeveloperMenu') {
{ return true;
return true; }
} return localStorageVal === null
return (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal)); ? lbry.defaultClientSettings[setting]
} : JSON.parse(localStorageVal);
};
lbry.setClientSettings = function(settings) { lbry.setClientSettings = function(settings) {
for (let setting of Object.keys(settings)) { for (let setting of Object.keys(settings)) {
lbry.setClientSetting(setting, settings[setting]); lbry.setClientSetting(setting, settings[setting]);
} }
} };
lbry.setClientSetting = function(setting, value) { lbry.setClientSetting = function(setting, value) {
return localStorage.setItem('setting_' + setting, JSON.stringify(value)); return localStorage.setItem('setting_' + setting, JSON.stringify(value));
} };
lbry.getSessionInfo = function(callback) { lbry.getSessionInfo = function(callback) {
lbry.call('status', {session_status: true}, callback); lbry.call('status', { session_status: true }, callback);
} };
lbry.reportBug = function(message, callback) { lbry.reportBug = function(message, callback) {
lbry.call('report_bug', { lbry.call(
message: message 'report_bug',
}, callback); {
} message: message
},
callback
);
};
//utilities //utilities
lbry.formatCredits = function(amount, precision) lbry.formatCredits = function(amount, precision) {
{ return amount.toFixed(precision || 1).replace(/\.?0+$/, '');
return amount.toFixed(precision || 1).replace(/\.?0+$/, ''); };
}
lbry.formatName = function(name) { lbry.formatName = function(name) {
// Converts LBRY name to standard format (all lower case, no special characters, spaces replaced by dashes) // Converts LBRY name to standard format (all lower case, no special characters, spaces replaced by dashes)
name = name.replace('/\s+/g', '-'); name = name.replace('/s+/g', '-');
name = name.toLowerCase().replace(/[^a-z0-9\-]/g, ''); name = name.toLowerCase().replace(/[^a-z0-9\-]/g, '');
return name; return name;
} };
lbry.imagePath = function(file) {
lbry.imagePath = function(file) return 'img/' + file;
{ };
return 'img/' + file;
}
lbry.getMediaType = function(contentType, fileName) { lbry.getMediaType = function(contentType, fileName) {
if (contentType) { if (contentType) {
return /^[^/]+/.exec(contentType)[0]; return /^[^/]+/.exec(contentType)[0];
} else if (fileName) { } else if (fileName) {
var dotIndex = fileName.lastIndexOf('.'); var dotIndex = fileName.lastIndexOf('.');
if (dotIndex == -1) { if (dotIndex == -1) {
return 'unknown'; return 'unknown';
} }
var ext = fileName.substr(dotIndex + 1); var ext = fileName.substr(dotIndex + 1);
if (/^mp4|mov|m4v|flv|f4v$/i.test(ext)) { if (/^mp4|mov|m4v|flv|f4v$/i.test(ext)) {
return 'video'; return 'video';
} else if (/^mp3|m4a|aac|wav|flac|ogg$/i.test(ext)) { } else if (/^mp3|m4a|aac|wav|flac|ogg$/i.test(ext)) {
return 'audio'; return 'audio';
} else if (/^html|htm|pdf|odf|doc|docx|md|markdown|txt$/i.test(ext)) { } else if (/^html|htm|pdf|odf|doc|docx|md|markdown|txt$/i.test(ext)) {
return 'document'; return 'document';
} else { } else {
return 'unknown'; return 'unknown';
} }
} else { } else {
return 'unknown'; return 'unknown';
} }
} };
lbry.stop = function(callback) { lbry.stop = function(callback) {
lbry.call('stop', {}, callback); lbry.call('stop', {}, callback);
}; };
lbry._subscribeIdCount = 0; lbry._subscribeIdCount = 0;
@ -319,49 +384,58 @@ lbry._balanceSubscribeInterval = 5000;
lbry._balanceUpdateInterval = null; lbry._balanceUpdateInterval = null;
lbry._updateBalanceSubscribers = function() { lbry._updateBalanceSubscribers = function() {
lbry.wallet_balance().then(function(balance) { lbry.wallet_balance().then(function(balance) {
for (let callback of Object.values(lbry._balanceSubscribeCallbacks)) { for (let callback of Object.values(lbry._balanceSubscribeCallbacks)) {
callback(balance); callback(balance);
} }
}); });
if (!lbry._balanceUpdateInterval && Object.keys(lbry._balanceSubscribeCallbacks).length) { if (
lbry._balanceUpdateInterval = setInterval(() => { !lbry._balanceUpdateInterval &&
lbry._updateBalanceSubscribers(); Object.keys(lbry._balanceSubscribeCallbacks).length
}, lbry._balanceSubscribeInterval); ) {
} lbry._balanceUpdateInterval = setInterval(() => {
} lbry._updateBalanceSubscribers();
}, lbry._balanceSubscribeInterval);
}
};
lbry.balanceSubscribe = function(callback) { lbry.balanceSubscribe = function(callback) {
const subscribeId = ++lbry._subscribeIdCount; const subscribeId = ++lbry._subscribeIdCount;
lbry._balanceSubscribeCallbacks[subscribeId] = callback; lbry._balanceSubscribeCallbacks[subscribeId] = callback;
lbry._updateBalanceSubscribers(); lbry._updateBalanceSubscribers();
return subscribeId; return subscribeId;
} };
lbry.balanceUnsubscribe = function(subscribeId) { lbry.balanceUnsubscribe = function(subscribeId) {
delete lbry._balanceSubscribeCallbacks[subscribeId]; delete lbry._balanceSubscribeCallbacks[subscribeId];
if (lbry._balanceUpdateInterval && !Object.keys(lbry._balanceSubscribeCallbacks).length) { if (
clearInterval(lbry._balanceUpdateInterval) lbry._balanceUpdateInterval &&
} !Object.keys(lbry._balanceSubscribeCallbacks).length
} ) {
clearInterval(lbry._balanceUpdateInterval);
}
};
lbry.showMenuIfNeeded = function() { lbry.showMenuIfNeeded = function() {
const showingMenu = sessionStorage.getItem('menuShown') || null; const showingMenu = sessionStorage.getItem('menuShown') || null;
const chosenMenu = lbry.getClientSetting('showDeveloperMenu') ? 'developer' : 'normal'; const chosenMenu = lbry.getClientSetting('showDeveloperMenu')
if (chosenMenu != showingMenu) { ? 'developer'
menu.showMenubar(chosenMenu == 'developer'); : 'normal';
} if (chosenMenu != showingMenu) {
sessionStorage.setItem('menuShown', chosenMenu); menu.showMenubar(chosenMenu == 'developer');
}
sessionStorage.setItem('menuShown', chosenMenu);
}; };
lbry.getAppVersionInfo = function() { lbry.getAppVersionInfo = function() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
ipcRenderer.once('version-info-received', (event, versionInfo) => { resolve(versionInfo) }); ipcRenderer.once('version-info-received', (event, versionInfo) => {
ipcRenderer.send('version-info-requested'); resolve(versionInfo);
}); });
} ipcRenderer.send('version-info-requested');
});
};
/** /**
* Wrappers for API methods to simulate missing or future behavior. Unlike the old-style stubs, * Wrappers for API methods to simulate missing or future behavior. Unlike the old-style stubs,
@ -372,86 +446,118 @@ lbry.getAppVersionInfo = function() {
* Returns results from the file_list API method, plus dummy entries for pending publishes. * Returns results from the file_list API method, plus dummy entries for pending publishes.
* (If a real publish with the same name is found, the pending publish will be ignored and removed.) * (If a real publish with the same name is found, the pending publish will be ignored and removed.)
*/ */
lbry.file_list = function(params={}) { lbry.file_list = function(params = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const {name, channel_name, outpoint} = params; const { name, channel_name, outpoint } = params;
/** /**
* If we're searching by outpoint, check first to see if there's a matching pending publish. * If we're searching by outpoint, check first to see if there's a matching pending publish.
* Pending publishes use their own faux outpoints that are always unique, so we don't need * Pending publishes use their own faux outpoints that are always unique, so we don't need
* to check if there's a real file. * to check if there's a real file.
*/ */
if (outpoint) { if (outpoint) {
const pendingPublish = getPendingPublish({outpoint}); const pendingPublish = getPendingPublish({ outpoint });
if (pendingPublish) { if (pendingPublish) {
resolve([pendingPublishToDummyFileInfo(pendingPublish)]); resolve([pendingPublishToDummyFileInfo(pendingPublish)]);
return; return;
} }
} }
lbry.call('file_list', params, (fileInfos) => { lbry.call(
removePendingPublishIfNeeded({name, channel_name, outpoint}); 'file_list',
params,
fileInfos => {
removePendingPublishIfNeeded({ name, channel_name, outpoint });
const dummyFileInfos = lbry.getPendingPublishes().map(pendingPublishToDummyFileInfo); const dummyFileInfos = lbry
resolve([...fileInfos, ...dummyFileInfos]); .getPendingPublishes()
}, reject, reject); .map(pendingPublishToDummyFileInfo);
}); resolve([...fileInfos, ...dummyFileInfos]);
} },
reject,
reject
);
});
};
lbry.claim_list_mine = function(params={}) { lbry.claim_list_mine = function(params = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lbry.call('claim_list_mine', params, (claims) => { lbry.call(
for (let {name, channel_name, txid, nout} of claims) { 'claim_list_mine',
removePendingPublishIfNeeded({name, channel_name, outpoint: txid + ':' + nout}); params,
} claims => {
for (let { name, channel_name, txid, nout } of claims) {
removePendingPublishIfNeeded({
name,
channel_name,
outpoint: txid + ':' + nout
});
}
const dummyClaims = lbry.getPendingPublishes().map(pendingPublishToDummyClaim); const dummyClaims = lbry
resolve([...claims, ...dummyClaims]); .getPendingPublishes()
}, reject, reject) .map(pendingPublishToDummyClaim);
}); resolve([...claims, ...dummyClaims]);
} },
reject,
reject
);
});
};
const claimCacheKey = 'resolve_claim_cache'; const claimCacheKey = 'resolve_claim_cache';
lbry._claimCache = getSession(claimCacheKey, {}); lbry._claimCache = getSession(claimCacheKey, {});
lbry._resolveXhrs = {} lbry._resolveXhrs = {};
lbry.resolve = function(params={}) { lbry.resolve = function(params = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!params.uri) { if (!params.uri) {
throw __("Resolve has hacked cache on top of it that requires a URI") throw __('Resolve has hacked cache on top of it that requires a URI');
} }
if (params.uri && lbry._claimCache[params.uri] !== undefined) { if (params.uri && lbry._claimCache[params.uri] !== undefined) {
resolve(lbry._claimCache[params.uri]); resolve(lbry._claimCache[params.uri]);
} else { } else {
lbry._resolveXhrs[params.uri] = lbry.call('resolve', params, function(data) { lbry._resolveXhrs[params.uri] = lbry.call(
if (data !== undefined) { 'resolve',
lbry._claimCache[params.uri] = data; params,
} function(data) {
setSession(claimCacheKey, lbry._claimCache) if (data !== undefined) {
resolve(data) lbry._claimCache[params.uri] = data;
}, reject) }
} setSession(claimCacheKey, lbry._claimCache);
}); resolve(data);
} },
reject
);
}
});
};
lbry.cancelResolve = function(params={}) { lbry.cancelResolve = function(params = {}) {
const xhr = lbry._resolveXhrs[params.uri] const xhr = lbry._resolveXhrs[params.uri];
if (xhr && xhr.readyState > 0 && xhr.readyState < 4) { if (xhr && xhr.readyState > 0 && xhr.readyState < 4) {
xhr.abort() xhr.abort();
} }
} };
lbry = new Proxy(lbry, { lbry = new Proxy(lbry, {
get: function(target, name) { get: function(target, name) {
if (name in target) { if (name in target) {
return target[name]; return target[name];
} }
return function(params={}) { return function(params = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
jsonrpc.call(lbry.daemonConnectionString, name, params, resolve, reject, reject); jsonrpc.call(
}); lbry.daemonConnectionString,
}; name,
} params,
resolve,
reject,
reject
);
});
};
}
}); });
export default lbry; export default lbry;

View file

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

View file

@ -25,170 +25,225 @@ const lbryuri = {};
* - contentName (string): For anon claims, the name; for channel claims, the path * - contentName (string): For anon claims, the name; for channel claims, the path
* - channelName (string, if present): Channel name without @ * - channelName (string, if present): Channel name without @
*/ */
lbryuri.parse = function(uri, requireProto=false) { lbryuri.parse = function(uri, requireProto = false) {
// Break into components. Empty sub-matches are converted to null // Break into components. Empty sub-matches are converted to null
const componentsRegex = new RegExp( const componentsRegex = new RegExp(
'^((?:lbry:\/\/)?)' + // protocol '^((?:lbry://)?)' + // protocol
'([^:$#/]*)' + // name (stops at the first separator or end) '([^:$#/]*)' + // name (stops at the first separator or end)
'([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end) '([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end)
'(/?)(.*)' // path separator, path '(/?)(.*)' // path separator, path
); );
const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(uri).slice(1).map(match => match || null); const [proto, name, modSep, modVal, pathSep, path] = componentsRegex
.exec(uri)
.slice(1)
.map(match => match || null);
let contentName; let contentName;
// Validate protocol // Validate protocol
if (requireProto && !proto) { if (requireProto && !proto) {
throw new Error(__('LBRY URIs must include a protocol prefix (lbry://).')); throw new Error(__('LBRY URIs must include a protocol prefix (lbry://).'));
} }
// Validate and process name // Validate and process name
if (!name) { if (!name) {
throw new Error(__('URI does not include name.')); throw new Error(__('URI does not include name.'));
} }
const isChannel = name.startsWith('@'); const isChannel = name.startsWith('@');
const channelName = isChannel ? name.slice(1) : name; const channelName = isChannel ? name.slice(1) : name;
if (isChannel) { if (isChannel) {
if (!channelName) { if (!channelName) {
throw new Error(__('No channel name after @.')); throw new Error(__('No channel name after @.'));
} }
if (channelName.length < CHANNEL_NAME_MIN_LEN) { if (channelName.length < CHANNEL_NAME_MIN_LEN) {
throw new Error(__(`Channel names must be at least %s characters.`, CHANNEL_NAME_MIN_LEN)); throw new Error(
} __(
`Channel names must be at least %s characters.`,
CHANNEL_NAME_MIN_LEN
)
);
}
contentName = path; contentName = path;
} }
const nameBadChars = (channelName || name).match(/[^A-Za-z0-9-]/g); const nameBadChars = (channelName || name).match(/[^A-Za-z0-9-]/g);
if (nameBadChars) { if (nameBadChars) {
throw new Error(__(`Invalid character %s in name: %s.`, nameBadChars.length == 1 ? '' : 's', nameBadChars.join(', ') )); throw new Error(
} __(
`Invalid character %s in name: %s.`,
nameBadChars.length == 1 ? '' : 's',
nameBadChars.join(', ')
)
);
}
// Validate and process modifier (claim ID, bid position or claim sequence) // Validate and process modifier (claim ID, bid position or claim sequence)
let claimId, claimSequence, bidPosition; let claimId, claimSequence, bidPosition;
if (modSep) { if (modSep) {
if (!modVal) { if (!modVal) {
throw new Error(__(`No modifier provided after separator %s.`, modSep)); throw new Error(__(`No modifier provided after separator %s.`, modSep));
} }
if (modSep == '#') { if (modSep == '#') {
claimId = modVal; claimId = modVal;
} else if (modSep == ':') { } else if (modSep == ':') {
claimSequence = modVal; claimSequence = modVal;
} else if (modSep == '$') { } else if (modSep == '$') {
bidPosition = modVal; bidPosition = modVal;
} }
} }
if (claimId && (claimId.length > CLAIM_ID_MAX_LEN || !claimId.match(/^[0-9a-f]+$/))) { if (
throw new Error(__(`Invalid claim ID %s.`, claimId)); claimId &&
} (claimId.length > CLAIM_ID_MAX_LEN || !claimId.match(/^[0-9a-f]+$/))
) {
throw new Error(__(`Invalid claim ID %s.`, claimId));
}
if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) { if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) {
throw new Error(__('Claim sequence must be a number.')); throw new Error(__('Claim sequence must be a number.'));
} }
if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) { if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) {
throw new Error(__('Bid position must be a number.')); throw new Error(__('Bid position must be a number.'));
} }
// Validate and process path // Validate and process path
if (path) { if (path) {
if (!isChannel) { if (!isChannel) {
throw new Error(__('Only channel URIs may have a path.')); throw new Error(__('Only channel URIs may have a path.'));
} }
const pathBadChars = path.match(/[^A-Za-z0-9-]/g); const pathBadChars = path.match(/[^A-Za-z0-9-]/g);
if (pathBadChars) { if (pathBadChars) {
throw new Error(__(`Invalid character %s in path: %s`,count == 1 ? '' : 's',nameBadChars.join(', '))); throw new Error(
} __(
`Invalid character %s in path: %s`,
count == 1 ? '' : 's',
nameBadChars.join(', ')
)
);
}
contentName = path; contentName = path;
} else if (pathSep) { } else if (pathSep) {
throw new Error(__('No path provided after /')); throw new Error(__('No path provided after /'));
} }
return { return {
name, path, isChannel, name,
... contentName ? {contentName} : {}, path,
... channelName ? {channelName} : {}, isChannel,
... claimSequence ? {claimSequence: parseInt(claimSequence)} : {}, ...(contentName ? { contentName } : {}),
... bidPosition ? {bidPosition: parseInt(bidPosition)} : {}, ...(channelName ? { channelName } : {}),
... claimId ? {claimId} : {}, ...(claimSequence ? { claimSequence: parseInt(claimSequence) } : {}),
... path ? {path} : {}, ...(bidPosition ? { bidPosition: parseInt(bidPosition) } : {}),
}; ...(claimId ? { claimId } : {}),
} ...(path ? { path } : {})
};
};
/** /**
* Takes an object in the same format returned by lbryuri.parse() and builds a URI. * Takes an object in the same format returned by lbryuri.parse() and builds a URI.
* *
* The channelName key will accept names with or without the @ prefix. * The channelName key will accept names with or without the @ prefix.
*/ */
lbryuri.build = function(uriObj, includeProto=true, allowExtraProps=false) { lbryuri.build = function(uriObj, includeProto = true, allowExtraProps = false) {
let {name, claimId, claimSequence, bidPosition, path, contentName, channelName} = uriObj; let {
name,
claimId,
claimSequence,
bidPosition,
path,
contentName,
channelName
} = uriObj;
if (channelName) { if (channelName) {
const channelNameFormatted = channelName.startsWith('@') ? channelName : '@' + channelName; const channelNameFormatted = channelName.startsWith('@')
if (!name) { ? channelName
name = channelNameFormatted; : '@' + channelName;
} else if (name !== channelNameFormatted) { if (!name) {
throw new Error(__('Received a channel content URI, but name and channelName do not match. \"name\" represents the value in the name position of the URI (lbry://name...), which for channel content will be the channel name. In most cases, to construct a channel URI you should just pass channelName and contentName.')); name = channelNameFormatted;
} } else if (name !== channelNameFormatted) {
} throw new Error(
__(
'Received a channel content URI, but name and channelName do not match. "name" represents the value in the name position of the URI (lbry://name...), which for channel content will be the channel name. In most cases, to construct a channel URI you should just pass channelName and contentName.'
)
);
}
}
if (contentName) { if (contentName) {
if (!name) { if (!name) {
name = contentName; name = contentName;
} else if (!path) { } else if (!path) {
path = contentName; path = contentName;
} }
if (path && path !== contentName) { if (path && path !== contentName) {
throw new Error(__('Path and contentName do not match. Only one is required; most likely you wanted contentName.')); throw new Error(
} __(
} 'Path and contentName do not match. Only one is required; most likely you wanted contentName.'
)
);
}
}
return (includeProto ? 'lbry://' : '') + name + return (
(claimId ? `#${claimId}` : '') + (includeProto ? 'lbry://' : '') +
(claimSequence ? `:${claimSequence}` : '') + name +
(bidPosition ? `\$${bidPosition}` : '') + (claimId ? `#${claimId}` : '') +
(path ? `/${path}` : ''); (claimSequence ? `:${claimSequence}` : '') +
(bidPosition ? `\$${bidPosition}` : '') +
} (path ? `/${path}` : '')
);
};
/* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just /* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just
* consists of adding the lbry:// prefix if needed) */ * consists of adding the lbry:// prefix if needed) */
lbryuri.normalize= function(uri) { lbryuri.normalize = function(uri) {
const {name, path, bidPosition, claimSequence, claimId} = lbryuri.parse(uri); const { name, path, bidPosition, claimSequence, claimId } = lbryuri.parse(
return lbryuri.build({name, path, claimSequence, bidPosition, claimId}); uri
} );
return lbryuri.build({ name, path, claimSequence, bidPosition, claimId });
};
lbryuri.isValid = function(uri) { lbryuri.isValid = function(uri) {
let parts let parts;
try { try {
parts = lbryuri.parse(lbryuri.normalize(uri)) parts = lbryuri.parse(lbryuri.normalize(uri));
} catch (error) { } catch (error) {
return false; return false;
} }
return parts && parts.name; return parts && parts.name;
} };
lbryuri.isValidName = function(name, checkCase=true) { lbryuri.isValidName = function(name, checkCase = true) {
const regexp = new RegExp('^[a-z0-9-]+$', checkCase ? '' : 'i'); const regexp = new RegExp('^[a-z0-9-]+$', checkCase ? '' : 'i');
return regexp.test(name); return regexp.test(name);
} };
lbryuri.isClaimable = function(uri) { lbryuri.isClaimable = function(uri) {
let parts let parts;
try { try {
parts = lbryuri.parse(lbryuri.normalize(uri)) parts = lbryuri.parse(lbryuri.normalize(uri));
} catch (error) { } catch (error) {
return false; return false;
} }
return parts && parts.name && !parts.claimId && !parts.bidPosition && !parts.claimSequence && !parts.isChannel && !parts.path; return (
} parts &&
parts.name &&
!parts.claimId &&
!parts.bidPosition &&
!parts.claimSequence &&
!parts.isChannel &&
!parts.path
);
};
window.lbryuri = lbryuri; window.lbryuri = lbryuri;
export default lbryuri; export default lbryuri;

View file

@ -4,9 +4,9 @@ import jsonrpc from './jsonrpc.js';
const queryTimeout = 3000; const queryTimeout = 3000;
const maxQueryTries = 2; const maxQueryTries = 2;
const defaultServers = [ const defaultServers = [
'http://lighthouse7.lbry.io:50005', 'http://lighthouse7.lbry.io:50005',
'http://lighthouse8.lbry.io:50005', 'http://lighthouse8.lbry.io:50005',
'http://lighthouse9.lbry.io:50005', 'http://lighthouse9.lbry.io:50005'
]; ];
const path = '/'; const path = '/';
@ -14,48 +14,71 @@ let server = null;
let connectTryNum = 0; let connectTryNum = 0;
function getServers() { function getServers() {
return lbry.getClientSetting('useCustomLighthouseServers') return lbry.getClientSetting('useCustomLighthouseServers')
? lbry.getClientSetting('customLighthouseServers') ? lbry.getClientSetting('customLighthouseServers')
: defaultServers; : defaultServers;
} }
function call(method, params, callback, errorCallback) { function call(method, params, callback, errorCallback) {
if (connectTryNum >= maxQueryTries) { if (connectTryNum >= maxQueryTries) {
errorCallback(new Error(__(`Could not connect to Lighthouse server. Last server attempted: %s`, server))); errorCallback(
return; new Error(
} __(
`Could not connect to Lighthouse server. Last server attempted: %s`,
server
)
)
);
return;
}
/** /**
* Set the Lighthouse server if it hasn't been set yet, if the current server is not in current * Set the Lighthouse server if it hasn't been set yet, if the current server is not in current
* set of servers (most likely because of a settings change), or we're re-trying after a failed * set of servers (most likely because of a settings change), or we're re-trying after a failed
* query. * query.
*/ */
if (!server || !getServers().includes(server) || connectTryNum > 0) { if (!server || !getServers().includes(server) || connectTryNum > 0) {
// If there's a current server, filter it out so we get a new one // If there's a current server, filter it out so we get a new one
const newServerChoices = server ? getServers().filter((s) => s != server) : getServers(); const newServerChoices = server
server = newServerChoices[Math.round(Math.random() * (newServerChoices.length - 1))]; ? getServers().filter(s => s != server)
} : getServers();
server =
newServerChoices[
Math.round(Math.random() * (newServerChoices.length - 1))
];
}
jsonrpc.call(server + path, method, params, (response) => { jsonrpc.call(
connectTryNum = 0; server + path,
callback(response); method,
}, (error) => { params,
connectTryNum = 0; response => {
errorCallback(error); connectTryNum = 0;
}, () => { callback(response);
connectTryNum++; },
call(method, params, callback, errorCallback); error => {
}, queryTimeout); connectTryNum = 0;
errorCallback(error);
},
() => {
connectTryNum++;
call(method, params, callback, errorCallback);
},
queryTimeout
);
} }
const lighthouse = new Proxy({}, { const lighthouse = new Proxy(
get: function(target, name) { {},
return function(...params) { {
return new Promise((resolve, reject) => { get: function(target, name) {
call(name, params, resolve, reject); return function(...params) {
}); return new Promise((resolve, reject) => {
}; call(name, params, resolve, reject);
}, });
}); };
}
}
);
export default lighthouse; export default lighthouse;

View file

@ -8,90 +8,87 @@ import SnackBar from 'component/snackBar';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import store from 'store.js'; import store from 'store.js';
import SplashScreen from 'component/splash.js'; import SplashScreen from 'component/splash.js';
import {AuthOverlay} from 'component/auth.js'; import { AuthOverlay } from 'component/auth.js';
import { import { doChangePath, doNavigate, doDaemonReady } from 'actions/app';
doChangePath, import { doFetchDaemonSettings } from 'actions/settings';
doNavigate, import { doFileList } from 'actions/file_info';
doDaemonReady import { toQueryString } from 'util/query_params';
} from 'actions/app'
import {
doFetchDaemonSettings
} from 'actions/settings'
import {
doFileList
} from 'actions/file_info'
import {
toQueryString,
} from 'util/query_params'
const {remote, ipcRenderer, shell} = require('electron'); const { remote, ipcRenderer, shell } = require('electron');
const contextMenu = remote.require('./menu/context-menu'); const contextMenu = remote.require('./menu/context-menu');
const app = require('./app') const app = require('./app');
lbry.showMenuIfNeeded(); lbry.showMenuIfNeeded();
window.addEventListener('contextmenu', (event) => { window.addEventListener('contextmenu', event => {
contextMenu.showContextMenu(remote.getCurrentWindow(), event.x, event.y, contextMenu.showContextMenu(
lbry.getClientSetting('showDeveloperMenu')); remote.getCurrentWindow(),
event.preventDefault(); event.x,
event.y,
lbry.getClientSetting('showDeveloperMenu')
);
event.preventDefault();
}); });
window.addEventListener('popstate', (event, param) => { window.addEventListener('popstate', (event, param) => {
const params = event.state const params = event.state;
const pathParts = document.location.pathname.split('/') const pathParts = document.location.pathname.split('/');
const route = '/' + pathParts[pathParts.length - 1] const route = '/' + pathParts[pathParts.length - 1];
const queryString = toQueryString(params) const queryString = toQueryString(params);
let action let action;
if (route.match(/html$/)) { if (route.match(/html$/)) {
action = doChangePath('/discover') action = doChangePath('/discover');
} else { } else {
action = doChangePath(`${route}?${queryString}`) action = doChangePath(`${route}?${queryString}`);
} }
app.store.dispatch(action) app.store.dispatch(action);
})
ipcRenderer.on('open-uri-requested', (event, uri) => {
if (uri && uri.startsWith('lbry://')) {
app.store.dispatch(doNavigate('/show', { uri }))
}
}); });
document.addEventListener('click', (event) => { ipcRenderer.on('open-uri-requested', (event, uri) => {
var target = event.target; if (uri && uri.startsWith('lbry://')) {
while (target && target !== document) { app.store.dispatch(doNavigate('/show', { uri }));
if (target.matches('a[href^="http"]')) { }
event.preventDefault(); });
shell.openExternal(target.href);
return; document.addEventListener('click', event => {
} var target = event.target;
target = target.parentNode; 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(); const initialState = app.store.getState();
var init = function() { var init = function() {
function onDaemonReady() {
window.sessionStorage.setItem('loaded', 'y'); //once we've made it here once per session, we don't need to show splash again
const actions = [];
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 app.store.dispatch(doChangePath('/discover'));
const actions = [] app.store.dispatch(doFetchDaemonSettings());
app.store.dispatch(doFileList());
app.store.dispatch(doDaemonReady()) ReactDOM.render(
app.store.dispatch(doChangePath('/discover')) <Provider store={store}>
app.store.dispatch(doFetchDaemonSettings()) <div>{lbryio.enabled ? <AuthOverlay /> : ''}<App /><SnackBar /></div>
app.store.dispatch(doFileList()) </Provider>,
canvas
);
}
ReactDOM.render(<Provider store={store}><div>{ lbryio.enabled ? <AuthOverlay/> : '' }<App /><SnackBar /></div></Provider>, canvas) if (window.sessionStorage.getItem('loaded') == 'y') {
} onDaemonReady();
} else {
if (window.sessionStorage.getItem('loaded') == 'y') { ReactDOM.render(<SplashScreen onLoadDone={onDaemonReady} />, canvas);
onDaemonReady(); }
} else {
ReactDOM.render(<SplashScreen onLoadDone={onDaemonReady} />, canvas);
}
}; };
init(); init();

View file

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

View file

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

View file

@ -1,94 +1,144 @@
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import React from 'react'; import React from 'react';
import {FormField} from '../component/form.js'; import { FormField } from '../component/form.js';
import Link from '../component/link'; import Link from '../component/link';
const fs = require('fs'); const fs = require('fs');
const {ipcRenderer} = require('electron'); const { ipcRenderer } = require('electron');
class DeveloperPage extends React.Component { class DeveloperPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
showDeveloperMenu: lbry.getClientSetting('showDeveloperMenu'), showDeveloperMenu: lbry.getClientSetting('showDeveloperMenu'),
useCustomLighthouseServers: lbry.getClientSetting('useCustomLighthouseServers'), useCustomLighthouseServers: lbry.getClientSetting(
customLighthouseServers: lbry.getClientSetting('customLighthouseServers').join('\n'), 'useCustomLighthouseServers'
upgradePath: '', ),
}; customLighthouseServers: lbry
} .getClientSetting('customLighthouseServers')
.join('\n'),
upgradePath: ''
};
}
handleShowDeveloperMenuChange(event) { handleShowDeveloperMenuChange(event) {
lbry.setClientSetting('showDeveloperMenu', event.target.checked); lbry.setClientSetting('showDeveloperMenu', event.target.checked);
lbry.showMenuIfNeeded(); lbry.showMenuIfNeeded();
this.setState({ this.setState({
showDeveloperMenu: event.target.checked, showDeveloperMenu: event.target.checked
}); });
} }
handleUseCustomLighthouseServersChange(event) { handleUseCustomLighthouseServersChange(event) {
lbry.setClientSetting('useCustomLighthouseServers', event.target.checked); lbry.setClientSetting('useCustomLighthouseServers', event.target.checked);
this.setState({ this.setState({
useCustomLighthouseServers: event.target.checked, useCustomLighthouseServers: event.target.checked
}); });
} }
handleUpgradeFileChange(event) { handleUpgradeFileChange(event) {
this.setState({ this.setState({
upgradePath: event.target.value, upgradePath: event.target.value
}); });
} }
handleForceUpgradeClick() { handleForceUpgradeClick() {
let upgradeSent = false; let upgradeSent = false;
if (!this.state.upgradePath) { if (!this.state.upgradePath) {
alert(__('Please select a file to upgrade from')); alert(__('Please select a file to upgrade from'));
} else { } else {
try { try {
const stats = fs.lstatSync(this.state.upgradePath); const stats = fs.lstatSync(this.state.upgradePath);
if (stats.isFile()) { if (stats.isFile()) {
console.log('Starting upgrade using ' + this.state.upgradePath); console.log('Starting upgrade using ' + this.state.upgradePath);
ipcRenderer.send('upgrade', this.state.upgradePath); ipcRenderer.send('upgrade', this.state.upgradePath);
upgradeSent = true; upgradeSent = true;
} }
} } catch (e) {}
catch (e) {} if (!upgradeSent) {
if (!upgradeSent) { alert(
alert('Failed to start upgrade. Is "' + this.state.upgradePath + '" a valid path to the upgrade?'); 'Failed to start upgrade. Is "' +
} this.state.upgradePath +
} '" a valid path to the upgrade?'
} );
}
}
}
render() { render() {
return ( return (
<main> <main>
<section className="card"> <section className="card">
<h3>{__("Developer Settings")}</h3> <h3>{__('Developer Settings')}</h3>
<div className="form-row"> <div className="form-row">
<label><FormField type="checkbox" onChange={(event) => { this.handleShowDeveloperMenuChange() }} checked={this.state.showDeveloperMenu} /> {__("Show developer menu")}</label> <label>
</div> <FormField
<div className="form-row"> type="checkbox"
<label><FormField type="checkbox" onChange={(event) => { this.handleUseCustomLighthouseServersChange() }} checked={this.state.useCustomLighthouseServers} /> {__("Use custom search servers")}</label> onChange={event => {
</div> this.handleShowDeveloperMenuChange();
{this.state.useCustomLighthouseServers }}
? <div className="form-row"> checked={this.state.showDeveloperMenu}
<label> />
{__("Custom search servers (one per line)")} {' '}
<div><FormField type="textarea" className="developer-page__custom-lighthouse-servers" value={this.state.customLighthouseServers} onChange={(event) => { this.handleCustomLighthouseServersChange() }} checked={this.state.debugMode} /></div> {__('Show developer menu')}
</label> </label>
</div> </div>
: null} <div className="form-row">
</section> <label>
<section className="card"> <FormField
<div className="form-row"> type="checkbox"
<FormField name="file" ref="file" type="file" onChange={(event) => { this.handleUpgradeFileChange() }}/> onChange={event => {
&nbsp; this.handleUseCustomLighthouseServersChange();
<Link label={__("Force Upgrade")} button="alt" onClick={(event) => { this.handleForceUpgradeClick() }} /> }}
</div> checked={this.state.useCustomLighthouseServers}
</section> />
</main> {' '}
); {__('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={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={event => {
this.handleUpgradeFileChange();
}}
/>
&nbsp;
<Link
label={__('Force Upgrade')}
button="alt"
onClick={event => {
this.handleForceUpgradeClick();
}}
/>
</div>
</section>
</main>
);
}
} }
export default DeveloperPage; export default DeveloperPage;

View file

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

View file

@ -1,62 +1,74 @@
import React from 'react'; import React from 'react';
import lbryio from 'lbryio.js'; import lbryio from 'lbryio.js';
import lbryuri from 'lbryuri' import lbryuri from 'lbryuri';
import FileCard from 'component/fileCard'; import FileCard from 'component/fileCard';
import {BusyMessage} from 'component/common.js'; import { BusyMessage } from 'component/common.js';
import ToolTip from 'component/tooltip.js'; import ToolTip from 'component/tooltip.js';
const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' + const communityCategoryToolTipText =
'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' + 'Community Content is a public space where anyone can share content with the ' +
'"five" to put your content here!'); 'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
'"five" to put your content here!';
const FeaturedCategory = (props) => { const FeaturedCategory = props => {
const { const { category, names } = props;
category,
names,
} = props
return <div className="card-row card-row--small"> return (
<h3 className="card-row__header">{category} <div className="card-row card-row--small">
{category && category.match(/^community/i) && <ToolTip label={__("What's this?")} body={__(communityCategoryToolTipText)} className="tooltip--header" />} <h3 className="card-row__header">
</h3> {category}
{names && names.map(name => <FileCard key={name} displayStyle="card" uri={lbryuri.normalize(name)} />)} {category &&
</div> 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{ class DiscoverPage extends React.Component {
componentWillMount() { componentWillMount() {
this.props.fetchFeaturedUris() this.props.fetchFeaturedUris();
} }
render() { render() {
const { const { featuredUris, fetchingFeaturedUris } = this.props;
featuredUris, const failedToLoad =
fetchingFeaturedUris, !fetchingFeaturedUris &&
} = this.props (featuredUris === undefined ||
const failedToLoad = !fetchingFeaturedUris && ( (featuredUris !== undefined && Object.keys(featuredUris).length === 0));
featuredUris === undefined ||
(featuredUris !== undefined && Object.keys(featuredUris).length === 0)
)
return ( return (
<main> <main>
{ {fetchingFeaturedUris &&
fetchingFeaturedUris && <BusyMessage message={__('Fetching content')} />}
<BusyMessage message={__("Fetching content")} /> {typeof featuredUris === 'object' &&
} Object.keys(featuredUris).map(
{ category =>
typeof featuredUris === "object" && featuredUris[category].length
Object.keys(featuredUris).map(category => ( ? <FeaturedCategory
featuredUris[category].length ? <FeaturedCategory key={category} category={category} names={featuredUris[category]} /> : '' key={category}
)) category={category}
} names={featuredUris[category]}
{ />
failedToLoad && : ''
<div className="empty">{__("Failed to load landing content.")}</div> )}
} {failedToLoad &&
</main> <div className="empty">{__('Failed to load landing content.')}</div>}
) </main>
} );
}
} }
export default DiscoverPage; export default DiscoverPage;

View file

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

View file

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

View file

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

View file

@ -2,66 +2,76 @@ import React from 'react';
import lbry from 'lbry.js'; import lbry from 'lbry.js';
import lbryuri from 'lbryuri.js'; import lbryuri from 'lbryuri.js';
import Link from 'component/link'; import Link from 'component/link';
import {FormField} from 'component/form.js'; import { FormField } from 'component/form.js';
import FileTile from 'component/fileTile'; import FileTile from 'component/fileTile';
import rewards from 'rewards.js'; import rewards from 'rewards.js';
import lbryio from 'lbryio.js'; import lbryio from 'lbryio.js';
import {BusyMessage, Thumbnail} from 'component/common.js'; import { BusyMessage, Thumbnail } from 'component/common.js';
import FileList from 'component/fileList' import FileList from 'component/fileList';
import SubHeader from 'component/subHeader' import SubHeader from 'component/subHeader';
class FileListPublished extends React.Component { class FileListPublished extends React.Component {
componentWillMount() { componentWillMount() {
this.props.fetchFileListPublished() this.props.fetchFileListPublished();
} }
componentDidUpdate() { componentDidUpdate() {
if(this.props.fileInfos.length > 0) this._requestPublishReward() if (this.props.fileInfos.length > 0) this._requestPublishReward();
} }
_requestPublishReward() { _requestPublishReward() {
// TODO this is throwing an error now // TODO this is throwing an error now
// Error: LBRY internal API is disabled // Error: LBRY internal API is disabled
// //
// lbryio.call('reward', 'list', {}).then(function(userRewards) { // lbryio.call('reward', 'list', {}).then(function(userRewards) {
// //already rewarded // //already rewarded
// if (userRewards.filter(function (reward) { // if (userRewards.filter(function (reward) {
// return reward.reward_type == rewards.TYPE_FIRST_PUBLISH && reward.transaction_id // return reward.reward_type == rewards.TYPE_FIRST_PUBLISH && reward.transaction_id
// }).length) { // }).length) {
// return // return
// } // }
// else { // else {
// rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {}) // rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
// } // }
// }) // })
} }
render() { render() {
const { const { fileInfos, isPending, navigate } = this.props;
fileInfos,
isPending,
navigate,
} = this.props
let content let content;
if (fileInfos && fileInfos.length > 0) { if (fileInfos && fileInfos.length > 0) {
content = <FileList fileInfos={fileInfos} fetching={isPending} fileTileShowEmpty={FileTile.SHOW_EMPTY_PENDING} /> content = (
} else { <FileList
if (isPending) { fileInfos={fileInfos}
content = <BusyMessage message={__("Loading")} /> fetching={isPending}
} else { fileTileShowEmpty={FileTile.SHOW_EMPTY_PENDING}
content = <span>{__("It looks like you haven't published anything to LBRY yet. Go")} <Link onClick={() => navigate('/publish')} label={__("share your beautiful cats with the world")} />!</span> />
} );
} } else {
if (isPending) {
content = <BusyMessage message={__('Loading')} />;
} else {
content = (
<span>
{__("It looks like you haven't published anything to LBRY yet. Go")}
{' '}<Link
onClick={() => navigate('/publish')}
label={__('share your beautiful cats with the world')}
/>!
</span>
);
}
}
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<SubHeader /> <SubHeader />
{content} {content}
</main> </main>
) );
} }
} }
export default FileListPublished export default FileListPublished;

View file

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

View file

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

View file

@ -1,14 +1,10 @@
import React from 'react' import React from 'react';
import { import { doNavigate } from 'actions/app';
doNavigate import { connect } from 'react-redux';
} from 'actions/app' import HelpPage from './view';
import {
connect
} from 'react-redux'
import HelpPage from './view'
const perform = (dispatch) => ({ const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)), navigate: (path, params) => dispatch(doNavigate(path, params))
}) });
export default connect(null, perform)(HelpPage) export default connect(null, perform)(HelpPage);

View file

@ -2,137 +2,165 @@
import React from 'react'; import React from 'react';
import lbry from 'lbry.js'; import lbry from 'lbry.js';
import Link from 'component/link'; import Link from 'component/link';
import SubHeader from 'component/subHeader' import SubHeader from 'component/subHeader';
import {BusyMessage} from 'component/common' import { BusyMessage } from 'component/common';
class HelpPage extends React.Component { class HelpPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
versionInfo: null, versionInfo: null,
lbryId: null, lbryId: null,
uiVersion: null, uiVersion: null,
upgradeAvailable: null upgradeAvailable: null
}; };
} }
componentWillMount() { componentWillMount() {
lbry.getAppVersionInfo().then(({remoteVersion, upgradeAvailable}) => { lbry.getAppVersionInfo().then(({ remoteVersion, upgradeAvailable }) => {
this.setState({ this.setState({
uiVersion: remoteVersion, uiVersion: remoteVersion,
upgradeAvailable: upgradeAvailable upgradeAvailable: upgradeAvailable
}); });
}); });
lbry.call('version', {}, (info) => { lbry.call('version', {}, info => {
this.setState({ this.setState({
versionInfo: info versionInfo: info
}) });
}) });
lbry.getSessionInfo((info) => { lbry.getSessionInfo(info => {
this.setState({ this.setState({
lbryId: info.lbry_id, lbryId: info.lbry_id
}); });
}); });
} }
render() { render() {
let ver, osName, platform, newVerLink; let ver, osName, platform, newVerLink;
const { const { navigate } = this.props;
navigate
} = this.props
if (this.state.versionInfo) { if (this.state.versionInfo) {
ver = this.state.versionInfo; ver = this.state.versionInfo;
if (ver.os_system == 'Darwin') { if (ver.os_system == 'Darwin') {
osName = (parseInt(ver.os_release.match(/^\d+/)) < 16 ? 'Mac OS X' : 'Mac OS'); osName = parseInt(ver.os_release.match(/^\d+/)) < 16
? 'Mac OS X'
: 'Mac OS';
platform = `${osName} ${ver.os_release}` platform = `${osName} ${ver.os_release}`;
newVerLink = 'https://lbry.io/get/lbry.dmg'; newVerLink = 'https://lbry.io/get/lbry.dmg';
} else if (ver.os_system == 'Linux') { } else if (ver.os_system == 'Linux') {
platform = `Linux (${ver.platform})`; platform = `Linux (${ver.platform})`;
newVerLink = 'https://lbry.io/get/lbry.deb'; newVerLink = 'https://lbry.io/get/lbry.deb';
} else { } else {
platform = `Windows (${ver.platform})`; platform = `Windows (${ver.platform})`;
newVerLink = 'https://lbry.io/get/lbry.msi'; newVerLink = 'https://lbry.io/get/lbry.msi';
} }
} else { } else {
ver = null; ver = null;
} }
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<SubHeader /> <SubHeader />
<section className="card"> <section className="card">
<div className="card__title-primary"> <div className="card__title-primary">
<h3>{__("Read the FAQ")}</h3> <h3>{__('Read the FAQ')}</h3>
</div> </div>
<div className="card__content"> <div className="card__content">
<p>{__("Our FAQ answers many common questions.")}</p> <p>{__('Our FAQ answers many common questions.')}</p>
<p><Link href="https://lbry.io/faq" label={__("Read the FAQ")} icon="icon-question" button="alt"/></p> <p>
</div> <Link
</section> href="https://lbry.io/faq"
<section className="card"> label={__('Read the FAQ')}
<div className="card__title-primary"> icon="icon-question"
<h3>{__("Get Live Help")}</h3> button="alt"
</div> />
<div className="card__content"> </p>
<p> </div>
{__("Live help is available most hours in the")} <strong>#help</strong> {__("channel of our Slack chat room.")} </section>
</p> <section className="card">
<p> <div className="card__title-primary">
<Link button="alt" label={__("Join Our Slack")} icon="icon-slack" href="https://slack.lbry.io" /> <h3>{__('Get Live Help')}</h3>
</p> </div>
</div> <div className="card__content">
</section> <p>
<section className="card"> {__('Live help is available most hours in the')}
<div className="card__title-primary"><h3>{__("Report a Bug")}</h3></div> {' '}<strong>#help</strong>
<div className="card__content"> {' '}{__('channel of our Slack chat room.')}
<p>{__("Did you find something wrong?")}</p> </p>
<p><Link onClick={() => navigate('report')} label={__("Submit a Bug Report")} icon="icon-bug" button="alt" /></p> <p>
<div className="meta">{__("Thanks! LBRY is made by its users.")}</div> <Link
</div> button="alt"
</section> label={__('Join Our Slack')}
<section className="card"> icon="icon-slack"
<div className="card__title-primary"><h3>{__("About")}</h3></div> href="https://slack.lbry.io"
<div className="card__content"> />
{ this.state.upgradeAvailable === null ? '' : </p>
( this.state.upgradeAvailable ? </div>
<p>{__("A newer version of LBRY is available.")} <Link href={newVerLink} label={__("Download now!")} /></p> </section>
: <p>{__("Your copy of LBRY is up to date.")}</p>)} <section className="card">
{ this.state.uiVersion && ver ? <div className="card__title-primary">
<table className="table-standard"> <h3>{__('Report a Bug')}</h3>
<tbody> </div>
<tr> <div className="card__content">
<th>{__("daemon (lbrynet)")}</th> <p>{__('Did you find something wrong?')}</p>
<td>{ver.lbrynet_version}</td> <p>
</tr> <Link
<tr> onClick={() => navigate('report')}
<th>{__("wallet (lbryum)")}</th> label={__('Submit a Bug Report')}
<td>{ver.lbryum_version}</td> icon="icon-bug"
</tr> button="alt"
<tr> />
<th>{__("interface")}</th> </p>
<td>{this.state.uiVersion}</td> <div className="meta">
</tr> {__('Thanks! LBRY is made by its users.')}
<tr> </div>
<th>{__("Platform")}</th> </div>
<td>{platform}</td> </section>
</tr> <section className="card">
<tr> <div className="card__title-primary"><h3>{__('About')}</h3></div>
<th>{__("Installation ID")}</th> <div className="card__content">
<td>{this.state.lbryId}</td> {this.state.upgradeAvailable === null
</tr> ? ''
</tbody> : this.state.upgradeAvailable
</table> : ? <p>
<BusyMessage message={__("Looking up version info")} /> {__('A newer version of LBRY is available.')}
} {' '}<Link href={newVerLink} label={__('Download now!')} />
</div> </p>
</section> : <p>{__('Your copy of LBRY is up to date.')}</p>}
</main> {this.state.uiVersion && ver
); ? <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>{this.state.uiVersion}</td>
</tr>
<tr>
<th>{__('Platform')}</th>
<td>{platform}</td>
</tr>
<tr>
<th>{__('Installation ID')}</th>
<td>{this.state.lbryId}</td>
</tr>
</tbody>
</table>
: <BusyMessage message={__('Looking up version info')} />}
</div>
</section>
</main>
);
}
} }
export default HelpPage; export default HelpPage;

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,68 +1,101 @@
import React from 'react'; import React from 'react';
import Link from 'component/link'; import Link from 'component/link';
import {FormRow} from 'component/form' import { FormRow } from 'component/form';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
class ReportPage extends React.Component { class ReportPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
submitting: false, submitting: false,
modal: null, modal: null
}; };
} }
submitMessage() { submitMessage() {
if (this._messageArea.value) { if (this._messageArea.value) {
this.setState({ this.setState({
submitting: true submitting: true
}); });
lbry.reportBug(this._messageArea.value, () => { lbry.reportBug(this._messageArea.value, () => {
this.setState({ this.setState({
submitting: false, submitting: false,
modal: 'submitted', modal: 'submitted'
}); });
}); });
this._messageArea.value = ''; this._messageArea.value = '';
} }
} }
closeModal() { closeModal() {
this.setState({ this.setState({
modal: null, modal: null
}) });
} }
render() { render() {
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<section className="card"> <section className="card">
<div className="card__content"> <div className="card__content">
<h3>{__("Report an Issue")}</h3> <h3>{__('Report an Issue')}</h3>
<p>{__("Please describe the problem you experienced and any information you think might be useful to us. Links to screenshots are great!")}</p> <p>
<div className="form-row"> {__(
<FormRow type="textarea" ref={(t) => this._messageArea = t} rows="10" name="message" placeholder={__("Description of your issue")} /> 'Please describe the problem you experienced and any information you think might be useful to us. Links to screenshots are great!'
</div> )}
<div className="form-row form-row-submit"> </p>
<button onClick={(event) => { this.submitMessage(event) }} className={'button-block button-primary ' + (this.state.submitting ? 'disabled' : '')}>{this.state.submitting ? __('Submitting...') : __('Submit Report')}</button> <div className="form-row">
</div> <FormRow
</div> type="textarea"
</section> ref={t => (this._messageArea = t)}
<section className="card"> rows="10"
<div className="card__content"> name="message"
<h3>{__("Developer?")}</h3> placeholder={__('Description of your issue')}
{__("You can also")} <Link href="https://github.com/lbryio/lbry/issues" label={__("submit an issue on GitHub")}/>. />
</div> </div>
</section> <div className="form-row form-row-submit">
<Modal isOpen={this.state.modal == 'submitted'} contentLabel={__("Bug report submitted")} <button
onConfirmed={(event) => { this.closeModal(event) }}> onClick={event => {
{__("Your bug report has been submitted! Thank you for your feedback.")} this.submitMessage(event);
</Modal> }}
</main> className={
); 'button-block button-primary ' +
} (this.state.submitting ? 'disabled' : '')
}
>
{this.state.submitting
? __('Submitting...')
: __('Submit Report')}
</button>
</div>
</div>
</section>
<section className="card">
<div className="card__content">
<h3>{__('Developer?')}</h3>
{__('You can also')}
{' '}<Link
href="https://github.com/lbryio/lbry/issues"
label={__('submit an issue on GitHub')}
/>.
</div>
</section>
<Modal
isOpen={this.state.modal == 'submitted'}
contentLabel={__('Bug report submitted')}
onConfirmed={event => {
this.closeModal(event);
}}
>
{__(
'Your bug report has been submitted! Thank you for your feedback.'
)}
</Modal>
</main>
);
}
} }
export default ReportPage; export default ReportPage;

View file

@ -1,77 +1,100 @@
import React from 'react'; import React from 'react';
import lbryio from 'lbryio'; import lbryio from 'lbryio';
import {CreditAmount, Icon} from 'component/common.js'; import { CreditAmount, Icon } from 'component/common.js';
import SubHeader from 'component/subHeader' import SubHeader from 'component/subHeader';
import {RewardLink} from 'component/reward-link'; import { RewardLink } from 'component/reward-link';
export class RewardTile extends React.Component { export class RewardTile extends React.Component {
static propTypes = { static propTypes = {
type: React.PropTypes.string.isRequired, type: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired, title: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired, description: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool.isRequired, claimed: React.PropTypes.bool.isRequired,
value: React.PropTypes.number.isRequired, value: React.PropTypes.number.isRequired,
onRewardClaim: React.PropTypes.func onRewardClaim: React.PropTypes.func
} };
render() { render() {
return ( return (
<section className="card"> <section className="card">
<div className="card__inner"> <div className="card__inner">
<div className="card__title-primary"> <div className="card__title-primary">
<CreditAmount amount={this.props.value} /> <CreditAmount amount={this.props.value} />
<h3>{this.props.title}</h3> <h3>{this.props.title}</h3>
</div> </div>
<div className="card__actions"> <div className="card__actions">
{this.props.claimed {this.props.claimed
? <span><Icon icon="icon-check" /> {__("Reward claimed.")}</span> ? <span><Icon icon="icon-check" /> {__('Reward claimed.')}</span>
: <RewardLink {...this.props} />} : <RewardLink {...this.props} />}
</div> </div>
<div className="card__content">{this.props.description}</div> <div className="card__content">{this.props.description}</div>
</div> </div>
</section> </section>
); );
} }
} }
export class RewardsPage extends React.Component { export class RewardsPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
userRewards: null, userRewards: null,
failed: null, failed: null
}; };
} }
componentWillMount() { componentWillMount() {
this.loadRewards() this.loadRewards();
} }
loadRewards() { loadRewards() {
lbryio.call('reward', 'list', {}).then((userRewards) => { lbryio.call('reward', 'list', {}).then(
this.setState({ userRewards => {
userRewards: userRewards, this.setState({
}); userRewards: userRewards
}, () => { });
this.setState({failed: true }) },
}); () => {
} this.setState({ failed: true });
}
);
}
render() { render() {
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<SubHeader /> <SubHeader />
<div> <div>
{!this.state.userRewards {!this.state.userRewards
? (this.state.failed ? <div className="empty">{__("Failed to load rewards.")}</div> : '') ? this.state.failed
: this.state.userRewards.map(({reward_type, reward_title, reward_description, transaction_id, reward_amount}) => { ? <div className="empty">{__('Failed to load rewards.')}</div>
return <RewardTile key={reward_type} onRewardClaim={this.loadRewards} type={reward_type} title={__(reward_title)} description={__(reward_description)} claimed={!!transaction_id} value={reward_amount} />; : ''
})} : this.state.userRewards.map(
</div> ({
</main> reward_type,
); reward_title,
} reward_description,
transaction_id,
reward_amount
}) => {
return (
<RewardTile
key={reward_type}
onRewardClaim={this.loadRewards}
type={reward_type}
title={__(reward_title)}
description={__(reward_description)}
claimed={!!transaction_id}
value={reward_amount}
/>
);
}
)}
</div>
</main>
);
}
} }
export default RewardsPage; export default RewardsPage;

View file

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

View file

@ -1,36 +1,47 @@
import React from 'react'; import React from 'react';
import lbryuri from 'lbryuri'; import lbryuri from 'lbryuri';
import FileTile from 'component/fileTile' import FileTile from 'component/fileTile';
import FileListSearch from 'component/fileListSearch' import FileListSearch from 'component/fileListSearch';
import {ToolTip} from 'component/tooltip.js'; import { ToolTip } from 'component/tooltip.js';
import {BusyMessage} from 'component/common.js'; import { BusyMessage } from 'component/common.js';
class SearchPage extends React.Component {
render() {
const { query } = this.props;
class SearchPage extends React.Component{ return (
render() { <main className="main--single-column">
const { {lbryuri.isValid(query)
query, ? <section className="section-spaced">
} = this.props <h3 className="card-row__header">
{__('Exact URL')}
return ( {' '}<ToolTip
<main className="main--single-column"> label="?"
{ lbryuri.isValid(query) ? body={__(
<section className="section-spaced"> 'This is the resolution of a LBRY URL and not controlled by LBRY Inc.'
<h3 className="card-row__header"> )}
{__("Exact URL")} <ToolTip label="?" body={__("This is the resolution of a LBRY URL and not controlled by LBRY Inc.")} className="tooltip--header"
className="tooltip--header" /> />
</h3> </h3>
<FileTile uri={lbryuri.normalize(query)} showEmpty={FileTile.SHOW_EMPTY_PUBLISH} /> <FileTile
</section> : '' } uri={lbryuri.normalize(query)}
<section className="section-spaced"> showEmpty={FileTile.SHOW_EMPTY_PUBLISH}
<h3 className="card-row__header"> />
{__("Search Results for")} {query} <ToolTip label="?" body={__("These search results are provided by LBRY, Inc.")} </section>
className="tooltip--header" /> : ''}
</h3> <section className="section-spaced">
<FileListSearch query={query} /> <h3 className="card-row__header">
</section> {__('Search Results for')} {query}
</main> {' '}<ToolTip
) label="?"
} body={__('These search results are provided by LBRY, Inc.')}
className="tooltip--header"
/>
</h3>
<FileListSearch query={query} />
</section>
</main>
);
}
} }
export default SearchPage; export default SearchPage;

View file

@ -1,21 +1,15 @@
import React from 'react' import React from 'react';
import { import { connect } from 'react-redux';
connect import { doSetDaemonSetting } from 'actions/settings';
} from 'react-redux' import { selectDaemonSettings } from 'selectors/settings';
import { import SettingsPage from './view';
doSetDaemonSetting
} from 'actions/settings'
import {
selectDaemonSettings
} from 'selectors/settings'
import SettingsPage from './view'
const select = (state) => ({ const select = state => ({
daemonSettings: selectDaemonSettings(state) daemonSettings: selectDaemonSettings(state)
}) });
const perform = (dispatch) => ({ const perform = dispatch => ({
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)), setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value))
}) });
export default connect(select, perform)(SettingsPage) export default connect(select, perform)(SettingsPage);

View file

@ -1,94 +1,93 @@
import React from 'react'; import React from 'react';
import {FormField, FormRow} from 'component/form.js'; import { FormField, FormRow } from 'component/form.js';
import SubHeader from 'component/subHeader' import SubHeader from 'component/subHeader';
import lbry from 'lbry.js'; import lbry from 'lbry.js';
class SettingsPage extends React.Component { class SettingsPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const daemonSettings = this.props.daemonSettings const daemonSettings = this.props.daemonSettings;
this.state = { this.state = {
isMaxUpload: daemonSettings && daemonSettings.max_upload != 0, isMaxUpload: daemonSettings && daemonSettings.max_upload != 0,
isMaxDownload: daemonSettings && daemonSettings.max_download != 0, isMaxDownload: daemonSettings && daemonSettings.max_download != 0,
showNsfw: lbry.getClientSetting('showNsfw'), showNsfw: lbry.getClientSetting('showNsfw'),
showUnavailable: lbry.getClientSetting('showUnavailable'), showUnavailable: lbry.getClientSetting('showUnavailable'),
language: lbry.getClientSetting('language'), language: lbry.getClientSetting('language')
} };
} }
setDaemonSetting(name, value) { setDaemonSetting(name, value) {
this.props.setDaemonSetting(name, value) this.props.setDaemonSetting(name, value);
} }
setClientSetting(name, value) { setClientSetting(name, value) {
lbry.setClientSetting(name, value) lbry.setClientSetting(name, value);
this._onSettingSaveSuccess() this._onSettingSaveSuccess();
} }
onRunOnStartChange(event) { onRunOnStartChange(event) {
this.setDaemonSetting('run_on_startup', event.target.checked); this.setDaemonSetting('run_on_startup', event.target.checked);
} }
onShareDataChange(event) { onShareDataChange(event) {
this.setDaemonSetting('share_usage_data', event.target.checked); this.setDaemonSetting('share_usage_data', event.target.checked);
} }
onDownloadDirChange(event) { onDownloadDirChange(event) {
this.setDaemonSetting('download_directory', event.target.value); this.setDaemonSetting('download_directory', event.target.value);
} }
onMaxUploadPrefChange(isLimited) { onMaxUploadPrefChange(isLimited) {
if (!isLimited) { if (!isLimited) {
this.setDaemonSetting('max_upload', 0.0); this.setDaemonSetting('max_upload', 0.0);
} }
this.setState({ this.setState({
isMaxUpload: isLimited isMaxUpload: isLimited
}); });
} }
onMaxUploadFieldChange(event) { onMaxUploadFieldChange(event) {
this.setDaemonSetting('max_upload', Number(event.target.value)); this.setDaemonSetting('max_upload', Number(event.target.value));
} }
onMaxDownloadPrefChange(isLimited) { onMaxDownloadPrefChange(isLimited) {
if (!isLimited) { if (!isLimited) {
this.setDaemonSetting('max_download', 0.0); this.setDaemonSetting('max_download', 0.0);
} }
this.setState({ this.setState({
isMaxDownload: isLimited isMaxDownload: isLimited
}); });
} }
onMaxDownloadFieldChange(event) { onMaxDownloadFieldChange(event) {
this.setDaemonSetting('max_download', Number(event.target.value)); this.setDaemonSetting('max_download', Number(event.target.value));
} }
onShowNsfwChange(event) { onShowNsfwChange(event) {
lbry.setClientSetting('showNsfw', event.target.checked); lbry.setClientSetting('showNsfw', event.target.checked);
} }
// onLanguageChange(language) { // onLanguageChange(language) {
// lbry.setClientSetting('language', language); // lbry.setClientSetting('language', language);
// i18n.setLocale(language); // i18n.setLocale(language);
// this.setState({language: language}) // this.setState({language: language})
// } // }
onShowUnavailableChange(event) { onShowUnavailableChange(event) {}
} render() {
const { daemonSettings } = this.props;
render() { if (!daemonSettings) {
const { return (
daemonSettings <main className="main--single-column">
} = this.props <span className="empty">{__('Failed to load settings.')}</span>
</main>
if (!daemonSettings) { );
return <main className="main--single-column"><span className="empty">{__("Failed to load settings.")}</span></main>; }
} /*
/*
<section className="card"> <section className="card">
<div className="card__content"> <div className="card__content">
<h3>Run on Startup</h3> <h3>Run on Startup</h3>
@ -101,100 +100,136 @@ class SettingsPage extends React.Component {
</div> </div>
</section> </section>
*/ */
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<SubHeader /> <SubHeader />
<section className="card"> <section className="card">
<div className="card__content"> <div className="card__content">
<h3>{__("Download Directory")}</h3> <h3>{__('Download Directory')}</h3>
</div> </div>
<div className="card__content"> <div className="card__content">
<FormRow type="directory" <FormRow
name="download_directory" type="directory"
defaultValue={daemonSettings.download_directory} name="download_directory"
helper={__("LBRY downloads will be saved here.")} defaultValue={daemonSettings.download_directory}
onChange={this.onDownloadDirChange.bind(this)} /> helper={__('LBRY downloads will be saved here.')}
</div> onChange={this.onDownloadDirChange.bind(this)}
</section> />
<section className="card"> </div>
<div className="card__content"> </section>
<h3>{__("Bandwidth Limits")}</h3> <section className="card">
</div> <div className="card__content">
<div className="card__content"> <h3>{__('Bandwidth Limits')}</h3>
<div className="form-row__label-row"><div className="form-field__label">{__("Max Upload")}</div></div> </div>
<FormRow type="radio" <div className="card__content">
name="max_upload_pref" <div className="form-row__label-row">
onChange={() => { this.onMaxUploadPrefChange(false) }} <div className="form-field__label">{__('Max Upload')}</div>
defaultChecked={!this.state.isMaxUpload} </div>
label={__("Unlimited")} /> <FormRow
<div className="form-row"> type="radio"
<FormField type="radio" name="max_upload_pref"
name="max_upload_pref" onChange={() => {
onChange={() => { this.onMaxUploadPrefChange(true) }} this.onMaxUploadPrefChange(false);
defaultChecked={this.state.isMaxUpload} }}
label={ this.state.isMaxUpload ? __("Up to") : __("Choose limit...") } /> defaultChecked={!this.state.isMaxUpload}
{ this.state.isMaxUpload ? label={__('Unlimited')}
<FormField type="number" />
min="0" <div className="form-row">
step=".5" <FormField
defaultValue={daemonSettings.max_upload} type="radio"
placeholder="10" name="max_upload_pref"
className="form-field__input--inline" onChange={() => {
onChange={this.onMaxUploadFieldChange.bind(this)} this.onMaxUploadPrefChange(true);
/> }}
: '' defaultChecked={this.state.isMaxUpload}
label={
this.state.isMaxUpload ? __('Up to') : __('Choose limit...')
}
/>
{this.state.isMaxUpload
? <FormField
type="number"
min="0"
step=".5"
defaultValue={daemonSettings.max_upload}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxUploadFieldChange.bind(this)}
/>
: ''}
{this.state.isMaxUpload
? <span className="form-field__label">MB/s</span>
: ''}
</div>
</div>
<div className="card__content">
<div className="form-row__label-row">
<div className="form-field__label">{__('Max Download')}</div>
</div>
<FormRow
label={__('Unlimited')}
type="radio"
name="max_download_pref"
onChange={() => {
this.onMaxDownloadPrefChange(false);
}}
defaultChecked={!this.state.isMaxDownload}
/>
<div className="form-row">
<FormField
type="radio"
name="max_download_pref"
onChange={() => {
this.onMaxDownloadPrefChange(true);
}}
defaultChecked={this.state.isMaxDownload}
label={
this.state.isMaxDownload ? __('Up to') : __('Choose limit...')
}
/>
{this.state.isMaxDownload
? <FormField
type="number"
min="0"
step=".5"
defaultValue={daemonSettings.max_download}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxDownloadFieldChange.bind(this)}
/>
: ''}
{this.state.isMaxDownload
? <span className="form-field__label">MB/s</span>
: ''}
</div>
</div>
</section>
<section className="card">
<div className="card__content">
<h3>{__('Content')}</h3>
</div>
<div className="card__content">
<FormRow
type="checkbox"
onChange={this.onShowUnavailableChange.bind(this)}
defaultChecked={this.state.showUnavailable}
label={__('Show unavailable content in search results')}
/>
</div>
<div className="card__content">
<FormRow
label={__('Show NSFW content')}
type="checkbox"
onChange={this.onShowNsfwChange.bind(this)}
defaultChecked={this.state.showNsfw}
helper={__(
'NSFW content may include nudity, intense sexuality, profanity, or other adult content. By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction. '
)}
/>
</div>
</section>
} {/*}
{ this.state.isMaxUpload ? <span className="form-field__label">MB/s</span> : '' }
</div>
</div>
<div className="card__content">
<div className="form-row__label-row"><div className="form-field__label">{__("Max Download")}</div></div>
<FormRow label={__("Unlimited")}
type="radio"
name="max_download_pref"
onChange={() => { this.onMaxDownloadPrefChange(false) }}
defaultChecked={!this.state.isMaxDownload} />
<div className="form-row">
<FormField type="radio"
name="max_download_pref"
onChange={() => { this.onMaxDownloadPrefChange(true) }}
defaultChecked={this.state.isMaxDownload}
label={ this.state.isMaxDownload ? __("Up to") : __("Choose limit...") } />
{ this.state.isMaxDownload ?
<FormField type="number"
min="0"
step=".5"
defaultValue={daemonSettings.max_download}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxDownloadFieldChange.bind(this)}
/>
: ''
}
{ this.state.isMaxDownload ? <span className="form-field__label">MB/s</span> : '' }
</div>
</div>
</section>
<section className="card">
<div className="card__content">
<h3>{__("Content")}</h3>
</div>
<div className="card__content">
<FormRow type="checkbox"
onChange={this.onShowUnavailableChange.bind(this)}
defaultChecked={this.state.showUnavailable}
label={__("Show unavailable content in search results")} />
</div>
<div className="card__content">
<FormRow label={__("Show NSFW content")} type="checkbox"
onChange={this.onShowNsfwChange.bind(this)} defaultChecked={this.state.showNsfw}
helper={__("NSFW content may include nudity, intense sexuality, profanity, or other adult content. By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction. ")} />
</div>
</section>
{/*}
<section className="card"> <section className="card">
<div className="card__content"> <div className="card__content">
<h3>{__("Language")}</h3> <h3>{__("Language")}</h3>
@ -217,20 +252,24 @@ class SettingsPage extends React.Component {
</div> </div>
</section>*/} </section>*/}
<section className="card"> <section className="card">
<div className="card__content"> <div className="card__content">
<h3>{__("Share Diagnostic Data")}</h3> <h3>{__('Share Diagnostic Data')}</h3>
</div> </div>
<div className="card__content"> <div className="card__content">
<FormRow type="checkbox" <FormRow
onChange={this.onShareDataChange.bind(this)} type="checkbox"
defaultChecked={daemonSettings.share_usage_data} onChange={this.onShareDataChange.bind(this)}
label={__("Help make LBRY better by contributing diagnostic data about my usage")} /> defaultChecked={daemonSettings.share_usage_data}
</div> label={__(
</section> 'Help make LBRY better by contributing diagnostic data about my usage'
</main> )}
); />
} </div>
</section>
</main>
);
}
} }
export default SettingsPage; export default SettingsPage;

View file

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

View file

@ -1,64 +1,57 @@
import React from 'react'; import React from 'react';
import lbryuri from 'lbryuri' import lbryuri from 'lbryuri';
import { import { BusyMessage } from 'component/common';
BusyMessage, import ChannelPage from 'page/channel';
} from 'component/common'; import FilePage from 'page/filePage';
import ChannelPage from 'page/channel'
import FilePage from 'page/filePage'
class ShowPage extends React.Component{ class ShowPage extends React.Component {
componentWillMount() { componentWillMount() {
this.resolve(this.props) this.resolve(this.props);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.resolve(nextProps) this.resolve(nextProps);
} }
resolve(props) { resolve(props) {
const { const { isResolvingUri, resolveUri, claim, uri } = props;
isResolvingUri,
resolveUri,
claim,
uri,
} = props
if(!isResolvingUri && claim === undefined && uri) { if (!isResolvingUri && claim === undefined && uri) {
resolveUri(uri) resolveUri(uri);
} }
} }
render() { render() {
const { const { claim, uri, isResolvingUri } = this.props;
claim,
uri,
isResolvingUri,
} = this.props
let innerContent = ""; let innerContent = '';
if (isResolvingUri || !claim) { if (isResolvingUri || !claim) {
innerContent = <section className="card"> innerContent = (
<div className="card__inner"> <section className="card">
<div className="card__title-identity"><h1>{uri}</h1></div> <div className="card__inner">
</div> <div className="card__title-identity"><h1>{uri}</h1></div>
<div className="card__content"> </div>
{ isResolvingUri && <BusyMessage message={__("Loading magic decentralized data...")} /> } <div className="card__content">
{ claim === null && <span className="empty">{__("There's nothing at this location.")}</span> } {isResolvingUri &&
</div> <BusyMessage
</section> message={__('Loading magic decentralized data...')}
} />}
else if (claim.name.length && claim.name[0] === '@') { {claim === null &&
innerContent = <ChannelPage uri={uri} /> <span className="empty">
} {__("There's nothing at this location.")}
else if (claim) { </span>}
innerContent = <FilePage uri={uri} /> </div>
} </section>
);
} else if (claim.name.length && claim.name[0] === '@') {
innerContent = <ChannelPage uri={uri} />;
} else if (claim) {
innerContent = <FilePage uri={uri} />;
}
return ( return <main className="main--single-column">{innerContent}</main>;
<main className="main--single-column">{innerContent}</main> }
)
}
} }
export default ShowPage export default ShowPage;

View file

@ -2,18 +2,18 @@ import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
class StartPage extends React.Component { class StartPage extends React.Component {
componentWillMount() { componentWillMount() {
lbry.stop(); lbry.stop();
} }
render() { render() {
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<h3>{__("LBRY is Closed")}</h3> <h3>{__('LBRY is Closed')}</h3>
<Link href="lbry://lbry" label={__("Click here to start LBRY")} /> <Link href="lbry://lbry" label={__('Click here to start LBRY')} />
</main> </main>
); );
} }
} }
export default StartPage; export default StartPage;

View file

@ -1,18 +1,12 @@
import React from 'react' import React from 'react';
import { import { connect } from 'react-redux';
connect import { selectCurrentPage } from 'selectors/app';
} from 'react-redux' import { selectBalance } from 'selectors/wallet';
import { import WalletPage from './view';
selectCurrentPage
} from 'selectors/app'
import {
selectBalance
} from 'selectors/wallet'
import WalletPage from './view'
const select = (state) => ({ const select = state => ({
currentPage: selectCurrentPage(state), currentPage: selectCurrentPage(state),
balance: selectBalance(state) balance: selectBalance(state)
}) });
export default connect(select, null)(WalletPage) export default connect(select, null)(WalletPage);

View file

@ -1,35 +1,30 @@
import React from 'react'; import React from 'react';
import SubHeader from 'component/subHeader' import SubHeader from 'component/subHeader';
import TransactionList from 'component/transactionList' import TransactionList from 'component/transactionList';
import WalletAddress from 'component/walletAddress' import WalletAddress from 'component/walletAddress';
import WalletSend from 'component/walletSend' import WalletSend from 'component/walletSend';
import { import { CreditAmount } from 'component/common';
CreditAmount
} from 'component/common';
const WalletPage = (props) => { const WalletPage = props => {
const { const { balance, currentPage } = props;
balance,
currentPage
} = props
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<SubHeader /> <SubHeader />
<section className="card"> <section className="card">
<div className="card__title-primary"> <div className="card__title-primary">
<h3>{__("Balance")}</h3> <h3>{__('Balance')}</h3>
</div> </div>
<div className="card__content"> <div className="card__content">
<CreditAmount amount={balance} precision={8} /> <CreditAmount amount={balance} precision={8} />
</div> </div>
</section> </section>
{ currentPage === 'wallet' ? <TransactionList {...props} /> : '' } {currentPage === 'wallet' ? <TransactionList {...props} /> : ''}
{ currentPage === 'send' ? <WalletSend {...props} /> : '' } {currentPage === 'send' ? <WalletSend {...props} /> : ''}
{ currentPage === 'receive' ? <WalletAddress /> : '' } {currentPage === 'receive' ? <WalletAddress /> : ''}
</main> </main>
) );
} };
export default WalletPage; export default WalletPage;

View file

@ -1,132 +1,127 @@
import * as types from 'constants/action_types' import * as types from 'constants/action_types';
import lbry from 'lbry' import lbry from 'lbry';
const reducers = {} const reducers = {};
const defaultState = { const defaultState = {
isLoaded: false, isLoaded: false,
currentPath: 'discover', currentPath: 'discover',
platform: process.platform, platform: process.platform,
upgradeSkipped: sessionStorage.getItem('upgradeSkipped'), upgradeSkipped: sessionStorage.getItem('upgradeSkipped'),
daemonReady: false, daemonReady: false,
obscureNsfw: !lbry.getClientSetting('showNsfw'), obscureNsfw: !lbry.getClientSetting('showNsfw'),
hasSignature: false, hasSignature: false
} };
reducers[types.DAEMON_READY] = function(state, action) { reducers[types.DAEMON_READY] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
daemonReady: true, daemonReady: true
}) });
} };
reducers[types.CHANGE_PATH] = function(state, action) { reducers[types.CHANGE_PATH] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
currentPath: action.data.path, currentPath: action.data.path
}) });
} };
reducers[types.UPGRADE_CANCELLED] = function(state, action) { reducers[types.UPGRADE_CANCELLED] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
downloadProgress: null, downloadProgress: null,
upgradeDownloadComplete: false, upgradeDownloadComplete: false,
modal: null, modal: null
}) });
} };
reducers[types.UPGRADE_DOWNLOAD_COMPLETED] = function(state, action) { reducers[types.UPGRADE_DOWNLOAD_COMPLETED] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
downloadPath: action.data.path, downloadPath: action.data.path,
upgradeDownloading: false, upgradeDownloading: false,
upgradeDownloadCompleted: true upgradeDownloadCompleted: true
}) });
} };
reducers[types.UPGRADE_DOWNLOAD_STARTED] = function(state, action) { reducers[types.UPGRADE_DOWNLOAD_STARTED] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
upgradeDownloading: true upgradeDownloading: true
}) });
} };
reducers[types.SKIP_UPGRADE] = function(state, action) { reducers[types.SKIP_UPGRADE] = function(state, action) {
sessionStorage.setItem('upgradeSkipped', true); sessionStorage.setItem('upgradeSkipped', true);
return Object.assign({}, state, { return Object.assign({}, state, {
upgradeSkipped: true, upgradeSkipped: true,
modal: null modal: null
}) });
} };
reducers[types.UPDATE_VERSION] = function(state, action) { reducers[types.UPDATE_VERSION] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
version: action.data.version version: action.data.version
}) });
} };
reducers[types.OPEN_MODAL] = function(state, action) { reducers[types.OPEN_MODAL] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
modal: action.data.modal, modal: action.data.modal,
modalExtraContent: action.data.extraContent modalExtraContent: action.data.extraContent
}) });
} };
reducers[types.CLOSE_MODAL] = function(state, action) { reducers[types.CLOSE_MODAL] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
modal: undefined, modal: undefined,
modalExtraContent: undefined modalExtraContent: undefined
}) });
} };
reducers[types.UPGRADE_DOWNLOAD_PROGRESSED] = function(state, action) { reducers[types.UPGRADE_DOWNLOAD_PROGRESSED] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
downloadProgress: action.data.percent downloadProgress: action.data.percent
}) });
} };
reducers[types.DAEMON_READY] = function(state, action) { reducers[types.DAEMON_READY] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
daemonReady: true daemonReady: true
}) });
} };
reducers[types.SHOW_SNACKBAR] = function(state, action) { reducers[types.SHOW_SNACKBAR] = function(state, action) {
const { const { message, linkText, linkTarget, isError } = action.data;
message, const snackBar = Object.assign({}, state.snackBar);
linkText, const snacks = Object.assign([], snackBar.snacks);
linkTarget, snacks.push({
isError, message,
} = action.data linkText,
const snackBar = Object.assign({}, state.snackBar) linkTarget,
const snacks = Object.assign([], snackBar.snacks) isError
snacks.push({ });
message, const newSnackBar = Object.assign({}, snackBar, {
linkText, snacks
linkTarget, });
isError,
})
const newSnackBar = Object.assign({}, snackBar, {
snacks,
})
return Object.assign({}, state, { return Object.assign({}, state, {
snackBar: newSnackBar, snackBar: newSnackBar
}) });
} };
reducers[types.REMOVE_SNACKBAR_SNACK] = function(state, action) { reducers[types.REMOVE_SNACKBAR_SNACK] = function(state, action) {
const snackBar = Object.assign({}, state.snackBar) const snackBar = Object.assign({}, state.snackBar);
const snacks = Object.assign([], snackBar.snacks) const snacks = Object.assign([], snackBar.snacks);
snacks.shift() snacks.shift();
const newSnackBar = Object.assign({}, snackBar, { const newSnackBar = Object.assign({}, snackBar, {
snacks, snacks
}) });
return Object.assign({}, state, { return Object.assign({}, state, {
snackBar: newSnackBar, snackBar: newSnackBar
}) });
} };
export default function reducer(state = defaultState, action) { export default function reducer(state = defaultState, action) {
const handler = reducers[action.type]; const handler = reducers[action.type];
if (handler) return handler(state, action); if (handler) return handler(state, action);
return state; return state;
} }

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