Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Mayesters 2017-06-12 11:11:13 +02:00
commit e83ace19c5
162 changed files with 15033 additions and 5196 deletions

View file

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

1
.gitignore vendored
View file

@ -16,3 +16,4 @@ dist
.#*
build/daemon.zip
.vimrc

View file

@ -27,6 +27,28 @@ Web UI version numbers should always match the corresponding version of LBRY App
*
*
## [0.12.0] - 2017-06-09
### Added
* More file types, like audio and documents, can be streamed and/or served from the app
* App is no longer gated. Reward authorization re-written. Added basic flows for new users.
* Videos now have a classy loading spinner
### Changed
* All UI strings are now rendered according to gettext standard, in prep for i18n
* Switched to new fee metadata format
### Fixed
* If a daemon is running but unresponsive, startup is no longer blocked indefinitely
* Updated deprecated LBRY API call signatures
* App scrolls to the top of the page on navigation
* Download progress works properly for purchased but deleted files
* Publish channels for less than 1 LBC
## [0.11.9] - 2017-06-01
### Fixed

View file

@ -48,3 +48,7 @@ to create distributable packages, which is run by calling:
This project has currently only been worked on in Linux and macOS. If you are on Windows, you can
checkout out the build steps in [appveyor.yml](https://github.com/lbryio/lbry-app/blob/master/.appveyor.yml) and probably figure out something from there.
## Internationalization
If you want to help translating the lbry-app, you can copy the en.json file in /app/locales and modify the values while leaving the keys as their original English strings. An example for this would be: `"Skip": "Überspringen",` Translations should automatically show up in options.

View file

@ -21,7 +21,13 @@ const VERSION_CHECK_INTERVAL = 30 * 60 * 1000;
const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest';
let client = jayson.client.http('http://localhost:5279/lbryapi');
let client = jayson.client.http({
host: 'localhost',
port: 5279,
path: '/lbryapi',
timeout: 1000
});
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

1470
app/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "LBRY",
"version": "0.11.9",
"version": "0.12.0",
"main": "main.js",
"description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.",
"author": {

View file

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

1764
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -32,11 +32,15 @@
},
"backgroundColor": "155B4A"
},
"protocols": [{
"name": "lbry",
"role": "Viewer",
"schemes": ["lbry"]
}],
"protocols": [
{
"name": "lbry",
"role": "Viewer",
"schemes": [
"lbry"
]
}
],
"linux": {
"target": "deb",
"desktop": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,36 +1,116 @@
import * as types from 'constants/action_types'
import lbry from 'lbry'
import lbryio from 'lbryio';
import rewards from 'rewards'
import * as types from "constants/action_types";
import lbry from "lbry";
import lbryio from "lbryio";
import rewards from "rewards";
import { selectRewards, selectRewardsByType } from "selectors/rewards";
export function doFetchRewards() {
export function doRewardList() {
return function(dispatch, getState) {
const state = getState()
const state = getState();
dispatch({
type: types.FETCH_REWARDS_STARTED,
})
lbryio.call('reward', 'list', {}).then(function(userRewards) {
dispatch({
type: types.FETCH_REWARDS_COMPLETED,
data: { userRewards }
})
});
}
lbryio
.call("reward", "list", {})
.then(userRewards => {
dispatch({
type: types.FETCH_REWARDS_COMPLETED,
data: { userRewards },
});
})
.catch(() => {
dispatch({
type: types.FETCH_REWARDS_COMPLETED,
data: { userRewards: [] },
});
});
};
}
export function doClaimReward(rewardType) {
export function doClaimRewardType(rewardType) {
return function(dispatch, getState) {
try {
rewards.claimReward(rewards[rewardType])
dispatch({
type: types.REWARD_CLAIMED,
data: {
reward: rewards[rewardType]
}
})
} catch(err) {
const rewardsByType = selectRewardsByType(getState()),
reward = rewardsByType[rewardType];
if (reward) {
dispatch(doClaimReward(reward));
}
}
};
}
export function doClaimReward(reward, saveError = false) {
return function(dispatch, getState) {
if (reward.transaction_id) {
//already claimed, do nothing
return;
}
dispatch({
type: types.CLAIM_REWARD_STARTED,
data: { reward },
});
const success = reward => {
dispatch({
type: types.CLAIM_REWARD_SUCCESS,
data: {
reward,
},
});
};
const failure = error => {
dispatch({
type: types.CLAIM_REWARD_FAILURE,
data: {
reward,
error: saveError ? error : null,
},
});
};
rewards.claimReward(reward.reward_type).then(success, failure);
};
}
export function doClaimEligiblePurchaseRewards() {
return function(dispatch, getState) {
if (!lbryio.enabled || !lbryio.getAccessToken()) {
return;
}
const rewardsByType = selectRewardsByType(getState());
let types = {};
types[rewards.TYPE_FIRST_STREAM] = false;
types[rewards.TYPE_FEATURED_DOWNLOAD] = false;
types[rewards.TYPE_MANY_DOWNLOADS] = false;
Object.values(rewardsByType).forEach(reward => {
if (types[reward.reward_type] === false && reward.transaction_id) {
types[reward.reward_type] = true;
}
});
let unclaimedType = Object.keys(types).find(type => {
return types[type] === false && type !== rewards.TYPE_FEATURED_DOWNLOAD; //handled below
});
if (unclaimedType) {
dispatch(doClaimRewardType(unclaimedType));
}
if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) {
dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD));
}
};
}
export function doClaimRewardClearError(reward) {
return function(dispatch, getState) {
dispatch({
type: types.CLAIM_REWARD_CLEAR_ERROR,
data: { reward },
});
};
}

View file

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

View file

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

132
ui/js/actions/user.js Normal file
View file

@ -0,0 +1,132 @@
import * as types from "constants/action_types";
import lbryio from "lbryio";
import { setLocal } from "utils";
import { doRewardList } from "actions/rewards";
import { selectEmailToVerify } from "selectors/user";
export function doAuthenticate() {
return function(dispatch, getState) {
dispatch({
type: types.AUTHENTICATION_STARTED,
});
lbryio
.authenticate()
.then(user => {
dispatch({
type: types.AUTHENTICATION_SUCCESS,
data: { user },
});
dispatch(doRewardList()); //FIXME - where should this happen?
})
.catch(error => {
dispatch({
type: types.AUTHENTICATION_FAILURE,
data: { error },
});
});
};
}
export function doUserFetch() {
return function(dispatch, getState) {
dispatch({
type: types.USER_FETCH_STARTED,
});
lbryio.setCurrentUser(
user => {
dispatch({
type: types.USER_FETCH_SUCCESS,
data: { user },
});
},
error => {
dispatch({
type: types.USER_FETCH_FAILURE,
data: { error },
});
}
);
};
}
export function doUserEmailNew(email) {
return function(dispatch, getState) {
dispatch({
type: types.USER_EMAIL_NEW_STARTED,
email: email,
});
lbryio.call("user_email", "new", { email }, "post").then(
() => {
dispatch({
type: types.USER_EMAIL_NEW_SUCCESS,
data: { email },
});
dispatch(doUserFetch());
},
error => {
if (
error.xhr &&
(error.xhr.status == 409 ||
error.message == "This email is already in use")
) {
dispatch({
type: types.USER_EMAIL_NEW_EXISTS,
data: { email },
});
} else {
dispatch({
type: types.USER_EMAIL_NEW_FAILURE,
data: { error: error.message },
});
}
}
);
};
}
export function doUserEmailDecline() {
return function(dispatch, getState) {
setLocal("user_email_declined", true);
dispatch({
type: types.USER_EMAIL_DECLINE,
});
};
}
export function doUserEmailVerify(verificationToken) {
return function(dispatch, getState) {
const email = selectEmailToVerify(getState());
dispatch({
type: types.USER_EMAIL_VERIFY_STARTED,
code: verificationToken,
});
const failure = error => {
dispatch({
type: types.USER_EMAIL_VERIFY_FAILURE,
data: { error: error.message },
});
};
lbryio
.call(
"user_email",
"confirm",
{ verification_token: verificationToken, email: email },
"post"
)
.then(userEmail => {
if (userEmail.is_verified) {
dispatch({
type: types.USER_EMAIL_VERIFY_SUCCESS,
data: { email },
});
dispatch(doUserFetch());
} else {
failure(new Error("Your email is still not verified.")); //shouldn't happen?
}
}, failure);
};
}

View file

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

View file

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

View file

