diff --git a/.gitignore b/.gitignore
index d1b68b9b7..17a8ad330 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,5 @@ dist
*.pyc
.#*
+
+build/daemon.zip
diff --git a/app/main.js b/app/main.js
index 07aed14d0..f95d63ba9 100644
--- a/app/main.js
+++ b/app/main.js
@@ -1,4 +1,7 @@
const {app, BrowserWindow, ipcMain} = require('electron');
+
+require('electron-debug')({showDevTools: true});
+
const path = require('path');
const jayson = require('jayson');
// tree-kill has better cross-platform handling of
diff --git a/package.json b/package.json
index f342a1e52..b1fed994b 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
},
"devDependencies": {
"electron": "^1.4.15",
- "electron-builder": "^11.7.0"
+ "electron-builder": "^11.7.0",
+ "electron-debug": "^1.1.0"
}
}
diff --git a/ui/js/actions/app.js b/ui/js/actions/app.js
new file mode 100644
index 000000000..d2c7679f3
--- /dev/null
+++ b/ui/js/actions/app.js
@@ -0,0 +1,210 @@
+import * as types from 'constants/action_types'
+import lbry from 'lbry'
+import {
+ selectUpdateUrl,
+ selectUpgradeDownloadDir,
+ selectUpgradeDownloadItem,
+ selectUpgradeFilename,
+ selectPageTitle,
+} from 'selectors/app'
+
+const {remote, ipcRenderer, shell} = require('electron');
+const path = require('path');
+const app = require('electron').remote.app;
+const {download} = remote.require('electron-dl');
+const fs = remote.require('fs');
+
+export function doNavigate(path) {
+ return function(dispatch, getState) {
+ dispatch({
+ type: types.NAVIGATE,
+ data: {
+ path,
+ }
+ })
+
+ const state = getState()
+ const pageTitle = selectPageTitle(state)
+ window.document.title = pageTitle
+ }
+}
+
+export function doLogoClick() {
+}
+
+export function doOpenDrawer() {
+ return {
+ type: types.OPEN_DRAWER
+ }
+}
+
+export function doCloseDrawer() {
+ return {
+ type: types.CLOSE_DRAWER
+ }
+}
+
+export function doOpenModal(modal) {
+ return {
+ type: types.OPEN_MODAL,
+ data: {
+ modal
+ }
+ }
+}
+
+export function doCloseModal() {
+ return {
+ type: types.CLOSE_MODAL,
+ }
+}
+
+export function doUpdateDownloadProgress(percent) {
+ return {
+ type: types.UPGRADE_DOWNLOAD_PROGRESSED,
+ data: {
+ percent: percent
+ }
+ }
+}
+
+export function doSkipUpgrade() {
+ return {
+ type: types.SKIP_UPGRADE
+ }
+}
+
+export function doStartUpgrade() {
+ return function(dispatch, getState) {
+ const state = getState()
+ const upgradeDownloadPath = selectUpgradeDownloadDir(state)
+
+ ipcRenderer.send('upgrade', upgradeDownloadPath)
+ }
+}
+
+export function doDownloadUpgrade() {
+ return function(dispatch, getState) {
+ const state = getState()
+ // Make a new directory within temp directory so the filename is guaranteed to be available
+ const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep);
+ const upgradeFilename = selectUpgradeFilename(state)
+
+ let options = {
+ onProgress: (p) => dispatch(doUpdateDownloadProgress(Math.round(p * 100))),
+ directory: dir,
+ };
+ download(remote.getCurrentWindow(), selectUpdateUrl(state), options)
+ .then(downloadItem => {
+ /**
+ * TODO: get the download path directly from the download object. It should just be
+ * downloadItem.getSavePath(), but the copy on the main process is being garbage collected
+ * too soon.
+ */
+
+ const _upgradeDownloadItem = downloadItem;
+ const _upgradeDownloadPath = path.join(dir, upgradeFilename);
+
+ dispatch({
+ type: types.UPGRADE_DOWNLOAD_COMPLETED,
+ data: {
+ dir,
+ downloadItem
+ }
+ })
+ });
+
+ dispatch({
+ type: types.UPGRADE_DOWNLOAD_STARTED
+ })
+ dispatch({
+ type: types.OPEN_MODAL,
+ data: {
+ modal: 'downloading'
+ }
+ })
+ }
+}
+
+export function doCancelUpgrade() {
+ return function(dispatch, getState) {
+ const state = getState()
+ const upgradeDownloadItem = selectUpgradeDownloadItem(state)
+
+ if (upgradeDownloadItem) {
+ /*
+ * Right now the remote reference to the download item gets garbage collected as soon as the
+ * the download is over (maybe even earlier), so trying to cancel a finished download may
+ * throw an error.
+ */
+ try {
+ upgradeDownloadItem.cancel();
+ } catch (err) {
+ console.error(err)
+ // Do nothing
+ }
+ }
+
+ dispatch({ type: types.UPGRADE_CANCELLED })
+ }
+}
+
+export function doCheckUpgradeAvailable() {
+ return function(dispatch, getState) {
+ const state = getState()
+
+ lbry.checkNewVersionAvailable(({isAvailable}) => {
+ if (!isAvailable) {
+ return;
+ }
+
+ lbry.getVersionInfo((versionInfo) => {
+ dispatch({
+ type: types.UPDATE_VERSION,
+ data: {
+ version: versionInfo.lbrynet_version
+ }
+ })
+ dispatch({
+ type: types.OPEN_MODAL,
+ data: {
+ modal: 'upgrade'
+ }
+ })
+ });
+ });
+ }
+}
+
+export function doAlertError(errorList) {
+ return function(dispatch, getState) {
+ const state = getState()
+
+ dispatch({
+ type: types.OPEN_MODAL,
+ data: {
+ modal: 'error',
+ error: errorList
+ }
+ })
+ }
+}
+
+export function doSearch(term) {
+ return function(dispatch, getState) {
+ const state = getState()
+
+ dispatch({
+ type: types.START_SEARCH,
+ data: {
+ searchTerm: term
+ }
+ })
+ }
+}
+
+export function doDaemonReady() {
+ return {
+ type: types.DAEMON_READY
+ }
+}
diff --git a/ui/js/actions/availability.js b/ui/js/actions/availability.js
new file mode 100644
index 000000000..74c85eaa3
--- /dev/null
+++ b/ui/js/actions/availability.js
@@ -0,0 +1,39 @@
+import * as types from 'constants/action_types'
+import lbry from 'lbry'
+import {
+ selectCurrentUri,
+} from 'selectors/app'
+
+export function doFetchUriAvailability(uri) {
+ return function(dispatch, getState) {
+ dispatch({
+ type: types.FETCH_AVAILABILITY_STARTED,
+ data: { uri }
+ })
+
+ const successCallback = (availability) => {
+ dispatch({
+ type: types.FETCH_AVAILABILITY_COMPLETED,
+ data: {
+ availability,
+ uri,
+ }
+ })
+ }
+
+ const errorCallback = () => {
+ console.debug('error')
+ }
+
+ lbry.get_availability({ uri }, successCallback, errorCallback)
+ }
+}
+
+export function doFetchCurrentUriAvailability() {
+ return function(dispatch, getState) {
+ const state = getState()
+ const uri = selectCurrentUri(state)
+
+ dispatch(doFetchUriAvailability(uri))
+ }
+}
diff --git a/ui/js/actions/content.js b/ui/js/actions/content.js
new file mode 100644
index 000000000..d887fc318
--- /dev/null
+++ b/ui/js/actions/content.js
@@ -0,0 +1,291 @@
+import * as types from 'constants/action_types'
+import lbry from 'lbry'
+import lbryio from 'lbryio'
+import lbryuri from 'lbryuri'
+import rewards from 'rewards'
+import {
+ selectCurrentUri,
+} from 'selectors/app'
+import {
+ selectBalance,
+} from 'selectors/wallet'
+import {
+ selectSearchTerm,
+} from 'selectors/content'
+import {
+ selectCurrentUriFileInfo,
+ selectDownloadingByUri,
+} from 'selectors/file_info'
+import {
+ selectCurrentUriCostInfo,
+} from 'selectors/cost_info'
+import {
+ selectCurrentResolvedUriClaimOutpoint,
+} from 'selectors/content'
+import {
+ selectClaimsByUri,
+} from 'selectors/claims'
+import {
+ doOpenModal,
+} from 'actions/app'
+import {
+ doFetchCostInfoForUri,
+} from 'actions/cost_info'
+import batchActions from 'util/batchActions'
+
+export function doResolveUri(uri) {
+ return function(dispatch, getState) {
+ dispatch({
+ type: types.RESOLVE_URI_STARTED,
+ data: { uri }
+ })
+
+ lbry.resolve({ uri }).then((resolutionInfo) => {
+ const {
+ claim,
+ certificate,
+ } = resolutionInfo
+
+ dispatch({
+ type: types.RESOLVE_URI_COMPLETED,
+ data: {
+ uri,
+ claim,
+ certificate,
+ }
+ })
+
+ dispatch(doFetchCostInfoForUri(uri))
+ })
+ }
+}
+
+export function doFetchDownloadedContent() {
+ return function(dispatch, getState) {
+ const state = getState()
+
+ dispatch({
+ type: types.FETCH_DOWNLOADED_CONTENT_STARTED,
+ })
+
+ lbry.claim_list_mine().then((myClaimInfos) => {
+ lbry.file_list().then((fileInfos) => {
+ const myClaimOutpoints = myClaimInfos.map(({txid, nout}) => txid + ':' + nout);
+
+ fileInfos.forEach(fileInfo => {
+ const uri = lbryuri.build({
+ channelName: fileInfo.channel_name,
+ contentName: fileInfo.name,
+ })
+ const claim = selectClaimsByUri(state)[uri]
+ if (!claim) dispatch(doResolveUri(uri))
+ })
+
+ dispatch({
+ type: types.FETCH_DOWNLOADED_CONTENT_COMPLETED,
+ data: {
+ fileInfos: fileInfos.filter(({outpoint}) => !myClaimOutpoints.includes(outpoint)),
+ }
+ })
+ });
+ });
+ }
+}
+
+export function doFetchPublishedContent() {
+ return function(dispatch, getState) {
+ const state = getState()
+
+ dispatch({
+ type: types.FETCH_PUBLISHED_CONTENT_STARTED,
+ })
+
+ lbry.claim_list_mine().then((claimInfos) => {
+ dispatch({
+ type: types.FETCH_MY_CLAIMS_COMPLETED,
+ data: {
+ claims: claimInfos,
+ }
+ })
+ lbry.file_list().then((fileInfos) => {
+ const myClaimOutpoints = claimInfos.map(({txid, nout}) => txid + ':' + nout)
+
+ dispatch({
+ type: types.FETCH_PUBLISHED_CONTENT_COMPLETED,
+ data: {
+ fileInfos: fileInfos.filter(({outpoint}) => myClaimOutpoints.includes(outpoint)),
+ }
+ })
+ })
+ })
+ }
+}
+
+export function doFetchFeaturedContent() {
+ return function(dispatch, getState) {
+ const state = getState()
+
+ dispatch({
+ type: types.FETCH_FEATURED_CONTENT_STARTED,
+ })
+
+ const success = ({ Categories, Uris }) => {
+ dispatch({
+ type: types.FETCH_FEATURED_CONTENT_COMPLETED,
+ data: {
+ categories: Categories,
+ uris: Uris,
+ }
+ })
+
+ Object.keys(Uris).forEach((category) => {
+ Uris[category].forEach((uri) => {
+ dispatch(doResolveUri(lbryuri.normalize(uri)))
+ })
+ })
+ }
+
+ const failure = () => {
+ }
+
+ lbryio.call('discover', 'list', { version: "early-access" } )
+ .then(success, failure)
+ }
+}
+
+export function doUpdateLoadStatus(uri, outpoint) {
+ return function(dispatch, getState) {
+ const state = getState()
+
+ lbry.file_list({
+ outpoint: outpoint,
+ full_status: true,
+ }).then(([fileInfo]) => {
+ if(!fileInfo || fileInfo.written_bytes == 0) {
+ // download hasn't started yet
+ setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)) }, 250)
+ } else if (fileInfo.completed) {
+ // TODO this isn't going to get called if they reload the client before
+ // the download finished
+ rewards.claimNextPurchaseReward()
+ dispatch({
+ type: types.DOWNLOADING_COMPLETED,
+ data: {
+ uri,
+ 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,
+ fileInfo,
+ progress,
+ }
+ })
+ setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)) }, 250)
+ }
+ })
+ }
+}
+
+export function doPlayVideo(uri) {
+ return {
+ type: types.PLAY_VIDEO_STARTED,
+ data: { uri }
+ }
+}
+
+export function doDownloadFile(uri, streamInfo) {
+ return function(dispatch, getState) {
+ const state = getState()
+
+ lbry.file_list({ outpoint: streamInfo.outpoint, full_status: true }).then(([fileInfo]) => {
+ dispatch({
+ type: types.DOWNLOADING_STARTED,
+ data: {
+ uri,
+ fileInfo,
+ }
+ })
+ })
+
+ lbryio.call('file', 'view', {
+ uri: uri,
+ outpoint: streamInfo.outpoint,
+ claimId: streamInfo.claim_id,
+ }).catch(() => {})
+ dispatch(doUpdateLoadStatus(uri, streamInfo.outpoint))
+ }
+}
+
+export function doLoadVideo() {
+ return function(dispatch, getState) {
+ const state = getState()
+ const uri = selectCurrentUri(state)
+
+ dispatch({
+ type: types.LOADING_VIDEO_STARTED,
+ data: {
+ uri
+ }
+ })
+
+ lbry.get({ uri }).then(streamInfo => {
+ if (streamInfo === null || typeof streamInfo !== 'object') {
+ dispatch({
+ type: types.LOADING_VIDEO_FAILED,
+ data: { uri }
+ })
+ dispatch(doOpenModal('timedOut'))
+ } else {
+ dispatch(doDownloadFile(uri, streamInfo))
+ }
+ })
+ }
+}
+
+export function doWatchVideo() {
+ return function(dispatch, getState) {
+ const state = getState()
+ const uri = selectCurrentUri(state)
+ const balance = selectBalance(state)
+ const fileInfo = selectCurrentUriFileInfo(state)
+ const costInfo = selectCurrentUriCostInfo(state)
+ const downloadingByUri = selectDownloadingByUri(state)
+ const alreadyDownloading = !!downloadingByUri[uri]
+ const { cost } = costInfo
+
+ // we already fully downloaded the file
+ if (fileInfo && fileInfo.completed) {
+ return Promise.resolve()
+ }
+
+ // we are already downloading the file
+ if (alreadyDownloading) {
+ return Promise.resolve()
+ }
+
+ // the file is free or we have partially downloaded it
+ if (cost <= 0.01 || fileInfo.download_directory) {
+ dispatch(doLoadVideo())
+ return Promise.resolve()
+ }
+
+ if (cost > balance) {
+ dispatch(doOpenModal('notEnoughCredits'))
+ } else {
+ dispatch(doOpenModal('affirmPurchase'))
+ }
+
+ return Promise.resolve()
+ }
+}
diff --git a/ui/js/actions/cost_info.js b/ui/js/actions/cost_info.js
new file mode 100644
index 000000000..278f1d7b4
--- /dev/null
+++ b/ui/js/actions/cost_info.js
@@ -0,0 +1,36 @@
+import * as types from 'constants/action_types'
+import {
+ selectCurrentUri,
+} from 'selectors/app'
+import lbry from 'lbry'
+
+export function doFetchCostInfoForUri(uri) {
+ return function(dispatch, getState) {
+ dispatch({
+ type: types.FETCH_COST_INFO_STARTED,
+ data: {
+ uri,
+ }
+ })
+
+ lbry.getCostInfo(uri).then(costInfo => {
+ dispatch({
+ type: types.FETCH_COST_INFO_COMPLETED,
+ data: {
+ uri,
+ costInfo,
+ }
+ })
+ })
+ }
+}
+
+export function doFetchCurrentUriCostInfo() {
+ return function(dispatch, getState) {
+ const state = getState()
+ const uri = selectCurrentUri(state)
+
+ dispatch(doFetchCostInfoForUri(uri))
+ }
+}
+
diff --git a/ui/js/actions/file_info.js b/ui/js/actions/file_info.js
new file mode 100644
index 000000000..b7611fb3d
--- /dev/null
+++ b/ui/js/actions/file_info.js
@@ -0,0 +1,102 @@
+import * as types from 'constants/action_types'
+import lbry from 'lbry'
+import {
+ selectCurrentUri,
+} from 'selectors/app'
+import {
+ selectCurrentUriClaimOutpoint,
+} from 'selectors/claims'
+import {
+ doCloseModal,
+} from 'actions/app'
+
+const {
+ shell,
+} = require('electron')
+
+export function doFetchCurrentUriFileInfo() {
+ return function(dispatch, getState) {
+ const state = getState()
+ const uri = selectCurrentUri(state)
+ const outpoint = selectCurrentUriClaimOutpoint(state)
+
+ dispatch({
+ type: types.FETCH_FILE_INFO_STARTED,
+ data: {
+ uri,
+ outpoint,
+ }
+ })
+
+ lbry.file_list({ outpoint: outpoint, full_status: true }).then(([fileInfo]) => {
+ dispatch({
+ type: types.FETCH_FILE_INFO_COMPLETED,
+ data: {
+ uri,
+ fileInfo,
+ }
+ })
+ })
+ }
+}
+
+export function doOpenFileInShell(fileInfo) {
+ return function(dispatch, getState) {
+ shell.openItem(fileInfo.download_path)
+ }
+}
+
+export function doOpenFileInFolder(fileInfo) {
+ return function(dispatch, getState) {
+ shell.showItemInFolder(fileInfo.download_path)
+ }
+}
+
+export function doDeleteFile(uri, fileInfo, deleteFromComputer) {
+ return function(dispatch, getState) {
+ dispatch({
+ type: types.DELETE_FILE_STARTED,
+ data: {
+ uri,
+ fileInfo,
+ deleteFromComputer,
+ }
+ })
+
+ const successCallback = () => {
+ dispatch({
+ type: types.DELETE_FILE_COMPLETED,
+ data: {
+ uri,
+ }
+ })
+ dispatch(doCloseModal())
+ }
+
+ lbry.removeFile(fileInfo.outpoint, deleteFromComputer, successCallback)
+ }
+}
+
+export function doFetchDownloadedContent() {
+ return function(dispatch, getState) {
+ const state = getState()
+
+ dispatch({
+ type: types.FETCH_DOWNLOADED_CONTENT_STARTED,
+ })
+
+ lbry.claim_list_mine().then((myClaimInfos) => {
+ lbry.file_list().then((fileInfos) => {
+ const myClaimOutpoints = myClaimInfos.map(({txid, nout}) => txid + ':' + nout);
+
+ dispatch({
+ type: types.FETCH_DOWNLOADED_CONTENT_COMPLETED,
+ data: {
+ fileInfos: fileInfos.filter(({outpoint}) => !myClaimOutpoints.includes(outpoint)),
+ }
+ })
+ });
+ });
+ }
+}
+
diff --git a/ui/js/actions/rewards.js b/ui/js/actions/rewards.js
new file mode 100644
index 000000000..84f65047f
--- /dev/null
+++ b/ui/js/actions/rewards.js
@@ -0,0 +1,36 @@
+import * as types from 'constants/action_types'
+import lbry from 'lbry'
+import lbryio from 'lbryio';
+import rewards from 'rewards'
+
+export function doFetchRewards() {
+ return function(dispatch, getState) {
+ const state = getState()
+
+ dispatch({
+ type: types.FETCH_REWARDS_STARTED,
+ })
+
+ lbryio.call('reward', 'list', {}).then(function(userRewards) {
+ dispatch({
+ type: types.FETCH_REWARDS_COMPLETED,
+ data: { userRewards }
+ })
+ });
+ }
+}
+
+export function doClaimReward(rewardType) {
+ return function(dispatch, getState) {
+ try {
+ rewards.claimReward(rewards[rewardType])
+ dispatch({
+ type: types.REWARD_CLAIMED,
+ data: {
+ reward: rewards[rewardType]
+ }
+ })
+ } catch(err) {
+ }
+ }
+}
diff --git a/ui/js/actions/search.js b/ui/js/actions/search.js
new file mode 100644
index 000000000..6bdb10592
--- /dev/null
+++ b/ui/js/actions/search.js
@@ -0,0 +1,77 @@
+import * as types from 'constants/action_types'
+import lbry from 'lbry'
+import lbryio from 'lbryio'
+import lbryuri from 'lbryuri'
+import lighthouse from 'lighthouse'
+import {
+ selectSearchQuery,
+} from 'selectors/search'
+import {
+ doResolveUri,
+} from 'actions/content'
+import {
+ doNavigate,
+} from 'actions/app'
+import {
+ selectCurrentPage,
+} from 'selectors/app'
+
+export function doSearchContent(query) {
+ return function(dispatch, getState) {
+ const state = getState()
+ const page = selectCurrentPage(state)
+
+
+ if (!query) {
+ return dispatch({
+ type: types.SEARCH_CANCELLED,
+ })
+ }
+
+ dispatch({
+ type: types.SEARCH_STARTED,
+ data: { query }
+ })
+
+ if(page != 'discover' && query != undefined) dispatch(doNavigate('discover'))
+
+ lighthouse.search(query).then(results => {
+ results.forEach(result => {
+ const uri = lbryuri.build({
+ channelName: result.channel_name,
+ contentName: result.name,
+ claimId: result.channel_id || result.claim_id,
+ })
+ dispatch(doResolveUri(uri))
+ })
+
+ dispatch({
+ type: types.SEARCH_COMPLETED,
+ data: {
+ query,
+ results,
+ }
+ })
+ })
+ }
+}
+
+export function doActivateSearch() {
+ return function(dispatch, getState) {
+ const state = getState()
+ const page = selectCurrentPage(state)
+ const query = selectSearchQuery(state)
+
+ if(page != 'discover' && query != undefined) dispatch(doNavigate('discover'))
+
+ dispatch({
+ type: types.ACTIVATE_SEARCH,
+ })
+ }
+}
+
+export function doDeactivateSearch() {
+ return {
+ type: types.DEACTIVATE_SEARCH,
+ }
+}
diff --git a/ui/js/actions/wallet.js b/ui/js/actions/wallet.js
new file mode 100644
index 000000000..79b22d8d6
--- /dev/null
+++ b/ui/js/actions/wallet.js
@@ -0,0 +1,125 @@
+import * as types from 'constants/action_types'
+import lbry from 'lbry'
+import {
+ selectDraftTransaction,
+ selectDraftTransactionAmount,
+ selectBalance,
+} from 'selectors/wallet'
+import {
+ doOpenModal,
+} from 'actions/app'
+
+export function doUpdateBalance(balance) {
+ return {
+ type: types.UPDATE_BALANCE,
+ data: {
+ balance: balance
+ }
+ }
+}
+
+export function doFetchTransactions() {
+ return function(dispatch, getState) {
+ dispatch({
+ type: types.FETCH_TRANSACTIONS_STARTED
+ })
+
+ lbry.call('get_transaction_history', {}, (results) => {
+ dispatch({
+ type: types.FETCH_TRANSACTIONS_COMPLETED,
+ data: {
+ transactions: results
+ }
+ })
+ })
+ }
+}
+
+export function doGetNewAddress() {
+ return function(dispatch, getState) {
+ dispatch({
+ type: types.GET_NEW_ADDRESS_STARTED
+ })
+
+ lbry.wallet_new_address().then(function(address) {
+ localStorage.setItem('wallet_address', address);
+ dispatch({
+ type: types.GET_NEW_ADDRESS_COMPLETED,
+ data: { address }
+ })
+ })
+ }
+}
+
+export function doCheckAddressIsMine(address) {
+ return function(dispatch, getState) {
+ dispatch({
+ type: types.CHECK_ADDRESS_IS_MINE_STARTED
+ })
+
+ lbry.checkAddressIsMine(address, (isMine) => {
+ if (!isMine) dispatch(doGetNewAddress())
+
+ dispatch({
+ type: types.CHECK_ADDRESS_IS_MINE_COMPLETED
+ })
+ })
+ }
+}
+
+export function doSendDraftTransaction() {
+ return function(dispatch, getState) {
+ const state = getState()
+ const draftTx = selectDraftTransaction(state)
+ const balance = selectBalance(state)
+ const amount = selectDraftTransactionAmount(state)
+
+ if (balance - amount < 1) {
+ return dispatch(doOpenModal('insufficientBalance'))
+ }
+
+ dispatch({
+ type: types.SEND_TRANSACTION_STARTED,
+ })
+
+ const successCallback = (results) => {
+ if(results === true) {
+ dispatch({
+ type: types.SEND_TRANSACTION_COMPLETED,
+ })
+ dispatch(doOpenModal('transactionSuccessful'))
+ }
+ else {
+ dispatch({
+ type: types.SEND_TRANSACTION_FAILED,
+ data: { error: results }
+ })
+ dispatch(doOpenModal('transactionFailed'))
+ }
+ }
+
+ const errorCallback = (error) => {
+ dispatch({
+ type: types.SEND_TRANSACTION_FAILED,
+ data: { error: error.message }
+ })
+ dispatch(doOpenModal('transactionFailed'))
+ }
+
+ lbry.sendToAddress(draftTx.amount, draftTx.address, successCallback, errorCallback);
+ }
+}
+
+export function doSetDraftTransactionAmount(amount) {
+ return {
+ type: types.SET_DRAFT_TRANSACTION_AMOUNT,
+ data: { amount }
+ }
+}
+
+export function doSetDraftTransactionAddress(address) {
+ return {
+ type: types.SET_DRAFT_TRANSACTION_ADDRESS,
+ data: { address }
+ }
+}
diff --git a/ui/js/app.js b/ui/js/app.js
index 9cfb43137..c2362282d 100644
--- a/ui/js/app.js
+++ b/ui/js/app.js
@@ -1,337 +1,342 @@
-import React from 'react';
-import {Line} from 'rc-progress';
+import store from 'store.js';
-import lbry from './lbry.js';
-import SettingsPage from './page/settings.js';
-import HelpPage from './page/help.js';
-import WatchPage from './page/watch.js';
-import ReportPage from './page/report.js';
-import StartPage from './page/start.js';
-import RewardsPage from './page/rewards.js';
-import RewardPage from './page/reward.js';
-import WalletPage from './page/wallet.js';
-import ShowPage from './page/show.js';
-import PublishPage from './page/publish.js';
-import DiscoverPage from './page/discover.js';
-import DeveloperPage from './page/developer.js';
-import {FileListDownloaded, FileListPublished} from './page/file-list.js';
-import Drawer from './component/drawer.js';
-import Header from './component/header.js';
-import {Modal, ExpandableModal} from './component/modal.js';
-import {Link} from './component/link.js';
-
-
-const {remote, ipcRenderer, shell} = require('electron');
-const {download} = remote.require('electron-dl');
-const path = require('path');
-const app = require('electron').remote.app;
-const fs = remote.require('fs');
-
-
-var App = React.createClass({
- _error_key_labels: {
- connectionString: 'API connection string',
- method: 'Method',
- params: 'Parameters',
- code: 'Error code',
- message: 'Error message',
- data: 'Error data',
- },
- _fullScreenPages: ['watch'],
-
- _upgradeDownloadItem: null,
- _isMounted: false,
- _version: null,
- getUpdateUrl: function() {
- switch (process.platform) {
- case 'darwin':
- return 'https://lbry.io/get/lbry.dmg';
- case 'linux':
- return 'https://lbry.io/get/lbry.deb';
- case 'win32':
- return 'https://lbry.io/get/lbry.exe';
- default:
- throw 'Unknown platform';
- }
- },
- // Hard code the filenames as a temporary workaround, because
- // electron-dl throws errors when you try to get the filename
- getUpgradeFilename: function() {
- switch (process.platform) {
- case 'darwin':
- return `LBRY-${this._version}.dmg`;
- case 'linux':
- return `LBRY_${this._version}_amd64.deb`;
- case 'windows':
- return `LBRY.Setup.${this._version}.exe`;
- default:
- throw 'Unknown platform';
- }
- },
- getViewingPageAndArgs: function(address) {
- // For now, routes are in format ?page or ?page=args
- let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/);
- return {
- viewingPage: viewingPage,
- pageArgs: pageArgs === undefined ? null : pageArgs
- };
- },
- getInitialState: function() {
- var match, param, val, viewingPage, pageArgs,
- drawerOpenRaw = sessionStorage.getItem('drawerOpen');
-
- return Object.assign(this.getViewingPageAndArgs(window.location.search), {
- drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true,
- errorInfo: null,
- modal: null,
- downloadProgress: null,
- downloadComplete: false,
- });
- },
- componentWillMount: function() {
- document.addEventListener('unhandledError', (event) => {
- this.alertError(event.detail);
- });
-
- //open links in external browser and skip full redraw on changing page
- document.addEventListener('click', (event) => {
- var target = event.target;
- while (target && target !== document) {
- if (target.matches('a[href^="http"]')) {
- event.preventDefault();
- shell.openExternal(target.href);
- return;
- }
- if (target.matches('a[href^="?"]')) {
- event.preventDefault();
- if (this._isMounted) {
- history.pushState({}, document.title, target.getAttribute('href'));
- this.registerHistoryPop();
- this.setState(this.getViewingPageAndArgs(target.getAttribute('href')));
- }
- }
- target = target.parentNode;
- }
- });
-
- if (!sessionStorage.getItem('upgradeSkipped')) {
- lbry.checkNewVersionAvailable(({isAvailable}) => {
- if (!isAvailable) {
- return;
- }
-
- lbry.getVersionInfo((versionInfo) => {
- this._version = versionInfo.lbrynet_version;
- this.setState({
- modal: 'upgrade',
- });
- });
- });
- }
- },
- openDrawer: function() {
- sessionStorage.setItem('drawerOpen', true);
- this.setState({ drawerOpen: true });
- },
- closeDrawer: function() {
- sessionStorage.setItem('drawerOpen', false);
- this.setState({ drawerOpen: false });
- },
- closeModal: function() {
- this.setState({
- modal: null,
- });
- },
- componentDidMount: function() {
- this._isMounted = true;
- },
- componentWillUnmount: function() {
- this._isMounted = false;
- },
- registerHistoryPop: function() {
- window.addEventListener("popstate", function() {
- this.setState(this.getViewingPageAndArgs(location.pathname));
- }.bind(this));
- },
- handleUpgradeClicked: function() {
- // Make a new directory within temp directory so the filename is guaranteed to be available
- const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep);
-
- let options = {
- onProgress: (p) => this.setState({downloadProgress: Math.round(p * 100)}),
- directory: dir,
- };
- download(remote.getCurrentWindow(), this.getUpdateUrl(), options)
- .then(downloadItem => {
- /**
- * TODO: get the download path directly from the download object. It should just be
- * downloadItem.getSavePath(), but the copy on the main process is being garbage collected
- * too soon.
- */
-
- this._upgradeDownloadItem = downloadItem;
- this._upgradeDownloadPath = path.join(dir, this.getUpgradeFilename());
- this.setState({
- downloadComplete: true
- });
- });
- this.setState({modal: 'downloading'});
- },
- handleStartUpgradeClicked: function() {
- ipcRenderer.send('upgrade', this._upgradeDownloadPath);
- },
- cancelUpgrade: function() {
- if (this._upgradeDownloadItem) {
- /*
- * Right now the remote reference to the download item gets garbage collected as soon as the
- * the download is over (maybe even earlier), so trying to cancel a finished download may
- * throw an error.
- */
- try {
- this._upgradeDownloadItem.cancel();
- } catch (err) {
- // Do nothing
- }
- }
- this.setState({
- downloadProgress: null,
- downloadComplete: false,
- modal: null,
- });
- },
- handleSkipClicked: function() {
- sessionStorage.setItem('upgradeSkipped', true);
- this.setState({
- modal: null,
- });
- },
- onSearch: function(term) {
- this.setState({
- viewingPage: 'discover',
- pageArgs: term
- });
- },
- alertError: function(error) {
- var errorInfoList = [];
- for (let key of Object.keys(error)) {
- let val = typeof error[key] == 'string' ? error[key] : JSON.stringify(error[key]);
- let label = this._error_key_labels[key];
- errorInfoList.push(
{label} : {val}
);
- }
-
- this.setState({
- modal: 'error',
- errorInfo: ,
- });
- },
- getHeaderLinks: function()
- {
- switch(this.state.viewingPage)
- {
- case 'wallet':
- case 'send':
- case 'receive':
- case 'rewards':
- return {
- '?wallet': 'Overview',
- '?send': 'Send',
- '?receive': 'Receive',
- '?rewards': 'Rewards',
- };
- case 'downloaded':
- case 'published':
- return {
- '?downloaded': 'Downloaded',
- '?published': 'Published',
- };
- default:
- return null;
- }
- },
- getMainContent: function()
- {
- switch(this.state.viewingPage)
- {
- case 'settings':
- return ;
- case 'help':
- return ;
- case 'report':
- return ;
- case 'downloaded':
- return ;
- case 'published':
- return ;
- case 'start':
- return ;
- case 'rewards':
- return ;
- case 'wallet':
- case 'send':
- case 'receive':
- return ;
- case 'show':
- return ;
- case 'publish':
- return ;
- case 'developer':
- return ;
- case 'discover':
- default:
- return ;
- }
- },
- render: function() {
- var mainContent = this.getMainContent(),
- headerLinks = this.getHeaderLinks(),
- searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : '';
-
- return (
- this._fullScreenPages.includes(this.state.viewingPage) ?
- mainContent :
-
-
-
-
- {mainContent}
-
-
- Your version of LBRY is out of date and may be unreliable or insecure.
-
-
- Downloading Update{this.state.downloadProgress ? `: ${this.state.downloadProgress}%` : null}
-
- {this.state.downloadComplete ? (
-
-
-
Click "Begin Upgrade" to start the upgrade process.
-
The app will close, and you will be prompted to install the latest version of LBRY.
-
After the install is complete, please reopen the app.
-
- ) : null }
-
- {this.state.downloadComplete
- ?
- : null}
-
-
-
-
- Error
-
-
-
-
We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.
-
-
-
- );
+const env = process.env.NODE_ENV || 'development';
+const config = require(`./config/${env}`);
+const logs = [];
+const app = {
+ env: env,
+ config: config,
+ store: store,
+ logs: logs,
+ log: function(message) {
+ console.log(message);
+ logs.push(message);
}
+}
+global.app = app;
+module.exports = app;
+
+/*
+ import React from 'react';
+ import {Line} from 'rc-progress';
+
+ import lbry from './lbry.js';
+ import SettingsPage from './page/settings.js';
+ import HelpPage from './page/help.js';
+ import WatchPage from './page/watch.js';
+ import ReportPage from './page/report.js';
+ import StartPage from './page/start.js';
+ import RewardsPage from './page/rewards.js';
+ import RewardPage from './page/reward.js';
+ import WalletPage from './page/wallet.js';
+ import ShowPage from './page/show.js';
+ import PublishPage from './page/publish.js';
+ import DiscoverPage from './page/discover.js';
+ import DeveloperPage from './page/developer.js';
+ import {FileListDownloaded, FileListPublished} from './page/file-list.js';
+ import Drawer from './component/drawer.js';
+ import Header from './component/header.js';
+ import {Modal, ExpandableModal} from './component/modal.js';
+ import Link from './component/link';
+
+
+ const {remote, ipcRenderer, shell} = require('electron');
+ const {download} = remote.require('electron-dl');
+ const path = require('path');
+ const app = require('electron').remote.app;
+ const fs = remote.require('fs');
+
+
+ var App = React.createClass({
+ _error_key_labels: {
+ connectionString: 'API connection string',
+ method: 'Method',
+ params: 'Parameters',
+ code: 'Error code',
+ message: 'Error message',
+ data: 'Error data',
+ },
+ _fullScreenPages: ['watch'],
+
+ _upgradeDownloadItem: null,
+ _isMounted: false,
+ _version: null,
+ getUpdateUrl: function() {
+ switch (process.platform) {
+ case 'darwin':
+ return 'https://lbry.io/get/lbry.dmg';
+ case 'linux':
+ return 'https://lbry.io/get/lbry.deb';
+ case 'win32':
+ return 'https://lbry.io/get/lbry.exe';
+ default:
+ throw 'Unknown platform';
+ }
+ },
+ // Hard code the filenames as a temporary workaround, because
+ // electron-dl throws errors when you try to get the filename
+ getUpgradeFilename: function() {
+ switch (process.platform) {
+ case 'darwin':
+ return `LBRY-${this._version}.dmg`;
+ case 'linux':
+ return `LBRY_${this._version}_amd64.deb`;
+ case 'windows':
+ return `LBRY.Setup.${this._version}.exe`;
+ default:
+ throw 'Unknown platform';
+ }
+ },
+ getViewingPageAndArgs: function(address) {
+ // For now, routes are in format ?page or ?page=args
+ let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/);
+ return {
+ viewingPage: viewingPage,
+ pageArgs: pageArgs === undefined ? null : pageArgs
+ };
+ },
+ getInitialState: function() {
+ var match, param, val, viewingPage, pageArgs,
+ drawerOpenRaw = sessionStorage.getItem('drawerOpen');
+
+ return Object.assign(this.getViewingPageAndArgs(window.location.search), {
+ drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true,
+ errorInfo: null,
+ modal: null,
+ downloadProgress: null,
+ downloadComplete: false,
+ });
+ },
+ componentWillMount: function() {
+ document.addEventListener('unhandledError', (event) => {
+ this.alertError(event.detail);
+ });
+
+ //open links in external browser and skip full redraw on changing page
+ document.addEventListener('click', (event) => {
+ var target = event.target;
+ while (target && target !== document) {
+ if (target.matches('a[href^="http"]')) {
+ event.preventDefault();
+ shell.openExternal(target.href);
+ return;
+ }
+ if (target.matches('a[href^="?"]')) {
+ event.preventDefault();
+ if (this._isMounted) {
+ history.pushState({}, document.title, target.getAttribute('href'));
+ this.registerHistoryPop();
+ this.setState(this.getViewingPageAndArgs(target.getAttribute('href')));
+ }
+ }
+ target = target.parentNode;
+ }
+ });
+
+ if (!sessionStorage.getItem('upgradeSkipped')) {
+ lbry.checkNewVersionAvailable(({isAvailable}) => {
+ if (!isAvailable) {
+ return;
+ }
+
+ lbry.getVersionInfo((versionInfo) => {
+ this._version = versionInfo.lbrynet_version;
+ this.setState({
+ modal: 'upgrade',
+ });
+ });
+ });
+ }
+ },
+ openDrawer: function() {
+ sessionStorage.setItem('drawerOpen', true);
+ this.setState({ drawerOpen: true });
+ },
+ closeDrawer: function() {
+ sessionStorage.setItem('drawerOpen', false);
+ this.setState({ drawerOpen: false });
+ },
+ closeModal: function() {
+ this.setState({
+ modal: null,
+ });
+ },
+ componentDidMount: function() {
+ this._isMounted = true;
+ },
+ componentWillUnmount: function() {
+ this._isMounted = false;
+ },
+ registerHistoryPop: function() {
+ window.addEventListener("popstate", function() {
+ this.setState(this.getViewingPageAndArgs(location.pathname));
+ }.bind(this));
+ },
+ handleUpgradeClicked: function() {
+ // Make a new directory within temp directory so the filename is guaranteed to be available
+ const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep);
+
+ let options = {
+ onProgress: (p) => this.setState({downloadProgress: Math.round(p * 100)}),
+ directory: dir,
+ };
+ download(remote.getCurrentWindow(), this.getUpdateUrl(), options)
+ .then(downloadItem => {
+
+this._upgradeDownloadItem = downloadItem;
+this._upgradeDownloadPath = path.join(dir, this.getUpgradeFilename());
+this.setState({
+ downloadComplete: true
});
+});
+this.setState({modal: 'downloading'});
+},
+handleStartUpgradeClicked: function() {
+ ipcRenderer.send('upgrade', this._upgradeDownloadPath);
+},
+cancelUpgrade: function() {
+ if (this._upgradeDownloadItem) {
+ try {
+ this._upgradeDownloadItem.cancel();
+ } catch (err) {
+ // Do nothing
+ }
+ }
+ this.setState({
+ downloadProgress: null,
+ downloadComplete: false,
+ modal: null,
+ });
+},
+handleSkipClicked: function() {
+ sessionStorage.setItem('upgradeSkipped', true);
+ this.setState({
+ modal: null,
+ });
+},
+onSearch: function(term) {
+ this.setState({
+ viewingPage: 'discover',
+ pageArgs: term
+ });
+},
+alertError: function(error) {
+ var errorInfoList = [];
+ for (let key of Object.keys(error)) {
+ let val = typeof error[key] == 'string' ? error[key] : JSON.stringify(error[key]);
+ let label = this._error_key_labels[key];
+ errorInfoList.push({label} : {val}
);
+ }
+ this.setState({
+ modal: 'error',
+ errorInfo: ,
+ });
+},
+getHeaderLinks: function()
+{
+ switch(this.state.viewingPage)
+ {
+ case 'wallet':
+ case 'send':
+ case 'receive':
+ case 'rewards':
+ return {
+ '?wallet': 'Overview',
+ '?send': 'Send',
+ '?receive': 'Receive',
+ '?rewards': 'Rewards',
+ };
+ case 'downloaded':
+ case 'published':
+ return {
+ '?downloaded': 'Downloaded',
+ '?published': 'Published',
+ };
+ default:
+ return null;
+ }
+},
+getMainContent: function()
+{
+ switch(this.state.viewingPage)
+ {
+ case 'settings':
+ return ;
+ case 'help':
+ return ;
+ case 'report':
+ return ;
+ case 'downloaded':
+ return ;
+ case 'published':
+ return ;
+ case 'start':
+ return ;
+ case 'rewards':
+ return ;
+ case 'wallet':
+ case 'send':
+ case 'receive':
+ return ;
+ case 'show':
+ return ;
+ case 'publish':
+ return ;
+ case 'developer':
+ return ;
+ case 'discover':
+ default:
+ return ;
+ }
+},
+render: function() {
+ var mainContent = this.getMainContent(),
+ headerLinks = this.getHeaderLinks(),
+ searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : '';
-export default App;
+ return (
+ this._fullScreenPages.includes(this.state.viewingPage) ?
+ mainContent :
+
+
+
+
+ {mainContent}
+
+
+ Your version of LBRY is out of date and may be unreliable or insecure.
+
+
+ Downloading Update{this.state.downloadProgress ? `: ${this.state.downloadProgress}%` : null}
+
+ {this.state.downloadComplete ? (
+
+
+
Click "Begin Upgrade" to start the upgrade process.
+
The app will close, and you will be prompted to install the latest version of LBRY.
+
After the install is complete, please reopen the app.
+
+ ) : null }
+
+ {this.state.downloadComplete
+ ?
+ : null}
+
+
+
+
+ Error
+
+
+
+
We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.
+
+
+
+ );
+ */
\ No newline at end of file
diff --git a/ui/js/component/app/index.js b/ui/js/component/app/index.js
new file mode 100644
index 000000000..0920e51d8
--- /dev/null
+++ b/ui/js/component/app/index.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import { connect } from 'react-redux'
+
+import {
+ selectCurrentPage,
+ selectCurrentModal,
+ selectDrawerOpen,
+ selectHeaderLinks,
+ selectSearchTerm,
+} from 'selectors/app'
+import {
+ doCheckUpgradeAvailable,
+ doOpenDrawer,
+ doCloseDrawer,
+ doOpenModal,
+ doCloseModal,
+ doSearch,
+} from 'actions/app'
+import App from './view'
+
+const select = (state) => ({
+ currentPage: selectCurrentPage(state),
+ modal: selectCurrentModal(state),
+ drawerOpen: selectDrawerOpen(state),
+ headerLinks: selectHeaderLinks(state),
+ searchTerm: selectSearchTerm(state)
+})
+
+const perform = (dispatch) => ({
+ checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()),
+ openDrawer: () => dispatch(doOpenDrawer()),
+ closeDrawer: () => dispatch(doCloseDrawer()),
+ openModal: () => dispatch(doOpenModal()),
+ closeModal: () => dispatch(doCloseModal()),
+})
+
+export default connect(select, perform)(App)
diff --git a/ui/js/component/app/view.jsx b/ui/js/component/app/view.jsx
new file mode 100644
index 000000000..ab9a63acf
--- /dev/null
+++ b/ui/js/component/app/view.jsx
@@ -0,0 +1,46 @@
+import React from 'react'
+import Router from 'component/router'
+import Drawer from 'component/drawer';
+import Header from 'component/header';
+import ErrorModal from 'component/errorModal'
+import DownloadingModal from 'component/downloadingModal'
+import UpgradeModal from 'component/upgradeModal'
+import {Line} from 'rc-progress';
+
+const App = React.createClass({
+ componentWillMount: function() {
+ document.addEventListener('unhandledError', (event) => {
+ this.props.alertError(event.detail);
+ });
+
+ if (!this.props.upgradeSkipped) {
+ this.props.checkUpgradeAvailable()
+ }
+ },
+ render: function() {
+ const {
+ currentPage,
+ openDrawer,
+ closeDrawer,
+ modal,
+ drawerOpen,
+ headerLinks,
+ search,
+ searchTerm,
+ } = this.props
+ const searchQuery = (currentPage == 'discover' && searchTerm ? searchTerm : '')
+
+ return
+
+
+
+
+
+ {modal == 'upgrade' &&
}
+ {modal == 'downloading' &&
}
+ {modal == 'error' &&
}
+
+ }
+});
+
+export default App
diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js
index f0e1f1074..4a98d80c9 100644
--- a/ui/js/component/auth.js
+++ b/ui/js/component/auth.js
@@ -2,7 +2,8 @@ import React from "react";
import lbryio from "../lbryio.js";
import Modal from "./modal.js";
import ModalPage from "./modal-page.js";
-import {Link, RewardLink} from "../component/link.js";
+import {RewardLink} from "../component/reward-link.js";
+import Link from "../component/link";
import {FormRow} from "../component/form.js";
import {CreditAmount, Address} from "../component/common.js";
import {getLocal, getSession, setSession, setLocal} from '../utils.js';
diff --git a/ui/js/component/common.js b/ui/js/component/common.js
index 7139e5dfe..ee491cb2f 100644
--- a/ui/js/component/common.js
+++ b/ui/js/component/common.js
@@ -97,54 +97,6 @@ export let CreditAmount = React.createClass({
}
});
-export let FilePrice = React.createClass({
- _isMounted: false,
-
- propTypes: {
- uri: React.PropTypes.string.isRequired,
- look: React.PropTypes.oneOf(['indicator', 'plain']),
- },
-
- getDefaultProps: function() {
- return {
- look: 'indicator',
- }
- },
-
- componentWillMount: function() {
- this.setState({
- cost: null,
- isEstimate: null,
- });
- },
-
- componentDidMount: function() {
- this._isMounted = true;
- lbry.getCostInfo(this.props.uri).then(({cost, includesData}) => {
- if (this._isMounted) {
- this.setState({
- cost: cost,
- isEstimate: !includesData,
- });
- }
- }, (err) => {
- // If we get an error looking up cost information, do nothing
- });
- },
-
- componentWillUnmount: function() {
- this._isMounted = false;
- },
-
- render: function() {
- if (this.state.cost === null) {
- return ??? ;
- }
-
- return
- }
-});
-
var addressStyle = {
fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace',
};
diff --git a/ui/js/component/downloadingModal/index.jsx b/ui/js/component/downloadingModal/index.jsx
new file mode 100644
index 000000000..618f335fb
--- /dev/null
+++ b/ui/js/component/downloadingModal/index.jsx
@@ -0,0 +1,25 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import {
+ doStartUpgrade,
+ doCancelUpgrade,
+} from 'actions/app'
+import {
+ selectDownloadProgress,
+ selectDownloadComplete,
+} from 'selectors/app'
+import DownloadingModal from './view'
+
+const select = (state) => ({
+ downloadProgress: selectDownloadProgress(state),
+ downloadComplete: selectDownloadComplete(state),
+})
+
+const perform = (dispatch) => ({
+ startUpgrade: () => dispatch(doStartUpgrade()),
+ cancelUpgrade: () => dispatch(doCancelUpgrade())
+})
+
+export default connect(select, perform)(DownloadingModal)
diff --git a/ui/js/component/downloadingModal/view.jsx b/ui/js/component/downloadingModal/view.jsx
new file mode 100644
index 000000000..b59605e28
--- /dev/null
+++ b/ui/js/component/downloadingModal/view.jsx
@@ -0,0 +1,40 @@
+import React from 'react'
+import {
+ Modal
+} from 'component/modal'
+import {Line} from 'rc-progress';
+import Link from 'component/link'
+
+class DownloadingModal extends React.Component {
+ render() {
+ const {
+ downloadProgress,
+ downloadComplete,
+ startUpgrade,
+ cancelUpgrade,
+ } = this.props
+
+ return (
+
+ Downloading Update{downloadProgress ? `: ${downloadProgress}%` : null}
+
+ {downloadComplete ? (
+
+
+
Click "Begin Upgrade" to start the upgrade process.
+
The app will close, and you will be prompted to install the latest version of LBRY.
+
After the install is complete, please reopen the app.
+
+ ) : null }
+
+ {downloadComplete
+ ?
+ : null}
+
+
+
+ )
+ }
+}
+
+export default DownloadingModal
diff --git a/ui/js/component/drawer.js b/ui/js/component/drawer.js
deleted file mode 100644
index e719af073..000000000
--- a/ui/js/component/drawer.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import lbry from '../lbry.js';
-import React from 'react';
-import {Link} from './link.js';
-
-var DrawerItem = React.createClass({
- getDefaultProps: function() {
- return {
- subPages: [],
- };
- },
- render: function() {
- var isSelected = (this.props.viewingPage == this.props.href.substr(1) ||
- this.props.subPages.indexOf(this.props.viewingPage) != -1);
- return
- }
-});
-
-var drawerImageStyle = { //@TODO: remove this, img should be properly scaled once size is settled
- height: '36px'
-};
-
-var Drawer = React.createClass({
- _balanceSubscribeId: null,
-
- handleLogoClicked: function(event) {
- if ((event.ctrlKey || event.metaKey) && event.shiftKey) {
- window.location.href = '?developer'
- event.preventDefault();
- }
- },
- getInitialState: function() {
- return {
- balance: 0,
- };
- },
- componentDidMount: function() {
- this._balanceSubscribeId = lbry.balanceSubscribe(function(balance) {
- this.setState({
- balance: balance
- });
- }.bind(this));
- },
- componentWillUnmount: function() {
- if (this._balanceSubscribeId) {
- lbry.balanceUnsubscribe(this._balanceSubscribeId)
- }
- },
- render: function() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-});
-
-
-export default Drawer;
diff --git a/ui/js/component/drawer/index.jsx b/ui/js/component/drawer/index.jsx
new file mode 100644
index 000000000..3c4c524b0
--- /dev/null
+++ b/ui/js/component/drawer/index.jsx
@@ -0,0 +1,33 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import Drawer from './view'
+import {
+ doNavigate,
+ doCloseDrawer,
+ doLogoClick,
+} from 'actions/app'
+import {
+ doUpdateBalance,
+} from 'actions/wallet'
+import {
+ selectCurrentPage,
+} from 'selectors/app'
+import {
+ selectBalance,
+} from 'selectors/wallet'
+
+const select = (state) => ({
+ currentPage: selectCurrentPage(state),
+ balance: selectBalance(state),
+})
+
+const perform = {
+ linkClick: doNavigate,
+ logoClick: doLogoClick,
+ closeDrawerClick: doCloseDrawer,
+ updateBalance: doUpdateBalance,
+}
+
+export default connect(select, perform)(Drawer)
diff --git a/ui/js/component/drawer/view.jsx b/ui/js/component/drawer/view.jsx
new file mode 100644
index 000000000..da65cd523
--- /dev/null
+++ b/ui/js/component/drawer/view.jsx
@@ -0,0 +1,68 @@
+import lbry from 'lbry.js';
+import React from 'react';
+import Link from 'component/link';
+
+const DrawerItem = (props) => {
+ const {
+ currentPage,
+ href,
+ subPages,
+ badge,
+ label,
+ linkClick,
+ icon,
+ } = props
+ const isSelected = (
+ currentPage == href.substr(0) ||
+ (subPages && subPages.indexOf(currentPage) != -1)
+ )
+
+ return linkClick(href)} className={ 'drawer-item ' + (isSelected ? 'drawer-item-selected' : '') } />
+}
+
+var drawerImageStyle = { //@TODO: remove this, img should be properly scaled once size is settled
+ height: '36px'
+};
+
+class Drawer extends React.Component {
+ constructor(props) {
+ super(props)
+ this._balanceSubscribeId = null
+ }
+
+ componentDidMount() {
+ const { updateBalance } = this.props
+
+ this._balanceSubscribeId = lbry.balanceSubscribe((balance) => {
+ updateBalance(balance)
+ });
+ }
+ componentWillUnmount() {
+ if (this._balanceSubscribeId) {
+ lbry.balanceUnsubscribe(this._balanceSubscribeId)
+ }
+ }
+
+ render() {
+ const {
+ closeDrawerClick,
+ logoClick,
+ balance,
+ } = this.props
+
+ return(
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default Drawer;
diff --git a/ui/js/component/errorModal/index.jsx b/ui/js/component/errorModal/index.jsx
new file mode 100644
index 000000000..c7db4cef4
--- /dev/null
+++ b/ui/js/component/errorModal/index.jsx
@@ -0,0 +1,23 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import {
+ selectCurrentModal,
+ selectError,
+} from 'selectors/app'
+import {
+ doCloseModal,
+} from 'actions/app'
+import ErrorModal from './view'
+
+const select = (state) => ({
+ modal: selectCurrentModal(state),
+ error: selectError(state),
+})
+
+const perform = (dispatch) => ({
+ closeModal: () => dispatch(doCloseModal())
+})
+
+export default connect(select, perform)(ErrorModal)
diff --git a/ui/js/component/errorModal/view.jsx b/ui/js/component/errorModal/view.jsx
new file mode 100644
index 000000000..676a2d52b
--- /dev/null
+++ b/ui/js/component/errorModal/view.jsx
@@ -0,0 +1,50 @@
+import React from 'react'
+import lbry from 'lbry'
+import {
+ ExpandableModal
+} from 'component/modal'
+
+class ErrorModal extends React.Component {
+ render() {
+ const {
+ modal,
+ closeModal,
+ error,
+ } = this.props
+
+ const _error_key_labels = {
+ connectionString: 'API connection string',
+ method: 'Method',
+ params: 'Parameters',
+ code: 'Error code',
+ message: 'Error message',
+ data: 'Error data',
+ }
+ const errorInfo =
+ const errorInfoList = []
+ for (let key of Object.keys(error)) {
+ let val = typeof error[key] == 'string' ? error[key] : JSON.stringify(error[key]);
+ let label = this._error_key_labels[key];
+ errorInfoList.push({label} : {val}
);
+ }
+
+ return(
+
+ Error
+
+
+
+
We're sorry that LBRY has encountered an error. This has been reported and we will investigate the problem.
+
+
+ )
+ }
+}
+
+export default ErrorModal
diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js
deleted file mode 100644
index 21d25eb47..000000000
--- a/ui/js/component/file-actions.js
+++ /dev/null
@@ -1,270 +0,0 @@
-import React from 'react';
-import lbry from '../lbry.js';
-import lbryuri from '../lbryuri.js';
-import {Link} from '../component/link.js';
-import {Icon, FilePrice} from '../component/common.js';
-import {Modal} from './modal.js';
-import {FormField} from './form.js';
-import {ToolTip} from '../component/tooltip.js';
-import {DropDownMenu, DropDownMenuItem} from './menu.js';
-
-const {shell} = require('electron');
-
-let FileActionsRow = React.createClass({
- _isMounted: false,
- _fileInfoSubscribeId: null,
-
- propTypes: {
- uri: React.PropTypes.string,
- outpoint: React.PropTypes.string.isRequired,
- metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]),
- contentType: React.PropTypes.string.isRequired,
- },
- getInitialState: function() {
- return {
- fileInfo: null,
- modal: null,
- menuOpen: false,
- deleteChecked: false,
- attemptingDownload: false,
- attemptingRemove: false,
- }
- },
- onFileInfoUpdate: function(fileInfo) {
- if (this._isMounted) {
- this.setState({
- fileInfo: fileInfo ? fileInfo : false,
- attemptingDownload: fileInfo ? false : this.state.attemptingDownload
- });
- }
- },
- tryDownload: function() {
- this.setState({
- attemptingDownload: true,
- attemptingRemove: false
- });
- lbry.getCostInfo(this.props.uri).then(({cost}) => {
- lbry.getBalance((balance) => {
- if (cost > balance) {
- this.setState({
- modal: 'notEnoughCredits',
- attemptingDownload: false,
- });
- } else if (this.state.affirmedPurchase) {
- lbry.get({uri: this.props.uri}).then((streamInfo) => {
- if (streamInfo === null || typeof streamInfo !== 'object') {
- this.setState({
- modal: 'timedOut',
- attemptingDownload: false,
- });
- }
- });
- } else {
- this.setState({
- attemptingDownload: false,
- modal: 'affirmPurchase'
- })
- }
- });
- });
- },
- closeModal: function() {
- this.setState({
- modal: null,
- })
- },
- onDownloadClick: function() {
- if (!this.state.fileInfo && !this.state.attemptingDownload) {
- this.tryDownload();
- }
- },
- onOpenClick: function() {
- if (this.state.fileInfo && this.state.fileInfo.download_path) {
- shell.openItem(this.state.fileInfo.download_path);
- }
- },
- handleDeleteCheckboxClicked: function(event) {
- this.setState({
- deleteChecked: event.target.checked,
- });
- },
- handleRevealClicked: function() {
- if (this.state.fileInfo && this.state.fileInfo.download_path) {
- shell.showItemInFolder(this.state.fileInfo.download_path);
- }
- },
- handleRemoveClicked: function() {
- this.setState({
- modal: 'confirmRemove',
- });
- },
- handleRemoveConfirmed: function() {
- lbry.removeFile(this.props.outpoint, this.state.deleteChecked);
- this.setState({
- modal: null,
- fileInfo: false,
- attemptingDownload: false
- });
- },
- onAffirmPurchase: function() {
- this.setState({
- affirmedPurchase: true,
- modal: null
- });
- this.tryDownload();
- },
- openMenu: function() {
- this.setState({
- menuOpen: !this.state.menuOpen,
- });
- },
- componentDidMount: function() {
- this._isMounted = true;
- this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
- },
- componentWillUnmount: function() {
- this._isMounted = false;
- if (this._fileInfoSubscribeId) {
- lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
- }
- },
- render: function() {
- if (this.state.fileInfo === null)
- {
- return null;
- }
-
- const openInFolderMessage = window.navigator.platform.startsWith('Mac') ? 'Open in Finder' : 'Open in Folder',
- showMenu = !!this.state.fileInfo;
-
- let linkBlock;
- if (this.state.fileInfo === false && !this.state.attemptingDownload) {
- linkBlock = ;
- } else if (this.state.attemptingDownload || (!this.state.fileInfo.completed && !this.state.fileInfo.isMine)) {
- const
- progress = this.state.fileInfo ? this.state.fileInfo.written_bytes / this.state.fileInfo.total_bytes * 100 : 0,
- label = this.state.fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...',
- labelWithIcon = {label} ;
-
- linkBlock = (
-
-
{labelWithIcon}
- {labelWithIcon}
-
- );
- } else {
- linkBlock = ;
- }
-
- const uri = lbryuri.normalize(this.props.uri);
- const title = this.props.metadata ? this.props.metadata.title : uri;
- return (
-
- {this.state.fileInfo !== null || this.state.fileInfo.isMine
- ? linkBlock
- : null}
- { showMenu ?
-
-
-
- : '' }
-
- Are you sure you'd like to buy {title} for credits?
-
-
- You don't have enough LBRY credits to pay for this stream.
-
-
- LBRY was unable to download the stream {uri} .
-
-
- Are you sure you'd like to remove {title} from LBRY?
-
- Delete this file from my computer
-
-
- );
- }
-});
-
-export let FileActions = React.createClass({
- _isMounted: false,
- _fileInfoSubscribeId: null,
-
- propTypes: {
- uri: React.PropTypes.string,
- outpoint: React.PropTypes.string.isRequired,
- metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]),
- contentType: React.PropTypes.string,
- },
- getInitialState: function() {
- return {
- available: true,
- forceShowActions: false,
- fileInfo: null,
- }
- },
- onShowFileActionsRowClicked: function() {
- this.setState({
- forceShowActions: true,
- });
- },
- onFileInfoUpdate: function(fileInfo) {
- if (this.isMounted) {
- this.setState({
- fileInfo: fileInfo,
- });
- }
- },
- componentDidMount: function() {
- this._isMounted = true;
- this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
-
- lbry.get_availability({uri: this.props.uri}, (availability) => {
- if (this._isMounted) {
- this.setState({
- available: availability > 0,
- });
- }
- }, () => {
- // Take any error to mean the file is unavailable
- if (this._isMounted) {
- this.setState({
- available: false,
- });
- }
- });
- },
- componentWillUnmount: function() {
- this._isMounted = false;
- if (this._fileInfoSubscribeId) {
- lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
- }
- },
- render: function() {
- const fileInfo = this.state.fileInfo;
- if (fileInfo === null) {
- return null;
- }
-
- return (
- {
- fileInfo || this.state.available || this.state.forceShowActions
- ?
- :
-
Content unavailable.
-
-
-
- }
- );
- }
-});
diff --git a/ui/js/component/file-tile.js b/ui/js/component/file-tile.js
deleted file mode 100644
index 51413bd02..000000000
--- a/ui/js/component/file-tile.js
+++ /dev/null
@@ -1,276 +0,0 @@
-import React from 'react';
-import lbry from '../lbry.js';
-import lbryuri from '../lbryuri.js';
-import {Link} from '../component/link.js';
-import {FileActions} from '../component/file-actions.js';
-import {Thumbnail, TruncatedText, FilePrice} from '../component/common.js';
-import UriIndicator from '../component/channel-indicator.js';
-
-/*should be merged into FileTile once FileTile is refactored to take a single id*/
-export let FileTileStream = React.createClass({
- _fileInfoSubscribeId: null,
- _isMounted: null,
-
- propTypes: {
- uri: React.PropTypes.string,
- metadata: React.PropTypes.object,
- contentType: React.PropTypes.string.isRequired,
- outpoint: React.PropTypes.string,
- hasSignature: React.PropTypes.bool,
- signatureIsValid: React.PropTypes.bool,
- hideOnRemove: React.PropTypes.bool,
- hidePrice: React.PropTypes.bool,
- obscureNsfw: React.PropTypes.bool
- },
- getInitialState: function() {
- return {
- showNsfwHelp: false,
- isHidden: false,
- }
- },
- getDefaultProps: function() {
- return {
- obscureNsfw: !lbry.getClientSetting('showNsfw'),
- hidePrice: false,
- hasSignature: false,
- }
- },
- componentDidMount: function() {
- this._isMounted = true;
- if (this.props.hideOnRemove) {
- this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
- }
- },
- componentWillUnmount: function() {
- if (this._fileInfoSubscribeId) {
- lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
- }
- },
- onFileInfoUpdate: function(fileInfo) {
- if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
- this.setState({
- isHidden: true
- });
- }
- },
- handleMouseOver: function() {
- if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
- this.setState({
- showNsfwHelp: true,
- });
- }
- },
- handleMouseOut: function() {
- if (this.state.showNsfwHelp) {
- this.setState({
- showNsfwHelp: false,
- });
- }
- },
- render: function() {
- if (this.state.isHidden) {
- return null;
- }
-
- const uri = lbryuri.normalize(this.props.uri);
- const metadata = this.props.metadata;
- const isConfirmed = !!metadata;
- const title = isConfirmed ? metadata.title : uri;
- const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
- return (
-
-
-
-
-
- { !this.props.hidePrice
- ?
- : null}
-
-
-
-
-
-
-
-
-
- {isConfirmed
- ? metadata.description
- : This file is pending confirmation. }
-
-
-
-
-
- {this.state.showNsfwHelp
- ?
-
- This content is Not Safe For Work.
- To view adult content, please change your .
-
-
- : null}
-
- );
- }
-});
-
-export let FileCardStream = React.createClass({
- _fileInfoSubscribeId: null,
- _isMounted: null,
- _metadata: null,
-
-
- propTypes: {
- uri: React.PropTypes.string,
- claimInfo: React.PropTypes.object,
- outpoint: React.PropTypes.string,
- hideOnRemove: React.PropTypes.bool,
- hidePrice: React.PropTypes.bool,
- obscureNsfw: React.PropTypes.bool
- },
- getInitialState: function() {
- return {
- showNsfwHelp: false,
- isHidden: false,
- }
- },
- getDefaultProps: function() {
- return {
- obscureNsfw: !lbry.getClientSetting('showNsfw'),
- hidePrice: false,
- hasSignature: false,
- }
- },
- componentDidMount: function() {
- this._isMounted = true;
- if (this.props.hideOnRemove) {
- this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
- }
- },
- componentWillUnmount: function() {
- if (this._fileInfoSubscribeId) {
- lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
- }
- },
- onFileInfoUpdate: function(fileInfo) {
- if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
- this.setState({
- isHidden: true
- });
- }
- },
- handleMouseOver: function() {
- this.setState({
- hovered: true,
- });
- },
- handleMouseOut: function() {
- this.setState({
- hovered: false,
- });
- },
- render: function() {
- if (this.state.isHidden) {
- return null;
- }
-
- const uri = lbryuri.normalize(this.props.uri);
- const metadata = this.props.metadata;
- const isConfirmed = !!metadata;
- const title = isConfirmed ? metadata.title : uri;
- const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
- const primaryUrl = '?show=' + uri;
- return (
-
- );
- }
-});
-
-export let FileTile = React.createClass({
- _isMounted: false,
-
- propTypes: {
- uri: React.PropTypes.string.isRequired,
- },
-
- getInitialState: function() {
- return {
- outpoint: null,
- claimInfo: null
- }
- },
-
- componentDidMount: function() {
- this._isMounted = true;
-
- lbry.resolve({uri: this.props.uri}).then((resolutionInfo) => {
- if (this._isMounted && resolutionInfo && resolutionInfo.claim && resolutionInfo.claim.value &&
- resolutionInfo.claim.value.stream && resolutionInfo.claim.value.stream.metadata) {
- // In case of a failed lookup, metadata will be null, in which case the component will never display
- this.setState({
- claimInfo: resolutionInfo.claim,
- });
- }
- });
- },
- componentWillUnmount: function() {
- this._isMounted = false;
- },
- render: function() {
- if (!this.state.claimInfo) {
- if (this.props.displayStyle == 'card') {
- return
- }
- return null;
- }
-
- const {txid, nout, has_signature, signature_is_valid,
- value: {stream: {metadata, source: {contentType}}}} = this.state.claimInfo;
-
- return this.props.displayStyle == 'card' ?
- :
- ;
- }
-});
diff --git a/ui/js/component/fileActions/index.js b/ui/js/component/fileActions/index.js
new file mode 100644
index 000000000..21a2c05d0
--- /dev/null
+++ b/ui/js/component/fileActions/index.js
@@ -0,0 +1,68 @@
+import React from 'react'
+import {
+ connect,
+} from 'react-redux'
+import {
+ selectObscureNsfw,
+ selectHidePrice,
+ selectHasSignature,
+ selectPlatform,
+} from 'selectors/app'
+import {
+ makeSelectFileInfoForUri,
+ makeSelectDownloadingForUri,
+ makeSelectLoadingForUri,
+} from 'selectors/file_info'
+import {
+ makeSelectAvailabilityForUri,
+} from 'selectors/availability'
+import {
+ selectCurrentModal,
+} from 'selectors/app'
+import {
+ doCloseModal,
+ doOpenModal,
+} from 'actions/app'
+import {
+ doOpenFileInShell,
+ doOpenFileInFolder,
+ doDeleteFile,
+} from 'actions/file_info'
+import {
+ doWatchVideo,
+ doLoadVideo,
+} from 'actions/content'
+import FileActions from './view'
+
+const makeSelect = () => {
+ const selectFileInfoForUri = makeSelectFileInfoForUri()
+ const selectAvailabilityForUri = makeSelectAvailabilityForUri()
+ const selectDownloadingForUri = makeSelectDownloadingForUri()
+ const selectLoadingForUri = makeSelectLoadingForUri()
+
+ const select = (state, props) => ({
+ obscureNsfw: selectObscureNsfw(state),
+ hidePrice: selectHidePrice(state),
+ hasSignature: selectHasSignature(state),
+ fileInfo: selectFileInfoForUri(state, props),
+ availability: selectAvailabilityForUri(state, props),
+ platform: selectPlatform(state),
+ modal: selectCurrentModal(state),
+ downloading: selectDownloadingForUri(state, props),
+ loading: selectLoadingForUri(state, props),
+ })
+
+ return select
+}
+
+const perform = (dispatch) => ({
+ closeModal: () => dispatch(doCloseModal()),
+ openInFolder: (fileInfo) => dispatch(doOpenFileInFolder(fileInfo)),
+ openInShell: (fileInfo) => dispatch(doOpenFileInShell(fileInfo)),
+ deleteFile: (fileInfo, deleteFromComputer) => dispatch(doDeleteFile(fileInfo, deleteFromComputer)),
+ openModal: (modal) => dispatch(doOpenModal(modal)),
+ downloadClick: () => dispatch(doWatchVideo()),
+ loadVideo: () => dispatch(doLoadVideo())
+})
+
+export default connect(makeSelect, perform)(FileActions)
diff --git a/ui/js/component/fileActions/view.jsx b/ui/js/component/fileActions/view.jsx
new file mode 100644
index 000000000..5fe6a2dea
--- /dev/null
+++ b/ui/js/component/fileActions/view.jsx
@@ -0,0 +1,345 @@
+import React from 'react';
+import lbry from 'lbry';
+import lbryuri from 'lbryuri';
+import {Icon,} 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 FileActionsRow extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ deleteChecked: false,
+ }
+ }
+
+ handleDeleteCheckboxClicked(event) {
+ this.setState({
+ deleteChecked: event.target.checked,
+ })
+ }
+
+ onAffirmPurchase() {
+ this.props.closeModal()
+ this.props.loadVideo()
+ }
+
+ render() {
+ const {
+ fileInfo,
+ platform,
+ downloading,
+ loading,
+ uri,
+ deleteFile,
+ openInFolder,
+ openInShell,
+ modal,
+ openModal,
+ affirmPurchase,
+ closeModal,
+ downloadClick,
+ } = this.props
+
+ const {
+ deleteChecked,
+ } = this.state
+
+ const metadata = fileInfo ? fileInfo.metadata : null
+
+ if (!fileInfo)
+ {
+ return null;
+ }
+
+ const openInFolderMessage = platform.startsWith('Mac') ? 'Open in Finder' : 'Open in Folder',
+ showMenu = Object.keys(fileInfo).length != 0;
+
+ let linkBlock;
+ if (Object.keys(fileInfo).length == 0 && !downloading && !loading) {
+ linkBlock = ;
+ } else if (downloading || loading) {
+ const
+ progress = (fileInfo && fileInfo.written_bytes) ? fileInfo.written_bytes / fileInfo.total_bytes * 100 : 0,
+ label = fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...',
+ labelWithIcon = {label} ;
+
+ linkBlock = (
+
+
{labelWithIcon}
+ {labelWithIcon}
+
+ );
+ } else {
+ linkBlock = openInShell(fileInfo)} />;
+ }
+
+ const title = metadata ? metadata.title : uri;
+ return (
+
+ {fileInfo !== null || fileInfo.isMine
+ ? linkBlock
+ : null}
+ { showMenu ?
+
+ openInFolder(fileInfo)} label={openInFolderMessage} />
+ openModal('confirmRemove')} label="Remove..." />
+ : '' }
+
+ Are you sure you'd like to buy {title} for credits?
+
+
+ You don't have enough LBRY credits to pay for this stream.
+
+
+ LBRY was unable to download the stream {uri} .
+
+
deleteFile(uri, fileInfo, deleteChecked)}
+ onAborted={closeModal}>
+ Are you sure you'd like to remove {title} from LBRY?
+
+ Delete this file from my computer
+
+
+ );
+ }
+}
+
+// const FileActionsRow = React.createClass({
+// _isMounted: false,
+// _fileInfoSubscribeId: null,
+
+// propTypes: {
+// uri: React.PropTypes.string,
+// outpoint: React.PropTypes.string.isRequired,
+// metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]),
+// contentType: React.PropTypes.string.isRequired,
+// },
+// getInitialState: function() {
+// return {
+// fileInfo: null,
+// modal: null,
+// menuOpen: false,
+// deleteChecked: false,
+// attemptingDownload: false,
+// attemptingRemove: false,
+// }
+// },
+// onFileInfoUpdate: function(fileInfo) {
+// if (this._isMounted) {
+// this.setState({
+// fileInfo: fileInfo ? fileInfo : false,
+// attemptingDownload: fileInfo ? false : this.state.attemptingDownload
+// });
+// }
+// },
+// tryDownload: function() {
+// this.setState({
+// attemptingDownload: true,
+// attemptingRemove: false
+// });
+// lbry.getCostInfo(this.props.uri).then(({cost}) => {
+// lbry.getBalance((balance) => {
+// if (cost > balance) {
+// this.setState({
+// modal: 'notEnoughCredits',
+// attemptingDownload: false,
+// });
+// } else if (this.state.affirmedPurchase) {
+// lbry.get({uri: this.props.uri}).then((streamInfo) => {
+// if (streamInfo === null || typeof streamInfo !== 'object') {
+// this.setState({
+// modal: 'timedOut',
+// attemptingDownload: false,
+// });
+// }
+// });
+// } else {
+// this.setState({
+// attemptingDownload: false,
+// modal: 'affirmPurchase'
+// })
+// }
+// });
+// });
+// },
+// closeModal: function() {
+// this.setState({
+// modal: null,
+// })
+// },
+// onDownloadClick: function() {
+// if (!this.state.fileInfo && !this.state.attemptingDownload) {
+// this.tryDownload();
+// }
+// },
+// onOpenClick: function() {
+// if (this.state.fileInfo && this.state.fileInfo.download_path) {
+// shell.openItem(this.state.fileInfo.download_path);
+// }
+// },
+// handleDeleteCheckboxClicked: function(event) {
+// this.setState({
+// deleteChecked: event.target.checked,
+// });
+// },
+// handleRevealClicked: function() {
+// if (this.state.fileInfo && this.state.fileInfo.download_path) {
+// shell.showItemInFolder(this.state.fileInfo.download_path);
+// }
+// },
+// handleRemoveClicked: function() {
+// this.setState({
+// modal: 'confirmRemove',
+// });
+// },
+// handleRemoveConfirmed: function() {
+// lbry.removeFile(this.props.outpoint, this.state.deleteChecked);
+// this.setState({
+// modal: null,
+// fileInfo: false,
+// attemptingDownload: false
+// });
+// },
+// onAffirmPurchase: function() {
+// this.setState({
+// affirmedPurchase: true,
+// modal: null
+// });
+// this.tryDownload();
+// },
+// openMenu: function() {
+// this.setState({
+// menuOpen: !this.state.menuOpen,
+// });
+// },
+// componentDidMount: function() {
+// this._isMounted = true;
+// this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
+// },
+// componentWillUnmount: function() {
+// this._isMounted = false;
+// if (this._fileInfoSubscribeId) {
+// lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
+// }
+// },
+// render: function() {
+// if (this.state.fileInfo === null)
+// {
+// return null;
+// }
+
+// const openInFolderMessage = window.navigator.platform.startsWith('Mac') ? 'Open in Finder' : 'Open in Folder',
+// showMenu = !!this.state.fileInfo;
+
+// let linkBlock;
+// if (this.state.fileInfo === false && !this.state.attemptingDownload) {
+// linkBlock = ;
+// } else if (this.state.attemptingDownload || (!this.state.fileInfo.completed && !this.state.fileInfo.isMine)) {
+// const
+// progress = this.state.fileInfo ? this.state.fileInfo.written_bytes / this.state.fileInfo.total_bytes * 100 : 0,
+// label = this.state.fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...',
+// labelWithIcon = {label} ;
+
+// linkBlock = (
+//
+//
{labelWithIcon}
+// {labelWithIcon}
+//
+// );
+// } else {
+// linkBlock = ;
+// }
+
+// const uri = lbryuri.normalize(this.props.uri);
+// const title = this.props.metadata ? this.props.metadata.title : uri;
+// return (
+//
+// {this.state.fileInfo !== null || this.state.fileInfo.isMine
+// ? linkBlock
+// : null}
+// { showMenu ?
+//
+//
+//
+// : '' }
+//
+// Are you sure you'd like to buy {title} for credits?
+//
+//
+// You don't have enough LBRY credits to pay for this stream.
+//
+//
+// LBRY was unable to download the stream {uri} .
+//
+//
+// Are you sure you'd like to remove {title} from LBRY?
+
+// Delete this file from my computer
+//
+//
+// );
+// }
+// });
+
+class FileActions extends React.Component {
+ constructor(props) {
+ super(props)
+ this._isMounted = false
+ this._fileInfoSubscribeId = null
+ this.state = {
+ available: true,
+ forceShowActions: false,
+ fileInfo: null,
+ }
+ }
+
+ onShowFileActionsRowClicked() {
+ this.setState({
+ forceShowActions: true,
+ });
+ }
+
+ render() {
+ const {
+ fileInfo,
+ availability,
+ } = this.props
+
+ if (fileInfo === null) {
+ return null;
+ }
+
+ return (
+ {
+ fileInfo || this.state.available || this.state.forceShowActions
+ ?
+ :
+
Content unavailable.
+
+
+
+ }
+ );
+ }
+}
+
+export default FileActions
diff --git a/ui/js/component/fileCardStream/index.js b/ui/js/component/fileCardStream/index.js
new file mode 100644
index 000000000..14e3dc7f9
--- /dev/null
+++ b/ui/js/component/fileCardStream/index.js
@@ -0,0 +1,44 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import {
+ doNavigate,
+} from 'actions/app'
+import {
+ selectHidePrice,
+ selectObscureNsfw,
+} from 'selectors/app'
+import {
+ makeSelectClaimForUri,
+ makeSelectSourceForUri,
+ makeSelectMetadataForUri,
+} from 'selectors/claims'
+import {
+ makeSelectFileInfoForUri,
+} from 'selectors/file_info'
+import FileCardStream from './view'
+
+const makeSelect = () => {
+ const selectClaimForUri = makeSelectClaimForUri()
+ const selectFileInfoForUri = makeSelectFileInfoForUri()
+ const selectMetadataForUri = makeSelectMetadataForUri()
+ const selectSourceForUri = makeSelectSourceForUri()
+ const select = (state, props) => ({
+ claim: selectClaimForUri(state, props),
+ fileInfo: selectFileInfoForUri(state, props),
+ hidePrice: selectHidePrice(state),
+ obscureNsfw: selectObscureNsfw(state),
+ hasSignature: false,
+ metadata: selectMetadataForUri(state, props),
+ source: selectSourceForUri(state, props),
+ })
+
+ return select
+}
+
+const perform = (dispatch) => ({
+ navigate: (path) => dispatch(doNavigate(path)),
+})
+
+export default connect(makeSelect, perform)(FileCardStream)
diff --git a/ui/js/component/fileCardStream/view.jsx b/ui/js/component/fileCardStream/view.jsx
new file mode 100644
index 000000000..c3954d9b7
--- /dev/null
+++ b/ui/js/component/fileCardStream/view.jsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import lbry from 'lbry.js';
+import lbryuri from 'lbryuri.js';
+import Link from 'component/link';
+import {Thumbnail, TruncatedText,} from 'component/common';
+import FilePrice from 'component/filePrice'
+import UriIndicator from 'component/channel-indicator';
+
+class FileCardStream extends React.Component {
+ constructor(props) {
+ super(props)
+ this._fileInfoSubscribeId = null
+ this._isMounted = null
+ this._metadata = null
+ this.state = {
+ showNsfwHelp: false,
+ isHidden: false,
+ }
+ }
+
+ componentDidMount() {
+ this._isMounted = true;
+ if (this.props.hideOnRemove) {
+ this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this._fileInfoSubscribeId) {
+ lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
+ }
+ }
+
+ onFileInfoUpdate(fileInfo) {
+ if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
+ this.setState({
+ isHidden: true
+ });
+ }
+ }
+
+ handleMouseOver() {
+ this.setState({
+ hovered: true,
+ });
+ }
+
+ handleMouseOut() {
+ this.setState({
+ hovered: false,
+ });
+ }
+
+ render() {
+ if (this.state.isHidden) {
+ return null;
+ }
+
+ // if (!this.props.metadata) {
+ // return null
+ // }
+
+ const uri = lbryuri.normalize(this.props.uri);
+ const metadata = this.props.metadata;
+ const isConfirmed = !!metadata;
+ const title = isConfirmed ? metadata.title : uri;
+ const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
+ const primaryUrl = 'show=' + uri;
+ return (
+
+ );
+ }
+}
+
+export default FileCardStream
diff --git a/ui/js/component/fileList/index.js b/ui/js/component/fileList/index.js
new file mode 100644
index 000000000..dd0209b40
--- /dev/null
+++ b/ui/js/component/fileList/index.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import FileList from './view'
+
+const select = (state) => ({
+})
+
+const perform = (dispatch) => ({
+})
+
+export default connect(select, perform)(FileList)
diff --git a/ui/js/component/fileList/view.jsx b/ui/js/component/fileList/view.jsx
new file mode 100644
index 000000000..2a946746c
--- /dev/null
+++ b/ui/js/component/fileList/view.jsx
@@ -0,0 +1,184 @@
+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 FileTileStream from 'component/fileTileStream';
+import rewards from 'rewards.js';
+import lbryio from 'lbryio.js';
+import {BusyMessage, Thumbnail} from 'component/common.js';
+
+class FileList extends React.Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ sortBy: 'date',
+ }
+
+ this._sortFunctions = {
+ date: function(fileInfos) {
+ return fileInfos.slice().reverse();
+ },
+ title: function(fileInfos) {
+ return fileInfos.slice().sort(function(fileInfo1, fileInfo2) {
+ const title1 = fileInfo1.metadata ? fileInfo1.metadata.stream.metadata.title.toLowerCase() : fileInfo1.name;
+ const title2 = fileInfo2.metadata ? fileInfo2.metadata.stream.metadata.title.toLowerCase() : fileInfo2.name;
+ if (title1 < title2) {
+ return -1;
+ } else if (title1 > title2) {
+ return 1;
+ } else {
+ return 0;
+ }
+ })
+ },
+ filename: function(fileInfos) {
+ return fileInfos.slice().sort(function({file_name: fileName1}, {file_name: fileName2}) {
+ const fileName1Lower = fileName1.toLowerCase();
+ const fileName2Lower = fileName2.toLowerCase();
+ if (fileName1Lower < fileName2Lower) {
+ return -1;
+ } else if (fileName2Lower > fileName1Lower) {
+ return 1;
+ } else {
+ return 0;
+ }
+ })
+ },
+ }
+ }
+
+ handleSortChanged(event) {
+ this.setState({
+ sortBy: event.target.value,
+ })
+ }
+
+ render() {
+ const {
+ handleSortChanged,
+ fileInfos,
+ hidePrices,
+ } = 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( )
+ })
+ return (
+
+
+ Sort by { ' ' }
+
+ Date
+ Title
+ File name
+
+
+ {content}
+
+ )
+ }
+}
+
+// const FileList = React.createClass({
+// _sortFunctions: {
+// date: function(fileInfos) {
+// return fileInfos.slice().reverse();
+// },
+// title: function(fileInfos) {
+// return fileInfos.slice().sort(function(fileInfo1, fileInfo2) {
+// const title1 = fileInfo1.metadata ? fileInfo1.metadata.title.toLowerCase() : fileInfo1.name;
+// const title2 = fileInfo2.metadata ? fileInfo2.metadata.title.toLowerCase() : fileInfo2.name;
+// if (title1 < title2) {
+// return -1;
+// } else if (title1 > title2) {
+// return 1;
+// } else {
+// return 0;
+// }
+// });
+// },
+// filename: function(fileInfos) {
+// return fileInfos.slice().sort(function({file_name: fileName1}, {file_name: fileName2}) {
+// const fileName1Lower = fileName1.toLowerCase();
+// const fileName2Lower = fileName2.toLowerCase();
+// if (fileName1Lower < fileName2Lower) {
+// return -1;
+// } else if (fileName2Lower > fileName1Lower) {
+// return 1;
+// } else {
+// return 0;
+// }
+// });
+// },
+// },
+// propTypes: {
+// fileInfos: React.PropTypes.array.isRequired,
+// hidePrices: React.PropTypes.bool,
+// },
+// getDefaultProps: function() {
+// return {
+// hidePrices: false,
+// };
+// },
+// getInitialState: function() {
+// return {
+// sortBy: 'date',
+// };
+// },
+// handleSortChanged: function(event) {
+// this.setState({
+// sortBy: event.target.value,
+// });
+// },
+// render: function() {
+// var content = [],
+// seenUris = {};
+
+// console.debug(this.props)
+
+// const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos);
+// for (let {outpoint, name, channel_name, metadata, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) {
+// if (seenUris[name] || !claim_id) {
+// continue;
+// }
+
+// let streamMetadata;
+// if (metadata) {
+// streamMetadata = metadata.stream.metadata;
+// } else {
+// streamMetadata = null;
+// }
+
+
+// const uri = lbryuri.build({contentName: name, channelName: channel_name});
+// seenUris[name] = true;
+// content.push( )
+// }
+
+// return (
+//
+//
+// Sort by { ' ' }
+//
+// Date
+// Title
+// File name
+//
+//
+// {content}
+//
+// );
+// }
+// });
+
+export default FileList
diff --git a/ui/js/component/filePrice/index.js b/ui/js/component/filePrice/index.js
new file mode 100644
index 000000000..c5a9497d0
--- /dev/null
+++ b/ui/js/component/filePrice/index.js
@@ -0,0 +1,22 @@
+import React from 'react'
+import {
+ connect,
+} from 'react-redux'
+import {
+ makeSelectCostInfoForUri,
+} from 'selectors/cost_info'
+import FilePrice from './view'
+
+const makeSelect = () => {
+ const selectCostInfoForUri = makeSelectCostInfoForUri()
+ const select = (state, props) => ({
+ costInfo: selectCostInfoForUri(state, props),
+ })
+
+ return select
+}
+
+const perform = (dispatch) => ({
+})
+
+export default connect(makeSelect, perform)(FilePrice)
diff --git a/ui/js/component/filePrice/view.jsx b/ui/js/component/filePrice/view.jsx
new file mode 100644
index 000000000..17b830bf2
--- /dev/null
+++ b/ui/js/component/filePrice/view.jsx
@@ -0,0 +1,21 @@
+import React from 'react'
+import {
+ CreditAmount,
+} from 'component/common'
+
+const FilePrice = (props) => {
+ const {
+ costInfo,
+ look = 'indicator',
+ } = props
+
+ const isEstimate = costInfo ? !costInfo.includesData : null
+
+ if (!costInfo) {
+ return ??? ;
+ }
+
+ return
+}
+
+export default FilePrice
diff --git a/ui/js/component/fileTile/index.js b/ui/js/component/fileTile/index.js
new file mode 100644
index 000000000..5703e28cd
--- /dev/null
+++ b/ui/js/component/fileTile/index.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import {
+ makeSelectClaimForUri,
+} from 'selectors/claims'
+import {
+ makeSelectFileInfoForUri,
+} from 'selectors/file_info'
+import FileTile from './view'
+
+const makeSelect = () => {
+ const selectClaimForUri = makeSelectClaimForUri()
+ const selectFileInfoForUri = makeSelectFileInfoForUri()
+ const select = (state, props) => ({
+ claim: selectClaimForUri(state, props),
+ fileInfo: selectFileInfoForUri(state, props),
+ })
+
+ return select
+}
+
+const perform = (dispatch) => ({
+})
+
+export default connect(makeSelect, perform)(FileTile)
diff --git a/ui/js/component/fileTile/view.jsx b/ui/js/component/fileTile/view.jsx
new file mode 100644
index 000000000..16853b852
--- /dev/null
+++ b/ui/js/component/fileTile/view.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import lbry from 'lbry.js';
+import lbryuri from 'lbryuri.js';
+import Link from 'component/link';
+import FileCardStream from 'component/fileCardStream'
+import FileTileStream from 'component/fileTileStream'
+import FileActions from 'component/fileActions';
+
+class FileTile extends React.Component {
+ render() {
+ const {
+ displayStyle,
+ uri,
+ claim,
+ } = this.props
+
+ if(!claim) {
+ if (displayStyle == 'card') {
+ return
+ }
+ return null
+ }
+
+ return displayStyle == 'card' ?
+
+ :
+
+ }
+}
+
+export default FileTile
diff --git a/ui/js/component/fileTileStream/index.js b/ui/js/component/fileTileStream/index.js
new file mode 100644
index 000000000..d212dfdae
--- /dev/null
+++ b/ui/js/component/fileTileStream/index.js
@@ -0,0 +1,50 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import {
+ doNavigate,
+} from 'actions/app'
+import {
+ makeSelectClaimForUri,
+ makeSelectSourceForUri,
+ makeSelectMetadataForUri,
+} from 'selectors/claims'
+import {
+ makeSelectFileInfoForUri,
+} from 'selectors/file_info'
+import {
+ makeSelectFetchingAvailabilityForUri,
+ makeSelectAvailabilityForUri,
+} from 'selectors/availability'
+import {
+ selectObscureNsfw,
+} from 'selectors/app'
+import FileTileStream from './view'
+
+const makeSelect = () => {
+ const selectClaimForUri = makeSelectClaimForUri()
+ const selectFileInfoForUri = makeSelectFileInfoForUri()
+ const selectFetchingAvailabilityForUri = makeSelectFetchingAvailabilityForUri()
+ const selectAvailabilityForUri = makeSelectAvailabilityForUri()
+ const selectMetadataForUri = makeSelectMetadataForUri()
+ const selectSourceForUri = makeSelectSourceForUri()
+
+ const select = (state, props) => ({
+ claim: selectClaimForUri(state, props),
+ fileInfo: selectFileInfoForUri(state, props),
+ fetchingAvailability: selectFetchingAvailabilityForUri(state, props),
+ selectAvailabilityForUri: selectAvailabilityForUri(state, props),
+ obscureNsfw: selectObscureNsfw(state),
+ metadata: selectMetadataForUri(state, props),
+ source: selectSourceForUri(state, props),
+ })
+
+ return select
+}
+
+const perform = (dispatch) => ({
+ navigate: (path) => dispatch(doNavigate(path))
+})
+
+export default connect(makeSelect, perform)(FileTileStream)
diff --git a/ui/js/component/fileTileStream/view.jsx b/ui/js/component/fileTileStream/view.jsx
new file mode 100644
index 000000000..2130df284
--- /dev/null
+++ b/ui/js/component/fileTileStream/view.jsx
@@ -0,0 +1,126 @@
+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/channel-indicator.js';
+
+/*should be merged into FileTile once FileTile is refactored to take a single id*/
+class FileTileStream extends React.Component {
+ constructor(props) {
+ super(props)
+ this._fileInfoSubscribeId = null
+ this._isMounted = null
+ this.state = {
+ showNsfwHelp: false,
+ isHidden: false,
+ }
+ }
+
+ componentDidMount() {
+ this._isMounted = true;
+ if (this.props.hideOnRemove) {
+ this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this._fileInfoSubscribeId) {
+ lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
+ }
+ }
+
+ onFileInfoUpdate(fileInfo) {
+ if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
+ this.setState({
+ isHidden: true
+ });
+ }
+ }
+
+ handleMouseOver() {
+ if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
+ this.setState({
+ showNsfwHelp: true,
+ });
+ }
+ }
+
+ handleMouseOut() {
+ if (this.state.showNsfwHelp) {
+ this.setState({
+ showNsfwHelp: false,
+ });
+ }
+ }
+
+ render() {
+ if (this.state.isHidden) {
+ return null;
+ }
+
+ const {
+ metadata,
+ navigate,
+ } = this.props
+
+ const uri = lbryuri.normalize(this.props.uri);
+ const isConfirmed = !!metadata;
+ const title = isConfirmed ? metadata.title : uri;
+ const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
+
+ return (
+
+
+
+
+
+ { !this.props.hidePrice
+ ?
+ : null}
+
+
+
+
+
+
+
+
+ {isConfirmed
+ ? metadata.description
+ : This file is pending confirmation. }
+
+
+
+
+
+ {this.state.showNsfwHelp
+ ?
+
+ This content is Not Safe For Work.
+ To view adult content, please change your navigate('settings')} label="Settings" />.
+
+
+ : null}
+
+ );
+ }
+}
+
+export default FileTileStream
diff --git a/ui/js/component/header/index.js b/ui/js/component/header/index.js
new file mode 100644
index 000000000..44efdc355
--- /dev/null
+++ b/ui/js/component/header/index.js
@@ -0,0 +1,33 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import {
+ selectCurrentPage,
+ selectHeaderLinks,
+ selectPageTitle,
+} from 'selectors/app'
+import {
+ doNavigate,
+} from 'actions/app'
+import {
+ doSearchContent,
+ doActivateSearch,
+ doDeactivateSearch,
+} from 'actions/search'
+import Header from './view'
+
+const select = (state) => ({
+ currentPage: selectCurrentPage(state),
+ subLinks: selectHeaderLinks(state),
+ pageTitle: selectPageTitle(state),
+})
+
+const perform = (dispatch) => ({
+ navigate: (path) => dispatch(doNavigate(path)),
+ search: (query) => dispatch(doSearchContent(query)),
+ activateSearch: () => dispatch(doActivateSearch()),
+ deactivateSearch: () => setTimeout(() => { dispatch(doDeactivateSearch()) }, 50),
+})
+
+export default connect(select, perform)(Header)
diff --git a/ui/js/component/header.js b/ui/js/component/header/view.jsx
similarity index 66%
rename from ui/js/component/header.js
rename to ui/js/component/header/view.jsx
index 463042cff..19fce75c3 100644
--- a/ui/js/component/header.js
+++ b/ui/js/component/header/view.jsx
@@ -1,6 +1,6 @@
import React from 'react';
-import {Link} from './link.js';
-import {Icon} from './common.js';
+import {Icon} from 'component/common.js';
+import Link from 'component/link';
var Header = React.createClass({
getInitialState: function() {
@@ -42,7 +42,7 @@ var Header = React.createClass({
//@TODO: Switch to React.js timing
var searchTerm = event.target.value;
this.userTypingTimer = setTimeout(() => {
- this.props.onSearch(searchTerm);
+ this.props.search(searchTerm);
}, 800); // 800ms delay, tweak for faster/slower
},
@@ -51,16 +51,18 @@ var Header = React.createClass({
@@ -68,24 +70,28 @@ var Header = React.createClass({
}
});
-var SubHeader = React.createClass({
- render: function() {
- var links = [],
- viewingUrl = '?' + this.props.viewingPage;
+const SubHeader = (props) => {
+ const {
+ subLinks,
+ currentPage,
+ navigate,
+ } = props
- for (let link of Object.keys(this.props.links)) {
- links.push(
-
- {this.props.links[link]}
-
- );
- }
- return (
-
- {links}
-
- );
+ const links = []
+
+ for(let link of Object.keys(subLinks)) {
+ links.push(
+ navigate(link)} key={link} className={link == currentPage ? 'sub-header-selected' : 'sub-header-unselected' }>
+ {subLinks[link]}
+
+ )
}
-});
+
+ return (
+
+ {links}
+
+ )
+}
export default Header;
diff --git a/ui/js/component/link/index.jsx b/ui/js/component/link/index.jsx
new file mode 100644
index 000000000..2ad86dc05
--- /dev/null
+++ b/ui/js/component/link/index.jsx
@@ -0,0 +1,7 @@
+import React from 'react'
+import {
+ connect,
+} from 'react-redux'
+import Link from './view'
+
+export default connect(null, null)(Link)
diff --git a/ui/js/component/link/view.js b/ui/js/component/link/view.js
new file mode 100644
index 000000000..6036d9a95
--- /dev/null
+++ b/ui/js/component/link/view.js
@@ -0,0 +1,99 @@
+import React from 'react';
+import {Icon} from 'component/common.js';
+
+const Link = (props) => {
+ const {
+ href,
+ title,
+ onClick,
+ style,
+ label,
+ icon,
+ badge,
+ button,
+ hidden,
+ 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 (props.children) {
+ content = this.props.children
+ } else {
+ content = (
+
+ {'icon' in props ? : null}
+ {{label} }
+ { badge ? {badge} : null}
+
+ )
+ }
+
+ return (
+
+ {content}
+
+ );
+}
+
+export default Link
+
+// export let Link = React.createClass({
+// propTypes: {
+// label: React.PropTypes.string,
+// icon: React.PropTypes.string,
+// button: React.PropTypes.string,
+// badge: React.PropTypes.string,
+// hidden: React.PropTypes.bool,
+// },
+// getDefaultProps: function() {
+// return {
+// hidden: false,
+// disabled: false,
+// };
+// },
+// handleClick: function(e) {
+// if (this.props.onClick) {
+// this.props.onClick(e);
+// }
+// },
+// render: function() {
+// if (this.props.hidden) {
+// return null;
+// }
+
+// /* The way the class name is generated here is a mess -- refactor */
+
+// const className = (this.props.className || '') +
+// (!this.props.className && !this.props.button ? 'button-text' : '') + // Non-button links get the same look as text buttons
+// (this.props.button ? ' button-block button-' + this.props.button + ' button-set-item' : '') +
+// (this.props.disabled ? ' disabled' : '');
+
+// let content;
+// if (this.props.children) { // Custom content
+// content = this.props.children;
+// } else {
+// content = (
+//
+// {'icon' in this.props ? : null}
+// {{this.props.label} }
+// {'badge' in this.props ? {this.props.badge} : null}
+//
+// );
+// }
+
+// return (
+//
+// {content}
+//
+// );
+// }
+// });
diff --git a/ui/js/component/load_screen.js b/ui/js/component/load_screen.js
index ad013030c..5c455a412 100644
--- a/ui/js/component/load_screen.js
+++ b/ui/js/component/load_screen.js
@@ -1,7 +1,7 @@
import React from 'react';
import lbry from '../lbry.js';
import {BusyMessage, Icon} from './common.js';
-import {Link} from '../component/link.js'
+import Link from 'component/link'
var LoadScreen = React.createClass({
propTypes: {
diff --git a/ui/js/component/menu.js b/ui/js/component/menu.js
index c35952426..c43d81f0e 100644
--- a/ui/js/component/menu.js
+++ b/ui/js/component/menu.js
@@ -1,6 +1,6 @@
import React from 'react';
import {Icon} from './common.js';
-import {Link} from '../component/link.js';
+import Link from 'component/link';
export let DropDownMenuItem = React.createClass({
propTypes: {
diff --git a/ui/js/component/modal.js b/ui/js/component/modal.js
index dbb8ff646..9dfedba6b 100644
--- a/ui/js/component/modal.js
+++ b/ui/js/component/modal.js
@@ -1,6 +1,6 @@
import React from 'react';
import ReactModal from 'react-modal';
-import {Link} from './link.js';
+import Link from 'component/link';
export const Modal = React.createClass({
diff --git a/ui/js/component/link.js b/ui/js/component/reward-link.js
similarity index 56%
rename from ui/js/component/link.js
rename to ui/js/component/reward-link.js
index 55c0060dd..d003e0303 100644
--- a/ui/js/component/link.js
+++ b/ui/js/component/reward-link.js
@@ -3,59 +3,6 @@ import {Icon} from './common.js';
import Modal from '../component/modal.js';
import rewards from '../rewards.js';
-export let Link = React.createClass({
- propTypes: {
- label: React.PropTypes.string,
- icon: React.PropTypes.string,
- button: React.PropTypes.string,
- badge: React.PropTypes.string,
- hidden: React.PropTypes.bool,
- },
- getDefaultProps: function() {
- return {
- hidden: false,
- disabled: false,
- };
- },
- handleClick: function(e) {
- if (this.props.onClick) {
- this.props.onClick(e);
- }
- },
- render: function() {
- if (this.props.hidden) {
- return null;
- }
-
- /* The way the class name is generated here is a mess -- refactor */
-
- const className = (this.props.className || '') +
- (!this.props.className && !this.props.button ? 'button-text' : '') + // Non-button links get the same look as text buttons
- (this.props.button ? ' button-block button-' + this.props.button + ' button-set-item' : '') +
- (this.props.disabled ? ' disabled' : '');
-
- let content;
- if (this.props.children) { // Custom content
- content = this.props.children;
- } else {
- content = (
-
- {'icon' in this.props ? : null}
- {{this.props.label} }
- {'badge' in this.props ? {this.props.badge} : null}
-
- );
- }
-
- return (
-
- {content}
-
- );
- }
-});
-
export let RewardLink = React.createClass({
propTypes: {
type: React.PropTypes.string.isRequired,
diff --git a/ui/js/component/router/index.jsx b/ui/js/component/router/index.jsx
new file mode 100644
index 000000000..c75222949
--- /dev/null
+++ b/ui/js/component/router/index.jsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import Router from './view.jsx';
+import {
+ selectCurrentPage
+} from 'selectors/app.js';
+
+const select = (state) => ({
+ currentPage: selectCurrentPage(state)
+})
+
+const perform = {
+}
+
+export default connect(select, null)(Router);
diff --git a/ui/js/component/router/view.jsx b/ui/js/component/router/view.jsx
new file mode 100644
index 000000000..4618368ba
--- /dev/null
+++ b/ui/js/component/router/view.jsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import SettingsPage from 'page/settings.js';
+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.js';
+import DiscoverPage from 'page/discover';
+import SplashScreen from 'component/splash.js';
+import RewardsPage from 'page/rewards.js';
+import DeveloperPage from 'page/developer.js';
+import FileListDownloaded from 'page/fileListDownloaded'
+import FileListPublished from 'page/fileListPublished'
+
+const route = (page, routesMap) => {
+ const component = routesMap[page]
+
+ return component
+};
+
+
+const Router = (props) => {
+ const {
+ currentPage,
+ } = props;
+
+ return route(currentPage, {
+ 'settings': ,
+ 'help': ,
+ 'report': ,
+ 'downloaded': ,
+ 'published': ,
+ 'rewards' : ,
+ 'start': ,
+ 'wallet': ,
+ 'send': ,
+ 'receive': ,
+ 'show': ,
+ 'publish': ,
+ 'developer': ,
+ 'discover': ,
+ })
+}
+
+export default Router
diff --git a/ui/js/component/upgradeModal/index.jsx b/ui/js/component/upgradeModal/index.jsx
new file mode 100644
index 000000000..e2872978c
--- /dev/null
+++ b/ui/js/component/upgradeModal/index.jsx
@@ -0,0 +1,19 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import {
+ doDownloadUpgrade,
+ doSkipUpgrade,
+} from 'actions/app'
+import UpgradeModal from './view'
+
+const select = (state) => ({
+})
+
+const perform = (dispatch) => ({
+ downloadUpgrade: () => dispatch(doDownloadUpgrade()),
+ skipUpgrade: () => dispatch(doSkipUpgrade()),
+})
+
+export default connect(select, perform)(UpgradeModal)
diff --git a/ui/js/component/upgradeModal/view.jsx b/ui/js/component/upgradeModal/view.jsx
new file mode 100644
index 000000000..a2a181c79
--- /dev/null
+++ b/ui/js/component/upgradeModal/view.jsx
@@ -0,0 +1,32 @@
+import React from 'react'
+import {
+ Modal
+} from 'component/modal'
+import {
+ downloadUpgrade,
+ skipUpgrade
+} from 'actions/app'
+
+class UpgradeModal extends React.Component {
+ render() {
+ const {
+ downloadUpgrade,
+ skipUpgrade
+ } = this.props
+
+ return (
+
+ Your version of LBRY is out of date and may be unreliable or insecure.
+
+ )
+ }
+}
+
+export default UpgradeModal
diff --git a/ui/js/config/development.js b/ui/js/config/development.js
new file mode 100644
index 000000000..631f37565
--- /dev/null
+++ b/ui/js/config/development.js
@@ -0,0 +1,2 @@
+module.exports = {
+}
diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js
new file mode 100644
index 000000000..80eecab79
--- /dev/null
+++ b/ui/js/constants/action_types.js
@@ -0,0 +1,66 @@
+export const NAVIGATE = 'NAVIGATE'
+export const OPEN_MODAL = 'OPEN_MODAL'
+export const CLOSE_MODAL = 'CLOSE_MODAL'
+
+export const OPEN_DRAWER = 'OPEN_DRAWER'
+export const CLOSE_DRAWER = 'CLOSE_DRAWER'
+
+export const DAEMON_READY = 'DAEMON_READY'
+
+// Upgrades
+export const UPGRADE_CANCELLED = 'UPGRADE_CANCELLED'
+export const DOWNLOAD_UPGRADE = 'DOWNLOAD_UPGRADE'
+export const UPGRADE_DOWNLOAD_STARTED = 'UPGRADE_DOWNLOAD_STARTED'
+export const UPGRADE_DOWNLOAD_COMPLETED = 'UPGRADE_DOWNLOAD_COMPLETED'
+export const UPGRADE_DOWNLOAD_PROGRESSED = 'UPGRADE_DOWNLOAD_PROGRESSED'
+export const CHECK_UPGRADE_AVAILABLE = 'CHECK_UPGRADE_AVAILABLE'
+export const UPDATE_VERSION = 'UPDATE_VERSION'
+export const SKIP_UPGRADE = 'SKIP_UPGRADE'
+export const START_UPGRADE = 'START_UPGRADE'
+
+// Wallet
+export const GET_NEW_ADDRESS_STARTED = 'GET_NEW_ADDRESS_STARTED'
+export const GET_NEW_ADDRESS_COMPLETED = 'GET_NEW_ADDRESS_COMPLETED'
+export const FETCH_TRANSACTIONS_STARTED = 'FETCH_TRANSACTIONS_STARTED'
+export const FETCH_TRANSACTIONS_COMPLETED = 'FETCH_TRANSACTIONS_COMPLETED'
+export const UPDATE_BALANCE = 'UPDATE_BALANCE'
+export const CHECK_ADDRESS_IS_MINE_STARTED = 'CHECK_ADDRESS_IS_MINE_STARTED'
+export const CHECK_ADDRESS_IS_MINE_COMPLETED = 'CHECK_ADDRESS_IS_MINE_COMPLETED'
+export const SET_DRAFT_TRANSACTION_AMOUNT = 'SET_DRAFT_TRANSACTION_AMOUNT'
+export const SET_DRAFT_TRANSACTION_ADDRESS = 'SET_DRAFT_TRANSACTION_ADDRESS'
+export const SEND_TRANSACTION_STARTED = 'SEND_TRANSACTION_STARTED'
+export const SEND_TRANSACTION_COMPLETED = 'SEND_TRANSACTION_COMPLETED'
+export const SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED'
+
+// Content
+export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED'
+export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED'
+export const RESOLVE_URI_STARTED = 'RESOLVE_URI_STARTED'
+export const RESOLVE_URI_COMPLETED = 'RESOLVE_URI_COMPLETED'
+export const FETCH_DOWNLOADED_CONTENT_STARTED = 'FETCH_DOWNLOADED_CONTENT_STARTED'
+export const FETCH_DOWNLOADED_CONTENT_COMPLETED = 'FETCH_DOWNLOADED_CONTENT_COMPLETED'
+export const FETCH_PUBLISHED_CONTENT_STARTED = 'FETCH_PUBLISHED_CONTENT_STARTED'
+export const FETCH_PUBLISHED_CONTENT_COMPLETED = 'FETCH_PUBLISHED_CONTENT_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 DELETE_FILE_STARTED = 'DELETE_FILE_STARTED'
+export const DELETE_FILE_COMPLETED = 'DELETE_FILE_COMPLETED'
+export const FETCH_MY_CLAIMS_COMPLETED = 'FETCH_MY_CLAIMS_COMPLETED'
+
+// Search
+export const SEARCH_STARTED = 'SEARCH_STARTED'
+export const SEARCH_COMPLETED = 'SEARCH_COMPLETED'
+export const SEARCH_CANCELLED = 'SEARCH_CANCELLED'
+export const ACTIVATE_SEARCH = 'ACTIVATE_SEARCH'
+export const DEACTIVATE_SEARCH = 'DEACTIVATE_SEARCH'
diff --git a/ui/js/lbry.js b/ui/js/lbry.js
index 2471b38b5..b44ecfb87 100644
--- a/ui/js/lbry.js
+++ b/ui/js/lbry.js
@@ -287,7 +287,7 @@ lbry.getMyClaims = function(callback) {
lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) {
this._removedFiles.push(outpoint);
- this._updateFileInfoSubscribers(outpoint);
+ // this._updateFileInfoSubscribers(outpoint);
lbry.file_delete({
outpoint: outpoint,
diff --git a/ui/js/lighthouse.js b/ui/js/lighthouse.js
index 9aa51747d..5ca4ef038 100644
--- a/ui/js/lighthouse.js
+++ b/ui/js/lighthouse.js
@@ -5,6 +5,8 @@ const queryTimeout = 3000;
const maxQueryTries = 2;
const defaultServers = [
'http://lighthouse7.lbry.io:50005',
+ 'http://lighthouse8.lbry.io:50005',
+ 'http://lighthouse9.lbry.io:50005',
];
const path = '/';
diff --git a/ui/js/main.js b/ui/js/main.js
index 610ca8594..d94093f24 100644
--- a/ui/js/main.js
+++ b/ui/js/main.js
@@ -3,13 +3,20 @@ import ReactDOM from 'react-dom';
import lbry from './lbry.js';
import lbryio from './lbryio.js';
import lighthouse from './lighthouse.js';
-import App from './app.js';
+import App from './component/app/index.js';
import SplashScreen from './component/splash.js';
import SnackBar from './component/snack-bar.js';
import {AuthOverlay} from './component/auth.js';
+import { Provider } from 'react-redux';
+import store from 'store.js';
+import { runTriggers } from 'triggers'
+import {
+ doDaemonReady
+} from 'actions/app'
const {remote} = require('electron');
const contextMenu = remote.require('./menu/context-menu');
+const app = require('./app')
lbry.showMenuIfNeeded();
@@ -19,7 +26,11 @@ window.addEventListener('contextmenu', (event) => {
event.preventDefault();
});
-let init = function() {
+const initialState = app.store.getState();
+app.store.subscribe(runTriggers);
+runTriggers();
+
+var init = function() {
window.lbry = lbry;
window.lighthouse = lighthouse;
let canvas = document.getElementById('canvas');
@@ -29,8 +40,8 @@ let 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
- ReactDOM.render({ lbryio.enabled ?
: '' }
, canvas)
+ app.store.dispatch(doDaemonReady())
+ ReactDOM.render({ lbryio.enabled ?
: '' }
, canvas)
}
if (window.sessionStorage.getItem('loaded') == 'y') {
diff --git a/ui/js/page/developer.js b/ui/js/page/developer.js
index 377204852..31b3b6634 100644
--- a/ui/js/page/developer.js
+++ b/ui/js/page/developer.js
@@ -1,7 +1,7 @@
import lbry from '../lbry.js';
import React from 'react';
import {FormField} from '../component/form.js';
-import {Link} from '../component/link.js';
+import Link from '../component/link';
const fs = require('fs');
const {ipcRenderer} = require('electron');
diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js
deleted file mode 100644
index b6afa426f..000000000
--- a/ui/js/page/discover.js
+++ /dev/null
@@ -1,206 +0,0 @@
-import React from 'react';
-import lbry from '../lbry.js';
-import lbryio from '../lbryio.js';
-import lbryuri from '../lbryuri.js';
-import lighthouse from '../lighthouse.js';
-import {FileTile, FileTileStream} from '../component/file-tile.js';
-import {Link} from '../component/link.js';
-import {ToolTip} from '../component/tooltip.js';
-import {BusyMessage} from '../component/common.js';
-
-var fetchResultsStyle = {
- color: '#888',
- textAlign: 'center',
- fontSize: '1.2em'
- };
-
-var SearchActive = React.createClass({
- render: function() {
- return (
-
-
-
- );
- }
-});
-
-var searchNoResultsStyle = {
- textAlign: 'center'
-}, searchNoResultsMessageStyle = {
- fontStyle: 'italic',
- marginRight: '5px'
-};
-
-var SearchNoResults = React.createClass({
- render: function() {
- return (
-
- No one has checked anything in for {this.props.query} yet.
-
-
- );
- }
-});
-
-var SearchResults = React.createClass({
- render: function() {
- var rows = [],
- seenNames = {}; //fix this when the search API returns claim IDs
-
- for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) {
- const uri = lbryuri.build({
- channelName: channel_name,
- contentName: name,
- claimId: channel_id || claim_id,
- });
-
- rows.push(
-
- );
- }
- return (
- {rows}
- );
- }
-});
-
-const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' +
-'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
-'"five" to put your content here!');
-
-var FeaturedCategory = React.createClass({
- render: function() {
- return (
- { this.props.category ?
-
{this.props.category}
- { this.props.category == "community" ?
-
- : '' }
- : '' }
- { this.props.names.map((name) => { return }) }
- )
- }
-})
-
-var FeaturedContent = React.createClass({
- getInitialState: function() {
- return {
- featuredUris: {},
- failed: false
- };
- },
- componentWillMount: function() {
- lbryio.call('discover', 'list', { version: "early-access" } ).then(({Categories, Uris}) => {
- let featuredUris = {}
- Categories.forEach((category) => {
- if (Uris[category] && Uris[category].length) {
- featuredUris[category] = Uris[category]
- }
- })
- this.setState({ featuredUris: featuredUris });
- }, () => {
- this.setState({
- failed: true
- })
- });
- },
- render: function() {
- return (
- this.state.failed ?
- Failed to load landing content.
:
-
- {
- Object.keys(this.state.featuredUris).map(function(category) {
- return this.state.featuredUris[category].length ?
- :
- '';
- }.bind(this))
- }
-
- );
- }
-});
-
-var DiscoverPage = React.createClass({
- userTypingTimer: null,
-
- propTypes: {
- showWelcome: React.PropTypes.bool.isRequired,
- },
-
- componentDidUpdate: function() {
- if (this.props.query != this.state.query)
- {
- this.handleSearchChanged(this.props.query);
- }
- },
-
- getDefaultProps: function() {
- return {
- showWelcome: false,
- }
- },
-
- componentWillReceiveProps: function(nextProps, nextState) {
- if (nextProps.query != nextState.query)
- {
- this.handleSearchChanged(nextProps.query);
- }
- },
-
- handleSearchChanged: function(query) {
- this.setState({
- searching: true,
- query: query,
- });
-
- lighthouse.search(query).then(this.searchCallback);
- },
-
- handleWelcomeDone: function() {
- this.setState({
- welcomeComplete: true,
- });
- },
-
- componentWillMount: function() {
- document.title = "Discover";
-
- if (this.props.query) {
- // Rendering with a query already typed
- this.handleSearchChanged(this.props.query);
- }
- },
-
- getInitialState: function() {
- return {
- welcomeComplete: false,
- results: [],
- query: this.props.query,
- searching: ('query' in this.props) && (this.props.query.length > 0)
- };
- },
-
- searchCallback: function(results) {
- if (this.state.searching) //could have canceled while results were pending, in which case nothing to do
- {
- this.setState({
- results: results,
- searching: false //multiple searches can be out, we're only done if we receive one we actually care about
- });
- }
- },
-
- render: function() {
- return (
-
- { this.state.searching ? : null }
- { !this.state.searching && this.props.query && this.state.results.length ? : null }
- { !this.state.searching && this.props.query && !this.state.results.length ? : null }
- { !this.props.query && !this.state.searching ? : null }
-
- );
- }
-});
-
-export default DiscoverPage;
diff --git a/ui/js/page/discover/index.js b/ui/js/page/discover/index.js
new file mode 100644
index 000000000..bb2d97e49
--- /dev/null
+++ b/ui/js/page/discover/index.js
@@ -0,0 +1,30 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import {
+ selectFeaturedContentByCategory
+} from 'selectors/content'
+import {
+ doSearchContent,
+} from 'actions/search'
+import {
+ selectIsSearching,
+ selectSearchQuery,
+ selectCurrentSearchResults,
+ selectSearchActivated,
+} from 'selectors/search'
+import DiscoverPage from './view'
+
+const select = (state) => ({
+ featuredContentByCategory: selectFeaturedContentByCategory(state),
+ isSearching: selectIsSearching(state),
+ query: selectSearchQuery(state),
+ results: selectCurrentSearchResults(state),
+ searchActive: selectSearchActivated(state),
+})
+
+const perform = (dispatch) => ({
+})
+
+export default connect(select, perform)(DiscoverPage)
diff --git a/ui/js/page/discover/view.jsx b/ui/js/page/discover/view.jsx
new file mode 100644
index 000000000..618ef675c
--- /dev/null
+++ b/ui/js/page/discover/view.jsx
@@ -0,0 +1,126 @@
+import React from 'react';
+import lbry from 'lbry.js';
+import lbryio from 'lbryio.js';
+import lbryuri from 'lbryuri.js';
+import lighthouse from 'lighthouse.js';
+import FileTile from 'component/fileTile';
+import FileTileStream from 'component/fileTileStream'
+import Link from 'component/link';
+import {ToolTip} from 'component/tooltip.js';
+import {BusyMessage} from 'component/common.js';
+
+const fetchResultsStyle = {
+ color: '#888',
+ textAlign: 'center',
+ fontSize: '1.2em'
+}
+
+const SearchActive = (props) => {
+ return (
+
+
+
+ )
+}
+
+const searchNoResultsStyle = {
+ textAlign: 'center'
+}, searchNoResultsMessageStyle = {
+ fontStyle: 'italic',
+ marginRight: '5px'
+};
+
+const SearchNoResults = (props) => {
+ const {
+ query,
+ } = props
+
+ return (
+
+ No one has checked anything in for {query} yet.
+
+
+ )
+}
+
+const SearchResults = (props) => {
+ const {
+ results
+ } = props
+
+ const rows = [],
+ seenNames = {}; //fix this when the search API returns claim IDs
+
+ for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of results) {
+ const uri = lbryuri.build({
+ channelName: channel_name,
+ contentName: name,
+ claimId: channel_id || claim_id,
+ });
+
+ rows.push(
+
+ );
+ }
+
+ return (
+ {rows}
+ )
+}
+
+const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' +
+'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
+'"five" to put your content here!');
+
+const FeaturedCategory = (props) => {
+ const {
+ category,
+ names,
+ } = props
+
+ return (
+
+
{category}
+ {category &&
+ }
+
+ {names && names.map(name => )}
+
+ )
+}
+
+const FeaturedContent = (props) => {
+ const {
+ featuredContentByCategory,
+ } = props
+
+ const categories = Object.keys(featuredContentByCategory)
+
+ return (
+
+ {categories.map(category =>
+
+ )}
+
+ )
+}
+
+const DiscoverPage = (props) => {
+ const {
+ isSearching,
+ query,
+ results,
+ searchActive,
+ } = props
+
+ return (
+
+ { (!searchActive || (!isSearching && !query)) && }
+ { searchActive && isSearching ? : null }
+ { searchActive && !isSearching && query && results.length ? : null }
+ { searchActive && !isSearching && query && !results.length ? : null }
+
+ );
+}
+
+export default DiscoverPage;
diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js
deleted file mode 100644
index 063730e7f..000000000
--- a/ui/js/page/file-list.js
+++ /dev/null
@@ -1,220 +0,0 @@
-import React from 'react';
-import lbry from '../lbry.js';
-import lbryuri from '../lbryuri.js';
-import {Link} from '../component/link.js';
-import {FormField} from '../component/form.js';
-import {FileTileStream} from '../component/file-tile.js';
-import rewards from '../rewards.js';
-import lbryio from '../lbryio.js';
-import {BusyMessage, Thumbnail} from '../component/common.js';
-
-
-export let FileListDownloaded = React.createClass({
- _isMounted: false,
-
- getInitialState: function() {
- return {
- fileInfos: null,
- };
- },
- componentDidMount: function() {
- this._isMounted = true;
- document.title = "Downloaded Files";
-
- lbry.claim_list_mine().then((myClaimInfos) => {
- if (!this._isMounted) { return; }
-
- lbry.file_list().then((fileInfos) => {
- if (!this._isMounted) { return; }
-
- const myClaimOutpoints = myClaimInfos.map(({txid, nout}) => txid + ':' + nout);
- this.setState({
- fileInfos: fileInfos.filter(({outpoint}) => !myClaimOutpoints.includes(outpoint)),
- });
- });
- });
- },
- componentWillUnmount: function() {
- this._isMounted = false;
- },
- render: function() {
- if (this.state.fileInfos === null) {
- return (
-
-
-
- );
- } else if (!this.state.fileInfos.length) {
- return (
-
- You haven't downloaded anything from LBRY yet. Go !
-
- );
- } else {
- return (
-
-
-
- );
- }
- }
-});
-
-export let FileListPublished = React.createClass({
- _isMounted: false,
-
- getInitialState: function () {
- return {
- fileInfos: null,
- };
- },
- _requestPublishReward: function() {
- lbryio.call('reward', 'list', {}).then(function(userRewards) {
- //already rewarded
- if (userRewards.filter(function (reward) {
- return reward.RewardType == rewards.TYPE_FIRST_PUBLISH && reward.TransactionID;
- }).length) {
- return;
- }
- else {
- rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
- }
- });
- },
- componentDidMount: function () {
- this._isMounted = true;
- this._requestPublishReward();
- document.title = "Published Files";
-
- lbry.claim_list_mine().then((claimInfos) => {
- if (!this._isMounted) { return; }
-
- lbry.file_list().then((fileInfos) => {
- if (!this._isMounted) { return; }
-
- const myClaimOutpoints = claimInfos.map(({txid, nout}) => txid + ':' + nout);
- this.setState({
- fileInfos: fileInfos.filter(({outpoint}) => myClaimOutpoints.includes(outpoint)),
- });
- });
- });
- },
- componentWillUnmount: function() {
- this._isMounted = false;
- },
- render: function () {
- if (this.state.fileInfos === null) {
- return (
-
-
-
- );
- }
- else if (!this.state.fileInfos.length) {
- return (
-
- You haven't published anything to LBRY yet. Try !
-
- );
- }
- else {
- return (
-
-
-
- );
- }
- }
-});
-
-export let FileList = React.createClass({
- _sortFunctions: {
- date: function(fileInfos) {
- return fileInfos.slice().reverse();
- },
- title: function(fileInfos) {
- return fileInfos.slice().sort(function(fileInfo1, fileInfo2) {
- const title1 = fileInfo1.metadata ? fileInfo1.metadata.title.toLowerCase() : fileInfo1.name;
- const title2 = fileInfo2.metadata ? fileInfo2.metadata.title.toLowerCase() : fileInfo2.name;
- if (title1 < title2) {
- return -1;
- } else if (title1 > title2) {
- return 1;
- } else {
- return 0;
- }
- });
- },
- filename: function(fileInfos) {
- return fileInfos.slice().sort(function({file_name: fileName1}, {file_name: fileName2}) {
- const fileName1Lower = fileName1.toLowerCase();
- const fileName2Lower = fileName2.toLowerCase();
- if (fileName1Lower < fileName2Lower) {
- return -1;
- } else if (fileName2Lower > fileName1Lower) {
- return 1;
- } else {
- return 0;
- }
- });
- },
- },
- propTypes: {
- fileInfos: React.PropTypes.array.isRequired,
- hidePrices: React.PropTypes.bool,
- },
- getDefaultProps: function() {
- return {
- hidePrices: false,
- };
- },
- getInitialState: function() {
- return {
- sortBy: 'date',
- };
- },
- handleSortChanged: function(event) {
- this.setState({
- sortBy: event.target.value,
- });
- },
- render: function() {
- var content = [],
- seenUris = {};
-
- const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos);
- for (let {outpoint, name, channel_name, metadata, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) {
- if (seenUris[name] || !claim_id) {
- continue;
- }
-
- let streamMetadata;
- if (metadata) {
- streamMetadata = metadata.stream.metadata;
- } else {
- streamMetadata = null;
- }
-
-
- const uri = lbryuri.build({contentName: name, channelName: channel_name});
- seenUris[name] = true;
- content.push( );
- }
-
- return (
-
-
- Sort by { ' ' }
-
- Date
- Title
- File name
-
-
- {content}
-
- );
- }
-});
\ No newline at end of file
diff --git a/ui/js/page/fileListDownloaded/index.js b/ui/js/page/fileListDownloaded/index.js
new file mode 100644
index 000000000..6d92a1867
--- /dev/null
+++ b/ui/js/page/fileListDownloaded/index.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import {
+ selectFetchingDownloadedContent,
+} from 'selectors/content'
+import {
+ selectDownloadedFileInfo,
+} from 'selectors/file_info'
+import {
+ doNavigate,
+} from 'actions/app'
+import FileListDownloaded from './view'
+
+const select = (state) => ({
+ downloadedContent: selectDownloadedFileInfo(state),
+ fetching: selectFetchingDownloadedContent(state),
+})
+
+const perform = (dispatch) => ({
+ navigate: (path) => dispatch(doNavigate(path)),
+})
+
+export default connect(select, perform)(FileListDownloaded)
diff --git a/ui/js/page/fileListDownloaded/view.jsx b/ui/js/page/fileListDownloaded/view.jsx
new file mode 100644
index 000000000..cf95e0224
--- /dev/null
+++ b/ui/js/page/fileListDownloaded/view.jsx
@@ -0,0 +1,42 @@
+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 {FileTileStream} from 'component/fileTile';
+import rewards from 'rewards.js';
+import lbryio from 'lbryio.js';
+import {BusyMessage, Thumbnail} from 'component/common.js';
+import FileList from 'component/fileList'
+
+class FileListDownloaded extends React.Component {
+ render() {
+ const {
+ downloadedContent,
+ fetching,
+ navigate,
+ } = this.props
+
+ if (fetching) {
+ return (
+
+
+
+ );
+ } else if (!downloadedContent.length) {
+ return (
+
+ You haven't downloaded anything from LBRY yet. Go navigate('discover')} label="search for your first download" />!
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
+ }
+}
+
+export default FileListDownloaded
diff --git a/ui/js/page/fileListPublished/index.js b/ui/js/page/fileListPublished/index.js
new file mode 100644
index 000000000..f2b5c78f5
--- /dev/null
+++ b/ui/js/page/fileListPublished/index.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import {
+ selectFetchingPublishedContent,
+} from 'selectors/content'
+import {
+ selectPublishedFileInfo,
+} from 'selectors/file_info'
+import {
+ doNavigate,
+} from 'actions/app'
+import FileListPublished from './view'
+
+const select = (state) => ({
+ publishedContent: selectPublishedFileInfo(state),
+ fetching: selectFetchingPublishedContent(state),
+})
+
+const perform = (dispatch) => ({
+ navigate: (path) => dispatch(doNavigate(path)),
+})
+
+export default connect(select, perform)(FileListPublished)
diff --git a/ui/js/page/fileListPublished/view.jsx b/ui/js/page/fileListPublished/view.jsx
new file mode 100644
index 000000000..56d00003b
--- /dev/null
+++ b/ui/js/page/fileListPublished/view.jsx
@@ -0,0 +1,126 @@
+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 {FileTileStream} from 'component/fileTile';
+import rewards from 'rewards.js';
+import lbryio from 'lbryio.js';
+import {BusyMessage, Thumbnail} from 'component/common.js';
+import FileList from 'component/fileList'
+
+class FileListPublished extends React.Component {
+ componentDidUpdate() {
+ if(this.props.publishedContent.length > 0) this._requestPublishReward()
+ }
+
+ _requestPublishReward() {
+ lbryio.call('reward', 'list', {}).then(function(userRewards) {
+ //already rewarded
+ if (userRewards.filter(function (reward) {
+ return reward.RewardType == rewards.TYPE_FIRST_PUBLISH && reward.TransactionID
+ }).length) {
+ return
+ }
+ else {
+ rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
+ }
+ })
+ }
+
+ render() {
+ const {
+ publishedContent,
+ fetching,
+ navigate,
+ } = this.props
+
+ if (fetching) {
+ return (
+
+
+
+ );
+ } else if (!publishedContent.length) {
+ return (
+
+ You haven't downloaded anything from LBRY yet. Go navigate('discover')} label="search for your first download" />!
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
+ }
+}
+// const FileListPublished = React.createClass({
+// _isMounted: false,
+
+// getInitialState: function () {
+// return {
+// fileInfos: null,
+// };
+// },
+// _requestPublishReward: function() {
+// lbryio.call('reward', 'list', {}).then(function(userRewards) {
+// //already rewarded
+// if (userRewards.filter(function (reward) {
+// return reward.RewardType == rewards.TYPE_FIRST_PUBLISH && reward.TransactionID;
+// }).length) {
+// return;
+// }
+// else {
+// rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
+// }
+// });
+// },
+// componentDidMount: function () {
+// this._isMounted = true;
+// this._requestPublishReward();
+// document.title = "Published Files";
+
+// lbry.claim_list_mine().then((claimInfos) => {
+// if (!this._isMounted) { return; }
+
+// lbry.file_list().then((fileInfos) => {
+// if (!this._isMounted) { return; }
+
+// const myClaimOutpoints = claimInfos.map(({txid, nout}) => txid + ':' + nout);
+// this.setState({
+// fileInfos: fileInfos.filter(({outpoint}) => myClaimOutpoints.includes(outpoint)),
+// });
+// });
+// });
+// },
+// componentWillUnmount: function() {
+// this._isMounted = false;
+// },
+// render: function () {
+// if (this.state.fileInfos === null) {
+// return (
+//
+//
+//
+// );
+// }
+// else if (!this.state.fileInfos.length) {
+// return (
+//
+// You haven't published anything to LBRY yet. Try !
+//
+// );
+// }
+// else {
+// return (
+//
+//
+//
+// );
+// }
+// }
+// });
+
+export default FileListPublished
diff --git a/ui/js/page/help/index.jsx b/ui/js/page/help/index.jsx
new file mode 100644
index 000000000..0663d5ffa
--- /dev/null
+++ b/ui/js/page/help/index.jsx
@@ -0,0 +1,7 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import HelpPage from './view'
+
+export default connect(null, null)(HelpPage)
diff --git a/ui/js/page/help.js b/ui/js/page/help/view.jsx
similarity index 96%
rename from ui/js/page/help.js
rename to ui/js/page/help/view.jsx
index 99e6ad0d7..34c10243a 100644
--- a/ui/js/page/help.js
+++ b/ui/js/page/help/view.jsx
@@ -1,9 +1,9 @@
//@TODO: Customize advice based on OS
//@TODO: Customize advice based on OS
import React from 'react';
-import lbry from '../lbry.js';
-import {Link} from '../component/link.js';
-import {version as uiVersion} from 'json!../../package.json';
+import lbry from 'lbry.js';
+import Link from 'component/link';
+import {version as uiVersion} from 'json!../../../package.json';
var HelpPage = React.createClass({
getInitialState: function() {
@@ -120,4 +120,4 @@ var HelpPage = React.createClass({
}
});
-export default HelpPage;
\ No newline at end of file
+export default HelpPage;
diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js
index 13736f0db..ad600f19d 100644
--- a/ui/js/page/publish.js
+++ b/ui/js/page/publish.js
@@ -1,7 +1,7 @@
import React from 'react';
import lbry from '../lbry.js';
import {FormField, FormRow} from '../component/form.js';
-import {Link} from '../component/link.js';
+import Link from '../component/link';
import rewards from '../rewards.js';
import lbryio from '../lbryio.js';
import Modal from '../component/modal.js';
diff --git a/ui/js/page/report.js b/ui/js/page/report.js
index 47a4d2a7a..93699feb3 100644
--- a/ui/js/page/report.js
+++ b/ui/js/page/report.js
@@ -1,5 +1,5 @@
import React from 'react';
-import {Link} from '../component/link.js';
+import Link from 'component/link';
import Modal from '../component/modal.js';
import lbry from '../lbry.js';
diff --git a/ui/js/page/reward.js b/ui/js/page/reward.js
index 2fb5b3e64..0a8aeebc6 100644
--- a/ui/js/page/reward.js
+++ b/ui/js/page/reward.js
@@ -1,6 +1,6 @@
import React from 'react';
import lbryio from '../lbryio.js';
-import {Link} from '../component/link.js';
+import {Link} from '../component/link';
import Notice from '../component/notice.js';
import {CreditAmount} from '../component/common.js';
//
diff --git a/ui/js/page/rewards.js b/ui/js/page/rewards.js
index 18e936aee..5bb55f9ff 100644
--- a/ui/js/page/rewards.js
+++ b/ui/js/page/rewards.js
@@ -4,7 +4,7 @@ import lbryio from '../lbryio.js';
import {CreditAmount, Icon} from '../component/common.js';
import rewards from '../rewards.js';
import Modal from '../component/modal.js';
-import {RewardLink} from '../component/link.js';
+import {RewardLink} from '../component/reward-link.js';
const RewardTile = React.createClass({
propTypes: {
diff --git a/ui/js/page/show.js b/ui/js/page/show.js
deleted file mode 100644
index cc2fb5cfc..000000000
--- a/ui/js/page/show.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import React from 'react';
-import lbry from '../lbry.js';
-import lighthouse from '../lighthouse.js';
-import lbryuri from '../lbryuri.js';
-import {Video} from '../page/watch.js'
-import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js';
-import {FileActions} from '../component/file-actions.js';
-import {Link} from '../component/link.js';
-import UriIndicator from '../component/channel-indicator.js';
-
-var FormatItem = React.createClass({
- propTypes: {
- metadata: React.PropTypes.object,
- contentType: React.PropTypes.string,
- uri: React.PropTypes.string,
- outpoint: React.PropTypes.string,
- },
- render: function() {
- const {thumbnail, author, title, description, language, license} = this.props.metadata;
- const mediaType = lbry.getMediaType(this.props.contentType);
-
- return (
-
-
-
- Content-Type {this.props.contentType}
-
-
- Author {author}
-
-
- Language {language}
-
-
- License {license}
-
-
-
- );
- }
-});
-
-let ShowPage = React.createClass({
- _uri: null,
-
- propTypes: {
- uri: React.PropTypes.string,
- },
- getInitialState: function() {
- return {
- metadata: null,
- contentType: null,
- hasSignature: false,
- signatureIsValid: false,
- cost: null,
- costIncludesData: null,
- uriLookupComplete: null,
- isDownloaded: null,
- };
- },
- componentWillMount: function() {
- this._uri = lbryuri.normalize(this.props.uri);
- document.title = this._uri;
-
- lbry.resolve({uri: this._uri}).then(({ claim: {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}}}) => {
- const outpoint = txid + ':' + nout;
-
- lbry.file_list({outpoint}).then((fileInfo) => {
- this.setState({
- isDownloaded: fileInfo.length > 0,
- });
- });
-
- this.setState({
- outpoint: outpoint,
- metadata: metadata,
- hasSignature: has_signature,
- signatureIsValid: signature_is_valid,
- contentType: contentType,
- uriLookupComplete: true,
- });
- });
-
- lbry.getCostInfo(this._uri).then(({cost, includesData}) => {
- this.setState({
- cost: cost,
- costIncludesData: includesData,
- });
- });
- },
- render: function() {
- const metadata = this.state.metadata;
- const title = metadata ? this.state.metadata.title : this._uri;
- return (
-
-
- { this.state.contentType && this.state.contentType.startsWith('video/') ?
- :
- (metadata ? : ) }
-
-
-
-
- {this.state.isDownloaded === false
- ?
- : null}
-
{title}
- { this.state.uriLookupComplete ?
-
: '' }
-
- { this.state.uriLookupComplete ?
-
-
- {metadata.description}
-
-
- :
}
-
- { metadata ?
-
-
-
: '' }
-
-
-
-
-
- );
- }
-});
-
-export default ShowPage;
diff --git a/ui/js/page/showPage/index.js b/ui/js/page/showPage/index.js
new file mode 100644
index 000000000..4ba0ba729
--- /dev/null
+++ b/ui/js/page/showPage/index.js
@@ -0,0 +1,33 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import {
+ selectCurrentUri,
+} from 'selectors/app'
+import {
+ selectCurrentUriIsDownloaded,
+} from 'selectors/file_info'
+import {
+ selectCurrentUriClaim,
+} from 'selectors/claims'
+import {
+ selectCurrentUriFileInfo,
+} from 'selectors/file_info'
+import {
+ selectCurrentUriCostInfo,
+} from 'selectors/cost_info'
+import ShowPage from './view'
+
+const select = (state) => ({
+ claim: selectCurrentUriClaim(state),
+ uri: selectCurrentUri(state),
+ isDownloaded: selectCurrentUriIsDownloaded(state),
+ fileInfo: selectCurrentUriFileInfo(state),
+ costInfo: selectCurrentUriCostInfo(state),
+})
+
+const perform = (dispatch) => ({
+})
+
+export default connect(select, perform)(ShowPage)
diff --git a/ui/js/page/showPage/view.jsx b/ui/js/page/showPage/view.jsx
new file mode 100644
index 000000000..9620d2c74
--- /dev/null
+++ b/ui/js/page/showPage/view.jsx
@@ -0,0 +1,135 @@
+import React from 'react';
+import lbry from 'lbry.js';
+import lighthouse from 'lighthouse.js';
+import lbryuri from 'lbryuri.js';
+import Video from 'page/video'
+import {
+ TruncatedText,
+ Thumbnail,
+ BusyMessage,
+} from 'component/common';
+import FilePrice from 'component/filePrice'
+import FileActions from 'component/fileActions';
+import Link from 'component/link';
+import UriIndicator from 'component/channel-indicator.js';
+
+const FormatItem = (props) => {
+ const {
+ contentType,
+ metadata,
+ metadata: {
+ thumbnail,
+ author,
+ title,
+ description,
+ language,
+ license,
+ },
+ cost,
+ uri,
+ outpoint,
+ costIncludesData,
+ } = props
+ const mediaType = lbry.getMediaType(contentType);
+
+ return (
+
+
+
+ Content-Type {contentType}
+
+
+ Author {author}
+
+
+ Language {language}
+
+
+ License {license}
+
+
+
+ )
+}
+
+const ShowPage = (props) => {
+ const {
+ claim,
+ claim: {
+ txid,
+ nout,
+ has_signature: hasSignature,
+ signature_is_valid: signatureIsValid,
+ value,
+ value: {
+ stream,
+ stream: {
+ metadata,
+ source,
+ metadata: {
+ title,
+ } = {},
+ source: {
+ contentType,
+ } = {},
+ } = {},
+ } = {},
+ },
+ uri,
+ isDownloaded,
+ fileInfo,
+ costInfo,
+ costInfo: {
+ cost,
+ includesData: costIncludesData,
+ } = {},
+ } = props
+
+ const outpoint = txid + ':' + nout;
+ const uriLookupComplete = !!claim && Object.keys(claim).length
+
+ return (
+
+
+ { contentType && contentType.startsWith('video/') ?
+ :
+ (metadata ? : ) }
+
+
+
+
+ {isDownloaded === false
+ ?
+ : null}
+
{title}
+ { uriLookupComplete ?
+
: '' }
+
+ { uriLookupComplete ?
+
+
+ {metadata.description}
+
+
+ :
}
+
+ { metadata ?
+
+
+
: '' }
+
+
+
+
+
+ )
+}
+
+export default ShowPage;
diff --git a/ui/js/page/video/index.js b/ui/js/page/video/index.js
new file mode 100644
index 000000000..597293b56
--- /dev/null
+++ b/ui/js/page/video/index.js
@@ -0,0 +1,42 @@
+import React from 'react'
+import {
+ connect,
+} from 'react-redux'
+import {
+ doCloseModal,
+} from 'actions/app'
+import {
+ selectCurrentModal,
+} from 'selectors/app'
+import {
+ doWatchVideo,
+ doLoadVideo,
+} from 'actions/content'
+import {
+ selectLoadingCurrentUri,
+ selectCurrentUriFileReadyToPlay,
+ selectCurrentUriIsPlaying,
+ selectCurrentUriFileInfo,
+ selectDownloadingCurrentUri,
+} from 'selectors/file_info'
+import {
+ selectCurrentUriCostInfo,
+} from 'selectors/cost_info'
+import Video from './view'
+
+const select = (state) => ({
+ costInfo: selectCurrentUriCostInfo(state),
+ fileInfo: selectCurrentUriFileInfo(state),
+ modal: selectCurrentModal(state),
+ isLoading: selectLoadingCurrentUri(state),
+ readyToPlay: selectCurrentUriFileReadyToPlay(state),
+ isDownloading: selectDownloadingCurrentUri(state),
+})
+
+const perform = (dispatch) => ({
+ loadVideo: () => dispatch(doLoadVideo()),
+ watchVideo: (elem) => dispatch(doWatchVideo()),
+ closeModal: () => dispatch(doCloseModal()),
+})
+
+export default connect(select, perform)(Video)
diff --git a/ui/js/page/video/view.jsx b/ui/js/page/video/view.jsx
new file mode 100644
index 000000000..c71307119
--- /dev/null
+++ b/ui/js/page/video/view.jsx
@@ -0,0 +1,183 @@
+import React from 'react';
+import {
+ Icon,
+ Thumbnail,
+} from 'component/common';
+import FilePrice from 'component/filePrice'
+import Link from 'component/link';
+import lbry from 'lbry';
+import Modal from 'component/modal';
+import lbryio from 'lbryio';
+import rewards from 'rewards';
+import LoadScreen from 'component/load_screen'
+
+const fs = require('fs');
+const VideoStream = require('videostream');
+
+class WatchLink extends React.Component {
+ confirmPurchaseClick() {
+ this.props.closeModal()
+ this.props.startPlaying()
+ this.props.loadVideo()
+ }
+
+ render() {
+ const {
+ button,
+ label,
+ className,
+ onWatchClick,
+ metadata,
+ metadata: {
+ title,
+ },
+ uri,
+ modal,
+ closeModal,
+ isLoading,
+ costInfo,
+ fileInfo,
+ } = this.props
+
+ return (
+
+ {modal}
+
+ You don't have enough LBRY credits to pay for this stream.
+
+
+ Are you sure you'd like to buy {this.props.metadata.title} for credits?
+
+
+ Sorry, your download timed out :(
+
+
);
+ }
+}
+
+class Video extends React.Component {
+ constructor(props) {
+ super(props)
+
+ // TODO none of this mouse handling stuff seems to actually do anything?
+ this._controlsHideDelay = 3000 // Note: this needs to be shorter than the built-in delay in Electron, or Electron will hide the controls before us
+ this._controlsHideTimeout = null
+ this.state = {}
+ }
+ handleMouseMove() {
+ if (this._controlsTimeout) {
+ clearTimeout(this._controlsTimeout);
+ }
+
+ if (!this.state.controlsShown) {
+ this.setState({
+ controlsShown: true,
+ });
+ }
+ this._controlsTimeout = setTimeout(() => {
+ if (!this.isMounted) {
+ return;
+ }
+
+ this.setState({
+ controlsShown: false,
+ });
+ }, this._controlsHideDelay);
+ }
+
+ handleMouseLeave() {
+ if (this._controlsTimeout) {
+ clearTimeout(this._controlsTimeout);
+ }
+
+ if (this.state.controlsShown) {
+ this.setState({
+ controlsShown: false,
+ });
+ }
+ }
+
+ onWatchClick() {
+ this.props.watchVideo().then(() => {
+ if (!this.props.modal) {
+ this.setState({
+ isPlaying: true
+ })
+ }
+ })
+ }
+
+ startPlaying() {
+ this.setState({
+ isPlaying: true
+ })
+ }
+
+ render() {
+ const {
+ readyToPlay = false,
+ thumbnail,
+ metadata,
+ isLoading,
+ isDownloading,
+ fileInfo,
+ } = this.props
+ const {
+ isPlaying = false,
+ } = this.state
+
+ let loadStatusMessage = ''
+
+ if (isLoading) {
+ loadStatusMessage = "Requesting stream... it may sit here for like 15-20 seconds in a really awkward way... we're working on it"
+ } else if (isDownloading) {
+ loadStatusMessage = "Downloading stream... not long left now!"
+ }
+
+ return (
+ {
+ isPlaying ?
+ !readyToPlay ?
+
this is the world's worst loading screen and we shipped our software with it anyway... {loadStatusMessage} :
+
:
+
+
+
+ }
+ );
+ }
+}
+
+class VideoPlayer extends React.PureComponent {
+ componentDidMount() {
+ const elem = this.refs.video
+ const {
+ downloadPath,
+ } = this.props
+ const mediaFile = {
+ createReadStream: (opts) =>
+ fs.createReadStream(downloadPath, opts)
+ }
+ const videostream = VideoStream(mediaFile, elem)
+ elem.play()
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
+
+export default Video
diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js
deleted file mode 100644
index 8540e5469..000000000
--- a/ui/js/page/wallet.js
+++ /dev/null
@@ -1,317 +0,0 @@
-import React from 'react';
-import lbry from '../lbry.js';
-import {Link} from '../component/link.js';
-import Modal from '../component/modal.js';
-import {FormField, FormRow} from '../component/form.js';
-import {Address, BusyMessage, CreditAmount} from '../component/common.js';
-
-var AddressSection = React.createClass({
- _refreshAddress: function(event) {
- if (typeof event !== 'undefined') {
- event.preventDefault();
- }
-
- lbry.getUnusedAddress((address) => {
- window.localStorage.setItem('wallet_address', address);
- this.setState({
- address: address,
- });
- });
- },
-
- _getNewAddress: function(event) {
- if (typeof event !== 'undefined') {
- event.preventDefault();
- }
-
- lbry.wallet_new_address().then(function(address) {
- window.localStorage.setItem('wallet_address', address);
- this.setState({
- address: address,
- });
- }.bind(this))
- },
-
- getInitialState: function() {
- return {
- address: null,
- modal: null,
- }
- },
- componentWillMount: function() {
- var address = window.localStorage.getItem('wallet_address');
- if (address === null) {
- this._refreshAddress();
- } else {
- lbry.checkAddressIsMine(address, (isMine) => {
- if (isMine) {
- this.setState({
- address: address,
- });
- } else {
- this._refreshAddress();
- }
- });
- }
- },
- render: function() {
- return (
-
-
-
Wallet Address
-
-
-
-
-
-
-
-
Other LBRY users may send credits to you by entering this address on the "Send" page.
-
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.
-
-
-
- );
- }
-});
-
-var SendToAddressSection = React.createClass({
- handleSubmit: function(event) {
- if (typeof event !== 'undefined') {
- event.preventDefault();
- }
-
- if ((this.state.balance - this.state.amount) < 1)
- {
- this.setState({
- modal: 'insufficientBalance',
- });
- return;
- }
-
- this.setState({
- results: "",
- });
-
- lbry.sendToAddress(this.state.amount, this.state.address, (results) => {
- if(results === true)
- {
- this.setState({
- results: "Your transaction was successfully placed in the queue.",
- });
- }
- else
- {
- this.setState({
- results: "Something went wrong: " + results
- });
- }
- }, (error) => {
- this.setState({
- results: "Something went wrong: " + error.message
- })
- });
- },
- closeModal: function() {
- this.setState({
- modal: null,
- });
- },
- getInitialState: function() {
- return {
- address: "",
- amount: 0.0,
- balance: ,
- results: "",
- }
- },
- componentWillMount: function() {
- lbry.getBalance((results) => {
- this.setState({
- balance: results,
- });
- });
- },
- setAmount: function(event) {
- this.setState({
- amount: parseFloat(event.target.value),
- })
- },
- setAddress: function(event) {
- this.setState({
- address: event.target.value,
- })
- },
- render: function() {
- return (
-
-
-
- Insufficient balance: after this transaction you would have less than 1 LBC in your wallet.
-
-
- );
- }
-});
-
-
-var TransactionList = React.createClass({
- getInitialState: function() {
- return {
- transactionItems: null,
- }
- },
- componentWillMount: function() {
- lbry.call('get_transaction_history', {}, (results) => {
- if (results.length == 0) {
- this.setState({ transactionItems: [] })
- } else {
- var transactionItems = [],
- condensedTransactions = {};
- results.forEach(function(tx) {
- var txid = tx["txid"];
- if (!(txid in condensedTransactions)) {
- condensedTransactions[txid] = 0;
- }
- condensedTransactions[txid] += parseFloat(tx["value"]);
- });
- results.reverse().forEach(function(tx) {
- var txid = tx["txid"];
- if (condensedTransactions[txid] && condensedTransactions[txid] != 0)
- {
- transactionItems.push({
- id: txid,
- date: tx["timestamp"] ? (new Date(parseInt(tx["timestamp"]) * 1000)) : null,
- amount: condensedTransactions[txid]
- });
- delete condensedTransactions[txid];
- }
- });
-
- this.setState({ transactionItems: transactionItems });
- }
- });
- },
- render: function() {
- var rows = [];
- if (this.state.transactionItems && this.state.transactionItems.length > 0)
- {
- this.state.transactionItems.forEach(function(item) {
- rows.push(
-
- { (item.amount > 0 ? '+' : '' ) + item.amount }
- { item.date ? item.date.toLocaleDateString() : (Transaction pending) }
- { item.date ? item.date.toLocaleTimeString() : (Transaction pending) }
-
- {item.id.substr(0, 7)}
-
-
- );
- });
- }
- return (
-
-
-
Transaction History
-
-
- { this.state.transactionItems === null ?
: '' }
- { this.state.transactionItems && rows.length === 0 ?
You have no transactions.
: '' }
- { this.state.transactionItems && rows.length > 0 ?
-
-
-
- Amount
- Date
- Time
- Transaction
-
-
-
- {rows}
-
-
- : ''
- }
-
-
- );
- }
-});
-
-
-var WalletPage = React.createClass({
- _balanceSubscribeId: null,
-
- propTypes: {
- viewingPage: React.PropTypes.string,
- },
- componentDidMount: function() {
- document.title = "My Wallet";
- },
- /*
- Below should be refactored so that balance is shared all of wallet page. Or even broader?
- What is the proper React pattern for sharing a global state like balance?
- */
- getInitialState: function() {
- return {
- balance: null,
- }
- },
- componentWillMount: function() {
- this._balanceSubscribeId = lbry.balanceSubscribe((results) => {
- this.setState({
- balance: results,
- })
- });
- },
- componentWillUnmount: function() {
- if (this._balanceSubscribeId) {
- lbry.balanceUnsubscribe(this._balanceSubscribeId);
- }
- },
- render: function() {
- return (
-
-
-
-
Balance
-
-
- { this.state.balance === null ? : ''}
- { this.state.balance !== null ? : '' }
-
-
- { this.props.viewingPage === 'wallet' ? : '' }
- { this.props.viewingPage === 'send' ? : '' }
- { this.props.viewingPage === 'receive' ? : '' }
-
- );
- }
-});
-
-export default WalletPage;
diff --git a/ui/js/page/wallet/index.js b/ui/js/page/wallet/index.js
new file mode 100644
index 000000000..ac9095938
--- /dev/null
+++ b/ui/js/page/wallet/index.js
@@ -0,0 +1,53 @@
+import React from 'react'
+import {
+ connect
+} from 'react-redux'
+import {
+ doCloseModal,
+} from 'actions/app'
+import {
+ doGetNewAddress,
+ doCheckAddressIsMine,
+ doSendDraftTransaction,
+ doSetDraftTransactionAmount,
+ doSetDraftTransactionAddress,
+} from 'actions/wallet'
+import {
+ selectCurrentPage,
+ selectCurrentModal,
+} from 'selectors/app'
+import {
+ selectBalance,
+ selectTransactions,
+ selectTransactionItems,
+ selectIsFetchingTransactions,
+ selectReceiveAddress,
+ selectGettingNewAddress,
+ selectDraftTransactionAmount,
+ selectDraftTransactionAddress,
+} from 'selectors/wallet'
+import WalletPage from './view'
+
+const select = (state) => ({
+ currentPage: selectCurrentPage(state),
+ balance: selectBalance(state),
+ transactions: selectTransactions(state),
+ fetchingTransactions: selectIsFetchingTransactions(state),
+ transactionItems: selectTransactionItems(state),
+ receiveAddress: selectReceiveAddress(state),
+ gettingNewAddress: selectGettingNewAddress(state),
+ modal: selectCurrentModal(state),
+ address: selectDraftTransactionAddress(state),
+ amount: selectDraftTransactionAmount(state),
+})
+
+const perform = (dispatch) => ({
+ closeModal: () => dispatch(doCloseModal()),
+ getNewAddress: () => dispatch(doGetNewAddress()),
+ checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)),
+ sendToAddress: () => dispatch(doSendDraftTransaction()),
+ setAmount: (event) => dispatch(doSetDraftTransactionAmount(event.target.value)),
+ setAddress: (event) => dispatch(doSetDraftTransactionAddress(event.target.value)),
+})
+
+export default connect(select, perform)(WalletPage)
diff --git a/ui/js/page/wallet/view.jsx b/ui/js/page/wallet/view.jsx
new file mode 100644
index 000000000..4cfb4c19f
--- /dev/null
+++ b/ui/js/page/wallet/view.jsx
@@ -0,0 +1,265 @@
+import React from 'react';
+import lbry from 'lbry.js';
+import Link from 'component/link';
+import Modal from 'component/modal';
+import {
+ FormField,
+ FormRow
+} from 'component/form';
+import {
+ Address,
+ BusyMessage,
+ CreditAmount
+} from 'component/common';
+
+class AddressSection extends React.Component {
+ componentWillMount() {
+ this.props.checkAddressIsMine(this.props.receiveAddress)
+ }
+
+ render() {
+ const {
+ receiveAddress,
+ getNewAddress,
+ gettingNewAddress,
+ } = this.props
+
+ return (
+
+
+
Wallet Address
+
+
+
+
+
+
+
+
Other LBRY users may send credits to you by entering this address on the "Send" page.
+
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.
+
+
+
+ );
+ }
+}
+
+const SendToAddressSection = (props) => {
+ const {
+ sendToAddress,
+ closeModal,
+ modal,
+ setAmount,
+ setAddress,
+ amount,
+ address,
+ } = props
+
+ return (
+
+
+ {modal == 'insufficientBalance' &&
+ Insufficient balance: after this transaction you would have less than 1 LBC in your wallet.
+ }
+ {modal == 'transactionSuccessful' &&
+ Your transaction was successfully placed in the queue.
+ }
+ {modal == 'transactionFailed' &&
+ Something went wrong:
+ }
+
+ )
+}
+
+// var SendToAddressSection = React.createClass({
+// handleSubmit: function(event) {
+// if (typeof event !== 'undefined') {
+// event.preventDefault();
+// }
+
+// if ((this.state.balance - this.state.amount) < 1)
+// {
+// this.setState({
+// modal: 'insufficientBalance',
+// });
+// return;
+// }
+
+// this.setState({
+// results: "",
+// });
+
+// lbry.sendToAddress(this.state.amount, this.state.address, (results) => {
+// if(results === true)
+// {
+// this.setState({
+// results: "Your transaction was successfully placed in the queue.",
+// });
+// }
+// else
+// {
+// this.setState({
+// results: "Something went wrong: " + results
+// });
+// }
+// }, (error) => {
+// this.setState({
+// results: "Something went wrong: " + error.message
+// })
+// });
+// },
+// closeModal: function() {
+// this.setState({
+// modal: null,
+// });
+// },
+// getInitialState: function() {
+// return {
+// address: "",
+// amount: 0.0,
+// balance: ,
+// results: "",
+// }
+// },
+// componentWillMount: function() {
+// lbry.getBalance((results) => {
+// this.setState({
+// balance: results,
+// });
+// });
+// },
+// setAmount: function(event) {
+// this.setState({
+// amount: parseFloat(event.target.value),
+// })
+// },
+// setAddress: function(event) {
+// this.setState({
+// address: event.target.value,
+// })
+// },
+// render: function() {
+// return (
+//
+//
+//
+// Insufficient balance: after this transaction you would have less than 1 LBC in your wallet.
+//
+//
+// );
+// }
+// });
+
+const TransactionList = (props) => {
+ const {
+ fetchingTransactions,
+ transactionItems,
+ } = props
+
+ const rows = []
+ if (transactionItems.length > 0) {
+ transactionItems.forEach(function(item) {
+ rows.push(
+
+ { (item.amount > 0 ? '+' : '' ) + item.amount }
+ { item.date ? item.date.toLocaleDateString() : (Transaction pending) }
+ { item.date ? item.date.toLocaleTimeString() : (Transaction pending) }
+
+ {item.id.substr(0, 7)}
+
+
+ );
+ });
+ }
+
+ return (
+
+
+
Transaction History
+
+
+ { fetchingTransactions ?
: '' }
+ { !fetchingTransactions && rows.length === 0 ?
You have no transactions.
: '' }
+ { rows.length > 0 ?
+
+
+
+ Amount
+ Date
+ Time
+ Transaction
+
+
+
+ {rows}
+
+
+ : ''
+ }
+
+
+ )
+}
+
+const WalletPage = (props) => {
+ const {
+ balance,
+ currentPage
+ } = props
+
+ return (
+
+
+ { currentPage === 'wallet' ? : '' }
+ { currentPage === 'send' ? : '' }
+ { currentPage === 'receive' ? : '' }
+
+ )
+}
+
+export default WalletPage;
diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js
deleted file mode 100644
index 2f01ae93b..000000000
--- a/ui/js/page/watch.js
+++ /dev/null
@@ -1,215 +0,0 @@
-import React from 'react';
-import {Icon, Thumbnail, FilePrice} from '../component/common.js';
-import {Link} from '../component/link.js';
-import lbry from '../lbry.js';
-import Modal from '../component/modal.js';
-import lbryio from '../lbryio.js';
-import rewards from '../rewards.js';
-import LoadScreen from '../component/load_screen.js'
-
-const fs = require('fs');
-const VideoStream = require('videostream');
-
-export let WatchLink = React.createClass({
- propTypes: {
- uri: React.PropTypes.string,
- metadata: React.PropTypes.object,
- downloadStarted: React.PropTypes.bool,
- onGet: React.PropTypes.func,
- },
- getInitialState: function() {
- affirmedPurchase: false
- },
- play: function() {
- lbry.get({uri: this.props.uri}).then((streamInfo) => {
- if (streamInfo === null || typeof streamInfo !== 'object') {
- this.setState({
- modal: 'timedOut',
- attemptingDownload: false,
- });
- }
-
- lbryio.call('file', 'view', {
- uri: this.props.uri,
- outpoint: streamInfo.outpoint,
- claimId: streamInfo.claim_id
- }).catch(() => {})
- });
- if (this.props.onGet) {
- this.props.onGet()
- }
- },
- onWatchClick: function() {
- this.setState({
- loading: true
- });
- lbry.getCostInfo(this.props.uri).then(({cost}) => {
- lbry.getBalance((balance) => {
- if (cost > balance) {
- this.setState({
- modal: 'notEnoughCredits',
- attemptingDownload: false,
- });
- } else if (cost <= 0.01) {
- this.play()
- } else {
- lbry.file_list({outpoint: this.props.outpoint}).then((fileInfo) => {
- if (fileInfo) { // Already downloaded
- this.play();
- } else {
- this.setState({
- modal: 'affirmPurchase'
- });
- }
- });
- }
- });
- });
- },
- getInitialState: function() {
- return {
- modal: null,
- loading: false,
- };
- },
- closeModal: function() {
- this.setState({
- loading: false,
- modal: null,
- });
- },
- render: function() {
- return (
-
-
- You don't have enough LBRY credits to pay for this stream.
-
-
- Are you sure you'd like to buy {this.props.metadata.title} for credits?
-
-
);
- }
-});
-
-
-export let Video = React.createClass({
- _isMounted: false,
- _controlsHideDelay: 3000, // Note: this needs to be shorter than the built-in delay in Electron, or Electron will hide the controls before us
- _controlsHideTimeout: null,
-
- propTypes: {
- uri: React.PropTypes.string.isRequired,
- metadata: React.PropTypes.object,
- outpoint: React.PropTypes.string,
- },
- getInitialState: function() {
- return {
- downloadStarted: false,
- readyToPlay: false,
- isPlaying: false,
- isPurchased: false,
- loadStatusMessage: "Requesting stream... it may sit here for like 15-20 seconds in a really awkward way... we're working on it",
- mimeType: null,
- controlsShown: false,
- };
- },
- onGet: function() {
- lbry.get({uri: this.props.uri}).then((fileInfo) => {
- this.updateLoadStatus();
- });
- this.setState({
- isPlaying: true
- })
- },
- componentDidMount: function() {
- if (this.props.autoplay) {
- this.start()
- }
- },
- handleMouseMove: function() {
- if (this._controlsTimeout) {
- clearTimeout(this._controlsTimeout);
- }
-
- if (!this.state.controlsShown) {
- this.setState({
- controlsShown: true,
- });
- }
- this._controlsTimeout = setTimeout(() => {
- if (!this.isMounted) {
- return;
- }
-
- this.setState({
- controlsShown: false,
- });
- }, this._controlsHideDelay);
- },
- handleMouseLeave: function() {
- if (this._controlsTimeout) {
- clearTimeout(this._controlsTimeout);
- }
-
- if (this.state.controlsShown) {
- this.setState({
- controlsShown: false,
- });
- }
- },
- updateLoadStatus: function() {
- lbry.file_list({
- outpoint: this.props.outpoint,
- full_status: true,
- }).then(([status]) => {
- if (!status || status.written_bytes == 0) {
- // Download hasn't started yet, so update status message (if available) then try again
- // TODO: Would be nice to check if we have the MOOV before starting playing
- if (status) {
- this.setState({
- loadStatusMessage: status.message
- });
- }
- setTimeout(() => { this.updateLoadStatus() }, 250);
- } else {
- this.setState({
- readyToPlay: true,
- mimeType: status.mime_type,
- })
- const mediaFile = {
- createReadStream: function (opts) {
- // Return a readable stream that provides the bytes
- // between offsets "start" and "end" inclusive
- console.log('Stream between ' + opts.start + ' and ' + opts.end + '.');
- return fs.createReadStream(status.download_path, opts)
- }
- };
-
- rewards.claimNextPurchaseReward()
-
- var elem = this.refs.video;
- var videostream = VideoStream(mediaFile, elem);
- elem.play();
- }
- });
- },
- render: function() {
- return (
- {
- this.state.isPlaying ?
- !this.state.readyToPlay ?
-
this is the world's worst loading screen and we shipped our software with it anyway... {this.state.loadStatusMessage} :
-
:
-
-
-
- }
- );
- }
-})
diff --git a/ui/js/reducers/app.js b/ui/js/reducers/app.js
new file mode 100644
index 000000000..4fa7442ba
--- /dev/null
+++ b/ui/js/reducers/app.js
@@ -0,0 +1,112 @@
+import * as types from 'constants/action_types'
+import lbry from 'lbry'
+
+const reducers = {}
+const defaultState = {
+ isLoaded: false,
+ currentPath: 'discover',
+ platform: process.platform,
+ drawerOpen: sessionStorage.getItem('drawerOpen') || true,
+ upgradeSkipped: sessionStorage.getItem('upgradeSkipped'),
+ daemonReady: false,
+ platform: window.navigator.platform,
+ obscureNsfw: !lbry.getClientSetting('showNsfw'),
+ hidePrice: false,
+ hasSignature: false,
+}
+
+reducers[types.NAVIGATE] = function(state, action) {
+ return Object.assign({}, state, {
+ currentPath: action.data.path,
+ })
+}
+
+reducers[types.UPGRADE_CANCELLED] = function(state, action) {
+ return Object.assign({}, state, {
+ downloadProgress: null,
+ downloadComplete: false,
+ modal: null,
+ })
+}
+
+reducers[types.UPGRADE_DOWNLOAD_COMPLETED] = function(state, action) {
+ return Object.assign({}, state, {
+ downloadDir: action.data.dir,
+ downloadComplete: true,
+ })
+}
+
+reducers[types.UPGRADE_DOWNLOAD_STARTED] = function(state, action) {
+ return Object.assign({}, state, {
+ upgradeDownloading: true
+ })
+}
+
+reducers[types.UPGRADE_DOWNLOAD_COMPLETED] = function(state, action) {
+ return Object.assign({}, state, {
+ upgradeDownloading: false,
+ upgradeDownloadCompleted: true
+ })
+}
+
+reducers[types.SKIP_UPGRADE] = function(state, action) {
+ sessionStorage.setItem('upgradeSkipped', true);
+
+ return Object.assign({}, state, {
+ upgradeSkipped: true,
+ modal: null
+ })
+}
+
+reducers[types.UPDATE_VERSION] = function(state, action) {
+ return Object.assign({}, state, {
+ version: action.data.version
+ })
+}
+
+reducers[types.OPEN_MODAL] = function(state, action) {
+ return Object.assign({}, state, {
+ modal: action.data.modal,
+ extraContent: action.data.errorList
+ })
+}
+
+reducers[types.CLOSE_MODAL] = function(state, action) {
+ return Object.assign({}, state, {
+ modal: undefined,
+ extraContent: undefined
+ })
+}
+
+reducers[types.OPEN_DRAWER] = function(state, action) {
+ sessionStorage.setItem('drawerOpen', false)
+ return Object.assign({}, state, {
+ drawerOpen: true
+ })
+}
+
+reducers[types.CLOSE_DRAWER] = function(state, action) {
+ sessionStorage.setItem('drawerOpen', false)
+ return Object.assign({}, state, {
+ drawerOpen: false
+ })
+}
+
+reducers[types.UPGRADE_DOWNLOAD_PROGRESSED] = function(state, action) {
+ return Object.assign({}, state, {
+ downloadProgress: action.data.percent
+ })
+}
+
+reducers[types.DAEMON_READY] = function(state, action) {
+ window.sessionStorage.setItem('loaded', 'y')
+ return Object.assign({}, state, {
+ daemonReady: true
+ })
+}
+
+export default function reducer(state = defaultState, action) {
+ const handler = reducers[action.type];
+ if (handler) return handler(state, action);
+ return state;
+}
diff --git a/ui/js/reducers/availability.js b/ui/js/reducers/availability.js
new file mode 100644
index 000000000..6c0e28a8b
--- /dev/null
+++ b/ui/js/reducers/availability.js
@@ -0,0 +1,45 @@
+import * as types from 'constants/action_types'
+
+const reducers = {}
+const defaultState = {
+}
+
+reducers[types.FETCH_AVAILABILITY_STARTED] = function(state, action) {
+ const {
+ uri,
+ } = action.data
+ const newFetching = Object.assign({}, state.fetching)
+ const newByUri = Object.assign({}, newFetching.byUri)
+
+ newByUri[uri] = true
+ newFetching.byUri = newByUri
+
+ return Object.assign({}, state, {
+ fetching: newFetching,
+ })
+}
+
+reducers[types.FETCH_AVAILABILITY_COMPLETED] = function(state, action) {
+ const {
+ uri,
+ availability,
+ } = action.data
+ const newFetching = Object.assign({}, state.fetching)
+ const newFetchingByUri = Object.assign({}, newFetching.byUri)
+ const newAvailabilityByUri = Object.assign({}, state.byUri)
+
+ delete newFetchingByUri[uri]
+ newFetching.byUri = newFetchingByUri
+ newAvailabilityByUri[uri] = availability
+
+ return Object.assign({}, state, {
+ fetching: newFetching,
+ byUri: newAvailabilityByUri
+ })
+}
+
+export default function reducer(state = defaultState, action) {
+ const handler = reducers[action.type];
+ if (handler) return handler(state, action);
+ return state;
+}
diff --git a/ui/js/reducers/certificates.js b/ui/js/reducers/certificates.js
new file mode 100644
index 000000000..ebeaed986
--- /dev/null
+++ b/ui/js/reducers/certificates.js
@@ -0,0 +1,27 @@
+import * as types from 'constants/action_types'
+
+const reducers = {}
+const defaultState = {
+}
+
+reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
+ const {
+ uri,
+ certificate,
+ } = action.data
+ if (!certificate) return state
+
+ const newByUri = Object.assign({}, state.byUri)
+
+ newByUri[uri] = certificate
+ return Object.assign({}, state, {
+ byUri: newByUri,
+ })
+}
+
+
+export default function reducer(state = defaultState, action) {
+ const handler = reducers[action.type];
+ if (handler) return handler(state, action);
+ return state;
+}
diff --git a/ui/js/reducers/claims.js b/ui/js/reducers/claims.js
new file mode 100644
index 000000000..c758052bc
--- /dev/null
+++ b/ui/js/reducers/claims.js
@@ -0,0 +1,42 @@
+import * as types from 'constants/action_types'
+import lbryuri from 'lbryuri'
+
+const reducers = {}
+const defaultState = {
+}
+
+reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
+ const {
+ uri,
+ claim,
+ } = action.data
+ const newByUri = Object.assign({}, state.byUri)
+
+ newByUri[uri] = claim
+ return Object.assign({}, state, {
+ byUri: newByUri,
+ })
+}
+
+reducers[types.FETCH_MY_CLAIMS_COMPLETED] = function(state, action) {
+ const {
+ claims,
+ } = action.data
+ const newMine = Object.assign({}, state.mine)
+ const newById = Object.assign({}, newMine.byId)
+
+ claims.forEach(claim => {
+ newById[claim.claim_id] = claim
+ })
+ newMine.byId = newById
+
+ return Object.assign({}, state, {
+ mine: newMine,
+ })
+}
+
+export default function reducer(state = defaultState, action) {
+ const handler = reducers[action.type];
+ if (handler) return handler(state, action);
+ return state;
+}
diff --git a/ui/js/reducers/content.js b/ui/js/reducers/content.js
new file mode 100644
index 000000000..6c36daf0c
--- /dev/null
+++ b/ui/js/reducers/content.js
@@ -0,0 +1,103 @@
+import * as types from 'constants/action_types'
+
+const reducers = {}
+const defaultState = {
+}
+
+reducers[types.FETCH_FEATURED_CONTENT_STARTED] = function(state, action) {
+ return Object.assign({}, state, {
+ fetchingFeaturedContent: true
+ })
+}
+
+reducers[types.FETCH_FEATURED_CONTENT_COMPLETED] = function(state, action) {
+ const {
+ uris
+ } = action.data
+ const newFeaturedContent = Object.assign({}, state.featuredContent, {
+ byCategory: uris,
+ })
+
+ return Object.assign({}, state, {
+ fetchingFeaturedContent: false,
+ featuredContent: newFeaturedContent
+ })
+}
+
+reducers[types.RESOLVE_URI_STARTED] = function(state, action) {
+ const {
+ uri
+ } = action.data
+
+ const oldResolving = state.resolvingUris || []
+ const newResolving = Object.assign([], oldResolving)
+ if (newResolving.indexOf(uri) == -1) newResolving.push(uri)
+
+ return Object.assign({}, state, {
+ resolvingUris: newResolving
+ })
+}
+
+reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
+ const {
+ uri,
+ } = action.data
+ const resolvingUris = state.resolvingUris
+ const index = state.resolvingUris.indexOf(uri)
+ const newResolvingUris = [
+ ...resolvingUris.slice(0, index),
+ ...resolvingUris.slice(index + 1)
+ ]
+
+ const newState = Object.assign({}, state, {
+ resolvingUris: newResolvingUris,
+ })
+
+ return Object.assign({}, state, newState)
+}
+
+reducers[types.FETCH_DOWNLOADED_CONTENT_STARTED] = function(state, action) {
+ return Object.assign({}, state, {
+ fetchingDownloadedContent: true,
+ })
+}
+
+reducers[types.FETCH_DOWNLOADED_CONTENT_COMPLETED] = function(state, action) {
+ const {
+ fileInfos
+ } = action.data
+ const newDownloadedContent = Object.assign({}, state.downloadedContent, {
+ fileInfos
+ })
+
+ return Object.assign({}, state, {
+ downloadedContent: newDownloadedContent,
+ fetchingDownloadedContent: false,
+ })
+}
+
+reducers[types.FETCH_PUBLISHED_CONTENT_STARTED] = function(state, action) {
+ return Object.assign({}, state, {
+ fetchingPublishedContent: true,
+ })
+}
+
+reducers[types.FETCH_PUBLISHED_CONTENT_COMPLETED] = function(state, action) {
+ const {
+ fileInfos
+ } = action.data
+ const newPublishedContent = Object.assign({}, state.publishedContent, {
+ fileInfos
+ })
+
+ return Object.assign({}, state, {
+ publishedContent: newPublishedContent,
+ fetchingPublishedContent: false,
+ })
+}
+
+export default function reducer(state = defaultState, action) {
+ const handler = reducers[action.type];
+ if (handler) return handler(state, action);
+ return state;
+}
diff --git a/ui/js/reducers/cost_info.js b/ui/js/reducers/cost_info.js
new file mode 100644
index 000000000..eafcf4bf3
--- /dev/null
+++ b/ui/js/reducers/cost_info.js
@@ -0,0 +1,40 @@
+import * as types from 'constants/action_types'
+
+const reducers = {}
+const defaultState = {
+}
+
+reducers[types.FETCH_COST_INFO_STARTED] = function(state, action) {
+ const {
+ uri,
+ } = action.data
+ const newFetching = Object.assign({}, state.fetching)
+ newFetching[uri] = true
+
+ return Object.assign({}, state, {
+ fetching: newFetching,
+ })
+}
+
+reducers[types.FETCH_COST_INFO_COMPLETED] = function(state, action) {
+ const {
+ uri,
+ costInfo,
+ } = action.data
+ const newByUri = Object.assign({}, state.byUri)
+ const newFetching = Object.assign({}, state.fetching)
+
+ newByUri[uri] = costInfo
+ delete newFetching[uri]
+
+ return Object.assign({}, state, {
+ byUri: newByUri,
+ fetching: newFetching,
+ })
+}
+
+export default function reducer(state = defaultState, action) {
+ const handler = reducers[action.type];
+ if (handler) return handler(state, action);
+ return state;
+}
diff --git a/ui/js/reducers/file_info.js b/ui/js/reducers/file_info.js
new file mode 100644
index 000000000..292508a2b
--- /dev/null
+++ b/ui/js/reducers/file_info.js
@@ -0,0 +1,206 @@
+import * as types from 'constants/action_types'
+import lbryuri from 'lbryuri'
+
+const reducers = {}
+const defaultState = {
+}
+
+reducers[types.FETCH_FILE_INFO_STARTED] = function(state, action) {
+ const {
+ uri,
+ } = action.data
+ const newFetching = Object.assign({}, state.fetching)
+
+ newFetching[uri] = true
+
+ return Object.assign({}, state, {
+ fetching: newFetching,
+ })
+}
+
+reducers[types.FETCH_FILE_INFO_COMPLETED] = function(state, action) {
+ const {
+ uri,
+ fileInfo,
+ } = action.data
+ const newByUri = Object.assign({}, state.byUri)
+ const newFetching = Object.assign({}, state.fetching)
+
+ newByUri[uri] = fileInfo || {}
+ delete newFetching[uri]
+
+ return Object.assign({}, state, {
+ byUri: newByUri,
+ fetching: newFetching,
+ })
+}
+
+reducers[types.DOWNLOADING_STARTED] = function(state, action) {
+ const {
+ uri,
+ fileInfo,
+ } = action.data
+ const newByUri = Object.assign({}, state.byUri)
+ const newDownloading = Object.assign({}, state.downloading)
+ const newDownloadingByUri = Object.assign({}, newDownloading.byUri)
+ const newLoading = Object.assign({}, state.loading)
+ const newLoadingByUri = Object.assign({}, newLoading)
+
+ newDownloadingByUri[uri] = true
+ newDownloading.byUri = newDownloadingByUri
+ newByUri[uri] = fileInfo
+ delete newLoadingByUri[uri]
+ newLoading.byUri = newLoadingByUri
+
+ return Object.assign({}, state, {
+ downloading: newDownloading,
+ byUri: newByUri,
+ loading: newLoading,
+ })
+}
+
+reducers[types.DOWNLOADING_PROGRESSED] = function(state, action) {
+ const {
+ uri,
+ fileInfo,
+ } = action.data
+ const newByUri = Object.assign({}, state.byUri)
+ const newDownloading = Object.assign({}, state.downloading)
+
+ newByUri[uri] = fileInfo
+ newDownloading[uri] = true
+
+ return Object.assign({}, state, {
+ byUri: newByUri,
+ downloading: newDownloading
+ })
+}
+
+reducers[types.DOWNLOADING_COMPLETED] = function(state, action) {
+ const {
+ uri,
+ fileInfo,
+ } = action.data
+ const newByUri = Object.assign({}, state.byUri)
+ const newDownloading = Object.assign({}, state.downloading)
+ const newDownloadingByUri = Object.assign({}, newDownloading.byUri)
+
+ newByUri[uri] = fileInfo
+ delete newDownloadingByUri[uri]
+ newDownloading.byUri = newDownloadingByUri
+
+ return Object.assign({}, state, {
+ byUri: newByUri,
+ downloading: newDownloading,
+ })
+}
+
+reducers[types.DELETE_FILE_STARTED] = function(state, action) {
+ const {
+ uri,
+ } = action.data
+ const newDeleting = Object.assign({}, state.deleting)
+ const newByUri = Object.assign({}, newDeleting.byUri)
+
+ newByUri[uri] = true
+ newDeleting.byUri = newByUri
+
+ return Object.assign({}, state, {
+ deleting: newDeleting,
+ })
+}
+
+reducers[types.DELETE_FILE_COMPLETED] = function(state, action) {
+ const {
+ uri,
+ } = action.data
+ const newDeleting = Object.assign({}, state.deleting)
+ const newDeletingByUri = Object.assign({}, newDeleting.byUri)
+ const newByUri = Object.assign({}, state.byUri)
+
+ delete newDeletingByUri[uri]
+ newDeleting.byUri = newDeletingByUri
+ delete newByUri[uri]
+
+ return Object.assign({}, state, {
+ deleting: newDeleting,
+ byUri: newByUri,
+ })
+}
+
+reducers[types.LOADING_VIDEO_STARTED] = function(state, action) {
+ const {
+ uri,
+ } = action.data
+ const newLoading = Object.assign({}, state.loading)
+ const newByUri = Object.assign({}, newLoading.byUri)
+
+ newByUri[uri] = true
+ newLoading.byUri = newByUri
+
+ return Object.assign({}, state, {
+ loading: newLoading,
+ })
+}
+
+reducers[types.LOADING_VIDEO_FAILED] = function(state, action) {
+ const {
+ uri,
+ } = action.data
+ const newLoading = Object.assign({}, state.loading)
+ const newByUri = Object.assign({}, newLoading.byUri)
+
+ delete newByUri[uri]
+ newLoading.byUri = newByUri
+
+ return Object.assign({}, state, {
+ loading: newLoading,
+ })
+}
+
+reducers[types.FETCH_DOWNLOADED_CONTENT_COMPLETED] = function(state, action) {
+ const {
+ fileInfos,
+ } = action.data
+ const newByUri = Object.assign({}, state.byUri)
+
+ fileInfos.forEach(fileInfo => {
+ const uri = lbryuri.build({
+ channelName: fileInfo.channel_name,
+ contentName: fileInfo.name,
+ })
+
+ newByUri[uri] = fileInfo
+ })
+
+ return Object.assign({}, state, {
+ byUri: newByUri
+ })
+}
+
+reducers[types.FETCH_PUBLISHED_CONTENT_COMPLETED] = function(state, action) {
+ const {
+ fileInfos
+ } = action.data
+ const newByUri = Object.assign({}, state.byUri)
+
+ fileInfos.forEach(fileInfo => {
+ const uri = lbryuri.build({
+ channelName: fileInfo.channel_name,
+ contentName: fileInfo.name,
+ })
+
+ newByUri[uri] = fileInfo
+ })
+
+ return Object.assign({}, state, {
+ byUri: newByUri
+ })
+}
+
+
+export default function reducer(state = defaultState, action) {
+ const handler = reducers[action.type];
+ if (handler) return handler(state, action);
+ return state;
+}
diff --git a/ui/js/reducers/rewards.js b/ui/js/reducers/rewards.js
new file mode 100644
index 000000000..db20378e2
--- /dev/null
+++ b/ui/js/reducers/rewards.js
@@ -0,0 +1,11 @@
+import * as types from 'constants/action_types'
+
+const reducers = {}
+const defaultState = {
+}
+
+export default function reducer(state = defaultState, action) {
+ const handler = reducers[action.type];
+ if (handler) return handler(state, action);
+ return state;
+}
diff --git a/ui/js/reducers/search.js b/ui/js/reducers/search.js
new file mode 100644
index 000000000..24cd757f6
--- /dev/null
+++ b/ui/js/reducers/search.js
@@ -0,0 +1,60 @@
+import * as types from 'constants/action_types'
+import lbryuri from 'lbryuri'
+
+const reducers = {}
+const defaultState = {
+}
+
+reducers[types.SEARCH_STARTED] = function(state, action) {
+ const {
+ query,
+ } = action.data
+
+ return Object.assign({}, state, {
+ searching: true,
+ query: query,
+ })
+}
+
+reducers[types.SEARCH_COMPLETED] = function(state, action) {
+ const {
+ query,
+ results,
+ } = action.data
+ const oldResults = Object.assign({}, state.results)
+ const newByQuery = Object.assign({}, oldResults.byQuery)
+ newByQuery[query] = results
+ const newResults = Object.assign({}, oldResults, {
+ byQuery: newByQuery
+ })
+
+ return Object.assign({}, state, {
+ searching: false,
+ results: newResults,
+ })
+}
+
+reducers[types.SEARCH_CANCELLED] = function(state, action) {
+ return Object.assign({}, state, {
+ searching: false,
+ query: undefined,
+ })
+}
+
+reducers[types.ACTIVATE_SEARCH] = function(state, action) {
+ return Object.assign({}, state, {
+ activated: true,
+ })
+}
+
+reducers[types.DEACTIVATE_SEARCH] = function(state, action) {
+ return Object.assign({}, state, {
+ activated: false,
+ })
+}
+
+export default function reducer(state = defaultState, action) {
+ const handler = reducers[action.type];
+ if (handler) return handler(state, action);
+ return state;
+}
diff --git a/ui/js/reducers/wallet.js b/ui/js/reducers/wallet.js
new file mode 100644
index 000000000..02cfe2485
--- /dev/null
+++ b/ui/js/reducers/wallet.js
@@ -0,0 +1,131 @@
+import * as types from 'constants/action_types'
+
+const reducers = {}
+const address = sessionStorage.getItem('receiveAddress')
+const buildDraftTransaction = () => ({
+ amount: undefined,
+ address: undefined
+})
+
+const defaultState = {
+ balance: 0,
+ transactions: [],
+ fetchingTransactions: false,
+ receiveAddress: address,
+ gettingNewAddress: false,
+ draftTransaction: buildDraftTransaction()
+}
+
+reducers[types.FETCH_TRANSACTIONS_STARTED] = function(state, action) {
+ return Object.assign({}, state, {
+ fetchingTransactions: true
+ })
+}
+
+reducers[types.FETCH_TRANSACTIONS_COMPLETED] = function(state, action) {
+ const oldTransactions = Object.assign({}, state.transactions)
+ const byId = Object.assign({}, oldTransactions.byId)
+ const { transactions } = action.data
+
+ transactions.forEach((transaction) => {
+ byId[transaction.txid] = transaction
+ })
+
+ const newTransactions = Object.assign({}, oldTransactions, {
+ byId: byId
+ })
+
+ return Object.assign({}, state, {
+ transactions: newTransactions,
+ fetchingTransactions: false
+ })
+}
+
+reducers[types.GET_NEW_ADDRESS_STARTED] = function(state, action) {
+ return Object.assign({}, state, {
+ gettingNewAddress: true
+ })
+}
+
+reducers[types.GET_NEW_ADDRESS_COMPLETED] = function(state, action) {
+ const { address } = action.data
+
+ sessionStorage.setItem('receiveAddress', address)
+ return Object.assign({}, state, {
+ gettingNewAddress: false,
+ receiveAddress: address
+ })
+}
+
+reducers[types.UPDATE_BALANCE] = function(state, action) {
+ return Object.assign({}, state, {
+ balance: action.data.balance
+ })
+}
+
+reducers[types.CHECK_ADDRESS_IS_MINE_STARTED] = function(state, action) {
+ return Object.assign({}, state, {
+ checkingAddressOwnership: true
+ })
+}
+
+reducers[types.CHECK_ADDRESS_IS_MINE_COMPLETED] = function(state, action) {
+ return Object.assign({}, state, {
+ checkingAddressOwnership: false
+ })
+}
+
+reducers[types.SET_DRAFT_TRANSACTION_AMOUNT] = function(state, action) {
+ const oldDraft = state.draftTransaction
+ const newDraft = Object.assign({}, oldDraft, {
+ amount: parseFloat(action.data.amount)
+ })
+
+ return Object.assign({}, state, {
+ draftTransaction: newDraft
+ })
+}
+
+reducers[types.SET_DRAFT_TRANSACTION_ADDRESS] = function(state, action) {
+ const oldDraft = state.draftTransaction
+ const newDraft = Object.assign({}, oldDraft, {
+ address: action.data.address
+ })
+
+ return Object.assign({}, state, {
+ draftTransaction: newDraft
+ })
+}
+
+reducers[types.SEND_TRANSACTION_STARTED] = function(state, action) {
+ const newDraftTransaction = Object.assign({}, state.draftTransaction, {
+ sending: true
+ })
+
+ return Object.assign({}, state, {
+ draftTransaction: newDraftTransaction
+ })
+}
+
+reducers[types.SEND_TRANSACTION_COMPLETED] = function(state, action) {
+ return Object.assign({}, state, {
+ draftTransaction: buildDraftTransaction()
+ })
+}
+
+reducers[types.SEND_TRANSACTION_FAILED] = function(state, action) {
+ const newDraftTransaction = Object.assign({}, state.draftTransaction, {
+ sending: false,
+ error: action.data.error
+ })
+
+ return Object.assign({}, state, {
+ draftTransaction: newDraftTransaction
+ })
+}
+
+export default function reducer(state = defaultState, action) {
+ const handler = reducers[action.type];
+ if (handler) return handler(state, action);
+ return state;
+}
diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js
new file mode 100644
index 000000000..a269e98f2
--- /dev/null
+++ b/ui/js/selectors/app.js
@@ -0,0 +1,196 @@
+import { createSelector } from 'reselect'
+
+export const _selectState = state => state.app || {}
+
+export const selectIsLoaded = createSelector(
+ _selectState,
+ (state) => state.isLoaded
+)
+
+export const selectCurrentPath = createSelector(
+ _selectState,
+ (state) => state.currentPath
+)
+
+export const selectCurrentPage = createSelector(
+ selectCurrentPath,
+ (path) => path.split('=')[0]
+)
+
+export const selectCurrentUri = createSelector(
+ selectCurrentPath,
+ (path) => {
+ if (path.match(/=/)) {
+ return path.split('=')[1]
+ } else {
+ return undefined
+ }
+ }
+)
+
+export const selectPageTitle = createSelector(
+ selectCurrentPage,
+ selectCurrentUri,
+ (page, uri) => {
+ switch(page)
+ {
+ case 'discover':
+ return 'Discover'
+ case 'wallet':
+ case 'send':
+ case 'receive':
+ case 'claim':
+ case 'referral':
+ return 'Wallet'
+ case 'downloaded':
+ return 'My Files'
+ case 'published':
+ return 'My Files'
+ case 'publish':
+ return 'Publish'
+ case 'help':
+ return 'Help'
+ default:
+ return 'LBRY';
+ }
+ }
+)
+
+export const selectPlatform = createSelector(
+ _selectState,
+ (state) => state.platform
+)
+
+export const selectUpdateUrl = createSelector(
+ selectPlatform,
+ (platform) => {
+ switch (platform) {
+ case 'darwin':
+ return 'https://lbry.io/get/lbry.dmg';
+ case 'linux':
+ return 'https://lbry.io/get/lbry.deb';
+ case 'win32':
+ return 'https://lbry.io/get/lbry.exe';
+ default:
+ throw 'Unknown platform';
+ }
+ }
+)
+
+export const selectVersion = createSelector(
+ _selectState,
+ (state) => {
+ return state.version
+ }
+)
+
+export const selectUpgradeFilename = createSelector(
+ selectPlatform,
+ selectVersion,
+ (platform, version) => {
+ switch (platform) {
+ case 'darwin':
+ return `LBRY-${version}.dmg`;
+ case 'linux':
+ return `LBRY_${version}_amd64.deb`;
+ case 'windows':
+ return `LBRY.Setup.${version}.exe`;
+ default:
+ throw 'Unknown platform';
+ }
+ }
+)
+
+export const selectCurrentModal = createSelector(
+ _selectState,
+ (state) => state.modal
+)
+
+export const selectDownloadProgress = createSelector(
+ _selectState,
+ (state) => state.downloadProgress
+)
+
+export const selectDownloadComplete = createSelector(
+ _selectState,
+ (state) => state.upgradeDownloadCompleted
+)
+
+export const selectDrawerOpen = createSelector(
+ _selectState,
+ (state) => state.drawerOpen
+)
+
+export const selectHeaderLinks = createSelector(
+ selectCurrentPage,
+ (page) => {
+ switch(page)
+ {
+ case 'wallet':
+ case 'send':
+ case 'receive':
+ case 'claim':
+ case 'referral':
+ return {
+ 'wallet' : 'Overview',
+ 'send' : 'Send',
+ 'receive' : 'Receive',
+ 'claim' : 'Claim Beta Code',
+ 'referral' : 'Check Referral Credit',
+ };
+ case 'downloaded':
+ case 'published':
+ return {
+ 'downloaded': 'Downloaded',
+ 'published': 'Published',
+ };
+ default:
+ return null;
+ }
+ }
+)
+
+export const selectUpgradeSkipped = createSelector(
+ _selectState,
+ (state) => state.upgradeSkipped
+)
+
+export const selectUpgradeDownloadDir = createSelector(
+ _selectState,
+ (state) => state.downloadDir
+)
+
+export const selectUpgradeDownloadItem = createSelector(
+ _selectState,
+ (state) => state.downloadItem
+)
+
+export const selectSearchTerm = createSelector(
+ _selectState,
+ (state) => state.searchTerm
+)
+
+export const selectError = createSelector(
+ _selectState,
+ (state) => state.error
+)
+
+export const selectDaemonReady = createSelector(
+ _selectState,
+ (state) => state.daemonReady
+)
+
+export const selectObscureNsfw = createSelector(
+ _selectState,
+ (state) => !!state.obscureNsfw
+)
+
+export const selectHidePrice = createSelector(
+ _selectState,
+ (state) => !!state.hidePrice
+)
+
+export const selectHasSignature = createSelector(
+ _selectState,
+ (state) => !!state.hasSignature
+)
diff --git a/ui/js/selectors/availability.js b/ui/js/selectors/availability.js
new file mode 100644
index 000000000..7aeaa6330
--- /dev/null
+++ b/ui/js/selectors/availability.js
@@ -0,0 +1,74 @@
+import {
+ createSelector,
+} from 'reselect'
+import {
+ selectDaemonReady,
+ selectCurrentPage,
+ selectCurrentUri,
+} from 'selectors/app'
+
+const _selectState = state => state.availability
+
+export const selectAvailabilityByUri = createSelector(
+ _selectState,
+ (state) => state.byUri || {}
+)
+
+export const selectFetchingAvailability = createSelector(
+ _selectState,
+ (state) => state.fetching || {}
+)
+
+export const selectFetchingAvailabilityByUri = createSelector(
+ selectFetchingAvailability,
+ (fetching) => fetching.byUri || {}
+)
+
+const selectAvailabilityForUri = (state, props) => {
+ return selectAvailabilityByUri(state)[props.uri]
+}
+
+export const makeSelectAvailabilityForUri = () => {
+ return createSelector(
+ selectAvailabilityForUri,
+ (availability) => availability
+ )
+}
+
+const selectFetchingAvailabilityForUri = (state, props) => {
+ return selectFetchingAvailabilityByUri(state)[props.uri]
+}
+
+export const makeSelectFetchingAvailabilityForUri = () => {
+ return createSelector(
+ selectFetchingAvailabilityForUri,
+ (fetching) => fetching
+ )
+}
+
+export const selectFetchingAvailabilityForCurrentUri = createSelector(
+ selectCurrentUri,
+ selectFetchingAvailabilityByUri,
+ (uri, byUri) => byUri[uri]
+)
+
+export const selectAvailabilityForCurrentUri = createSelector(
+ selectCurrentUri,
+ selectAvailabilityByUri,
+ (uri, byUri) => byUri[uri]
+)
+
+export const shouldFetchCurrentUriAvailability = createSelector(
+ selectDaemonReady,
+ selectCurrentPage,
+ selectFetchingAvailabilityForCurrentUri,
+ selectAvailabilityForCurrentUri,
+ (daemonReady, page, fetching, availability) => {
+ if (!daemonReady) return false
+ if (page != 'show') return false
+ if (fetching) return false
+ if (availability) return false
+
+ return true
+ }
+)
diff --git a/ui/js/selectors/claims.js b/ui/js/selectors/claims.js
new file mode 100644
index 000000000..45007b94e
--- /dev/null
+++ b/ui/js/selectors/claims.js
@@ -0,0 +1,89 @@
+import {
+ createSelector,
+} from 'reselect'
+import lbryuri from 'lbryuri'
+import {
+ selectCurrentUri,
+} from 'selectors/app'
+
+export const _selectState = state => state.claims || {}
+
+export const selectClaimsByUri = createSelector(
+ _selectState,
+ (state) => state.byUri || {}
+)
+
+export const selectCurrentUriClaim = createSelector(
+ selectCurrentUri,
+ selectClaimsByUri,
+ (uri, byUri) => byUri[uri] || {}
+)
+
+export const selectCurrentUriClaimOutpoint = createSelector(
+ selectCurrentUriClaim,
+ (claim) => `${claim.txid}:${claim.nout}`
+)
+
+const selectClaimForUri = (state, props) => {
+ const uri = lbryuri.normalize(props.uri)
+ return selectClaimsByUri(state)[uri]
+}
+
+export const makeSelectClaimForUri = () => {
+ return createSelector(
+ selectClaimForUri,
+ (claim) => claim
+ )
+}
+
+const selectMetadataForUri = (state, props) => {
+ const claim = selectClaimForUri(state, props)
+ const metadata = claim && claim.value && claim.value.stream && claim.value.stream.metadata
+
+ return metadata ? metadata : undefined
+}
+
+export const makeSelectMetadataForUri = () => {
+ return createSelector(
+ selectMetadataForUri,
+ (metadata) => metadata
+ )
+}
+
+const selectSourceForUri = (state, props) => {
+ const claim = selectClaimForUri(state, props)
+ const source = claim && claim.value && claim.value.stream && claim.value.stream.source
+
+ return source ? source : undefined
+}
+
+export const makeSelectSourceForUri = () => {
+ return createSelector(
+ selectSourceForUri,
+ (source) => source
+ )
+}
+
+export const selectMyClaims = createSelector(
+ _selectState,
+ (state) => state.mine || {}
+)
+
+export const selectMyClaimsById = createSelector(
+ selectMyClaims,
+ (mine) => mine.byId || {}
+)
+
+export const selectMyClaimsOutpoints = createSelector(
+ selectMyClaimsById,
+ (byId) => {
+ const outpoints = []
+ Object.keys(byId).forEach(key => {
+ const claim = byId[key]
+ const outpoint = `${claim.txid}:${claim.nout}`
+ outpoints.push(outpoint)
+ })
+
+ return outpoints
+ }
+)
diff --git a/ui/js/selectors/content.js b/ui/js/selectors/content.js
new file mode 100644
index 000000000..be38c21b3
--- /dev/null
+++ b/ui/js/selectors/content.js
@@ -0,0 +1,98 @@
+import { createSelector } from 'reselect'
+import {
+ selectDaemonReady,
+ selectCurrentPage,
+ selectCurrentUri,
+} from 'selectors/app'
+
+export const _selectState = state => state.content || {}
+
+export const selectFeaturedContent = createSelector(
+ _selectState,
+ (state) => state.featuredContent || {}
+)
+
+export const selectFeaturedContentByCategory = createSelector(
+ selectFeaturedContent,
+ (featuredContent) => featuredContent.byCategory || {}
+)
+
+export const selectFetchingFeaturedContent = createSelector(
+ _selectState,
+ (state) => !!state.fetchingFeaturedContent
+)
+
+export const shouldFetchFeaturedContent = createSelector(
+ selectDaemonReady,
+ selectCurrentPage,
+ selectFetchingFeaturedContent,
+ selectFeaturedContentByCategory,
+ (daemonReady, page, fetching, byCategory) => {
+ if (!daemonReady) return false
+ if (page != 'discover') return false
+ if (fetching) return false
+ if (Object.keys(byCategory).length != 0) return false
+
+ return true
+ }
+)
+
+export const selectFetchingFileInfos = createSelector(
+ _selectState,
+ (state) => state.fetchingFileInfos || {}
+)
+
+export const selectFetchingDownloadedContent = createSelector(
+ _selectState,
+ (state) => !!state.fetchingDownloadedContent
+)
+
+export const selectDownloadedContent = createSelector(
+ _selectState,
+ (state) => state.downloadedContent || {}
+)
+
+export const selectDownloadedContentFileInfos = createSelector(
+ selectDownloadedContent,
+ (downloadedContent) => downloadedContent.fileInfos || []
+)
+
+export const shouldFetchDownloadedContent = createSelector(
+ selectDaemonReady,
+ selectCurrentPage,
+ selectFetchingDownloadedContent,
+ selectDownloadedContent,
+ (daemonReady, page, fetching, content) => {
+ if (!daemonReady) return false
+ if (page != 'downloaded') return false
+ if (fetching) return false
+ if (Object.keys(content).length != 0) return false
+
+ return true
+ }
+)
+
+export const selectFetchingPublishedContent = createSelector(
+ _selectState,
+ (state) => !!state.fetchingPublishedContent
+)
+
+export const selectPublishedContent = createSelector(
+ _selectState,
+ (state) => state.publishedContent || {}
+)
+
+export const shouldFetchPublishedContent = createSelector(
+ selectDaemonReady,
+ selectCurrentPage,
+ selectFetchingPublishedContent,
+ selectPublishedContent,
+ (daemonReady, page, fetching, content) => {
+ if (!daemonReady) return false
+ if (page != 'published') return false
+ if (fetching) return false
+ if (Object.keys(content).length != 0) return false
+
+ return true
+ }
+)
diff --git a/ui/js/selectors/cost_info.js b/ui/js/selectors/cost_info.js
new file mode 100644
index 000000000..d250aaf19
--- /dev/null
+++ b/ui/js/selectors/cost_info.js
@@ -0,0 +1,54 @@
+import { createSelector } from 'reselect'
+import {
+ selectCurrentUri,
+ selectCurrentPage,
+} from 'selectors/app'
+
+export const _selectState = state => state.costInfo || {}
+
+export const selectAllCostInfoByUri = createSelector(
+ _selectState,
+ (state) => state.byUri || {}
+)
+
+export const selectCurrentUriCostInfo = createSelector(
+ selectCurrentUri,
+ selectAllCostInfoByUri,
+ (uri, byUri) => byUri[uri] || {}
+)
+
+export const selectFetchingCostInfo = createSelector(
+ _selectState,
+ (state) => state.fetching || {}
+)
+
+export const selectFetchingCurrentUriCostInfo = createSelector(
+ selectCurrentUri,
+ selectFetchingCostInfo,
+ (uri, byUri) => !!byUri[uri]
+)
+
+export const shouldFetchCurrentUriCostInfo = createSelector(
+ selectCurrentPage,
+ selectCurrentUri,
+ selectFetchingCurrentUriCostInfo,
+ selectCurrentUriCostInfo,
+ (page, uri, fetching, costInfo) => {
+ if (page != 'show') return false
+ if (fetching) return false
+ if (Object.keys(costInfo).length != 0) return false
+
+ return true
+ }
+)
+
+const selectCostInfoForUri = (state, props) => {
+ return selectAllCostInfoByUri(state)[props.uri]
+}
+
+export const makeSelectCostInfoForUri = () => {
+ return createSelector(
+ selectCostInfoForUri,
+ (costInfo) => costInfo
+ )
+}
diff --git a/ui/js/selectors/file_info.js b/ui/js/selectors/file_info.js
new file mode 100644
index 000000000..7c9439495
--- /dev/null
+++ b/ui/js/selectors/file_info.js
@@ -0,0 +1,172 @@
+import {
+ createSelector,
+} from 'reselect'
+import {
+ selectCurrentUri,
+ selectCurrentPage,
+} from 'selectors/app'
+import {
+ selectMyClaimsOutpoints,
+} from 'selectors/claims'
+
+export const _selectState = state => state.fileInfo || {}
+
+export const selectAllFileInfoByUri = createSelector(
+ _selectState,
+ (state) => state.byUri || {}
+)
+
+export const selectCurrentUriRawFileInfo = createSelector(
+ selectCurrentUri,
+ selectAllFileInfoByUri,
+ (uri, byUri) => byUri[uri]
+)
+
+export const selectCurrentUriFileInfo = createSelector(
+ selectCurrentUriRawFileInfo,
+ (fileInfo) => fileInfo
+)
+
+export const selectFetchingFileInfo = createSelector(
+ _selectState,
+ (state) => state.fetching || {}
+)
+
+export const selectFetchingCurrentUriFileInfo = createSelector(
+ selectCurrentUri,
+ selectFetchingFileInfo,
+ (uri, byUri) => !!byUri[uri]
+)
+
+export const selectDownloading = createSelector(
+ _selectState,
+ (state) => state.downloading || {}
+)
+
+export const selectDownloadingByUri = createSelector(
+ selectDownloading,
+ (downloading) => downloading.byUri || {}
+)
+
+export const selectDownloadingCurrentUri = createSelector(
+ selectCurrentUri,
+ selectDownloadingByUri,
+ (uri, byUri) => !!byUri[uri]
+)
+
+export const selectCurrentUriIsDownloaded = createSelector(
+ selectCurrentUriFileInfo,
+ (fileInfo) => {
+ if (!fileInfo) return false
+ if (!fileInfo.completed) return false
+ if (!fileInfo.written_bytes > 0) return false
+
+ return true
+ }
+)
+
+export const shouldFetchCurrentUriFileInfo = createSelector(
+ selectCurrentPage,
+ selectCurrentUri,
+ selectFetchingCurrentUriFileInfo,
+ selectCurrentUriFileInfo,
+ (page, uri, fetching, fileInfo) => {
+ if (page != 'show') return false
+ if (fetching) return false
+ if (fileInfo != undefined) return false
+
+ return true
+ }
+)
+
+const selectFileInfoForUri = (state, props) => {
+ return selectAllFileInfoByUri(state)[props.uri]
+}
+
+export const makeSelectFileInfoForUri = () => {
+ return createSelector(
+ selectFileInfoForUri,
+ (fileInfo) => fileInfo
+ )
+}
+
+const selectDownloadingForUri = (state, props) => {
+ const byUri = selectDownloadingByUri(state)
+ return byUri[props.uri]
+}
+
+export const makeSelectDownloadingForUri = () => {
+ return createSelector(
+ selectDownloadingForUri,
+ (downloadingForUri) => !!downloadingForUri
+ )
+}
+
+export const selectLoading = createSelector(
+ _selectState,
+ (state) => state.loading || {}
+)
+
+export const selectLoadingByUri = createSelector(
+ selectLoading,
+ (loading) => loading.byUri || {}
+)
+
+export const selectLoadingCurrentUri = createSelector(
+ selectLoadingByUri,
+ selectCurrentUri,
+ (byUri, uri) => !!byUri[uri]
+)
+
+// TODO make this smarter so it doesn't start playing and immediately freeze
+// while downloading more.
+export const selectCurrentUriFileReadyToPlay = createSelector(
+ selectCurrentUriFileInfo,
+ (fileInfo) => (fileInfo || {}).written_bytes > 0
+)
+
+const selectLoadingForUri = (state, props) => {
+ const byUri = selectLoadingByUri(state)
+ return byUri[props.uri]
+}
+
+export const makeSelectLoadingForUri = () => {
+ return createSelector(
+ selectLoadingForUri,
+ (loading) => !!loading
+ )
+}
+
+export const selectDownloadedFileInfo = createSelector(
+ selectAllFileInfoByUri,
+ (byUri) => {
+ const fileInfoList = []
+ Object.keys(byUri).forEach(key => {
+ const fileInfo = byUri[key]
+
+ if (fileInfo.completed || fileInfo.written_bytes) {
+ fileInfoList.push(fileInfo)
+ }
+ })
+
+ return fileInfoList
+ }
+)
+
+export const selectPublishedFileInfo = createSelector(
+ selectAllFileInfoByUri,
+ selectMyClaimsOutpoints,
+ (byUri, outpoints) => {
+ const fileInfos = []
+ outpoints.forEach(outpoint => {
+ Object.keys(byUri).forEach(key => {
+ const fileInfo = byUri[key]
+ if (fileInfo.outpoint == outpoint) {
+ fileInfos.push(fileInfo)
+ }
+ })
+ })
+
+ return fileInfos
+ }
+)
diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js
new file mode 100644
index 000000000..30f8ecbf7
--- /dev/null
+++ b/ui/js/selectors/rewards.js
@@ -0,0 +1,3 @@
+import { createSelector } from 'reselect'
+
+export const _selectState = state => state.rewards || {}
diff --git a/ui/js/selectors/search.js b/ui/js/selectors/search.js
new file mode 100644
index 000000000..b5f7bc1a4
--- /dev/null
+++ b/ui/js/selectors/search.js
@@ -0,0 +1,34 @@
+import { createSelector } from 'reselect'
+
+export const _selectState = state => state.search || {}
+
+export const selectSearchQuery = createSelector(
+ _selectState,
+ (state) => state.query
+)
+
+export const selectIsSearching = createSelector(
+ _selectState,
+ (state) => !!state.searching
+)
+
+export const selectSearchResults = createSelector(
+ _selectState,
+ (state) => state.results || {}
+)
+
+export const selectSearchResultsByQuery = createSelector(
+ selectSearchResults,
+ (results) => results.byQuery || {}
+)
+
+export const selectCurrentSearchResults = createSelector(
+ selectSearchQuery,
+ selectSearchResultsByQuery,
+ (query, byQuery) => byQuery[query] || []
+)
+
+export const selectSearchActivated = createSelector(
+ _selectState,
+ (state) => !!state.activated
+)
diff --git a/ui/js/selectors/wallet.js b/ui/js/selectors/wallet.js
new file mode 100644
index 000000000..eedaffa3e
--- /dev/null
+++ b/ui/js/selectors/wallet.js
@@ -0,0 +1,109 @@
+import { createSelector } from 'reselect'
+import {
+ selectCurrentPage,
+ selectDaemonReady,
+} from 'selectors/app'
+
+export const _selectState = state => state.wallet || {}
+
+export const selectBalance = createSelector(
+ _selectState,
+ (state) => state.balance || 0
+)
+
+export const selectTransactions = createSelector(
+ _selectState,
+ (state) => state.transactions || {}
+)
+
+export const selectTransactionsById = createSelector(
+ selectTransactions,
+ (transactions) => transactions.byId || {}
+)
+
+export const selectTransactionItems = createSelector(
+ selectTransactionsById,
+ (byId) => {
+ const transactionItems = []
+ const txids = Object.keys(byId)
+ txids.forEach((txid) => {
+ const tx = byId[txid]
+ transactionItems.push({
+ id: txid,
+ date: tx.timestamp ? (new Date(parseInt(tx.timestamp) * 1000)) : null,
+ amount: parseFloat(tx.value)
+ })
+ })
+ return transactionItems.reverse()
+ }
+)
+
+export const selectIsFetchingTransactions = createSelector(
+ _selectState,
+ (state) => state.fetchingTransactions
+)
+
+export const shouldFetchTransactions = createSelector(
+ selectCurrentPage,
+ selectTransactions,
+ selectIsFetchingTransactions,
+ (page, transactions, fetching) => {
+ if (page != 'wallet') return false
+ if (fetching) return false
+ if (transactions.length != 0) return false
+
+ return true
+ }
+)
+
+export const selectReceiveAddress = createSelector(
+ _selectState,
+ (state) => state.receiveAddress
+)
+
+export const selectGettingNewAddress = createSelector(
+ _selectState,
+ (state) => state.gettingNewAddress
+)
+
+export const shouldGetReceiveAddress = createSelector(
+ selectReceiveAddress,
+ selectGettingNewAddress,
+ selectDaemonReady,
+ (address, fetching, daemonReady) => {
+ if (!daemonReady) return false
+ if (fetching) return false
+ if (address) return false
+
+ return true
+ }
+)
+
+export const shouldCheckAddressIsMine = createSelector(
+ _selectState,
+ selectCurrentPage,
+ selectReceiveAddress,
+ selectDaemonReady,
+ (state, page, address, daemonReady) => {
+ if (!daemonReady) return false
+ if (address === undefined) return false
+ if (state.addressOwnershipChecked) return false
+
+ return true
+ }
+)
+
+export const selectDraftTransaction = createSelector(
+ _selectState,
+ (state) => state.draftTransaction || {}
+)
+
+export const selectDraftTransactionAmount = createSelector(
+ selectDraftTransaction,
+ (draft) => draft.amount
+)
+
+export const selectDraftTransactionAddress = createSelector(
+ selectDraftTransaction,
+ (draft) => draft.address
+)
diff --git a/ui/js/store.js b/ui/js/store.js
new file mode 100644
index 000000000..462c6a5d0
--- /dev/null
+++ b/ui/js/store.js
@@ -0,0 +1,78 @@
+const redux = require('redux');
+const thunk = require("redux-thunk").default;
+const env = process.env.NODE_ENV || 'development';
+
+import {
+ createLogger
+} from 'redux-logger'
+import appReducer from 'reducers/app';
+import availabilityReducer from 'reducers/availability'
+import certificatesReducer from 'reducers/certificates'
+import claimsReducer from 'reducers/claims'
+import contentReducer from 'reducers/content';
+import costInfoReducer from 'reducers/cost_info'
+import fileInfoReducer from 'reducers/file_info'
+import rewardsReducer from 'reducers/rewards'
+import searchReducer from 'reducers/search'
+import walletReducer from 'reducers/wallet'
+
+function isFunction(object) {
+ return typeof object === 'function';
+}
+
+function isNotFunction(object) {
+ return !isFunction(object);
+}
+
+function createBulkThunkMiddleware() {
+ return ({ dispatch, getState }) => next => (action) => {
+ if (action.type === 'BATCH_ACTIONS') {
+ action.actions.filter(isFunction).map(actionFn =>
+ actionFn(dispatch, getState)
+ )
+ }
+ return next(action)
+ }
+}
+
+function enableBatching(reducer) {
+ return function batchingReducer(state, action) {
+ switch (action.type) {
+ case 'BATCH_ACTIONS':
+ return action.actions.filter(isNotFunction).reduce(batchingReducer, state)
+ default:
+ return reducer(state, action)
+ }
+ }
+}
+
+const reducers = redux.combineReducers({
+ app: appReducer,
+ availability: availabilityReducer,
+ certificates: certificatesReducer,
+ claims: claimsReducer,
+ fileInfo: fileInfoReducer,
+ content: contentReducer,
+ costInfo: costInfoReducer,
+ rewards: rewardsReducer,
+ search: searchReducer,
+ wallet: walletReducer,
+});
+
+const bulkThunk = createBulkThunkMiddleware()
+const middleware = [thunk, bulkThunk]
+
+if (env === 'development') {
+ const logger = createLogger({
+ collapsed: true
+ });
+ middleware.push(logger)
+}
+
+const createStoreWithMiddleware = redux.compose(
+ redux.applyMiddleware(...middleware)
+)(redux.createStore);
+
+const reduxStore = createStoreWithMiddleware(enableBatching(reducers));
+
+export default reduxStore;
diff --git a/ui/js/triggers.js b/ui/js/triggers.js
new file mode 100644
index 000000000..41d0e0763
--- /dev/null
+++ b/ui/js/triggers.js
@@ -0,0 +1,91 @@
+import {
+ shouldFetchTransactions,
+ shouldGetReceiveAddress,
+} from 'selectors/wallet'
+import {
+ shouldFetchFeaturedContent,
+ shouldFetchDownloadedContent,
+ shouldFetchPublishedContent,
+} from 'selectors/content'
+import {
+ shouldFetchCurrentUriFileInfo,
+} from 'selectors/file_info'
+import {
+ shouldFetchCurrentUriCostInfo,
+} from 'selectors/cost_info'
+import {
+ shouldFetchCurrentUriAvailability,
+} from 'selectors/availability'
+import {
+ doFetchTransactions,
+ doGetNewAddress,
+} from 'actions/wallet'
+import {
+ doFetchFeaturedContent,
+ doFetchDownloadedContent,
+ doFetchPublishedContent,
+} from 'actions/content'
+import {
+ doFetchCurrentUriFileInfo,
+} from 'actions/file_info'
+import {
+ doFetchCurrentUriCostInfo,
+} from 'actions/cost_info'
+import {
+ doFetchCurrentUriAvailability,
+} from 'actions/availability'
+
+const triggers = []
+
+triggers.push({
+ selector: shouldFetchTransactions,
+ action: doFetchTransactions,
+})
+
+triggers.push({
+ selector: shouldGetReceiveAddress,
+ action: doGetNewAddress
+})
+
+triggers.push({
+ selector: shouldFetchFeaturedContent,
+ action: doFetchFeaturedContent,
+})
+
+triggers.push({
+ selector: shouldFetchDownloadedContent,
+ action: doFetchDownloadedContent,
+})
+
+triggers.push({
+ selector: shouldFetchPublishedContent,
+ action: doFetchPublishedContent,
+})
+
+triggers.push({
+ selector: shouldFetchCurrentUriFileInfo,
+ action: doFetchCurrentUriFileInfo,
+})
+
+triggers.push({
+ selector: shouldFetchCurrentUriCostInfo,
+ action: doFetchCurrentUriCostInfo,
+})
+
+triggers.push({
+ selector: shouldFetchCurrentUriAvailability,
+ action: doFetchCurrentUriAvailability,
+})
+
+const runTriggers = function() {
+ triggers.forEach(function(trigger) {
+ const state = app.store.getState();
+ const should = trigger.selector(state)
+ if (trigger.selector(state)) app.store.dispatch(trigger.action())
+ });
+}
+
+module.exports = {
+ triggers: triggers,
+ runTriggers: runTriggers
+}
diff --git a/ui/js/util/batchActions.js b/ui/js/util/batchActions.js
new file mode 100644
index 000000000..eedab1cc6
--- /dev/null
+++ b/ui/js/util/batchActions.js
@@ -0,0 +1,9 @@
+// https://github.com/reactjs/redux/issues/911
+function batchActions(...actions) {
+ return {
+ type: 'BATCH_ACTIONS',
+ actions: actions
+ };
+}
+
+export default batchActions
diff --git a/ui/package.json b/ui/package.json
index decc4bebf..8f7e754e1 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -3,7 +3,8 @@
"version": "0.10.0rc5",
"description": "LBRY UI",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "dev": "webpack-dev-server --devtool eval --progress --colors --inline"
},
"keywords": [
"lbry"
@@ -22,12 +23,18 @@
"babel-preset-es2015": "^6.13.2",
"babel-preset-react": "^6.11.1",
"clamp-js-main": "^0.11.1",
+ "intersection-observer": "^0.2.1",
"mediaelement": "^2.23.4",
"node-sass": "^3.8.0",
"rc-progress": "^2.0.6",
"react": "^15.4.0",
"react-dom": "^15.4.0",
"react-modal": "^1.5.2",
+ "react-redux": "^5.0.3",
+ "redux": "^3.6.0",
+ "redux-logger": "^3.0.1",
+ "redux-thunk": "^2.2.0",
+ "reselect": "^3.0.0",
"videostream": "^2.4.2"
},
"devDependencies": {
@@ -48,6 +55,7 @@
"json-loader": "^0.5.4",
"node-sass": "^3.13.0",
"webpack": "^1.13.3",
+ "webpack-dev-server": "^2.4.4",
"webpack-target-electron-renderer": "^0.4.0"
}
}
diff --git a/ui/webpack.config.js b/ui/webpack.config.js
index be349baa9..9afb1d82f 100644
--- a/ui/webpack.config.js
+++ b/ui/webpack.config.js
@@ -1,4 +1,5 @@
const path = require('path');
+const appPath = path.resolve(__dirname, 'js');
const PATHS = {
app: path.join(__dirname, 'app'),
@@ -13,6 +14,10 @@ module.exports = {
filename: "bundle.js"
},
devtool: 'source-map',
+ resolve: {
+ root: appPath,
+ extensions: ['', '.js', '.jsx', '.css'],
+ },
module: {
preLoaders: [
{
@@ -25,8 +30,8 @@ module.exports = {
loaders: [
{ test: /\.css$/, loader: "style!css" },
{
- test: /\.jsx?$/,
- loader: 'babel',
+ test: /\.jsx?$/,
+ loader: 'babel',
query: {
cacheDirectory: true,
presets:[ 'es2015', 'react', 'stage-2' ]
diff --git a/ui/webpack.dev.config.js b/ui/webpack.dev.config.js
index 358d6c04d..cb284e233 100644
--- a/ui/webpack.dev.config.js
+++ b/ui/webpack.dev.config.js
@@ -1,4 +1,5 @@
const path = require('path');
+const appPath = path.resolve(__dirname, 'js');
const PATHS = {
app: path.join(__dirname, 'app'),
@@ -16,6 +17,10 @@ module.exports = {
debug: true,
cache: true,
devtool: 'eval',
+ resolve: {
+ root: appPath,
+ extensions: ['', '.js', '.jsx', '.css'],
+ },
module: {
preLoaders: [
{
@@ -28,9 +33,9 @@ module.exports = {
loaders: [
{ test: /\.css$/, loader: "style!css" },
{
- test: /\.jsx?$/,
- loader: 'babel',
- query: {
+ test: /\.jsx?$/,
+ loader: 'babel',
+ query: {
cacheDirectory: true,
presets:[ 'es2015', 'react', 'stage-2' ]
}