diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 235981e26..6d0330007 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.13.0 +current_version = 0.14.3 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/.gitignore b/.gitignore index 5ec3a5c07..b3d2ad41c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ /app/node_modules /build/venv /lbry-app-venv +/lbry-app /lbry-venv /daemon/build /daemon/venv @@ -27,3 +28,4 @@ build/daemon.zip .vimrc package-lock.json +ui/yarn.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c6a2184e..2db7d593f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,15 +8,19 @@ Web UI version numbers should always match the corresponding version of LBRY App ## [Unreleased] ### Added - * + * Added a new component, `FormFieldPrice` which is now used in Publish and Settings * ### Changed - * + * Some form field refactoring as we progress towards form sanity. + * When an "Open" button is clicked on a show page, if the file fails to open, the app will try to open the file's folder. * ### Fixed - * + * Tiles will no longer be blurry on hover (Windows only bug) + * Removed placeholder values from price selection form fields, which was causing confusion that these were real values (#426) + * Fixed showing "other currency" help tip in publish form, which was caused due to not "setting" state for price + * Now using setState in formFieldPrice * ### Deprecated @@ -24,8 +28,90 @@ Web UI version numbers should always match the corresponding version of LBRY App * ### Removed + * Removed the label "Max Purchase Price" from settings page. It was redundant. + * Unused old files from previous commit(9c3d633) * - * + +## [0.14.3] - 2017-08-03 + +### Added + * Add tooltips to controls in header + * New flow for rewards authentication failure + + +### Changed + * Make it clearer how to skip identity verification and add link to FAQ + * Reward-eligible content icon is now a rocket ship :D :D :D + * Change install description shown by operating systems + * Improved flow for when app is run with incompatible daemon + + +### Fixed + * Corrected improper pluralization on loading screen + + + +## [0.14.2] - 2017-07-30 + +### Added + * Replaced horizontal scrollbars with scroll arrows + * Featured weekly reward content shows with an orange star + * Added pagination to channel pages + + +### Fixed + * Fixed requirement to double click play button on many videos + * Fixed errors from calls to `get` not bubbling correctly + * Fixed some corner-case flows that could break file pages + + + +## [0.14.1] - 2017-07-28 + +### Fixed + * Fixed upgrade file path missing file name + + + +## [0.14.0] - 2017-07-28 + +### Added + * Identity verification for new reward participants + * Support rich markup in publishing descriptions and show pages. + * Release past publishing claims (and recover LBC) via the UI + * Added transition to card hovers to smooth animation + * Use randomly colored tiles when image is missing from metadata + * Added a loading message to file actions + * URL is auto suggested in Publish Page + + +### Changed + * Publishing revamped. Editing claims is much easier. + * Daemon updated from v0.13.1 to [v0.14.2](https://github.com/lbryio/lbry/releases/tag/v0.14.2) + * Publish page now use `claim_list` rather than `file_list` + + +### Removed + * Removed bandwidth caps from settings, because the daemon was not respecting them anyway. + + +### Fixed + * Fixed bug with download notice when switching window focus + * Fixed newly published files appearing twice + * Fixed unconfirmed published files missing channel name + * Fixed old files from updated published claims appearing in downloaded list + * Fixed inappropriate text showing on searches + * Stop discover page from pushing jumping vertically while loading + * Restored feedback on claim amounts + * Fixed hiding price input when Free is checked on publish form + * Fixed hiding new identity fields on publish form + * Fixed files on downloaded tab not showing download progress + * Fixed downloading files that are deleted not being removed from the downloading list + * Fixed download progress bar not being cleared when a downloading file is deleted + * Fixed refresh regression after adding scroll position to history state + * Fixed app not monitoring download progress on files in progress between restarts + + ## [0.13.0] - 2017-06-30 diff --git a/app/main.js b/app/main.js index 35a50f224..69fad7d92 100644 --- a/app/main.js +++ b/app/main.js @@ -27,7 +27,7 @@ const {version: localVersion} = require(app.getAppPath() + '/package.json'); const VERSION_CHECK_INTERVAL = 30 * 60 * 1000; const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest'; - +const DAEMON_PATH = process.env.LBRY_DAEMON || path.join(__dirname, 'dist', 'lbrynet-daemon'); let client = jayson.client.http({ host: 'localhost', @@ -207,13 +207,8 @@ function handleDaemonSubprocessExited() { function launchDaemon() { assert(!daemonSubprocess, 'Tried to launch daemon twice'); - if (process.env.LBRY_DAEMON) { - executable = process.env.LBRY_DAEMON; - } else { - executable = path.join(__dirname, 'dist', 'lbrynet-daemon'); - } - console.log('Launching daemon:', executable) - daemonSubprocess = child_process.spawn(executable) + console.log('Launching daemon:', DAEMON_PATH) + daemonSubprocess = child_process.spawn(DAEMON_PATH) // Need to handle the data event instead of attaching to // process.stdout because the latter doesn't work. I believe on // windows it buffers stdout and we don't get any meaningful output diff --git a/app/package.json b/app/package.json index aee4e6085..81027f7a8 100644 --- a/app/package.json +++ b/app/package.json @@ -1,8 +1,8 @@ { "name": "LBRY", - "version": "0.13.0", + "version": "0.14.3", "main": "main.js", - "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", + "description": "A browser for the LBRY network, a digital marketplace controlled by its users.", "author": { "name": "LBRY Inc.", "email": "hello@lbry.io" @@ -18,5 +18,8 @@ }, "devDependencies": { "electron-rebuild": "^1.5.11" + }, + "lbrySettings": { + "lbrynetDaemonVersion": "0.14.2" } -} +} \ No newline at end of file diff --git a/build/DAEMON_URL b/build/DAEMON_URL index b3b581882..10aa02a3a 100644 --- a/build/DAEMON_URL +++ b/build/DAEMON_URL @@ -1 +1 @@ -https://github.com/lbryio/lbry/releases/download/v0.13.1/lbrynet-daemon-v0.13.1-OSNAME.zip +https://github.com/lbryio/lbry/releases/download/v0.14.2/lbrynet-daemon-v0.14.2-OSNAME.zip \ No newline at end of file diff --git a/build/build.ps1 b/build/build.ps1 index 6c93b172c..569fbd875 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -46,8 +46,8 @@ dir dist # verify that binary was built/named correctly # sign binary nuget install secure-file -ExcludeVersion -secure-file\tools\secure-file -decrypt build\lbry2.pfx.enc -secret "$env:pfx_key" -& ${env:SIGNTOOL_PATH} sign /f build\lbry2.pfx /p "$env:key_pass" /tr http://tsa.starfieldtech.com /td SHA256 /fd SHA256 dist\*.exe +secure-file\tools\secure-file -decrypt build\lbry3.pfx.enc -secret "$env:pfx_key" +& ${env:SIGNTOOL_PATH} sign /f build\lbry3.pfx /p "$env:key_pass" /tr http://tsa.starfieldtech.com /td SHA256 /fd SHA256 dist\*.exe -python build\upload_assets.py \ No newline at end of file +python build\upload_assets.py diff --git a/build/build.sh b/build/build.sh index c5ccba34f..9fa139877 100755 --- a/build/build.sh +++ b/build/build.sh @@ -79,11 +79,14 @@ if $OSX; then else OSNAME="linux" fi -DAEMON_URL="$(cat "$BUILD_DIR/DAEMON_URL" | sed "s/OSNAME/${OSNAME}/")" +DAEMON_VER=$(node -e "console.log(require(\"$ROOT/app/package.json\").lbrySettings.lbrynetDaemonVersion)") +DAEMON_URL="https://github.com/lbryio/lbry/releases/download/v${DAEMON_VER}/lbrynet-daemon-v${DAEMON_VER}-${OSNAME}.zip" wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip" unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/" rm "$BUILD_DIR/daemon.zip" + + ################### # Build the app # ################### diff --git a/build/lbry2.pfx.enc b/build/lbry2.pfx.enc deleted file mode 100644 index 46e52260a..000000000 Binary files a/build/lbry2.pfx.enc and /dev/null differ diff --git a/build/lbry3.pfx.enc b/build/lbry3.pfx.enc new file mode 100644 index 000000000..330cfc05b Binary files /dev/null and b/build/lbry3.pfx.enc differ diff --git a/package.json b/package.json index e0f83e824..50eae487d 100644 --- a/package.json +++ b/package.json @@ -57,8 +57,8 @@ }, "devDependencies": { "devtron": "^1.4.0", - "electron": "^1.4.15", + "electron": "^1.7.5", "electron-builder": "^11.7.0", - "electron-debug": "^1.1.0" + "electron-debug": "^1.4.0" } } diff --git a/ui/js/actions/app.js b/ui/js/actions/app.js index de0f158c9..3640e8623 100644 --- a/ui/js/actions/app.js +++ b/ui/js/actions/app.js @@ -13,31 +13,43 @@ import { doSearch } from "actions/search"; import { doFetchDaemonSettings } from "actions/settings"; import { doAuthenticate } from "actions/user"; import { doFileList } from "actions/file_info"; +import { toQueryString } from "util/query_params"; +import { parseQueryParams } from "util/query_params"; const { remote, ipcRenderer, shell } = require("electron"); const path = require("path"); -const app = require("electron").remote.app; const { download } = remote.require("electron-dl"); const fs = remote.require("fs"); +const { lbrySettings: config } = require("../../../app/package.json"); -const queryStringFromParams = params => { - return Object.keys(params).map(key => `${key}=${params[key]}`).join("&"); -}; - -export function doNavigate(path, params = {}) { +export function doNavigate(path, params = {}, options = {}) { return function(dispatch, getState) { let url = path; - if (params) url = `${url}?${queryStringFromParams(params)}`; + if (params) url = `${url}?${toQueryString(params)}`; dispatch(doChangePath(url)); const state = getState(); const pageTitle = selectPageTitle(state); - dispatch(doHistoryPush(params, pageTitle, url)); + dispatch(doHistoryPush({ params }, pageTitle, url)); }; } -export function doChangePath(path) { +export function doAuthNavigate(pathAfterAuth = null, params = {}) { + return function(dispatch, getState) { + if (pathAfterAuth) { + dispatch({ + type: types.CHANGE_AFTER_AUTH_PATH, + data: { + path: `${pathAfterAuth}?${toQueryString(params)}`, + }, + }); + } + dispatch(doNavigate("/auth")); + }; +} + +export function doChangePath(path, options = {}) { return function(dispatch, getState) { dispatch({ type: types.CHANGE_PATH, @@ -48,8 +60,12 @@ export function doChangePath(path) { const state = getState(); const pageTitle = selectPageTitle(state); + const scrollY = options.scrollY; + window.document.title = pageTitle; - window.scrollTo(0, 0); + + if (scrollY) window.scrollTo(0, scrollY); + else window.scrollTo(0, 0); const currentPage = selectCurrentPage(state); if (currentPage === "search") { @@ -62,15 +78,32 @@ export function doChangePath(path) { export function doHistoryBack() { return function(dispatch, getState) { if (!history.state) return; + if (history.state.index === 0) return; history.back(); }; } -export function doHistoryPush(params, title, relativeUrl) { +export function doHistoryPush(currentState, title, relativeUrl) { return function(dispatch, getState) { title += " - LBRY"; - history.pushState(params, title, `#${relativeUrl}`); + history.pushState(currentState, title, `#${relativeUrl}`); + }; +} + +export function doRecordScroll(scroll) { + return function(dispatch, getState) { + const state = getState(); + const historyState = history.state; + + if (!historyState) return; + + historyState.scrollY = scroll; + history.replaceState( + historyState, + document.title, + `#${state.app.currentPath}` + ); }; } @@ -117,8 +150,10 @@ 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); + const dir = fs.mkdtempSync( + remote.app.getPath("temp") + require("path").sep + ), + upgradeFilename = selectUpgradeFilename(state); let options = { onProgress: p => dispatch(doUpdateDownloadProgress(Math.round(p * 100))), @@ -202,11 +237,21 @@ export function doCheckUpgradeAvailable() { }; } +export function doCheckDaemonVersion() { + return function(dispatch, getState) { + lbry.version().then(({ lbrynet_version }) => { + dispatch({ + type: config.lbrynetDaemonVersion == lbrynet_version + ? types.DAEMON_VERSION_MATCH + : types.DAEMON_VERSION_MISMATCH, + }); + }); + }; +} + export function doAlertError(errorList) { return function(dispatch, getState) { const state = getState(); - console.log("do alert error"); - console.log(errorList); dispatch({ type: types.OPEN_MODAL, data: { @@ -219,6 +264,9 @@ export function doAlertError(errorList) { export function doDaemonReady() { return function(dispatch, getState) { + const path = window.location.hash || "#/discover"; + const params = parseQueryParams(path.split("?")[1] || ""); + history.replaceState({ params, index: 0 }, document.title, `${path}`); dispatch(doAuthenticate()); dispatch({ type: types.DAEMON_READY, @@ -256,3 +304,9 @@ export function doChangeLanguage(newLanguage) { data: { newLanguage: newLanguage }, }; } + +export function doQuit() { + return function(dispatch, getState) { + remote.app.quit(); + }; +} diff --git a/ui/js/actions/content.js b/ui/js/actions/content.js index b80f8fc0c..144375b6e 100644 --- a/ui/js/actions/content.js +++ b/ui/js/actions/content.js @@ -5,17 +5,18 @@ import lbryuri from "lbryuri"; import { selectBalance } from "selectors/wallet"; import { selectFileInfoForUri, - selectUrisDownloading, + selectDownloadingByOutpoint, } from "selectors/file_info"; import { selectResolvingUris } from "selectors/content"; import { selectCostInfoForUri } from "selectors/cost_info"; -import { doOpenModal } from "actions/app"; +import { doAlertError, doOpenModal } from "actions/app"; import { doClaimEligiblePurchaseRewards } from "actions/rewards"; import { selectBadgeNumber } from "selectors/app"; import { selectTotalDownloadProgress } from "selectors/file_info"; import setBadge from "util/setBadge"; import setProgressBar from "util/setProgressBar"; import batchActions from "util/batchActions"; +import * as modals from "constants/modal_types"; const { ipcRenderer } = require("electron"); @@ -132,6 +133,34 @@ export function doFetchFeaturedUris() { }; } +export function doFetchRewardedContent() { + return function(dispatch, getState) { + const state = getState(); + + const success = nameToClaimId => { + dispatch({ + type: types.FETCH_REWARD_CONTENT_COMPLETED, + data: { + claimIds: Object.values(nameToClaimId), + success: true, + }, + }); + }; + + const failure = () => { + dispatch({ + type: types.FETCH_REWARD_CONTENT_COMPLETED, + data: { + claimIds: [], + success: false, + }, + }); + }; + + lbryio.call("reward", "list_featured").then(success, failure); + }; +} + export function doUpdateLoadStatus(uri, outpoint) { return function(dispatch, getState) { const state = getState(); @@ -198,24 +227,38 @@ export function doUpdateLoadStatus(uri, outpoint) { }; } +export function doStartDownload(uri, outpoint) { + return function(dispatch, getState) { + const state = getState(); + + if (!outpoint) { + throw new Error("outpoint is required to begin a download"); + } + + const { downloadingByOutpoint = {} } = state.fileInfo; + + if (downloadingByOutpoint[outpoint]) return; + + lbry.file_list({ outpoint, full_status: true }).then(([fileInfo]) => { + dispatch({ + type: types.DOWNLOADING_STARTED, + data: { + uri, + outpoint, + fileInfo, + }, + }); + + dispatch(doUpdateLoadStatus(uri, outpoint)); + }); + }; +} + export function doDownloadFile(uri, streamInfo) { return function(dispatch, getState) { const state = getState(); - lbry - .file_list({ outpoint: streamInfo.outpoint, full_status: true }) - .then(([fileInfo]) => { - dispatch({ - type: types.DOWNLOADING_STARTED, - data: { - uri, - outpoint: streamInfo.outpoint, - fileInfo, - }, - }); - - dispatch(doUpdateLoadStatus(uri, streamInfo.outpoint)); - }); + dispatch(doStartDownload(uri, streamInfo.outpoint)); lbryio .call("file", "view", { @@ -240,22 +283,32 @@ export function doLoadVideo(uri) { }, }); - lbry.get({ uri }).then(streamInfo => { - const timeout = - streamInfo === null || - typeof streamInfo !== "object" || - streamInfo.error == "Timeout"; + lbry + .get({ uri }) + .then(streamInfo => { + const timeout = + streamInfo === null || + typeof streamInfo !== "object" || + streamInfo.error == "Timeout"; - if (timeout) { + if (timeout) { + dispatch({ + type: types.LOADING_VIDEO_FAILED, + data: { uri }, + }); + + dispatch(doOpenModal("timedOut")); + } else { + dispatch(doDownloadFile(uri, streamInfo)); + } + }) + .catch(error => { dispatch({ type: types.LOADING_VIDEO_FAILED, data: { uri }, }); - dispatch(doOpenModal("timedOut")); - } else { - dispatch(doDownloadFile(uri, streamInfo)); - } - }); + dispatch(doAlertError(error)); + }); }; } @@ -264,8 +317,9 @@ export function doPurchaseUri(uri, purchaseModalName) { const state = getState(); const balance = selectBalance(state); const fileInfo = selectFileInfoForUri(state, { uri }); - const downloadingByUri = selectUrisDownloading(state); - const alreadyDownloading = !!downloadingByUri[uri]; + const downloadingByOutpoint = selectDownloadingByOutpoint(state); + const alreadyDownloading = + fileInfo && !!downloadingByOutpoint[fileInfo.outpoint]; // we already fully downloaded the file. if (fileInfo && fileInfo.completed) { @@ -292,7 +346,7 @@ export function doPurchaseUri(uri, purchaseModalName) { } if (cost > balance) { - dispatch(doOpenModal("notEnoughCredits")); + dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS)); } else { dispatch(doOpenModal(purchaseModalName)); } @@ -301,22 +355,28 @@ export function doPurchaseUri(uri, purchaseModalName) { }; } -export function doFetchClaimsByChannel(uri, page = 1) { +export function doFetchClaimsByChannel(uri, page) { return function(dispatch, getState) { dispatch({ type: types.FETCH_CHANNEL_CLAIMS_STARTED, - data: { uri }, + data: { uri, page }, }); lbry.claim_list_by_channel({ uri, page }).then(result => { const claimResult = result[uri], - claims = claimResult ? claimResult.claims_in_channel : []; + claims = claimResult ? claimResult.claims_in_channel : [], + totalPages = claimResult + ? claimResult.claims_in_channel_pages + : undefined, + currentPage = claimResult ? claimResult.returned_page : undefined; dispatch({ type: types.FETCH_CHANNEL_CLAIMS_COMPLETED, data: { uri, - claims: claims, + claims, + totalPages, + page: currentPage, }, }); }); @@ -339,3 +399,68 @@ export function doFetchClaimListMine() { }); }; } + +export function doFetchChannelListMine() { + return function(dispatch, getState) { + dispatch({ + type: types.FETCH_CHANNEL_LIST_MINE_STARTED, + }); + + const callback = channels => { + dispatch({ + type: types.FETCH_CHANNEL_LIST_MINE_COMPLETED, + data: { claims: channels }, + }); + }; + + lbry.channel_list_mine().then(callback); + }; +} + +export function doCreateChannel(name, amount) { + return function(dispatch, getState) { + dispatch({ + type: types.CREATE_CHANNEL_STARTED, + }); + + return new Promise((resolve, reject) => { + lbry + .channel_new({ + channel_name: name, + amount: parseFloat(amount), + }) + .then( + channelClaim => { + channelClaim.name = name; + dispatch({ + type: types.CREATE_CHANNEL_COMPLETED, + data: { channelClaim }, + }); + resolve(channelClaim); + }, + err => { + reject(err); + } + ); + }); + }; +} + +export function doPublish(params) { + return function(dispatch, getState) { + return new Promise((resolve, reject) => { + const success = claim => { + resolve(claim); + + if (claim === true) dispatch(doFetchClaimListMine()); + else + setTimeout(() => dispatch(doFetchClaimListMine()), 20000, { + once: true, + }); + }; + const failure = err => reject(err); + + lbry.publishDeprecated(params, null, success, failure); + }); + }; +} diff --git a/ui/js/actions/file_info.js b/ui/js/actions/file_info.js index 9d3861fb7..c69db230c 100644 --- a/ui/js/actions/file_info.js +++ b/ui/js/actions/file_info.js @@ -3,15 +3,18 @@ import lbry from "lbry"; import { doFetchClaimListMine } from "actions/content"; import { selectClaimsByUri, - selectClaimListMineIsPending, + selectIsFetchingClaimListMine, selectMyClaimsOutpoints, } from "selectors/claims"; import { - selectFileListIsPending, + selectIsFetchingFileList, selectFileInfosByOutpoint, selectUrisLoading, + selectTotalDownloadProgress, } from "selectors/file_info"; -import { doCloseModal } from "actions/app"; +import { doCloseModal, doHistoryBack } from "actions/app"; +import setProgressBar from "util/setProgressBar"; +import batchActions from "util/batchActions"; const { shell } = require("electron"); @@ -48,16 +51,16 @@ export function doFetchFileInfo(uri) { export function doFileList() { return function(dispatch, getState) { const state = getState(); - const isPending = selectFileListIsPending(state); + const isFetching = selectIsFetchingFileList(state); - if (!isPending) { + if (!isFetching) { dispatch({ type: types.FILE_LIST_STARTED, }); lbry.file_list().then(fileInfos => { dispatch({ - type: types.FILE_LIST_COMPLETED, + type: types.FILE_LIST_SUCCEEDED, data: { fileInfos, }, @@ -69,7 +72,10 @@ export function doFileList() { export function doOpenFileInShell(fileInfo) { return function(dispatch, getState) { - shell.openItem(fileInfo.download_path); + const success = shell.openItem(fileInfo.download_path); + if (!success) { + dispatch(doOpenFileInFolder(fileInfo)); + } }; } @@ -102,14 +108,12 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) { }, }); - const success = () => { - dispatch({ - type: types.ABANDON_CLAIM_COMPLETED, - data: { - claimId: fileInfo.claim_id, - }, - }); - }; + const success = dispatch({ + type: types.ABANDON_CLAIM_SUCCEEDED, + data: { + claimId: fileInfo.claim_id, + }, + }); lbry.claim_abandon({ claim_id: fileInfo.claim_id }).then(success); } } @@ -121,17 +125,32 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) { }, }); - dispatch(doCloseModal()); + const totalProgress = selectTotalDownloadProgress(getState()); + setProgressBar(totalProgress); + }; +} + +export function doDeleteFileAndGoBack( + fileInfo, + deleteFromComputer, + abandonClaim +) { + return function(dispatch, getState) { + const actions = []; + actions.push(doCloseModal()); + actions.push(doHistoryBack()); + actions.push(doDeleteFile(fileInfo, deleteFromComputer, abandonClaim)); + dispatch(batchActions(...actions)); }; } export function doFetchFileInfosAndPublishedClaims() { return function(dispatch, getState) { const state = getState(), - isClaimListMinePending = selectClaimListMineIsPending(state), - isFileInfoListPending = selectFileListIsPending(state); + isFetchingClaimListMine = selectIsFetchingClaimListMine(state), + isFetchingFileInfo = selectIsFetchingFileList(state); - dispatch(doFetchClaimListMine()); - dispatch(doFileList()); + if (!isFetchingClaimListMine) dispatch(doFetchClaimListMine()); + if (!isFetchingFileInfo) dispatch(doFileList()); }; } diff --git a/ui/js/actions/rewards.js b/ui/js/actions/rewards.js index 9c5d5143a..95ae3b25b 100644 --- a/ui/js/actions/rewards.js +++ b/ui/js/actions/rewards.js @@ -1,4 +1,5 @@ import * as types from "constants/action_types"; +import * as modals from "constants/modal_types"; import lbryio from "lbryio"; import rewards from "rewards"; import { selectRewardsByType } from "selectors/rewards"; @@ -58,6 +59,12 @@ export function doClaimReward(reward, saveError = false) { reward, }, }); + if (reward.reward_type == rewards.TYPE_NEW_USER) { + dispatch({ + type: types.OPEN_MODAL, + data: { modal: modals.FIRST_REWARD }, + }); + } }; const failure = error => { @@ -99,9 +106,7 @@ export function doClaimEligiblePurchaseRewards() { if (unclaimedType) { dispatch(doClaimRewardType(unclaimedType)); } - if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) { - dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD)); - } + dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD)); }; } diff --git a/ui/js/actions/settings.js b/ui/js/actions/settings.js index f4fd86142..fccc16e9c 100644 --- a/ui/js/actions/settings.js +++ b/ui/js/actions/settings.js @@ -1,5 +1,8 @@ import * as types from "constants/action_types"; +import batchActions from "util/batchActions"; import lbry from "lbry"; +import fs from "fs"; +import http from "http"; export function doFetchDaemonSettings() { return function(dispatch, getState) { @@ -41,3 +44,296 @@ export function doSetClientSetting(key, value) { }, }; } + +export function doResolveLanguage(locale) { + const langs = { + aa: ["Afar", "Afar"], + ab: ["Abkhazian", "Аҧсуа"], + af: ["Afrikaans", "Afrikaans"], + ak: ["Akan", "Akana"], + am: ["Amharic", "አማርኛ"], + an: ["Aragonese", "Aragonés"], + ar: ["Arabic", "العربية"], + as: ["Assamese", "অসমীয়া"], + av: ["Avar", "Авар"], + ay: ["Aymara", "Aymar"], + az: ["Azerbaijani", "Azərbaycanca / آذربايجان"], + ba: ["Bashkir", "Башҡорт"], + be: ["Belarusian", "Беларуская"], + bg: ["Bulgarian", "Български"], + bh: ["Bihari", "भोजपुरी"], + bi: ["Bislama", "Bislama"], + bm: ["Bambara", "Bamanankan"], + bn: ["Bengali", "বাংলা"], + bo: ["Tibetan", "བོད་ཡིག / Bod skad"], + br: ["Breton", "Brezhoneg"], + bs: ["Bosnian", "Bosanski"], + ca: ["Catalan", "Català"], + ce: ["Chechen", "Нохчийн"], + ch: ["Chamorro", "Chamoru"], + co: ["Corsican", "Corsu"], + cr: ["Cree", "Nehiyaw"], + cs: ["Czech", "Česky"], + cu: ["Old Church Slavonic / Old Bulgarian", "словѣньскъ / slověnĭskŭ"], + cv: ["Chuvash", "Чăваш"], + cy: ["Welsh", "Cymraeg"], + da: ["Danish", "Dansk"], + de: ["German", "Deutsch"], + dv: ["Divehi", "ދިވެހިބަސް"], + dz: ["Dzongkha", "ཇོང་ཁ"], + ee: ["Ewe", "Ɛʋɛ"], + el: ["Greek", "Ελληνικά"], + en: ["English", "English"], + eo: ["Esperanto", "Esperanto"], + es: ["Spanish", "Español"], + et: ["Estonian", "Eesti"], + eu: ["Basque", "Euskara"], + fa: ["Persian", "فارسی"], + ff: ["Peul", "Fulfulde"], + fi: ["Finnish", "Suomi"], + fj: ["Fijian", "Na Vosa Vakaviti"], + fo: ["Faroese", "Føroyskt"], + fr: ["French", "Français"], + fy: ["West Frisian", "Frysk"], + ga: ["Irish", "Gaeilge"], + gd: ["Scottish Gaelic", "Gàidhlig"], + gl: ["Galician", "Galego"], + gn: ["Guarani", "Avañe'ẽ"], + gu: ["Gujarati", "ગુજરાતી"], + gv: ["Manx", "Gaelg"], + ha: ["Hausa", "هَوُسَ"], + he: ["Hebrew", "עברית"], + hi: ["Hindi", "हिन्दी"], + ho: ["Hiri Motu", "Hiri Motu"], + hr: ["Croatian", "Hrvatski"], + ht: ["Haitian", "Krèyol ayisyen"], + hu: ["Hungarian", "Magyar"], + hy: ["Armenian", "Հայերեն"], + hz: ["Herero", "Otsiherero"], + ia: ["Interlingua", "Interlingua"], + id: ["Indonesian", "Bahasa Indonesia"], + ie: ["Interlingue", "Interlingue"], + ig: ["Igbo", "Igbo"], + ii: ["Sichuan Yi", "ꆇꉙ / 四川彝语"], + ik: ["Inupiak", "Iñupiak"], + io: ["Ido", "Ido"], + is: ["Icelandic", "Íslenska"], + it: ["Italian", "Italiano"], + iu: ["Inuktitut", "ᐃᓄᒃᑎᑐᑦ"], + ja: ["Japanese", "日本語"], + jv: ["Javanese", "Basa Jawa"], + ka: ["Georgian", "ქართული"], + kg: ["Kongo", "KiKongo"], + ki: ["Kikuyu", "Gĩkũyũ"], + kj: ["Kuanyama", "Kuanyama"], + kk: ["Kazakh", "Қазақша"], + kl: ["Greenlandic", "Kalaallisut"], + km: ["Cambodian", "ភាសាខ្មែរ"], + kn: ["Kannada", "ಕನ್ನಡ"], + ko: ["Korean", "한국어"], + kr: ["Kanuri", "Kanuri"], + ks: ["Kashmiri", "कश्मीरी / كشميري"], + ku: ["Kurdish", "Kurdî / كوردی"], + kv: ["Komi", "Коми"], + kw: ["Cornish", "Kernewek"], + ky: ["Kirghiz", "Kırgızca / Кыргызча"], + la: ["Latin", "Latina"], + lb: ["Luxembourgish", "Lëtzebuergesch"], + lg: ["Ganda", "Luganda"], + li: ["Limburgian", "Limburgs"], + ln: ["Lingala", "Lingála"], + lo: ["Laotian", "ລາວ / Pha xa lao"], + lt: ["Lithuanian", "Lietuvių"], + lv: ["Latvian", "Latviešu"], + mg: ["Malagasy", "Malagasy"], + mh: ["Marshallese", "Kajin Majel / Ebon"], + mi: ["Maori", "Māori"], + mk: ["Macedonian", "Македонски"], + ml: ["Malayalam", "മലയാളം"], + mn: ["Mongolian", "Монгол"], + mo: ["Moldovan", "Moldovenească"], + mr: ["Marathi", "मराठी"], + ms: ["Malay", "Bahasa Melayu"], + mt: ["Maltese", "bil-Malti"], + my: ["Burmese", "Myanmasa"], + na: ["Nauruan", "Dorerin Naoero"], + nd: ["North Ndebele", "Sindebele"], + ne: ["Nepali", "नेपाली"], + ng: ["Ndonga", "Oshiwambo"], + nl: ["Dutch", "Nederlands"], + nn: ["Norwegian Nynorsk", "Norsk (nynorsk)"], + no: ["Norwegian", "Norsk (bokmål / riksmål)"], + nr: ["South Ndebele", "isiNdebele"], + nv: ["Navajo", "Diné bizaad"], + ny: ["Chichewa", "Chi-Chewa"], + oc: ["Occitan", "Occitan"], + oj: ["Ojibwa", "ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin"], + om: ["Oromo", "Oromoo"], + or: ["Oriya", "ଓଡ଼ିଆ"], + os: ["Ossetian / Ossetic", "Иронау"], + pa: ["Panjabi / Punjabi", "ਪੰਜਾਬੀ / पंजाबी / پنجابي"], + pi: ["Pali", "Pāli / पाऴि"], + pl: ["Polish", "Polski"], + ps: ["Pashto", "پښتو"], + pt: ["Portuguese", "Português"], + qu: ["Quechua", "Runa Simi"], + rm: ["Raeto Romance", "Rumantsch"], + rn: ["Kirundi", "Kirundi"], + ro: ["Romanian", "Română"], + ru: ["Russian", "Русский"], + rw: ["Rwandi", "Kinyarwandi"], + sa: ["Sanskrit", "संस्कृतम्"], + sc: ["Sardinian", "Sardu"], + sd: ["Sindhi", "सिनधि"], + se: ["Northern Sami", "Sámegiella"], + sg: ["Sango", "Sängö"], + sh: ["Serbo-Croatian", "Srpskohrvatski / Српскохрватски"], + si: ["Sinhalese", "සිංහල"], + sk: ["Slovak", "Slovenčina"], + sl: ["Slovenian", "Slovenščina"], + sm: ["Samoan", "Gagana Samoa"], + sn: ["Shona", "chiShona"], + so: ["Somalia", "Soomaaliga"], + sq: ["Albanian", "Shqip"], + sr: ["Serbian", "Српски"], + ss: ["Swati", "SiSwati"], + st: ["Southern Sotho", "Sesotho"], + su: ["Sundanese", "Basa Sunda"], + sv: ["Swedish", "Svenska"], + sw: ["Swahili", "Kiswahili"], + ta: ["Tamil", "தமிழ்"], + te: ["Telugu", "తెలుగు"], + tg: ["Tajik", "Тоҷикӣ"], + th: ["Thai", "ไทย / Phasa Thai"], + ti: ["Tigrinya", "ትግርኛ"], + tk: ["Turkmen", "Туркмен / تركمن"], + tl: ["Tagalog / Filipino", "Tagalog"], + tn: ["Tswana", "Setswana"], + to: ["Tonga", "Lea Faka-Tonga"], + tr: ["Turkish", "Türkçe"], + ts: ["Tsonga", "Xitsonga"], + tt: ["Tatar", "Tatarça"], + tw: ["Twi", "Twi"], + ty: ["Tahitian", "Reo Mā`ohi"], + ug: ["Uyghur", "Uyƣurqə / ئۇيغۇرچە"], + uk: ["Ukrainian", "Українська"], + ur: ["Urdu", "اردو"], + uz: ["Uzbek", "Ўзбек"], + ve: ["Venda", "Tshivenḓa"], + vi: ["Vietnamese", "Tiếng Việt"], + vo: ["Volapük", "Volapük"], + wa: ["Walloon", "Walon"], + wo: ["Wolof", "Wollof"], + xh: ["Xhosa", "isiXhosa"], + yi: ["Yiddish", "ייִדיש"], + yo: ["Yoruba", "Yorùbá"], + za: ["Zhuang", "Cuengh / Tôô / 壮语"], + zh: ["Chinese", "中文"], + zu: ["Zulu", "isiZulu"], + }; + + const lang = locale.substring(0, 2); + return { + type: types.LANGUAGE_RESOLVED, + data: { key: locale, value: `${langs[lang][0]} (${langs[lang][1]})` }, + }; +} + +export function doDownloadLanguage(lang, destinationPath) { + return function(dispatch, getState) { + const plainLanguage = lang.replace(".json", ""); + + dispatch({ + type: types.DOWNLOAD_LANGUAGE_STARTED, + data: plainLanguage, + }); + + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(destinationPath); + const req = http.request( + `http://i18n.lbry.io/langs/${lang}`, + response => { + file.on("finish", () => { + file.close(); + + // push to our local list + dispatch({ + type: types.DOWNLOAD_LANGUAGE_SUCCEEDED, + data: plainLanguage, + }); + + resolve(); + }); + + response.pipe(file); + } + ); + + const errorHandler = err => { + if (file) { + file.close(); + } + + fs.unlink(destinationPath); // Delete the file async. (But we don't check the result) + dispatch({ + type: types.DOWNLOAD_LANGUAGE_FAILED, + data: plainLanguage, + }); + reject(err); + }; + req.on("error", errorHandler); + file.on("error", errorHandler); + + req.end(); + }); + }; +} + +export function doDownloadLanguages() { + return function(dispatch, getState) { + if (!fs.existsSync("app/locales")) { + fs.mkdirSync("app/locales"); + } + + dispatch({ + type: types.DOWNLOAD_LANGUAGES_STARTED, + data: {}, + }); + + return new Promise((resolve, reject) => { + const req = http.request( + { host: "i18n.lbry.io", path: "/" }, + response => { + let str = ""; + + response.on("data", chunk => { + str += chunk; + }); + + response.on("end", () => { + const files = JSON.parse(str); + const actions = []; + files.forEach(file => { + actions.push(doDownloadLanguage(file, `app/locales/${file}`)); + }); + + dispatch(batchActions(...actions)); + + dispatch({ + type: types.DOWNLOAD_LANGUAGES_COMPLETED, + data: {}, + }); + + resolve(); + }); + } + ); + + req.on("error", err => { + reject(err); + }); + + req.end(); + }); + }; +} diff --git a/ui/js/actions/user.js b/ui/js/actions/user.js index ebfa8ed4b..1e5957c65 100644 --- a/ui/js/actions/user.js +++ b/ui/js/actions/user.js @@ -1,8 +1,11 @@ import * as types from "constants/action_types"; +import * as modals from "constants/modal_types"; import lbryio from "lbryio"; import { setLocal } from "utils"; -import { doRewardList } from "actions/rewards"; -import { selectEmailToVerify } from "selectors/user"; +import { doOpenModal } from "actions/app"; +import { doRewardList, doClaimRewardType } from "actions/rewards"; +import { selectEmailToVerify, selectUser } from "selectors/user"; +import rewards from "rewards"; export function doAuthenticate() { return function(dispatch, getState) { @@ -19,6 +22,7 @@ export function doAuthenticate() { dispatch(doRewardList()); }) .catch(error => { + dispatch(doOpenModal(modals.AUTHENTICATION_FAILURE)); dispatch({ type: types.AUTHENTICATION_FAILURE, data: { error }, @@ -136,3 +140,45 @@ export function doUserEmailVerify(verificationToken) { }); }; } + +export function doUserIdentityVerify(stripeToken) { + return function(dispatch, getState) { + dispatch({ + type: types.USER_IDENTITY_VERIFY_STARTED, + token: stripeToken, + }); + + lbryio + .call("user", "verify_identity", { stripe_token: stripeToken }, "post") + .then(user => { + if (user.is_identity_verified) { + dispatch({ + type: types.USER_IDENTITY_VERIFY_SUCCESS, + data: { user }, + }); + dispatch(doClaimRewardType(rewards.TYPE_NEW_USER)); + } else { + throw new Error( + "Your identity is still not verified. This should not happen." + ); //shouldn't happen + } + }) + .catch(error => { + dispatch({ + type: types.USER_IDENTITY_VERIFY_FAILURE, + data: { error: error.toString() }, + }); + }); + }; +} + +export function doFetchAccessToken() { + return function(dispatch, getState) { + const success = token => + dispatch({ + type: types.FETCH_ACCESS_TOKEN_SUCCESS, + data: { token }, + }); + lbryio.getAuthToken().then(success); + }; +} diff --git a/ui/js/app.js b/ui/js/app.js index 41701ee18..c5bfdcfbd 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -2,7 +2,9 @@ import store from "store.js"; import lbry from "./lbry.js"; const env = ENV; -const config = require(`./config/${env}`); +const config = { + ...require(`./config/${env}`), +}; const language = lbry.getClientSetting("language") ? lbry.getClientSetting("language") : "en"; diff --git a/ui/js/component/app/index.js b/ui/js/component/app/index.js index 4c966cb56..252fdf783 100644 --- a/ui/js/component/app/index.js +++ b/ui/js/component/app/index.js @@ -1,19 +1,33 @@ import React from "react"; import { connect } from "react-redux"; - import { selectCurrentModal } from "selectors/app"; -import { doCheckUpgradeAvailable, doAlertError } from "actions/app"; -import { doUpdateBalance } from "actions/wallet"; -import App from "./view"; +import { + doCheckUpgradeAvailable, + doOpenModal, + doAlertError, + doRecordScroll, +} from "actions/app"; +import { doFetchRewardedContent } from "actions/content"; -const select = state => ({ +import { doUpdateBalance } from "actions/wallet"; +import { selectWelcomeModalAcknowledged } from "selectors/app"; +import { selectUser } from "selectors/user"; +import App from "./view"; +import * as modals from "constants/modal_types"; + +const select = (state, props) => ({ modal: selectCurrentModal(state), + isWelcomeAcknowledged: selectWelcomeModalAcknowledged(state), + user: selectUser(state), }); const perform = dispatch => ({ alertError: errorList => dispatch(doAlertError(errorList)), checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()), + openWelcomeModal: () => dispatch(doOpenModal(modals.WELCOME)), updateBalance: balance => dispatch(doUpdateBalance(balance)), + fetchRewardedContent: () => dispatch(doFetchRewardedContent()), + recordScroll: scrollPosition => dispatch(doRecordScroll(scrollPosition)), }); export default connect(select, perform)(App); diff --git a/ui/js/component/app/view.jsx b/ui/js/component/app/view.jsx index 727f4e39a..e8ebbcd09 100644 --- a/ui/js/component/app/view.jsx +++ b/ui/js/component/app/view.jsx @@ -2,25 +2,64 @@ import React from "react"; import Router from "component/router"; import Header from "component/header"; import ModalError from "component/modalError"; +import ModalAuthFailure from "component/modalAuthFailure"; import ModalDownloading from "component/modalDownloading"; -import UpgradeModal from "component/modalUpgrade"; -import WelcomeModal from "component/modalWelcome"; +import ModalInsufficientCredits from "component/modalInsufficientCredits"; +import ModalUpgrade from "component/modalUpgrade"; +import ModalWelcome from "component/modalWelcome"; +import ModalFirstReward from "component/modalFirstReward"; import lbry from "lbry"; -import { Line } from "rc-progress"; +import * as modals from "constants/modal_types"; class App extends React.PureComponent { componentWillMount() { + const { + alertError, + checkUpgradeAvailable, + updateBalance, + fetchRewardedContent, + } = this.props; + document.addEventListener("unhandledError", event => { - this.props.alertError(event.detail); + alertError(event.detail); }); if (!this.props.upgradeSkipped) { - this.props.checkUpgradeAvailable(); + checkUpgradeAvailable(); } lbry.balanceSubscribe(balance => { - this.props.updateBalance(balance); + updateBalance(balance); }); + + fetchRewardedContent(); + + this.showWelcome(this.props); + + this.scrollListener = () => this.props.recordScroll(window.scrollY); + + window.addEventListener("scroll", this.scrollListener); + } + + componentWillReceiveProps(nextProps) { + this.showWelcome(nextProps); + } + + showWelcome(props) { + const { isWelcomeAcknowledged, openWelcomeModal, user } = props; + + if ( + !isWelcomeAcknowledged && + user && + !user.is_reward_approved && + !user.is_identity_verified + ) { + openWelcomeModal(); + } + } + + componentWillUnmount() { + window.removeEventListener("scroll", this.scrollListener); } render() { @@ -32,10 +71,13 @@ class App extends React.PureComponent {
- {modal == "upgrade" && } - {modal == "downloading" && } - {modal == "error" && } - {modal == "welcome" && } + {modal == modals.UPGRADE && } + {modal == modals.DOWNLOADING && } + {modal == modals.ERROR && } + {modal == modals.INSUFFICIENT_CREDITS && } + {modal == modals.WELCOME && } + {modal == modals.FIRST_REWARD && } + {modal == modals.AUTHENTICATION_FAILURE && } ); } diff --git a/ui/js/component/auth/index.js b/ui/js/component/auth/index.js deleted file mode 100644 index 37af9f90f..000000000 --- a/ui/js/component/auth/index.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import { connect } from "react-redux"; -import { - selectAuthenticationIsPending, - selectEmailToVerify, - selectUserIsVerificationCandidate, -} from "selectors/user"; -import Auth from "./view"; - -const select = state => ({ - isPending: selectAuthenticationIsPending(state), - email: selectEmailToVerify(state), - isVerificationCandidate: selectUserIsVerificationCandidate(state), -}); - -export default connect(select, null)(Auth); diff --git a/ui/js/component/auth/view.jsx b/ui/js/component/auth/view.jsx deleted file mode 100644 index 551113ffa..000000000 --- a/ui/js/component/auth/view.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react"; -import { BusyMessage } from "component/common"; -import UserEmailNew from "component/userEmailNew"; -import UserEmailVerify from "component/userEmailVerify"; - -export class Auth extends React.PureComponent { - render() { - const { isPending, email, isVerificationCandidate } = this.props; - - if (isPending) { - return ; - } else if (!email) { - return ; - } else if (isVerificationCandidate) { - return ; - } else { - return {__("No further steps.")}; - } - } -} - -export default Auth; diff --git a/ui/js/component/authOverlay/index.jsx b/ui/js/component/authOverlay/index.jsx deleted file mode 100644 index 28b49333c..000000000 --- a/ui/js/component/authOverlay/index.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; -import * as modal from "constants/modal_types"; -import rewards from "rewards.js"; -import { connect } from "react-redux"; -import { doUserEmailDecline } from "actions/user"; -import { doOpenModal } from "actions/app"; -import { - selectAuthenticationIsPending, - selectUserHasEmail, - selectUserIsAuthRequested, -} from "selectors/user"; -import { makeSelectHasClaimedReward } from "selectors/rewards"; -import AuthOverlay from "./view"; - -const select = (state, props) => { - const selectHasClaimed = makeSelectHasClaimedReward(); - - return { - hasEmail: selectUserHasEmail(state), - isPending: selectAuthenticationIsPending(state), - isShowing: selectUserIsAuthRequested(state), - hasNewUserReward: selectHasClaimed(state, { - reward_type: rewards.TYPE_NEW_USER, - }), - }; -}; - -const perform = dispatch => ({ - userEmailDecline: () => dispatch(doUserEmailDecline()), - openWelcomeModal: () => dispatch(doOpenModal(modal.WELCOME)), -}); - -export default connect(select, perform)(AuthOverlay); diff --git a/ui/js/component/authOverlay/view.jsx b/ui/js/component/authOverlay/view.jsx deleted file mode 100644 index 753fe2fe4..000000000 --- a/ui/js/component/authOverlay/view.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from "react"; -import lbryio from "lbryio.js"; -import ModalPage from "component/modal-page.js"; -import Auth from "component/auth"; -import Link from "component/link"; -import { getLocal, setLocal } from "utils"; - -export class AuthOverlay extends React.PureComponent { - constructor(props) { - super(props); - - this.state = { - showNoEmailConfirm: false, - }; - } - - componentWillReceiveProps(nextProps) { - if ( - this.props.isShowing && - !this.props.isPending && - !nextProps.hasNewUserReward && - !nextProps.isShowing /* && !getLocal("welcome_screen_shown")*/ - ) { - setLocal("welcome_screen_shown", true); - setTimeout(() => this.props.openWelcomeModal(), 1); - } - } - - onEmailSkipClick() { - this.setState({ showNoEmailConfirm: true }); - } - - onEmailSkipConfirm() { - this.props.userEmailDecline(); - } - - render() { - if (!lbryio.enabled) { - return null; - } - - const { isPending, isShowing, hasEmail } = this.props; - - if (isShowing) { - return ( - -

