Merge branch 'redesign' into v16-scrollbar

This commit is contained in:
Baltazar Gomez 2017-09-21 10:48:29 -06:00 committed by GitHub
commit 440d982690
83 changed files with 1273 additions and 1084 deletions

View file

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.15.1 current_version = 0.16.0
commit = True commit = True
tag = True tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((?P<release>[a-z]+)(?P<candidate>\d+))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((?P<release>[a-z]+)(?P<candidate>\d+))?

View file

@ -8,12 +8,8 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased] ## [Unreleased]
### Added ### 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 ### 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 ## [0.15.1] - 2017-09-08
### Added ### Added

View file

@ -46,10 +46,52 @@ to create distributable packages, which is run by calling:
`node_modules/.bin/build -p never` `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 ### Windows Dependency
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.
1. Download and install `npm` and `node` from <a href="https://nodejs.org/en/download/current/">nodejs.org<a>
2. Download and install `python 2.7` from <a href="https://www.python.org/downloads/windows/">python.org</a>
3. Download and Install `Microsoft Visual C++ Compiler for Python 2.7` from <a href="https://www.microsoft.com/en-us/download/confirmation.aspx?id=44266">Microsoft<a>
4. Download and install `.NET Framework 2.0 Software Development Kit (SDK) (x64)` from <a href="https://www.microsoft.com/en-gb/download/details.aspx?id=15354">Microsoft<a>
### 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 ## Internationalization

View file

@ -1,6 +1,6 @@
{ {
"name": "LBRY", "name": "LBRY",
"version": "0.15.1", "version": "0.16.0",
"main": "main.js", "main": "main.js",
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.", "description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
"author": { "author": {
@ -20,7 +20,7 @@
"electron-rebuild": "^1.5.11" "electron-rebuild": "^1.5.11"
}, },
"lbrySettings": { "lbrySettings": {
"lbrynetDaemonVersion": "0.16.0rc8", "lbrynetDaemonVersion": "0.16.1",
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-daemon-vDAEMONVER-OSNAME.zip" "lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-daemon-vDAEMONVER-OSNAME.zip"
}, },
"license": "MIT" "license": "MIT"

View file

@ -1,5 +1,4 @@
#!/bin/bash #!/bin/bash
# this is here because teamcity runs /build.sh to build the project # this is here because teamcity runs /build.sh to build the project
set -euxo pipefail
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
$DIR/build/build.sh $DIR/build/build.sh

View file

@ -1,7 +1,6 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
set -x
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
cd "$ROOT" cd "$ROOT"
@ -10,8 +9,10 @@ BUILD_DIR="$ROOT/build"
LINUX=false LINUX=false
OSX=false OSX=false
if [ "$(uname)" == "Darwin" ]; then if [ "$(uname)" == "Darwin" ]; then
echo -e "\033[0;32mBuilding for OSX\x1b[m"
OSX=true OSX=true
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
echo -e "\033[0;32mBuilding for Linux\x1b[m"
LINUX=true LINUX=true
else else
echo -e "\033[1;31mPlatform detection failed\x1b[m" 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 if [ -n "${TEAMCITY_VERSION:-}" -o -n "${APPVEYOR:-}" ]; then
FULL_BUILD="true" FULL_BUILD="true"
fi 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 if [ "$FULL_BUILD" == "true" ]; then
# install dependencies # install dependencies
echo -e "\033[0;32mInstalling Dependencies\x1b[m"
$BUILD_DIR/prebuild.sh $BUILD_DIR/prebuild.sh
VENV="$BUILD_DIR/venv" VENV="$BUILD_DIR/venv"
@ -57,7 +62,7 @@ yarn install
############ ############
# UI # # UI #
############ ############
echo -e "\033[0;32mCompiling UI\x1b[m"
( (
cd "$ROOT/ui" cd "$ROOT/ui"
yarn install yarn install
@ -73,7 +78,7 @@ yarn install
#################### ####################
# daemon and cli # # daemon and cli #
#################### ####################
echo -e "\033[0;32mGrabbing Daemon and CLI\x1b[m"
if $OSX; then if $OSX; then
OSNAME="macos" OSNAME="macos"
else else
@ -99,7 +104,7 @@ fi
################### ###################
# Build the app # # Build the app #
################### ###################
echo -e '\033[0;32mBuilding Lbry-app\x1b[m'
( (
cd "$ROOT/app" cd "$ROOT/app"
yarn install yarn install

View file

@ -48,6 +48,9 @@
"Exec": "/opt/LBRY/lbry %U" "Exec": "/opt/LBRY/lbry %U"
} }
}, },
"deb": {
"depends": ["gconf2", "gconf-service", "libnotify4", "libappindicator1", "libxtst6", "libnss3", "libsecret-1-0"]
},
"win": { "win": {
"target": "nsis" "target": "nsis"
}, },

View file

@ -27,8 +27,8 @@
--search-border: 1px solid rgba(0,0,0, 0.25); --search-border: 1px solid rgba(0,0,0, 0.25);
/* Tab */ /* Tab */
--tab-color: #757575; --tab-color: rgba(255,255,255, 0.5) ;
--tab-active-color: #CCC; --tab-active-color: rgba(255,255,255, 0.75);
/* Header */ /* Header */
--header-color: #CCC; --header-color: #CCC;
@ -47,4 +47,7 @@
/* Scrollbar */ /* Scrollbar */
--scrollbar-thumb-bg: rgba(255, 255, 255, 0.20); --scrollbar-thumb-bg: rgba(255, 255, 255, 0.20);
--scrollbar-thumb-hover-bg: rgba(255, 255, 255, 0.35); --scrollbar-thumb-hover-bg: rgba(255, 255, 255, 0.35);
/* Divider */
--divider: 1px solid rgba(255,255,255, 0.12);
} }

View file

@ -4,6 +4,11 @@ import { selectFetchingAvailability } from "selectors/availability";
export function doFetchAvailability(uri) { export function doFetchAvailability(uri) {
return function(dispatch, getState) { return function(dispatch, getState) {
/*
this is disabled atm - Jeremy
*/
return;
const state = getState(); const state = getState();
const alreadyFetching = !!selectFetchingAvailability(state)[uri]; const alreadyFetching = !!selectFetchingAvailability(state)[uri];

View file

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

View file

@ -322,38 +322,29 @@ export function doPurchaseUri(uri) {
const downloadingByOutpoint = selectDownloadingByOutpoint(state); const downloadingByOutpoint = selectDownloadingByOutpoint(state);
const alreadyDownloading = const alreadyDownloading =
fileInfo && !!downloadingByOutpoint[fileInfo.outpoint]; 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 costInfo = makeSelectCostInfoForUri(uri)(state);
const { cost } = costInfo; const { cost } = costInfo;
// the file is free or we have partially downloaded it if (
if (cost === 0 || (fileInfo && fileInfo.download_directory)) { alreadyDownloading ||
dispatch(doLoadVideo(uri)); (fileInfo && fileInfo.completed && fileInfo.written_bytes > 0)
return Promise.resolve(); ) {
return;
}
// we already fully downloaded the file.
if (
cost === 0 ||
(fileInfo && (fileInfo.completed || fileInfo.download_directory))
) {
return dispatch(doLoadVideo(uri));
} }
if (cost > balance) { if (cost > balance) {
dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS)); return dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS));
} else {
dispatch(doOpenModal(modals.AFFIRM_PURCHASE, { uri }));
} }
return Promise.resolve(); return dispatch(doOpenModal(modals.AFFIRM_PURCHASE, { uri }));
}; };
} }
@ -364,7 +355,7 @@ export function doFetchClaimsByChannel(uri, page) {
data: { 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], const claimResult = result[uri],
claims = claimResult ? claimResult.claims_in_channel : [], claims = claimResult ? claimResult.claims_in_channel : [],
currentPage = claimResult ? claimResult.returned_page : undefined; 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() { export function doFetchChannelListMine() {
return function(dispatch, getState) { return function(dispatch, getState) {
dispatch({ dispatch({

View file

@ -33,26 +33,53 @@ export function doFetchCostInfoForUri(uri) {
}); });
} }
if (isGenerous && claim) { /**
let cost; * "Generous" check below is disabled. We're no longer attempting to include or estimate data fees regardless of settings.
const fee = claim.value && *
claim.value.stream && * This should be modified when lbry.stream_cost_estimate is reliable and performant.
claim.value.stream.metadata */
/*
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 ? claim.value.stream.metadata.fee
: undefined; : undefined;
if (fee === undefined) { if (fee === undefined) {
resolve({ cost: 0, includesData: true }); resolve({ cost: 0, includesData: true });
} else if (fee.currency == "LBC") { } else if (fee.currency == "LBC") {
resolve({ cost: fee.amount, includesData: true }); resolve({ cost: fee.amount, includesData: true });
} else { } else {
begin(); // begin();
lbryio.getExchangeRates().then(({ lbc_usd }) => { lbryio.getExchangeRates().then(({ lbc_usd }) => {
resolve({ cost: fee.amount / lbc_usd, includesData: true }); resolve({ cost: fee.amount / lbc_usd, includesData: true });
}); });
} }
} else {
begin(); // if (isGenerous && claim) {
lbry.getCostInfo(uri).then(resolve); // 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);
// }
}; };
} }

View file

@ -71,18 +71,18 @@ export function doFileList() {
}; };
} }
export function doOpenFileInShell(fileInfo) { export function doOpenFileInShell(path) {
return function(dispatch, getState) { return function(dispatch, getState) {
const success = shell.openItem(fileInfo.download_path); const success = shell.openItem(path);
if (!success) { if (!success) {
dispatch(doOpenFileInFolder(fileInfo)); dispatch(doOpenFileInFolder(path));
} }
}; };
} }
export function doOpenFileInFolder(fileInfo) { export function doOpenFileInFolder(path) {
return function(dispatch, getState) { return function(dispatch, getState) {
shell.showItemInFolder(fileInfo.download_path); shell.showItemInFolder(path);
}; };
} }

View file

@ -1,5 +1,6 @@
import * as types from "constants/action_types"; import * as types from "constants/action_types";
import { import {
computePageFromPath,
selectPageTitle, selectPageTitle,
selectCurrentPage, selectCurrentPage,
selectCurrentParams, selectCurrentParams,
@ -20,9 +21,17 @@ export function doNavigate(path, params = {}, options = {}) {
url += "?" + toQueryString(params); 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({ dispatch({
type: types.HISTORY_NAVIGATE, 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) { export function doHistoryTraverse(dispatch, state, modifier) {
const stack = selectHistoryStack(state), const stack = selectHistoryStack(state),
index = selectHistoryIndex(state) + modifier; index = selectHistoryIndex(state) + modifier;

View file

@ -6,6 +6,7 @@ import {
selectBalance, selectBalance,
} from "selectors/wallet"; } from "selectors/wallet";
import { doOpenModal, doShowSnackBar } from "actions/app"; import { doOpenModal, doShowSnackBar } from "actions/app";
import { doNavigate } from "actions/navigation";
import * as modals from "constants/modal_types"; import * as modals from "constants/modal_types";
export function doUpdateBalance(balance) { export function doUpdateBalance(balance) {
@ -143,3 +144,55 @@ export function doSetDraftTransactionAddress(address) {
data: { 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);
};
}

View file

@ -70,8 +70,7 @@ export class CreditAmount extends React.PureComponent {
showFree: React.PropTypes.bool, showFree: React.PropTypes.bool,
showFullPrice: React.PropTypes.bool, showFullPrice: React.PropTypes.bool,
showPlus: React.PropTypes.bool, showPlus: React.PropTypes.bool,
look: React.PropTypes.oneOf(["indicator", "plain"]), look: React.PropTypes.oneOf(["indicator", "plain", "fee"]),
fee: React.PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -79,10 +78,8 @@ export class CreditAmount extends React.PureComponent {
label: true, label: true,
showFree: false, showFree: false,
look: "indicator", look: "indicator",
showFree: false,
showFullPrice: false, showFullPrice: false,
showPlus: false, showPlus: false,
fee: false,
}; };
render() { render() {
@ -119,10 +116,7 @@ export class CreditAmount extends React.PureComponent {
return ( return (
<span <span
className={`credit-amount credit-amount--${this.props.look} ${this.props className={`credit-amount credit-amount--${this.props.look}`}
.fee
? " meta"
: ""}`}
title={fullPrice} title={fullPrice}
> >
<span> <span>

View file

@ -1,6 +1,10 @@
import React from "react"; import React from "react";
class DateTime extends React.PureComponent { class DateTime extends React.PureComponent {
static SHOW_DATE = "date";
static SHOW_TIME = "time";
static SHOW_BOTH = "both";
componentWillMount() { componentWillMount() {
this.refreshDate(this.props); this.refreshDate(this.props);
} }
@ -17,9 +21,20 @@ class DateTime extends React.PureComponent {
} }
render() { render() {
const { date } = this.props; const { date, formatOptions } = this.props;
const show = this.props.show || DateTime.SHOW_BOTH;
return <span>{date && date.toLocaleString()}</span>; return (
<span>
{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()}
</span>
);
} }
} }

View file

@ -1,14 +1,9 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { selectPlatform } from "selectors/app";
import { makeSelectFileInfoForUri } from "selectors/file_info"; import { makeSelectFileInfoForUri } from "selectors/file_info";
import { makeSelectCostInfoForUri } from "selectors/cost_info"; import { makeSelectCostInfoForUri } from "selectors/cost_info";
import { doOpenModal } from "actions/app"; import { doOpenModal } from "actions/app";
import { doFetchAvailability } from "actions/availability";
import { doOpenFileInShell, doOpenFileInFolder } from "actions/file_info";
import { makeSelectClaimIsMine } from "selectors/claims"; import { makeSelectClaimIsMine } from "selectors/claims";
import { doPurchaseUri, doLoadVideo, doStartDownload } from "actions/content";
import { doNavigate } from "actions/navigation";
import FileActions from "./view"; import FileActions from "./view";
const select = (state, props) => ({ const select = (state, props) => ({
@ -19,12 +14,7 @@ const select = (state, props) => ({
}); });
const perform = dispatch => ({ const perform = dispatch => ({
checkAvailability: uri => dispatch(doFetchAvailability(uri)),
openInShell: fileInfo => dispatch(doOpenFileInShell(fileInfo)),
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), 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); export default connect(select, perform)(FileActions);

View file

@ -4,19 +4,11 @@ import FileDownloadLink from "component/fileDownloadLink";
import * as modals from "constants/modal_types"; import * as modals from "constants/modal_types";
class FileActions extends React.PureComponent { class FileActions extends React.PureComponent {
handleSupportButtonClicked() {
this.props.onTipShow();
}
render() { render() {
const { fileInfo, uri, openModal, claimIsMine, editClaim } = this.props; const { fileInfo, uri, openModal, claimIsMine } = this.props;
const name = fileInfo ? fileInfo.name : null; const claimId = fileInfo ? fileInfo.claim_id : null,
const channel = fileInfo ? fileInfo.channel_name : null; showDelete = fileInfo && Object.keys(fileInfo).length > 0;
const metadata = fileInfo ? fileInfo.metadata : null,
showMenu = fileInfo && Object.keys(fileInfo).length > 0,
title = metadata ? metadata.title : uri;
return ( return (
<section className="card__actions"> <section className="card__actions">
@ -25,22 +17,25 @@ class FileActions extends React.PureComponent {
button="text" button="text"
icon="icon-edit" icon="icon-edit"
label={__("Edit")} label={__("Edit")}
onClick={() => editClaim({ name, channel })} navigate="/publish"
navigateParams={{ id: claimId }}
/>} />}
<FileDownloadLink uri={uri} /> <FileDownloadLink uri={uri} />
<Link <Link
button="text" button="text"
icon="icon-gift" icon="icon-gift"
label={__("Support")} label={__("Support")}
onClick={this.handleSupportButtonClicked.bind(this)} navigate="/show"
navigateParams={{ uri, tab: "tip" }}
/> />
{showDelete &&
<Link <Link
button="text" button="text"
icon="icon-trash" icon="icon-trash"
label={__("Remove")} label={__("Remove")}
className="card__action--right" className="card__action--right"
onClick={() => openModal(modals.CONFIRM_FILE_REMOVE, { uri })} onClick={() => openModal(modals.CONFIRM_FILE_REMOVE, { uri })}
/> />}
</section> </section>
); );
} }

View file

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

View file

@ -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 (
<div className="card__content">
<span className="empty">{__("Empty claim or metadata info.")}</span>
</div>
);
}
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 (
<div>
<FileActions uri={uri} />
<div className="card__content card__subtext card__subtext--allow-newlines">
<ReactMarkdown
source={description || ""}
escapeHtml={true}
disallowedTypes={["Heading", "HtmlInline", "HtmlBlock"]}
/>
</div>
<div className="card__content">
<table className="table-standard table-stretch">
<tbody>
<tr>
<td>{__("Published on")}</td>
<td><DateTime block={height} /></td>
</tr>
<tr>
<td>{__("Content-Type")}</td><td>{mediaType}</td>
</tr>
<tr>
<td>{__("Language")}</td><td>{language}</td>
</tr>
<tr>
<td>{__("License")}</td><td>{license}</td>
</tr>
{directory &&
<tr>
<td>{__("Downloaded to")}</td>
<td>
<Link onClick={() => openFolder(directory)}>
{directory}
</Link>
</td>
</tr>}
</tbody>
</table>
<p>
<Link
href={`https://lbry.io/dmca?claim_id=${claim.claim_id}`}
label={__("report")}
/>
</p>
</div>
</div>
);
}
}
export default FileDetails;

