diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d7578e6b9..eae458369 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.14.3 +current_version = 0.15.0 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/CHANGELOG.md b/CHANGELOG.md index fa92bcbda..b38bfb0f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,40 +9,72 @@ Web UI version numbers should always match the corresponding version of LBRY App ## [Unreleased] ### Added * Added a tipping button to send LBRY Credits to the publisher - * Added a forward button and improved history behavior. Back/forward disable when unusable. - * Added a new component, `FormFieldPrice` which is now used in Publish and Settings. - * Added wallet backup guide reference. - + * + ### Changed - * Updated to daemon [0.15](https://github.com/lbryio/lbry/releases). Most relevant changes for app are improved announcing of content and a fix for the daemon getting stuck running. - * Continued to refine first-run process, process for new users, and introducing people to LBRY and LBRY credits. - * Changed the default price settings. - * 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. - * Some form field refactoring as we take baby steps towards form sanity. - * Replaced confusing placeholder text from email input. - * Refactored modal and settings logic. - * Updated several packages and fixed warnings in build process (all but the [fsevents warning](https://github.com/yarnpkg/yarn/issues/3738), which is a rather dramatic debate) + * + * ### 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 - * Public page now properly checks for all required fields are filled - * Fixed pagination styling for pages > 5 (#416) - * Fixed sizing on squat videos (#419) - * Support claims no longer show up on Published page (#384) - * Fixed rendering of small prices (#461) - * Fixed incorrect URI in Downloads/Published page (#460) - * Fixed menu bug (#503) + * + * ### Deprecated * * ### Removed - * Removed the label "Max Purchase Price" from settings page. It was redundant. - * Unused old files from previous commit(9c3d633) * + * + +## [0.15.0] - 2017-08-31 + +### Added + * Added an Invites area inside of the Wallet. This allows users to invite others and shows the status of all past invites (including all invite data from the past year). Up to one referral reward can now be claimed, but only if both users have passed the humanity test. + * Added new summary components for rewards and invites to the Wallet landing page. + * Added a forward button and improved history behavior. Back/forward disable when unusable. + * Added past history of rewards to the rewards page. + * Added wallet backup guide reference. + * Added a new widget for setting prices (`FormFieldPrice`), used in Publish and Settings. + + +### Changed + * Updated to daemon [0.15](https://github.com/lbryio/lbry/releases). Most relevant changes for app are improved announcing of content and a fix for the daemon getting stuck running. + * Significant refinements to first-run process, process for new users, and introducing people to LBRY and LBRY credits. + * Changed Wallet landing page to summarize status of other areas. Refactored wallet and transaction logic. + * Added icons to missing page, improved icon and title logic. + * Changed the default price settings for priced publishes. + * 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. + * Updated several packages and fixed warnings in build process (all but the [fsevents warning](https://github.com/yarnpkg/yarn/issues/3738), which is a rather dramatic debate) + * Some form field refactoring as we take baby steps towards form sanity. + * Replaced confusing placeholder text from email input. + * Refactored modal and settings logic. + * Refactored history and navigation logic. + + +### Removed + * Removed the label "Max Purchase Price" from settings page. It was redundant. + * Unused old files from previous commit(9c3d633) + + +### 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 + * Publish page now properly checks for all required fields are filled + * Fixed pagination styling for pages > 5 (#416) + * Fixed sizing on squat videos (#419) + * Support claims no longer show up on Published page (#384) + * Fixed rendering of small prices (#461) + * Fixed incorrect URI in Downloads/Published page (#460) + * Fixed menu bug (#503) + * Fixed incorrect URLs on some channel content (#505) + * Fixed video sizing for squat videos (#492) + * Fixed issues with small prices (#461) + * Fixed issues with negative values not being stopped by app on entry (#441) + * Fixed source file error when editing existing claim (#467) + + ## [0.14.3] - 2017-08-03 diff --git a/app/main.js b/app/main.js index 27c8f72c8..b572eba82 100644 --- a/app/main.js +++ b/app/main.js @@ -51,7 +51,7 @@ let daemonStopRequested = false; let readyToQuit = false; // If we receive a URI to open from an external app but there's no window to -// send it to, it's cached in this variable. +// sendCredits it to, it's cached in this variable. let openUri = null; function processRequestedUri(uri) { diff --git a/app/package.json b/app/package.json index 7739fd667..e1293e5ac 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.14.3", + "version": "0.15.0", "main": "main.js", "description": "A browser for the LBRY network, a digital marketplace controlled by its users.", "author": { diff --git a/ui/dist/font/FontAwesome.otf b/ui/dist/font/FontAwesome.otf index f7936cc1e..401ec0f36 100644 Binary files a/ui/dist/font/FontAwesome.otf and b/ui/dist/font/FontAwesome.otf differ diff --git a/ui/dist/font/fontawesome-webfont.eot b/ui/dist/font/fontawesome-webfont.eot index 33b2bb800..e9f60ca95 100644 Binary files a/ui/dist/font/fontawesome-webfont.eot and b/ui/dist/font/fontawesome-webfont.eot differ diff --git a/ui/dist/font/fontawesome-webfont.svg b/ui/dist/font/fontawesome-webfont.svg index 1ee89d436..855c845e5 100644 --- a/ui/dist/font/fontawesome-webfont.svg +++ b/ui/dist/font/fontawesome-webfont.svg @@ -1,565 +1,2671 @@ - - + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/dist/font/fontawesome-webfont.ttf b/ui/dist/font/fontawesome-webfont.ttf index ed9372f8e..35acda2fa 100644 Binary files a/ui/dist/font/fontawesome-webfont.ttf and b/ui/dist/font/fontawesome-webfont.ttf differ diff --git a/ui/dist/font/fontawesome-webfont.woff b/ui/dist/font/fontawesome-webfont.woff index 8b280b98f..400014a4b 100644 Binary files a/ui/dist/font/fontawesome-webfont.woff and b/ui/dist/font/fontawesome-webfont.woff differ diff --git a/ui/dist/font/fontawesome-webfont.woff2 b/ui/dist/font/fontawesome-webfont.woff2 index 3311d5851..4d13fc604 100644 Binary files a/ui/dist/font/fontawesome-webfont.woff2 and b/ui/dist/font/fontawesome-webfont.woff2 differ diff --git a/ui/js/actions/app.js b/ui/js/actions/app.js index 34cceed37..3d81d6c69 100644 --- a/ui/js/actions/app.js +++ b/ui/js/actions/app.js @@ -5,18 +5,10 @@ import { selectUpgradeDownloadPath, selectUpgradeDownloadItem, selectUpgradeFilename, - selectPageTitle, - selectCurrentPage, - selectCurrentParams, - selectHistoryBack, - selectHistoryForward, } from "selectors/app"; -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"); @@ -24,126 +16,6 @@ const { download } = remote.require("electron-dl"); const fs = remote.require("fs"); const { lbrySettings: config } = require("../../../app/package.json"); -export function doNavigate(path, params = {}, options = {}) { - return function(dispatch, getState) { - let url = path; - if (params) url = `${url}?${toQueryString(params)}`; - - dispatch(doChangePath(url)); - - const state = getState(); - const pageTitle = selectPageTitle(state); - const historyState = history.state; - - dispatch( - doHistoryPush({ params, page: historyState.page + 1 }, pageTitle, url) - ); - }; -} - -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, - data: { - path, - }, - }); - - const state = getState(); - const pageTitle = selectPageTitle(state); - const scrollY = options.scrollY; - - window.document.title = pageTitle; - - if (scrollY) window.scrollTo(0, scrollY); - else window.scrollTo(0, 0); - - const currentPage = selectCurrentPage(state); - if (currentPage === "search") { - const params = selectCurrentParams(state); - dispatch(doSearch(params.query)); - } - }; -} - -export function doHistoryBack() { - return function(dispatch, getState) { - // Get back history from stack - const back = selectHistoryBack(getState()); - - if (back) { - // Set location - dispatch(doChangePath(back.location)); - - dispatch({ - type: types.HISTORY_NAVIGATE, - data: { page: back }, - }); - } - }; -} - -export function doHistoryForward() { - return function(dispatch, getState) { - // Get forward history from stack - const forward = selectHistoryForward(getState()); - - if (forward) { - // Set location - dispatch(doChangePath(forward.location)); - - dispatch({ - type: types.HISTORY_NAVIGATE, - data: { page: forward }, - }); - } - }; -} - -export function doHistoryPush(currentState, title, relativeUrl) { - return function(dispatch, getState) { - title += " - LBRY"; - history.pushState(currentState, title, `#${relativeUrl}`); - dispatch({ - type: types.HISTORY_NAVIGATE, - data: { - location: 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}` - ); - }; -} - export function doOpenModal(modal) { return { type: types.OPEN_MODAL, @@ -301,25 +173,8 @@ export function doAlertError(errorList) { export function doDaemonReady() { return function(dispatch, getState) { - const path = window.location.hash || "#/discover"; - const params = parseQueryParams(path.split("?")[1] || ""); - - // Get first page - const page = { - index: 0, - location: path.replace(/^#/, ""), - }; - - history.replaceState( - { params, is_first_page: true, page: 1 }, - document.title, - `${path}` - ); dispatch(doAuthenticate()); - dispatch({ - type: types.DAEMON_READY, - data: { page }, - }); + dispatch({ type: types.DAEMON_READY }); dispatch(doFetchDaemonSettings()); dispatch(doFileList()); }; diff --git a/ui/js/actions/content.js b/ui/js/actions/content.js index 95ebf28f1..69c717e88 100644 --- a/ui/js/actions/content.js +++ b/ui/js/actions/content.js @@ -20,6 +20,8 @@ import * as modals from "constants/modal_types"; const { ipcRenderer } = require("electron"); +const DOWNLOAD_POLL_INTERVAL = 250; + export function doResolveUri(uri) { return function(dispatch, getState) { uri = lbryuri.normalize(uri); @@ -175,7 +177,7 @@ export function doUpdateLoadStatus(uri, outpoint) { // download hasn't started yet setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)); - }, 250); + }, DOWNLOAD_POLL_INTERVAL); } else if (fileInfo.completed) { // TODO this isn't going to get called if they reload the client before // the download finished @@ -221,7 +223,7 @@ export function doUpdateLoadStatus(uri, outpoint) { setTimeout(() => { dispatch(doUpdateLoadStatus(uri, outpoint)); - }, 250); + }, DOWNLOAD_POLL_INTERVAL); } }); }; @@ -303,6 +305,7 @@ export function doLoadVideo(uri) { } }) .catch(error => { + console.log(error); dispatch({ type: types.LOADING_VIDEO_FAILED, data: { uri }, diff --git a/ui/js/actions/cost_info.js b/ui/js/actions/cost_info.js index 42025718f..6568a3575 100644 --- a/ui/js/actions/cost_info.js +++ b/ui/js/actions/cost_info.js @@ -35,7 +35,11 @@ export function doFetchCostInfoForUri(uri) { if (isGenerous && claim) { let cost; - const fee = claim.value.stream.metadata.fee; + const fee = claim.value && + claim.value.stream && + claim.value.stream.metadata + ? claim.value.stream.metadata.fee + : undefined; if (fee === undefined) { resolve({ cost: 0, includesData: true }); } else if (fee.currency == "LBC") { diff --git a/ui/js/actions/file_info.js b/ui/js/actions/file_info.js index c69db230c..7d9d102c6 100644 --- a/ui/js/actions/file_info.js +++ b/ui/js/actions/file_info.js @@ -12,7 +12,8 @@ import { selectUrisLoading, selectTotalDownloadProgress, } from "selectors/file_info"; -import { doCloseModal, doHistoryBack } from "actions/app"; +import { doCloseModal } from "actions/app"; +import { doHistoryBack } from "actions/navigation"; import setProgressBar from "util/setProgressBar"; import batchActions from "util/batchActions"; diff --git a/ui/js/actions/navigation.js b/ui/js/actions/navigation.js new file mode 100644 index 000000000..b34a2e876 --- /dev/null +++ b/ui/js/actions/navigation.js @@ -0,0 +1,104 @@ +import * as types from "constants/action_types"; +import { + selectPageTitle, + selectCurrentPage, + selectCurrentParams, + selectHistoryStack, + selectHistoryIndex, +} from "selectors/navigation"; +import { doSearch } from "actions/search"; +import { toQueryString } from "util/query_params"; + +export function doNavigate(path, params = {}, options = {}) { + return function(dispatch, getState) { + if (!path) { + return; + } + + let url = path; + if (params && Object.values(params).length) { + url += "?" + toQueryString(params); + } + + dispatch(doChangePath(url, options)); + + const pageTitle = selectPageTitle(getState()) + " - LBRY"; + + dispatch({ + type: types.HISTORY_NAVIGATE, + data: { url, index: options.index }, + }); + }; +} + +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, + data: { + path, + }, + }); + + const state = getState(); + const scrollY = options.scrollY; + + //I wasn't seeing it scroll to the proper position without this -- possibly because the page isn't fully rendered? Not sure - Jeremy + setTimeout(() => { + window.scrollTo(0, scrollY ? scrollY : 0); + }, 100); + + const currentPage = selectCurrentPage(state); + if (currentPage === "search") { + const params = selectCurrentParams(state); + dispatch(doSearch(params.query)); + } + }; +} + +export function doHistoryTraverse(dispatch, state, modifier) { + const stack = selectHistoryStack(state), + index = selectHistoryIndex(state) + modifier; + + if (index >= 0 && index < stack.length) { + const historyItem = stack[index]; + return dispatch( + doNavigate(historyItem.path, {}, { scrollY: historyItem.scrollY, index }) + ); + } +} + +export function doHistoryBack() { + return function(dispatch, getState) { + return doHistoryTraverse(dispatch, getState(), -1); + }; +} + +export function doHistoryForward() { + return function(dispatch, getState) { + return doHistoryTraverse(dispatch, getState(), 1); + }; +} + +export function doRecordScroll(scroll) { + return function(dispatch, getState) { + dispatch({ + type: types.WINDOW_SCROLLED, + data: { scrollY: scroll }, + }); + }; +} diff --git a/ui/js/actions/rewards.js b/ui/js/actions/rewards.js index 95ae3b25b..5d43c245f 100644 --- a/ui/js/actions/rewards.js +++ b/ui/js/actions/rewards.js @@ -2,7 +2,8 @@ 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"; +import { selectUnclaimedRewardsByType } from "selectors/rewards"; +import { selectUserIsRewardApproved } from "selectors/user"; export function doRewardList() { return function(dispatch, getState) { @@ -13,7 +14,7 @@ export function doRewardList() { }); lbryio - .call("reward", "list", {}) + .call("reward", "list", { multiple_rewards_per_type: true }) .then(userRewards => { dispatch({ type: types.FETCH_REWARDS_COMPLETED, @@ -31,22 +32,23 @@ export function doRewardList() { export function doClaimRewardType(rewardType) { return function(dispatch, getState) { - const rewardsByType = selectRewardsByType(getState()), - reward = rewardsByType[rewardType]; + const state = getState(), + rewardsByType = selectUnclaimedRewardsByType(state), + reward = rewardsByType[rewardType], + userIsRewardApproved = selectUserIsRewardApproved(state); - if (reward) { - dispatch(doClaimReward(reward)); - } - }; -} - -export function doClaimReward(reward, saveError = false) { - return function(dispatch, getState) { if (reward.transaction_id) { //already claimed, do nothing return; } + if (!userIsRewardApproved) { + return dispatch({ + type: types.OPEN_MODAL, + data: { modal: modals.REWARD_APPROVAL_REQUIRED }, + }); + } + dispatch({ type: types.CLAIM_REWARD_STARTED, data: { reward }, @@ -70,43 +72,34 @@ export function doClaimReward(reward, saveError = false) { const failure = error => { dispatch({ type: types.CLAIM_REWARD_FAILURE, - data: { - reward, - error: saveError ? error : null, - }, + data: { reward, error }, }); }; - rewards.claimReward(reward.reward_type).then(success, failure); + rewards.claimReward(rewardType).then(success, failure); }; } export function doClaimEligiblePurchaseRewards() { return function(dispatch, getState) { - if (!lbryio.enabled) { + const state = getState(), + rewardsByType = selectUnclaimedRewardsByType(state), + userIsRewardApproved = selectUserIsRewardApproved(state); + + if (!userIsRewardApproved || !lbryio.enabled) { return; } - const rewardsByType = selectRewardsByType(getState()); - - let types = {}; - - types[rewards.TYPE_FIRST_STREAM] = false; - types[rewards.TYPE_FEATURED_DOWNLOAD] = false; - types[rewards.TYPE_MANY_DOWNLOADS] = false; - Object.values(rewardsByType).forEach(reward => { - if (types[reward.reward_type] === false && reward.transaction_id) { - types[reward.reward_type] = true; - } - }); - - let unclaimedType = Object.keys(types).find(type => { - return types[type] === false && type !== rewards.TYPE_FEATURED_DOWNLOAD; //handled below - }); - if (unclaimedType) { - dispatch(doClaimRewardType(unclaimedType)); + if (rewardsByType[rewards.TYPE_FIRST_STREAM]) { + dispatch(doClaimRewardType(rewards.TYPE_FIRST_STREAM)); + } else { + [ + rewards.TYPE_MANY_DOWNLOADS, + rewards.TYPE_FEATURED_DOWNLOAD, + ].forEach(type => { + dispatch(doClaimRewardType(type)); + }); } - dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD)); }; } diff --git a/ui/js/actions/search.js b/ui/js/actions/search.js index a693dc26a..2d6be08ee 100644 --- a/ui/js/actions/search.js +++ b/ui/js/actions/search.js @@ -2,8 +2,8 @@ import * as types from "constants/action_types"; import lbryuri from "lbryuri"; import lighthouse from "lighthouse"; import { doResolveUri } from "actions/content"; -import { doNavigate, doHistoryPush } from "actions/app"; -import { selectCurrentPage } from "selectors/app"; +import { doNavigate } from "actions/navigation"; +import { selectCurrentPage } from "selectors/navigation"; import batchActions from "util/batchActions"; export function doSearch(query) { diff --git a/ui/js/actions/settings.js b/ui/js/actions/settings.js index 4bdec4300..7038fd284 100644 --- a/ui/js/actions/settings.js +++ b/ui/js/actions/settings.js @@ -101,6 +101,9 @@ export function doDownloadLanguage(langFile) { export function doDownloadLanguages() { return function(dispatch, getState) { + //temporarily disable i18n so I can get a working build out -- Jeremy + return; + if (!fs.existsSync(app.i18n.directory)) { fs.mkdirSync(app.i18n.directory); } diff --git a/ui/js/actions/user.js b/ui/js/actions/user.js index 8a0e59ef1..360707a43 100644 --- a/ui/js/actions/user.js +++ b/ui/js/actions/user.js @@ -1,7 +1,7 @@ import * as types from "constants/action_types"; import * as modals from "constants/modal_types"; import lbryio from "lbryio"; -import { doOpenModal } from "actions/app"; +import { doOpenModal, doShowSnackBar } from "actions/app"; import { doRewardList, doClaimRewardType } from "actions/rewards"; import { selectEmailToVerify, selectUser } from "selectors/user"; import rewards from "rewards"; @@ -19,6 +19,7 @@ export function doAuthenticate() { data: { user }, }); dispatch(doRewardList()); + dispatch(doFetchInviteStatus()); }) .catch(error => { dispatch(doOpenModal(modals.AUTHENTICATION_FAILURE)); @@ -172,3 +173,62 @@ export function doFetchAccessToken() { lbryio.getAuthToken().then(success); }; } + +export function doFetchInviteStatus() { + return function(dispatch, getState) { + dispatch({ + type: types.USER_INVITE_STATUS_FETCH_STARTED, + }); + + lbryio + .call("user", "invite_status") + .then(status => { + dispatch({ + type: types.USER_INVITE_STATUS_FETCH_SUCCESS, + data: { + invitesRemaining: status.invites_remaining + ? status.invites_remaining + : 0, + invitees: status.invitees, + }, + }); + }) + .catch(error => { + dispatch({ + type: types.USER_INVITE_STATUS_FETCH_FAILURE, + data: { error }, + }); + }); + }; +} + +export function doUserInviteNew(email) { + return function(dispatch, getState) { + dispatch({ + type: types.USER_INVITE_NEW_STARTED, + }); + + lbryio + .call("user", "invite", { email: email }, "post") + .then(invite => { + dispatch({ + type: types.USER_INVITE_NEW_SUCCESS, + data: { email }, + }); + + dispatch( + doShowSnackBar({ + message: __("Invite sent to %s", email), + }) + ); + + dispatch(doFetchInviteStatus()); + }) + .catch(error => { + dispatch({ + type: types.USER_INVITE_NEW_FAILURE, + data: { error }, + }); + }); + }; +} diff --git a/ui/js/app.js b/ui/js/app.js index 2504b72e8..ecbb5f83c 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -22,7 +22,6 @@ const app = { i18n: i18n, logs: logs, log: function(message) { - console.log(message); logs.push(message); }, }; diff --git a/ui/js/component/app/index.js b/ui/js/component/app/index.js index 252fdf783..574c5f862 100644 --- a/ui/js/component/app/index.js +++ b/ui/js/component/app/index.js @@ -1,30 +1,21 @@ import React from "react"; import { connect } from "react-redux"; -import { selectCurrentModal } from "selectors/app"; -import { - doCheckUpgradeAvailable, - doOpenModal, - doAlertError, - doRecordScroll, -} from "actions/app"; -import { doFetchRewardedContent } from "actions/content"; - -import { doUpdateBalance } from "actions/wallet"; -import { selectWelcomeModalAcknowledged } from "selectors/app"; +import { selectPageTitle } from "selectors/navigation"; import { selectUser } from "selectors/user"; +import { doCheckUpgradeAvailable, doAlertError } from "actions/app"; +import { doRecordScroll } from "actions/navigation"; +import { doFetchRewardedContent } from "actions/content"; +import { doUpdateBalance } from "actions/wallet"; import App from "./view"; -import * as modals from "constants/modal_types"; const select = (state, props) => ({ - modal: selectCurrentModal(state), - isWelcomeAcknowledged: selectWelcomeModalAcknowledged(state), + pageTitle: selectPageTitle(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)), diff --git a/ui/js/component/app/view.jsx b/ui/js/component/app/view.jsx index 32cb74247..e3c2465de 100644 --- a/ui/js/component/app/view.jsx +++ b/ui/js/component/app/view.jsx @@ -30,12 +30,22 @@ class App extends React.PureComponent { this.scrollListener = () => this.props.recordScroll(window.scrollY); window.addEventListener("scroll", this.scrollListener); + + this.setTitleFromProps(this.props); } componentWillUnmount() { window.removeEventListener("scroll", this.scrollListener); } + componentWillReceiveProps(props) { + this.setTitleFromProps(props); + } + + setTitleFromProps(props) { + window.document.title = props.pageTitle; + } + render() { return (
diff --git a/ui/js/component/cardVerify/view.jsx b/ui/js/component/cardVerify/view.jsx index 0432a69e1..4494cbf6c 100644 --- a/ui/js/component/cardVerify/view.jsx +++ b/ui/js/component/cardVerify/view.jsx @@ -165,7 +165,7 @@ class CardVerify extends React.Component { render() { return ( 0) { + amountText = "+" + amountText; + } } return ( diff --git a/ui/js/component/fileActions/index.js b/ui/js/component/fileActions/index.js index 5802526af..02fb213da 100644 --- a/ui/js/component/fileActions/index.js +++ b/ui/js/component/fileActions/index.js @@ -1,13 +1,12 @@ import React from "react"; import { connect } from "react-redux"; -import { selectPlatform } from "selectors/app"; +import { selectPlatform, selectCurrentModal } from "selectors/app"; import { makeSelectFileInfoForUri, makeSelectDownloadingForUri, makeSelectLoadingForUri, } from "selectors/file_info"; import { makeSelectIsAvailableForUri } from "selectors/availability"; -import { selectCurrentModal } from "selectors/app"; import { makeSelectCostInfoForUri } from "selectors/cost_info"; import { doCloseModal, doOpenModal } from "actions/app"; import { doFetchAvailability } from "actions/availability"; diff --git a/ui/js/component/fileActions/view.jsx b/ui/js/component/fileActions/view.jsx index cead792db..2642ea7e4 100644 --- a/ui/js/component/fileActions/view.jsx +++ b/ui/js/component/fileActions/view.jsx @@ -178,18 +178,20 @@ class FileActions extends React.PureComponent { onClick={this.handleSupportButtonClicked.bind(this)} /> {showMenu - ? - openInFolder(fileInfo)} - label={openInFolderMessage} - /> - openModal(modals.CONFIRM_FILE_REMOVE)} - label={__("Remove...")} - /> - + ?
+ + openInFolder(fileInfo)} + label={openInFolderMessage} + /> + openModal(modals.CONFIRM_FILE_REMOVE)} + label={__("Remove...")} + /> + +
: ""} ({ diff --git a/ui/js/component/fileTile/index.js b/ui/js/component/fileTile/index.js index 0631ae849..2df504226 100644 --- a/ui/js/component/fileTile/index.js +++ b/ui/js/component/fileTile/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doResolveUri } from "actions/content"; import { makeSelectClaimForUri, diff --git a/ui/js/component/header/index.js b/ui/js/component/header/index.js index 464a471c8..857a04beb 100644 --- a/ui/js/component/header/index.js +++ b/ui/js/component/header/index.js @@ -1,9 +1,16 @@ import React from "react"; import { formatCredits } from "util/formatCredits"; import { connect } from "react-redux"; -import { selectIsBackDisabled, selectIsForwardDisabled } from "selectors/app"; +import { + selectIsBackDisabled, + selectIsForwardDisabled, +} from "selectors/navigation"; import { selectBalance } from "selectors/wallet"; -import { doNavigate, doHistoryBack, doHistoryForward } from "actions/app"; +import { + doNavigate, + doHistoryBack, + doHistoryForward, +} from "actions/navigation"; import Header from "./view"; const select = state => ({ diff --git a/ui/js/component/inviteList/index.js b/ui/js/component/inviteList/index.js new file mode 100644 index 000000000..3f7361454 --- /dev/null +++ b/ui/js/component/inviteList/index.js @@ -0,0 +1,16 @@ +import React from "react"; +import { connect } from "react-redux"; +import { + selectUserInvitees, + selectUserInviteStatusIsPending, +} from "selectors/user"; +import InviteList from "./view"; + +const select = state => ({ + invitees: selectUserInvitees(state), + isPending: selectUserInviteStatusIsPending(state), +}); + +const perform = dispatch => ({}); + +export default connect(select, perform)(InviteList); diff --git a/ui/js/component/inviteList/view.jsx b/ui/js/component/inviteList/view.jsx new file mode 100644 index 000000000..8d748116a --- /dev/null +++ b/ui/js/component/inviteList/view.jsx @@ -0,0 +1,77 @@ +import React from "react"; +import { Icon } from "component/common"; +import RewardLink from "component/rewardLink"; +import rewards from "rewards.js"; + +class InviteList extends React.PureComponent { + render() { + const { invitees } = this.props; + + if (!invitees) { + return null; + } + + return ( +
+
+

{__("Invite History")}

+
+
+ {invitees.length === 0 && + {__("You haven't invited anyone.")} } + {invitees.length > 0 && + + + + + + + + + + {invitees.map((invitee, index) => { + return ( + + + + + + ); + })} + +
+ {__("Invitee Email")} + + {__("Invite Status")} + + {__("Reward")} +
{invitee.email} + {invitee.invite_accepted + ? + : {__("unused")}} + + {invitee.invite_reward_claimed + ? + : invitee.invite_accepted + ? + : + {__("unclaimable")} + } +
} +
+
+
+ {__( + "The maximum number of invite rewards is currently limited. Invite reward can only be claimed if the invitee passes the humanness test." + )} +
+
+
+ ); + } +} + +export default InviteList; diff --git a/ui/js/component/inviteNew/index.js b/ui/js/component/inviteNew/index.js new file mode 100644 index 000000000..b31213b3f --- /dev/null +++ b/ui/js/component/inviteNew/index.js @@ -0,0 +1,29 @@ +import React from "react"; +import { connect } from "react-redux"; +import InviteNew from "./view"; +import { + selectUserInvitesRemaining, + selectUserInviteNewIsPending, + selectUserInviteNewErrorMessage, +} from "selectors/user"; +import rewards from "rewards"; +import { makeSelectRewardAmountByType } from "selectors/rewards"; + +import { doUserInviteNew } from "actions/user"; + +const select = state => { + const selectReward = makeSelectRewardAmountByType(); + + return { + errorMessage: selectUserInviteNewErrorMessage(state), + invitesRemaining: selectUserInvitesRemaining(state), + isPending: selectUserInviteNewIsPending(state), + rewardAmount: selectReward(state, { reward_type: rewards.TYPE_REFERRAL }), + }; +}; + +const perform = dispatch => ({ + inviteNew: email => dispatch(doUserInviteNew(email)), +}); + +export default connect(select, perform)(InviteNew); diff --git a/ui/js/component/inviteNew/view.jsx b/ui/js/component/inviteNew/view.jsx new file mode 100644 index 000000000..3f37e9c1b --- /dev/null +++ b/ui/js/component/inviteNew/view.jsx @@ -0,0 +1,100 @@ +import React from "react"; +import { BusyMessage, CreditAmount } from "component/common"; +import Link from "component/link"; +import { FormRow } from "component/form.js"; + +class FormInviteNew extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + email: "", + }; + } + + handleEmailChanged(event) { + this.setState({ + email: event.target.value, + }); + } + + handleSubmit(event) { + event.preventDefault(); + this.props.inviteNew(this.state.email); + } + + render() { + const { errorMessage, isPending } = this.props; + + return ( +
+ { + this.handleEmailChanged(event); + }} + /> +
+ { + this.handleSubmit(event); + }} + /> +
+ + ); + } +} + +class InviteNew extends React.PureComponent { + render() { + const { + errorMessage, + invitesRemaining, + inviteNew, + inviteStatusIsPending, + isPending, + rewardAmount, + } = this.props; + + return ( +
+
+ +

+ {__("Invite a Friend")} +

+
+ {/* +
+ {invitesRemaining > 0 && +

{__("You have %s invites remaining.", invitesRemaining)}

} + {invitesRemaining <= 0 && +

{__("You have no invites.")}

} +
*/} +
+

+ {__( + "Or an enemy. Or your cousin Jerry, who you're kind of unsure about." + )} +

+ +
+
+ ); + } +} + +export default InviteNew; diff --git a/ui/js/component/link/index.js b/ui/js/component/link/index.js index 601927420..5847c97d9 100644 --- a/ui/js/component/link/index.js +++ b/ui/js/component/link/index.js @@ -1,5 +1,10 @@ import React from "react"; import { connect } from "react-redux"; +import { doNavigate } from "actions/navigation"; import Link from "./view"; -export default connect(null, null)(Link); +const perform = dispatch => ({ + doNavigate: path => dispatch(doNavigate(path)), +}); + +export default connect(null, perform)(Link); diff --git a/ui/js/component/link/view.jsx b/ui/js/component/link/view.jsx index f385225f3..6760c6cd3 100644 --- a/ui/js/component/link/view.jsx +++ b/ui/js/component/link/view.jsx @@ -5,7 +5,6 @@ const Link = props => { const { href, title, - onClick, style, label, icon, @@ -13,6 +12,8 @@ const Link = props => { button, disabled, children, + navigate, + doNavigate, } = props; const className = @@ -21,6 +22,12 @@ const Link = props => { (button ? " button-block button-" + button + " button-set-item" : "") + (disabled ? " disabled" : ""); + const onClick = !props.onClick && navigate + ? () => { + doNavigate(navigate); + } + : props.onClick; + let content; if (children) { content = children; diff --git a/ui/js/component/linkTransaction/index.js b/ui/js/component/linkTransaction/index.js new file mode 100644 index 000000000..601927420 --- /dev/null +++ b/ui/js/component/linkTransaction/index.js @@ -0,0 +1,5 @@ +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/linkTransaction/view.jsx b/ui/js/component/linkTransaction/view.jsx new file mode 100644 index 000000000..e1fe32159 --- /dev/null +++ b/ui/js/component/linkTransaction/view.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import Link from "component/link"; + +const LinkTransaction = props => { + const { id } = props; + const linkProps = Object.assign({}, props); + + linkProps.href = "https://explorer.lbry.io/#!/transaction/" + id; + linkProps.label = id.substr(0, 7); + + return ; +}; + +export default LinkTransaction; diff --git a/ui/js/component/nsfwOverlay/index.js b/ui/js/component/nsfwOverlay/index.js index eb3242410..8395df0ac 100644 --- a/ui/js/component/nsfwOverlay/index.js +++ b/ui/js/component/nsfwOverlay/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import NsfwOverlay from "./view"; const perform = dispatch => ({ diff --git a/ui/js/component/publishForm/view.jsx b/ui/js/component/publishForm/view.jsx index 39bce0d4d..f770ae38e 100644 --- a/ui/js/component/publishForm/view.jsx +++ b/ui/js/component/publishForm/view.jsx @@ -16,6 +16,7 @@ class PublishForm extends React.PureComponent { this._requiredFields = ["name", "bid", "meta_title", "tosAgree"]; this._defaultCopyrightNotice = "All rights reserved."; + this._defaultPaidPrice = 0.01; this.state = { rawName: "", @@ -46,6 +47,7 @@ class PublishForm extends React.PureComponent { modal: null, isFee: false, customUrl: false, + source: null, }; } @@ -115,8 +117,12 @@ class PublishForm extends React.PureComponent { : {}), }; + const { source } = this.state; + if (this.refs.file.getValue() !== "") { publishArgs.file_path = this.refs.file.getValue(); + } else if (source) { + publishArgs.sources = source; } const success = claim => {}; @@ -257,6 +263,7 @@ class PublishForm extends React.PureComponent { handlePrefillClicked() { const claimInfo = this.myClaimInfo(); + const { source } = claimInfo.value.stream; const { license, licenseUrl, @@ -275,6 +282,7 @@ class PublishForm extends React.PureComponent { meta_nsfw: nsfw, prefillDone: true, bid: claimInfo.amount, + source, }; if (license == this._defaultCopyrightNotice) { @@ -318,7 +326,9 @@ class PublishForm extends React.PureComponent { handleFeePrefChange(feeEnabled) { this.setState({ isFee: feeEnabled, - feeAmount: this.state.feeAmount == "" ? "5.00" : this.state.feeAmount, + feeAmount: this.state.feeAmount == "" + ? this._defaultPaidPrice + : this.state.feeAmount, }); } @@ -786,7 +796,6 @@ class PublishForm extends React.PureComponent { ref="bid" type="number" step="0.01" - min="0" label={__("Deposit")} postfix="LBC" onChange={event => { diff --git a/ui/js/component/rewardLink/index.js b/ui/js/component/rewardLink/index.js index cf53b2367..aa2034835 100644 --- a/ui/js/component/rewardLink/index.js +++ b/ui/js/component/rewardLink/index.js @@ -5,8 +5,8 @@ import { makeSelectRewardByType, makeSelectIsRewardClaimPending, } from "selectors/rewards"; -import { doNavigate } from "actions/app"; -import { doClaimReward, doClaimRewardClearError } from "actions/rewards"; +import { doNavigate } from "actions/navigation"; +import { doClaimRewardType, doClaimRewardClearError } from "actions/rewards"; import RewardLink from "./view"; const makeSelect = () => { @@ -24,7 +24,7 @@ const makeSelect = () => { }; const perform = dispatch => ({ - claimReward: reward => dispatch(doClaimReward(reward, true)), + claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)), clearError: reward => dispatch(doClaimRewardClearError(reward)), navigate: path => dispatch(doNavigate(path)), }); diff --git a/ui/js/component/rewardLink/view.jsx b/ui/js/component/rewardLink/view.jsx index 1f36dd86e..9eeee17e2 100644 --- a/ui/js/component/rewardLink/view.jsx +++ b/ui/js/component/rewardLink/view.jsx @@ -9,15 +9,18 @@ const RewardLink = props => { claimReward, clearError, errorMessage, + label, isPending, } = props; return (
{ claimReward(reward); }} diff --git a/ui/js/component/rewardListClaimed/index.js b/ui/js/component/rewardListClaimed/index.js new file mode 100644 index 000000000..095121cad --- /dev/null +++ b/ui/js/component/rewardListClaimed/index.js @@ -0,0 +1,10 @@ +import React from "react"; +import { connect } from "react-redux"; +import { selectClaimedRewards } from "selectors/rewards"; +import RewardListClaimed from "./view"; + +const select = state => ({ + rewards: selectClaimedRewards(state), +}); + +export default connect(select, null)(RewardListClaimed); diff --git a/ui/js/component/rewardListClaimed/view.jsx b/ui/js/component/rewardListClaimed/view.jsx new file mode 100644 index 000000000..f568348cd --- /dev/null +++ b/ui/js/component/rewardListClaimed/view.jsx @@ -0,0 +1,44 @@ +import React from "react"; +import LinkTransaction from "component/linkTransaction"; + +const RewardListClaimed = props => { + const { rewards } = props; + + if (!rewards || !rewards.length) { + return null; + } + + return ( +
+

Claimed Rewards

+
+ + + + + + + + + + + {rewards.map(reward => { + return ( + + + + + + + ); + })} + +
{__("Title")}{__("Amount")}{__("Transaction")}{__("Date")}
{reward.reward_title}{reward.reward_amount} + {reward.created_at.replace("Z", " ").replace("T", " ")} +
+
+
+ ); +}; + +export default RewardListClaimed; diff --git a/ui/js/component/rewardSummary/index.js b/ui/js/component/rewardSummary/index.js new file mode 100644 index 000000000..dac44f34f --- /dev/null +++ b/ui/js/component/rewardSummary/index.js @@ -0,0 +1,10 @@ +import React from "react"; +import { connect } from "react-redux"; +import { selectUnclaimedRewardValue } from "selectors/rewards"; +import RewardSummary from "./view"; + +const select = state => ({ + unclaimedRewardAmount: selectUnclaimedRewardValue(state), +}); + +export default connect(select, null)(RewardSummary); diff --git a/ui/js/component/rewardSummary/view.jsx b/ui/js/component/rewardSummary/view.jsx new file mode 100644 index 000000000..d96fe79aa --- /dev/null +++ b/ui/js/component/rewardSummary/view.jsx @@ -0,0 +1,29 @@ +import React from "react"; +import Link from "component/link"; +import { CreditAmount } from "component/common"; + +const RewardSummary = props => { + const { balance, unclaimedRewardAmount } = props; + + return ( +
+
+

{__("Rewards")}

+
+
+ {unclaimedRewardAmount > 0 && +

+ You have{" "} + in + unclaimed rewards. +

} +
+
+ + +
+
+ ); +}; + +export default RewardSummary; diff --git a/ui/js/component/rewardTile/index.js b/ui/js/component/rewardTile/index.js new file mode 100644 index 000000000..2a1f0f485 --- /dev/null +++ b/ui/js/component/rewardTile/index.js @@ -0,0 +1,5 @@ +import React from "react"; +import { connect } from "react-redux"; +import RewardTile from "./view"; + +export default connect(null, null)(RewardTile); diff --git a/ui/js/component/rewardTile/view.jsx b/ui/js/component/rewardTile/view.jsx new file mode 100644 index 000000000..027fc484f --- /dev/null +++ b/ui/js/component/rewardTile/view.jsx @@ -0,0 +1,37 @@ +import React from "react"; +import { CreditAmount, Icon } from "component/common"; +import RewardLink from "component/rewardLink"; +import Link from "component/link"; +import rewards from "rewards"; + +const RewardTile = props => { + const { reward } = props; + + const claimed = !!reward.transaction_id; + + return ( +
+
+
+ +

{reward.reward_title}

+
+
{reward.reward_description}
+
+ {reward.reward_type == rewards.TYPE_REFERRAL && + } + {reward.reward_type !== rewards.TYPE_REFERRAL && + (claimed + ? {__("Reward claimed.")} + : )} +
+
+
+ ); +}; + +export default RewardTile; diff --git a/ui/js/component/router/index.js b/ui/js/component/router/index.js index bc40986f5..a5c0e9e63 100644 --- a/ui/js/component/router/index.js +++ b/ui/js/component/router/index.js @@ -1,7 +1,10 @@ import React from "react"; import { connect } from "react-redux"; import Router from "./view.jsx"; -import { selectCurrentPage, selectCurrentParams } from "selectors/app.js"; +import { + selectCurrentPage, + selectCurrentParams, +} from "selectors/navigation.js"; const select = state => ({ params: selectCurrentParams(state), diff --git a/ui/js/component/router/view.jsx b/ui/js/component/router/view.jsx index fa26f61eb..d5ff60a81 100644 --- a/ui/js/component/router/view.jsx +++ b/ui/js/component/router/view.jsx @@ -2,18 +2,21 @@ import React from "react"; import SettingsPage from "page/settings"; import HelpPage from "page/help"; import ReportPage from "page/report.js"; -import StartPage from "page/start.js"; import WalletPage from "page/wallet"; -import ShowPage from "page/showPage"; +import ReceiveCreditsPage from "page/receiveCredits"; +import SendCreditsPage from "page/sendCredits"; +import ShowPage from "page/show"; import PublishPage from "page/publish"; import DiscoverPage from "page/discover"; import DeveloperPage from "page/developer.js"; import RewardsPage from "page/rewards"; import FileListDownloaded from "page/fileListDownloaded"; import FileListPublished from "page/fileListPublished"; +import TransactionHistoryPage from "page/transactionHistory"; import ChannelPage from "page/channel"; import SearchPage from "page/search"; import AuthPage from "page/auth"; +import InvitePage from "page/invite"; import BackupPage from "page/backup"; const route = (page, routesMap) => { @@ -33,16 +36,17 @@ const Router = props => { discover: , downloaded: , help: , + history: , + invite: , publish: , published: , - receive: , + receive: , report: , rewards: , search: , - send: , + send: , settings: , show: , - start: , wallet: , }); }; diff --git a/ui/js/component/snackBar/index.js b/ui/js/component/snackBar/index.js index 7996009c4..ff083c3d3 100644 --- a/ui/js/component/snackBar/index.js +++ b/ui/js/component/snackBar/index.js @@ -1,11 +1,10 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate, doRemoveSnackBarSnack } from "actions/app"; +import { doRemoveSnackBarSnack } from "actions/app"; import { selectSnackBarSnacks } from "selectors/app"; import SnackBar from "./view"; const perform = dispatch => ({ - navigate: path => dispatch(doNavigate(path)), removeSnack: () => dispatch(doRemoveSnackBarSnack()), }); diff --git a/ui/js/component/snackBar/view.jsx b/ui/js/component/snackBar/view.jsx index 363c412d6..f2ee2c49d 100644 --- a/ui/js/component/snackBar/view.jsx +++ b/ui/js/component/snackBar/view.jsx @@ -10,7 +10,7 @@ class SnackBar extends React.PureComponent { } render() { - const { navigate, snacks, removeSnack } = this.props; + const { snacks, removeSnack } = this.props; if (!snacks.length) { this._hideTimeout = null; //should be unmounting anyway, but be safe? @@ -33,7 +33,7 @@ class SnackBar extends React.PureComponent { {linkText && linkTarget && navigate(linkTarget)} + navigate={linkTarget} className="snack-bar__action" label={linkText} />} diff --git a/ui/js/component/subHeader/index.js b/ui/js/component/subHeader/index.js index 97a991436..2806828ae 100644 --- a/ui/js/component/subHeader/index.js +++ b/ui/js/component/subHeader/index.js @@ -1,7 +1,7 @@ import React from "react"; import { connect } from "react-redux"; -import { selectCurrentPage, selectHeaderLinks } from "selectors/app"; -import { doNavigate } from "actions/app"; +import { selectCurrentPage, selectHeaderLinks } from "selectors/navigation"; +import { doNavigate } from "actions/navigation"; import SubHeader from "./view"; const select = (state, props) => ({ diff --git a/ui/js/component/transactionList/index.js b/ui/js/component/transactionList/index.js index c650e9eac..9e2d9ec6b 100644 --- a/ui/js/component/transactionList/index.js +++ b/ui/js/component/transactionList/index.js @@ -1,23 +1,5 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate } from "actions/app"; -import { doFetchTransactions } from "actions/wallet"; -import { - selectBalance, - selectTransactionItems, - selectIsFetchingTransactions, -} from "selectors/wallet"; - import TransactionList from "./view"; -const select = state => ({ - fetchingTransactions: selectIsFetchingTransactions(state), - transactionItems: selectTransactionItems(state), -}); - -const perform = dispatch => ({ - navigate: (path, params) => dispatch(doNavigate(path, params)), - fetchTransactions: () => dispatch(doFetchTransactions()), -}); - -export default connect(select, perform)(TransactionList); +export default connect(null, null)(TransactionList); diff --git a/ui/js/component/transactionList/view.jsx b/ui/js/component/transactionList/view.jsx index 5b06f5f8f..0ad7f4eeb 100644 --- a/ui/js/component/transactionList/view.jsx +++ b/ui/js/component/transactionList/view.jsx @@ -1,103 +1,57 @@ import React from "react"; -import { Address, BusyMessage, CreditAmount } from "component/common"; +import LinkTransaction from "component/linkTransaction"; +import { CreditAmount } from "component/common"; -class TransactionList extends React.PureComponent { - componentWillMount() { - this.props.fetchTransactions(); - } - - render() { - const { fetchingTransactions, transactionItems, navigate } = this.props; - - function findTypeOfTx(type, is_tip) { - if (is_tip && type === "Support") return "Tip"; - else return type; - } - - function getClaimLink(claim_name, claim_id) { - if (claim_id !== "" && claim_name !== "") { - let uri = `lbry://${claim_name}#${claim_id}`; - - return ( - - navigate("/show", { uri })} - > - {claim_name} - - - ); - } - - return {__("N/A")}; - } - - const rows = []; - if (transactionItems.length > 0) { - transactionItems.forEach(function(item) { - rows.push( - - {findTypeOfTx(item.type, item.is_tip)} - {(item.amount > 0 ? "+" : "") + item.amount} - {item.fee} - - {item.date - ? item.date.toLocaleDateString() - : {__("(Transaction pending)")}} - - - {item.date - ? item.date.toLocaleTimeString() - : {__("(Transaction pending)")}} - - {getClaimLink(item.claim_name, item.claim_id)} - - - {item.id.substr(0, 7)} - - - - ); - }); - } +const TransactionList = props => { + const { emptyMessage, transactions } = props; + if (!transactions || !transactions.length) { return ( -
-
-

{__("Transaction History")}

-
-
- {fetchingTransactions && - } - {!fetchingTransactions && rows.length === 0 - ?
{__("You have no transactions.")}
- : ""} - {rows.length > 0 - ? - - - - - - - - - - - - - {rows} - -
{__("Type")}{__("Amount")}{__("Fee")}{__("Date")}{__("Time")}{__("Claim")}{__("Transaction")}
- : ""} -
-
+
+ {emptyMessage || __("No transactions to list.")} +
); } -} + + return ( + + + + + + + + + + {transactions.map(item => { + return ( + + + + + + ); + })} + +
{__("Date")}{__("Amount")}{__("Transaction")}
+ {item.date + ? item.date.toLocaleDateString() + + " " + + item.date.toLocaleTimeString() + : + {__("(Transaction pending)")} + } + + {" "} + + +
+ ); +}; export default TransactionList; diff --git a/ui/js/component/transactionListRecent/index.js b/ui/js/component/transactionListRecent/index.js new file mode 100644 index 000000000..bea2fdac1 --- /dev/null +++ b/ui/js/component/transactionListRecent/index.js @@ -0,0 +1,23 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doFetchTransactions } from "actions/wallet"; +import { + selectBalance, + selectRecentTransactions, + selectHasTransactions, + selectIsFetchingTransactions, +} from "selectors/wallet"; + +import TransactionListRecent from "./view"; + +const select = state => ({ + fetchingTransactions: selectIsFetchingTransactions(state), + transactions: selectRecentTransactions(state), + hasTransactions: selectHasTransactions(state), +}); + +const perform = dispatch => ({ + fetchTransactions: () => dispatch(doFetchTransactions()), +}); + +export default connect(select, perform)(TransactionListRecent); diff --git a/ui/js/component/transactionListRecent/view.jsx b/ui/js/component/transactionListRecent/view.jsx new file mode 100644 index 000000000..82c09995d --- /dev/null +++ b/ui/js/component/transactionListRecent/view.jsx @@ -0,0 +1,41 @@ +import React from "react"; +import { BusyMessage } from "component/common"; +import Link from "component/link"; +import TransactionList from "component/transactionList"; + +class TransactionListRecent extends React.PureComponent { + componentWillMount() { + this.props.fetchTransactions(); + } + + render() { + const { fetchingTransactions, hasTransactions, transactions } = this.props; + + return ( +
+
+

{__("Recent Transactions")}

+
+
+ {fetchingTransactions && + } + {!fetchingTransactions && + } +
+ {hasTransactions && +
+ +
} +
+ ); + } +} + +export default TransactionListRecent; diff --git a/ui/js/component/userEmailNew/index.js b/ui/js/component/userEmailNew/index.js index 4040606eb..699f794ec 100644 --- a/ui/js/component/userEmailNew/index.js +++ b/ui/js/component/userEmailNew/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doUserEmailNew } from "actions/user"; +import { doUserEmailNew, doUserInviteNew } from "actions/user"; import { selectEmailNewIsPending, selectEmailNewErrorMessage, diff --git a/ui/js/component/userEmailNew/view.jsx b/ui/js/component/userEmailNew/view.jsx index 7f20ecd7c..70ed0f45e 100644 --- a/ui/js/component/userEmailNew/view.jsx +++ b/ui/js/component/userEmailNew/view.jsx @@ -31,6 +31,16 @@ class UserEmailNew extends React.PureComponent { this.handleSubmit(event); }} > +

+ {__( + "This process is required to prevent abuse of the rewards program." + )} +

+

+ {__( + "We will also contact you about updates and new content, but you can unsubscribe at any time." + )} +

-

- {__( - "To ensure you are a real person, we require a valid credit or debit card." - ) + - " " + - __("There is no charge at all, now or in the future.") + - " "} - -

- {errorMessage &&

{errorMessage}

} -

- -

-

- {__( - "You can continue without this step, but you will not be eligible to earn rewards." - )} -

- navigate("/discover")} - button="alt" - label={__("Skip Rewards")} - /> +
+
+

{__("Final Human Proof")}

+
+
+

+ Finally, please complete one and only one of the + options below. +

+
+
+
+
+

{__("1) Proof via Credit")}

+
+
+ {__( + "If you have a valid credit or debit card, you can use it to instantly prove your humanity." + ) + + " " + + __("There is no charge at all for this, now or in the future.") + + " "} +
+
+ {errorMessage && +

{errorMessage}

} + +
+
+
+ {__( + "A $1 authorization may temporarily appear with your provider." + )}{" "} + {" "} + +
+
+
+
+
+

{__("2) Proof via YouTube")}

+
+
+

+ {__( + "If you have a YouTube account with subscribers and views, you can sync your account to be granted instant verification." + )} +

+
+
+ +
+
+
+ This will not automatically refresh after approval. Once you have + synced your account, just navigate away or click + {" "} . +
+
+
+
+
+

{__("3) Proof via Chat")}

+
+
+

+ {__( + "A moderator capable of approving you is typically available in the #verification channel of our chat room." + )} +

+

+ {__( + "This process will likely involve providing proof of a stable and established online or real-life identity." + )} +

+
+
+ +
+
+
+
+
{__("Or, Skip It Entirely")}
+
+
+ +

+ {__( + "You can continue without this step, but you will not be eligible to earn rewards." + )} +

+ +
+
+ navigate("/discover")} + button="alt" + label={__("Skip Rewards")} + /> +
+
); } diff --git a/ui/js/component/video/index.js b/ui/js/component/video/index.js index dc4b465cd..f28fb94b8 100644 --- a/ui/js/component/video/index.js +++ b/ui/js/component/video/index.js @@ -1,7 +1,7 @@ import React from "react"; import { connect } from "react-redux"; import { doCloseModal } from "actions/app"; -import { doNavigate, doChangeVolume } from "actions/app"; +import { doChangeVolume } from "actions/app"; import { selectCurrentModal, selectVolume } from "selectors/app"; import { doPurchaseUri, doLoadVideo } from "actions/content"; import { diff --git a/ui/js/component/walletAddress/index.js b/ui/js/component/walletAddress/index.js index ad0021f41..d5a8d7696 100644 --- a/ui/js/component/walletAddress/index.js +++ b/ui/js/component/walletAddress/index.js @@ -5,7 +5,7 @@ import { selectReceiveAddress, selectGettingNewAddress, } from "selectors/wallet"; -import WalletPage from "./view"; +import WalletAddress from "./view"; const select = state => ({ receiveAddress: selectReceiveAddress(state), @@ -17,4 +17,4 @@ const perform = dispatch => ({ getNewAddress: () => dispatch(doGetNewAddress()), }); -export default connect(select, perform)(WalletPage); +export default connect(select, perform)(WalletAddress); diff --git a/ui/js/component/walletAddress/view.jsx b/ui/js/component/walletAddress/view.jsx index 2e1121cc6..bdf6f81fe 100644 --- a/ui/js/component/walletAddress/view.jsx +++ b/ui/js/component/walletAddress/view.jsx @@ -16,6 +16,11 @@ class WalletAddress extends React.PureComponent {

{__("Wallet Address")}

+

+ {__( + "Use this address to receive credits sent by another user (or yourself)." + )} +

@@ -29,11 +34,6 @@ class WalletAddress extends React.PureComponent {
-

- {__( - '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." diff --git a/ui/js/component/walletBalance/index.js b/ui/js/component/walletBalance/index.js new file mode 100644 index 000000000..1121cc463 --- /dev/null +++ b/ui/js/component/walletBalance/index.js @@ -0,0 +1,10 @@ +import React from "react"; +import { connect } from "react-redux"; +import { selectBalance } from "selectors/wallet"; +import WalletBalance from "./view"; + +const select = state => ({ + balance: selectBalance(state), +}); + +export default connect(select, null)(WalletBalance); diff --git a/ui/js/component/walletBalance/view.jsx b/ui/js/component/walletBalance/view.jsx new file mode 100644 index 000000000..5e0eca86f --- /dev/null +++ b/ui/js/component/walletBalance/view.jsx @@ -0,0 +1,43 @@ +import React from "react"; +import Link from "component/link"; +import { CreditAmount } from "component/common"; + +const WalletBalance = props => { + const { balance, navigate } = props; + /* +

+ navigate("/backup")} + label={__("Backup Your Wallet")} + /> +
+ */ + return ( +
+
+

{__("Balance")}

+
+
+ {(balance || balance === 0) && + } +
+
+ + + +
+
+ ); +}; + +export default WalletBalance; diff --git a/ui/js/component/walletSend/view.jsx b/ui/js/component/walletSend/view.jsx index 51f9b1f15..8c41d1000 100644 --- a/ui/js/component/walletSend/view.jsx +++ b/ui/js/component/walletSend/view.jsx @@ -34,15 +34,15 @@ const WalletSend = props => { onChange={setAddress} value={address} /> -
-
- 0.0) || !address} - /> - +
+ 0.0) || !address} + /> + +
diff --git a/ui/js/component/wunderbar/index.js b/ui/js/component/wunderbar/index.js index 9d23f4d26..bd327fd92 100644 --- a/ui/js/component/wunderbar/index.js +++ b/ui/js/component/wunderbar/index.js @@ -2,7 +2,7 @@ import React from "react"; import { connect } from "react-redux"; import lbryuri from "lbryuri.js"; import { selectWunderBarAddress, selectWunderBarIcon } from "selectors/search"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import Wunderbar from "./view"; const select = state => ({ diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index b587aca29..97f321f31 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -1,16 +1,19 @@ -export const CHANGE_PATH = "CHANGE_PATH"; export const OPEN_MODAL = "OPEN_MODAL"; export const CLOSE_MODAL = "CLOSE_MODAL"; -export const HISTORY_NAVIGATE = "HISTORY_NAVIGATE"; export const SHOW_SNACKBAR = "SHOW_SNACKBAR"; export const REMOVE_SNACKBAR_SNACK = "REMOVE_SNACKBAR_SNACK"; export const WINDOW_FOCUSED = "WINDOW_FOCUSED"; -export const CHANGE_AFTER_AUTH_PATH = "CHANGE_AFTER_AUTH_PATH"; export const DAEMON_READY = "DAEMON_READY"; export const DAEMON_VERSION_MATCH = "DAEMON_VERSION_MATCH"; export const DAEMON_VERSION_MISMATCH = "DAEMON_VERSION_MISMATCH"; export const VOLUME_CHANGED = "VOLUME_CHANGED"; +// Navigation +export const CHANGE_PATH = "CHANGE_PATH"; +export const CHANGE_AFTER_AUTH_PATH = "CHANGE_AFTER_AUTH_PATH"; +export const WINDOW_SCROLLED = "WINDOW_SCROLLED"; +export const HISTORY_NAVIGATE = "HISTORY_NAVIGATE"; + // Upgrades export const UPGRADE_CANCELLED = "UPGRADE_CANCELLED"; export const DOWNLOAD_UPGRADE = "DOWNLOAD_UPGRADE"; @@ -108,6 +111,15 @@ export const USER_IDENTITY_VERIFY_FAILURE = "USER_IDENTITY_VERIFY_FAILURE"; export const USER_FETCH_STARTED = "USER_FETCH_STARTED"; export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS"; export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE"; +export const USER_INVITE_STATUS_FETCH_STARTED = + "USER_INVITE_STATUS_FETCH_STARTED"; +export const USER_INVITE_STATUS_FETCH_SUCCESS = + "USER_INVITE_STATUS_FETCH_SUCCESS"; +export const USER_INVITE_STATUS_FETCH_FAILURE = + "USER_INVITE_STATUS_FETCH_FAILURE"; +export const USER_INVITE_NEW_STARTED = "USER_INVITE_NEW_STARTED"; +export const USER_INVITE_NEW_SUCCESS = "USER_INVITE_NEW_SUCCESS"; +export const USER_INVITE_NEW_FAILURE = "USER_INVITE_NEW_FAILURE"; export const FETCH_ACCESS_TOKEN_SUCCESS = "FETCH_ACCESS_TOKEN_SUCCESS"; // Rewards diff --git a/ui/js/constants/modal_types.js b/ui/js/constants/modal_types.js index 9f101cd98..09ddb8451 100644 --- a/ui/js/constants/modal_types.js +++ b/ui/js/constants/modal_types.js @@ -9,4 +9,5 @@ export const FIRST_REWARD = "first_reward"; export const AUTHENTICATION_FAILURE = "auth_failure"; export const TRANSACTION_FAILED = "transaction_failed"; export const INSUFFICIENT_BALANCE = "insufficient_balance"; +export const REWARD_APPROVAL_REQUIRED = "reward_approval_required"; export const CREDIT_INTRO = "credit_intro"; diff --git a/ui/js/constants/settings.js b/ui/js/constants/settings.js index 5187baea1..146c882cc 100644 --- a/ui/js/constants/settings.js +++ b/ui/js/constants/settings.js @@ -1,5 +1,8 @@ +/*hardcoded names still exist for these in reducers/settings.js - only discovered when debugging*/ +/*Many settings are stored in the localStorage by their name - + be careful about changing the value of a settings constant, as doing so can invalidate existing settings*/ export const CREDIT_INTRO_ACKNOWLEDGED = "credit_intro_acknowledged"; -export const FIRST_RUN_ACKNOWLEDGED = "welcome_acknowledged"; +export const NEW_USER_ACKNOWLEDGED = "welcome_acknowledged"; export const LANGUAGE = "language"; export const SHOW_NSFW = "showNsfw"; export const SHOW_UNAVAILABLE = "showUnavailable"; diff --git a/ui/js/main.js b/ui/js/main.js index 42c40a3a2..e8a3221a0 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -6,9 +6,9 @@ import SnackBar from "component/snackBar"; import { Provider } from "react-redux"; import store from "store.js"; import SplashScreen from "component/splash"; -import { doChangePath, doNavigate, doDaemonReady } from "actions/app"; +import { doDaemonReady } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doDownloadLanguages } from "actions/settings"; -import { toQueryString } from "util/query_params"; import * as types from "constants/action_types"; const env = ENV; @@ -28,23 +28,6 @@ window.addEventListener("contextmenu", event => { event.preventDefault(); }); -window.addEventListener("popstate", (event, param) => { - event.preventDefault(); - - const hash = document.location.hash; - let action; - - if (hash !== "") { - const url = hash.replace(/^#/, ""); - const { params, scrollY } = event.state || {}; - const queryString = toQueryString(params); - - app.store.dispatch(doChangePath(`${url}?${queryString}`, { scrollY })); - } else { - app.store.dispatch(doChangePath("/discover")); - } -}); - ipcRenderer.on("open-uri-requested", (event, uri) => { if (uri && uri.startsWith("lbry://")) { app.store.dispatch(doNavigate("/show", { uri })); @@ -87,13 +70,6 @@ win.on("focus", () => { dock.setBadge(""); }); -const updateProgress = () => { - const state = app.store.getState(); - const progress = selectTotalDownloadProgress(state); - - win.setProgressBar(progress || -1); -}; - const initialState = app.store.getState(); var init = function() { diff --git a/ui/js/modal/modalCreditIntro/index.js b/ui/js/modal/modalCreditIntro/index.js index 0626eee8f..c793ec22a 100644 --- a/ui/js/modal/modalCreditIntro/index.js +++ b/ui/js/modal/modalCreditIntro/index.js @@ -1,13 +1,14 @@ import React from "react"; -import rewards from "rewards"; import { connect } from "react-redux"; -import { doCloseModal, doAuthNavigate } from "actions/app"; +import { doCloseModal } from "actions/app"; +import { doAuthNavigate } from "actions/navigation"; import { doSetClientSetting } from "actions/settings"; import { selectUserIsRewardApproved } from "selectors/user"; +import { selectBalance } from "selectors/wallet"; import { makeSelectHasClaimedReward, makeSelectRewardByType, - selectTotalRewardValue, + selectUnclaimedRewardValue, } from "selectors/rewards"; import * as settings from "constants/settings"; import ModalCreditIntro from "./view"; @@ -17,9 +18,9 @@ const select = (state, props) => { selectReward = makeSelectRewardByType(); return { + currentBalance: selectBalance(state), isRewardApproved: selectUserIsRewardApproved(state), - reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), - totalRewardValue: selectTotalRewardValue(state), + totalRewardValue: selectUnclaimedRewardValue(state), }; }; diff --git a/ui/js/modal/modalCreditIntro/view.jsx b/ui/js/modal/modalCreditIntro/view.jsx index 8a2b7b500..5f56c11b4 100644 --- a/ui/js/modal/modalCreditIntro/view.jsx +++ b/ui/js/modal/modalCreditIntro/view.jsx @@ -1,40 +1,64 @@ import React from "react"; import { Modal } from "modal/modal"; -import { CreditAmount } from "component/common"; +import { CreditAmount, CurrencySymbol } from "component/common"; import Link from "component/link/index"; +import { formatCredits } from "util/formatCredits"; const ModalCreditIntro = props => { - const { closeModal, totalRewardValue, verifyAccount } = props; + const { closeModal, currentBalance, totalRewardValue, verifyAccount } = props; const totalRewardRounded = Math.round(totalRewardValue / 10) * 10; return (
-

{__("Claim Your Credits")}

-

- The LBRY network is controlled and powered by credits called{" "} - LBC, a blockchain asset. -

-

- {__("New patrons receive ")} {" "} - {totalRewardValue - ? - : {__("credits")}} - {" "} {__("in rewards for usage and influence of the network.")} -

+

{__("Blockchain 101")}

+ LBRY is controlled and powered by a blockchain asset called {" "} + .{" "} + {" "} {__( - "You'll also earn weekly bonuses for checking out the greatest new stuff." + "is used to publish content, to have a say in the network rules, and to access paid content." )}

+ {currentBalance <= 0 + ?
+

+ You currently have , so + the actions you can take are limited. +

+

+ However, there are a variety of ways to get credits, including + more than {" "} + {totalRewardValue + ? + : {__("?? credits")}} + {" "}{" "} + {__( + " in rewards available for being a proven human during the LBRY beta." + )} +

+
+ :
+

+ But you probably knew this, since you've already got{" "} + . +

+
} +
+ -
diff --git a/ui/js/modal/modalInsufficientCredits/index.js b/ui/js/modal/modalInsufficientCredits/index.js index 0ece4f1fe..c50d34d44 100644 --- a/ui/js/modal/modalInsufficientCredits/index.js +++ b/ui/js/modal/modalInsufficientCredits/index.js @@ -1,13 +1,14 @@ import React from "react"; import { connect } from "react-redux"; -import { doCloseModal, doNavigate } from "actions/app"; +import { doCloseModal } from "actions/app"; +import { doNavigate } from "actions/navigation"; import ModalInsufficientCredits from "./view"; const select = state => ({}); const perform = dispatch => ({ addFunds: () => { - dispatch(doNavigate("/rewards")); + dispatch(doNavigate("/wallet")); dispatch(doCloseModal()); }, closeModal: () => dispatch(doCloseModal()), diff --git a/ui/js/modal/modalInsufficientCredits/view.jsx b/ui/js/modal/modalInsufficientCredits/view.jsx index 681fc4c9f..397fc0301 100644 --- a/ui/js/modal/modalInsufficientCredits/view.jsx +++ b/ui/js/modal/modalInsufficientCredits/view.jsx @@ -1,5 +1,6 @@ import React from "react"; import { Modal } from "modal/modal"; +import { CurrencySymbol } from "component/common"; class ModalInsufficientCredits extends React.PureComponent { render() { @@ -11,11 +12,12 @@ class ModalInsufficientCredits extends React.PureComponent { type="confirm" contentLabel={__("Not enough credits")} confirmButtonLabel={__("Get Credits")} - abortButtonLabel={__("Cancel")} + abortButtonLabel={__("Not Now")} onAborted={closeModal} onConfirmed={addFunds} > - {__("More LBRY credits are required to purchase this.")} +

{__("More Credits Required")}

+

You'll need more to do this.

); } diff --git a/ui/js/modal/modalRemoveFile/index.js b/ui/js/modal/modalRemoveFile/index.js index 211bdff26..f64c1987e 100644 --- a/ui/js/modal/modalRemoveFile/index.js +++ b/ui/js/modal/modalRemoveFile/index.js @@ -1,9 +1,8 @@ import React from "react"; import { connect } from "react-redux"; -import { doCloseModal, doHistoryBack } from "actions/app"; +import { doCloseModal } from "actions/app"; import { doDeleteFileAndGoBack } from "actions/file_info"; import { makeSelectClaimForUriIsMine } from "selectors/claims"; -import batchActions from "util/batchActions"; import ModalRemoveFile from "./view"; diff --git a/ui/js/modal/modalRewardApprovalRequired/index.js b/ui/js/modal/modalRewardApprovalRequired/index.js new file mode 100644 index 000000000..15413b6e3 --- /dev/null +++ b/ui/js/modal/modalRewardApprovalRequired/index.js @@ -0,0 +1,15 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doCloseModal } from "actions/app"; +import { doAuthNavigate } from "actions/navigation"; +import ModalRewardApprovalRequired from "./view"; + +const perform = dispatch => ({ + doAuth: () => { + dispatch(doCloseModal()); + dispatch(doAuthNavigate()); + }, + closeModal: () => dispatch(doCloseModal()), +}); + +export default connect(null, perform)(ModalRewardApprovalRequired); diff --git a/ui/js/modal/modalRewardApprovalRequired/view.jsx b/ui/js/modal/modalRewardApprovalRequired/view.jsx new file mode 100644 index 000000000..fcdab9f98 --- /dev/null +++ b/ui/js/modal/modalRewardApprovalRequired/view.jsx @@ -0,0 +1,33 @@ +import React from "react"; +import { Modal } from "modal/modal"; + +class ModalRewardApprovalRequired extends React.PureComponent { + render() { + const { closeModal, doAuth } = this.props; + + return ( + +
+

+ {__("This is awkward. Are you real?")} +

+

+ {__( + "Before we can give you any credits, we need to perform a brief check to make sure you're a new and unique person." + )} +

+
+
+ ); + } +} + +export default ModalRewardApprovalRequired; diff --git a/ui/js/modal/modalRouter/index.js b/ui/js/modal/modalRouter/index.js index 971f83139..79f02385f 100644 --- a/ui/js/modal/modalRouter/index.js +++ b/ui/js/modal/modalRouter/index.js @@ -1,20 +1,31 @@ import React from "react"; import { connect } from "react-redux"; -import { selectCurrentModal } from "selectors/app"; import { doOpenModal } from "actions/app"; -import { selectWelcomeModalAcknowledged } from "selectors/app"; +import * as settings from "constants/settings"; +import { selectCurrentModal } from "selectors/app"; +import { selectCurrentPage } from "selectors/navigation"; +import { selectCostForCurrentPageUri } from "selectors/cost_info"; +import { makeSelectClientSetting } from "selectors/settings"; import { selectUser } from "selectors/user"; +import { selectBalance } from "selectors/wallet"; import ModalRouter from "./view"; -import * as modals from "constants/modal_types"; const select = (state, props) => ({ + balance: selectBalance(state), + showPageCost: selectCostForCurrentPageUri(state), modal: selectCurrentModal(state), - isWelcomeAcknowledged: selectWelcomeModalAcknowledged(state), + page: selectCurrentPage(state), + isWelcomeAcknowledged: makeSelectClientSetting( + settings.NEW_USER_ACKNOWLEDGED + )(state), + isCreditIntroAcknowledged: makeSelectClientSetting( + settings.CREDIT_INTRO_ACKNOWLEDGED + )(state), user: selectUser(state), }); const perform = dispatch => ({ - openWelcomeModal: () => dispatch(doOpenModal(modals.WELCOME)), + openModal: modal => dispatch(doOpenModal(modal)), }); export default connect(select, perform)(ModalRouter); diff --git a/ui/js/modal/modalRouter/view.jsx b/ui/js/modal/modalRouter/view.jsx index 6d8ca4017..47ff2d80c 100644 --- a/ui/js/modal/modalRouter/view.jsx +++ b/ui/js/modal/modalRouter/view.jsx @@ -6,33 +6,97 @@ import ModalInsufficientCredits from "modal/modalInsufficientCredits"; import ModalUpgrade from "modal/modalUpgrade"; import ModalWelcome from "modal/modalWelcome"; import ModalFirstReward from "modal/modalFirstReward"; +import ModalRewardApprovalRequired from "modal/modalRewardApprovalRequired"; import ModalCreditIntro from "modal/modalCreditIntro"; import ModalTransactionFailed from "modal/modalTransactionFailed"; import ModalInsufficientBalance from "modal/modalInsufficientBalance"; import * as modals from "constants/modal_types"; class ModalRouter extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + lastTransitionModal: null, + lastTransitionPage: null, + }; + } + componentWillMount() { - this.showWelcome(this.props); + this.showTransitionModals(this.props); } componentWillReceiveProps(nextProps) { - this.showWelcome(nextProps); + this.showTransitionModals(nextProps); } - showWelcome(props) { - const { isWelcomeAcknowledged, openWelcomeModal, user } = props; + showTransitionModals(props) { + const { modal, openModal, page } = props; + if (modal) { + return; + } + + const transitionModal = [ + this.checkShowWelcome, + this.checkShowCreditIntro, + this.checkShowInsufficientCredits, + ].reduce((acc, func) => { + return !acc ? func.bind(this)(props) : acc; + }, false); + + if ( + transitionModal && + (transitionModal != this.state.lastTransitionModal || + page != this.state.lastTransitionPage) + ) { + openModal(transitionModal); + this.setState({ + lastTransitionModal: transitionModal, + lastTransitionPage: page, + }); + } + } + + checkShowWelcome(props) { + const { isWelcomeAcknowledged, user } = props; if ( !isWelcomeAcknowledged && user && !user.is_reward_approved && !user.is_identity_verified ) { - openWelcomeModal(); + return modals.WELCOME; } } + checkShowCreditIntro(props) { + const { page, isCreditIntroAcknowledged, user } = props; + + if ( + !isCreditIntroAcknowledged && + user && + !user.is_reward_approved && + (["rewards", "send", "receive", "publish", "wallet"].includes(page) || + this.isPaidShowPage(props)) + ) { + return modals.CREDIT_INTRO; + } + } + + checkShowInsufficientCredits(props) { + const { balance, page } = props; + + if (balance <= 0 && ["send", "publish"].includes(page)) { + return modals.INSUFFICIENT_CREDITS; + } + } + + isPaidShowPage(props) { + const { page, showPageCost } = props; + return page === "show" && showPageCost > 0; + } + render() { const { modal } = this.props; @@ -57,6 +121,8 @@ class ModalRouter extends React.PureComponent { return ; case modals.INSUFFICIENT_BALANCE: return ; + case modals.REWARD_APPROVAL_REQUIRED: + return ; default: return null; } diff --git a/ui/js/modal/modalWelcome/index.js b/ui/js/modal/modalWelcome/index.js index 1b62eba42..dd2cc0f29 100644 --- a/ui/js/modal/modalWelcome/index.js +++ b/ui/js/modal/modalWelcome/index.js @@ -8,9 +8,8 @@ import ModalWelcome from "./view"; const perform = dispatch => () => ({ closeModal: () => { - dispatch(doSetClientSetting(settings.FIRST_RUN_ACKNOWLEDGED, true)); + dispatch(doSetClientSetting(settings.NEW_USER_ACKNOWLEDGED, true)); dispatch(doCloseModal()); - dispatch(doOpenModal(modals.CREDIT_INTRO)); }, }); diff --git a/ui/js/modal/modalWelcome/view.jsx b/ui/js/modal/modalWelcome/view.jsx index 85b33a224..adcf76ab6 100644 --- a/ui/js/modal/modalWelcome/view.jsx +++ b/ui/js/modal/modalWelcome/view.jsx @@ -22,7 +22,11 @@ const ModalWelcome = props => { )}

- +
diff --git a/ui/js/page/auth/index.js b/ui/js/page/auth/index.js index 541504dbb..205400088 100644 --- a/ui/js/page/auth/index.js +++ b/ui/js/page/auth/index.js @@ -1,7 +1,7 @@ import React from "react"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { connect } from "react-redux"; -import { selectPathAfterAuth } from "selectors/app"; +import { selectPathAfterAuth } from "selectors/navigation"; import { selectAuthenticationIsPending, selectEmailToVerify, diff --git a/ui/js/page/auth/view.jsx b/ui/js/page/auth/view.jsx index 55bef96a8..b9ac51bbd 100644 --- a/ui/js/page/auth/view.jsx +++ b/ui/js/page/auth/view.jsx @@ -30,11 +30,11 @@ export class AuthPage extends React.PureComponent { const { email, isPending, isVerificationCandidate, user } = this.props; if (isPending || (user && !user.has_verified_email && !email)) { - return __("Welcome to LBRY"); + return __("Human Proofing"); } else if (user && !user.has_verified_email) { return __("Confirm Email"); } else if (user && !user.is_identity_verified && !user.is_reward_approved) { - return __("Confirm Identity"); + return __("Final Verification"); } else { return __("Welcome to LBRY"); } @@ -44,51 +44,45 @@ export class AuthPage extends React.PureComponent { const { email, isPending, isVerificationCandidate, user } = this.props; if (isPending) { - return ; + return [, true]; } else if (user && !user.has_verified_email && !email) { - return ; + return [, true]; } else if (user && !user.has_verified_email) { - return ; + return [, true]; } else if (user && !user.is_identity_verified) { - return ; + return [, false]; } else { - return {__("No further steps.")}; + return [{__("No further steps.")}, true]; } } render() { const { email, user, isPending, navigate } = this.props; + const [innerContent, useTemplate] = this.renderMain(); - return ( -
-
-
-

{this.getTitle()}

-
-
- {!isPending && - !email && - user && - !user.has_verified_email && -

- {__("Create a verified identity and receive LBC rewards.")} -

} - {this.renderMain()} -
-
-
- {__( - "This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards." - ) + " "} - navigate("/discover")} - label={__("Return home")} - />. + return useTemplate + ?
+
+
+

{this.getTitle()}

-
-
-
- ); +
+ {innerContent} +
+
+
+ {__( + "This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards." + ) + " "} + navigate("/discover")} + label={__("Return home")} + />. +
+
+ + + : innerContent; } } diff --git a/ui/js/page/channel/index.js b/ui/js/page/channel/index.js index dfd75d5fe..95134bdc3 100644 --- a/ui/js/page/channel/index.js +++ b/ui/js/page/channel/index.js @@ -9,8 +9,8 @@ import { makeSelectClaimsInChannelForCurrentPage, makeSelectFetchingChannelClaims, } from "selectors/claims"; -import { selectCurrentParams } from "selectors/app"; -import { doNavigate } from "actions/app"; +import { selectCurrentParams } from "selectors/navigation"; +import { doNavigate } from "actions/navigation"; import { makeSelectTotalPagesForChannel } from "selectors/content"; import ChannelPage from "./view"; diff --git a/ui/js/page/filePage/index.js b/ui/js/page/file/index.js similarity index 96% rename from ui/js/page/filePage/index.js rename to ui/js/page/file/index.js index 26b98c93a..19cdde31f 100644 --- a/ui/js/page/filePage/index.js +++ b/ui/js/page/file/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doFetchFileInfo } from "actions/file_info"; import { makeSelectFileInfoForUri } from "selectors/file_info"; import { selectRewardContentClaimIds } from "selectors/content"; diff --git a/ui/js/page/filePage/view.jsx b/ui/js/page/file/view.jsx similarity index 100% rename from ui/js/page/filePage/view.jsx rename to ui/js/page/file/view.jsx diff --git a/ui/js/page/fileListDownloaded/index.js b/ui/js/page/fileListDownloaded/index.js index c2473e02a..449d3f196 100644 --- a/ui/js/page/fileListDownloaded/index.js +++ b/ui/js/page/fileListDownloaded/index.js @@ -10,7 +10,7 @@ import { selectIsFetchingClaimListMine, } from "selectors/claims"; import { doFetchClaimListMine } from "actions/content"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doCancelAllResolvingUris } from "actions/content"; import FileListDownloaded from "./view"; diff --git a/ui/js/page/fileListPublished/index.js b/ui/js/page/fileListPublished/index.js index 8100c2977..c6bf2a43a 100644 --- a/ui/js/page/fileListPublished/index.js +++ b/ui/js/page/fileListPublished/index.js @@ -7,7 +7,7 @@ import { selectIsFetchingClaimListMine, } from "selectors/claims"; import { doClaimRewardType } from "actions/rewards"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doCancelAllResolvingUris } from "actions/content"; import FileListPublished from "./view"; diff --git a/ui/js/page/help/index.js b/ui/js/page/help/index.js index c4ea548b2..601cdb8a7 100644 --- a/ui/js/page/help/index.js +++ b/ui/js/page/help/index.js @@ -1,5 +1,5 @@ import React from "react"; -import { doNavigate } from "actions/app"; +import { doAuthNavigate } from "actions/navigation"; import { connect } from "react-redux"; import { doFetchAccessToken } from "actions/user"; import { selectAccessToken, selectUser } from "selectors/user"; @@ -11,7 +11,7 @@ const select = state => ({ }); const perform = dispatch => ({ - navigate: (path, params) => dispatch(doNavigate(path, params)), + doAuth: () => dispatch(doAuthNavigate("/help")), fetchAccessToken: () => dispatch(doFetchAccessToken()), }); diff --git a/ui/js/page/help/view.jsx b/ui/js/page/help/view.jsx index f56d058e8..cb1627635 100644 --- a/ui/js/page/help/view.jsx +++ b/ui/js/page/help/view.jsx @@ -3,7 +3,7 @@ import React from "react"; import lbry from "lbry.js"; import Link from "component/link"; import SubHeader from "component/subHeader"; -import { BusyMessage } from "component/common"; +import { BusyMessage, Icon } from "component/common"; class HelpPage extends React.PureComponent { constructor(props) { @@ -50,7 +50,7 @@ class HelpPage extends React.PureComponent { render() { let ver, osName, platform, newVerLink; - const { navigate, user } = this.props; + const { doAuth, user } = this.props; if (this.state.versionInfo) { ver = this.state.versionInfo; @@ -119,7 +119,7 @@ class HelpPage extends React.PureComponent {

{__("Did you find something wrong?")}

navigate("report")} + navigate="/report" label={__("Submit a Bug Report")} icon="icon-bug" button="alt" @@ -143,7 +143,7 @@ class HelpPage extends React.PureComponent {

:

{__("Your copy of LBRY is up to date.")}

} {this.state.uiVersion && ver - ? + ?
@@ -162,7 +162,21 @@ class HelpPage extends React.PureComponent { + + + + diff --git a/ui/js/page/invite/index.js b/ui/js/page/invite/index.js new file mode 100644 index 000000000..c97cd1b29 --- /dev/null +++ b/ui/js/page/invite/index.js @@ -0,0 +1,19 @@ +import React from "react"; +import { connect } from "react-redux"; +import InvitePage from "./view"; +import { doFetchInviteStatus } from "actions/user"; +import { + selectUserInviteStatusFailed, + selectUserInviteStatusIsPending, +} from "selectors/user"; + +const select = state => ({ + isFailed: selectUserInviteStatusFailed(state), + isPending: selectUserInviteStatusIsPending(state), +}); + +const perform = dispatch => ({ + fetchInviteStatus: () => dispatch(doFetchInviteStatus()), +}); + +export default connect(select, perform)(InvitePage); diff --git a/ui/js/page/invite/view.jsx b/ui/js/page/invite/view.jsx new file mode 100644 index 000000000..0488ca1f3 --- /dev/null +++ b/ui/js/page/invite/view.jsx @@ -0,0 +1,32 @@ +import React from "react"; +import { BusyMessage } from "component/common"; +import SubHeader from "component/subHeader"; +import InviteNew from "component/inviteNew"; +import InviteList from "component/inviteList"; + +class InvitePage extends React.PureComponent { + componentWillMount() { + this.props.fetchInviteStatus(); + } + + render() { + const { isPending, isFailed } = this.props; + + return ( +
+ + {isPending && + } + {!isPending && + isFailed && + + {__("Failed to retrieve invite status.")} + } + {!isPending && !isFailed && } + {!isPending && !isFailed && } +
+ ); + } +} + +export default InvitePage; diff --git a/ui/js/page/publish/index.js b/ui/js/page/publish/index.js index f296f1687..f0547941d 100644 --- a/ui/js/page/publish/index.js +++ b/ui/js/page/publish/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate, doHistoryBack } from "actions/app"; +import { doNavigate, doHistoryBack } from "actions/navigation"; import { doClaimRewardType } from "actions/rewards"; import { selectMyClaims, @@ -16,10 +16,12 @@ import { doCreateChannel, doPublish, } from "actions/content"; +import { selectBalance } from "selectors/wallet"; import rewards from "rewards"; import PublishPage from "./view"; const select = state => ({ + balance: selectBalance(state), myClaims: selectMyClaims(state), fetchingChannels: selectFetchingMyChannels(state), channels: selectMyChannelClaims(state), diff --git a/ui/js/page/receiveCredits/index.js b/ui/js/page/receiveCredits/index.js new file mode 100644 index 000000000..475548da7 --- /dev/null +++ b/ui/js/page/receiveCredits/index.js @@ -0,0 +1,5 @@ +import React from "react"; +import { connect } from "react-redux"; +import ReceiveCreditsPage from "./view"; + +export default connect(null, null)(ReceiveCreditsPage); diff --git a/ui/js/page/receiveCredits/view.jsx b/ui/js/page/receiveCredits/view.jsx new file mode 100644 index 000000000..120071748 --- /dev/null +++ b/ui/js/page/receiveCredits/view.jsx @@ -0,0 +1,34 @@ +import React from "react"; +import SubHeader from "component/subHeader"; +import Link from "component/link"; +import WalletAddress from "component/walletAddress"; + +const ReceiveCreditsPage = props => { + return ( +
+ + +
+
+

{__("Where To Find Credits")}

+
+
+

+ { + "LBRY credits can be purchased on exchanges, earned for contributions, for mining, and more." + } +

+
+
+ +
+
+
+ ); +}; + +export default ReceiveCreditsPage; diff --git a/ui/js/page/rewards/index.js b/ui/js/page/rewards/index.js index 8a7a29d98..e9dca47f9 100644 --- a/ui/js/page/rewards/index.js +++ b/ui/js/page/rewards/index.js @@ -1,23 +1,18 @@ import React from "react"; import { connect } from "react-redux"; import { - makeSelectRewardByType, selectFetchingRewards, - selectRewards, + selectUnclaimedRewards, } from "selectors/rewards"; import { selectUser } from "selectors/user"; -import { doAuthNavigate, doNavigate } from "actions/app"; +import { doAuthNavigate, doNavigate } from "actions/navigation"; import { doRewardList } from "actions/rewards"; -import rewards from "rewards"; import RewardsPage from "./view"; const select = (state, props) => { - const selectReward = makeSelectRewardByType(); - return { fetching: selectFetchingRewards(state), - rewards: selectRewards(state), - newUserReward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), + rewards: selectUnclaimedRewards(state), user: selectUser(state), }; }; diff --git a/ui/js/page/rewards/view.jsx b/ui/js/page/rewards/view.jsx index 1e7bba459..51d75e406 100644 --- a/ui/js/page/rewards/view.jsx +++ b/ui/js/page/rewards/view.jsx @@ -1,31 +1,9 @@ import React from "react"; -import { BusyMessage, CreditAmount, Icon } from "component/common"; +import { BusyMessage } from "component/common"; +import RewardListClaimed from "component/rewardListClaimed"; +import RewardTile from "component/rewardTile"; import SubHeader from "component/subHeader"; import Link from "component/link"; -import RewardLink from "component/rewardLink"; - -const RewardTile = props => { - const { reward } = props; - - const claimed = !!reward.transaction_id; - - return ( -
-
-
- -

{reward.reward_title}

-
-
- {claimed - ? {__("Reward claimed.")} - : } -
-
{reward.reward_description}
-
-
- ); -}; class RewardsPage extends React.PureComponent { componentDidMount() { @@ -44,32 +22,8 @@ class RewardsPage extends React.PureComponent { } } - render() { - const { doAuth, fetching, navigate, rewards, user } = this.props; - - let content, cardHeader; - - if (fetching) { - content = ( -
- -
- ); - } else if (rewards.length > 0) { - content = ( -
- {rewards.map(reward => - - )} -
- ); - } else { - content = ( -
- {__("Failed to load rewards.")} -
- ); - } + renderPageHeader() { + const { doAuth, navigate, user } = this.props; if (user && !user.is_reward_approved) { if ( @@ -77,20 +31,27 @@ class RewardsPage extends React.PureComponent { !user.has_verified_email || !user.is_identity_verified ) { - cardHeader = ( -
+ return ( +
+
+

{__("Humans Only")}

+

- {__("Only verified accounts are eligible to earn rewards.")} + {__("Rewards are for human beings only.")} + {" "} + {__( + "You'll have to prove you're one of us before you can claim any rewards." + )}

- +
-
+ ); } else { - cardHeader = ( + return (

{__( @@ -122,25 +83,52 @@ class RewardsPage extends React.PureComponent {

); } + } + } + + renderUnclaimedRewards() { + const { fetching, rewards, user } = this.props; + + if (fetching) { + return ( +
+ +
+ ); } else if (user === null) { - cardHeader = ( -
-
-

- {__( - "This application is unable to earn rewards due to an authentication failure." - )} -

-
+ return ( +
+

+ {__( + "This application is unable to earn rewards due to an authentication failure." + )} +

+
+ ); + } else if (!rewards || rewards.length <= 0) { + return ( +
+ {__("Failed to load rewards.")} +
+ ); + } else { + return ( +
+ {rewards.map(reward => + + )}
); } + } + render() { return (
- {cardHeader &&
{cardHeader}
} - {content} + {this.renderPageHeader()} + {this.renderUnclaimedRewards()} + {}
); } diff --git a/ui/js/page/search/index.js b/ui/js/page/search/index.js index c8d93fae1..a6c571882 100644 --- a/ui/js/page/search/index.js +++ b/ui/js/page/search/index.js @@ -5,7 +5,7 @@ import { selectSearchQuery, selectCurrentSearchResults, } from "selectors/search"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import SearchPage from "./view"; const select = state => ({ diff --git a/ui/js/page/sendCredits/index.js b/ui/js/page/sendCredits/index.js new file mode 100644 index 000000000..2a36f2719 --- /dev/null +++ b/ui/js/page/sendCredits/index.js @@ -0,0 +1,5 @@ +import React from "react"; +import { connect } from "react-redux"; +import SendCreditsPage from "./view"; + +export default connect(null, null)(SendCreditsPage); diff --git a/ui/js/page/sendCredits/view.jsx b/ui/js/page/sendCredits/view.jsx new file mode 100644 index 000000000..96937bcc6 --- /dev/null +++ b/ui/js/page/sendCredits/view.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import SubHeader from "component/subHeader"; +import WalletSend from "component/walletSend"; + +const SendCreditsPage = props => { + return ( +
+ + +
+ ); +}; + +export default SendCreditsPage; diff --git a/ui/js/page/settings/view.jsx b/ui/js/page/settings/view.jsx index 1813e83bb..ad8c375b6 100644 --- a/ui/js/page/settings/view.jsx +++ b/ui/js/page/settings/view.jsx @@ -116,6 +116,7 @@ class SettingsPage extends React.PureComponent { return (
+ {/*

{__("Language")}

@@ -137,7 +138,7 @@ class SettingsPage extends React.PureComponent {
- + */}

{__("Download Directory")}

diff --git a/ui/js/page/showPage/index.js b/ui/js/page/show/index.js similarity index 100% rename from ui/js/page/showPage/index.js rename to ui/js/page/show/index.js diff --git a/ui/js/page/showPage/view.jsx b/ui/js/page/show/view.jsx similarity index 97% rename from ui/js/page/showPage/view.jsx rename to ui/js/page/show/view.jsx index 67bbf6178..aae0aabfe 100644 --- a/ui/js/page/showPage/view.jsx +++ b/ui/js/page/show/view.jsx @@ -2,7 +2,7 @@ import React from "react"; import lbryuri from "lbryuri"; import { BusyMessage } from "component/common"; import ChannelPage from "page/channel"; -import FilePage from "page/filePage"; +import FilePage from "page/file"; class ShowPage extends React.PureComponent { componentWillMount() { diff --git a/ui/js/page/start.js b/ui/js/page/start.js deleted file mode 100644 index 41a796c2c..000000000 --- a/ui/js/page/start.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import lbry from "../lbry.js"; - -class StartPage extends React.PureComponent { - componentWillMount() { - lbry.stop(); - } - - render() { - return ( -
-

{__("LBRY is Closed")}

- -
- ); - } -} - -export default StartPage; diff --git a/ui/js/page/transactionHistory/index.js b/ui/js/page/transactionHistory/index.js new file mode 100644 index 000000000..12acf47ad --- /dev/null +++ b/ui/js/page/transactionHistory/index.js @@ -0,0 +1,19 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doFetchTransactions } from "actions/wallet"; +import { + selectTransactionItems, + selectIsFetchingTransactions, +} from "selectors/wallet"; +import TransactionHistoryPage from "./view"; + +const select = state => ({ + fetchingTransactions: selectIsFetchingTransactions(state), + transactions: selectTransactionItems(state), +}); + +const perform = dispatch => ({ + fetchTransactions: () => dispatch(doFetchTransactions()), +}); + +export default connect(select, perform)(TransactionHistoryPage); diff --git a/ui/js/page/transactionHistory/view.jsx b/ui/js/page/transactionHistory/view.jsx new file mode 100644 index 000000000..8b9f1a90b --- /dev/null +++ b/ui/js/page/transactionHistory/view.jsx @@ -0,0 +1,32 @@ +import React from "react"; +import { BusyMessage } from "component/common"; +import SubHeader from "component/subHeader"; +import TransactionList from "component/transactionList"; + +class TransactionHistoryPage extends React.PureComponent { + componentWillMount() { + this.props.fetchTransactions(); + } + + render() { + const { fetchingTransactions, transactions } = this.props; + return ( +
+ +
+
+

{__("Transaction History")}

+
+
+ {fetchingTransactions && + } + {!fetchingTransactions && + } +
+
+
+ ); + } +} + +export default TransactionHistoryPage; diff --git a/ui/js/page/wallet/index.js b/ui/js/page/wallet/index.js index 810899d54..01fdf2a64 100644 --- a/ui/js/page/wallet/index.js +++ b/ui/js/page/wallet/index.js @@ -1,17 +1,5 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate } from "actions/app"; -import { selectCurrentPage } from "selectors/app"; -import { selectBalance } from "selectors/wallet"; import WalletPage from "./view"; -const select = state => ({ - currentPage: selectCurrentPage(state), - balance: selectBalance(state), -}); - -const perform = dispatch => ({ - navigate: path => dispatch(doNavigate(path)), -}); - -export default connect(select, perform)(WalletPage); +export default connect(null, null)(WalletPage); diff --git a/ui/js/page/wallet/view.jsx b/ui/js/page/wallet/view.jsx index 423c15473..b8bee1c94 100644 --- a/ui/js/page/wallet/view.jsx +++ b/ui/js/page/wallet/view.jsx @@ -1,36 +1,18 @@ import React from "react"; import SubHeader from "component/subHeader"; -import TransactionList from "component/transactionList"; -import WalletAddress from "component/walletAddress"; -import WalletSend from "component/walletSend"; -import Link from "component/link"; -import { CreditAmount } from "component/common"; +import WalletBalance from "component/walletBalance"; +import RewardSummary from "component/rewardSummary"; +import TransactionListRecent from "component/transactionListRecent"; const WalletPage = props => { - const { balance, currentPage, navigate } = props; - return ( -
+
-
-
-

{__("Balance")}

-
-
- -
-
-
- navigate("/backup")} - label={__("Backup Your Wallet")} - /> -
-
-
- {currentPage === "wallet" ? : ""} - {currentPage === "send" ? : ""} - {currentPage === "receive" ? : ""} +
+ + +
+
); }; diff --git a/ui/js/reducers/app.js b/ui/js/reducers/app.js index dd0237f28..857a5a708 100644 --- a/ui/js/reducers/app.js +++ b/ui/js/reducers/app.js @@ -1,12 +1,6 @@ import * as types from "constants/action_types"; import * as modalTypes from "constants/modal_types"; -const currentPath = () => { - const hash = document.location.hash; - if (hash !== "") return hash.replace(/^#/, ""); - else return "/discover"; -}; - const { remote } = require("electron"); const application = remote.app; const win = remote.BrowserWindow.getFocusedWindow(); @@ -14,28 +8,18 @@ const win = remote.BrowserWindow.getFocusedWindow(); const reducers = {}; const defaultState = { isLoaded: false, - isBackDisabled: true, - isForwardDisabled: true, - currentPath: currentPath(), - pathAfterAuth: "/discover", platform: process.platform, upgradeSkipped: sessionStorage.getItem("upgradeSkipped"), daemonVersionMatched: null, daemonReady: false, hasSignature: false, badgeNumber: 0, - history: { index: 0, stack: [] }, volume: sessionStorage.getItem("volume") || 1, }; reducers[types.DAEMON_READY] = function(state, action) { - const { history } = state; - const { page } = action.data; - history.stack.push(page); - return Object.assign({}, state, { daemonReady: true, - history, }); }; @@ -52,18 +36,6 @@ reducers[types.DAEMON_VERSION_MISMATCH] = function(state, action) { }); }; -reducers[types.CHANGE_PATH] = function(state, action) { - return Object.assign({}, state, { - currentPath: action.data.path, - }); -}; - -reducers[types.CHANGE_AFTER_AUTH_PATH] = function(state, action) { - return Object.assign({}, state, { - pathAfterAuth: action.data.path, - }); -}; - reducers[types.UPGRADE_CANCELLED] = function(state, action) { return Object.assign({}, state, { downloadProgress: null, @@ -171,55 +143,6 @@ reducers[types.WINDOW_FOCUSED] = function(state, action) { }); }; -reducers[types.HISTORY_NAVIGATE] = (state, action) => { - let page = false; - let location = false; - - // Get history from state - const { history } = state; - - if (action.data.page) { - // Get page - page = action.data.page; - } else if (action.data.location) { - // Get new location - location = action.data.location; - } - - // Add new location to stack - if (location) { - const lastItem = history.stack.length - 1; - - // Check for duplicated - let is_duplicate = lastItem > -1 - ? history.stack[lastItem].location === location - : false; - - if (!is_duplicate) { - // Create new page - page = { - index: history.stack.length, - location, - }; - - // Update index - history.index = history.stack.length; - - // Add to stack - history.stack.push(page); - } - } else if (page) { - // Update index - history.index = page.index; - } - - return Object.assign({}, state, { - history, - isBackDisabled: history.index === 0, // First page - isForwardDisabled: history.index === history.stack.length - 1, // Last page - }); -}; - reducers[types.VOLUME_CHANGED] = function(state, action) { return Object.assign({}, state, { volume: action.data.volume, diff --git a/ui/js/reducers/navigation.js b/ui/js/reducers/navigation.js new file mode 100644 index 000000000..821db9183 --- /dev/null +++ b/ui/js/reducers/navigation.js @@ -0,0 +1,80 @@ +import * as types from "constants/action_types"; +import { parseQueryParams } from "util/query_params"; + +const currentPath = () => { + const hash = document.location.hash; + if (hash !== "") return hash.replace(/^#/, ""); + else return "/discover"; +}; + +const reducers = {}; +const defaultState = { + currentPath: currentPath(), + pathAfterAuth: "/discover", + index: 0, + stack: [], +}; + +reducers[types.DAEMON_READY] = function(state, action) { + const { currentPath } = state; + const params = parseQueryParams(currentPath.split("?")[1] || ""); + + return Object.assign({}, state, { + stack: [{ path: currentPath, scrollY: 0 }], + }); +}; + +reducers[types.CHANGE_PATH] = function(state, action) { + return Object.assign({}, state, { + currentPath: action.data.path, + }); +}; + +reducers[types.CHANGE_AFTER_AUTH_PATH] = function(state, action) { + return Object.assign({}, state, { + pathAfterAuth: action.data.path, + }); +}; + +reducers[types.HISTORY_NAVIGATE] = (state, action) => { + const { stack, index } = state; + + let newState = {}; + + const path = action.data.url; + + // Check for duplicated + if (action.data.index >= 0) { + newState.index = action.data.index; + } else if (!stack[index] || stack[index].path !== path) { + newState.stack = [...stack.slice(0, index + 1), { path, scrollY: 0 }]; + newState.index = newState.stack.length - 1; + } + + history.replaceState(null, null, "#" + path); //this allows currentPath() to retain the URL on reload + + return Object.assign({}, state, newState); +}; + +reducers[types.WINDOW_SCROLLED] = (state, action) => { + const { stack, index } = state; + const { scrollY } = action.data; + + return Object.assign({}, state, { + stack: state.stack.map((stackItem, itemIndex) => { + if (itemIndex !== index) { + return stackItem; + } + return { + ...stackItem, + scrollY: scrollY, + }; + }), + }); +}; + +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 index 0994d730b..42279d01d 100644 --- a/ui/js/reducers/rewards.js +++ b/ui/js/reducers/rewards.js @@ -3,7 +3,8 @@ import * as types from "constants/action_types"; const reducers = {}; const defaultState = { fetching: false, - rewardsByType: {}, + claimedRewardsById: {}, //id => reward + unclaimedRewardsByType: {}, claimPendingByType: {}, claimErrorsByType: {}, }; @@ -17,11 +18,19 @@ reducers[types.FETCH_REWARDS_STARTED] = function(state, action) { reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) { const { userRewards } = action.data; - const rewardsByType = {}; - userRewards.forEach(reward => (rewardsByType[reward.reward_type] = reward)); + let unclaimedRewards = {}, + claimedRewards = {}; + userRewards.forEach(reward => { + if (reward.transaction_id) { + claimedRewards[reward.id] = reward; + } else { + unclaimedRewards[reward.reward_type] = reward; + } + }); return Object.assign({}, state, { - rewardsByType: rewardsByType, + claimedRewardsById: claimedRewards, + unclaimedRewardsByType: unclaimedRewards, fetching: false, }); }; @@ -55,16 +64,22 @@ reducers[types.CLAIM_REWARD_STARTED] = function(state, action) { reducers[types.CLAIM_REWARD_SUCCESS] = function(state, action) { const { reward } = action.data; - const existingReward = state.rewardsByType[reward.reward_type]; + let unclaimedRewardsByType = Object.assign({}, state.unclaimedRewardsByType); + const existingReward = unclaimedRewardsByType[reward.reward_type]; + delete state.unclaimedRewardsByType[reward.reward_type]; + const newReward = Object.assign({}, reward, { reward_title: existingReward.reward_title, reward_description: existingReward.reward_description, }); - const rewardsByType = Object.assign({}, state.rewardsByType); - rewardsByType[reward.reward_type] = newReward; + let claimedRewardsById = Object.assign({}, state.claimedRewardsById); + claimedRewardsById[reward.id] = newReward; - const newState = Object.assign({}, state, { rewardsByType }); + const newState = Object.assign({}, state, { + unclaimedRewardsByType, + claimedRewardsById, + }); return setClaimRewardState(newState, newReward, false, ""); }; diff --git a/ui/js/reducers/settings.js b/ui/js/reducers/settings.js index e6d32f0f0..16e53e2da 100644 --- a/ui/js/reducers/settings.js +++ b/ui/js/reducers/settings.js @@ -1,4 +1,5 @@ import * as types from "constants/action_types"; +import * as settings from "constants/settings"; import LANGUAGES from "constants/languages"; import lbry from "lbry"; @@ -6,7 +7,11 @@ const reducers = {}; const defaultState = { clientSettings: { showNsfw: lbry.getClientSetting("showNsfw"), - language: lbry.getClientSetting("language"), + welcome_acknowledged: lbry.getClientSetting(settings.NEW_USER_ACKNOWLEDGED), + credit_intro_acknowledged: lbry.getClientSetting( + settings.CREDIT_INTRO_ACKNOWLEDGED + ), + language: lbry.getClientSetting(settings.LANGUAGE), }, languages: {}, }; diff --git a/ui/js/reducers/user.js b/ui/js/reducers/user.js index 02203cdfe..bc19f3464 100644 --- a/ui/js/reducers/user.js +++ b/ui/js/reducers/user.js @@ -8,6 +8,11 @@ const defaultState = { emailNewIsPending: false, emailNewErrorMessage: "", emailToVerify: "", + inviteNewErrorMessage: "", + inviteNewIsPending: false, + inviteStatusIsPending: false, + invitesRemaining: undefined, + invitees: undefined, user: undefined, }; @@ -142,6 +147,49 @@ reducers[types.FETCH_ACCESS_TOKEN_SUCCESS] = function(state, action) { }); }; +reducers[types.USER_INVITE_STATUS_FETCH_STARTED] = function(state, action) { + return Object.assign({}, state, { + inviteStatusIsPending: true, + }); +}; + +reducers[types.USER_INVITE_STATUS_FETCH_SUCCESS] = function(state, action) { + return Object.assign({}, state, { + inviteStatusIsPending: false, + invitesRemaining: action.data.invitesRemaining, + invitees: action.data.invitees, + }); +}; + +reducers[types.USER_INVITE_NEW_STARTED] = function(state, action) { + return Object.assign({}, state, { + inviteNewIsPending: true, + inviteNewErrorMessage: "", + }); +}; + +reducers[types.USER_INVITE_NEW_SUCCESS] = function(state, action) { + return Object.assign({}, state, { + inviteNewIsPending: false, + inviteNewErrorMessage: "", + }); +}; + +reducers[types.USER_INVITE_NEW_FAILURE] = function(state, action) { + return Object.assign({}, state, { + inviteNewIsPending: false, + inviteNewErrorMessage: action.data.error.message, + }); +}; + +reducers[types.USER_INVITE_STATUS_FETCH_FAILURE] = function(state, action) { + return Object.assign({}, state, { + inviteStatusIsPending: false, + invitesRemaining: null, + invitees: null, + }); +}; + export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; if (handler) return handler(state, action); diff --git a/ui/js/rewards.js b/ui/js/rewards.js index fd952032c..0a3475246 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -95,6 +95,18 @@ rewards.TYPE_FIRST_STREAM = "first_stream"; rewards.TYPE_MANY_DOWNLOADS = "many_downloads"; rewards.TYPE_FIRST_PUBLISH = "first_publish"; rewards.TYPE_FEATURED_DOWNLOAD = "featured_download"; +rewards.TYPE_REFERRAL = "referral"; +rewards.SORT_ORDER = [ + rewards.TYPE_NEW_USER, + rewards.TYPE_CONFIRM_EMAIL, + rewards.TYPE_FIRST_STREAM, + rewards.TYPE_FIRST_CHANNEL, + rewards.TYPE_FIRST_PUBLISH, + rewards.TYPE_FEATURED_DOWNLOAD, + rewards.TYPE_MANY_DOWNLOADS, + rewards.TYPE_REFERRAL, + rewards.TYPE_NEW_DEVELOPER, +]; rewards.claimReward = function(type) { function requestReward(resolve, reject, params) { diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js index 1ad6a81b9..3e8e68223 100644 --- a/ui/js/selectors/app.js +++ b/ui/js/selectors/app.js @@ -1,83 +1,7 @@ import { createSelector } from "reselect"; -import { parseQueryParams, toQueryString } from "util/query_params"; -import * as settings from "constants/settings.js"; -import lbry from "lbry"; -import lbryuri from "lbryuri"; 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 => { - return path.replace(/^\//, "").split("?")[0]; -}); - -export const selectCurrentParams = createSelector(selectCurrentPath, path => { - if (path === undefined) return {}; - if (!path.match(/\?/)) return {}; - - return parseQueryParams(path.split("?")[1]); -}); - -export const selectPageTitle = createSelector( - selectCurrentPage, - selectCurrentParams, - (page, params) => { - switch (page) { - case "settings": - return __("Settings"); - case "report": - return __("Report"); - case "wallet": - return __("Wallet"); - case "send": - return __("Send"); - case "receive": - return __("Receive"); - case "backup": - return __("Backup"); - case "rewards": - return __("Rewards"); - case "start": - return __("Start"); - case "publish": - return __("Publish"); - case "help": - return __("Help"); - case "developer": - return __("Developer"); - case "search": - return params.query - ? __("Search results for %s", params.query) - : __("Search"); - case "show": { - const parts = [lbryuri.normalize(params.uri)]; - // If the params has any keys other than "uri" - if (Object.keys(params).length > 1) { - parts.push(toQueryString(Object.assign({}, params, { uri: null }))); - } - return parts.join("?"); - } - case "downloaded": - return __("Downloads & Purchases"); - case "published": - return __("Publishes"); - case "discover": - return __("Home"); - default: - return ""; - } - } -); - export const selectPlatform = createSelector( _selectState, state => state.platform @@ -132,37 +56,6 @@ export const selectDownloadComplete = createSelector( state => state.upgradeDownloadCompleted ); -export const selectHeaderLinks = createSelector(selectCurrentPage, page => { - // This contains intentional fall throughs - switch (page) { - case "wallet": - case "send": - case "receive": - case "rewards": - case "backup": - return { - wallet: __("Overview"), - send: __("Send"), - receive: __("Receive"), - rewards: __("Rewards"), - }; - case "downloaded": - case "published": - return { - downloaded: __("Downloaded"), - published: __("Published"), - }; - case "settings": - case "help": - return { - settings: __("Settings"), - help: __("Help"), - }; - default: - return null; - } -}); - export const selectUpgradeSkipped = createSelector( _selectState, state => state.upgradeSkipped @@ -203,16 +96,6 @@ export const selectSnackBarSnacks = createSelector( snackBar => snackBar.snacks || [] ); -export const selectCreditsIntroAcknowledged = createSelector( - _selectState, - state => lbry.getClientSetting(settings.CREDIT_INTRO_ACKNOWLEDGED) -); - -export const selectWelcomeModalAcknowledged = createSelector( - _selectState, - state => lbry.getClientSetting(settings.FIRST_RUN_ACKNOWLEDGED) -); - export const selectBadgeNumber = createSelector( _selectState, state => state.badgeNumber @@ -223,41 +106,4 @@ export const selectCurrentLanguage = createSelector( () => app.i18n.getLocale() || "en" ); -export const selectPathAfterAuth = createSelector( - _selectState, - state => state.pathAfterAuth -); - -export const selectIsBackDisabled = createSelector( - _selectState, - state => state.isBackDisabled -); - -export const selectIsForwardDisabled = createSelector( - _selectState, - state => state.isForwardDisabled -); - -export const selectHistoryBack = createSelector(_selectState, state => { - const { history } = state; - const index = history.index - 1; - - // Check if page exists - if (index > -1) { - // Get back history - return history.stack[index]; - } -}); - -export const selectHistoryForward = createSelector(_selectState, state => { - const { history } = state; - const index = history.index + 1; - - // Check if page exists - if (index <= history.stack.length) { - // Get forward history - return history.stack[index]; - } -}); - export const selectVolume = createSelector(_selectState, state => state.volume); diff --git a/ui/js/selectors/claims.js b/ui/js/selectors/claims.js index 2c4b4b511..630e72c3e 100644 --- a/ui/js/selectors/claims.js +++ b/ui/js/selectors/claims.js @@ -1,5 +1,5 @@ import { createSelector } from "reselect"; -import { selectCurrentParams } from "selectors/app"; +import { selectCurrentParams } from "selectors/navigation"; import lbryuri from "lbryuri"; const _selectState = state => state.claims || {}; diff --git a/ui/js/selectors/cost_info.js b/ui/js/selectors/cost_info.js index ec5280485..b40d62d5e 100644 --- a/ui/js/selectors/cost_info.js +++ b/ui/js/selectors/cost_info.js @@ -1,4 +1,5 @@ import { createSelector } from "reselect"; +import { selectCurrentParams } from "selectors/navigation"; export const _selectState = state => state.costInfo || {}; @@ -15,6 +16,13 @@ export const makeSelectCostInfoForUri = () => { return createSelector(selectCostInfoForUri, costInfo => costInfo); }; +export const selectCostForCurrentPageUri = createSelector( + selectAllCostInfoByUri, + selectCurrentParams, + (costInfo, params) => + params.uri && costInfo[params.uri] ? costInfo[params.uri].cost : undefined +); + export const selectFetchingCostInfo = createSelector( _selectState, state => state.fetching || {} diff --git a/ui/js/selectors/navigation.js b/ui/js/selectors/navigation.js new file mode 100644 index 000000000..ca95e259f --- /dev/null +++ b/ui/js/selectors/navigation.js @@ -0,0 +1,139 @@ +import { createSelector } from "reselect"; +import { parseQueryParams, toQueryString } from "util/query_params"; +import * as settings from "constants/settings.js"; +import lbryuri from "lbryuri"; + +export const _selectState = state => state.navigation || {}; + +export const selectCurrentPath = createSelector( + _selectState, + state => state.currentPath +); + +export const selectCurrentPage = createSelector(selectCurrentPath, path => { + return path.replace(/^\//, "").split("?")[0]; +}); + +export const selectCurrentParams = createSelector(selectCurrentPath, path => { + if (path === undefined) return {}; + if (!path.match(/\?/)) return {}; + + return parseQueryParams(path.split("?")[1]); +}); + +export const selectHeaderLinks = createSelector(selectCurrentPage, page => { + // This contains intentional fall throughs + switch (page) { + case "wallet": + case "history": + case "send": + case "receive": + case "invite": + case "rewards": + case "backup": + return { + wallet: __("Overview"), + history: __("History"), + send: __("Send"), + receive: __("Receive"), + rewards: __("Rewards"), + invite: __("Invites"), + }; + case "downloaded": + case "published": + return { + downloaded: __("Downloaded"), + published: __("Published"), + }; + case "settings": + case "help": + return { + settings: __("Settings"), + help: __("Help"), + }; + default: + return null; + } +}); + +export const selectPageTitle = createSelector( + selectCurrentPage, + selectCurrentParams, + (page, params) => { + switch (page) { + case "settings": + return __("Settings"); + case "report": + return __("Report"); + case "wallet": + return __("Wallet"); + case "send": + return __("Send Credits"); + case "receive": + return __("Wallet Address"); + case "backup": + return __("Backup Your Wallet"); + case "rewards": + return __("Rewards"); + case "invite": + return __("Invites"); + case "start": + return __("Start"); + case "publish": + return __("Publish"); + case "help": + return __("Help"); + case "developer": + return __("Developer"); + case "search": + return params.query + ? __("Search results for %s", params.query) + : __("Search"); + case "show": { + const parts = [lbryuri.normalize(params.uri)]; + // If the params has any keys other than "uri" + if (Object.keys(params).length > 1) { + parts.push(toQueryString(Object.assign({}, params, { uri: null }))); + } + return parts.join("?"); + } + case "downloaded": + return __("Downloads & Purchases"); + case "published": + return __("Publishes"); + case "discover": + return __("Home"); + case false: + case null: + case "": + return ""; + default: + return page[0].toUpperCase() + (page.length > 0 ? page.substr(1) : ""); + } + } +); + +export const selectPathAfterAuth = createSelector( + _selectState, + state => state.pathAfterAuth +); + +export const selectIsBackDisabled = createSelector( + _selectState, + state => state.index === 0 +); + +export const selectIsForwardDisabled = createSelector( + _selectState, + state => state.index === state.stack.length - 1 +); + +export const selectHistoryIndex = createSelector( + _selectState, + state => state.index +); + +export const selectHistoryStack = createSelector( + _selectState, + state => state.stack +); diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index bddb7714e..c2a646563 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -1,16 +1,33 @@ import { createSelector } from "reselect"; import { selectUser } from "selectors/user"; +import rewards from "rewards"; const _selectState = state => state.rewards || {}; -export const selectRewardsByType = createSelector( +export const selectUnclaimedRewardsByType = createSelector( _selectState, - state => state.rewardsByType || {} + state => state.unclaimedRewardsByType ); -export const selectRewards = createSelector( - selectRewardsByType, - byType => Object.values(byType) || [] +export const selectClaimedRewardsById = createSelector( + _selectState, + state => state.claimedRewardsById +); + +export const selectClaimedRewards = createSelector( + selectClaimedRewardsById, + byId => Object.values(byId) || [] +); + +export const selectUnclaimedRewards = createSelector( + selectUnclaimedRewardsByType, + byType => + Object.values(byType).sort(function(a, b) { + return rewards.SORT_ORDER.indexOf(a.reward_type) < + rewards.SORT_ORDER.indexOf(b.reward_type) + ? -1 + : 1; + }) || [] ); export const selectIsRewardEligible = createSelector( @@ -23,10 +40,12 @@ export const selectFetchingRewards = createSelector( state => !!state.fetching ); -export const selectTotalRewardValue = createSelector(selectRewards, rewards => - rewards.reduce((sum, reward) => { - return sum + reward.reward_amount; - }, 0) +export const selectUnclaimedRewardValue = createSelector( + selectUnclaimedRewards, + rewards => + rewards.reduce((sum, reward) => { + return sum + reward.reward_amount; + }, 0) ); export const selectHasClaimedReward = (state, props) => { @@ -65,9 +84,16 @@ export const makeSelectClaimRewardError = () => { }; const selectRewardByType = (state, props) => { - return selectRewardsByType(state)[props.reward_type]; + return selectUnclaimedRewardsByType(state)[props.reward_type]; }; export const makeSelectRewardByType = () => { return createSelector(selectRewardByType, reward => reward); }; + +export const makeSelectRewardAmountByType = () => { + return createSelector( + selectRewardByType, + reward => (reward ? reward.reward_amount : 0) + ); +}; diff --git a/ui/js/selectors/search.js b/ui/js/selectors/search.js index 1daa1ef79..e3c6fbb4a 100644 --- a/ui/js/selectors/search.js +++ b/ui/js/selectors/search.js @@ -1,5 +1,5 @@ import { createSelector } from "reselect"; -import { selectPageTitle, selectCurrentPage } from "selectors/app"; +import { selectPageTitle, selectCurrentPage } from "selectors/navigation"; export const _selectState = state => state.search || {}; @@ -38,6 +38,8 @@ export const selectWunderBarAddress = createSelector( export const selectWunderBarIcon = createSelector(selectCurrentPage, page => { switch (page) { + case "auth": + return "icon-user"; case "search": return "icon-search"; case "settings": @@ -50,24 +52,29 @@ export const selectWunderBarIcon = createSelector(selectCurrentPage, page => { return "icon-folder"; case "published": return "icon-folder"; - case "start": - return "icon-file"; - case "rewards": - return "icon-bank"; - case "wallet": + case "history": + return "icon-history"; case "send": + return "icon-send"; + case "rewards": + return "icon-rocket"; + case "invite": + return "icon-envelope-open"; + case "address": case "receive": + return "icon-address-book"; + case "wallet": case "backup": return "icon-bank"; case "show": return "icon-file"; case "publish": return "icon-upload"; - case "developer": - return "icon-file"; case "developer": return "icon-code"; case "discover": return "icon-home"; + default: + return "icon-file"; } }); diff --git a/ui/js/selectors/settings.js b/ui/js/selectors/settings.js index 61c7dc4f0..10b9191df 100644 --- a/ui/js/selectors/settings.js +++ b/ui/js/selectors/settings.js @@ -12,6 +12,13 @@ export const selectClientSettings = createSelector( state => state.clientSettings || {} ); +export const makeSelectClientSetting = setting => { + return createSelector( + selectClientSettings, + settings => (settings ? settings[setting] : undefined) + ); +}; + export const selectSettingsIsGenerous = createSelector( selectDaemonSettings, settings => settings && settings.is_generous_host diff --git a/ui/js/selectors/user.js b/ui/js/selectors/user.js index a499e5666..43ac07413 100644 --- a/ui/js/selectors/user.js +++ b/ui/js/selectors/user.js @@ -68,3 +68,33 @@ export const selectAccessToken = createSelector( _selectState, state => state.accessToken ); + +export const selectUserInviteStatusIsPending = createSelector( + _selectState, + state => state.inviteStatusIsPending +); + +export const selectUserInvitesRemaining = createSelector( + _selectState, + state => state.invitesRemaining +); + +export const selectUserInvitees = createSelector( + _selectState, + state => state.invitees +); + +export const selectUserInviteStatusFailed = createSelector( + selectUserInvitesRemaining, + inviteStatus => selectUserInvitesRemaining === null +); + +export const selectUserInviteNewIsPending = createSelector( + _selectState, + state => state.inviteNewIsPending +); + +export const selectUserInviteNewErrorMessage = createSelector( + _selectState, + state => state.inviteNewErrorMessage +); diff --git a/ui/js/selectors/wallet.js b/ui/js/selectors/wallet.js index 6637c8bff..dbf32639f 100644 --- a/ui/js/selectors/wallet.js +++ b/ui/js/selectors/wallet.js @@ -1,5 +1,4 @@ import { createSelector } from "reselect"; -import { selectCurrentPage, selectDaemonReady } from "selectors/app"; export const _selectState = state => state.wallet || {}; @@ -40,6 +39,24 @@ export const selectTransactionItems = createSelector( } ); +export const selectRecentTransactions = createSelector( + selectTransactionItems, + transactions => { + let threshold = new Date(); + threshold.setDate(threshold.getDate() - 7); + return transactions.filter(transaction => { + return transaction.date > threshold; + }); + } +); + +export const selectHasTransactions = createSelector( + selectTransactionItems, + transactions => { + return transactions && transactions.length > 0; + } +); + export const selectIsFetchingTransactions = createSelector( _selectState, state => state.fetchingTransactions diff --git a/ui/js/store.js b/ui/js/store.js index 5eb84d4cf..af1e68c3a 100644 --- a/ui/js/store.js +++ b/ui/js/store.js @@ -5,6 +5,7 @@ import claimsReducer from "reducers/claims"; import contentReducer from "reducers/content"; import costInfoReducer from "reducers/cost_info"; import fileInfoReducer from "reducers/file_info"; +import navigationReducer from "reducers/navigation"; import rewardsReducer from "reducers/rewards"; import searchReducer from "reducers/search"; import settingsReducer from "reducers/settings"; @@ -13,8 +14,8 @@ import walletReducer from "reducers/wallet"; import { persistStore, autoRehydrate } from "redux-persist"; import createCompressor from "redux-persist-transform-compress"; import createFilter from "redux-persist-transform-filter"; -import { REHYDRATE } from "redux-persist/constants"; -import createActionBuffer from "redux-action-buffer"; +//import { REHYDRATE } from "redux-persist/constants"; +//import createActionBuffer from "redux-action-buffer"; const localForage = require("localforage"); const redux = require("redux"); @@ -55,6 +56,7 @@ function enableBatching(reducer) { const reducers = redux.combineReducers({ app: appReducer, + navigation: navigationReducer, availability: availabilityReducer, claims: claimsReducer, fileInfo: fileInfoReducer, diff --git a/ui/package.json b/ui/package.json index 02c223936..3dc9384a3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.14.3", + "version": "0.15.0", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", diff --git a/ui/scss/_global.scss b/ui/scss/_global.scss index b554b77a1..9a838b2d7 100644 --- a/ui/scss/_global.scss +++ b/ui/scss/_global.scss @@ -2,8 +2,8 @@ $spacing-vertical: 24px; -$padding-button: 12px; -$padding-text-link: 4px; +$padding-button: $spacing-vertical * 2/3; +$padding-text-link: 0px; $color-primary: #155B4A; $color-primary-light: saturate(lighten($color-primary, 50%), 20%); diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index b8d9ec040..338ccac3c 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -134,15 +134,6 @@ p } } -/*should this be here or work this way? had to hack additional rule below*/ -.icon:only-child { - position: relative; - top: 0.16em; -} -.icon-featured > .icon { - top: 0; -} - .help { font-size: .85em; color: $color-help; diff --git a/ui/scss/_icons.scss b/ui/scss/_icons.scss index 441113e39..cb487975b 100644 --- a/ui/scss/_icons.scss +++ b/ui/scss/_icons.scss @@ -2,8 +2,8 @@ @font-face { font-family: 'FontAwesome'; - src: url('../font/fontawesome-webfont.eot?v=4.3.0'); - src: url('../font/fontawesome-webfont.eot?#iefix&v=4.3.0') format('embedded-opentype'), url('../font/fontawesome-webfont.woff2?v=4.3.0') format('woff2'), url('../font/fontawesome-webfont.woff?v=4.3.0') format('woff'), url('../font/fontawesome-webfont.ttf?v=4.3.0') format('truetype'), url('../font/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular') format('svg'); + src: url('../font/fontawesome-webfont.eot?v=4.7.0'); + src: url('../font/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../font/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../font/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../font/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../font/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); font-weight: normal; font-style: normal; } diff --git a/ui/scss/component/_button.scss b/ui/scss/component/_button.scss index 5c6fed22f..cd00b8fe4 100644 --- a/ui/scss/component/_button.scss +++ b/ui/scss/component/_button.scss @@ -8,7 +8,7 @@ $button-focus-shift: 12%; + .button-set-item { - margin-left: $padding-button; + margin-left: $spacing-vertical; } } diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss index 712eb9ea7..472e95d63 100644 --- a/ui/scss/component/_card.scss +++ b/ui/scss/component/_card.scss @@ -23,10 +23,11 @@ $width-card-small: $spacing-vertical * 10; } .card__title-primary, .card__title-identity, -.card__actions, .card__content, -.card__subtext { - padding: 0 $padding-card-horizontal; +.card__subtext, +.card__actions { + padding-left: $padding-card-horizontal; + padding-right: $padding-card-horizontal; } .card--small { .card__title-primary, @@ -39,6 +40,7 @@ $width-card-small: $spacing-vertical * 10; } .card__title-primary { margin-top: $spacing-vertical * 2/3; + margin-bottom: $spacing-vertical * 2/3; } .card__title-identity { margin-top: $spacing-vertical * 1/3; @@ -46,13 +48,6 @@ $width-card-small: $spacing-vertical * 10; } .card__actions { margin-top: $spacing-vertical * 2/3; -} -.card__actions--bottom { - margin-top: $spacing-vertical * 1/3; - margin-bottom: $spacing-vertical * 1/3; -} -.card__actions--form-submit { - margin-top: $spacing-vertical; margin-bottom: $spacing-vertical * 2/3; } .card__content { @@ -259,4 +254,21 @@ $padding-right-card-hover-hack: 30px; .card__icon-featured-content { color: orangered; +} + + +/* +if we keep doing things like this, we should add a real grid system, but I'm going to be a selective dick about it - Jeremy + */ +.card-grid { + $margin-card-grid: $spacing-vertical * 2/3; + display:flex; + flex-wrap: wrap; + > .card { + width: $width-page-constrained / 2 - $margin-card-grid / 2; + flex-grow:1; + } + > .card:nth-of-type(2n - 1):not(:last-child) { + margin-right: $margin-card-grid; + } } \ No newline at end of file diff --git a/ui/scss/component/_table.scss b/ui/scss/component/_table.scss index 9d60cf6e8..38893eae5 100644 --- a/ui/scss/component/_table.scss +++ b/ui/scss/component/_table.scss @@ -24,6 +24,9 @@ table.table-standard { img { vertical-align: text-bottom; } + &.text-center { + text-align: center; + } } tr.thead:not(:first-child) th { border-top: 1px solid #e2e2e2; @@ -49,6 +52,11 @@ table.table-standard { } } } +.table-standard--definition-list { + th { + text-align: right; + } +} table.table-stretch { width: 100%; diff --git a/ui/scss/component/_video.scss b/ui/scss/component/_video.scss index 5515cbf8a..61143a2dd 100644 --- a/ui/scss/component/_video.scss +++ b/ui/scss/component/_video.scss @@ -1,3 +1,5 @@ +@import "../global"; + video { object-fit: contain; box-sizing: border-box;
{__("App")} {user && user.primary_email ? user.primary_email - : {__("none")}} + : + {__("none")} + ( doAuth()} + label={__("set email")} + />) + } +
{__("Reward Eligible")} + {user && user.is_reward_approved + ? + : }