@ -1,26 +1,19 @@
import React from 'react';
import { connect } from 'react-redux'
import React from "react";
import { connect } from "react-redux";
import {
selectCurrentModal,
} from 'selectors/app'
import {
doCheckUpgradeAvailable,
doAlertError,
} from 'actions/app'
import {
doUpdateBalance,
} from 'actions/wallet'
import App from './view'
import { selectCurrentModal } from "selectors/app";
import { doCheckUpgradeAvailable, doAlertError } from "actions/app";
import { doUpdateBalance } from "actions/wallet";
import App from "./view";
const select = (state) => ({
const select = state => ({
modal: selectCurrentModal(state),
})
});
const perform = (dispatch) => ({
alertError: (errorList) => dispatch(doAlertError(errorList)),
const perform = dispatch => ({
alertError: errorList => dispatch(doAlertError(errorList)),
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,44 @@
import React from 'react'
import Router from 'component/router'
import Header from 'component/header';
import ErrorModal from 'component/errorModal'
import DownloadingModal from 'component/downloadingModal'
import UpgradeModal from 'component/upgradeModal'
import lbry from 'lbry'
import {Line} from 'rc-progress'
import React from "react";
import Router from "component/router";
import Header from "component/header";
import ErrorModal from "component/errorModal";
import DownloadingModal from "component/downloadingModal";
import UpgradeModal from "component/upgradeModal";
import WelcomeModal from "component/welcomeModal";
import lbry from "lbry";
import { Line } from "rc-progress";
class App extends React.Component {
class App extends React.PureComponent {
componentWillMount() {
document.addEventListener('unhandledError', (event) => {
document.addEventListener("unhandledError", event => {
this.props.alertError(event.detail);
});
if (!this.props.upgradeSkipped) {
this.props.checkUpgradeAvailable()
this.props.checkUpgradeAvailable();
}
lbry.balanceSubscribe((balance) => {
this.props.updateBalance(balance)
})
lbry.balanceSubscribe(balance => {
this.props.updateBalance(balance);
});
}
render() {
const {
modal,
} = this.props
const { modal } = this.props;
return <div id="window">
<Header />
<div id="main-content">
<Router />
return (
<div id="window">
<Header />
<div id="main-content">
<Router />
</div>
{modal == "upgrade" && <UpgradeModal />}
{modal == "downloading" && <DownloadingModal />}
{modal == "error" && <ErrorModal />}
{modal == "welcome" && <WelcomeModal />}
</div>
{modal == 'upgrade' && <UpgradeModal />}
{modal == 'downloading' && <DownloadingModal />}
{modal == 'error' && <ErrorModal />}
</div>
);
}
}
export default App
export default App;

View file

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

@ -0,0 +1,16 @@
import React from "react";
import { connect } from "react-redux";
import {
selectAuthenticationIsPending,
selectEmailToVerify,
selectUserIsVerificationCandidate,
} from "selectors/user";
import Auth from "./view";
const select = state => ({
isPending: selectAuthenticationIsPending(state),
email: selectEmailToVerify(state),
isVerificationCandidate: selectUserIsVerificationCandidate(state),
});
export default connect(select, null)(Auth);

View file

@ -0,0 +1,22 @@
import React from "react";
import { BusyMessage } from "component/common";
import UserEmailNew from "component/userEmailNew";
import UserEmailVerify from "component/userEmailVerify";
export class Auth extends React.PureComponent {
render() {
const { isPending, email, isVerificationCandidate } = this.props;
if (isPending) {
return <BusyMessage message={__("Authenticating")} />;
} else if (!email) {
return <UserEmailNew />;
} else if (isVerificationCandidate) {
return <UserEmailVerify />;
} else {
return <span className="empty">{__("No further steps.")}</span>;
}
}
}
export default Auth;

View file

@ -0,0 +1,24 @@
import React from "react";
import * as modal from "constants/modal_types";
import { connect } from "react-redux";
import { doUserEmailDecline } from "actions/user";
import { doOpenModal } from "actions/app";
import {
selectAuthenticationIsPending,
selectUserHasEmail,
selectUserIsAuthRequested,
} from "selectors/user";
import AuthOverlay from "./view";
const select = state => ({
hasEmail: selectUserHasEmail(state),
isPending: selectAuthenticationIsPending(state),
isShowing: selectUserIsAuthRequested(state),
});
const perform = dispatch => ({
userEmailDecline: () => dispatch(doUserEmailDecline()),
openWelcomeModal: () => dispatch(doOpenModal(modal.WELCOME)),
});
export default connect(select, perform)(AuthOverlay);

View file

@ -0,0 +1,83 @@
import React from "react";
import lbryio from "lbryio.js";
import ModalPage from "component/modal-page.js";
import Auth from "component/auth";
import Link from "component/link";
export class AuthOverlay extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
showNoEmailConfirm: false,
};
}
componentWillReceiveProps(nextProps) {
if (this.props.isShowing && !this.props.isPending && !nextProps.isShowing) {
setTimeout(() => this.props.openWelcomeModal(), 1);
}
}
onEmailSkipClick() {
this.setState({ showNoEmailConfirm: true });
}
onEmailSkipConfirm() {
this.props.userEmailDecline();
}
render() {
if (!lbryio.enabled) {
return null;
}
const { isPending, isShowing, hasEmail } = this.props;
if (isShowing) {
return (
<ModalPage
className="modal-page--full"
isOpen={true}
contentLabel="Authentication"
>
<h1>LBRY Early Access</h1>
<Auth />
{isPending
? ""
: <div className="form-row-submit">
{!hasEmail && this.state.showNoEmailConfirm
? <div className="help form-input-width">
<p>
{__(
"If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications."
)}
</p>
<Link
onClick={() => {
this.onEmailSkipConfirm();
}}
label={__("Continue without email")}
/>
</div>
: <Link
className={"button-text-help"}
onClick={() => {
hasEmail
? this.onEmailSkipConfirm()
: this.onEmailSkipClick();
}}
label={
hasEmail ? __("Skip for now") : __("Do I have to?")
}
/>}
</div>}
</ModalPage>
);
}
return null;
}
}
export default AuthOverlay;

View file

@ -1,76 +1,95 @@
import React from 'react';
import lbry from '../lbry.js';
import React from "react";
import lbry from "../lbry.js";
//component/icon.js
export class Icon extends React.Component {
export class Icon extends React.PureComponent {
static propTypes = {
icon: React.PropTypes.string.isRequired,
className: React.PropTypes.string,
fixed: React.PropTypes.bool,
}
};
render() {
const {fixed, className} = this.props;
const spanClassName = ('icon ' + ('fixed' in this.props ? 'icon-fixed-width ' : '') +
this.props.icon + ' ' + (this.props.className || ''));
return <span className={spanClassName}></span>
const { fixed, className } = this.props;
const spanClassName =
"icon " +
("fixed" in this.props ? "icon-fixed-width " : "") +
this.props.icon +
" " +
(this.props.className || "");
return <span className={spanClassName} />;
}
}
export class TruncatedText extends React.Component {
export class TruncatedText extends React.PureComponent {
static propTypes = {
lines: React.PropTypes.number,
}
};
static defaultProps = {
lines: null
}
lines: null,
};
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.PureComponent {
static propTypes = {
message: React.PropTypes.string,
}
};
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.PureComponent {
render() {
return <span>LBC</span>;
}
}
export class CreditAmount extends React.Component {
export class CreditAmount extends React.PureComponent {
static propTypes = {
amount: React.PropTypes.number.isRequired,
precision: React.PropTypes.number,
isEstimate: React.PropTypes.bool,
label: React.PropTypes.bool,
showFree: React.PropTypes.bool,
look: React.PropTypes.oneOf(['indicator', 'plain']),
}
look: React.PropTypes.oneOf(["indicator", "plain"]),
};
static defaultProps = {
precision: 1,
label: true,
showFree: false,
look: 'indicator',
}
look: "indicator",
};
render() {
const formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision);
const formattedAmount = lbry.formatCredits(
this.props.amount,
this.props.precision
);
let amountText;
if (this.props.showFree && parseFloat(formattedAmount) == 0) {
amountText = 'free';
amountText = __("free");
} else if (this.props.label) {
amountText = formattedAmount + (parseFloat(formattedAmount) == 1 ? ' credit' : ' credits');
amountText =
formattedAmount +
" " +
(parseFloat(formattedAmount) == 1 ? __("credit") : __("credits"));
} else {
amountText = formattedAmount;
}
@ -80,19 +99,27 @@ export class CreditAmount extends React.Component {
<span>
{amountText}
</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
className="credit-amount__estimate"
title={__("This is an estimate and does not include data fees")}
>
*
</span>
: null}
</span>
);
}
}
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.PureComponent {
static propTypes = {
address: React.PropTypes.string,
}
};
constructor(props) {
super(props);
@ -102,16 +129,27 @@ export class Address extends React.Component {
render() {
return (
<input className="input-copyable" type="text" ref={(input) => { this._inputElem = input; }}
onFocus={() => { this._inputElem.select(); }} style={addressStyle} readOnly="readonly" value={this.props.address}></input>
<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.PureComponent {
static propTypes = {
src: React.PropTypes.string,
}
};
handleError() {
if (this.state.imageUrl != this._defaultImageUri) {
@ -124,9 +162,9 @@ export class Thumbnail extends React.Component {
constructor(props) {
super(props);
this._defaultImageUri = lbry.imagePath('default-thumb.svg')
this._maxLoadTime = 10000
this._isMounted = false
this._defaultImageUri = lbry.imagePath("default-thumb.svg");
this._maxLoadTime = 10000;
this._isMounted = false;
this.state = {
imageUri: this.props.src || this._defaultImageUri,
@ -149,9 +187,19 @@ export class Thumbnail extends React.Component {
}
render() {
const className = this.props.className ? this.props.className : '',
otherProps = Object.assign({}, this.props)
const className = this.props.className ? this.props.className : "",
otherProps = Object.assign({}, this.props);
delete otherProps.className;
return <img ref="img" onError={() => { this.handleError() }} {...otherProps} className={className} src={this.state.imageUri} />
return (
<img
ref="img"
onError={() => {
this.handleError();
}}
{...otherProps}
className={className}
src={this.state.imageUri}
/>
);
}
}

View file

@ -1,25 +1,17 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doStartUpgrade,
doCancelUpgrade,
} from 'actions/app'
import {
selectDownloadProgress,
selectDownloadComplete,
} from 'selectors/app'
import DownloadingModal from './view'
import React from "react";
import { connect } from "react-redux";
import { doStartUpgrade, doCancelUpgrade } from "actions/app";
import { selectDownloadProgress, selectDownloadComplete } from "selectors/app";
import DownloadingModal from "./view";
const select = (state) => ({
const select = state => ({
downloadProgress: selectDownloadProgress(state),
downloadComplete: selectDownloadComplete(state),
})
});
const perform = (dispatch) => ({
const perform = dispatch => ({
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 {
Modal
} from 'component/modal'
import {Line} from 'rc-progress';
import Link from 'component/link'
import React from "react";
import { Modal } from "component/modal";
import { Line } from "rc-progress";
import Link from "component/link";
class DownloadingModal extends React.Component {
class DownloadingModal extends React.PureComponent {
render() {
const {
downloadProgress,
downloadComplete,
startUpgrade,
cancelUpgrade,
} = this.props
} = this.props;
return (
<Modal isOpen={true} contentLabel="Downloading Update" type="custom">
Downloading Update{downloadProgress ? `: ${downloadProgress}%` : null}
<Line percent={downloadProgress ? downloadProgress : 0} strokeWidth="4"/>
{downloadComplete ? (
<div>
<br />
<p>Click "Begin Upgrade" to start the upgrade process.</p>
<p>The app will close, and you will be prompted to install the latest version of LBRY.</p>
<p>After the install is complete, please reopen the app.</p>
</div>
) : null }
<Modal
isOpen={true}
contentLabel={__("Downloading Update")}
type="custom"
>
{__("Downloading Update")}
{downloadProgress ? `: ${downloadProgress}%` : null}
<Line
percent={downloadProgress ? downloadProgress : 0}
strokeWidth="4"
/>
{downloadComplete
? <div>
<br />
<p>{__('Click "Begin Upgrade" to start the upgrade process.')}</p>
<p>
{__(
"The app will close, and you will be prompted to install the latest version of LBRY."
)}
</p>
<p>
{__("After the install is complete, please reopen the app.")}
</p>
</div>
: null}
<div className="modal__buttons">
{downloadComplete
? <Link button="primary" label="Begin Upgrade" className="modal__button" onClick={startUpgrade} />
? <Link
button="primary"
label={__("Begin Upgrade")}
className="modal__button"
onClick={startUpgrade}
/>
: null}
<Link button="alt" label="Cancel" className="modal__button" onClick={cancelUpgrade} />
<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 {
connect
} from 'react-redux'
import {
selectCurrentModal,
selectModalExtraContent,
} from 'selectors/app'
import {
doCloseModal,
} from 'actions/app'
import ErrorModal from './view'
import React from "react";
import { connect } from "react-redux";
import { selectCurrentModal, selectModalExtraContent } from "selectors/app";
import { doCloseModal } from "actions/app";
import ErrorModal from "./view";
const select = (state) => ({
const select = state => ({
modal: selectCurrentModal(state),
error: selectModalExtraContent(state),
})
});
const perform = (dispatch) => ({
closeModal: () => dispatch(doCloseModal())
})
const perform = dispatch => ({
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 lbry from 'lbry'
import {
ExpandableModal
} from 'component/modal'
import React from "react";
import lbry from "lbry";
import { ExpandableModal } from "component/modal";
class ErrorModal extends React.Component {
class ErrorModal extends React.PureComponent {
render() {
const {
modal,
closeModal,
error
} = this.props
const { 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 = {
connectionString: 'API connection string',
method: 'Method',
params: 'Parameters',
code: 'Error code',
message: 'Error message',
data: 'Error data',
}
connectionString: __("API connection string"),
method: __("Method"),
params: __("Parameters"),
code: __("Error code"),
message: __("Error message"),
data: __("Error data"),
};
const errorInfoList = []
const errorInfoList = [];
for (let key of Object.keys(error)) {
let val = typeof error[key] == 'string' ? error[key] : JSON.stringify(error[key]);
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>);
errorInfoList.push(
<li key={key}><strong>{label}</strong>: <code>{val}</code></li>
);
}
const errorInfo = <ul className="error-modal__error-list">{errorInfoList}</ul>
const errorInfo = (
<ul className="error-modal__error-list">{errorInfoList}</ul>
);
return(
return (
<ExpandableModal
isOpen={modal == 'error'}
contentLabel="Error" className="error-modal"
isOpen={modal == "error"}
contentLabel={__("Error")}
className="error-modal"
overlayClassName="error-modal-overlay"
onConfirmed={closeModal}
extraContent={errorInfo}
>
<h3 className="modal__header">Error</h3>
<h3 className="modal__header">{__("Error")}</h3>
<div className="error-modal__content">
<div><img className="error-modal__warning-symbol" src={lbry.imagePath('warning.png')} /></div>
<p>We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.</p>
<div>
<img
className="error-modal__warning-symbol"
src={lbry.imagePath("warning.png")}
/>
</div>
<p>
{__(
"We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem."
)}
</p>
</div>
</ExpandableModal>
)
);
}
}
export default ErrorModal
export default ErrorModal;

View file

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

View file

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

View file

@ -1,33 +1,33 @@
import React from 'react';
import {Icon,BusyMessage} from 'component/common';
import FilePrice from 'component/filePrice'
import {Modal} from 'component/modal';
import {FormField} from 'component/form';
import Link from 'component/link';
import {ToolTip} from 'component/tooltip';
import {DropDownMenu, DropDownMenuItem} from 'component/menu';
import React from "react";
import { Icon, BusyMessage } from "component/common";
import FilePrice from "component/filePrice";
import { Modal } from "component/modal";
import { FormField } from "component/form";
import Link from "component/link";
import { ToolTip } from "component/tooltip";
import { DropDownMenu, DropDownMenuItem } from "component/menu";
class FileActions extends React.Component {
class FileActions extends React.PureComponent {
constructor(props) {
super(props)
super(props);
this.state = {
forceShowActions: false,
deleteChecked: false,
}
};
}
componentWillMount() {
this.checkAvailability(this.props.uri)
this.checkAvailability(this.props.uri);
}
componentWillReceiveProps(nextProps) {
this.checkAvailability(nextProps.uri)
this.checkAvailability(nextProps.uri);
}
checkAvailability(uri) {
if (!this._uri || uri !== this._uri) {
this._uri = uri;
this.props.checkAvailability(uri)
this.props.checkAvailability(uri);
}
}
@ -40,12 +40,12 @@ class FileActions extends React.Component {
handleDeleteCheckboxClicked(event) {
this.setState({
deleteChecked: event.target.checked,
})
});
}
onAffirmPurchase() {
this.props.closeModal()
this.props.loadVideo(this.props.uri)
this.props.closeModal();
this.props.loadVideo(this.props.uri);
}
render() {
@ -64,88 +64,159 @@ class FileActions extends React.Component {
startDownload,
costInfo,
loading,
} = this.props
} = this.props;
const deleteChecked = this.state.deleteChecked,
metadata = fileInfo ? fileInfo.metadata : null,
openInFolderMessage = platform.startsWith('Mac') ? 'Open in Finder' : 'Open in Folder',
showMenu = fileInfo && Object.keys(fileInfo).length > 0,
title = metadata ? metadata.title : uri;
metadata = fileInfo ? fileInfo.metadata : null,
openInFolderMessage = platform.startsWith("Mac")
? __("Open in Finder")
: __("Open in Folder"),
showMenu = fileInfo && Object.keys(fileInfo).length > 0,
title = metadata ? metadata.title : uri;
let content
let content;
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
progress = (fileInfo && fileInfo.written_bytes) ? fileInfo.written_bytes / fileInfo.total_bytes * 100 : 0,
label = fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...',
labelWithIcon = <span className="button__content"><Icon icon="icon-download" /><span>{label}</span></span>;
content = <div className="faux-button-block file-actions__download-status-bar button-set-item">
<div className="faux-button-block file-actions__download-status-bar-overlay" style={{ width: progress + '%' }}>{labelWithIcon}</div>
{labelWithIcon}
</div>
content = (
<div className="faux-button-block file-actions__download-status-bar button-set-item">
<div
className="faux-button-block file-actions__download-status-bar-overlay"
style={{ width: progress + "%" }}
>
{labelWithIcon}
</div>
{labelWithIcon}
</div>
);
} else if (!fileInfo && isAvailable === undefined) {
content = <BusyMessage message="Checking availability" />
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>
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" />
content = <BusyMessage message={__("Fetching cost info")} />;
} else {
content = <Link button="text" label="Download" icon="icon-download" onClick={() => { startDownload(uri) } } />;
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)} />;
content = (
<Link
label={__("Open")}
button="text"
icon="icon-folder-open"
onClick={() => openInShell(fileInfo)}
/>
);
} else {
console.log('handle this case of file action props?');
console.log("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.
{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
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
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>
<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>
<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,34 +1,21 @@
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 React from "react";
import { connect } from "react-redux";
import { doNavigate } from "actions/app";
import { doResolveUri, doCancelResolveUri } from "actions/content";
import { selectObscureNsfw } from "selectors/app";
import {
makeSelectClaimForUri,
makeSelectMetadataForUri,
} from 'selectors/claims'
import {
makeSelectFileInfoForUri,
} from 'selectors/file_info'
import {
makeSelectIsResolvingForUri,
} from 'selectors/content'
import FileCard from './view'
} from "selectors/claims";
import { makeSelectFileInfoForUri } from "selectors/file_info";
import { makeSelectIsResolvingForUri } from "selectors/content";
import FileCard from "./view";
const makeSelect = () => {
const selectClaimForUri = makeSelectClaimForUri()
const selectFileInfoForUri = makeSelectFileInfoForUri()
const selectMetadataForUri = makeSelectMetadataForUri()
const selectResolvingUri = makeSelectIsResolvingForUri()
const selectClaimForUri = makeSelectClaimForUri();
const selectFileInfoForUri = makeSelectFileInfoForUri();
const selectMetadataForUri = makeSelectMetadataForUri();
const selectResolvingUri = makeSelectIsResolvingForUri();
const select = (state, props) => ({
claim: selectClaimForUri(state, props),
@ -36,15 +23,15 @@ const makeSelect = () => {
obscureNsfw: selectObscureNsfw(state),
metadata: selectMetadataForUri(state, props),
isResolvingUri: selectResolvingUri(state, props),
})
});
return select
}
return select;
};
const perform = (dispatch) => ({
const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)),
resolveUri: (uri) => dispatch(doResolveUri(uri)),
cancelResolveUri: (uri) => dispatch(doCancelResolveUri(uri))
})
resolveUri: uri => dispatch(doResolveUri(uri)),
cancelResolveUri: uri => dispatch(doCancelResolveUri(uri)),
});
export default connect(makeSelect, perform)(FileCard)
export default connect(makeSelect, perform)(FileCard);

View file

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

View file

@ -1,13 +1,9 @@
import React from 'react'
import {
connect
} from 'react-redux'
import FileList from './view'
import React from "react";
import { connect } 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

@ -1,20 +1,20 @@
import React from 'react';
import lbry from 'lbry.js';
import lbryuri from 'lbryuri.js';
import Link from 'component/link';
import {FormField} from 'component/form.js';
import FileTile from 'component/fileTile';
import rewards from 'rewards.js';
import lbryio from 'lbryio.js';
import {BusyMessage, Thumbnail} from 'component/common.js';
import React from "react";
import lbry from "lbry.js";
import lbryuri from "lbryuri.js";
import Link from "component/link";
import { FormField } from "component/form.js";
import FileTile from "component/fileTile";
import rewards from "rewards.js";
import lbryio from "lbryio.js";
import { BusyMessage, Thumbnail } from "component/common.js";
class FileList extends React.Component {
class FileList extends React.PureComponent {
constructor(props) {
super(props)
super(props);
this.state = {
sortBy: 'date',
}
sortBy: "date",
};
this._sortFunctions = {
date: function(fileInfos) {
@ -22,8 +22,12 @@ class FileList extends React.Component {
},
title: function(fileInfos) {
return fileInfos.slice().sort(function(fileInfo1, fileInfo2) {
const title1 = fileInfo1.metadata ? fileInfo1.metadata.stream.metadata.title.toLowerCase() : fileInfo1.name;
const title2 = fileInfo2.metadata ? fileInfo2.metadata.stream.metadata.title.toLowerCase() : fileInfo2.name;
const title1 = fileInfo1.metadata
? fileInfo1.metadata.stream.metadata.title.toLowerCase()
: fileInfo1.name;
const title2 = fileInfo2.metadata
? fileInfo2.metadata.stream.metadata.title.toLowerCase()
: fileInfo2.name;
if (title1 < title2) {
return -1;
} else if (title1 > title2) {
@ -31,63 +35,66 @@ class FileList extends React.Component {
} else {
return 0;
}
})
});
},
filename: function(fileInfos) {
return fileInfos.slice().sort(function({file_name: fileName1}, {file_name: fileName2}) {
const fileName1Lower = fileName1.toLowerCase();
const fileName2Lower = fileName2.toLowerCase();
if (fileName1Lower < fileName2Lower) {
return -1;
} else if (fileName2Lower > fileName1Lower) {
return 1;
} else {
return 0;
}
})
return fileInfos
.slice()
.sort(function({ file_name: fileName1 }, { file_name: fileName2 }) {
const fileName1Lower = fileName1.toLowerCase();
const fileName2Lower = fileName2.toLowerCase();
if (fileName1Lower < fileName2Lower) {
return -1;
} else if (fileName2Lower > fileName1Lower) {
return 1;
} else {
return 0;
}
});
},
}
};
}
handleSortChanged(event) {
this.setState({
sortBy: event.target.value,
})
});
}
render() {
const {
handleSortChanged,
fetching,
fileInfos,
} = this.props
const {
sortBy,
} = this.state
const content = []
const { handleSortChanged, fetching, fileInfos } = this.props;
const { sortBy } = this.state;
const content = [];
this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
const uri = lbryuri.build({
contentName: fileInfo.name,
channelName: fileInfo.channel_name,
})
content.push(<FileTile key={uri} uri={uri} hidePrice={true} showEmpty={this.props.fileTileShowEmpty} />)
})
});
content.push(
<FileTile
key={uri}
uri={uri}
hidePrice={true}
showEmpty={this.props.fileTileShowEmpty}
/>
);
});
return (
<section className="file-list__header">
{ fetching && <span className="busy-indicator"/> }
<span className='sort-section'>
Sort by { ' ' }
{fetching && <span className="busy-indicator" />}
<span className="sort-section">
{__("Sort by")} {" "}
<FormField type="select" onChange={this.handleSortChanged.bind(this)}>
<option value="date">Date</option>
<option value="title">Title</option>
<option value="filename">File name</option>
<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 {
connect,
} from 'react-redux'
import {
doSearch,
} from 'actions/search'
import React from "react";
import { connect } from "react-redux";
import { doSearch } from "actions/search";
import {
selectIsSearching,
selectCurrentSearchResults,
selectSearchQuery,
} from 'selectors/search'
import {
doNavigate,
} from 'actions/app'
import FileListSearch from './view'
} from "selectors/search";
import { doNavigate } from "actions/app";
import FileListSearch from "./view";
const select = (state) => ({
const select = state => ({
isSearching: selectIsSearching(state),
query: selectSearchQuery(state),
results: selectCurrentSearchResults(state)
})
results: selectCurrentSearchResults(state),
});
const perform = (dispatch) => ({
navigate: (path) => dispatch(doNavigate(path)),
search: (search) => dispatch(doSearch(search))
})
const perform = dispatch => ({
navigate: path => dispatch(doNavigate(path)),
search: search => dispatch(doSearch(search)),
});
export default connect(select, perform)(FileListSearch)
export default connect(select, perform)(FileListSearch);

View file

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

View file

@ -1,31 +1,32 @@
import React from 'react'
import {
connect,
} from 'react-redux'
import {
doFetchCostInfoForUri,
} from 'actions/cost_info'
import React from "react";
import { connect } from "react-redux";
import { doFetchCostInfoForUri } from "actions/cost_info";
import {
makeSelectCostInfoForUri,
makeSelectFetchingCostInfoForUri,
} from 'selectors/cost_info'
import {
makeSelectClaimForUri,
} from 'selectors/claims'
import FilePrice from './view'
const makeSelect = () => {
const selectCostInfoForUri = makeSelectCostInfoForUri()
const selectFetchingCostInfoForUri = makeSelectFetchingCostInfoForUri()
const selectCostInfoForUri = makeSelectCostInfoForUri();
const selectFetchingCostInfoForUri = makeSelectFetchingCostInfoForUri();
const selectClaim = makeSelectClaimForUri();
const select = (state, props) => ({
costInfo: selectCostInfoForUri(state, props),
fetching: selectFetchingCostInfoForUri(state, props),
})
claim: selectClaim(state, props),
});
return select
}
return select;
};
const perform = (dispatch) => ({
fetchCostInfo: (uri) => dispatch(doFetchCostInfoForUri(uri)),
const perform = dispatch => ({
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(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 {
CreditAmount,
} from 'component/common'
import React from "react";
import { CreditAmount } from "component/common";
class FilePrice extends React.Component{
class FilePrice extends React.PureComponent {
componentWillMount() {
this.fetchCost(this.props)
this.fetchCost(this.props);
}
componentWillReceiveProps(nextProps) {
this.fetchCost(nextProps)
this.fetchCost(nextProps);
}
fetchCost(props) {
const {
costInfo,
fetchCostInfo,
uri,
fetching,
} = props
const { costInfo, fetchCostInfo, uri, fetching, claim } = props;
if (costInfo === undefined && !fetching) {
fetchCostInfo(uri)
if (costInfo === undefined && !fetching && claim) {
fetchCostInfo(uri);
}
}
render() {
const {
costInfo,
look = 'indicator',
} = this.props
const { costInfo, look = "indicator" } = this.props;
const isEstimate = costInfo ? !costInfo.includesData : null
const isEstimate = costInfo ? !costInfo.includesData : null;
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,33 +1,21 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doNavigate,
} from 'actions/app'
import {
doResolveUri,
} from 'actions/content'
import React from "react";
import { connect } from "react-redux";
import { doNavigate } from "actions/app";
import { doResolveUri } from "actions/content";
import {
makeSelectClaimForUri,
makeSelectMetadataForUri,
} from 'selectors/claims'
import {
makeSelectFileInfoForUri,
} from 'selectors/file_info'
import {
selectObscureNsfw,
} from 'selectors/app'
import {
makeSelectIsResolvingForUri,
} from 'selectors/content'
import FileTile from './view'
} from "selectors/claims";
import { makeSelectFileInfoForUri } from "selectors/file_info";
import { selectObscureNsfw } from "selectors/app";
import { makeSelectIsResolvingForUri } from "selectors/content";
import FileTile from "./view";
const makeSelect = () => {
const selectClaimForUri = makeSelectClaimForUri()
const selectFileInfoForUri = makeSelectFileInfoForUri()
const selectMetadataForUri = makeSelectMetadataForUri()
const selectResolvingUri = makeSelectIsResolvingForUri()
const selectClaimForUri = makeSelectClaimForUri();
const selectFileInfoForUri = makeSelectFileInfoForUri();
const selectMetadataForUri = makeSelectMetadataForUri();
const selectResolvingUri = makeSelectIsResolvingForUri();
const select = (state, props) => ({
claim: selectClaimForUri(state, props),
@ -35,14 +23,14 @@ const makeSelect = () => {
obscureNsfw: selectObscureNsfw(state),
metadata: selectMetadataForUri(state, props),
isResolvingUri: selectResolvingUri(state, props),
})
});
return select
}
return select;
};
const perform = (dispatch) => ({
const perform = dispatch => ({
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

@ -1,38 +1,41 @@
import React from 'react';
import lbry from 'lbry.js';
import lbryuri from 'lbryuri.js';
import Link from 'component/link';
import FileActions from 'component/fileActions';
import {Thumbnail, TruncatedText,} from 'component/common.js';
import FilePrice from 'component/filePrice'
import UriIndicator from 'component/uriIndicator';
import React from "react";
import lbry from "lbry.js";
import lbryuri from "lbryuri.js";
import Link from "component/link";
import FileActions from "component/fileActions";
import { Thumbnail, TruncatedText } from "component/common.js";
import FilePrice from "component/filePrice";
import UriIndicator from "component/uriIndicator";
class FileTile extends React.Component {
static SHOW_EMPTY_PUBLISH = "publish"
static SHOW_EMPTY_PENDING = "pending"
class FileTile extends React.PureComponent {
static SHOW_EMPTY_PUBLISH = "publish";
static SHOW_EMPTY_PENDING = "pending";
constructor(props) {
super(props)
super(props);
this.state = {
showNsfwHelp: false,
}
};
}
componentDidMount() {
const {
isResolvingUri,
resolveUri,
claim,
uri,
} = this.props
this.resolve(this.props);
}
if(!isResolvingUri && !claim && uri) {
resolveUri(uri)
}
componentWillReceiveProps(nextProps) {
this.resolve(nextProps);
}
resolve({ isResolvingUri, claim, uri, resolveUri }) {
if (!isResolvingUri && claim === undefined && uri) resolveUri(uri);
}
handleMouseOver() {
if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
if (
this.props.obscureNsfw &&
this.props.metadata &&
this.props.metadata.nsfw
) {
this.setState({
showNsfwHelp: true,
});
@ -55,40 +58,61 @@ class FileTile extends React.Component {
showEmpty,
navigate,
hidePrice,
} = this.props
} = this.props;
const uri = lbryuri.normalize(this.props.uri);
const isClaimed = !!claim;
const isClaimable = lbryuri.isClaimable(uri)
const title = isClaimed && metadata && metadata.title ? metadata.title : uri;
const isClaimable = lbryuri.isClaimable(uri);
const title = isClaimed && metadata && metadata.title
? metadata.title
: uri;
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
let onClick = () => navigate('/show', { uri })
let onClick = () => navigate("/show", { uri });
let description = ""
let description = "";
if (isClaimed) {
description = metadata && metadata.description
description = metadata && metadata.description;
} else if (isResolvingUri) {
description = "Loading..."
description = __("Loading...");
} else if (showEmpty === FileTile.SHOW_EMPTY_PUBLISH) {
onClick = () => navigate('/publish', { })
description = <span className="empty">
This location is unused. { ' ' }
{ isClaimable && <span className="button-text">Put something here!</span> }
</span>
onClick = () => navigate("/publish", {});
description = (
<span className="empty">
{__("This location is unused.")} {" "}
{isClaimable &&
<span className="button-text">{__("Put something here!")}</span>}
</span>
);
} else if (showEmpty === FileTile.SHOW_EMPTY_PENDING) {
description = <span className="empty">This file is pending confirmation.</span>
description = (
<span className="empty">
{__("This file is pending confirmation.")}
</span>
);
}
return (
<section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver.bind(this)} onMouseLeave={this.handleMouseOut.bind(this)}>
<section
className={"file-tile card " + (obscureNsfw ? "card--obscured " : "")}
onMouseEnter={this.handleMouseOver.bind(this)}
onMouseLeave={this.handleMouseOut.bind(this)}
>
<Link onClick={onClick} className="card__link">
<div className={"card__inner file-tile__row"}>
<div className="card__media"
style={{ backgroundImage: "url('" + (metadata && metadata.thumbnail ? metadata.thumbnail : lbry.imagePath('default-thumb.svg')) + "')" }}>
</div>
<div
className="card__media"
style={{
backgroundImage:
"url('" +
(metadata && metadata.thumbnail
? metadata.thumbnail
: lbry.imagePath("default-thumb.svg")) +
"')",
}}
/>
<div className="file-tile__content">
<div className="card__title-primary">
{ !hidePrice ? <FilePrice uri={this.props.uri} /> : null}
{!hidePrice ? <FilePrice uri={this.props.uri} /> : null}
<div className="meta">{uri}</div>
<h3><TruncatedText lines={1}>{title}</TruncatedText></h3>
</div>
@ -101,16 +125,23 @@ class FileTile extends React.Component {
</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>
? <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,27 +1,27 @@
import React from 'react';
import FileSelector from './file-selector.js';
import {Icon} from './common.js';
import React from "react";
import FileSelector from "./file-selector.js";
import { Icon } from "./common.js";
var formFieldCounter = 0,
formFieldFileSelectorTypes = ['file', 'directory'],
formFieldNestedLabelTypes = ['radio', 'checkbox'];
formFieldFileSelectorTypes = ["file", "directory"],
formFieldNestedLabelTypes = ["radio", "checkbox"];
function formFieldId() {
return "form-field-" + (++formFieldCounter);
return "form-field-" + ++formFieldCounter;
}
export class FormField extends React.Component {
export class FormField extends React.PureComponent {
static propTypes = {
type: React.PropTypes.string.isRequired,
prefix: React.PropTypes.string,
postfix: React.PropTypes.string,
hasError: React.PropTypes.bool
}
hasError: React.PropTypes.bool,
};
constructor(props) {
super(props);
this._fieldRequiredText = 'This field is required';
this._fieldRequiredText = __("This field is required");
this._type = null;
this._element = null;
@ -32,15 +32,15 @@ export class FormField extends React.Component {
}
componentWillMount() {
if (['text', 'number', 'radio', 'checkbox'].includes(this.props.type)) {
this._element = 'input';
if (["text", "number", "radio", "checkbox"].includes(this.props.type)) {
this._element = "input";
this._type = this.props.type;
} else if (this.props.type == 'text-number') {
this._element = 'input';
this._type = 'text';
} else if (this.props.type == "text-number") {
this._element = "input";
this._type = "text";
} else if (formFieldFileSelectorTypes.includes(this.props.type)) {
this._element = 'input';
this._type = 'hidden';
this._element = "input";
this._type = "hidden";
} else {
// Non <input> field, e.g. <select>, <textarea>
this._element = this.props.type;
@ -52,15 +52,16 @@ export class FormField extends React.Component {
* We have to add the webkitdirectory attribute here because React doesn't allow it in JSX
* https://github.com/facebook/react/issues/3468
*/
if (this.props.type == 'directory') {
if (this.props.type == "directory") {
this.refs.field.webkitdirectory = true;
}
}
handleFileChosen(path) {
this.refs.field.value = path;
if (this.props.onChange) { // Updating inputs programmatically doesn't generate an event, so we have to make our own
const event = new Event('change', {bubbles: true})
if (this.props.onChange) {
// Updating inputs programmatically doesn't generate an event, so we have to make our own
const event = new Event("change", { bubbles: true });
this.refs.field.dispatchEvent(event); // This alone won't generate a React event, but we use it to attach the field as a target
this.props.onChange(event);
}
@ -78,7 +79,7 @@ export class FormField extends React.Component {
}
getValue() {
if (this.props.type == 'checkbox') {
if (this.props.type == "checkbox") {
return this.refs.field.checked;
} else {
return this.refs.field.value;
@ -92,9 +93,12 @@ export class FormField extends React.Component {
render() {
// Pass all unhandled props to the field element
const otherProps = Object.assign({}, this.props),
isError = this.state.isError !== null ? this.state.isError : this.props.hasError,
elementId = this.props.id ? this.props.id : formFieldId(),
renderElementInsideLabel = this.props.label && formFieldNestedLabelTypes.includes(this.props.type);
isError = this.state.isError !== null
? this.state.isError
: 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.label;
@ -103,44 +107,90 @@ export class FormField extends React.Component {
delete otherProps.postfix;
delete otherProps.prefix;
const element = <this._element id={elementId} type={this._type} name={this.props.name} ref="field" placeholder={this.props.placeholder}
className={'form-field__input form-field__input-' + this.props.type + ' ' + (this.props.className || '') + (isError ? 'form-field__input--error' : '')}
{...otherProps}>
{this.props.children}
</this._element>;
const element = (
<this._element
id={elementId}
type={this._type}
name={this.props.name}
ref="field"
placeholder={this.props.placeholder}
className={
"form-field__input form-field__input-" +
this.props.type +
" " +
(this.props.className || "") +
(isError ? "form-field__input--error" : "")
}
{...otherProps}
>
{this.props.children}
</this._element>
);
return <div className={"form-field form-field--" + this.props.type}>
{ this.props.prefix ? <span className="form-field__prefix">{this.props.prefix}</span> : '' }
{ renderElementInsideLabel ?
<label htmlFor={elementId} className={"form-field__label " + (isError ? 'form-field__label--error' : '')}>
{element}
{this.props.label}
</label> :
element }
{ 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>
return (
<div className={"form-field form-field--" + this.props.type}>
{this.props.prefix
? <span className="form-field__prefix">{this.props.prefix}</span>
: ""}
{renderElementInsideLabel
? <label
htmlFor={elementId}
className={
"form-field__label " +
(isError ? "form-field__label--error" : "")
}
>
{element}
{this.props.label}
</label>
: element}
{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.PureComponent {
static propTypes = {
label: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element]),
label: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.element,
]),
// helper: React.PropTypes.html,
}
};
constructor(props) {
super(props);
this._fieldRequiredText = 'This field is required';
this._fieldRequiredText = __("This field is required");
this.state = {
isError: false,
errorMessage: null,
this.state = this.getStateFromProps(props);
}
componentWillReceiveProps(nextProps) {
this.setState(this.getStateFromProps(nextProps));
}
getStateFromProps(props) {
return {
isError: !!props.errorMessage,
errorMessage: typeof props.errorMessage === "string"
? props.errorMessage
: "",
};
}
@ -158,7 +208,7 @@ export class FormRow extends React.Component {
clearError(text) {
this.setState({
isError: false,
errorMessage: ''
errorMessage: "",
});
}
@ -176,24 +226,45 @@ export class FormRow extends React.Component {
render() {
const fieldProps = Object.assign({}, this.props),
elementId = formFieldId(),
renderLabelInFormField = formFieldNestedLabelTypes.includes(this.props.type);
elementId = formFieldId(),
renderLabelInFormField = formFieldNestedLabelTypes.includes(
this.props.type
);
if (!renderLabelInFormField) {
delete fieldProps.label;
}
delete fieldProps.helper;
delete fieldProps.errorMessage;
return <div className="form-row">
{ this.props.label && !renderLabelInFormField ?
<div className={"form-row__label-row " + (this.props.labelPrefix ? "form-row__label-row--prefix" : "") }>
<label htmlFor={elementId} className={"form-field__label " + (this.state.isError ? 'form-field__label--error' : '')}>
{this.props.label}
</label>
</div> : '' }
<FormField ref="field" hasError={this.state.isError} {...fieldProps} />
{ !this.state.isError && this.props.helper ? <div className="form-field__helper">{this.props.helper}</div> : '' }
{ this.state.isError ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
</div>
return (
<div className="form-row">
{this.props.label && !renderLabelInFormField
? <div
className={
"form-row__label-row " +
(this.props.labelPrefix ? "form-row__label-row--prefix" : "")
}
>
<label
htmlFor={elementId}
className={
"form-field__label " +
(this.state.isError ? "form-field__label--error" : "")
}
>
{this.props.label}
</label>
</div>
: ""}
<FormField ref="field" hasError={this.state.isError} {...fieldProps} />
{!this.state.isError && this.props.helper
? <div className="form-field__helper">{this.props.helper}</div>
: ""}
{this.state.isError
? <div className="form-field__error">{this.state.errorMessage}</div>
: ""}
</div>
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,34 +1,38 @@
import React from 'react';
import {Icon} from './common.js';
import Link from 'component/link';
import React from "react";
import { Icon } from "./common.js";
import Link from "component/link";
export class DropDownMenuItem extends React.Component {
export class DropDownMenuItem extends React.PureComponent {
static propTypes = {
href: React.PropTypes.string,
label: React.PropTypes.string,
icon: React.PropTypes.string,
onClick: React.PropTypes.func,
}
};
static defaultProps = {
iconPosition: 'left',
}
iconPosition: "left",
};
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 (
<a className="menu__menu-item" onClick={this.props.onClick}
href={this.props.href || 'javascript:'} label={this.props.label}>
{this.props.iconPosition == 'left' ? icon : null}
<a
className="menu__menu-item"
onClick={this.props.onClick}
href={this.props.href || "javascript:"}
label={this.props.label}
>
{this.props.iconPosition == "left" ? icon : null}
{this.props.label}
{this.props.iconPosition == 'left' ? null : icon}
{this.props.iconPosition == "left" ? null : icon}
</a>
);
}
}
export class DropDownMenu extends React.Component {
export class DropDownMenu extends React.PureComponent {
constructor(props) {
super(props);
@ -42,7 +46,7 @@ export class DropDownMenu extends React.Component {
componentWillUnmount() {
if (this._isWindowClickBound) {
window.removeEventListener('click', this.handleWindowClick, false);
window.removeEventListener("click", this.handleWindowClick, false);
}
}
@ -52,7 +56,7 @@ export class DropDownMenu extends React.Component {
});
if (!this.state.menuOpen && !this._isWindowClickBound) {
this._isWindowClickBound = true;
window.addEventListener('click', this.handleWindowClick, false);
window.addEventListener("click", this.handleWindowClick, false);
e.stopPropagation();
}
return false;
@ -66,10 +70,12 @@ export class DropDownMenu extends React.Component {
}
handleWindowClick(e) {
if (this.state.menuOpen &&
(!this._menuDiv || !this._menuDiv.contains(e.target))) {
if (
this.state.menuOpen &&
(!this._menuDiv || !this._menuDiv.contains(e.target))
) {
this.setState({
menuOpen: false
menuOpen: false,
});
}
}
@ -77,13 +83,26 @@ export class DropDownMenu extends React.Component {
render() {
if (!this.state.menuOpen && this._isWindowClickBound) {
this._isWindowClickBound = false;
window.removeEventListener('click', this.handleWindowClick, false);
window.removeEventListener("click", this.handleWindowClick, false);
}
return (
<div className="menu-container">
<Link ref={(span) => this._menuButton = span} button="text" icon="icon-ellipsis-v" onClick={(event) => { this.handleMenuIconClick(event) }} />
<Link
ref={span => (this._menuButton = span)}
button="text"
icon="icon-ellipsis-v"
onClick={event => {
this.handleMenuIconClick(event);
}}
/>
{this.state.menuOpen
? <div ref={(div) => this._menuDiv = div} className="menu" onClick={(event) => { this.handleMenuClick(event) }}>
? <div
ref={div => (this._menuDiv = div)}
className="menu"
onClick={event => {
this.handleMenuClick(event);
}}
>
{this.props.children}
</div>
: null}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,35 @@
import React from "react";
import { connect } from "react-redux";
import {
makeSelectHasClaimedReward,
makeSelectClaimRewardError,
makeSelectRewardByType,
makeSelectIsRewardClaimPending,
} from "selectors/rewards";
import { doNavigate } from "actions/app";
import { doClaimReward, doClaimRewardClearError } from "actions/rewards";
import RewardLink from "./view";
const makeSelect = () => {
const selectHasClaimedReward = makeSelectHasClaimedReward();
const selectIsPending = makeSelectIsRewardClaimPending();
const selectReward = makeSelectRewardByType();
const selectError = makeSelectClaimRewardError();
const select = (state, props) => ({
isClaimed: selectHasClaimedReward(state, props),
errorMessage: selectError(state, props),
isPending: selectIsPending(state, props),
reward: selectReward(state, props),
});
return select;
};
const perform = dispatch => ({
claimReward: reward => dispatch(doClaimReward(reward, true)),
clearError: reward => dispatch(doClaimRewardClearError(reward)),
navigate: path => dispatch(doNavigate(path)),
});
export default connect(makeSelect, perform)(RewardLink);

View file

@ -0,0 +1,44 @@
import React from "react";
import { Icon } from "component/common";
import Modal from "component/modal";
import Link from "component/link";
const RewardLink = props => {
const {
reward,
button,
claimReward,
clearError,
errorMessage,
isClaimed,
isPending,
} = props;
return (
<div className="reward-link">
{isClaimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <Link
button={button ? button : "alt"}
disabled={isPending}
label={isPending ? "Claiming..." : "Claim Reward"}
onClick={() => {
claimReward(reward);
}}
/>}
{errorMessage
? <Modal
isOpen={true}
contentLabel="Reward Claim Error"
className="error-modal"
onConfirmed={() => {
clearError(reward);
}}
>
{errorMessage}
</Modal>
: ""}
</div>
);
};
export default RewardLink;

View file

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

View file

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

View file

@ -1,23 +1,16 @@
import React from 'react'
import {
connect,
} from 'react-redux'
import {
doNavigate,
doRemoveSnackBarSnack,
} from 'actions/app'
import {
selectSnackBarSnacks,
} from 'selectors/app'
import SnackBar from './view'
import React from "react";
import { connect } from "react-redux";
import { doNavigate, doRemoveSnackBarSnack } from "actions/app";
import { selectSnackBarSnacks } from "selectors/app";
import SnackBar from "./view";
const perform = (dispatch) => ({
navigate: (path) => dispatch(doNavigate(path)),
const perform = dispatch => ({
navigate: path => dispatch(doNavigate(path)),
removeSnack: () => dispatch(doRemoveSnackBarSnack()),
})
});
const select = (state) => ({
const select = state => ({
snacks: selectSnackBarSnacks(state),
})
});
export default connect(select, perform)(SnackBar)
export default connect(select, perform)(SnackBar);

View file

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

View file

@ -1,47 +1,49 @@
import React from 'react';
import lbry from '../lbry.js';
import LoadScreen from './load_screen.js';
import React from "react";
import lbry from "../lbry.js";
import LoadScreen from "./load_screen.js";
export class SplashScreen extends React.Component {
export class SplashScreen extends React.PureComponent {
static propTypes = {
message: React.PropTypes.string,
onLoadDone: React.PropTypes.func,
}
};
constructor(props) {
super(props);
this.state = {
details: 'Starting daemon',
message: "Connecting",
details: __("Starting daemon"),
message: __("Connecting"),
isLagging: false,
};
}
updateStatus() {
lbry.status().then((status) => { this._updateStatusCallback(status) });
lbry.status().then(status => {
this._updateStatusCallback(status);
});
}
_updateStatusCallback(status) {
const startupStatus = status.startup_status
if (startupStatus.code == 'started') {
const startupStatus = status.startup_status;
if (startupStatus.code == "started") {
// Wait until we are able to resolve a name before declaring
// that we are done.
// 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
this.setState({
message: "Testing Network",
details: "Waiting for name resolution",
isLagging: false
message: __("Testing Network"),
details: __("Waiting for name resolution"),
isLagging: false,
});
lbry.resolve({uri: "lbry://one"}).then(() => {
lbry.resolve({ uri: "lbry://one" }).then(() => {
this.props.onLoadDone();
});
return;
}
this.setState({
details: startupStatus.message + (startupStatus.is_lagging ? '' : '...'),
details: startupStatus.message + (startupStatus.is_lagging ? "" : "..."),
isLagging: startupStatus.is_lagging,
});
setTimeout(() => {
@ -50,19 +52,30 @@ export class SplashScreen extends React.Component {
}
componentDidMount() {
lbry.connect()
.then(() => { this.updateStatus() })
lbry
.connect()
.then(() => {
this.updateStatus();
})
.catch(() => {
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."
})
})
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() {
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}
/>
);
}
}

View file

@ -1,23 +1,16 @@
import React from 'react'
import {
connect,
} from 'react-redux'
import {
selectCurrentPage,
selectHeaderLinks,
} from 'selectors/app'
import {
doNavigate,
} from 'actions/app'
import SubHeader from './view'
import React from "react";
import { connect } from "react-redux";
import { selectCurrentPage, selectHeaderLinks } from "selectors/app";
import { doNavigate } from "actions/app";
import SubHeader from "./view";
const select = (state, props) => ({
currentPage: selectCurrentPage(state),
subLinks: selectHeaderLinks(state),
})
});
const perform = (dispatch) => ({
navigate: (path) => dispatch(doNavigate(path)),
})
const perform = dispatch => ({
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 Link from 'component/link'
import React from "react";
import Link from "component/link";
const SubHeader = (props) => {
const {
subLinks,
currentPage,
navigate,
modifier,
} = props
const SubHeader = props => {
const { 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(
<Link onClick={(event) => navigate(`/${link}`, event)} key={link} className={link == currentPage ? 'sub-header-selected' : 'sub-header-unselected' }>
<Link
onClick={event => navigate(`/${link}`, event)}
key={link}
className={
link == currentPage ? "sub-header-selected" : "sub-header-unselected"
}
>
{subLinks[link]}
</Link>
)
);
}
return (
<nav className={'sub-header' + (modifier ? ' sub-header--' + modifier : '')}>
<nav
className={"sub-header" + (modifier ? " sub-header--" + modifier : "")}
>
{links}
</nav>
)
}
);
};
export default SubHeader
export default SubHeader;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,19 @@
import React from "react";
import { connect } from "react-redux";
import { doUserEmailNew } from "actions/user";
import {
selectEmailNewIsPending,
selectEmailNewErrorMessage,
} from "selectors/user";
import UserEmailNew from "./view";
const select = state => ({
isPending: selectEmailNewIsPending(state),
errorMessage: selectEmailNewErrorMessage(state),
});
const perform = dispatch => ({
addUserEmail: email => dispatch(doUserEmailNew(email)),
});
export default connect(select, perform)(UserEmailNew);

View file

@ -0,0 +1,61 @@
import React from "react";
import Link from "component/link";
import { FormRow } from "component/form.js";
class UserEmailNew extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
email: "",
};
}
handleEmailChanged(event) {
this.setState({
email: event.target.value,
});
}
handleSubmit(event) {
event.preventDefault();
this.props.addUserEmail(this.state.email);
}
render() {
const { errorMessage, isPending } = this.props;
return (
<form
className="form-input-width"
onSubmit={event => {
this.handleSubmit(event);
}}
>
<FormRow
type="text"
label="Email"
placeholder="scrwvwls@lbry.io"
name="email"
value={this.state.email}
errorMessage={errorMessage}
onChange={event => {
this.handleEmailChanged(event);
}}
/>
<div className="form-row-submit">
<Link
button="primary"
label="Next"
disabled={isPending}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div>
</form>
);
}
}
export default UserEmailNew;

View file

@ -0,0 +1,21 @@
import React from "react";
import { connect } from "react-redux";
import { doUserEmailVerify } from "actions/user";
import {
selectEmailVerifyIsPending,
selectEmailToVerify,
selectEmailVerifyErrorMessage,
} from "selectors/user";
import UserEmailVerify from "./view";
const select = state => ({
isPending: selectEmailVerifyIsPending(state),
email: selectEmailToVerify(state),
errorMessage: selectEmailVerifyErrorMessage(state),
});
const perform = dispatch => ({
verifyUserEmail: code => dispatch(doUserEmailVerify(code)),
});
export default connect(select, perform)(UserEmailVerify);

View file

@ -0,0 +1,68 @@
import React from "react";
import Link from "component/link";
import { FormRow } from "component/form.js";
class UserEmailVerify extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
code: "",
};
}
handleCodeChanged(event) {
this.setState({
code: event.target.value,
});
}
handleSubmit(event) {
event.preventDefault();
this.props.verifyUserEmail(this.state.code);
}
render() {
const { errorMessage, isPending } = this.props;
return (
<form
className="form-input-width"
onSubmit={event => {
this.handleSubmit(event);
}}
>
<FormRow
type="text"
label="Verification Code"
placeholder="a94bXXXXXXXXXXXXXX"
name="code"
value={this.state.code}
onChange={event => {
this.handleCodeChanged(event);
}}
errorMessage={errorMessage}
/>
{/* render help separately so it always shows */}
<div className="form-field__helper">
<p>
Email <Link href="mailto:help@lbry.io" label="help@lbry.io" /> if
you did not receive or are having trouble with your code.
</p>
</div>
<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>
</form>
);
}
}
export default UserEmailVerify;

View file

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

View file

@ -0,0 +1,14 @@
import React from "react";
const LoadingScreen = ({ status, spinner = true }) =>
<div className="video__loading-screen">
<div>
{spinner && <div className="video__loading-spinner" />}
<div className="video__loading-status">
{status}
</div>
</div>
</div>;
export default LoadingScreen;

View file

@ -0,0 +1,92 @@
import React from "react";
import FilePrice from "component/filePrice";
import Link from "component/link";
import Modal from "component/modal";
class VideoPlayButton extends React.PureComponent {
onPurchaseConfirmed() {
this.props.closeModal();
this.props.startPlaying();
this.props.loadVideo(this.props.uri);
}
onWatchClick() {
this.props.purchaseUri(this.props.uri).then(() => {
if (!this.props.modal) {
this.props.startPlaying();
}
});
}
render() {
const {
button,
label,
metadata,
metadata: { title },
uri,
modal,
closeModal,
isLoading,
costInfo,
fileInfo,
mediaType,
} = this.props;
/*
title={
isLoading ? "Video is Loading" :
!costInfo ? "Waiting on cost info..." :
fileInfo === undefined ? "Waiting on file info..." : ""
}
*/
const disabled =
isLoading ||
fileInfo === undefined ||
(fileInfo === null && (!costInfo || costInfo.cost === undefined));
const icon = ["audio", "video"].indexOf(mediaType) !== -1
? "icon-play"
: "icon-folder-o";
return (
<div>
<Link
button={button ? button : null}
disabled={disabled}
label={label ? label : ""}
className="video__play-button"
icon={icon}
onClick={this.onWatchClick.bind(this)}
/>
<Modal
contentLabel={__("Not enough credits")}
isOpen={modal == "notEnoughCredits"}
onConfirmed={closeModal}
>
{__("You don't have enough LBRY credits to pay for this stream.")}
</Modal>
<Modal
type="confirm"
isOpen={modal == "affirmPurchaseAndPlay"}
contentLabel={__("Confirm Purchase")}
onConfirmed={this.onPurchaseConfirmed.bind(this)}
onAborted={closeModal}
>
{__("This will purchase")} <strong>{title}</strong> {__("for")}
{" "}<strong><FilePrice uri={uri} look="plain" /></strong>
{" "}{__("credits")}.
</Modal>
<Modal
isOpen={modal == "timedOut"}
onConfirmed={closeModal}
contentLabel={__("Timed Out")}
>
{__("Sorry, your download timed out :(")}
</Modal>
</div>
);
}
}
export default VideoPlayButton;

View file

@ -0,0 +1,102 @@
import React from "react";
import { Thumbnail } from "component/common";
import player from "render-media";
import fs from "fs";
import LoadingScreen from "./loading-screen";
class VideoPlayer extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
hasMetadata: false,
startedPlaying: false,
unplayable: false,
};
}
componentDidMount() {
const container = this.refs.media;
const { mediaType } = this.props;
const loadedMetadata = e => {
this.setState({ hasMetadata: true, startedPlaying: true });
this.refs.media.children[0].play();
};
const renderMediaCallback = err => {
if (err) this.setState({ unplayable: true });
};
player.append(
this.file(),
container,
{ autoplay: false, controls: true },
renderMediaCallback.bind(this)
);
const mediaElement = this.refs.media.children[0];
if (mediaElement) {
mediaElement.addEventListener(
"loadedmetadata",
loadedMetadata.bind(this),
{
once: true,
}
);
}
}
componentDidUpdate() {
const { mediaType, downloadCompleted } = this.props;
const { startedPlaying } = this.state;
if (this.playableType() && !startedPlaying && downloadCompleted) {
const container = this.refs.media.children[0];
player.render(this.file(), container, { autoplay: true, controls: true });
}
}
file() {
const { downloadPath, filename } = this.props;
return {
name: filename,
createReadStream: opts => {
return fs.createReadStream(downloadPath, opts);
},
};
}
playableType() {
const { mediaType } = this.props;
return ["audio", "video"].indexOf(mediaType) !== -1;
}
render() {
const { mediaType, poster } = this.props;
const { hasMetadata, unplayable } = this.state;
const noMetadataMessage = "Waiting for metadata.";
const unplayableMessage = "Sorry, looks like we can't play this file.";
const needsMetadata = this.playableType();
return (
<div>
{["audio", "application"].indexOf(mediaType) !== -1 &&
(!this.playableType() || hasMetadata) &&
!unplayable &&
<Thumbnail src={poster} className="video-embedded" />}
{this.playableType() &&
!hasMetadata &&
!unplayable &&
<LoadingScreen status={noMetadataMessage} />}
{unplayable &&
<LoadingScreen status={unplayableMessage} spinner={false} />}
<div ref="media" />
</div>
);
}
}
export default VideoPlayer;

View file

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

View file

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

View file

@ -1,36 +1,44 @@
import React from 'react';
import Link from 'component/link';
import {
Address
} from 'component/common';
import React from "react";
import Link from "component/link";
import { Address } from "component/common";
class WalletAddress extends React.Component {
class WalletAddress extends React.PureComponent {
componentWillMount() {
this.props.checkAddressIsMine(this.props.receiveAddress)
this.props.checkAddressIsMine(this.props.receiveAddress);
}
render() {
const {
receiveAddress,
getNewAddress,
gettingNewAddress,
} = this.props
const { receiveAddress, getNewAddress, gettingNewAddress } = this.props;
return (
<section className="card">
<div className="card__title-primary">
<h3>Wallet Address</h3>
<h3>{__("Wallet Address")}</h3>
</div>
<div className="card__content">
<Address address={receiveAddress} />
</div>
<div className="card__actions">
<Link label="Get New Address" button="primary" icon='icon-refresh' onClick={getNewAddress} disabled={gettingNewAddress} />
<Link
label={__("Get New Address")}
button="primary"
icon="icon-refresh"
onClick={getNewAddress}
disabled={gettingNewAddress}
/>
</div>
<div className="card__content">
<div className="help">
<p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p>
<p>You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.</p>
<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>
@ -38,4 +46,4 @@ class WalletAddress extends React.Component {
}
}
export default WalletAddress
export default WalletAddress;

View file

@ -1,36 +1,31 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doCloseModal,
} from 'actions/app'
import React from "react";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import {
doSendDraftTransaction,
doSetDraftTransactionAmount,
doSetDraftTransactionAddress,
} from 'actions/wallet'
import {
selectCurrentModal,
} from 'selectors/app'
} from "actions/wallet";
import { selectCurrentModal } from "selectors/app";
import {
selectDraftTransactionAmount,
selectDraftTransactionAddress,
} from 'selectors/wallet'
} from "selectors/wallet";
import WalletSend from './view'
import WalletSend from "./view";
const select = (state) => ({
const select = state => ({
modal: selectCurrentModal(state),
address: selectDraftTransactionAddress(state),
amount: selectDraftTransactionAmount(state),
})
});
const perform = (dispatch) => ({
const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()),
sendToAddress: () => dispatch(doSendDraftTransaction()),
setAmount: (event) => dispatch(doSetDraftTransactionAmount(event.target.value)),
setAddress: (event) => dispatch(doSetDraftTransactionAddress(event.target.value)),
})
setAmount: event => dispatch(doSetDraftTransactionAmount(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,11 +1,9 @@
import React from 'react';
import Link from 'component/link';
import Modal from 'component/modal';
import {
FormRow
} from 'component/form';
import React from "react";
import Link from "component/link";
import Modal from "component/modal";
import { FormRow } from "component/form";
const WalletSend = (props) => {
const WalletSend = props => {
const {
sendToAddress,
closeModal,
@ -14,36 +12,74 @@ const WalletSend = (props) => {
setAddress,
amount,
address,
} = props
} = props;
return (
<section className="card">
<form onSubmit={sendToAddress}>
<div className="card__title-primary">
<h3>Send Credits</h3>
<h3>{__("Send Credits")}</h3>
</div>
<div className="card__content">
<FormRow label="Amount" postfix="LBC" step="0.01" type="number" placeholder="1.23" size="10" onChange={setAmount} value={amount} />
<FormRow
label={__("Amount")}
postfix="LBC"
step="0.01"
type="number"
placeholder="1.23"
size="10"
onChange={setAmount}
value={amount}
/>
</div>
<div className="card__content">
<FormRow label="Recipient Address" placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs" type="text" size="60" onChange={setAddress} value={address} />
<FormRow
label={__("Recipient Address")}
placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs"
type="text"
size="60"
onChange={setAddress}
value={address}
/>
</div>
<div className="card__actions card__actions--form-submit">
<Link button="primary" label="Send" onClick={sendToAddress} disabled={!(parseFloat(amount) > 0.0) || !address} />
<input type='submit' className='hidden' />
<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>}
{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

@ -0,0 +1,28 @@
import React from "react";
import rewards from "rewards";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import { selectUserIsRewardApproved } from "selectors/user";
import {
makeSelectHasClaimedReward,
makeSelectClaimRewardError,
makeSelectRewardByType,
} from "selectors/rewards";
import WelcomeModal from "./view";
const select = (state, props) => {
const selectHasClaimed = makeSelectHasClaimedReward(),
selectReward = makeSelectRewardByType();
return {
hasClaimed: selectHasClaimed(state, { reward_type: rewards.TYPE_NEW_USER }),
isRewardApproved: selectUserIsRewardApproved(state),
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
};
};
const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()),
});
export default connect(select, perform)(WelcomeModal);

View file

@ -0,0 +1,75 @@
import React from "react";
import { Modal } from "component/modal";
import { CreditAmount } from "component/common";
import Link from "component/link";
import RewardLink from "component/rewardLink";
class WelcomeModal extends React.PureComponent {
render() {
const { closeModal, hasClaimed, isRewardApproved, reward } = this.props;
return !hasClaimed
? <Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY">
<section>
<h3 className="modal__header">Welcome to LBRY.</h3>
<p>
Using LBRY is like dating a centaur. Totally normal up top, and
{" "}<em>way different</em> underneath.
</p>
<p>Up top, LBRY is similar to popular media sites.</p>
<p>
Below, LBRY is controlled by users -- you -- via blockchain and
decentralization.
</p>
<p>
Thank you for making content freedom possible!
{" "}{isRewardApproved ? __("Here's a nickel, kid.") : ""}
</p>
<div className="text-center">
{isRewardApproved
? <RewardLink reward_type="new_user" button="primary" />
: <Link
button="primary"
onClick={closeModal}
label="Continue"
/>}
</div>
</section>
</Modal>
: <Modal
type="alert"
overlayClassName="modal-overlay modal-overlay--clear"
isOpen={true}
contentLabel="Welcome to LBRY"
onConfirmed={closeModal}
>
<section>
<h3 className="modal__header">About Your Reward</h3>
<p>
You earned a reward of
{" "}<CreditAmount amount={reward.reward_amount} label={false} />
{" "}LBRY
credits, or <em>LBC</em>.
</p>
<p>
This reward will show in your Wallet momentarily, probably while
you are reading this message.
</p>
<p>
LBC is used to compensate creators, to publish, and to have say in
how the network works.
</p>
<p>
No need to understand it all just yet! Try watching or downloading
something next.
</p>
<p>
Finally, know that LBRY is an early beta and that it earns the
name.
</p>
</section>
</Modal>;
}
}
export default WelcomeModal;

View file

@ -1,25 +1,19 @@
import React from 'react'
import {
connect
} from 'react-redux'
import lbryuri from 'lbryuri.js'
import {
selectWunderBarAddress,
selectWunderBarIcon
} from 'selectors/search'
import {
doNavigate,
} from 'actions/app'
import Wunderbar from './view'
import React from "react";
import { connect } from "react-redux";
import lbryuri from "lbryuri.js";
import { selectWunderBarAddress, selectWunderBarIcon } from "selectors/search";
import { doNavigate } from "actions/app";
import Wunderbar from "./view";
const select = (state) => ({
const select = state => ({
address: selectWunderBarAddress(state),
icon: selectWunderBarIcon(state)
})
icon: selectWunderBarIcon(state),
});
const perform = (dispatch) => ({
onSearch: (query) => dispatch(doNavigate('/search', { query, })),
onSubmit: (query) => dispatch(doNavigate('/show', { uri: lbryuri.normalize(query) } ))
})
const perform = dispatch => ({
onSearch: query => dispatch(doNavigate("/search", { 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,14 +1,14 @@
import React from 'react';
import lbryuri from 'lbryuri.js';
import {Icon} from 'component/common.js';
import React from "react";
import lbryuri from "lbryuri.js";
import { Icon } from "component/common.js";
class WunderBar extends React.PureComponent {
static TYPING_TIMEOUT = 800
static TYPING_TIMEOUT = 800;
static propTypes = {
onSearch: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired
}
onSubmit: React.PropTypes.func.isRequired,
};
constructor(props) {
super(props);
@ -24,7 +24,7 @@ class WunderBar extends React.PureComponent {
this.onReceiveRef = this.onReceiveRef.bind(this);
this.state = {
address: this.props.address,
icon: this.props.icon
icon: this.props.icon,
};
}
@ -35,13 +35,11 @@ class WunderBar extends React.PureComponent {
}
onChange(event) {
if (this._userTypingTimer)
{
if (this._userTypingTimer) {
clearTimeout(this._userTypingTimer);
}
this.setState({ address: event.target.value })
this.setState({ address: event.target.value });
this._isSearchDispatchPending = true;
@ -58,7 +56,10 @@ class WunderBar extends React.PureComponent {
}
componentWillReceiveProps(nextProps) {
if (nextProps.viewingPage !== this.props.viewingPage || nextProps.address != this.props.address) {
if (
nextProps.viewingPage !== this.props.viewingPage ||
nextProps.address != this.props.address
) {
this.setState({ address: nextProps.address, icon: nextProps.icon });
}
}
@ -67,14 +68,17 @@ class WunderBar extends React.PureComponent {
this._stateBeforeSearch = this.state;
let newState = {
icon: "icon-search",
isActive: true
}
isActive: true,
};
this._focusPending = true;
//below is hacking, improved when we have proper routing
if (!this.state.address.startsWith('lbry://') && this.state.icon !== "icon-search") //onFocus, if they are not on an exact URL or a search page, clear the bar
{
newState.address = '';
if (
!this.state.address.startsWith("lbry://") &&
this.state.icon !== "icon-search"
) {
//onFocus, if they are not on an exact URL or a search page, clear the bar
newState.address = "";
}
this.setState(newState);
}
@ -83,14 +87,13 @@ class WunderBar extends React.PureComponent {
if (this._isSearchDispatchPending) {
setTimeout(() => {
this.onBlur();
}, WunderBar.TYPING_TIMEOUT + 1)
}, WunderBar.TYPING_TIMEOUT + 1);
} else {
let commonState = {isActive: false};
let commonState = { isActive: false };
if (this._resetOnNextBlur) {
this.setState(Object.assign({}, this._stateBeforeSearch, commonState));
this._input.value = this.state.address;
}
else {
} else {
this._resetOnNextBlur = true;
this._stateBeforeSearch = this.state;
this.setState(commonState);
@ -116,9 +119,8 @@ class WunderBar extends React.PureComponent {
onKeyPress(event) {
if (event.charCode == 13 && this._input.value) {
let uri = null,
method = "onSubmit";
method = "onSubmit";
this._resetOnNextBlur = false;
clearTimeout(this._userTypingTimer);
@ -126,7 +128,8 @@ class WunderBar extends React.PureComponent {
try {
uri = lbryuri.normalize(this._input.value);
this.setState({ value: uri });
} catch (error) { //then it's not a valid URL, so let's search
} catch (error) {
//then it's not a valid URL, so let's search
uri = this._input.value;
method = "onSearch";
}
@ -142,16 +145,23 @@ class WunderBar extends React.PureComponent {
render() {
return (
<div className={'wunderbar' + (this.state.isActive ? ' wunderbar--active' : '')}>
{this.state.icon ? <Icon fixed icon={this.state.icon} /> : '' }
<input className="wunderbar__input" type="search"
ref={this.onReceiveRef}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChange={this.onChange}
onKeyPress={this.onKeyPress}
value={this.state.address}
placeholder="Find movies, music, games, and more" />
<div
className={
"wunderbar" + (this.state.isActive ? " wunderbar--active" : "")
}
>
{this.state.icon ? <Icon fixed icon={this.state.icon} /> : ""}
<input
className="wunderbar__input"
type="search"
ref={this.onReceiveRef}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChange={this.onChange}
onKeyPress={this.onKeyPress}
value={this.state.address}
placeholder={__("Find movies, music, games, and more")}
/>
</div>
);
}

View file

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

View file

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

View file

@ -1,68 +1,95 @@
export const CHANGE_PATH = 'CHANGE_PATH'
export const OPEN_MODAL = 'OPEN_MODAL'
export const CLOSE_MODAL = 'CLOSE_MODAL'
export const HISTORY_BACK = 'HISTORY_BACK'
export const SHOW_SNACKBAR = 'SHOW_SNACKBAR'
export const REMOVE_SNACKBAR_SNACK = 'REMOVE_SNACKBAR_SNACK'
export const CHANGE_PATH = "CHANGE_PATH";
export const OPEN_MODAL = "OPEN_MODAL";
export const CLOSE_MODAL = "CLOSE_MODAL";
export const HISTORY_BACK = "HISTORY_BACK";
export const SHOW_SNACKBAR = "SHOW_SNACKBAR";
export const REMOVE_SNACKBAR_SNACK = "REMOVE_SNACKBAR_SNACK";
export const DAEMON_READY = 'DAEMON_READY'
export const DAEMON_READY = "DAEMON_READY";
// Upgrades
export const UPGRADE_CANCELLED = 'UPGRADE_CANCELLED'
export const DOWNLOAD_UPGRADE = 'DOWNLOAD_UPGRADE'
export const UPGRADE_DOWNLOAD_STARTED = 'UPGRADE_DOWNLOAD_STARTED'
export const UPGRADE_DOWNLOAD_COMPLETED = 'UPGRADE_DOWNLOAD_COMPLETED'
export const UPGRADE_DOWNLOAD_PROGRESSED = 'UPGRADE_DOWNLOAD_PROGRESSED'
export const CHECK_UPGRADE_AVAILABLE = 'CHECK_UPGRADE_AVAILABLE'
export const UPDATE_VERSION = 'UPDATE_VERSION'
export const SKIP_UPGRADE = 'SKIP_UPGRADE'
export const START_UPGRADE = 'START_UPGRADE'
export const UPGRADE_CANCELLED = "UPGRADE_CANCELLED";
export const DOWNLOAD_UPGRADE = "DOWNLOAD_UPGRADE";
export const UPGRADE_DOWNLOAD_STARTED = "UPGRADE_DOWNLOAD_STARTED";
export const UPGRADE_DOWNLOAD_COMPLETED = "UPGRADE_DOWNLOAD_COMPLETED";
export const UPGRADE_DOWNLOAD_PROGRESSED = "UPGRADE_DOWNLOAD_PROGRESSED";
export const CHECK_UPGRADE_AVAILABLE = "CHECK_UPGRADE_AVAILABLE";
export const UPDATE_VERSION = "UPDATE_VERSION";
export const SKIP_UPGRADE = "SKIP_UPGRADE";
export const START_UPGRADE = "START_UPGRADE";
// Wallet
export const GET_NEW_ADDRESS_STARTED = 'GET_NEW_ADDRESS_STARTED'
export const GET_NEW_ADDRESS_COMPLETED = 'GET_NEW_ADDRESS_COMPLETED'
export const FETCH_TRANSACTIONS_STARTED = 'FETCH_TRANSACTIONS_STARTED'
export const FETCH_TRANSACTIONS_COMPLETED = 'FETCH_TRANSACTIONS_COMPLETED'
export const UPDATE_BALANCE = 'UPDATE_BALANCE'
export const CHECK_ADDRESS_IS_MINE_STARTED = 'CHECK_ADDRESS_IS_MINE_STARTED'
export const CHECK_ADDRESS_IS_MINE_COMPLETED = 'CHECK_ADDRESS_IS_MINE_COMPLETED'
export const SET_DRAFT_TRANSACTION_AMOUNT = 'SET_DRAFT_TRANSACTION_AMOUNT'
export const SET_DRAFT_TRANSACTION_ADDRESS = 'SET_DRAFT_TRANSACTION_ADDRESS'
export const SEND_TRANSACTION_STARTED = 'SEND_TRANSACTION_STARTED'
export const SEND_TRANSACTION_COMPLETED = 'SEND_TRANSACTION_COMPLETED'
export const SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED'
export const GET_NEW_ADDRESS_STARTED = "GET_NEW_ADDRESS_STARTED";
export const GET_NEW_ADDRESS_COMPLETED = "GET_NEW_ADDRESS_COMPLETED";
export const FETCH_TRANSACTIONS_STARTED = "FETCH_TRANSACTIONS_STARTED";
export const FETCH_TRANSACTIONS_COMPLETED = "FETCH_TRANSACTIONS_COMPLETED";
export const UPDATE_BALANCE = "UPDATE_BALANCE";
export const CHECK_ADDRESS_IS_MINE_STARTED = "CHECK_ADDRESS_IS_MINE_STARTED";
export const CHECK_ADDRESS_IS_MINE_COMPLETED =
"CHECK_ADDRESS_IS_MINE_COMPLETED";
export const SET_DRAFT_TRANSACTION_AMOUNT = "SET_DRAFT_TRANSACTION_AMOUNT";
export const SET_DRAFT_TRANSACTION_ADDRESS = "SET_DRAFT_TRANSACTION_ADDRESS";
export const SEND_TRANSACTION_STARTED = "SEND_TRANSACTION_STARTED";
export const SEND_TRANSACTION_COMPLETED = "SEND_TRANSACTION_COMPLETED";
export const SEND_TRANSACTION_FAILED = "SEND_TRANSACTION_FAILED";
// Content
export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED'
export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED'
export const RESOLVE_URI_STARTED = 'RESOLVE_URI_STARTED'
export const RESOLVE_URI_COMPLETED = 'RESOLVE_URI_COMPLETED'
export const RESOLVE_URI_CANCELED = 'RESOLVE_URI_CANCELED'
export const FETCH_CHANNEL_CLAIMS_STARTED = 'FETCH_CHANNEL_CLAIMS_STARTED'
export const FETCH_CHANNEL_CLAIMS_COMPLETED = 'FETCH_CHANNEL_CLAIMS_COMPLETED'
export const FETCH_CLAIM_LIST_MINE_STARTED = 'FETCH_CLAIM_LIST_MINE_STARTED'
export const FETCH_CLAIM_LIST_MINE_COMPLETED = 'FETCH_CLAIM_LIST_MINE_COMPLETED'
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED'
export const FILE_LIST_COMPLETED = 'FILE_LIST_COMPLETED'
export const FETCH_FILE_INFO_STARTED = 'FETCH_FILE_INFO_STARTED'
export const FETCH_FILE_INFO_COMPLETED = 'FETCH_FILE_INFO_COMPLETED'
export const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED'
export const FETCH_COST_INFO_COMPLETED = 'FETCH_COST_INFO_COMPLETED'
export const LOADING_VIDEO_STARTED = 'LOADING_VIDEO_STARTED'
export const LOADING_VIDEO_COMPLETED = 'LOADING_VIDEO_COMPLETED'
export const LOADING_VIDEO_FAILED = 'LOADING_VIDEO_FAILED'
export const DOWNLOADING_STARTED = 'DOWNLOADING_STARTED'
export const DOWNLOADING_PROGRESSED = 'DOWNLOADING_PROGRESSED'
export const DOWNLOADING_COMPLETED = 'DOWNLOADING_COMPLETED'
export const PLAY_VIDEO_STARTED = 'PLAY_VIDEO_STARTED'
export const FETCH_AVAILABILITY_STARTED = 'FETCH_AVAILABILITY_STARTED'
export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED'
export const FILE_DELETE = 'FILE_DELETE'
export const FETCH_FEATURED_CONTENT_STARTED = "FETCH_FEATURED_CONTENT_STARTED";
export const FETCH_FEATURED_CONTENT_COMPLETED =
"FETCH_FEATURED_CONTENT_COMPLETED";
export const RESOLVE_URI_STARTED = "RESOLVE_URI_STARTED";
export const RESOLVE_URI_COMPLETED = "RESOLVE_URI_COMPLETED";
export const RESOLVE_URI_CANCELED = "RESOLVE_URI_CANCELED";
export const FETCH_CHANNEL_CLAIMS_STARTED = "FETCH_CHANNEL_CLAIMS_STARTED";
export const FETCH_CHANNEL_CLAIMS_COMPLETED = "FETCH_CHANNEL_CLAIMS_COMPLETED";
export const FETCH_CLAIM_LIST_MINE_STARTED = "FETCH_CLAIM_LIST_MINE_STARTED";
export const FETCH_CLAIM_LIST_MINE_COMPLETED =
"FETCH_CLAIM_LIST_MINE_COMPLETED";
export const FILE_LIST_STARTED = "FILE_LIST_STARTED";
export const FILE_LIST_COMPLETED = "FILE_LIST_COMPLETED";
export const FETCH_FILE_INFO_STARTED = "FETCH_FILE_INFO_STARTED";
export const FETCH_FILE_INFO_COMPLETED = "FETCH_FILE_INFO_COMPLETED";
export const FETCH_COST_INFO_STARTED = "FETCH_COST_INFO_STARTED";
export const FETCH_COST_INFO_COMPLETED = "FETCH_COST_INFO_COMPLETED";
export const LOADING_VIDEO_STARTED = "LOADING_VIDEO_STARTED";
export const LOADING_VIDEO_COMPLETED = "LOADING_VIDEO_COMPLETED";
export const LOADING_VIDEO_FAILED = "LOADING_VIDEO_FAILED";
export const DOWNLOADING_STARTED = "DOWNLOADING_STARTED";
export const DOWNLOADING_PROGRESSED = "DOWNLOADING_PROGRESSED";
export const DOWNLOADING_COMPLETED = "DOWNLOADING_COMPLETED";
export const PLAY_VIDEO_STARTED = "PLAY_VIDEO_STARTED";
export const FETCH_AVAILABILITY_STARTED = "FETCH_AVAILABILITY_STARTED";
export const FETCH_AVAILABILITY_COMPLETED = "FETCH_AVAILABILITY_COMPLETED";
export const FILE_DELETE = "FILE_DELETE";
// Search
export const SEARCH_STARTED = 'SEARCH_STARTED'
export const SEARCH_COMPLETED = 'SEARCH_COMPLETED'
export const SEARCH_CANCELLED = 'SEARCH_CANCELLED'
export const SEARCH_STARTED = "SEARCH_STARTED";
export const SEARCH_COMPLETED = "SEARCH_COMPLETED";
export const SEARCH_CANCELLED = "SEARCH_CANCELLED";
// Settings
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED'
export const DAEMON_SETTINGS_RECEIVED = "DAEMON_SETTINGS_RECEIVED";
// User
export const AUTHENTICATION_STARTED = "AUTHENTICATION_STARTED";
export const AUTHENTICATION_SUCCESS = "AUTHENTICATION_SUCCESS";
export const AUTHENTICATION_FAILURE = "AUTHENTICATION_FAILURE";
export const USER_EMAIL_DECLINE = "USER_EMAIL_DECLINE";
export const USER_EMAIL_NEW_STARTED = "USER_EMAIL_NEW_STARTED";
export const USER_EMAIL_NEW_SUCCESS = "USER_EMAIL_NEW_SUCCESS";
export const USER_EMAIL_NEW_EXISTS = "USER_EMAIL_NEW_EXISTS";
export const USER_EMAIL_NEW_FAILURE = "USER_EMAIL_NEW_FAILURE";
export const USER_EMAIL_VERIFY_STARTED = "USER_EMAIL_VERIFY_STARTED";
export const USER_EMAIL_VERIFY_SUCCESS = "USER_EMAIL_VERIFY_SUCCESS";
export const USER_EMAIL_VERIFY_FAILURE = "USER_EMAIL_VERIFY_FAILURE";
export const USER_FETCH_STARTED = "USER_FETCH_STARTED";
export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS";
export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE";
// Rewards
export const FETCH_REWARDS_STARTED = "FETCH_REWARDS_STARTED";
export const FETCH_REWARDS_COMPLETED = "FETCH_REWARDS_COMPLETED";
export const CLAIM_REWARD_STARTED = "CLAIM_REWARD_STARTED";
export const CLAIM_REWARD_SUCCESS = "CLAIM_REWARD_SUCCESS";
export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE";
export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR";

View file

@ -0,0 +1 @@
export const WELCOME = "welcome";

View file

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

View file

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

View file

@ -1,56 +1,61 @@
import {getSession, setSession} from './utils.js';
import lbry from './lbry.js';
import { getSession, setSession, setLocal } from "./utils.js";
import lbry from "./lbry.js";
const querystring = require('querystring');
const querystring = require("querystring");
const lbryio = {
_accessToken: getSession('accessToken'),
_accessToken: getSession("accessToken"),
_authenticationPromise: null,
_user : null,
enabled: true
enabled: true,
};
const CONNECTION_STRING = process.env.LBRY_APP_API_URL ?
process.env.LBRY_APP_API_URL.replace(/\/*$/,'/') : // exactly one slash at the end
'https://api.lbry.io/'
const CONNECTION_STRING = process.env.LBRY_APP_API_URL
? process.env.LBRY_APP_API_URL.replace(/\/*$/, "/") // exactly one slash at the end
: "https://api.lbry.io/";
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
lbryio._exchangePromise = null;
lbryio._exchangeLastFetched = null;
lbryio.getExchangeRates = function() {
if (!lbryio._exchangeLastFetched || Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT) {
if (
!lbryio._exchangeLastFetched ||
Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT
) {
lbryio._exchangePromise = new Promise((resolve, reject) => {
lbryio.call('lbc', 'exchange_rate', {}, 'get', true).then(({lbc_usd, lbc_btc, btc_usd}) => {
const rates = {lbc_usd, lbc_btc, btc_usd};
resolve(rates);
}).catch(reject);
lbryio
.call("lbc", "exchange_rate", {}, "get", true)
.then(({ lbc_usd, lbc_btc, btc_usd }) => {
const rates = { lbc_usd, lbc_btc, btc_usd };
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(resource, action, params = {}, method = "get") {
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
if (!lbryio.enabled && (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) {
reject(new Error("Something went wrong making an internal API call."));
xhr.addEventListener("error", function(event) {
reject(
new Error(__("Something went wrong making an internal API call."))
);
});
xhr.addEventListener('timeout', function() {
reject(new Error('XMLHttpRequest connection timed out'));
xhr.addEventListener("timeout", function() {
reject(new Error(__("XMLHttpRequest connection timed out")));
});
xhr.addEventListener('load', function() {
xhr.addEventListener("load", function() {
const response = JSON.parse(xhr.responseText);
if (!response.success) {
@ -59,15 +64,17 @@ lbryio.call = function(resource, action, params={}, method='get', evenIfDisabled
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} : {},
}
}));
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);
@ -79,85 +86,107 @@ lbryio.call = function(resource, action, params={}, method='get', evenIfDisabled
//const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}};
// Temp app ID based auth:
const fullParams = {app_id: lbryio.getAccessToken(), ...params};
const fullParams = { app_id: lbryio.getAccessToken(), ...params };
if (method == 'get') {
xhr.open('get', CONNECTION_STRING + resource + '/' + action + '?' + querystring.stringify(fullParams), true);
if (method == "get") {
xhr.open(
"get",
CONNECTION_STRING +
resource +
"/" +
action +
"?" +
querystring.stringify(fullParams),
true
);
xhr.send();
} else if (method == 'post') {
xhr.open('post', CONNECTION_STRING + resource + '/' + action, true);
} 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"));
reject(new Error(__("Invalid method")));
}
});
};
lbryio.getAccessToken = () => {
const token = getSession('accessToken');
const token = getSession("accessToken");
return token ? token.toString().trim() : token;
}
};
lbryio.setAccessToken = (token) => {
setSession('accessToken', token ? token.toString().trim() : token)
}
lbryio.setAccessToken = token => {
setSession("accessToken", token ? token.toString().trim() : token);
};
lbryio.setCurrentUser = (resolve, reject) => {
lbryio
.call("user", "me")
.then(data => {
resolve(data);
})
.catch(function(err) {
lbryio.setAccessToken(null);
reject(err);
});
};
lbryio.authenticate = function() {
if (!lbryio.enabled) {
return new Promise((resolve, reject) => {
resolve({
id: 1,
has_verified_email: true
})
})
language: "en",
has_email: true,
has_verified_email: true,
is_reward_approved: false,
is_reward_eligible: false,
});
});
}
if (lbryio._authenticationPromise === null) {
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);
}
})
}
if (!lbryio.getAccessToken()) {
lbryio.call('user', 'new', {
language: 'en',
app_id: installation_id,
}, 'post').then(function(responseData) {
if (!responseData.id) {
reject(new Error("Received invalid authentication response."));
}
lbryio.setAccessToken(installation_id)
setCurrentUser()
}).catch(function(error) {
/*
until we have better error code format, assume all errors are duplicate application id
if we're wrong, this will be caught by later attempts to make a valid call
*/
lbryio.setAccessToken(installation_id)
setCurrentUser()
})
} else {
setCurrentUser()
}
}).catch(reject);
if (!lbryio.getAccessToken()) {
lbryio
.call(
"user",
"new",
{
language: "en",
app_id: installation_id,
},
"post"
)
.then(function(responseData) {
if (!responseData.id) {
reject(
new Error("Received invalid authentication response.")
);
}
lbryio.setAccessToken(installation_id);
lbryio.setCurrentUser(resolve, reject);
})
.catch(function(error) {
/*
until we have better error code format, assume all errors are duplicate application id
if we're wrong, this will be caught by later attempts to make a valid call
*/
lbryio.setAccessToken(installation_id);
lbryio.setCurrentUser(resolve, reject);
});
} else {
lbryio.setCurrentUser(resolve, reject);
}
})
.catch(reject);
});
}
return lbryio._authenticationPromise;
}
};
export default lbryio;

View file

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

View file

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

View file

@ -1,67 +1,61 @@
import React from 'react';
import ReactDOM from 'react-dom';
import lbry from './lbry.js';
import lbryio from './lbryio.js';
import lighthouse from './lighthouse.js';
import App from 'component/app/index.js';
import SplashScreen from 'component/splash.js';
import SnackBar from 'component/snackBar';
import {AuthOverlay} from 'component/auth.js';
import { Provider } from 'react-redux';
import store from 'store.js';
import {
doChangePath,
doNavigate,
doDaemonReady
} from 'actions/app'
import {
doFetchDaemonSettings
} from 'actions/settings'
import {
doFileList
} from 'actions/file_info'
import {
toQueryString,
} from 'util/query_params'
import React from "react";
import ReactDOM from "react-dom";
import lbry from "./lbry.js";
import App from "component/app/index.js";
import SnackBar from "component/snackBar";
import { Provider } from "react-redux";
import store from "store.js";
import SplashScreen from "component/splash.js";
import AuthOverlay from "component/authOverlay";
import { doChangePath, doNavigate, doDaemonReady } from "actions/app";
import { toQueryString } from "util/query_params";
const {remote, ipcRenderer, shell} = require('electron');
const contextMenu = remote.require('./menu/context-menu');
const app = require('./app')
const env = ENV;
const { remote, ipcRenderer, shell } = require("electron");
const contextMenu = remote.require("./menu/context-menu");
const app = require("./app");
lbry.showMenuIfNeeded();
window.addEventListener('contextmenu', (event) => {
contextMenu.showContextMenu(remote.getCurrentWindow(), event.x, event.y,
lbry.getClientSetting('showDeveloperMenu'));
window.addEventListener("contextmenu", event => {
contextMenu.showContextMenu(
remote.getCurrentWindow(),
event.x,
event.y,
lbry.getClientSetting("showDeveloperMenu")
);
event.preventDefault();
});
window.addEventListener('popstate', (event, param) => {
const params = event.state
const pathParts = document.location.pathname.split('/')
const route = '/' + pathParts[pathParts.length - 1]
const queryString = toQueryString(params)
window.addEventListener("popstate", (event, param) => {
const params = event.state;
const pathParts = document.location.pathname.split("/");
const route = "/" + pathParts[pathParts.length - 1];
const queryString = toQueryString(params);
let action
let action;
if (route.match(/html$/)) {
action = doChangePath('/discover')
action = doChangePath("/discover");
} 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 }))
ipcRenderer.on("open-uri-requested", (event, uri) => {
if (uri && uri.startsWith("lbry://")) {
app.store.dispatch(doNavigate("/show", { uri }));
}
});
document.addEventListener('click', (event) => {
document.addEventListener("click", event => {
var target = event.target;
while (target && target !== document) {
if (target.matches('a[href^="http"]')) {
if (
target.matches('a[href^="http"]') ||
target.matches('a[href^="mailto"]')
) {
event.preventDefault();
shell.openExternal(target.href);
return;
@ -72,21 +66,33 @@ document.addEventListener('click', (event) => {
const initialState = app.store.getState();
// import whyDidYouUpdate from "why-did-you-update";
// if (env === "development") {
// /*
// https://github.com/garbles/why-did-you-update
// "A function that monkey patches React and notifies you in the console when
// potentially unnecessary re-renders occur."
//
// Just checks if props change between updates. Can be fixed by manually
// adding a check in shouldComponentUpdate or using React.PureComponent
// */
// whyDidYouUpdate(React);
// }
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 = []
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(doDaemonReady());
app.store.dispatch(doDaemonReady())
app.store.dispatch(doChangePath('/discover'))
app.store.dispatch(doFetchDaemonSettings())
app.store.dispatch(doFileList())
ReactDOM.render(<Provider store={store}><div>{ lbryio.enabled ? <AuthOverlay/> : '' }<App /><SnackBar /></div></Provider>, canvas)
ReactDOM.render(
<Provider store={store}>
<div><AuthOverlay /><App /><SnackBar /></div>
</Provider>,
canvas
);
}
if (window.sessionStorage.getItem('loaded') == 'y') {
if (window.sessionStorage.getItem("loaded") == "y") {
onDaemonReady();
} else {
ReactDOM.render(<SplashScreen onLoadDone={onDaemonReady} />, canvas);

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