View file

@ -10,7 +10,6 @@ import { doFetchAvailability } from "actions/availability";
import { doOpenFileInShell } from "actions/file_info"; import { doOpenFileInShell } from "actions/file_info";
import { doPurchaseUri, doStartDownload } from "actions/content"; import { doPurchaseUri, doStartDownload } from "actions/content";
import FileDownloadLink from "./view"; import FileDownloadLink from "./view";
import * as modals from "constants/modal_types";
const select = (state, props) => ({ const select = (state, props) => ({
fileInfo: makeSelectFileInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state),
@ -22,9 +21,8 @@ const select = (state, props) => ({
const perform = dispatch => ({ const perform = dispatch => ({
checkAvailability: uri => dispatch(doFetchAvailability(uri)), checkAvailability: uri => dispatch(doFetchAvailability(uri)),
openInShell: fileInfo => dispatch(doOpenFileInShell(fileInfo)), openInShell: path => dispatch(doOpenFileInShell(path)),
startDownload: uri => purchaseUri: uri => dispatch(doPurchaseUri(uri)),
dispatch(doPurchaseUri(uri, modals.CONFIRM_FILE_PURCHASE)),
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)), restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
}); });

View file

@ -39,7 +39,7 @@ class FileDownloadLink extends React.PureComponent {
downloading, downloading,
uri, uri,
openInShell, openInShell,
startDownload, purchaseUri,
costInfo, costInfo,
loading, loading,
} = this.props; } = this.props;
@ -81,7 +81,7 @@ class FileDownloadLink extends React.PureComponent {
label={__("Download")} label={__("Download")}
icon="icon-download" icon="icon-download"
onClick={() => { onClick={() => {
startDownload(uri); purchaseUri(uri);
}} }}
/> />
); );
@ -92,7 +92,7 @@ class FileDownloadLink extends React.PureComponent {
label={__("Open")} label={__("Open")}
button="text" button="text"
icon="icon-external-link-square" icon="icon-external-link-square"
onClick={() => openInShell(fileInfo)} onClick={() => openInShell(fileInfo.download_path)}
/> />
); );
} }

View file

