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:
      {errorInfoList}
    , + }); +}, +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?

      - - -
      -
      - ); - } -}); - -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} - -

      - - - {title} - - -

      -
      -
      - -
      -
      -

      - - {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 ( -
      -
      - -
      -
      {title}
      -
      - { !this.props.hidePrice ? : null} - -
      -
      -
      -
      - - {isConfirmed - ? metadata.description - : This file is pending confirmation.} - -
      -
      - {this.state.showNsfwHelp && this.state.hovered - ?
      -

      - This content is Not Safe For Work. - To view adult content, please change your . -

      -
      - : null} -
      -
      - ); - } -}); - -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?

      + + +
      +
      + ); + } +} + +// 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?

      + +// +//
      +//
      +// ); +// } +// }); + +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 ( +
      +
      + this.props.navigate(primaryUrl)} className="card__link"> +
      +
      {title}
      +
      + { !this.props.hidePrice ? : null} + +
      +
      + {metadata && +
      + } +
      + + {isConfirmed + ? metadata.description + : This file is pending confirmation.} + +
      +
      + {this.state.showNsfwHelp && this.state.hovered + ?
      +

      + This content is Not Safe For Work. + To view adult content, please change your . +

      +
      + : null} +
      +
      + ); + } +} + +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 { ' ' } + + + + + + + {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 { ' ' } +// +// +// +// +// +// +// {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 ( +
      +
      + +
      + +
      +
      +
      +

      + + {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 ( - - ); + 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 ( + + ) +} 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 { ' ' } - - - - - - - {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/') ? -
      -
      -
      -
      - {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/') ? +
      +
      +
      +
      + {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 ( -
      -
      -
      -

      Send Credits

      -
      -
      - -
      -
      - -
      -
      - 0.0) || this.state.address == ""} /> - -
      - { - this.state.results ? -
      -

      Results

      - {this.state.results} -
      : '' - } -
      - - 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 ? - - - - - - - - - - - {rows} - -
      AmountDateTimeTransaction
      - : '' - } -
      -
      - ); - } -}); - - -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 ( +
      +
      +
      +

      Send Credits

      +
      +
      + +
      +
      + +
      +
      + 0.0) || !address} /> + +
      +
      + {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 ( +//
      +//
      +//
      +//

      Send Credits

      +//
      +//
      +// +//
      +//
      +// +//
      +//
      +// 0.0) || this.state.address == ""} /> +// +//
      +// { +// this.state.results ? +//
      +//

      Results

      +// {this.state.results} +//
      : '' +// } +//
      +// +// 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 ? + + + + + + + + + + + {rows} + +
      AmountDateTimeTransaction
      + : '' + } +
      +
      + ) +} + +const WalletPage = (props) => { + const { + balance, + currentPage + } = props + + return ( +
      +
      +
      +

      Balance

      +
      +
      + +
      +
      + { 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' ] }