diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b00c73445..206eca6b6 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.15.1 +current_version = 0.16.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 3ec3ce54c..0e4786a85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,8 @@ 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 edit button on published content / improved UX for editing claims. - * File pages now show the time of a publish. - * The "auth token" displayable on Help offers security warning - * Added a new component for rendering dates and times. This component can render the date and time of a block height, as well. - * Improved scrollbar ( UI / UX ), now is less annoying and can be customized for different themes. + * + * ### Changed * @@ -31,6 +27,29 @@ Web UI version numbers should always match the corresponding version of LBRY App * * +## [0.16.0] - 2017-09-21 + +### Added + * Added a tipping button to send LBRY Credits to a creator. + * Added an edit button on published content. Significantly improved UX for editing claims. + * Added theme settings option and new Dark theme. + * Significantly more detail is shown about past transactions and new filtering options for transactions. + * File pages now show the time of a publish. + * The "auth token" displayable on Help offers security warning + * Added a new component for rendering dates and times. This component can render the date and time of a block height, as well. + * Added a `Form` component, to further progress towards form sanity. + * Added `gnome-keyring` dependency to .deb + + +### Changed + * CSS significantly refactored to support CSS vars (and consequently easy theming). + + +### Fixed + * URLs on cards no longer wrap and show an ellipsis if longer than one line + + + ## [0.15.1] - 2017-09-08 ### Added diff --git a/README.md b/README.md index 5e94a8bf5..623ee135a 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,52 @@ to create distributable packages, which is run by calling: `node_modules/.bin/build -p never` -### Development on Windows +## Development on Windows -This project has currently only been worked on in Linux and macOS. If you are on Windows, you can -checkout out the build steps in [appveyor.yml](https://github.com/lbryio/lbry-app/blob/master/.appveyor.yml) and probably figure out something from there. +### Windows Dependency + +1. Download and install `npm` and `node` from nodejs.org +2. Download and install `python 2.7` from python.org +3. Download and Install `Microsoft Visual C++ Compiler for Python 2.7` from Microsoft +4. Download and install `.NET Framework 2.0 Software Development Kit (SDK) (x64)` from Microsoft + +### One-time Setup +1. Open command prompt in the root of the project and run the following; +``` +python -m pip install -r build\requirements.txt +python build\set_version.py +npm install -g yarn +yarn install +``` +2. Change directory to `app` and run the following; +``` +yarn install +node_modules\.bin\electron-rebuild +node_modules\.bin\electron-rebuild +cd .. +``` +3. Change directory to `ui` and run the following +``` +yarn install +npm rebuild node-sass +node node_modules\node-sass\bin\node-sass --output dist\css --sourcemap=none scss\ +node_modules\.bin\webpack --config webpack.dev.config.js +xcopy dist ..\app\dist +cd .. +``` +4. Download the lbry daemon and cli binaries and place them in `app\dist\` + +### Building lbry-app +1. run `node_modules\.bin\build -p never` from the root of the project. + +### Running the electron app +1. Run `./node_modules/.bin/electron app` + +### Ongoing Development +1. `cd ui` +2. `watch.bat` + +This will set up a monitor that will automatically compile any changes to JS or CSS folders inside of the `ui` folder. This allows you to make changes and see them immediately by reloading the app. ## Internationalization diff --git a/app/package.json b/app/package.json index e9022a2e9..15878e6e7 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.15.1", + "version": "0.16.0", "main": "main.js", "description": "A browser for the LBRY network, a digital marketplace controlled by its users.", "author": { @@ -20,7 +20,7 @@ "electron-rebuild": "^1.5.11" }, "lbrySettings": { - "lbrynetDaemonVersion": "0.16.0rc8", + "lbrynetDaemonVersion": "0.16.1", "lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-daemon-vDAEMONVER-OSNAME.zip" }, "license": "MIT" diff --git a/build.sh b/build.sh index 1105489f8..6dc7044f1 100755 --- a/build.sh +++ b/build.sh @@ -1,5 +1,4 @@ #!/bin/bash # this is here because teamcity runs /build.sh to build the project -set -euxo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -$DIR/build/build.sh \ No newline at end of file +$DIR/build/build.sh diff --git a/build/build.sh b/build/build.sh index 96f7bf4af..86c9a6217 100755 --- a/build/build.sh +++ b/build/build.sh @@ -1,7 +1,6 @@ #!/bin/bash set -euo pipefail -set -x ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" cd "$ROOT" @@ -10,8 +9,10 @@ BUILD_DIR="$ROOT/build" LINUX=false OSX=false if [ "$(uname)" == "Darwin" ]; then + echo -e "\033[0;32mBuilding for OSX\x1b[m" OSX=true elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + echo -e "\033[0;32mBuilding for Linux\x1b[m" LINUX=true else echo -e "\033[1;31mPlatform detection failed\x1b[m" @@ -28,9 +29,13 @@ FULL_BUILD="${FULL_BUILD:-false}" if [ -n "${TEAMCITY_VERSION:-}" -o -n "${APPVEYOR:-}" ]; then FULL_BUILD="true" fi +if [ "$FULL_BUILD" != "true" ]; then + echo -e "\033[1;36mDependencies will NOT be installed. Run with 'FULL_BUILD=true' to install dependencies.\x1b[m" +fi if [ "$FULL_BUILD" == "true" ]; then # install dependencies + echo -e "\033[0;32mInstalling Dependencies\x1b[m" $BUILD_DIR/prebuild.sh VENV="$BUILD_DIR/venv" @@ -57,7 +62,7 @@ yarn install ############ # UI # ############ - +echo -e "\033[0;32mCompiling UI\x1b[m" ( cd "$ROOT/ui" yarn install @@ -73,7 +78,7 @@ yarn install #################### # daemon and cli # #################### - +echo -e "\033[0;32mGrabbing Daemon and CLI\x1b[m" if $OSX; then OSNAME="macos" else @@ -99,7 +104,7 @@ fi ################### # Build the app # ################### - +echo -e '\033[0;32mBuilding Lbry-app\x1b[m' ( cd "$ROOT/app" yarn install diff --git a/package.json b/package.json index 673ea306b..18565cf6b 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,9 @@ "Exec": "/opt/LBRY/lbry %U" } }, + "deb": { + "depends": ["gconf2", "gconf-service", "libnotify4", "libappindicator1", "libxtst6", "libnss3", "libsecret-1-0"] + }, "win": { "target": "nsis" }, diff --git a/ui/dist/themes/dark.css b/ui/dist/themes/dark.css index e99b70df7..b1fdfebb4 100644 --- a/ui/dist/themes/dark.css +++ b/ui/dist/themes/dark.css @@ -27,8 +27,8 @@ --search-border: 1px solid rgba(0,0,0, 0.25); /* Tab */ - --tab-color: #757575; - --tab-active-color: #CCC; + --tab-color: rgba(255,255,255, 0.5) ; + --tab-active-color: rgba(255,255,255, 0.75); /* Header */ --header-color: #CCC; @@ -47,4 +47,7 @@ /* Scrollbar */ --scrollbar-thumb-bg: rgba(255, 255, 255, 0.20); --scrollbar-thumb-hover-bg: rgba(255, 255, 255, 0.35); + + /* Divider */ + --divider: 1px solid rgba(255,255,255, 0.12); } diff --git a/ui/js/actions/availability.js b/ui/js/actions/availability.js index 2c7cbc3cb..4cde9d2ed 100644 --- a/ui/js/actions/availability.js +++ b/ui/js/actions/availability.js @@ -4,6 +4,11 @@ import { selectFetchingAvailability } from "selectors/availability"; export function doFetchAvailability(uri) { return function(dispatch, getState) { + /* + this is disabled atm - Jeremy + */ + return; + const state = getState(); const alreadyFetching = !!selectFetchingAvailability(state)[uri]; diff --git a/ui/js/actions/claims.js b/ui/js/actions/claims.js deleted file mode 100644 index 71f94852c..000000000 --- a/ui/js/actions/claims.js +++ /dev/null @@ -1,56 +0,0 @@ -import lbry from "lbry"; -import { selectBalance } from "selectors/wallet"; -import { doOpenModal, doShowSnackBar } from "actions/app"; -import * as types from "constants/action_types"; -import * as modals from "constants/modal_types"; - -export function doSendSupport(amount, claim_id) { - return function(dispatch, getState) { - const state = getState(); - const balance = selectBalance(state); - - if (balance - amount <= 0) { - return dispatch(doOpenModal(modals.INSUFFICIENT_BALANCE)); - } - - dispatch({ - type: types.SUPPORT_TRANSACTION_STARTED, - }); - - const successCallback = results => { - if (results.txid) { - dispatch({ - type: types.SUPPORT_TRANSACTION_COMPLETED, - }); - dispatch( - doShowSnackBar({ - message: __(`You sent ${amount} LBC as support, Mahalo!`), - linkText: __("History"), - linkTarget: __("/wallet"), - }) - ); - } else { - dispatch({ - type: types.SUPPORT_TRANSACTION_FAILED, - data: { error: results }, - }); - dispatch(doOpenModal(modals.TRANSACTION_FAILED)); - } - }; - - const errorCallback = error => { - dispatch({ - type: types.SUPPORT_TRANSACTION_FAILED, - data: { error: error.message }, - }); - dispatch(doOpenModal(modals.TRANSACTION_FAILED)); - }; - - lbry - .wallet_send({ - claim_id: claim_id, - amount: amount, - }) - .then(successCallback, errorCallback); - }; -} diff --git a/ui/js/actions/content.js b/ui/js/actions/content.js index ed9324f77..eb62506a2 100644 --- a/ui/js/actions/content.js +++ b/ui/js/actions/content.js @@ -322,38 +322,29 @@ export function doPurchaseUri(uri) { const downloadingByOutpoint = selectDownloadingByOutpoint(state); const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint]; - - // we already fully downloaded the file. - if (fileInfo && fileInfo.completed) { - // If written_bytes is false that means the user has deleted/moved the - // file manually on their file system, so we need to dispatch a - // doLoadVideo action to reconstruct the file from the blobs - if (!fileInfo.written_bytes) dispatch(doLoadVideo(uri)); - - return Promise.resolve(); - } - - // we are already downloading the file - if (alreadyDownloading) { - return Promise.resolve(); - } - const costInfo = makeSelectCostInfoForUri(uri)(state); const { cost } = costInfo; - // the file is free or we have partially downloaded it - if (cost === 0 || (fileInfo && fileInfo.download_directory)) { - dispatch(doLoadVideo(uri)); - return Promise.resolve(); + if ( + alreadyDownloading || + (fileInfo && fileInfo.completed && fileInfo.written_bytes > 0) + ) { + return; + } + + // we already fully downloaded the file. + if ( + cost === 0 || + (fileInfo && (fileInfo.completed || fileInfo.download_directory)) + ) { + return dispatch(doLoadVideo(uri)); } if (cost > balance) { - dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS)); - } else { - dispatch(doOpenModal(modals.AFFIRM_PURCHASE, { uri })); + return dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS)); } - return Promise.resolve(); + return dispatch(doOpenModal(modals.AFFIRM_PURCHASE, { uri })); }; } @@ -364,7 +355,7 @@ export function doFetchClaimsByChannel(uri, page) { data: { uri, page }, }); - lbry.claim_list_by_channel({ uri, page }).then(result => { + lbry.claim_list_by_channel({ uri, page: page || 1 }).then(result => { const claimResult = result[uri], claims = claimResult ? claimResult.claims_in_channel : [], currentPage = claimResult ? claimResult.returned_page : undefined; @@ -420,6 +411,22 @@ export function doFetchClaimListMine() { }; } +export function doPlayUri(uri) { + return function(dispatch, getState) { + dispatch(doSetPlayingUri(uri)); + dispatch(doPurchaseUri(uri)); + }; +} + +export function doSetPlayingUri(uri) { + return function(dispatch, getState) { + dispatch({ + type: types.SET_PLAYING_URI, + data: { uri }, + }); + }; +} + export function doFetchChannelListMine() { return function(dispatch, getState) { dispatch({ diff --git a/ui/js/actions/cost_info.js b/ui/js/actions/cost_info.js index 6568a3575..001c4ae56 100644 --- a/ui/js/actions/cost_info.js +++ b/ui/js/actions/cost_info.js @@ -33,26 +33,53 @@ export function doFetchCostInfoForUri(uri) { }); } - if (isGenerous && claim) { - let cost; - 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") { - resolve({ cost: fee.amount, includesData: true }); - } else { - begin(); - lbryio.getExchangeRates().then(({ lbc_usd }) => { - resolve({ cost: fee.amount / lbc_usd, includesData: true }); - }); - } + /** + * "Generous" check below is disabled. We're no longer attempting to include or estimate data fees regardless of settings. + * + * This should be modified when lbry.stream_cost_estimate is reliable and performant. + */ + + /* + lbry.stream_cost_estimate({ uri }).then(cost => { + cacheAndResolve(cost); + }, reject); + */ + + 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") { + resolve({ cost: fee.amount, includesData: true }); } else { - begin(); - lbry.getCostInfo(uri).then(resolve); + // begin(); + lbryio.getExchangeRates().then(({ lbc_usd }) => { + resolve({ cost: fee.amount / lbc_usd, includesData: true }); + }); } + + // if (isGenerous && claim) { + // let cost; + // 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") { + // resolve({ cost: fee.amount, includesData: true }); + // } else { + // // begin(); + // lbryio.getExchangeRates().then(({ lbc_usd }) => { + // resolve({ cost: fee.amount / lbc_usd, includesData: true }); + // }); + // } + // } else { + // begin(); + // lbry.getCostInfo(uri).then(resolve); + // } }; } diff --git a/ui/js/actions/file_info.js b/ui/js/actions/file_info.js index eb317e484..49c949e5c 100644 --- a/ui/js/actions/file_info.js +++ b/ui/js/actions/file_info.js @@ -71,18 +71,18 @@ export function doFileList() { }; } -export function doOpenFileInShell(fileInfo) { +export function doOpenFileInShell(path) { return function(dispatch, getState) { - const success = shell.openItem(fileInfo.download_path); + const success = shell.openItem(path); if (!success) { - dispatch(doOpenFileInFolder(fileInfo)); + dispatch(doOpenFileInFolder(path)); } }; } -export function doOpenFileInFolder(fileInfo) { +export function doOpenFileInFolder(path) { return function(dispatch, getState) { - shell.showItemInFolder(fileInfo.download_path); + shell.showItemInFolder(path); }; } diff --git a/ui/js/actions/navigation.js b/ui/js/actions/navigation.js index b34a2e876..0ab3a22eb 100644 --- a/ui/js/actions/navigation.js +++ b/ui/js/actions/navigation.js @@ -1,5 +1,6 @@ import * as types from "constants/action_types"; import { + computePageFromPath, selectPageTitle, selectCurrentPage, selectCurrentParams, @@ -20,9 +21,17 @@ export function doNavigate(path, params = {}, options = {}) { url += "?" + toQueryString(params); } - dispatch(doChangePath(url, options)); + const state = getState(), + currentPage = selectCurrentPage(state), + nextPage = computePageFromPath(path), + scrollY = options.scrollY; - const pageTitle = selectPageTitle(getState()) + " - LBRY"; + if (currentPage != nextPage) { + //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); + } dispatch({ type: types.HISTORY_NAVIGATE, @@ -45,31 +54,6 @@ export function doAuthNavigate(pathAfterAuth = null, params = {}) { }; } -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; diff --git a/ui/js/actions/wallet.js b/ui/js/actions/wallet.js index 025cb1def..0e90266fb 100644 --- a/ui/js/actions/wallet.js +++ b/ui/js/actions/wallet.js @@ -6,6 +6,7 @@ import { selectBalance, } from "selectors/wallet"; import { doOpenModal, doShowSnackBar } from "actions/app"; +import { doNavigate } from "actions/navigation"; import * as modals from "constants/modal_types"; export function doUpdateBalance(balance) { @@ -143,3 +144,55 @@ export function doSetDraftTransactionAddress(address) { data: { address }, }; } + +export function doSendSupport(amount, claim_id, uri) { + return function(dispatch, getState) { + const state = getState(); + const balance = selectBalance(state); + + if (balance - amount <= 0) { + return dispatch(doOpenModal(modals.INSUFFICIENT_BALANCE)); + } + + dispatch({ + type: types.SUPPORT_TRANSACTION_STARTED, + }); + + const successCallback = results => { + if (results.txid) { + dispatch({ + type: types.SUPPORT_TRANSACTION_COMPLETED, + }); + dispatch( + doShowSnackBar({ + message: __(`You sent ${amount} LBC as support, Mahalo!`), + linkText: __("History"), + linkTarget: __("/wallet"), + }) + ); + dispatch(doNavigate("/show", { uri })); + } else { + dispatch({ + type: types.SUPPORT_TRANSACTION_FAILED, + data: { error: results.code }, + }); + dispatch(doOpenModal(modals.TRANSACTION_FAILED)); + } + }; + + const errorCallback = error => { + dispatch({ + type: types.SUPPORT_TRANSACTION_FAILED, + data: { error: error.code }, + }); + dispatch(doOpenModal(modals.TRANSACTION_FAILED)); + }; + + lbry + .wallet_send({ + claim_id: claim_id, + amount: amount, + }) + .then(successCallback, errorCallback); + }; +} diff --git a/ui/js/component/common.js b/ui/js/component/common.js index 8af8733c2..598e61a31 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -70,8 +70,7 @@ export class CreditAmount extends React.PureComponent { showFree: React.PropTypes.bool, showFullPrice: React.PropTypes.bool, showPlus: React.PropTypes.bool, - look: React.PropTypes.oneOf(["indicator", "plain"]), - fee: React.PropTypes.bool, + look: React.PropTypes.oneOf(["indicator", "plain", "fee"]), }; static defaultProps = { @@ -79,10 +78,8 @@ export class CreditAmount extends React.PureComponent { label: true, showFree: false, look: "indicator", - showFree: false, showFullPrice: false, showPlus: false, - fee: false, }; render() { @@ -119,10 +116,7 @@ export class CreditAmount extends React.PureComponent { return ( diff --git a/ui/js/component/dateTime/view.jsx b/ui/js/component/dateTime/view.jsx index 72d47c130..747286daa 100644 --- a/ui/js/component/dateTime/view.jsx +++ b/ui/js/component/dateTime/view.jsx @@ -1,6 +1,10 @@ import React from "react"; class DateTime extends React.PureComponent { + static SHOW_DATE = "date"; + static SHOW_TIME = "time"; + static SHOW_BOTH = "both"; + componentWillMount() { this.refreshDate(this.props); } @@ -17,9 +21,20 @@ class DateTime extends React.PureComponent { } render() { - const { date } = this.props; + const { date, formatOptions } = this.props; + const show = this.props.show || DateTime.SHOW_BOTH; - return {date && date.toLocaleString()}; + return ( + + {date && + (show == DateTime.SHOW_BOTH || show === DateTime.SHOW_DATE) && + date.toLocaleDateString()} + {show == DateTime.SHOW_BOTH && " "} + {date && + (show == DateTime.SHOW_BOTH || show === DateTime.SHOW_TIME) && + date.toLocaleTimeString()} + + ); } } diff --git a/ui/js/component/fileActions/index.js b/ui/js/component/fileActions/index.js index fea552312..68597a571 100644 --- a/ui/js/component/fileActions/index.js +++ b/ui/js/component/fileActions/index.js @@ -1,14 +1,9 @@ import React from "react"; import { connect } from "react-redux"; -import { selectPlatform } from "selectors/app"; import { makeSelectFileInfoForUri } from "selectors/file_info"; import { makeSelectCostInfoForUri } from "selectors/cost_info"; import { doOpenModal } from "actions/app"; -import { doFetchAvailability } from "actions/availability"; -import { doOpenFileInShell, doOpenFileInFolder } from "actions/file_info"; import { makeSelectClaimIsMine } from "selectors/claims"; -import { doPurchaseUri, doLoadVideo, doStartDownload } from "actions/content"; -import { doNavigate } from "actions/navigation"; import FileActions from "./view"; const select = (state, props) => ({ @@ -19,12 +14,7 @@ const select = (state, props) => ({ }); const perform = dispatch => ({ - checkAvailability: uri => dispatch(doFetchAvailability(uri)), - openInShell: fileInfo => dispatch(doOpenFileInShell(fileInfo)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)), - startDownload: uri => dispatch(doPurchaseUri(uri)), - restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)), - editClaim: fileInfo => dispatch(doNavigate("/publish", fileInfo)), }); export default connect(select, perform)(FileActions); diff --git a/ui/js/component/fileActions/view.jsx b/ui/js/component/fileActions/view.jsx index eed07ab44..ecb218228 100644 --- a/ui/js/component/fileActions/view.jsx +++ b/ui/js/component/fileActions/view.jsx @@ -4,19 +4,11 @@ import FileDownloadLink from "component/fileDownloadLink"; import * as modals from "constants/modal_types"; class FileActions extends React.PureComponent { - handleSupportButtonClicked() { - this.props.onTipShow(); - } - render() { - const { fileInfo, uri, openModal, claimIsMine, editClaim } = this.props; + const { fileInfo, uri, openModal, claimIsMine } = this.props; - const name = fileInfo ? fileInfo.name : null; - const channel = fileInfo ? fileInfo.channel_name : null; - - const metadata = fileInfo ? fileInfo.metadata : null, - showMenu = fileInfo && Object.keys(fileInfo).length > 0, - title = metadata ? metadata.title : uri; + const claimId = fileInfo ? fileInfo.claim_id : null, + showDelete = fileInfo && Object.keys(fileInfo).length > 0; return (
@@ -25,22 +17,25 @@ class FileActions extends React.PureComponent { button="text" icon="icon-edit" label={__("Edit")} - onClick={() => editClaim({ name, channel })} + navigate="/publish" + navigateParams={{ id: claimId }} />} - openModal(modals.CONFIRM_FILE_REMOVE, { uri })} + navigate="/show" + navigateParams={{ uri, tab: "tip" }} /> + {showDelete && + openModal(modals.CONFIRM_FILE_REMOVE, { uri })} + />}
); } diff --git a/ui/js/component/fileDetails/index.js b/ui/js/component/fileDetails/index.js new file mode 100644 index 000000000..738746d0a --- /dev/null +++ b/ui/js/component/fileDetails/index.js @@ -0,0 +1,23 @@ +import React from "react"; +import { connect } from "react-redux"; +import { + makeSelectClaimForUri, + makeSelectContentTypeForUri, + makeSelectMetadataForUri, +} from "selectors/claims"; +import FileDetails from "./view"; +import { doOpenFileInFolder } from "actions/file_info"; +import { makeSelectFileInfoForUri } from "selectors/file_info"; + +const select = (state, props) => ({ + claim: makeSelectClaimForUri(props.uri)(state), + contentType: makeSelectContentTypeForUri(props.uri)(state), + fileInfo: makeSelectFileInfoForUri(props.uri)(state), + metadata: makeSelectMetadataForUri(props.uri)(state), +}); + +const perform = dispatch => ({ + openFolder: path => dispatch(doOpenFileInFolder(path)), +}); + +export default connect(select, perform)(FileDetails); diff --git a/ui/js/component/fileDetails/view.jsx b/ui/js/component/fileDetails/view.jsx new file mode 100644 index 000000000..c2cfda468 --- /dev/null +++ b/ui/js/component/fileDetails/view.jsx @@ -0,0 +1,85 @@ +import React from "react"; +import ReactMarkdown from "react-markdown"; +import lbry from "lbry.js"; +import FileActions from "component/fileActions"; +import Link from "component/link"; +import DateTime from "component/dateTime"; + +const path = require("path"); + +class FileDetails extends React.PureComponent { + render() { + const { + claim, + contentType, + fileInfo, + metadata, + openFolder, + uri, + } = this.props; + + if (!claim || !metadata) { + return ( +
+ {__("Empty claim or metadata info.")} +
+ ); + } + + const { description, language, license } = metadata; + const { height } = claim; + const mediaType = lbry.getMediaType(contentType); + const directory = fileInfo && fileInfo.download_path + ? path.dirname(fileInfo.download_path) + : null; + + return ( +
+ +
+ +
+
+ + + + + + + + + + + + + + + + {directory && + + + + } + +
{__("Published on")}
{__("Content-Type")}{mediaType}
{__("Language")}{language}
{__("License")}{license}
{__("Downloaded to")} + openFolder(directory)}> + {directory} + +
+

+ +

+
+
+ ); + } +} + +export default FileDetails; diff --git a/ui/js/component/fileDownloadLink/index.js b/ui/js/component/fileDownloadLink/index.js index f9657f17b..2131b5f78 100644 --- a/ui/js/component/fileDownloadLink/index.js +++ b/ui/js/component/fileDownloadLink/index.js @@ -10,7 +10,6 @@ import { doFetchAvailability } from "actions/availability"; import { doOpenFileInShell } from "actions/file_info"; import { doPurchaseUri, doStartDownload } from "actions/content"; import FileDownloadLink from "./view"; -import * as modals from "constants/modal_types"; const select = (state, props) => ({ fileInfo: makeSelectFileInfoForUri(props.uri)(state), @@ -22,9 +21,8 @@ const select = (state, props) => ({ const perform = dispatch => ({ checkAvailability: uri => dispatch(doFetchAvailability(uri)), - openInShell: fileInfo => dispatch(doOpenFileInShell(fileInfo)), - startDownload: uri => - dispatch(doPurchaseUri(uri, modals.CONFIRM_FILE_PURCHASE)), + openInShell: path => dispatch(doOpenFileInShell(path)), + purchaseUri: uri => dispatch(doPurchaseUri(uri)), restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)), }); diff --git a/ui/js/component/fileDownloadLink/view.jsx b/ui/js/component/fileDownloadLink/view.jsx index 82bae5b8a..49c6c29ce 100644 --- a/ui/js/component/fileDownloadLink/view.jsx +++ b/ui/js/component/fileDownloadLink/view.jsx @@ -39,7 +39,7 @@ class FileDownloadLink extends React.PureComponent { downloading, uri, openInShell, - startDownload, + purchaseUri, costInfo, loading, } = this.props; @@ -81,7 +81,7 @@ class FileDownloadLink extends React.PureComponent { label={__("Download")} icon="icon-download" onClick={() => { - startDownload(uri); + purchaseUri(uri); }} /> ); @@ -92,7 +92,7 @@ class FileDownloadLink extends React.PureComponent { label={__("Open")} button="text" icon="icon-external-link-square" - onClick={() => openInShell(fileInfo)} + onClick={() => openInShell(fileInfo.download_path)} /> ); } diff --git a/ui/js/component/fileListSearch/view.jsx b/ui/js/component/fileListSearch/view.jsx index 682d07e7c..59884da8e 100644 --- a/ui/js/component/fileListSearch/view.jsx +++ b/ui/js/component/fileListSearch/view.jsx @@ -49,7 +49,17 @@ const FileListSearchResults = props => { class FileListSearch extends React.PureComponent { componentWillMount() { - this.props.search(this.props.query); + this.doSearch(this.props); + } + + componentWillReceiveProps(props) { + if (props.query != this.props.query) { + this.doSearch(props); + } + } + + doSearch(props) { + this.props.search(props.query); } render() { diff --git a/ui/js/component/form.js b/ui/js/component/form.js index 4b353eb94..7eaac87e4 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -1,5 +1,6 @@ import React from "react"; import FormField from "component/formField"; +import { Icon } from "component/common.js"; let formFieldCounter = 0; @@ -9,6 +10,29 @@ export function formFieldId() { return "form-field-" + ++formFieldCounter; } +export class Form extends React.PureComponent { + static propTypes = { + onSubmit: React.PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + } + + handleSubmit(event) { + event.preventDefault(); + this.props.onSubmit(); + } + + render() { + return ( +
this.handleSubmit(event)}> + {this.props.children} +
+ ); + } +} + export class FormRow extends React.PureComponent { static propTypes = { label: React.PropTypes.oneOfType([ @@ -131,3 +155,27 @@ export class FormRow extends React.PureComponent { ); } } + +export const Submit = props => { + const { title, label, icon, disabled } = props; + + const className = + "button-block" + + " button-primary" + + " button-set-item" + + " button--submit" + + (disabled ? " disabled" : ""); + + const content = ( + + {"icon" in props ? : null} + {label ? {label} : null} + + ); + + return ( + + ); +}; diff --git a/ui/js/component/inviteList/view.jsx b/ui/js/component/inviteList/view.jsx index 8d748116a..8e6e0c938 100644 --- a/ui/js/component/inviteList/view.jsx +++ b/ui/js/component/inviteList/view.jsx @@ -47,9 +47,9 @@ class InviteList extends React.PureComponent { {invitee.invite_reward_claimed ? - : invitee.invite_accepted + : invitee.invite_reward_claimable ? : diff --git a/ui/js/component/inviteNew/view.jsx b/ui/js/component/inviteNew/view.jsx index 3f37e9c1b..af15a5fd2 100644 --- a/ui/js/component/inviteNew/view.jsx +++ b/ui/js/component/inviteNew/view.jsx @@ -1,7 +1,6 @@ import React from "react"; import { BusyMessage, CreditAmount } from "component/common"; -import Link from "component/link"; -import { FormRow } from "component/form.js"; +import { Form, FormRow, Submit } from "component/form.js"; class FormInviteNew extends React.PureComponent { constructor(props) { @@ -18,16 +17,16 @@ class FormInviteNew extends React.PureComponent { }); } - handleSubmit(event) { - event.preventDefault(); - this.props.inviteNew(this.state.email); + handleSubmit() { + const { email } = this.state; + this.props.inviteNew(email); } render() { const { errorMessage, isPending } = this.props; return ( -
+
- { - this.handleSubmit(event); - }} - /> +
- + ); } } diff --git a/ui/js/component/link/index.js b/ui/js/component/link/index.js index 5847c97d9..37340987a 100644 --- a/ui/js/component/link/index.js +++ b/ui/js/component/link/index.js @@ -4,7 +4,7 @@ import { doNavigate } from "actions/navigation"; import Link from "./view"; const perform = dispatch => ({ - doNavigate: path => dispatch(doNavigate(path)), + doNavigate: (path, params) => dispatch(doNavigate(path, params)), }); export default connect(null, perform)(Link); diff --git a/ui/js/component/link/view.jsx b/ui/js/component/link/view.jsx index a30d1a981..1ba8f4b52 100644 --- a/ui/js/component/link/view.jsx +++ b/ui/js/component/link/view.jsx @@ -12,6 +12,7 @@ const Link = props => { disabled, children, navigate, + navigateParams, doNavigate, } = props; @@ -23,7 +24,7 @@ const Link = props => { const onClick = !props.onClick && navigate ? () => { - doNavigate(navigate); + doNavigate(navigate, navigateParams || {}); } : props.onClick; diff --git a/ui/js/component/linkTransaction/index.js b/ui/js/component/linkTransaction/index.js index 601927420..9983f1bfc 100644 --- a/ui/js/component/linkTransaction/index.js +++ b/ui/js/component/linkTransaction/index.js @@ -1,5 +1,5 @@ import React from "react"; import { connect } from "react-redux"; -import Link from "./view"; +import LinkTransaction from "./view"; -export default connect(null, null)(Link); +export default connect(null, null)(LinkTransaction); diff --git a/ui/js/component/publishForm/view.jsx b/ui/js/component/publishForm/view.jsx index aa05e4e43..b502f4add 100644 --- a/ui/js/component/publishForm/view.jsx +++ b/ui/js/component/publishForm/view.jsx @@ -2,7 +2,7 @@ import React from "react"; import lbry from "lbry"; import lbryuri from "lbryuri"; import FormField from "component/formField"; -import { FormRow } from "component/form.js"; +import { Form, FormRow, Submit } from "component/form.js"; import Link from "component/link"; import FormFieldPrice from "component/formFieldPrice"; import Modal from "modal/modal"; @@ -19,6 +19,7 @@ class PublishForm extends React.PureComponent { this._defaultPaidPrice = 0.01; this.state = { + id: null, rawName: "", name: "", bid: 10, @@ -58,11 +59,7 @@ class PublishForm extends React.PureComponent { if (!fetchingChannels) fetchChannelListMine(); } - handleSubmit(event) { - if (typeof event !== "undefined") { - event.preventDefault(); - } - + handleSubmit() { this.setState({ submitting: true, }); @@ -189,10 +186,10 @@ class PublishForm extends React.PureComponent { } handleEditClaim() { - const isMine = this.myClaimExists(); + const claimInfo = this.claim() || this.myClaimInfo(); - if (isMine) { - this.handlePrefillClicked(); + if (claimInfo) { + this.handlePrefillClaim(claimInfo); } } @@ -212,10 +209,10 @@ class PublishForm extends React.PureComponent { } myClaimInfo() { - const { name } = this.state; + const { id } = this.state; return Object.values(this.props.myClaims).find( - claim => claim.name === name + claim => claim.claim_id === id ); } @@ -272,9 +269,10 @@ class PublishForm extends React.PureComponent { }); } - handlePrefillClicked() { - const claimInfo = this.myClaimInfo(); - const { source } = claimInfo.value.stream; + handlePrefillClaim(claimInfo) { + const { claim_id, name, channel_name, amount } = claimInfo; + const { source, metadata } = claimInfo.value.stream; + const { license, licenseUrl, @@ -283,17 +281,21 @@ class PublishForm extends React.PureComponent { description, language, nsfw, - } = claimInfo.value.stream.metadata; + } = metadata; let newState = { - mode: "edit", + id: claim_id, + channel: channel_name || "anonymous", + bid: amount, meta_title: title, meta_thumbnail: thumbnail, meta_description: description, meta_language: language, meta_nsfw: nsfw, + mode: "edit", prefillDone: true, - bid: claimInfo.amount, + rawName: name, + name, source, }; @@ -423,16 +425,11 @@ class PublishForm extends React.PureComponent { } componentWillMount() { - let { name, channel } = this.props.params; - - channel = channel || this.state.channel; - this.props.fetchClaimListMine(); this._updateChannelList(); - if (name) { - this.setState({ name, rawName: name, channel }); - } + const { id } = this.props.params; + this.setState({ id }); } componentDidMount() { @@ -461,37 +458,38 @@ class PublishForm extends React.PureComponent { } getNameBidHelpText() { - if (this.state.prefillDone) { + const { prefillDone, name, uri } = this.state; + const { resolvingUris } = this.props; + const claim = this.claim(); + + if (prefillDone) { return __("Existing claim data was prefilled"); } - if ( - this.state.uri && - this.props.resolvingUris.indexOf(this.state.uri) !== -1 && - this.claim() === undefined - ) { + if (uri && resolvingUris.indexOf(uri) !== -1 && claim === undefined) { return __("Checking..."); - } else if (!this.state.name) { + } else if (!name) { return __("Select a URL for this publish."); - } else if (!this.claim()) { + } else if (!claim) { return __("This URL is unused."); - } else if (this.myClaimExists() && !this.state.prefillDone) { + } else if (this.myClaimExists() && !prefillDone) { return ( {__("You already have a claim with this name.")}{" "} this.handlePrefillClicked()} + onClick={() => this.handleEditClaim()} /> ); - } else if (this.claim()) { - if (this.topClaimValue() === 1) { + } else if (claim) { + const topClaimValue = this.topClaimValue(); + if (topClaimValue === 1) { return ( {__( 'A deposit of at least one credit is required to win "%s". However, you can still get a permanent URL for any amount.', - this.state.name + name )} ); @@ -500,8 +498,8 @@ class PublishForm extends React.PureComponent { {__( 'A deposit of at least "%s" credits is required to win "%s". However, you can still get a permanent URL for any amount.', - this.topClaimValue(), - this.state.name + topClaimValue, + name )} ); @@ -532,11 +530,7 @@ class PublishForm extends React.PureComponent { return (
-
{ - this.handleSubmit(event); - }} - > +

{__("Content")}

@@ -870,12 +864,10 @@ class PublishForm extends React.PureComponent {
- { - this.handleSubmit(event); - }} + -
-
+ ({}); - -const perform = dispatch => ({ - sendSupport: (amount, claim_id) => dispatch(doSendSupport(amount, claim_id)), -}); - -export default connect(select, perform)(TipLink); diff --git a/ui/js/component/tipLink/view.jsx b/ui/js/component/tipLink/view.jsx deleted file mode 100644 index 831806d48..000000000 --- a/ui/js/component/tipLink/view.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from "react"; -import Link from "component/link"; -import { FormRow } from "component/form"; - -class TipLink extends React.PureComponent { - constructor(props) { - super(props); - - this.state = { - tipAmount: 1.0, - }; - } - - handleSendButtonClicked() { - let claim_id = this.props.claim_id; - let amount = this.state.tipAmount; - this.props.sendSupport(amount, claim_id); - this.props.onTipHide(); - } - - handleSupportCancelButtonClicked() { - this.props.onTipHide(); - } - - handleSupportPriceChange(event) { - this.setState({ - tipAmount: Number(event.target.value), - }); - } - - render() { - return ( -
-
-

{__("Support")}

-
-
- {__( - "Support the creator and the success of their content by sending a tip. " - )} - -
-
- this.handleSupportPriceChange(event)} - /> -
-
- - -
-
- ); - } -} - -export default TipLink; diff --git a/ui/js/component/transactionList/index.js b/ui/js/component/transactionList/index.js index 0b4d0e1af..5a6c10b6b 100644 --- a/ui/js/component/transactionList/index.js +++ b/ui/js/component/transactionList/index.js @@ -1,8 +1,13 @@ import React from "react"; import { connect } from "react-redux"; import { doNavigate } from "actions/navigation"; +import { selectClaimedRewardsByTransactionId } from "selectors/rewards"; import TransactionList from "./view"; +const select = state => ({ + rewards: selectClaimedRewardsByTransactionId(state), +}); + const perform = dispatch => ({ navigate: (path, params) => dispatch(doNavigate(path, params)), }); diff --git a/ui/js/component/transactionList/internal/TransactionListBody.jsx b/ui/js/component/transactionList/internal/TransactionListBody.jsx deleted file mode 100644 index 12beb2e8d..000000000 --- a/ui/js/component/transactionList/internal/TransactionListBody.jsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from "react"; -import LinkTransaction from "component/linkTransaction"; -import { CreditAmount } from "component/common"; - -class TransactionTableBody extends React.PureComponent { - constructor(props) { - super(props); - } - - getClaimLink(claim_name, claim_id) { - let uri = `lbry://${claim_name}#${claim_id}`; - - return ( -
this.props.navigate(uri)}> - {claim_name} - - ); - } - - filterList(transaction) { - if (this.props.filter == "claim") { - return transaction.claim_info.length > 0; - } else if (this.props.filter == "support") { - return transaction.support_info.length > 0; - } else if (this.props.filter == "update") { - return transaction.update_info.length > 0; - } else { - return transaction; - } - } - - renderBody(transaction) { - const txid = transaction.id; - const date = transaction.date; - const fee = transaction.fee; - const filter = this.props.filter; - const options = { - weekday: "short", - year: "2-digit", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }; - - if (filter == "tipSupport") - transaction["tipSupport_info"] = transaction["support_info"].filter( - tx => tx.is_tip - ); - - return filter != "unfiltered" - ? transaction[`${filter}_info`].map(item => { - return ( - - - {date - ? date.toLocaleDateString("en-US", options) - : - {__("(Transaction pending)")} - } - - - -
- - - - {this.getClaimLink(item.claim_name, item.claim_id)} - - - - - - ); - }) - : - - {date - ? date.toLocaleDateString("en-US", options) - : - {__("(Transaction pending)")} - } - - - -
- - - - - - ; - } - - removeFeeTx(transaction) { - if (this.props.filter == "unfiltered") - return Math.abs(transaction.amount) != Math.abs(transaction.fee); - else return true; - } - - render() { - const { transactions, filter } = this.props; - - return ( - - {transactions - .filter(this.filterList, this) - .filter(this.removeFeeTx, this) - .map(this.renderBody, this)} - - ); - } -} - -export default TransactionTableBody; diff --git a/ui/js/component/transactionList/internal/TransactionListHeader.jsx b/ui/js/component/transactionList/internal/TransactionListHeader.jsx deleted file mode 100644 index 52f735fb9..000000000 --- a/ui/js/component/transactionList/internal/TransactionListHeader.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -class TransactionTableHeader extends React.PureComponent { - render() { - const { filter } = this.props; - return ( - - - {__("Date")} - {__("Amount(Fee)")} - {filter != "unfiltered" && {__("Claim Name")} } - {__("Transaction")} - - - ); - } -} - -export default TransactionTableHeader; diff --git a/ui/js/component/transactionList/internal/TransactionListItem.jsx b/ui/js/component/transactionList/internal/TransactionListItem.jsx new file mode 100644 index 000000000..2a4ba28ae --- /dev/null +++ b/ui/js/component/transactionList/internal/TransactionListItem.jsx @@ -0,0 +1,78 @@ +import React from "react"; +import LinkTransaction from "component/linkTransaction"; +import { CreditAmount } from "component/common"; +import DateTime from "component/dateTime"; +import Link from "component/link"; +import lbryuri from "lbryuri"; + +class TransactionListItem extends React.PureComponent { + render() { + const { reward, transaction } = this.props; + const { + amount, + claim_id: claimId, + claim_name: name, + date, + fee, + txid, + type, + } = transaction; + + return ( + + + {date + ?
+ +
+ +
+
+ : + {__("(Transaction pending)")} + } + + + +
+ {fee != 0 && + } + + + {type} + + + {reward && + + {__("Reward: %s", reward.reward_title)} + } + {name && + claimId && + + {name} + } + + + + + + ); + } +} + +export default TransactionListItem; diff --git a/ui/js/component/transactionList/view.jsx b/ui/js/component/transactionList/view.jsx index ee78e2acc..beb2f8514 100644 --- a/ui/js/component/transactionList/view.jsx +++ b/ui/js/component/transactionList/view.jsx @@ -1,6 +1,5 @@ import React from "react"; -import TransactionTableHeader from "./internal/TransactionListHeader"; -import TransactionTableBody from "./internal/TransactionListBody"; +import TransactionListItem from "./internal/TransactionListItem"; import FormField from "component/formField"; class TransactionList extends React.PureComponent { @@ -8,7 +7,7 @@ class TransactionList extends React.PureComponent { super(props); this.state = { - filter: "unfiltered", + filter: null, }; } @@ -18,45 +17,63 @@ class TransactionList extends React.PureComponent { }); } - handleClaimNameClicked(uri) { - this.props.navigate("/show", { uri }); + filterTransaction(transaction) { + const { filter } = this.state; + + return !filter || filter == transaction.type; } render() { - const { emptyMessage, transactions } = this.props; - const { filter } = this.state; + const { emptyMessage, rewards, transactions } = this.props; - if (!transactions || !transactions.length) { - return ( -
- {emptyMessage || __("No transactions to list.")} -
- ); - } + let transactionList = transactions.filter( + this.filterTransaction.bind(this) + ); return (
- - {__("Filter")} {" "} - - - - - - - - - - - -
+ {(transactionList.length || this.state.filter) && + + {__("Filter")} {" "} + + + + + + + + + + + } + {!transactionList.length && +
+ {emptyMessage || __("No transactions to list.")} +
} + {Boolean(transactionList.length) && + + + + + + + + + + + + {transactionList.map(t => + + )} + +
{__("Date")}{__("Amount (Fee)")}{__("Type")} {__("Details")} {__("Transaction")}
}
); } diff --git a/ui/js/component/uriIndicator/view.jsx b/ui/js/component/uriIndicator/view.jsx index b69abd591..a6dcb1a50 100644 --- a/ui/js/component/uriIndicator/view.jsx +++ b/ui/js/component/uriIndicator/view.jsx @@ -1,5 +1,7 @@ import React from "react"; import { Icon } from "component/common"; +import Link from "component/link"; +import lbryuri from "lbryuri.js"; class UriIndicator extends React.PureComponent { componentWillMount() { @@ -19,7 +21,7 @@ class UriIndicator extends React.PureComponent { } render() { - const { claim, uri, isResolvingUri } = this.props; + const { claim, link, uri, isResolvingUri } = this.props; if (isResolvingUri && !claim) { return Validating...; @@ -33,21 +35,30 @@ class UriIndicator extends React.PureComponent { channel_name: channelName, has_signature: hasSignature, signature_is_valid: signatureIsValid, + value, } = claim; + const channelClaimId = + value && + value.publisherSignature && + value.publisherSignature.certificateId; if (!hasSignature || !channelName) { return Anonymous; } - let icon, modifier; + let icon, channelLink, modifier; + if (signatureIsValid) { modifier = "valid"; + channelLink = link + ? lbryuri.build({ channelName, claimId: channelClaimId }, false) + : false; } else { icon = "icon-times-circle"; modifier = "invalid"; } - return ( + const inner = ( {channelName} {" "} {!signatureIsValid @@ -58,6 +69,16 @@ class UriIndicator extends React.PureComponent { : ""} ); + + if (!channelLink) { + return inner; + } + + return ( + + {inner} + + ); } } diff --git a/ui/js/component/userEmailNew/view.jsx b/ui/js/component/userEmailNew/view.jsx index 70ed0f45e..5b34e62e6 100644 --- a/ui/js/component/userEmailNew/view.jsx +++ b/ui/js/component/userEmailNew/view.jsx @@ -1,6 +1,6 @@ import React from "react"; import Link from "component/link"; -import { FormRow } from "component/form.js"; +import { Form, FormRow, Submit } from "component/form.js"; class UserEmailNew extends React.PureComponent { constructor(props) { @@ -17,20 +17,16 @@ class UserEmailNew extends React.PureComponent { }); } - handleSubmit(event) { - event.preventDefault(); - this.props.addUserEmail(this.state.email); + handleSubmit() { + const { email } = this.state; + this.props.addUserEmail(email); } render() { const { errorMessage, isPending } = this.props; return ( -
{ - this.handleSubmit(event); - }} - > +

{__( "This process is required to prevent abuse of the rewards program." @@ -53,16 +49,9 @@ class UserEmailNew extends React.PureComponent { }} />

- { - this.handleSubmit(event); - }} - /> +
-
+ ); } } diff --git a/ui/js/component/userEmailVerify/view.jsx b/ui/js/component/userEmailVerify/view.jsx index 47ed10bc9..1ec92202e 100644 --- a/ui/js/component/userEmailVerify/view.jsx +++ b/ui/js/component/userEmailVerify/view.jsx @@ -1,6 +1,6 @@ import React from "react"; import Link from "component/link"; -import { FormRow } from "component/form.js"; +import { Form, FormRow, Submit } from "component/form.js"; class UserEmailVerify extends React.PureComponent { constructor(props) { @@ -17,19 +17,15 @@ class UserEmailVerify extends React.PureComponent { }); } - handleSubmit(event) { - event.preventDefault(); - this.props.verifyUserEmail(this.state.code); + handleSubmit() { + const { code } = this.state; + this.props.verifyUserEmail(code); } render() { const { errorMessage, isPending } = this.props; return ( -
{ - this.handleSubmit(event); - }} - > +

{__("Please enter the verification code emailed to you.")}

- { - this.handleSubmit(event); - }} - /> +
- + ); } } diff --git a/ui/js/component/video/index.js b/ui/js/component/video/index.js index 36eeb3168..3c80869dc 100644 --- a/ui/js/component/video/index.js +++ b/ui/js/component/video/index.js @@ -1,9 +1,8 @@ import React from "react"; import { connect } from "react-redux"; -import { doCloseModal } from "actions/app"; import { doChangeVolume } from "actions/app"; -import { selectCurrentModal, selectVolume } from "selectors/app"; -import { doPurchaseUri, doLoadVideo } from "actions/content"; +import { selectVolume } from "selectors/app"; +import { doPlayUri, doSetPlayingUri } from "actions/content"; import { makeSelectMetadataForUri, makeSelectContentTypeForUri, @@ -16,6 +15,7 @@ import { import { makeSelectCostInfoForUri } from "selectors/cost_info"; import { selectShowNsfw } from "selectors/settings"; import Video from "./view"; +import { selectPlayingUri } from "selectors/content"; const select = (state, props) => ({ costInfo: makeSelectCostInfoForUri(props.uri)(state), @@ -24,13 +24,14 @@ const select = (state, props) => ({ obscureNsfw: !selectShowNsfw(state), isLoading: makeSelectLoadingForUri(props.uri)(state), isDownloading: makeSelectDownloadingForUri(props.uri)(state), + playingUri: selectPlayingUri(state), contentType: makeSelectContentTypeForUri(props.uri)(state), volume: selectVolume(state), }); const perform = dispatch => ({ - loadVideo: uri => dispatch(doLoadVideo(uri)), - purchaseUri: uri => dispatch(doPurchaseUri(uri)), + play: uri => dispatch(doPlayUri(uri)), + cancelPlay: () => dispatch(doSetPlayingUri(null)), changeVolume: volume => dispatch(doChangeVolume(volume)), }); diff --git a/ui/js/component/video/internal/play-button.jsx b/ui/js/component/video/internal/play-button.jsx index 23f9259aa..73905f298 100644 --- a/ui/js/component/video/internal/play-button.jsx +++ b/ui/js/component/video/internal/play-button.jsx @@ -17,18 +17,12 @@ class VideoPlayButton extends React.PureComponent { "Space" === event.code ) { event.preventDefault(); - this.onWatchClick(); + this.watch(); } } - onWatchClick() { - this.props.purchaseUri(this.props.uri).then(() => { - if (!this.props.modal) { - this.props.startPlaying(); - } else { - alert("fix me set pending play"); - } - }); + watch() { + this.props.play(this.props.uri); } render() { @@ -54,7 +48,7 @@ class VideoPlayButton extends React.PureComponent { label={label ? label : ""} className="video__play-button" icon={icon} - onClick={this.onWatchClick.bind(this)} + onClick={() => this.watch()} /> ); } diff --git a/ui/js/component/video/view.jsx b/ui/js/component/video/view.jsx index 4e396ddd4..fb1bd3420 100644 --- a/ui/js/component/video/view.jsx +++ b/ui/js/component/video/view.jsx @@ -9,20 +9,12 @@ class Video extends React.PureComponent { constructor(props) { super(props); this.state = { - isPlaying: false, showNsfwHelp: false, }; } - componentWillReceiveProps(nextProps) { - // reset playing state upon change path action - if ( - !this.isMediaSame(nextProps) && - this.props.fileInfo && - this.state.isPlaying - ) { - this.state.isPlaying = false; - } + componentWillUnmount() { + this.props.cancelPlay(); } isMediaSame(nextProps) { @@ -33,12 +25,6 @@ class Video extends React.PureComponent { ); } - startPlaying() { - this.setState({ - isPlaying: true, - }); - } - handleMouseOver() { if ( this.props.obscureNsfw && @@ -64,13 +50,15 @@ class Video extends React.PureComponent { metadata, isLoading, isDownloading, + playingUri, fileInfo, contentType, changeVolume, volume, + uri, } = this.props; - const { isPlaying = false } = this.state; + const isPlaying = playingUri === uri; const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; const mediaType = lbry.getMediaType( @@ -129,11 +117,7 @@ class Video extends React.PureComponent { className="video__cover" style={{ backgroundImage: 'url("' + metadata.thumbnail + '")' }} > - + } {this.state.showNsfwHelp && } diff --git a/ui/js/component/walletSend/view.jsx b/ui/js/component/walletSend/view.jsx index adaf9ad84..00beb2a86 100644 --- a/ui/js/component/walletSend/view.jsx +++ b/ui/js/component/walletSend/view.jsx @@ -1,54 +1,69 @@ import React from "react"; -import Link from "component/link"; -import { FormRow } from "component/form"; +import { Form, FormRow, Submit } from "component/form"; import lbryuri from "lbryuri"; -const WalletSend = props => { - const { sendToAddress, setAmount, setAddress, amount, address } = props; +class WalletSend extends React.PureComponent { + handleSubmit() { + const { amount, address, sendToAddress } = this.props; + const validSubmit = parseFloat(amount) > 0.0 && address; - return ( -
-
-
-

{__("Send Credits")}

-
-
- -
-
- -
- 0.0) || !address} - /> - + if (validSubmit) { + sendToAddress(); + } + } + + render() { + const { + closeModal, + modal, + setAmount, + setAddress, + amount, + address, + error, + } = this.props; + + return ( +
+ +
+

{__("Send Credits")}

-
- -
- ); -}; +
+ +
+
+ +
+ 0.0) || !address} + /> +
+
+ + + ); + } +} export default WalletSend; diff --git a/ui/js/component/walletSendTip/index.js b/ui/js/component/walletSendTip/index.js new file mode 100644 index 000000000..6d3275321 --- /dev/null +++ b/ui/js/component/walletSendTip/index.js @@ -0,0 +1,18 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doSendSupport } from "actions/wallet"; +import WalletSendTip from "./view"; +import { makeSelectTitleForUri } from "selectors/claims"; +import { selectIsSendingSupport } from "selectors/wallet"; + +const select = (state, props) => ({ + isPending: selectIsSendingSupport(state), + title: makeSelectTitleForUri(props.uri)(state), +}); + +const perform = dispatch => ({ + sendSupport: (amount, claim_id, uri) => + dispatch(doSendSupport(amount, claim_id, uri)), +}); + +export default connect(select, perform)(WalletSendTip); diff --git a/ui/js/component/walletSendTip/view.jsx b/ui/js/component/walletSendTip/view.jsx new file mode 100644 index 000000000..9b8ce0338 --- /dev/null +++ b/ui/js/component/walletSendTip/view.jsx @@ -0,0 +1,79 @@ +import React from "react"; +import Link from "component/link"; +import { FormRow } from "component/form"; +import UriIndicator from "component/uriIndicator"; + +class WalletSendTip extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + amount: 0.0, + }; + } + + handleSendButtonClicked() { + const { claim_id, uri } = this.props; + let amount = this.state.amount; + this.props.sendSupport(amount, claim_id, uri); + } + + handleSupportPriceChange(event) { + this.setState({ + amount: Number(event.target.value), + }); + } + + render() { + const { errorMessage, isPending, title, uri } = this.props; + + return ( +
+
+

{__("Support")}

+
+
+ + {__( + 'This will appear as a tip for "%s" located at %s.', + title, + uri + ) + " "} + + + } + placeholder="1.00" + onChange={event => this.handleSupportPriceChange(event)} + /> +
+ + +
+
+
+ ); + } +} + +export default WalletSendTip; diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 5281e6b97..140509461 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -9,7 +9,6 @@ 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"; @@ -40,8 +39,11 @@ export const SEND_TRANSACTION_STARTED = "SEND_TRANSACTION_STARTED"; export const SEND_TRANSACTION_COMPLETED = "SEND_TRANSACTION_COMPLETED"; export const SEND_TRANSACTION_FAILED = "SEND_TRANSACTION_FAILED"; export const FETCH_BLOCK_SUCCESS = "FETCH_BLOCK_SUCCESS"; +export const SUPPORT_TRANSACTION_STARTED = "SUPPORT_TRANSACTION_STARTED"; +export const SUPPORT_TRANSACTION_COMPLETED = "SUPPORT_TRANSACTION_COMPLETED"; +export const SUPPORT_TRANSACTION_FAILED = "SUPPORT_TRANSACTION_FAILED"; -// Content +// Claims export const FETCH_FEATURED_CONTENT_STARTED = "FETCH_FEATURED_CONTENT_STARTED"; export const FETCH_FEATURED_CONTENT_COMPLETED = "FETCH_FEATURED_CONTENT_COMPLETED"; @@ -57,6 +59,20 @@ export const FETCH_CHANNEL_CLAIM_COUNT_COMPLETED = export const FETCH_CLAIM_LIST_MINE_STARTED = "FETCH_CLAIM_LIST_MINE_STARTED"; export const FETCH_CLAIM_LIST_MINE_COMPLETED = "FETCH_CLAIM_LIST_MINE_COMPLETED"; +export const ABANDON_CLAIM_STARTED = "ABANDON_CLAIM_STARTED"; +export const ABANDON_CLAIM_SUCCEEDED = "ABANDON_CLAIM_SUCCEEDED"; +export const FETCH_CHANNEL_LIST_MINE_STARTED = + "FETCH_CHANNEL_LIST_MINE_STARTED"; +export const FETCH_CHANNEL_LIST_MINE_COMPLETED = + "FETCH_CHANNEL_LIST_MINE_COMPLETED"; +export const CREATE_CHANNEL_STARTED = "CREATE_CHANNEL_STARTED"; +export const CREATE_CHANNEL_COMPLETED = "CREATE_CHANNEL_COMPLETED"; +export const PUBLISH_STARTED = "PUBLISH_STARTED"; +export const PUBLISH_COMPLETED = "PUBLISH_COMPLETED"; +export const PUBLISH_FAILED = "PUBLISH_FAILED"; +export const SET_PLAYING_URI = "PLAY_URI"; + +// Files export const FILE_LIST_STARTED = "FILE_LIST_STARTED"; export const FILE_LIST_SUCCEEDED = "FILE_LIST_SUCCEEDED"; export const FETCH_FILE_INFO_STARTED = "FETCH_FILE_INFO_STARTED"; @@ -73,17 +89,6 @@ export const PLAY_VIDEO_STARTED = "PLAY_VIDEO_STARTED"; export const FETCH_AVAILABILITY_STARTED = "FETCH_AVAILABILITY_STARTED"; export const FETCH_AVAILABILITY_COMPLETED = "FETCH_AVAILABILITY_COMPLETED"; export const FILE_DELETE = "FILE_DELETE"; -export const ABANDON_CLAIM_STARTED = "ABANDON_CLAIM_STARTED"; -export const ABANDON_CLAIM_SUCCEEDED = "ABANDON_CLAIM_SUCCEEDED"; -export const FETCH_CHANNEL_LIST_MINE_STARTED = - "FETCH_CHANNEL_LIST_MINE_STARTED"; -export const FETCH_CHANNEL_LIST_MINE_COMPLETED = - "FETCH_CHANNEL_LIST_MINE_COMPLETED"; -export const CREATE_CHANNEL_STARTED = "CREATE_CHANNEL_STARTED"; -export const CREATE_CHANNEL_COMPLETED = "CREATE_CHANNEL_COMPLETED"; -export const PUBLISH_STARTED = "PUBLISH_STARTED"; -export const PUBLISH_COMPLETED = "PUBLISH_COMPLETED"; -export const PUBLISH_FAILED = "PUBLISH_FAILED"; // Search export const SEARCH_STARTED = "SEARCH_STARTED"; @@ -132,11 +137,6 @@ export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE"; export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR"; export const FETCH_REWARD_CONTENT_COMPLETED = "FETCH_REWARD_CONTENT_COMPLETED"; -// Supports -export const SUPPORT_TRANSACTION_STARTED = "SUPPORT_TRANSACTION_STARTED"; -export const SUPPORT_TRANSACTION_COMPLETED = "SUPPORT_TRANSACTION_COMPLETED"; -export const SUPPORT_TRANSACTION_FAILED = "SUPPORT_TRANSACTION_FAILED"; - //Language export const DOWNLOAD_LANGUAGE_SUCCEEDED = "DOWNLOAD_LANGUAGE_SUCCEEDED"; export const DOWNLOAD_LANGUAGE_FAILED = "DOWNLOAD_LANGUAGE_FAILED"; diff --git a/ui/js/lbry.js b/ui/js/lbry.js index a0b270647..767d4479c 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -176,60 +176,6 @@ lbry.connect = function() { return lbry._connectPromise; }; -/** - * Takes a LBRY URI; will first try and calculate a total cost using - * Lighthouse. If Lighthouse can't be reached, it just retrives the - * key fee. - * - * Returns an object with members: - * - cost: Number; the calculated cost of the name - * - includes_data: Boolean; indicates whether or not the data fee info - * from Lighthouse is included. - */ -lbry.costPromiseCache = {}; -lbry.getCostInfo = function(uri) { - if (lbry.costPromiseCache[uri] === undefined) { - lbry.costPromiseCache[uri] = new Promise((resolve, reject) => { - const COST_INFO_CACHE_KEY = "cost_info_cache"; - let costInfoCache = getSession(COST_INFO_CACHE_KEY, {}); - - function cacheAndResolve(cost, includesData) { - costInfoCache[uri] = { cost, includesData }; - setSession(COST_INFO_CACHE_KEY, costInfoCache); - resolve({ cost, includesData }); - } - - if (!uri) { - return reject(new Error(`URI required.`)); - } - - if (costInfoCache[uri] && costInfoCache[uri].cost) { - return resolve(costInfoCache[uri]); - } - - function getCost(uri, size) { - lbry - .stream_cost_estimate({ uri, ...(size !== null ? { size } : {}) }) - .then(cost => { - cacheAndResolve(cost, size !== null); - }, reject); - } - - const uriObj = lbryuri.parse(uri); - const name = uriObj.path || uriObj.name; - - lighthouse.get_size_for_name(name).then(size => { - if (size) { - getCost(name, size); - } else { - getCost(name, null); - } - }); - }); - } - return lbry.costPromiseCache[uri]; -}; - /** * Publishes a file. The optional fileListedCallback is called when the file becomes available in * lbry.file_list() during the publish process. diff --git a/ui/js/modal/modalAffirmPurchase/index.js b/ui/js/modal/modalAffirmPurchase/index.js index c42afc373..d0f4affa2 100644 --- a/ui/js/modal/modalAffirmPurchase/index.js +++ b/ui/js/modal/modalAffirmPurchase/index.js @@ -1,7 +1,7 @@ import React from "react"; import { connect } from "react-redux"; import { doCloseModal } from "actions/app"; -import { doLoadVideo } from "actions/content"; +import { doLoadVideo, doSetPlayingUri } from "actions/content"; import { makeSelectMetadataForUri } from "selectors/claims"; import ModalAffirmPurchase from "./view"; @@ -10,6 +10,10 @@ const select = (state, props) => ({ }); const perform = dispatch => ({ + cancelPurchase: () => { + dispatch(doSetPlayingUri(null)); + dispatch(doCloseModal()); + }, closeModal: () => dispatch(doCloseModal()), loadVideo: uri => dispatch(doLoadVideo(uri)), }); diff --git a/ui/js/modal/modalAffirmPurchase/view.jsx b/ui/js/modal/modalAffirmPurchase/view.jsx index a38e82f1c..96dd6c1c9 100644 --- a/ui/js/modal/modalAffirmPurchase/view.jsx +++ b/ui/js/modal/modalAffirmPurchase/view.jsx @@ -9,7 +9,7 @@ class ModalAffirmPurchase extends React.PureComponent { } render() { - const { closeModal, metadata: { title }, uri } = this.props; + const { cancelPurchase, metadata: { title }, uri } = this.props; return ( {__("This will purchase")} {title} {__("for")}{" "} diff --git a/ui/js/modal/modalRemoveFile/index.js b/ui/js/modal/modalRemoveFile/index.js index 8c00f383e..39ae89ceb 100644 --- a/ui/js/modal/modalRemoveFile/index.js +++ b/ui/js/modal/modalRemoveFile/index.js @@ -2,16 +2,13 @@ import React from "react"; import { connect } from "react-redux"; import { doCloseModal } from "actions/app"; import { doDeleteFileAndGoBack } from "actions/file_info"; -import { - makeSelectMetadataForUri, - makeSelectClaimIsMine, -} from "selectors/claims"; +import { makeSelectTitleForUri, makeSelectClaimIsMine } from "selectors/claims"; import { makeSelectFileInfoForUri } from "selectors/file_info"; import ModalRemoveFile from "./view"; const select = (state, props) => ({ claimIsMine: makeSelectClaimIsMine(props.uri)(state), - metadata: makeSelectMetadataForUri(props.uri)(state), + title: makeSelectTitleForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state), }); diff --git a/ui/js/modal/modalRemoveFile/view.jsx b/ui/js/modal/modalRemoveFile/view.jsx index f646c5400..c18acb6f2 100644 --- a/ui/js/modal/modalRemoveFile/view.jsx +++ b/ui/js/modal/modalRemoveFile/view.jsx @@ -30,7 +30,7 @@ class ModalRemoveFile extends React.PureComponent { closeModal, deleteFile, fileInfo: { outpoint }, - metadata: { title }, + title, } = this.props; const { deleteChecked, abandonClaimChecked } = this.state; diff --git a/ui/js/page/channel/index.js b/ui/js/page/channel/index.js index a407f8c48..f69d5b15e 100644 --- a/ui/js/page/channel/index.js +++ b/ui/js/page/channel/index.js @@ -9,18 +9,19 @@ import { makeSelectClaimsInChannelForCurrentPage, makeSelectFetchingChannelClaims, } from "selectors/claims"; -import { selectCurrentParams } from "selectors/navigation"; +import { + makeSelectCurrentParam, + selectCurrentParams, +} from "selectors/navigation"; import { doNavigate } from "actions/navigation"; import { makeSelectTotalPagesForChannel } from "selectors/content"; import ChannelPage from "./view"; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), - claimsInChannel: makeSelectClaimsInChannelForCurrentPage( - props.uri, - props.page - )(state), + claimsInChannel: makeSelectClaimsInChannelForCurrentPage(props.uri)(state), fetching: makeSelectFetchingChannelClaims(props.uri)(state), + page: makeSelectCurrentParam("page")(state), params: selectCurrentParams(state), totalPages: makeSelectTotalPagesForChannel(props.uri)(state), }); diff --git a/ui/js/page/channel/view.jsx b/ui/js/page/channel/view.jsx index 617ef84ba..f7ccd1c8f 100644 --- a/ui/js/page/channel/view.jsx +++ b/ui/js/page/channel/view.jsx @@ -2,7 +2,6 @@ import React from "react"; import lbryuri from "lbryuri"; import { BusyMessage } from "component/common"; import FileTile from "component/fileTile"; -import Link from "component/link"; import ReactPaginate from "react-paginate"; class ChannelPage extends React.PureComponent { @@ -16,7 +15,7 @@ class ChannelPage extends React.PureComponent { componentWillReceiveProps(nextProps) { const { page, uri, fetching, fetchClaims, fetchClaimCount } = this.props; - if (fetching !== nextProps.page && page !== nextProps.page) { + if (nextProps.page && page !== nextProps.page) { fetchClaims(nextProps.uri, nextProps.page); } if (nextProps.uri != uri) { @@ -25,7 +24,7 @@ class ChannelPage extends React.PureComponent { } changePage(pageNumber) { - const { params, currentPage } = this.props; + const { params } = this.props; const newParams = Object.assign({}, params, { page: pageNumber }); this.props.navigate("/show", newParams); @@ -42,10 +41,10 @@ class ChannelPage extends React.PureComponent { } = this.props; let contentList; - if (claimsInChannel === undefined) { + if (fetching) { contentList = ; - } else if (claimsInChannel) { - contentList = claimsInChannel.length + } else { + contentList = claimsInChannel && claimsInChannel.length ? claimsInChannel.map(claim => +
{!hasContent && fetchingFeaturedUris && } diff --git a/ui/js/page/file/index.js b/ui/js/page/file/index.js index d0d1fb476..e56391051 100644 --- a/ui/js/page/file/index.js +++ b/ui/js/page/file/index.js @@ -13,6 +13,7 @@ import { import { makeSelectCostInfoForUri } from "selectors/cost_info"; import { selectShowNsfw } from "selectors/settings"; import FilePage from "./view"; +import { makeSelectCurrentParam } from "selectors/navigation"; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), @@ -20,6 +21,7 @@ const select = (state, props) => ({ costInfo: makeSelectCostInfoForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state), obscureNsfw: !selectShowNsfw(state), + tab: makeSelectCurrentParam("tab")(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state), rewardedContentClaimIds: selectRewardContentClaimIds(state, props), }); diff --git a/ui/js/page/file/view.jsx b/ui/js/page/file/view.jsx index 3e1a2e2f9..35209ae18 100644 --- a/ui/js/page/file/view.jsx +++ b/ui/js/page/file/view.jsx @@ -1,55 +1,15 @@ import React from "react"; -import ReactMarkdown from "react-markdown"; import lbry from "lbry.js"; import lbryuri from "lbryuri.js"; import Video from "component/video"; -import TipLink from "component/tipLink"; import { Thumbnail } from "component/common"; import FilePrice from "component/filePrice"; -import FileActions from "component/fileActions"; -import Link from "component/link"; +import FileDetails from "component/fileDetails"; import UriIndicator from "component/uriIndicator"; import IconFeatured from "component/iconFeatured"; -import DateTime from "component/dateTime"; - -const FormatItem = props => { - const { - publishedDate, - contentType, - claim: { height }, - metadata: { language, license }, - } = props; - - const mediaType = lbry.getMediaType(contentType); - - return ( - - - - - - - - - - - - - - - -
{__("Published on")}
{__("Content-Type")}{mediaType}
{__("Language")}{language}
{__("License")}{license}
- ); -}; +import WalletSendTip from "component/walletSendTip"; class FilePage extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - showTipBox: false, - }; - } - componentDidMount() { this.fetchFileInfo(this.props); this.fetchCostInfo(this.props); @@ -71,29 +31,18 @@ class FilePage extends React.PureComponent { } } - handleTipShow() { - this.setState({ - showTipBox: true, - }); - } - - handleTipHide() { - this.setState({ - showTipBox: false, - }); - } - render() { const { claim, fileInfo, metadata, contentType, + tab, uri, rewardedContentClaimIds, } = this.props; - const { showTipBox } = this.state; + const showTipBox = tab == "tip"; if (!claim || !metadata) { return ( @@ -101,24 +50,7 @@ class FilePage extends React.PureComponent { ); } - const { - txid, - nout, - channel_name: channelName, - has_signature: hasSignature, - signature_is_valid: signatureIsValid, - value, - } = claim; - - const outpoint = txid + ":" + nout; const title = metadata.title; - const channelClaimId = claim.value && claim.value.publisherSignature - ? claim.value.publisherSignature.certificateId - : null; - const channelUri = signatureIsValid && hasSignature && channelName - ? lbryuri.build({ channelName, claimId: channelClaimId }, false) - : null; - const uriIndicator = ; const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id); const mediaType = lbry.getMediaType(contentType); const player = require("render-media"); @@ -128,7 +60,7 @@ class FilePage extends React.PureComponent { mediaType === "audio"; return ( -
+
{isPlayable ?
-
- {!fileInfo || fileInfo.written_bytes <= 0 - ? - - {isRewardContent && {" "}} - - : null} -

{title}

-
- {channelUri - ? - this.props.navigate("/show", { uri: channelUri })} - > - {uriIndicator} - - : uriIndicator} -
- -
- {!showTipBox && -
- + {(!tab || tab === "details") && +
+ {" "} {" "} +
+ {!fileInfo || fileInfo.written_bytes <= 0 + ? + + {isRewardContent && {" "}} + + : null} +

{title}

+
+ +
+
+
} + {tab === "tip" && + }
- {metadata && claim && !showTipBox - ?
- -
- : ""} - {showTipBox - ? - : ""} - {!showTipBox && -
- -
}
-
+ ); } } diff --git a/ui/js/page/rewards/view.jsx b/ui/js/page/rewards/view.jsx index 51d75e406..423d45faa 100644 --- a/ui/js/page/rewards/view.jsx +++ b/ui/js/page/rewards/view.jsx @@ -6,21 +6,27 @@ import SubHeader from "component/subHeader"; import Link from "component/link"; class RewardsPage extends React.PureComponent { - componentDidMount() { - this.fetchRewards(this.props); - } + /* + Below is broken for users who have claimed all rewards. - componentWillReceiveProps(nextProps) { - this.fetchRewards(nextProps); - } + It can safely be disabled since we fetch all rewards after authentication, but should be re-enabled once fixed. - fetchRewards(props) { - const { fetching, rewards, fetchRewards } = props; - - if (!fetching && (!rewards || !rewards.length)) { - fetchRewards(); - } - } + */ + // componentDidMount() { + // this.fetchRewards(this.props); + // } + // + // componentWillReceiveProps(nextProps) { + // this.fetchRewards(nextProps); + // } + // + // fetchRewards(props) { + // const { fetching, rewards, fetchRewards } = props; + // + // if (!fetching && (!rewards || !rewards.length)) { + // fetchRewards(); + // } + // } renderPageHeader() { const { doAuth, navigate, user } = this.props; diff --git a/ui/js/page/show/view.jsx b/ui/js/page/show/view.jsx index 679626d28..8b1e05789 100644 --- a/ui/js/page/show/view.jsx +++ b/ui/js/page/show/view.jsx @@ -36,6 +36,7 @@ class ShowPage extends React.PureComponent { message={__("Loading magic decentralized data...")} />} {claim === null && + !isResolvingUri && {__("There's nothing at this location.")} } diff --git a/ui/js/page/transactionHistory/view.jsx b/ui/js/page/transactionHistory/view.jsx index 8b9f1a90b..2f3a75715 100644 --- a/ui/js/page/transactionHistory/view.jsx +++ b/ui/js/page/transactionHistory/view.jsx @@ -10,18 +10,26 @@ class TransactionHistoryPage extends React.PureComponent { render() { const { fetchingTransactions, transactions } = this.props; + return (
-
+

{__("Transaction History")}

- {fetchingTransactions && - } - {!fetchingTransactions && - } + {fetchingTransactions && !transactions.length + ? + : ""} + {transactions && transactions.length + ? + : ""}
diff --git a/ui/js/reducers/claims.js b/ui/js/reducers/claims.js index e9e69a134..7f883a4ea 100644 --- a/ui/js/reducers/claims.js +++ b/ui/js/reducers/claims.js @@ -1,6 +1,7 @@ import * as types from "constants/action_types"; const reducers = {}; + const defaultState = {}; reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) { @@ -49,21 +50,23 @@ reducers[types.FETCH_CLAIM_LIST_MINE_COMPLETED] = function(state, action) { .filter(claimId => Object.keys(abandoningById).indexOf(claimId) === -1) ); - claims.filter(claim => claim.category.match(/claim/)).forEach(claim => { - byId[claim.claim_id] = claim; + claims + .filter(claim => claim.category && claim.category.match(/claim/)) + .forEach(claim => { + byId[claim.claim_id] = claim; - const pending = Object.values(pendingById).find(pendingClaim => { - return ( - pendingClaim.name == claim.name && - pendingClaim.channel_name == claim.channel_name - ); + const pending = Object.values(pendingById).find(pendingClaim => { + return ( + pendingClaim.name == claim.name && + pendingClaim.channel_name == claim.channel_name + ); + }); + + if (pending) { + delete pendingById[pending.claim_id]; + } }); - if (pending) { - delete pendingById[pending.claim_id]; - } - }); - // Remove old timed out pending publishes const old = Object.values(pendingById) .filter(pendingClaim => Date.now() - pendingClaim.time >= 20 * 60 * 1000) @@ -189,31 +192,6 @@ reducers[types.CREATE_CHANNEL_COMPLETED] = function(state, action) { }); }; -reducers[types.SUPPORT_TRANSACTION_STARTED] = function(state, action) { - const newSupportTransaction = Object.assign({}, state.supportTransaction, { - sendingSupport: true, - }); - - return Object.assign({}, state, { - supportTransaction: newSupportTransaction, - }); -}; - -reducers[types.SUPPORT_TRANSACTION_COMPLETED] = function(state, action) { - return Object.assign({}, state); -}; - -reducers[types.SUPPORT_TRANSACTION_FAILED] = function(state, action) { - const newSupportTransaction = Object.assign({}, state.supportTransaction, { - sendingSupport: false, - error: action.data.error, - }); - - return Object.assign({}, state, { - supportTransaction: newSupportTransaction, - }); -}; - export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; if (handler) return handler(state, action); diff --git a/ui/js/reducers/content.js b/ui/js/reducers/content.js index 9d8c5867e..56195e917 100644 --- a/ui/js/reducers/content.js +++ b/ui/js/reducers/content.js @@ -2,6 +2,7 @@ import * as types from "constants/action_types"; const reducers = {}; const defaultState = { + playingUri: null, rewardedContentClaimIds: [], channelPages: {}, }; @@ -58,6 +59,12 @@ reducers[types.RESOLVE_URI_CANCELED] = reducers[ }); }; +reducers[types.SET_PLAYING_URI] = (state, action) => { + return Object.assign({}, state, { + playingUri: action.data.uri, + }); +}; + // reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) { // const channelPages = Object.assign({}, state.channelPages); // const { uri, claims } = action.data; @@ -73,7 +80,7 @@ reducers[types.FETCH_CHANNEL_CLAIM_COUNT_COMPLETED] = function(state, action) { const channelPages = Object.assign({}, state.channelPages); const { uri, totalClaims } = action.data; - channelPages[uri] = totalClaims / 10; + channelPages[uri] = Math.ceil(totalClaims / 10); return Object.assign({}, state, { channelPages, diff --git a/ui/js/reducers/navigation.js b/ui/js/reducers/navigation.js index 821db9183..1ebcab2c9 100644 --- a/ui/js/reducers/navigation.js +++ b/ui/js/reducers/navigation.js @@ -24,12 +24,6 @@ reducers[types.DAEMON_READY] = 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, @@ -38,15 +32,16 @@ reducers[types.CHANGE_AFTER_AUTH_PATH] = function(state, action) { reducers[types.HISTORY_NAVIGATE] = (state, action) => { const { stack, index } = state; - - let newState = {}; - const path = action.data.url; - // Check for duplicated + let newState = { + currentPath: path, + }; + if (action.data.index >= 0) { newState.index = action.data.index; } else if (!stack[index] || stack[index].path !== path) { + // ^ Check for duplicated newState.stack = [...stack.slice(0, index + 1), { path, scrollY: 0 }]; newState.index = newState.stack.length - 1; } diff --git a/ui/js/reducers/search.js b/ui/js/reducers/search.js index be6c3bec7..ed500b987 100644 --- a/ui/js/reducers/search.js +++ b/ui/js/reducers/search.js @@ -1,5 +1,4 @@ import * as types from "constants/action_types"; -import lbryuri from "lbryuri"; const reducers = {}; const defaultState = {}; @@ -9,7 +8,6 @@ reducers[types.SEARCH_STARTED] = function(state, action) { return Object.assign({}, state, { searching: true, - query: query, }); }; @@ -31,7 +29,6 @@ reducers[types.SEARCH_COMPLETED] = function(state, action) { reducers[types.SEARCH_CANCELLED] = function(state, action) { return Object.assign({}, state, { searching: false, - query: undefined, }); }; diff --git a/ui/js/reducers/wallet.js b/ui/js/reducers/wallet.js index e704222a6..a4e25b964 100644 --- a/ui/js/reducers/wallet.js +++ b/ui/js/reducers/wallet.js @@ -10,11 +10,12 @@ const buildDraftTransaction = () => ({ const defaultState = { balance: undefined, blocks: {}, - transactions: [], + transactions: {}, fetchingTransactions: false, receiveAddress: address, gettingNewAddress: false, draftTransaction: buildDraftTransaction(), + sendingSupport: false, }; reducers[types.FETCH_TRANSACTIONS_STARTED] = function(state, action) { @@ -24,20 +25,16 @@ reducers[types.FETCH_TRANSACTIONS_STARTED] = function(state, action) { }; reducers[types.FETCH_TRANSACTIONS_COMPLETED] = function(state, action) { - const oldTransactions = Object.assign({}, state.transactions); - const byId = Object.assign({}, oldTransactions.byId); + let byId = Object.assign({}, state.transactions); + const { transactions } = action.data; transactions.forEach(transaction => { byId[transaction.txid] = transaction; }); - const newTransactions = Object.assign({}, oldTransactions, { - byId: byId, - }); - return Object.assign({}, state, { - transactions: newTransactions, + transactions: byId, fetchingTransactions: false, }); }; @@ -125,6 +122,25 @@ reducers[types.SEND_TRANSACTION_FAILED] = function(state, action) { }); }; +reducers[types.SUPPORT_TRANSACTION_STARTED] = function(state, action) { + return Object.assign({}, state, { + sendingSupport: true, + }); +}; + +reducers[types.SUPPORT_TRANSACTION_COMPLETED] = function(state, action) { + return Object.assign({}, state, { + sendingSupport: false, + }); +}; + +reducers[types.SUPPORT_TRANSACTION_FAILED] = function(state, action) { + return Object.assign({}, state, { + error: action.data.error, + sendingSupport: false, + }); +}; + reducers[types.FETCH_BLOCK_SUCCESS] = (state, action) => { const { block, block: { height } } = action.data, blocks = Object.assign({}, state.blocks); diff --git a/ui/js/selectors/claims.js b/ui/js/selectors/claims.js index ce8ebc8cd..34c4ea688 100644 --- a/ui/js/selectors/claims.js +++ b/ui/js/selectors/claims.js @@ -1,6 +1,7 @@ import { createSelector } from "reselect"; import { selectCurrentParams } from "selectors/navigation"; import lbryuri from "lbryuri"; +import { makeSelectCurrentParam } from "./navigation"; const _selectState = state => state.claims || {}; @@ -66,19 +67,22 @@ export const selectAllFetchingChannelClaims = createSelector( ); export const makeSelectFetchingChannelClaims = uri => { - createSelector( + return createSelector( selectAllFetchingChannelClaims, fetching => fetching && fetching[uri] ); }; -export const makeSelectClaimsInChannelForCurrentPage = (uri, page = 1) => { +export const makeSelectClaimsInChannelForCurrentPage = uri => { + const pageSelector = makeSelectCurrentParam("page"); + return createSelector( selectClaimsById, selectAllClaimsByChannel, - (byId, allClaims) => { + pageSelector, + (byId, allClaims, page) => { const byChannel = allClaims[uri] || {}; - const claimIds = byChannel[page]; + const claimIds = byChannel[page || 1]; if (!claimIds) return claimIds; @@ -97,6 +101,13 @@ export const makeSelectMetadataForUri = uri => { }); }; +export const makeSelectTitleForUri = uri => { + return createSelector( + makeSelectMetadataForUri(uri), + metadata => metadata && metadata.title + ); +}; + export const makeSelectContentTypeForUri = uri => { return createSelector(makeSelectClaimForUri(uri), claim => { const source = diff --git a/ui/js/selectors/content.js b/ui/js/selectors/content.js index e8c0d42cc..0818134de 100644 --- a/ui/js/selectors/content.js +++ b/ui/js/selectors/content.js @@ -17,6 +17,11 @@ export const selectResolvingUris = createSelector( state => state.resolvingUris || [] ); +export const selectPlayingUri = createSelector( + _selectState, + state => state.playingUri +); + export const makeSelectIsUriResolving = uri => { return createSelector( selectResolvingUris, diff --git a/ui/js/selectors/navigation.js b/ui/js/selectors/navigation.js index 08ae51774..b5eb98ade 100644 --- a/ui/js/selectors/navigation.js +++ b/ui/js/selectors/navigation.js @@ -10,8 +10,11 @@ export const selectCurrentPath = createSelector( state => state.currentPath ); +export const computePageFromPath = path => + path.replace(/^\//, "").split("?")[0]; + export const selectCurrentPage = createSelector(selectCurrentPath, path => { - return path.replace(/^\//, "").split("?")[0]; + return computePageFromPath(path); }); export const selectCurrentParams = createSelector(selectCurrentPath, path => { @@ -87,7 +90,7 @@ export const selectPageTitle = createSelector( case "start": return __("Start"); case "publish": - return __("Publish"); + return params.id ? __("Edit") : __("Publish"); case "help": return __("Help"); case "developer": diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index c2a646563..f9fbc4fe4 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -19,6 +19,15 @@ export const selectClaimedRewards = createSelector( byId => Object.values(byId) || [] ); +export const selectClaimedRewardsByTransactionId = createSelector( + selectClaimedRewards, + rewards => + rewards.reduce((map, reward) => { + map[reward.transaction_id] = reward; + return map; + }, {}) +); + export const selectUnclaimedRewards = createSelector( selectUnclaimedRewardsByType, byType => diff --git a/ui/js/selectors/search.js b/ui/js/selectors/search.js index e3c6fbb4a..9b967d66e 100644 --- a/ui/js/selectors/search.js +++ b/ui/js/selectors/search.js @@ -1,11 +1,16 @@ import { createSelector } from "reselect"; -import { selectPageTitle, selectCurrentPage } from "selectors/navigation"; +import { + selectPageTitle, + selectCurrentPage, + selectCurrentParams, +} from "selectors/navigation"; export const _selectState = state => state.search || {}; export const selectSearchQuery = createSelector( - _selectState, - state => state.query + selectCurrentPage, + selectCurrentParams, + (page, params) => (page === "search" ? params && params.query : null) ); export const selectIsSearching = createSelector( @@ -36,45 +41,49 @@ export const selectWunderBarAddress = createSelector( (page, title, query) => (page != "search" ? title : query ? query : title) ); -export const selectWunderBarIcon = createSelector(selectCurrentPage, page => { - switch (page) { - case "auth": - return "icon-user"; - case "search": - return "icon-search"; - case "settings": - return "icon-gear"; - case "help": - return "icon-question"; - case "report": - return "icon-file"; - case "downloaded": - return "icon-folder"; - case "published": - return "icon-folder"; - 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-code"; - case "discover": - return "icon-home"; - default: - return "icon-file"; +export const selectWunderBarIcon = createSelector( + selectCurrentPage, + selectCurrentParams, + (page, params) => { + switch (page) { + case "auth": + return "icon-user"; + case "search": + return "icon-search"; + case "settings": + return "icon-gear"; + case "help": + return "icon-question"; + case "report": + return "icon-file"; + case "downloaded": + return "icon-folder"; + case "published": + return "icon-folder"; + 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 params.id ? __("icon-pencil") : __("icon-upload"); + case "developer": + return "icon-code"; + case "discover": + return "icon-home"; + default: + return "icon-file"; + } } -}); +); diff --git a/ui/js/selectors/wallet.js b/ui/js/selectors/wallet.js index ab7e22e07..2c9b6704b 100644 --- a/ui/js/selectors/wallet.js +++ b/ui/js/selectors/wallet.js @@ -7,34 +7,76 @@ export const selectBalance = createSelector( state => state.balance ); -export const selectTransactions = createSelector( - _selectState, - state => state.transactions || {} -); - export const selectTransactionsById = createSelector( - selectTransactions, - transactions => transactions.byId || {} + _selectState, + state => state.transactions ); export const selectTransactionItems = createSelector( selectTransactionsById, byId => { - const transactionItems = []; - const txids = Object.keys(byId); - txids.forEach(txid => { + const items = []; + + Object.keys(byId).forEach(txid => { const tx = byId[txid]; - transactionItems.push({ - id: txid, - date: tx.timestamp ? new Date(parseInt(tx.timestamp) * 1000) : null, - amount: parseFloat(tx.value), - claim_info: tx.claim_info, - support_info: tx.support_info, - update_info: tx.update_info, - fee: tx.fee, - }); + + //ignore dust/fees + if (Math.abs(tx.amount) === Math.abs(tx.fee)) { + return; + } + + let append = []; + + append.push( + ...tx.claim_info.map(item => + Object.assign({}, tx, item, { + type: item.claim_name[0] === "@" ? "channel" : "publish", + }) + ) + ); + append.push( + ...tx.support_info.map(item => + Object.assign({}, tx, item, { + type: !item.is_tip ? "support" : "tip", + }) + ) + ); + append.push( + ...tx.update_info.map(item => + Object.assign({}, tx, item, { type: "update" }) + ) + ); + + if (!append.length) { + append.push( + Object.assign({}, tx, { + type: tx.value < 0 ? "spend" : "receive", + }) + ); + } + + items.push( + ...append.map(item => { + //value on transaction, amount on outpoint + //amount is always positive, but should match sign of value + const amount = parseFloat( + item.amount ? (item.value < 0 ? -1 : 1) * item.amount : item.value + ); + + return { + txid: txid, + date: tx.timestamp ? new Date(parseInt(tx.timestamp) * 1000) : null, + amount: amount, + fee: amount < 0 ? -1 * tx.fee / append.length : 0, + claim_id: item.claim_id, + claim_name: item.claim_name, + type: item.type || "send", + nout: item.nout, + }; + }) + ); }); - return transactionItems.reverse(); + return items.reverse(); } ); @@ -61,6 +103,11 @@ export const selectIsFetchingTransactions = createSelector( state => state.fetchingTransactions ); +export const selectIsSendingSupport = createSelector( + _selectState, + state => state.sendingSupport +); + export const selectReceiveAddress = createSelector( _selectState, state => state.receiveAddress diff --git a/ui/package.json b/ui/package.json index c0e6e73b5..82f5d0889 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.15.1", + "version": "0.16.0", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index dffbc5298..67e6b48db 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -3,6 +3,7 @@ html height: 100%; font-size: var(--font-size); } + body { color: var(--text-color); @@ -27,6 +28,11 @@ body font-weight: bold; color: var(--color-money); } +.credit-amount--fee +{ + font-size: 0.9em; + color: var(--color-meta-light); +} #main-content { @@ -50,7 +56,8 @@ body width: $width-page-constrained; } } -main.main--refreshing { + +.reloading { &:before { $width: 30px; position: absolute; diff --git a/ui/scss/_icons.scss b/ui/scss/_icons.scss index 329b65566..537531e19 100644 --- a/ui/scss/_icons.scss +++ b/ui/scss/_icons.scss @@ -1656,3 +1656,9 @@ .icon-medium:before { content: "\f23a"; } +.icon-address-book:before { + content: "\f2b9"; +} +.icon-envelope-open:before { + content: "\f2b6"; +} diff --git a/ui/scss/_vars.scss b/ui/scss/_vars.scss index e7eb45008..43fe41bfe 100644 --- a/ui/scss/_vars.scss +++ b/ui/scss/_vars.scss @@ -90,8 +90,8 @@ $text-color: #000; /* Tabs */ --tab-bg: transparent; - --tab-color: #666; - --tab-active-color: var(--header-active-color); + --tab-color: rgba(0, 0, 0, 0.5); + --tab-active-color: var(--color-primary); --tab-border-size: 2px; --tab-border: var(--tab-border-size) solid var(--tab-active-color); @@ -132,4 +132,7 @@ $text-color: #000; --scrollbar-thumb-hover-bg: rgba(0, 0, 0, 0.35); --scrollbar-thumb-active-bg: var(--color-primary); --scrollbar-track-bg: transparent; + + /* Divider */ + --divider: 1px solid rgba(0, 0, 0, 0.12); } diff --git a/ui/scss/all.scss b/ui/scss/all.scss index 191a24756..be7733a21 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -22,5 +22,6 @@ @import "component/_pagination.scss"; @import "component/_markdown-editor.scss"; @import "component/_scrollbar.scss"; +@import "component/_tabs.scss"; @import "page/_developer.scss"; @import "page/_show.scss"; diff --git a/ui/scss/component/_button.scss b/ui/scss/component/_button.scss index 28075c123..acc67e0aa 100644 --- a/ui/scss/component/_button.scss +++ b/ui/scss/component/_button.scss @@ -86,3 +86,9 @@ $button-focus-shift: 12%; { box-shadow: none !important; } + +.button--submit { + font-family: inherit; + font-size: inherit; + line-height: 0; +} diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss index 156a5406f..0562b4040 100644 --- a/ui/scss/component/_card.scss +++ b/ui/scss/component/_card.scss @@ -7,6 +7,9 @@ border-radius: var(--card-radius); margin-bottom: var(--card-margin); overflow: auto; + + //below added to prevent scrollbar on long titles when show page loads, would prefer a cleaner CSS solution + overflow-x: hidden; } .card--obscured { @@ -35,6 +38,11 @@ margin-top: var(--card-margin); margin-bottom: var(--card-margin); } +.card__title-primary .meta { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} .card__title-identity { margin-top: $spacing-vertical * 1/3; margin-bottom: $spacing-vertical * 1/3; @@ -57,6 +65,9 @@ .card__content { margin-top: var(--card-margin); margin-bottom: var(--card-margin); + table:not(:last-child) { + margin-bottom: var(--card-margin); + } } $font-size-subtext-multiple: 0.82; .card__subtext { diff --git a/ui/scss/component/_header.scss b/ui/scss/component/_header.scss index 9428298e2..7d5e0e0af 100644 --- a/ui/scss/component/_header.scss +++ b/ui/scss/component/_header.scss @@ -56,38 +56,3 @@ border-color: var(--color-primary); } } - -nav.sub-header -{ - text-transform: uppercase; - padding: 0 0 $spacing-vertical; - max-width: $width-page-constrained; - margin-left: auto; - margin-right: auto; - > a - { - display: inline-block; - margin: 0 15px; - padding: 0 5px; - line-height:calc(var(--header-height) - $spacing-vertical - var(--tab-border-size)); - color: var(--tab-color); - - &:first-child - { - margin-left: 0; - } - &:last-child - { - margin-right: 0; - } - &.sub-header-selected - { - border-bottom: var(--tab-border); - color: var(--tab-active-color); - } - &:hover - { - color: var(--tab-active-color); - } - } -} diff --git a/ui/scss/component/_table.scss b/ui/scss/component/_table.scss index 2e2b664a1..59edeb480 100644 --- a/ui/scss/component/_table.scss +++ b/ui/scss/component/_table.scss @@ -60,3 +60,11 @@ table.table-standard { table.table-stretch { width: 100%; } + +table.table-transactions { + td:nth-of-type(1) { width: 15%; } + td:nth-of-type(2) { width: 15%; } + td:nth-of-type(3) { width: 15%; } + td:nth-of-type(4) { width: 40%; } + td:nth-of-type(5) { width: 15%; } +} \ No newline at end of file diff --git a/ui/scss/component/_tabs.scss b/ui/scss/component/_tabs.scss new file mode 100644 index 000000000..f2059dfef --- /dev/null +++ b/ui/scss/component/_tabs.scss @@ -0,0 +1,57 @@ +/* Tabs */ + +nav.sub-header +{ + text-transform: uppercase; + max-width: $width-page-constrained; + margin-bottom: 40px; + border-bottom: var(--divider); + > a + { + height: 38px; + line-height: 38px; + text-align: center; + font-weight: 600; + text-transform: uppercase; + display: inline-block; + vertical-align: baseline; + margin: 0 8px; + padding: 0 8px; + color: var(--tab-color); + position: relative; + + &:first-child + { + margin-left: 0; + } + &:last-child + { + margin-right: 0; + } + &.sub-header-selected + { + color: var(--tab-active-color); + &:before { + width: 100%; + height: var(--tab-border-size); + background: var(--tab-active-color); + position: absolute; + bottom: 0; + left: 0; + content: ''; + animation-name: activeTab; + animation-duration: 0.3s; + animation-timing-function: cubic-bezier(.55,0,.1,1); + } + } + &:hover + { + color: var(--tab-active-color); + } + } +} + +@keyframes activeTab { + from {width: 0;} + to {width: 100%;} +} diff --git a/ui/watch.bat b/ui/watch.bat new file mode 100644 index 000000000..0c064759a --- /dev/null +++ b/ui/watch.bat @@ -0,0 +1,11 @@ +@echo off + +set found= +for %%F in ( + "%~dp0\node_modules\node-sass\bin\node-sass" + "%~dp0\node_modules\.bin\webpack" +) do if exist %%F (set found=1) +if not defined found EXIT + +node %~dp0\node_modules\node-sass\bin\node-sass --output %~dp0\..\app\dist\css --sourcemap=none %~dp0\scss +%~dp0\node_modules\.bin\webpack --config %~dp0\webpack.dev.config.js --progress --colors --watch \ No newline at end of file