@ -49,7 +49,17 @@ const FileListSearchResults = props => {
class FileListSearch extends React.PureComponent { class FileListSearch extends React.PureComponent {
componentWillMount() { 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() { render() {

View file

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import FormField from "component/formField"; import FormField from "component/formField";
import { Icon } from "component/common.js";
let formFieldCounter = 0; let formFieldCounter = 0;
@ -9,6 +10,29 @@ export function formFieldId() {
return "form-field-" + ++formFieldCounter; 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 (
<form onSubmit={event => this.handleSubmit(event)}>
{this.props.children}
</form>
);
}
}
export class FormRow extends React.PureComponent { export class FormRow extends React.PureComponent {
static propTypes = { static propTypes = {
label: React.PropTypes.oneOfType([ 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 = (
<span className="button__content">
{"icon" in props ? <Icon icon={icon} fixed={true} /> : null}
{label ? <span className="button-label">{label}</span> : null}
</span>
);
return (
<button type="submit" className={className} title={title}>
{content}
</button>
);
};

View file

@ -47,9 +47,9 @@ class InviteList extends React.PureComponent {
<td className="text-center"> <td className="text-center">
{invitee.invite_reward_claimed {invitee.invite_reward_claimed
? <Icon icon="icon-check" /> ? <Icon icon="icon-check" />
: invitee.invite_accepted : invitee.invite_reward_claimable
? <RewardLink ? <RewardLink
label={__("Claim")} label={__("claim")}
reward_type={rewards.TYPE_REFERRAL} reward_type={rewards.TYPE_REFERRAL}
/> />
: <span className="empty"> : <span className="empty">

View file

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import { BusyMessage, CreditAmount } from "component/common"; import { BusyMessage, CreditAmount } from "component/common";
import Link from "component/link"; import { Form, FormRow, Submit } from "component/form.js";
import { FormRow } from "component/form.js";
class FormInviteNew extends React.PureComponent { class FormInviteNew extends React.PureComponent {
constructor(props) { constructor(props) {
@ -18,16 +17,16 @@ class FormInviteNew extends React.PureComponent {
}); });
} }
handleSubmit(event) { handleSubmit() {
event.preventDefault(); const { email } = this.state;
this.props.inviteNew(this.state.email); this.props.inviteNew(email);
} }
render() { render() {
const { errorMessage, isPending } = this.props; const { errorMessage, isPending } = this.props;
return ( return (
<form> <Form onSubmit={this.handleSubmit.bind(this)}>
<FormRow <FormRow
type="text" type="text"
label="Email" label="Email"
@ -40,16 +39,9 @@ class FormInviteNew extends React.PureComponent {
}} }}
/> />
<div className="form-row-submit"> <div className="form-row-submit">
<Link <Submit label={__("Send Invite")} disabled={isPending} />
button="primary"
label={__("Send Invite")}
disabled={isPending}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div> </div>
</form> </Form>
); );
} }
} }

View file

@ -4,7 +4,7 @@ import { doNavigate } from "actions/navigation";
import Link from "./view"; import Link from "./view";
const perform = dispatch => ({ const perform = dispatch => ({
doNavigate: path => dispatch(doNavigate(path)), doNavigate: (path, params) => dispatch(doNavigate(path, params)),
}); });
export default connect(null, perform)(Link); export default connect(null, perform)(Link);

View file

@ -12,6 +12,7 @@ const Link = props => {
disabled, disabled,
children, children,
navigate, navigate,
navigateParams,
doNavigate, doNavigate,
} = props; } = props;
@ -23,7 +24,7 @@ const Link = props => {
const onClick = !props.onClick && navigate const onClick = !props.onClick && navigate
? () => { ? () => {
doNavigate(navigate); doNavigate(navigate, navigateParams || {});
} }
: props.onClick; : props.onClick;

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; 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);

View file

@ -2,7 +2,7 @@ import React from "react";
import lbry from "lbry"; import lbry from "lbry";
import lbryuri from "lbryuri"; import lbryuri from "lbryuri";
import FormField from "component/formField"; 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 Link from "component/link";
import FormFieldPrice from "component/formFieldPrice"; import FormFieldPrice from "component/formFieldPrice";
import Modal from "modal/modal"; import Modal from "modal/modal";
@ -19,6 +19,7 @@ class PublishForm extends React.PureComponent {
this._defaultPaidPrice = 0.01; this._defaultPaidPrice = 0.01;
this.state = { this.state = {
id: null,
rawName: "", rawName: "",
name: "", name: "",
bid: 10, bid: 10,
@ -58,11 +59,7 @@ class PublishForm extends React.PureComponent {
if (!fetchingChannels) fetchChannelListMine(); if (!fetchingChannels) fetchChannelListMine();
} }
handleSubmit(event) { handleSubmit() {
if (typeof event !== "undefined") {
event.preventDefault();
}
this.setState({ this.setState({
submitting: true, submitting: true,
}); });
@ -189,10 +186,10 @@ class PublishForm extends React.PureComponent {
} }
handleEditClaim() { handleEditClaim() {
const isMine = this.myClaimExists(); const claimInfo = this.claim() || this.myClaimInfo();
if (isMine) { if (claimInfo) {
this.handlePrefillClicked(); this.handlePrefillClaim(claimInfo);
} }
} }
@ -212,10 +209,10 @@ class PublishForm extends React.PureComponent {
} }
myClaimInfo() { myClaimInfo() {
const { name } = this.state; const { id } = this.state;
return Object.values(this.props.myClaims).find( 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() { handlePrefillClaim(claimInfo) {
const claimInfo = this.myClaimInfo(); const { claim_id, name, channel_name, amount } = claimInfo;
const { source } = claimInfo.value.stream; const { source, metadata } = claimInfo.value.stream;
const { const {
license, license,
licenseUrl, licenseUrl,
@ -283,17 +281,21 @@ class PublishForm extends React.PureComponent {
description, description,
language, language,
nsfw, nsfw,
} = claimInfo.value.stream.metadata; } = metadata;
let newState = { let newState = {
mode: "edit", id: claim_id,
channel: channel_name || "anonymous",
bid: amount,
meta_title: title, meta_title: title,
meta_thumbnail: thumbnail, meta_thumbnail: thumbnail,
meta_description: description, meta_description: description,
meta_language: language, meta_language: language,
meta_nsfw: nsfw, meta_nsfw: nsfw,
mode: "edit",
prefillDone: true, prefillDone: true,
bid: claimInfo.amount, rawName: name,
name,
source, source,
}; };
@ -423,16 +425,11 @@ class PublishForm extends React.PureComponent {
} }
componentWillMount() { componentWillMount() {
let { name, channel } = this.props.params;
channel = channel || this.state.channel;
this.props.fetchClaimListMine(); this.props.fetchClaimListMine();
this._updateChannelList(); this._updateChannelList();
if (name) { const { id } = this.props.params;
this.setState({ name, rawName: name, channel }); this.setState({ id });
}
} }
componentDidMount() { componentDidMount() {
@ -461,37 +458,38 @@ class PublishForm extends React.PureComponent {
} }
getNameBidHelpText() { 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"); return __("Existing claim data was prefilled");
} }
if ( if (uri && resolvingUris.indexOf(uri) !== -1 && claim === undefined) {
this.state.uri &&
this.props.resolvingUris.indexOf(this.state.uri) !== -1 &&
this.claim() === undefined
) {
return __("Checking..."); return __("Checking...");
} else if (!this.state.name) { } else if (!name) {
return __("Select a URL for this publish."); return __("Select a URL for this publish.");
} else if (!this.claim()) { } else if (!claim) {
return __("This URL is unused."); return __("This URL is unused.");
} else if (this.myClaimExists() && !this.state.prefillDone) { } else if (this.myClaimExists() && !prefillDone) {
return ( return (
<span> <span>
{__("You already have a claim with this name.")}{" "} {__("You already have a claim with this name.")}{" "}
<Link <Link
label={__("Edit existing claim")} label={__("Edit existing claim")}
onClick={() => this.handlePrefillClicked()} onClick={() => this.handleEditClaim()}
/> />
</span> </span>
); );
} else if (this.claim()) { } else if (claim) {
if (this.topClaimValue() === 1) { const topClaimValue = this.topClaimValue();
if (topClaimValue === 1) {
return ( return (
<span> <span>
{__( {__(
'A deposit of at least one credit is required to win "%s". However, you can still get a permanent URL for any amount.', '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
)} )}
</span> </span>
); );
@ -500,8 +498,8 @@ class PublishForm extends React.PureComponent {
<span> <span>
{__( {__(
'A deposit of at least "%s" credits is required to win "%s". However, you can still get a permanent URL for any amount.', '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(), topClaimValue,
this.state.name name
)} )}
</span> </span>
); );
@ -532,11 +530,7 @@ class PublishForm extends React.PureComponent {
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<form <Form onSubmit={this.handleSubmit.bind(this)}>
onSubmit={event => {
this.handleSubmit(event);
}}
>
<section className="card"> <section className="card">
<div className="card__title-primary"> <div className="card__title-primary">
<h4>{__("Content")}</h4> <h4>{__("Content")}</h4>
@ -870,12 +864,10 @@ class PublishForm extends React.PureComponent {
</section> </section>
<div className="card-series-submit"> <div className="card-series-submit">
<Link <Submit
button="primary" label={
label={submitLabel} !this.state.submitting ? __("Publish") : __("Publishing...")
onClick={event => { }
this.handleSubmit(event);
}}
disabled={ disabled={
this.state.submitting || this.state.submitting ||
(this.state.uri && (this.state.uri &&
@ -890,9 +882,8 @@ class PublishForm extends React.PureComponent {
onClick={this.props.back} onClick={this.props.back}
label={__("Cancel")} label={__("Cancel")}
/> />
<input type="submit" className="hidden" />
</div> </div>
</form> </Form>
<Modal <Modal
isOpen={this.state.modal == "publishStarted"} isOpen={this.state.modal == "publishStarted"}

View file

@ -1,12 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import { doSendSupport } from "actions/claims";
import TipLink from "./view";
const select = state => ({});
const perform = dispatch => ({
sendSupport: (amount, claim_id) => dispatch(doSendSupport(amount, claim_id)),
});
export default connect(select, perform)(TipLink);

View file

@ -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 (
<div className="card__content">
<div className="card__title-primary">
<h4>{__("Support")}</h4>
</div>
<div className="card__content">
{__(
"Support the creator and the success of their content by sending a tip. "
)}
<Link label={__("Learn more")} href="https://lbry.io/faq/tipping" />
</div>
<div className="card__content">
<FormRow
label={__("Amount")}
postfix={__("LBC")}
min="0"
step="0.1"
type="number"
placeholder="1.00"
onChange={event => this.handleSupportPriceChange(event)}
/>
</div>
<div className="card__actions">
<Link
label={__("Send")}
button="primary"
onClick={this.handleSendButtonClicked.bind(this)}
/>
<Link
label={__("Cancel")}
button="alt"
onClick={this.handleSupportCancelButtonClicked.bind(this)}
/>
</div>
</div>
);
}
}
export default TipLink;

View file

@ -1,8 +1,13 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doNavigate } from "actions/navigation"; import { doNavigate } from "actions/navigation";
import { selectClaimedRewardsByTransactionId } from "selectors/rewards";
import TransactionList from "./view"; import TransactionList from "./view";
const select = state => ({
rewards: selectClaimedRewardsByTransactionId(state),
});
const perform = dispatch => ({ const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)), navigate: (path, params) => dispatch(doNavigate(path, params)),
}); });

View file

@ -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 (
<a className="button-text" onClick={() => this.props.navigate(uri)}>
{claim_name}
</a>
);
}
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 (
<tr key={`${txid}:${item.nout}`}>
<td>
{date
? date.toLocaleDateString("en-US", options)
: <span className="empty">
{__("(Transaction pending)")}
</span>}
</td>
<td>
<CreditAmount
amount={item.amount}
look="plain"
label={false}
showPlus={true}
precision={8}
/>
<br />
<CreditAmount
amount={fee}
look="plain"
fee={true}
label={false}
precision={8}
/>
</td>
<td>
{this.getClaimLink(item.claim_name, item.claim_id)}
</td>
<td>
<LinkTransaction id={txid} />
</td>
</tr>
);
})
: <tr key={txid}>
<td>
{date
? date.toLocaleDateString("en-US", options)
: <span className="empty">
{__("(Transaction pending)")}
</span>}
</td>
<td>
<CreditAmount
amount={transaction.amount}
look="plain"
label={false}
showPlus={true}
precision={8}
/>
<br />
<CreditAmount
amount={fee}
look="plain"
fee={true}
label={false}
precision={8}
/>
</td>
<td>
<LinkTransaction id={txid} />
</td>
</tr>;
}
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 (
<tbody>
{transactions
.filter(this.filterList, this)
.filter(this.removeFeeTx, this)
.map(this.renderBody, this)}
</tbody>
);
}
}
export default TransactionTableBody;

View file

@ -1,19 +0,0 @@
import React from "react";
class TransactionTableHeader extends React.PureComponent {
render() {
const { filter } = this.props;
return (
<thead>
<tr>
<th>{__("Date")}</th>
<th>{__("Amount(Fee)")}</th>
{filter != "unfiltered" && <th> {__("Claim Name")} </th>}
<th>{__("Transaction")}</th>
</tr>
</thead>
);
}
}
export default TransactionTableHeader;

View file

@ -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 (
<tr>
<td>
{date
? <div>
<DateTime date={date} show={DateTime.SHOW_DATE} />
<div className="meta">
<DateTime date={date} show={DateTime.SHOW_TIME} />
</div>
</div>
: <span className="empty">
{__("(Transaction pending)")}
</span>}
</td>
<td>
<CreditAmount
amount={amount}
look="plain"
label={false}
showPlus={true}
precision={8}
/>
<br />
{fee != 0 &&
<CreditAmount
amount={fee}
look="fee"
label={false}
precision={8}
/>}
</td>
<td>
{type}
</td>
<td>
{reward &&
<Link navigate="/rewards">
{__("Reward: %s", reward.reward_title)}
</Link>}
{name &&
claimId &&
<Link
className="button-text"
navigate="/show"
navigateParams={{ uri: lbryuri.build({ name, claimId }) }}
>
{name}
</Link>}
</td>
<td>
<LinkTransaction id={txid} />
</td>
</tr>
);
}
}
export default TransactionListItem;

View file

@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import TransactionTableHeader from "./internal/TransactionListHeader"; import TransactionListItem from "./internal/TransactionListItem";
import TransactionTableBody from "./internal/TransactionListBody";
import FormField from "component/formField"; import FormField from "component/formField";
class TransactionList extends React.PureComponent { class TransactionList extends React.PureComponent {
@ -8,7 +7,7 @@ class TransactionList extends React.PureComponent {
super(props); super(props);
this.state = { this.state = {
filter: "unfiltered", filter: null,
}; };
} }
@ -18,45 +17,63 @@ class TransactionList extends React.PureComponent {
}); });
} }
handleClaimNameClicked(uri) { filterTransaction(transaction) {
this.props.navigate("/show", { uri }); const { filter } = this.state;
return !filter || filter == transaction.type;
} }
render() { render() {
const { emptyMessage, transactions } = this.props; const { emptyMessage, rewards, transactions } = this.props;
const { filter } = this.state;
if (!transactions || !transactions.length) { let transactionList = transactions.filter(
return ( this.filterTransaction.bind(this)
<div className="empty">
{emptyMessage || __("No transactions to list.")}
</div>
); );
}
return ( return (
<div> <div>
{(transactionList.length || this.state.filter) &&
<span className="sort-section"> <span className="sort-section">
{__("Filter")} {" "} {__("Filter")} {" "}
<FormField <FormField
type="select" type="select"
onChange={this.handleFilterChanged.bind(this)} onChange={this.handleFilterChanged.bind(this)}
> >
<option value="unfiltered">{__("All")}</option> <option value="">{__("All")}</option>
<option value="claim">{__("Publishes")}</option> <option value="spend">{__("Spends")}</option>
<option value="receive">{__("Receives")}</option>
<option value="publish">{__("Publishes")}</option>
<option value="channel">{__("Channels")}</option>
<option value="tip">{__("Tips")}</option>
<option value="support">{__("Supports")}</option> <option value="support">{__("Supports")}</option>
<option value="tipSupport">{__("Tips")}</option>
<option value="update">{__("Updates")}</option> <option value="update">{__("Updates")}</option>
</FormField> </FormField>
</span> </span>}
<table className="table-standard table-stretch"> {!transactionList.length &&
<TransactionTableHeader filter={filter} /> <div className="empty">
<TransactionTableBody {emptyMessage || __("No transactions to list.")}
transactions={transactions} </div>}
filter={filter} {Boolean(transactionList.length) &&
navigate={this.handleClaimNameClicked.bind(this)} <table className="table-standard table-transactions table-stretch">
<thead>
<tr>
<th>{__("Date")}</th>
<th>{__("Amount (Fee)")}</th>
<th>{__("Type")} </th>
<th>{__("Details")} </th>
<th>{__("Transaction")}</th>
</tr>
</thead>
<tbody>
{transactionList.map(t =>
<TransactionListItem
key={`${t.txid}:${t.nout}`}
transaction={t}
reward={rewards && rewards[t.txid]}
/> />
</table> )}
</tbody>
</table>}
</div> </div>
); );
} }

View file

@ -1,5 +1,7 @@
import React from "react"; import React from "react";
import { Icon } from "component/common"; import { Icon } from "component/common";
import Link from "component/link";
import lbryuri from "lbryuri.js";
class UriIndicator extends React.PureComponent { class UriIndicator extends React.PureComponent {
componentWillMount() { componentWillMount() {
@ -19,7 +21,7 @@ class UriIndicator extends React.PureComponent {
} }
render() { render() {
const { claim, uri, isResolvingUri } = this.props; const { claim, link, uri, isResolvingUri } = this.props;
if (isResolvingUri && !claim) { if (isResolvingUri && !claim) {
return <span className="empty">Validating...</span>; return <span className="empty">Validating...</span>;
@ -33,21 +35,30 @@ class UriIndicator extends React.PureComponent {
channel_name: channelName, channel_name: channelName,
has_signature: hasSignature, has_signature: hasSignature,
signature_is_valid: signatureIsValid, signature_is_valid: signatureIsValid,
value,
} = claim; } = claim;
const channelClaimId =
value &&
value.publisherSignature &&
value.publisherSignature.certificateId;
if (!hasSignature || !channelName) { if (!hasSignature || !channelName) {
return <span className="empty">Anonymous</span>; return <span className="empty">Anonymous</span>;
} }
let icon, modifier; let icon, channelLink, modifier;
if (signatureIsValid) { if (signatureIsValid) {
modifier = "valid"; modifier = "valid";
channelLink = link
? lbryuri.build({ channelName, claimId: channelClaimId }, false)
: false;
} else { } else {
icon = "icon-times-circle"; icon = "icon-times-circle";
modifier = "invalid"; modifier = "invalid";
} }
return ( const inner = (
<span> <span>
{channelName} {" "} {channelName} {" "}
{!signatureIsValid {!signatureIsValid
@ -58,6 +69,16 @@ class UriIndicator extends React.PureComponent {
: ""} : ""}
</span> </span>
); );
if (!channelLink) {
return inner;
}
return (
<Link navigate="/show" navigateParams={{ uri: channelLink }}>
{inner}
</Link>
);
} }
} }

View file

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import Link from "component/link"; import Link from "component/link";
import { FormRow } from "component/form.js"; import { Form, FormRow, Submit } from "component/form.js";
class UserEmailNew extends React.PureComponent { class UserEmailNew extends React.PureComponent {
constructor(props) { constructor(props) {
@ -17,20 +17,16 @@ class UserEmailNew extends React.PureComponent {
}); });
} }
handleSubmit(event) { handleSubmit() {
event.preventDefault(); const { email } = this.state;
this.props.addUserEmail(this.state.email); this.props.addUserEmail(email);
} }
render() { render() {
const { errorMessage, isPending } = this.props; const { errorMessage, isPending } = this.props;
return ( return (
<form <Form onSubmit={this.handleSubmit.bind(this)}>
onSubmit={event => {
this.handleSubmit(event);
}}
>
<p> <p>
{__( {__(
"This process is required to prevent abuse of the rewards program." "This process is required to prevent abuse of the rewards program."
@ -53,16 +49,9 @@ class UserEmailNew extends React.PureComponent {
}} }}
/> />
<div className="form-row-submit"> <div className="form-row-submit">
<Link <Submit label="Next" disabled={isPending} />
button="primary"
label="Next"
disabled={isPending}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div> </div>
</form> </Form>
); );
} }
} }

View file

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import Link from "component/link"; import Link from "component/link";
import { FormRow } from "component/form.js"; import { Form, FormRow, Submit } from "component/form.js";
class UserEmailVerify extends React.PureComponent { class UserEmailVerify extends React.PureComponent {
constructor(props) { constructor(props) {
@ -17,19 +17,15 @@ class UserEmailVerify extends React.PureComponent {
}); });
} }
handleSubmit(event) { handleSubmit() {
event.preventDefault(); const { code } = this.state;
this.props.verifyUserEmail(this.state.code); this.props.verifyUserEmail(code);
} }
render() { render() {
const { errorMessage, isPending } = this.props; const { errorMessage, isPending } = this.props;
return ( return (
<form <Form onSubmit={this.handleSubmit.bind(this)}>
onSubmit={event => {
this.handleSubmit(event);
}}
>
<p>{__("Please enter the verification code emailed to you.")}</p> <p>{__("Please enter the verification code emailed to you.")}</p>
<FormRow <FormRow
type="text" type="text"
@ -50,16 +46,9 @@ class UserEmailVerify extends React.PureComponent {
</p> </p>
</div> </div>
<div className="form-row-submit form-row-submit--with-footer"> <div className="form-row-submit form-row-submit--with-footer">
<Link <Submit label={__("Verify")} disabled={this.state.submitting} />
button="primary"
label={__("Verify")}
disabled={this.state.submitting}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div> </div>
</form> </Form>
); );
} }
} }

View file

@ -1,9 +1,8 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import { doChangeVolume } from "actions/app"; import { doChangeVolume } from "actions/app";
import { selectCurrentModal, selectVolume } from "selectors/app"; import { selectVolume } from "selectors/app";
import { doPurchaseUri, doLoadVideo } from "actions/content"; import { doPlayUri, doSetPlayingUri } from "actions/content";
import { import {
makeSelectMetadataForUri, makeSelectMetadataForUri,
makeSelectContentTypeForUri, makeSelectContentTypeForUri,
@ -16,6 +15,7 @@ import {
import { makeSelectCostInfoForUri } from "selectors/cost_info"; import { makeSelectCostInfoForUri } from "selectors/cost_info";
import { selectShowNsfw } from "selectors/settings"; import { selectShowNsfw } from "selectors/settings";
import Video from "./view"; import Video from "./view";
import { selectPlayingUri } from "selectors/content";
const select = (state, props) => ({ const select = (state, props) => ({
costInfo: makeSelectCostInfoForUri(props.uri)(state), costInfo: makeSelectCostInfoForUri(props.uri)(state),
@ -24,13 +24,14 @@ const select = (state, props) => ({
obscureNsfw: !selectShowNsfw(state), obscureNsfw: !selectShowNsfw(state),
isLoading: makeSelectLoadingForUri(props.uri)(state), isLoading: makeSelectLoadingForUri(props.uri)(state),
isDownloading: makeSelectDownloadingForUri(props.uri)(state), isDownloading: makeSelectDownloadingForUri(props.uri)(state),
playingUri: selectPlayingUri(state),
contentType: makeSelectContentTypeForUri(props.uri)(state), contentType: makeSelectContentTypeForUri(props.uri)(state),
volume: selectVolume(state), volume: selectVolume(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
loadVideo: uri => dispatch(doLoadVideo(uri)), play: uri => dispatch(doPlayUri(uri)),
purchaseUri: uri => dispatch(doPurchaseUri(uri)), cancelPlay: () => dispatch(doSetPlayingUri(null)),
changeVolume: volume => dispatch(doChangeVolume(volume)), changeVolume: volume => dispatch(doChangeVolume(volume)),
}); });

View file

@ -17,18 +17,12 @@ class VideoPlayButton extends React.PureComponent {
"Space" === event.code "Space" === event.code
) { ) {
event.preventDefault(); event.preventDefault();
this.onWatchClick(); this.watch();
} }
} }
onWatchClick() { watch() {
this.props.purchaseUri(this.props.uri).then(() => { this.props.play(this.props.uri);
if (!this.props.modal) {
this.props.startPlaying();
} else {
alert("fix me set pending play");
}
});
} }
render() { render() {
@ -54,7 +48,7 @@ class VideoPlayButton extends React.PureComponent {
label={label ? label : ""} label={label ? label : ""}
className="video__play-button" className="video__play-button"
icon={icon} icon={icon}
onClick={this.onWatchClick.bind(this)} onClick={() => this.watch()}
/> />
); );
} }

View file

@ -9,20 +9,12 @@ class Video extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
isPlaying: false,
showNsfwHelp: false, showNsfwHelp: false,
}; };
} }
componentWillReceiveProps(nextProps) { componentWillUnmount() {
// reset playing state upon change path action this.props.cancelPlay();
if (
!this.isMediaSame(nextProps) &&
this.props.fileInfo &&
this.state.isPlaying
) {
this.state.isPlaying = false;
}
} }
isMediaSame(nextProps) { isMediaSame(nextProps) {
@ -33,12 +25,6 @@ class Video extends React.PureComponent {
); );
} }
startPlaying() {
this.setState({
isPlaying: true,
});
}
handleMouseOver() { handleMouseOver() {
if ( if (
this.props.obscureNsfw && this.props.obscureNsfw &&
@ -64,13 +50,15 @@ class Video extends React.PureComponent {
metadata, metadata,
isLoading, isLoading,
isDownloading, isDownloading,
playingUri,
fileInfo, fileInfo,
contentType, contentType,
changeVolume, changeVolume,
volume, volume,
uri,
} = this.props; } = this.props;
const { isPlaying = false } = this.state;
const isPlaying = playingUri === uri;
const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0; const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0;
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
const mediaType = lbry.getMediaType( const mediaType = lbry.getMediaType(
@ -129,11 +117,7 @@ class Video extends React.PureComponent {
className="video__cover" className="video__cover"
style={{ backgroundImage: 'url("' + metadata.thumbnail + '")' }} style={{ backgroundImage: 'url("' + metadata.thumbnail + '")' }}
> >
<VideoPlayButton <VideoPlayButton {...this.props} mediaType={mediaType} />
startPlaying={this.startPlaying.bind(this)}
{...this.props}
mediaType={mediaType}
/>
</div>} </div>}
{this.state.showNsfwHelp && <NsfwOverlay />} {this.state.showNsfwHelp && <NsfwOverlay />}
</div> </div>

View file

@ -1,14 +1,31 @@
import React from "react"; import React from "react";
import Link from "component/link"; import { Form, FormRow, Submit } from "component/form";
import { FormRow } from "component/form";
import lbryuri from "lbryuri"; import lbryuri from "lbryuri";
const WalletSend = props => { class WalletSend extends React.PureComponent {
const { sendToAddress, setAmount, setAddress, amount, address } = props; handleSubmit() {
const { amount, address, sendToAddress } = this.props;
const validSubmit = parseFloat(amount) > 0.0 && address;
if (validSubmit) {
sendToAddress();
}
}
render() {
const {
closeModal,
modal,
setAmount,
setAddress,
amount,
address,
error,
} = this.props;
return ( return (
<section className="card"> <section className="card">
<form onSubmit={sendToAddress}> <Form onSubmit={this.handleSubmit.bind(this)}>
<div className="card__title-primary"> <div className="card__title-primary">
<h3>{__("Send Credits")}</h3> <h3>{__("Send Credits")}</h3>
</div> </div>
@ -37,18 +54,16 @@ const WalletSend = props => {
trim={true} trim={true}
/> />
<div className="form-row-submit"> <div className="form-row-submit">
<Link <Submit
button="primary"
label={__("Send")} label={__("Send")}
onClick={sendToAddress}
disabled={!(parseFloat(amount) > 0.0) || !address} disabled={!(parseFloat(amount) > 0.0) || !address}
/> />
<input type="submit" className="hidden" />
</div> </div>
</div> </div>
</form> </Form>
</section> </section>
); );
}; }
}
export default WalletSend; export default WalletSend;

View file

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

View file

@ -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 (
<div>
<div className="card__title-primary">
<h1>{__("Support")} <UriIndicator uri={uri} /></h1>
</div>
<div className="card__content">
<FormRow
label={__("Amount")}
postfix={__("LBC")}
min="0"
step="0.1"
type="number"
errorMessage={errorMessage}
helper={
<span>
{__(
'This will appear as a tip for "%s" located at %s.',
title,
uri
) + " "}
<Link
label={__("Learn more")}
href="https://lbry.io/faq/tipping"
/>
</span>
}
placeholder="1.00"
onChange={event => this.handleSupportPriceChange(event)}
/>
<div className="form-row-submit">
<Link
label={__("Send")}
button="primary"
disabled={isPending}
onClick={this.handleSendButtonClicked.bind(this)}
/>
<Link
label={__("Cancel")}
button="alt"
navigate="/show"
navigateParams={{ uri }}
/>
</div>
</div>
</div>
);
}
}
export default WalletSendTip;

View file

@ -9,7 +9,6 @@ export const DAEMON_VERSION_MISMATCH = "DAEMON_VERSION_MISMATCH";
export const VOLUME_CHANGED = "VOLUME_CHANGED"; export const VOLUME_CHANGED = "VOLUME_CHANGED";
// Navigation // Navigation
export const CHANGE_PATH = "CHANGE_PATH";
export const CHANGE_AFTER_AUTH_PATH = "CHANGE_AFTER_AUTH_PATH"; export const CHANGE_AFTER_AUTH_PATH = "CHANGE_AFTER_AUTH_PATH";
export const WINDOW_SCROLLED = "WINDOW_SCROLLED"; export const WINDOW_SCROLLED = "WINDOW_SCROLLED";
export const HISTORY_NAVIGATE = "HISTORY_NAVIGATE"; 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_COMPLETED = "SEND_TRANSACTION_COMPLETED";
export const SEND_TRANSACTION_FAILED = "SEND_TRANSACTION_FAILED"; export const SEND_TRANSACTION_FAILED = "SEND_TRANSACTION_FAILED";
export const FETCH_BLOCK_SUCCESS = "FETCH_BLOCK_SUCCESS"; 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_STARTED = "FETCH_FEATURED_CONTENT_STARTED";
export const FETCH_FEATURED_CONTENT_COMPLETED = export const FETCH_FEATURED_CONTENT_COMPLETED =
"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_STARTED = "FETCH_CLAIM_LIST_MINE_STARTED";
export const FETCH_CLAIM_LIST_MINE_COMPLETED = export const FETCH_CLAIM_LIST_MINE_COMPLETED =
"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_STARTED = "FILE_LIST_STARTED";
export const FILE_LIST_SUCCEEDED = "FILE_LIST_SUCCEEDED"; export const FILE_LIST_SUCCEEDED = "FILE_LIST_SUCCEEDED";
export const FETCH_FILE_INFO_STARTED = "FETCH_FILE_INFO_STARTED"; 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_STARTED = "FETCH_AVAILABILITY_STARTED";
export const FETCH_AVAILABILITY_COMPLETED = "FETCH_AVAILABILITY_COMPLETED"; export const FETCH_AVAILABILITY_COMPLETED = "FETCH_AVAILABILITY_COMPLETED";
export const FILE_DELETE = "FILE_DELETE"; 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 // Search
export const SEARCH_STARTED = "SEARCH_STARTED"; 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 CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR";
export const FETCH_REWARD_CONTENT_COMPLETED = "FETCH_REWARD_CONTENT_COMPLETED"; 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 //Language
export const DOWNLOAD_LANGUAGE_SUCCEEDED = "DOWNLOAD_LANGUAGE_SUCCEEDED"; export const DOWNLOAD_LANGUAGE_SUCCEEDED = "DOWNLOAD_LANGUAGE_SUCCEEDED";
export const DOWNLOAD_LANGUAGE_FAILED = "DOWNLOAD_LANGUAGE_FAILED"; export const DOWNLOAD_LANGUAGE_FAILED = "DOWNLOAD_LANGUAGE_FAILED";

View file

@ -176,60 +176,6 @@ lbry.connect = function() {
return lbry._connectPromise; 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 * Publishes a file. The optional fileListedCallback is called when the file becomes available in
* lbry.file_list() during the publish process. * lbry.file_list() during the publish process.

View file

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doCloseModal } from "actions/app"; import { doCloseModal } from "actions/app";
import { doLoadVideo } from "actions/content"; import { doLoadVideo, doSetPlayingUri } from "actions/content";
import { makeSelectMetadataForUri } from "selectors/claims"; import { makeSelectMetadataForUri } from "selectors/claims";
import ModalAffirmPurchase from "./view"; import ModalAffirmPurchase from "./view";
@ -10,6 +10,10 @@ const select = (state, props) => ({
}); });
const perform = dispatch => ({ const perform = dispatch => ({
cancelPurchase: () => {
dispatch(doSetPlayingUri(null));
dispatch(doCloseModal());
},
closeModal: () => dispatch(doCloseModal()), closeModal: () => dispatch(doCloseModal()),
loadVideo: uri => dispatch(doLoadVideo(uri)), loadVideo: uri => dispatch(doLoadVideo(uri)),
}); });

View file

@ -9,7 +9,7 @@ class ModalAffirmPurchase extends React.PureComponent {
} }
render() { render() {
const { closeModal, metadata: { title }, uri } = this.props; const { cancelPurchase, metadata: { title }, uri } = this.props;
return ( return (
<Modal <Modal
@ -17,7 +17,7 @@ class ModalAffirmPurchase extends React.PureComponent {
isOpen={true} isOpen={true}
contentLabel={__("Confirm Purchase")} contentLabel={__("Confirm Purchase")}
onConfirmed={this.onAffirmPurchase.bind(this)} onConfirmed={this.onAffirmPurchase.bind(this)}
onAborted={closeModal} onAborted={cancelPurchase}
> >
{__("This will purchase")} <strong>{title}</strong> {__("for")}{" "} {__("This will purchase")} <strong>{title}</strong> {__("for")}{" "}
<strong> <strong>

View file

@ -2,16 +2,13 @@ import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doCloseModal } from "actions/app"; import { doCloseModal } from "actions/app";
import { doDeleteFileAndGoBack } from "actions/file_info"; import { doDeleteFileAndGoBack } from "actions/file_info";
import { import { makeSelectTitleForUri, makeSelectClaimIsMine } from "selectors/claims";
makeSelectMetadataForUri,
makeSelectClaimIsMine,
} from "selectors/claims";
import { makeSelectFileInfoForUri } from "selectors/file_info"; import { makeSelectFileInfoForUri } from "selectors/file_info";
import ModalRemoveFile from "./view"; import ModalRemoveFile from "./view";
const select = (state, props) => ({ const select = (state, props) => ({
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state), title: makeSelectTitleForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state),
}); });

View file

@ -30,7 +30,7 @@ class ModalRemoveFile extends React.PureComponent {
closeModal, closeModal,
deleteFile, deleteFile,
fileInfo: { outpoint }, fileInfo: { outpoint },
metadata: { title }, title,
} = this.props; } = this.props;
const { deleteChecked, abandonClaimChecked } = this.state; const { deleteChecked, abandonClaimChecked } = this.state;

View file

@ -9,18 +9,19 @@ import {
makeSelectClaimsInChannelForCurrentPage, makeSelectClaimsInChannelForCurrentPage,
makeSelectFetchingChannelClaims, makeSelectFetchingChannelClaims,
} from "selectors/claims"; } from "selectors/claims";
import { selectCurrentParams } from "selectors/navigation"; import {
makeSelectCurrentParam,
selectCurrentParams,
} from "selectors/navigation";
import { doNavigate } from "actions/navigation"; import { doNavigate } from "actions/navigation";
import { makeSelectTotalPagesForChannel } from "selectors/content"; import { makeSelectTotalPagesForChannel } from "selectors/content";
import ChannelPage from "./view"; import ChannelPage from "./view";
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
claimsInChannel: makeSelectClaimsInChannelForCurrentPage( claimsInChannel: makeSelectClaimsInChannelForCurrentPage(props.uri)(state),
props.uri,
props.page
)(state),
fetching: makeSelectFetchingChannelClaims(props.uri)(state), fetching: makeSelectFetchingChannelClaims(props.uri)(state),
page: makeSelectCurrentParam("page")(state),
params: selectCurrentParams(state), params: selectCurrentParams(state),
totalPages: makeSelectTotalPagesForChannel(props.uri)(state), totalPages: makeSelectTotalPagesForChannel(props.uri)(state),
}); });