LBRY Early Access

- - {isPending - ? "" - :
- {!hasEmail && this.state.showNoEmailConfirm - ?
-

- {__( - "If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications." - )} -

- { - this.onEmailSkipConfirm(); - }} - label={__("Continue without email")} - /> -
- : { - hasEmail - ? this.onEmailSkipConfirm() - : this.onEmailSkipClick(); - }} - label={ - hasEmail ? __("Skip for now") : __("Do I have to?") - } - />} -
} -
- ); - } - - return null; - } -} - -export default AuthOverlay; diff --git a/ui/js/component/cardMedia/index.js b/ui/js/component/cardMedia/index.js new file mode 100644 index 000000000..3616b0331 --- /dev/null +++ b/ui/js/component/cardMedia/index.js @@ -0,0 +1,8 @@ +import React from "react"; +import { connect } from "react-redux"; +import CardMedia from "./view"; + +const select = state => ({}); +const perform = dispatch => ({}); + +export default connect(select, perform)(CardMedia); diff --git a/ui/js/component/cardMedia/view.jsx b/ui/js/component/cardMedia/view.jsx new file mode 100644 index 000000000..d0f45f4ac --- /dev/null +++ b/ui/js/component/cardMedia/view.jsx @@ -0,0 +1,54 @@ +import React from "react"; + +class CardMedia extends React.PureComponent { + static AUTO_THUMB_CLASSES = [ + "purple", + "red", + "pink", + "indigo", + "blue", + "light-blue", + "cyan", + "teal", + "green", + "yellow", + "orange", + ]; + + componentWillMount() { + this.setState({ + autoThumbClass: + CardMedia.AUTO_THUMB_CLASSES[ + Math.floor(Math.random() * CardMedia.AUTO_THUMB_CLASSES.length) + ], + }); + } + + render() { + const { title, thumbnail } = this.props; + const atClass = this.state.autoThumbClass; + + if (thumbnail) { + return ( +
+ ); + } + + return ( +
+
+ {title && + title + .replace(/\s+/g, "") + .substring(0, Math.min(title.replace(" ", "").length, 5)) + .toUpperCase()} +
+
+ ); + } +} + +export default CardMedia; diff --git a/ui/js/component/cardVerify/index.js b/ui/js/component/cardVerify/index.js new file mode 100644 index 000000000..32cd22aef --- /dev/null +++ b/ui/js/component/cardVerify/index.js @@ -0,0 +1,12 @@ +import React from "react"; +import { connect } from "react-redux"; +import { selectUserEmail } from "selectors/user"; +import CardVerify from "./view"; + +const select = state => ({ + email: selectUserEmail(state), +}); + +const perform = dispatch => ({}); + +export default connect(select, perform)(CardVerify); diff --git a/ui/js/component/cardVerify/view.jsx b/ui/js/component/cardVerify/view.jsx new file mode 100644 index 000000000..0432a69e1 --- /dev/null +++ b/ui/js/component/cardVerify/view.jsx @@ -0,0 +1,180 @@ +import React from "react"; +import PropTypes from "prop-types"; +import Link from "component/link"; + +let scriptLoading = false; +let scriptLoaded = false; +let scriptDidError = false; + +class CardVerify extends React.Component { + static propTypes = { + disabled: PropTypes.bool, + + label: PropTypes.string, + + // ===================================================== + // Required by stripe + // see Stripe docs for more info: + // https://stripe.com/docs/checkout#integration-custom + // ===================================================== + + // Your publishable key (test or live). + // can't use "key" as a prop in react, so have to change the keyname + stripeKey: PropTypes.string.isRequired, + + // The callback to invoke when the Checkout process is complete. + // function(token) + // token is the token object created. + // token.id can be used to create a charge or customer. + // token.email contains the email address entered by the user. + token: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + open: false, + }; + } + + componentDidMount() { + if (scriptLoaded) { + return; + } + + if (scriptLoading) { + return; + } + + scriptLoading = true; + + const script = document.createElement("script"); + script.src = "https://checkout.stripe.com/checkout.js"; + script.async = 1; + + this.loadPromise = (() => { + let canceled = false; + const promise = new Promise((resolve, reject) => { + script.onload = () => { + scriptLoaded = true; + scriptLoading = false; + resolve(); + this.onScriptLoaded(); + }; + script.onerror = event => { + scriptDidError = true; + scriptLoading = false; + reject(event); + this.onScriptError(event); + }; + }); + const wrappedPromise = new Promise((accept, cancel) => { + promise.then( + () => (canceled ? cancel({ isCanceled: true }) : accept()) + ); + promise.catch( + error => (canceled ? cancel({ isCanceled: true }) : cancel(error)) + ); + }); + + return { + promise: wrappedPromise, + cancel() { + canceled = true; + }, + }; + })(); + + this.loadPromise.promise + .then(this.onScriptLoaded) + .catch(this.onScriptError); + + document.body.appendChild(script); + } + + componentDidUpdate() { + if (!scriptLoading) { + this.updateStripeHandler(); + } + } + + componentWillUnmount() { + if (this.loadPromise) { + this.loadPromise.cancel(); + } + if (CardVerify.stripeHandler && this.state.open) { + CardVerify.stripeHandler.close(); + } + } + + onScriptLoaded = () => { + if (!CardVerify.stripeHandler) { + CardVerify.stripeHandler = StripeCheckout.configure({ + key: this.props.stripeKey, + }); + if (this.hasPendingClick) { + this.showStripeDialog(); + } + } + }; + + onScriptError = (...args) => { + throw new Error("Unable to load credit validation script."); + }; + + onClosed = () => { + this.setState({ open: false }); + }; + + updateStripeHandler() { + if (!CardVerify.stripeHandler) { + CardVerify.stripeHandler = StripeCheckout.configure({ + key: this.props.stripeKey, + }); + } + } + + showStripeDialog() { + this.setState({ open: true }); + CardVerify.stripeHandler.open({ + allowRememberMe: false, + closed: this.onClosed, + description: __("Confirm Identity"), + email: this.props.email, + locale: "auto", + panelLabel: "Verify", + token: this.props.token, + zipCode: true, + }); + } + + onClick = () => { + if (scriptDidError) { + try { + throw new Error( + "Tried to call onClick, but StripeCheckout failed to load" + ); + } catch (x) {} + } else if (CardVerify.stripeHandler) { + this.showStripeDialog(); + } else { + this.hasPendingClick = true; + } + }; + + render() { + return ( + + ); + } +} + +export default CardVerify; diff --git a/ui/js/component/common.js b/ui/js/component/common.js index 38dbf83fd..8e7279248 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -1,4 +1,5 @@ import React from "react"; +import { formatCredits } from "utils"; import lbry from "../lbry.js"; //component/icon.js @@ -78,7 +79,7 @@ export class CreditAmount extends React.PureComponent { }; render() { - const formattedAmount = lbry.formatCredits( + const formattedAmount = formatCredits( this.props.amount, this.props.precision ); @@ -140,7 +141,7 @@ export class Address extends React.PureComponent { }} style={addressStyle} readOnly="readonly" - value={this.props.address} + value={this.props.address || ""} /> ); } diff --git a/ui/js/component/fileActions/index.js b/ui/js/component/fileActions/index.js index 769a1390d..4311823d1 100644 --- a/ui/js/component/fileActions/index.js +++ b/ui/js/component/fileActions/index.js @@ -13,7 +13,7 @@ import { doCloseModal, doOpenModal } from "actions/app"; import { doFetchAvailability } from "actions/availability"; import { doOpenFileInShell, doOpenFileInFolder } from "actions/file_info"; import { makeSelectClaimForUriIsMine } from "selectors/claims"; -import { doPurchaseUri, doLoadVideo } from "actions/content"; +import { doPurchaseUri, doLoadVideo, doStartDownload } from "actions/content"; import FileActions from "./view"; const makeSelect = () => { @@ -47,6 +47,7 @@ const perform = dispatch => ({ openModal: modal => dispatch(doOpenModal(modal)), startDownload: uri => dispatch(doPurchaseUri(uri, "affirmPurchase")), loadVideo: uri => dispatch(doLoadVideo(uri)), + restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)), }); export default connect(makeSelect, perform)(FileActions); diff --git a/ui/js/component/fileActions/view.jsx b/ui/js/component/fileActions/view.jsx index 53188a1f5..df624a345 100644 --- a/ui/js/component/fileActions/view.jsx +++ b/ui/js/component/fileActions/view.jsx @@ -22,6 +22,21 @@ class FileActions extends React.PureComponent { componentWillReceiveProps(nextProps) { this.checkAvailability(nextProps.uri); + this.restartDownload(nextProps); + } + + restartDownload(props) { + const { downloading, fileInfo, uri, restartDownload } = props; + + if ( + !downloading && + fileInfo && + !fileInfo.completed && + fileInfo.written_bytes !== false && + fileInfo.written_bytes < fileInfo.total_bytes + ) { + restartDownload(uri, fileInfo.outpoint); + } } checkAvailability(uri) { @@ -142,6 +157,8 @@ class FileActions extends React.PureComponent { onClick={() => openInShell(fileInfo)} /> ); + } else if (!fileInfo) { + content = ; } else { console.log("handle this case of file action props?"); } @@ -176,13 +193,6 @@ class FileActions extends React.PureComponent { {" "} {__("credits")}. - - {__("You don't have enough LBRY credits to pay for this stream.")} - { @@ -22,6 +25,7 @@ const makeSelect = () => { fileInfo: selectFileInfoForUri(state, props), obscureNsfw: !selectShowNsfw(state), metadata: selectMetadataForUri(state, props), + rewardedContentClaimIds: selectRewardContentClaimIds(state, props), isResolvingUri: selectResolvingUri(state, props), }); diff --git a/ui/js/component/fileCard/view.jsx b/ui/js/component/fileCard/view.jsx index 42c0e5b68..eb29a1b2e 100644 --- a/ui/js/component/fileCard/view.jsx +++ b/ui/js/component/fileCard/view.jsx @@ -1,10 +1,13 @@ import React from "react"; import lbryuri from "lbryuri.js"; +import CardMedia from "component/cardMedia"; import Link from "component/link"; import { TruncatedText, Icon } from "component/common"; +import IconFeatured from "component/iconFeatured"; import FilePrice from "component/filePrice"; import UriIndicator from "component/uriIndicator"; import NsfwOverlay from "component/nsfwOverlay"; +import TruncatedMarkdown from "component/truncatedMarkdown"; class FileCard extends React.PureComponent { constructor(props) { @@ -44,11 +47,23 @@ class FileCard extends React.PureComponent { } render() { - const { claim, fileInfo, metadata, isResolvingUri, navigate } = this.props; + const { + claim, + fileInfo, + metadata, + isResolvingUri, + navigate, + rewardedContentClaimIds, + } = this.props; const uri = lbryuri.normalize(this.props.uri); const title = metadata && metadata.title ? metadata.title : uri; + const thumbnail = metadata && metadata.thumbnail + ? metadata.thumbnail + : null; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; + const isRewardContent = + claim && rewardedContentClaimIds.includes(claim.claim_id); let description = ""; if (isResolvingUri && !claim) { @@ -73,28 +88,23 @@ class FileCard extends React.PureComponent { onClick={() => navigate("/show", { uri })} className="card__link" > +
-
+
{title} -
+
- {fileInfo - ? {" "} - : ""} + {isRewardContent && {" "}} + {fileInfo && + {" "}}
- {metadata && - metadata.thumbnail && -
}
- {description} + {description}
diff --git a/ui/js/component/fileList/view.jsx b/ui/js/component/fileList/view.jsx index 20631b59e..cc397ddbe 100644 --- a/ui/js/component/fileList/view.jsx +++ b/ui/js/component/fileList/view.jsx @@ -2,7 +2,7 @@ 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 FormField from "component/formField"; import FileTile from "component/fileTile"; import rewards from "rewards.js"; import lbryio from "lbryio.js"; @@ -67,7 +67,9 @@ class FileList extends React.PureComponent { const content = []; this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => { - let uriParams = {}; + let uriParams = { + claimId: fileInfo.claim_id, + }; if (fileInfo.channel_name) { uriParams.channelName = fileInfo.channel_name; uriParams.contentName = fileInfo.name; @@ -79,7 +81,7 @@ class FileList extends React.PureComponent { content.push( - {content} diff --git a/ui/js/component/fileListSearch/view.jsx b/ui/js/component/fileListSearch/view.jsx index bd8efeb00..682d07e7c 100644 --- a/ui/js/component/fileListSearch/view.jsx +++ b/ui/js/component/fileListSearch/view.jsx @@ -67,7 +67,7 @@ class FileListSearch extends React.PureComponent { {results && !!results.length ? - : } + : !isSearching && } ); } diff --git a/ui/js/component/fileTile/index.js b/ui/js/component/fileTile/index.js index 6e9936fe1..0631ae849 100644 --- a/ui/js/component/fileTile/index.js +++ b/ui/js/component/fileTile/index.js @@ -8,7 +8,10 @@ import { } from "selectors/claims"; import { makeSelectFileInfoForUri } from "selectors/file_info"; import { selectShowNsfw } from "selectors/settings"; -import { makeSelectIsResolvingForUri } from "selectors/content"; +import { + makeSelectIsResolvingForUri, + selectRewardContentClaimIds, +} from "selectors/content"; import FileTile from "./view"; const makeSelect = () => { @@ -23,6 +26,7 @@ const makeSelect = () => { obscureNsfw: !selectShowNsfw(state), metadata: selectMetadataForUri(state, props), isResolvingUri: selectResolvingUri(state, props), + rewardedContentClaimIds: selectRewardContentClaimIds(state, props), }); return select; diff --git a/ui/js/component/fileTile/view.jsx b/ui/js/component/fileTile/view.jsx index c0c25a202..c57172408 100644 --- a/ui/js/component/fileTile/view.jsx +++ b/ui/js/component/fileTile/view.jsx @@ -1,10 +1,11 @@ import React from "react"; -import lbry from "lbry.js"; import lbryuri from "lbryuri.js"; +import CardMedia from "component/cardMedia"; import Link from "component/link"; import { TruncatedText } from "component/common.js"; import FilePrice from "component/filePrice"; import NsfwOverlay from "component/nsfwOverlay"; +import IconFeatured from "component/iconFeatured"; class FileTile extends React.PureComponent { static SHOW_EMPTY_PUBLISH = "publish"; @@ -57,6 +58,7 @@ class FileTile extends React.PureComponent { showEmpty, navigate, hidePrice, + rewardedContentClaimIds, } = this.props; const uri = lbryuri.normalize(this.props.uri); @@ -64,8 +66,14 @@ class FileTile extends React.PureComponent { const isClaimable = lbryuri.isClaimable(uri); const title = isClaimed && metadata && metadata.title ? metadata.title - : uri; + : lbryuri.parse(uri).contentName; + const thumbnail = metadata && metadata.thumbnail + ? metadata.thumbnail + : null; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; + const isRewardContent = + claim && rewardedContentClaimIds.includes(claim.claim_id); + let onClick = () => navigate("/show", { uri }); let description = ""; @@ -98,22 +106,15 @@ class FileTile extends React.PureComponent { >
-
+
{!hidePrice ? : null} + {isRewardContent && }
{uri}
-

{title}

+

+ {title} +

diff --git a/ui/js/component/form.js b/ui/js/component/form.js index 7ab78325c..d47d7445a 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -1,181 +1,32 @@ import React from "react"; -import FileSelector from "./file-selector.js"; -import { Icon } from "./common.js"; +import FormField from "component/formField"; -var formFieldCounter = 0, - formFieldFileSelectorTypes = ["file", "directory"], - formFieldNestedLabelTypes = ["radio", "checkbox"]; +let formFieldCounter = 0; -function formFieldId() { +export const formFieldNestedLabelTypes = ["radio", "checkbox"]; + +export function formFieldId() { return "form-field-" + ++formFieldCounter; } -export class FormField extends React.PureComponent { - static propTypes = { - type: React.PropTypes.string.isRequired, - prefix: React.PropTypes.string, - postfix: React.PropTypes.string, - hasError: React.PropTypes.bool, - }; - - constructor(props) { - super(props); - - this._fieldRequiredText = __("This field is required"); - this._type = null; - this._element = null; - - this.state = { - isError: null, - errorMessage: null, - }; - } - - componentWillMount() { - if (["text", "number", "radio", "checkbox"].includes(this.props.type)) { - this._element = "input"; - this._type = this.props.type; - } else if (this.props.type == "text-number") { - this._element = "input"; - this._type = "text"; - } else if (formFieldFileSelectorTypes.includes(this.props.type)) { - this._element = "input"; - this._type = "hidden"; - } else { - // Non field, e.g.