View file

@ -2,7 +2,6 @@ import React from "react";
import lbryuri from "lbryuri"; import lbryuri from "lbryuri";
import { BusyMessage } from "component/common"; import { BusyMessage } from "component/common";
import FileTile from "component/fileTile"; import FileTile from "component/fileTile";
import Link from "component/link";
import ReactPaginate from "react-paginate"; import ReactPaginate from "react-paginate";
class ChannelPage extends React.PureComponent { class ChannelPage extends React.PureComponent {
@ -16,7 +15,7 @@ class ChannelPage extends React.PureComponent {
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const { page, uri, fetching, fetchClaims, fetchClaimCount } = this.props; 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); fetchClaims(nextProps.uri, nextProps.page);
} }
if (nextProps.uri != uri) { if (nextProps.uri != uri) {
@ -25,7 +24,7 @@ class ChannelPage extends React.PureComponent {
} }
changePage(pageNumber) { changePage(pageNumber) {
const { params, currentPage } = this.props; const { params } = this.props;
const newParams = Object.assign({}, params, { page: pageNumber }); const newParams = Object.assign({}, params, { page: pageNumber });
this.props.navigate("/show", newParams); this.props.navigate("/show", newParams);
@ -42,10 +41,10 @@ class ChannelPage extends React.PureComponent {
} = this.props; } = this.props;
let contentList; let contentList;
if (claimsInChannel === undefined) { if (fetching) {
contentList = <BusyMessage message={__("Fetching content")} />; contentList = <BusyMessage message={__("Fetching content")} />;
} else if (claimsInChannel) { } else {
contentList = claimsInChannel.length contentList = claimsInChannel && claimsInChannel.length
? claimsInChannel.map(claim => ? claimsInChannel.map(claim =>
<FileTile <FileTile
key={claim.claim_id} key={claim.claim_id}

View file

@ -215,11 +215,7 @@ class DiscoverPage extends React.PureComponent {
failedToLoad = !fetchingFeaturedUris && !hasContent; failedToLoad = !fetchingFeaturedUris && !hasContent;
return ( return (
<main <main className={hasContent && fetchingFeaturedUris ? "reloading" : null}>
className={
hasContent && fetchingFeaturedUris ? "main--refreshing" : null
}
>
{!hasContent && {!hasContent &&
fetchingFeaturedUris && fetchingFeaturedUris &&
<BusyMessage message={__("Fetching content")} />} <BusyMessage message={__("Fetching content")} />}

View file

@ -13,6 +13,7 @@ import {
import { makeSelectCostInfoForUri } from "selectors/cost_info"; import { makeSelectCostInfoForUri } from "selectors/cost_info";
import { selectShowNsfw } from "selectors/settings"; import { selectShowNsfw } from "selectors/settings";
import FilePage from "./view"; import FilePage from "./view";
import { makeSelectCurrentParam } from "selectors/navigation";
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
@ -20,6 +21,7 @@ const select = (state, props) => ({
costInfo: makeSelectCostInfoForUri(props.uri)(state), costInfo: makeSelectCostInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state),
obscureNsfw: !selectShowNsfw(state), obscureNsfw: !selectShowNsfw(state),
tab: makeSelectCurrentParam("tab")(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props), rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
}); });

View file

@ -1,55 +1,15 @@
import React from "react"; import React from "react";
import ReactMarkdown from "react-markdown";
import lbry from "lbry.js"; import lbry from "lbry.js";
import lbryuri from "lbryuri.js"; import lbryuri from "lbryuri.js";
import Video from "component/video"; import Video from "component/video";
import TipLink from "component/tipLink";
import { Thumbnail } from "component/common"; import { Thumbnail } from "component/common";
import FilePrice from "component/filePrice"; import FilePrice from "component/filePrice";
import FileActions from "component/fileActions"; import FileDetails from "component/fileDetails";
import Link from "component/link";
import UriIndicator from "component/uriIndicator"; import UriIndicator from "component/uriIndicator";
import IconFeatured from "component/iconFeatured"; import IconFeatured from "component/iconFeatured";
import DateTime from "component/dateTime"; import WalletSendTip from "component/walletSendTip";
const FormatItem = props => {
const {
publishedDate,
contentType,
claim: { height },
metadata: { language, license },
} = props;
const mediaType = lbry.getMediaType(contentType);
return (
<table className="table-standard table-stretch">
<tbody>
<tr>
<td>{__("Published on")}</td><td><DateTime block={height} /></td>
</tr>
<tr>
<td>{__("Content-Type")}</td><td>{mediaType}</td>
</tr>
<tr>
<td>{__("Language")}</td><td>{language}</td>
</tr>
<tr>
<td>{__("License")}</td><td>{license}</td>
</tr>
</tbody>
</table>
);
};
class FilePage extends React.PureComponent { class FilePage extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
showTipBox: false,
};
}
componentDidMount() { componentDidMount() {
this.fetchFileInfo(this.props); this.fetchFileInfo(this.props);
this.fetchCostInfo(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() { render() {
const { const {
claim, claim,
fileInfo, fileInfo,
metadata, metadata,
contentType, contentType,
tab,
uri, uri,
rewardedContentClaimIds, rewardedContentClaimIds,
} = this.props; } = this.props;
const { showTipBox } = this.state; const showTipBox = tab == "tip";
if (!claim || !metadata) { if (!claim || !metadata) {
return ( 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 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 = <UriIndicator uri={uri} />;
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id); const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
const mediaType = lbry.getMediaType(contentType); const mediaType = lbry.getMediaType(contentType);
const player = require("render-media"); const player = require("render-media");
@ -128,7 +60,7 @@ class FilePage extends React.PureComponent {
mediaType === "audio"; mediaType === "audio";
return ( return (
<main className="main--single-column"> <div>
<section className="show-page-media"> <section className="show-page-media">
{isPlayable {isPlayable
? <Video className="video-embedded" uri={uri} /> ? <Video className="video-embedded" uri={uri} />
@ -138,6 +70,9 @@ class FilePage extends React.PureComponent {
</section> </section>
<section className={"card " + (obscureNsfw ? "card--obscured " : "")}> <section className={"card " + (obscureNsfw ? "card--obscured " : "")}>
<div className="card__inner"> <div className="card__inner">
{(!tab || tab === "details") &&
<div>
{" "} {" "}
<div className="card__title-identity"> <div className="card__title-identity">
{!fileInfo || fileInfo.written_bytes <= 0 {!fileInfo || fileInfo.written_bytes <= 0
? <span style={{ float: "right" }}> ? <span style={{ float: "right" }}>
@ -147,55 +82,16 @@ class FilePage extends React.PureComponent {
: null} : null}
<h1>{title}</h1> <h1>{title}</h1>
<div className="card__subtitle"> <div className="card__subtitle">
{channelUri <UriIndicator uri={uri} link={true} />
? <Link
onClick={() =>
this.props.navigate("/show", { uri: channelUri })}
>
{uriIndicator}
</Link>
: uriIndicator}
</div> </div>
<FileActions
uri={uri}
onTipShow={this.handleTipShow.bind(this)}
/>
</div> </div>
{!showTipBox && <FileDetails uri={uri} />
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
<ReactMarkdown
source={(metadata && metadata.description) || ""}
escapeHtml={true}
disallowedTypes={["Heading", "HtmlInline", "HtmlBlock"]}
/>
</div>} </div>}
{tab === "tip" &&
<WalletSendTip claim_id={claim.claim_id} uri={uri} />}
</div> </div>
{metadata && claim && !showTipBox
? <div className="card__content">
<FormatItem
metadata={metadata}
contentType={contentType}
claim={claim}
/>
</div>
: ""}
{showTipBox
? <TipLink
onTipShow={this.handleTipShow.bind(this)}
onTipHide={this.handleTipHide.bind(this)}
claim_id={claim.claim_id}
/>
: ""}
{!showTipBox &&
<div className="card__content">
<Link
href={`https://lbry.io/dmca?claim_id=${claim.claim_id}`}
label={__("report")}
className="button-text-help"
/>
</div>}
</section> </section>
</main> </div>
); );
} }
} }

View file

@ -6,21 +6,27 @@ import SubHeader from "component/subHeader";
import Link from "component/link"; import Link from "component/link";
class RewardsPage extends React.PureComponent { class RewardsPage extends React.PureComponent {
componentDidMount() { /*
this.fetchRewards(this.props); Below is broken for users who have claimed all rewards.
}
componentWillReceiveProps(nextProps) { It can safely be disabled since we fetch all rewards after authentication, but should be re-enabled once fixed.
this.fetchRewards(nextProps);
}
fetchRewards(props) { */
const { fetching, rewards, fetchRewards } = props; // componentDidMount() {
// this.fetchRewards(this.props);
if (!fetching && (!rewards || !rewards.length)) { // }
fetchRewards(); //
} // componentWillReceiveProps(nextProps) {
} // this.fetchRewards(nextProps);
// }
//
// fetchRewards(props) {
// const { fetching, rewards, fetchRewards } = props;
//
// if (!fetching && (!rewards || !rewards.length)) {
// fetchRewards();
// }
// }
renderPageHeader() { renderPageHeader() {
const { doAuth, navigate, user } = this.props; const { doAuth, navigate, user } = this.props;

View file

@ -36,6 +36,7 @@ class ShowPage extends React.PureComponent {
message={__("Loading magic decentralized data...")} message={__("Loading magic decentralized data...")}
/>} />}
{claim === null && {claim === null &&
!isResolvingUri &&
<span className="empty"> <span className="empty">
{__("There's nothing at this location.")} {__("There's nothing at this location.")}
</span>} </span>}

View file

@ -10,18 +10,26 @@ class TransactionHistoryPage extends React.PureComponent {
render() { render() {
const { fetchingTransactions, transactions } = this.props; const { fetchingTransactions, transactions } = this.props;
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<SubHeader /> <SubHeader />
<section className="card"> <section className="card">
<div className="card__title-primary"> <div
className={
"card__title-primary " +
(fetchingTransactions && transactions.length ? "reloading" : "")
}
>
<h3>{__("Transaction History")}</h3> <h3>{__("Transaction History")}</h3>
</div> </div>
<div className="card__content"> <div className="card__content">
{fetchingTransactions && {fetchingTransactions && !transactions.length
<BusyMessage message={__("Loading transactions")} />} ? <BusyMessage message={__("Loading transactions")} />
{!fetchingTransactions && : ""}
<TransactionList transactions={transactions} />} {transactions && transactions.length
? <TransactionList transactions={transactions} />
: ""}
</div> </div>
</section> </section>
</main> </main>

View file

@ -1,6 +1,7 @@
import * as types from "constants/action_types"; import * as types from "constants/action_types";
const reducers = {}; const reducers = {};
const defaultState = {}; const defaultState = {};
reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) { reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
@ -49,7 +50,9 @@ reducers[types.FETCH_CLAIM_LIST_MINE_COMPLETED] = function(state, action) {
.filter(claimId => Object.keys(abandoningById).indexOf(claimId) === -1) .filter(claimId => Object.keys(abandoningById).indexOf(claimId) === -1)
); );
claims.filter(claim => claim.category.match(/claim/)).forEach(claim => { claims
.filter(claim => claim.category && claim.category.match(/claim/))
.forEach(claim => {
byId[claim.claim_id] = claim; byId[claim.claim_id] = claim;
const pending = Object.values(pendingById).find(pendingClaim => { const pending = Object.values(pendingById).find(pendingClaim => {
@ -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) { export default function reducer(state = defaultState, action) {
const handler = reducers[action.type]; const handler = reducers[action.type];
if (handler) return handler(state, action); if (handler) return handler(state, action);

View file

@ -2,6 +2,7 @@ import * as types from "constants/action_types";
const reducers = {}; const reducers = {};
const defaultState = { const defaultState = {
playingUri: null,
rewardedContentClaimIds: [], rewardedContentClaimIds: [],
channelPages: {}, 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) { // reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) {
// const channelPages = Object.assign({}, state.channelPages); // const channelPages = Object.assign({}, state.channelPages);
// const { uri, claims } = action.data; // 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 channelPages = Object.assign({}, state.channelPages);
const { uri, totalClaims } = action.data; const { uri, totalClaims } = action.data;
channelPages[uri] = totalClaims / 10; channelPages[uri] = Math.ceil(totalClaims / 10);
return Object.assign({}, state, { return Object.assign({}, state, {
channelPages, channelPages,

View file

@ -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) { reducers[types.CHANGE_AFTER_AUTH_PATH] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
pathAfterAuth: action.data.path, pathAfterAuth: action.data.path,
@ -38,15 +32,16 @@ reducers[types.CHANGE_AFTER_AUTH_PATH] = function(state, action) {
reducers[types.HISTORY_NAVIGATE] = (state, action) => { reducers[types.HISTORY_NAVIGATE] = (state, action) => {
const { stack, index } = state; const { stack, index } = state;
let newState = {};
const path = action.data.url; const path = action.data.url;
// Check for duplicated let newState = {
currentPath: path,
};
if (action.data.index >= 0) { if (action.data.index >= 0) {
newState.index = action.data.index; newState.index = action.data.index;
} else if (!stack[index] || stack[index].path !== path) { } else if (!stack[index] || stack[index].path !== path) {
// ^ Check for duplicated
newState.stack = [...stack.slice(0, index + 1), { path, scrollY: 0 }]; newState.stack = [...stack.slice(0, index + 1), { path, scrollY: 0 }];
newState.index = newState.stack.length - 1; newState.index = newState.stack.length - 1;
} }

View file

@ -1,5 +1,4 @@
import * as types from "constants/action_types"; import * as types from "constants/action_types";
import lbryuri from "lbryuri";
const reducers = {}; const reducers = {};
const defaultState = {}; const defaultState = {};
@ -9,7 +8,6 @@ reducers[types.SEARCH_STARTED] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
searching: true, searching: true,
query: query,
}); });
}; };
@ -31,7 +29,6 @@ reducers[types.SEARCH_COMPLETED] = function(state, action) {
reducers[types.SEARCH_CANCELLED] = function(state, action) { reducers[types.SEARCH_CANCELLED] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
searching: false, searching: false,
query: undefined,
}); });
}; };

View file

@ -10,11 +10,12 @@ const buildDraftTransaction = () => ({
const defaultState = { const defaultState = {
balance: undefined, balance: undefined,
blocks: {}, blocks: {},
transactions: [], transactions: {},
fetchingTransactions: false, fetchingTransactions: false,
receiveAddress: address, receiveAddress: address,
gettingNewAddress: false, gettingNewAddress: false,
draftTransaction: buildDraftTransaction(), draftTransaction: buildDraftTransaction(),
sendingSupport: false,
}; };
reducers[types.FETCH_TRANSACTIONS_STARTED] = function(state, action) { 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) { reducers[types.FETCH_TRANSACTIONS_COMPLETED] = function(state, action) {
const oldTransactions = Object.assign({}, state.transactions); let byId = Object.assign({}, state.transactions);
const byId = Object.assign({}, oldTransactions.byId);
const { transactions } = action.data; const { transactions } = action.data;
transactions.forEach(transaction => { transactions.forEach(transaction => {
byId[transaction.txid] = transaction; byId[transaction.txid] = transaction;
}); });
const newTransactions = Object.assign({}, oldTransactions, {
byId: byId,
});
return Object.assign({}, state, { return Object.assign({}, state, {
transactions: newTransactions, transactions: byId,
fetchingTransactions: false, 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) => { reducers[types.FETCH_BLOCK_SUCCESS] = (state, action) => {
const { block, block: { height } } = action.data, const { block, block: { height } } = action.data,
blocks = Object.assign({}, state.blocks); blocks = Object.assign({}, state.blocks);

View file

@ -1,6 +1,7 @@
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import { selectCurrentParams } from "selectors/navigation"; import { selectCurrentParams } from "selectors/navigation";
import lbryuri from "lbryuri"; import lbryuri from "lbryuri";
import { makeSelectCurrentParam } from "./navigation";
const _selectState = state => state.claims || {}; const _selectState = state => state.claims || {};
@ -66,19 +67,22 @@ export const selectAllFetchingChannelClaims = createSelector(
); );
export const makeSelectFetchingChannelClaims = uri => { export const makeSelectFetchingChannelClaims = uri => {
createSelector( return createSelector(
selectAllFetchingChannelClaims, selectAllFetchingChannelClaims,
fetching => fetching && fetching[uri] fetching => fetching && fetching[uri]
); );
}; };
export const makeSelectClaimsInChannelForCurrentPage = (uri, page = 1) => { export const makeSelectClaimsInChannelForCurrentPage = uri => {
const pageSelector = makeSelectCurrentParam("page");
return createSelector( return createSelector(
selectClaimsById, selectClaimsById,
selectAllClaimsByChannel, selectAllClaimsByChannel,
(byId, allClaims) => { pageSelector,
(byId, allClaims, page) => {
const byChannel = allClaims[uri] || {}; const byChannel = allClaims[uri] || {};
const claimIds = byChannel[page]; const claimIds = byChannel[page || 1];
if (!claimIds) return claimIds; 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 => { export const makeSelectContentTypeForUri = uri => {
return createSelector(makeSelectClaimForUri(uri), claim => { return createSelector(makeSelectClaimForUri(uri), claim => {
const source = const source =

View file

@ -17,6 +17,11 @@ export const selectResolvingUris = createSelector(
state => state.resolvingUris || [] state => state.resolvingUris || []
); );
export const selectPlayingUri = createSelector(
_selectState,
state => state.playingUri
);
export const makeSelectIsUriResolving = uri => { export const makeSelectIsUriResolving = uri => {
return createSelector( return createSelector(
selectResolvingUris, selectResolvingUris,

View file

@ -10,8 +10,11 @@ export const selectCurrentPath = createSelector(
state => state.currentPath state => state.currentPath
); );
export const computePageFromPath = path =>
path.replace(/^\//, "").split("?")[0];
export const selectCurrentPage = createSelector(selectCurrentPath, path => { export const selectCurrentPage = createSelector(selectCurrentPath, path => {
return path.replace(/^\//, "").split("?")[0]; return computePageFromPath(path);
}); });
export const selectCurrentParams = createSelector(selectCurrentPath, path => { export const selectCurrentParams = createSelector(selectCurrentPath, path => {
@ -87,7 +90,7 @@ export const selectPageTitle = createSelector(
case "start": case "start":
return __("Start"); return __("Start");
case "publish": case "publish":
return __("Publish"); return params.id ? __("Edit") : __("Publish");
case "help": case "help":
return __("Help"); return __("Help");
case "developer": case "developer":

View file

@ -19,6 +19,15 @@ export const selectClaimedRewards = createSelector(
byId => Object.values(byId) || [] 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( export const selectUnclaimedRewards = createSelector(
selectUnclaimedRewardsByType, selectUnclaimedRewardsByType,
byType => byType =>

View file

@ -1,11 +1,16 @@
import { createSelector } from "reselect"; 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 _selectState = state => state.search || {};
export const selectSearchQuery = createSelector( export const selectSearchQuery = createSelector(
_selectState, selectCurrentPage,
state => state.query selectCurrentParams,
(page, params) => (page === "search" ? params && params.query : null)
); );
export const selectIsSearching = createSelector( export const selectIsSearching = createSelector(
@ -36,7 +41,10 @@ export const selectWunderBarAddress = createSelector(
(page, title, query) => (page != "search" ? title : query ? query : title) (page, title, query) => (page != "search" ? title : query ? query : title)
); );
export const selectWunderBarIcon = createSelector(selectCurrentPage, page => { export const selectWunderBarIcon = createSelector(
selectCurrentPage,
selectCurrentParams,
(page, params) => {
switch (page) { switch (page) {
case "auth": case "auth":
return "icon-user"; return "icon-user";
@ -69,7 +77,7 @@ export const selectWunderBarIcon = createSelector(selectCurrentPage, page => {
case "show": case "show":
return "icon-file"; return "icon-file";
case "publish": case "publish":
return "icon-upload"; return params.id ? __("icon-pencil") : __("icon-upload");
case "developer": case "developer":
return "icon-code"; return "icon-code";
case "discover": case "discover":
@ -77,4 +85,5 @@ export const selectWunderBarIcon = createSelector(selectCurrentPage, page => {
default: default:
return "icon-file"; return "icon-file";
} }
}); }
);

View file

@ -7,34 +7,76 @@ export const selectBalance = createSelector(
state => state.balance state => state.balance
); );
export const selectTransactions = createSelector(
_selectState,
state => state.transactions || {}
);
export const selectTransactionsById = createSelector( export const selectTransactionsById = createSelector(
selectTransactions, _selectState,
transactions => transactions.byId || {} state => state.transactions
); );
export const selectTransactionItems = createSelector( export const selectTransactionItems = createSelector(
selectTransactionsById, selectTransactionsById,
byId => { byId => {
const transactionItems = []; const items = [];
const txids = Object.keys(byId);
txids.forEach(txid => { Object.keys(byId).forEach(txid => {
const tx = byId[txid]; const tx = byId[txid];
transactionItems.push({
id: txid, //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, date: tx.timestamp ? new Date(parseInt(tx.timestamp) * 1000) : null,
amount: parseFloat(tx.value), amount: amount,
claim_info: tx.claim_info, fee: amount < 0 ? -1 * tx.fee / append.length : 0,
support_info: tx.support_info, claim_id: item.claim_id,
update_info: tx.update_info, claim_name: item.claim_name,
fee: tx.fee, type: item.type || "send",
nout: item.nout,
};
})
);
}); });
}); return items.reverse();
return transactionItems.reverse();
} }
); );
@ -61,6 +103,11 @@ export const selectIsFetchingTransactions = createSelector(
state => state.fetchingTransactions state => state.fetchingTransactions
); );
export const selectIsSendingSupport = createSelector(
_selectState,
state => state.sendingSupport
);
export const selectReceiveAddress = createSelector( export const selectReceiveAddress = createSelector(
_selectState, _selectState,
state => state.receiveAddress state => state.receiveAddress

View file

@ -1,6 +1,6 @@
{ {
"name": "lbry-web-ui", "name": "lbry-web-ui",
"version": "0.15.1", "version": "0.16.0",
"description": "LBRY UI", "description": "LBRY UI",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",

View file

@ -3,6 +3,7 @@ html
height: 100%; height: 100%;
font-size: var(--font-size); font-size: var(--font-size);
} }
body body
{ {
color: var(--text-color); color: var(--text-color);
@ -27,6 +28,11 @@ body
font-weight: bold; font-weight: bold;
color: var(--color-money); color: var(--color-money);
} }
.credit-amount--fee
{
font-size: 0.9em;
color: var(--color-meta-light);
}
#main-content #main-content
{ {
@ -50,7 +56,8 @@ body
width: $width-page-constrained; width: $width-page-constrained;
} }
} }
main.main--refreshing {
.reloading {
&:before { &:before {
$width: 30px; $width: 30px;
position: absolute; position: absolute;

View file

@ -1656,3 +1656,9 @@
.icon-medium:before { .icon-medium:before {
content: "\f23a"; content: "\f23a";
} }
.icon-address-book:before {
content: "\f2b9";
}
.icon-envelope-open:before {
content: "\f2b6";
}

View file

@ -90,8 +90,8 @@ $text-color: #000;
/* Tabs */ /* Tabs */
--tab-bg: transparent; --tab-bg: transparent;
--tab-color: #666; --tab-color: rgba(0, 0, 0, 0.5);
--tab-active-color: var(--header-active-color); --tab-active-color: var(--color-primary);
--tab-border-size: 2px; --tab-border-size: 2px;
--tab-border: var(--tab-border-size) solid var(--tab-active-color); --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-hover-bg: rgba(0, 0, 0, 0.35);
--scrollbar-thumb-active-bg: var(--color-primary); --scrollbar-thumb-active-bg: var(--color-primary);
--scrollbar-track-bg: transparent; --scrollbar-track-bg: transparent;
/* Divider */
--divider: 1px solid rgba(0, 0, 0, 0.12);
} }

View file

@ -22,5 +22,6 @@
@import "component/_pagination.scss"; @import "component/_pagination.scss";
@import "component/_markdown-editor.scss"; @import "component/_markdown-editor.scss";
@import "component/_scrollbar.scss"; @import "component/_scrollbar.scss";
@import "component/_tabs.scss";
@import "page/_developer.scss"; @import "page/_developer.scss";
@import "page/_show.scss"; @import "page/_show.scss";

View file

@ -86,3 +86,9 @@ $button-focus-shift: 12%;
{ {
box-shadow: none !important; box-shadow: none !important;
} }
.button--submit {
font-family: inherit;
font-size: inherit;
line-height: 0;
}

View file

@ -7,6 +7,9 @@
border-radius: var(--card-radius); border-radius: var(--card-radius);
margin-bottom: var(--card-margin); margin-bottom: var(--card-margin);
overflow: auto; 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 .card--obscured
{ {
@ -35,6 +38,11 @@
margin-top: var(--card-margin); margin-top: var(--card-margin);
margin-bottom: var(--card-margin); margin-bottom: var(--card-margin);
} }
.card__title-primary .meta {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card__title-identity { .card__title-identity {
margin-top: $spacing-vertical * 1/3; margin-top: $spacing-vertical * 1/3;
margin-bottom: $spacing-vertical * 1/3; margin-bottom: $spacing-vertical * 1/3;
@ -57,6 +65,9 @@
.card__content { .card__content {
margin-top: var(--card-margin); margin-top: var(--card-margin);
margin-bottom: var(--card-margin); margin-bottom: var(--card-margin);
table:not(:last-child) {
margin-bottom: var(--card-margin);
}
} }
$font-size-subtext-multiple: 0.82; $font-size-subtext-multiple: 0.82;
.card__subtext { .card__subtext {

View file

@ -56,38 +56,3 @@
border-color: var(--color-primary); 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);
}
}
}

View file

@ -60,3 +60,11 @@ table.table-standard {
table.table-stretch { table.table-stretch {
width: 100%; 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%; }
}

View file

@ -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%;}
}

11
ui/watch.bat Normal file
View file

@ -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