Merge branch 'v16'

This commit is contained in:
Jeremy Kauffman 2017-09-18 17:48:39 -04:00
commit 76352161a5
124 changed files with 2153 additions and 1594 deletions

View file

@ -8,15 +8,19 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased]
### 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.
### 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
*
### Deprecated

View file

@ -402,4 +402,4 @@ ipcMain.on('get-auth-token', (event) => {
ipcMain.on('set-auth-token', (event, token) => {
keytar.setPassword("LBRY", "auth_token", token ? token.toString().trim() : null);
});
});

View file

@ -20,8 +20,8 @@
"electron-rebuild": "^1.5.11"
},
"lbrySettings": {
"lbrynetDaemonVersion": "0.15.2",
"lbrynetDaemonVersion": "0.16.0",
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-daemon-vDAEMONVER-OSNAME.zip"
},
"license": "MIT"
}
}

View file

@ -21,7 +21,7 @@ fi
if $OSX; then
ICON="$BUILD_DIR/icon.icns"
else
ICON="$BUILD_DIR/icons/lbry48.png"
ICON="$BUILD_DIR/icons/48x48.png"
fi
FULL_BUILD="${FULL_BUILD:-false}"

1
ui/dist/quit.html vendored
View file

@ -6,7 +6,6 @@
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600italic,600' rel='stylesheet' type='text/css'>
<link href="./css/all.css" rel="stylesheet" type="text/css" media="screen,print" />
<link href="./js/mediaelement/mediaelementplayer.css" rel="stylesheet" type="text/css" />
<link rel="icon" type="image/png" href="./img/fav/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="./img/fav/favicon-194x194.png" sizes="194x194">
<link rel="icon" type="image/png" href="./img/fav/favicon-96x96.png" sizes="96x96">

46
ui/dist/themes/dark.css vendored Normal file
View file

@ -0,0 +1,46 @@
:root {
/* Colors */
--color-primary: #009688;
--color-canvas: #0f1517;
--color-bg: #1a2327;
--color-bg-alt: #314048;
--color-help: #AAA;
--color-error: #a94442;
--color-load-screen-text: #FFF;
--color-money: var(--color-primary);
--color-meta-light: #757575;
--color-dark-overlay: rgba(15, 21, 23, 0.85);
/* Text */
--text-color: #FFF;
--text-selection-bg: rgba(0,150,136, 0.95);
/* Input */
--input-bg: transparent;
--input-active-bg: rgba(0,0,0, 0.5);
--input-border-color: rgba(255,255,255, 0.25);
/* Search */
--search-bg: rgba(0,0,0, 0.45);
--search-color: #757575;
--search-active-bg: rgba(0,0,0, 0.75);
--search-border: 1px solid rgba(0,0,0, 0.25);
/* Tab */
--tab-color: #757575;
--tab-active-color: #CCC;
/* Header */
--header-color: #CCC;
--header-active-color: #FFF;
--header-button-bg: transparent;
/* Table */
--table-border: 0;
--table-item-even: var(--color-bg-alt);
--table-item-odd: transparent;
/* Modla */
--modal-overlay-bg: var(--color-dark-overlay);
--modal-border: 1px solid rgba(0, 0, 0, 0.25);
}

4
ui/dist/themes/light.css vendored Normal file
View file

@ -0,0 +1,4 @@
:root {
/* Colors */
--color-primary: #155B4A;
}

View file

@ -1,4 +1,5 @@
import * as types from "constants/action_types";
import * as settings from "constants/settings";
import lbry from "lbry";
import {
selectUpdateUrl,
@ -8,7 +9,7 @@ import {
} from "selectors/app";
import { doFetchDaemonSettings } from "actions/settings";
import { doAuthenticate } from "actions/user";
import { doFileList } from "actions/file_info";
import { doFetchFileInfosAndPublishedClaims } from "actions/file_info";
const { remote, ipcRenderer, shell } = require("electron");
const path = require("path");
@ -16,11 +17,12 @@ const { download } = remote.require("electron-dl");
const fs = remote.require("fs");
const { lbrySettings: config } = require("../../../app/package.json");
export function doOpenModal(modal) {
export function doOpenModal(modal, modalProps = {}) {
return {
type: types.OPEN_MODAL,
data: {
modal,
modalProps,
},
};
}
@ -165,7 +167,7 @@ export function doAlertError(errorList) {
type: types.OPEN_MODAL,
data: {
modal: "error",
extraContent: errorList,
modalProps: { error: errorList },
},
});
};
@ -176,7 +178,7 @@ export function doDaemonReady() {
dispatch(doAuthenticate());
dispatch({ type: types.DAEMON_READY });
dispatch(doFetchDaemonSettings());
dispatch(doFileList());
dispatch(doFetchFileInfosAndPublishedClaims());
};
}

View file

@ -4,11 +4,11 @@ import lbryio from "lbryio";
import lbryuri from "lbryuri";
import { selectBalance } from "selectors/wallet";
import {
selectFileInfoForUri,
makeSelectFileInfoForUri,
selectDownloadingByOutpoint,
} from "selectors/file_info";
import { selectResolvingUris } from "selectors/content";
import { selectCostInfoForUri } from "selectors/cost_info";
import { makeSelectCostInfoForUri } from "selectors/cost_info";
import { doAlertError, doOpenModal } from "actions/app";
import { doClaimEligiblePurchaseRewards } from "actions/rewards";
import { selectBadgeNumber } from "selectors/app";
@ -299,13 +299,12 @@ export function doLoadVideo(uri) {
data: { uri },
});
dispatch(doOpenModal("timedOut"));
dispatch(doOpenModal(modals.FILE_TIMEOUT, { uri }));
} else {
dispatch(doDownloadFile(uri, streamInfo));
}
})
.catch(error => {
console.log(error);
dispatch({
type: types.LOADING_VIDEO_FAILED,
data: { uri },
@ -315,46 +314,37 @@ export function doLoadVideo(uri) {
};
}
export function doPurchaseUri(uri, purchaseModalName) {
export function doPurchaseUri(uri) {
return function(dispatch, getState) {
const state = getState();
const balance = selectBalance(state);
const fileInfo = selectFileInfoForUri(state, { uri });
const fileInfo = makeSelectFileInfoForUri(uri)(state);
const downloadingByOutpoint = selectDownloadingByOutpoint(state);
const alreadyDownloading =
fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
// we already fully downloaded the file.
if (fileInfo && fileInfo.completed) {
// If written_bytes is false that means the user has deleted/moved the
// file manually on their file system, so we need to dispatch a
// doLoadVideo action to reconstruct the file from the blobs
if (!fileInfo.written_bytes) dispatch(doLoadVideo(uri));
return Promise.resolve();
}
// we are already downloading the file
if (alreadyDownloading) {
return Promise.resolve();
}
const costInfo = selectCostInfoForUri(state, { uri });
const costInfo = makeSelectCostInfoForUri(uri)(state);
const { cost } = costInfo;
// the file is free or we have partially downloaded it
if (cost === 0 || (fileInfo && fileInfo.download_directory)) {
dispatch(doLoadVideo(uri));
return Promise.resolve();
if (
alreadyDownloading ||
(fileInfo && fileInfo.completed && fileInfo.written_bytes > 0)
) {
return;
}
// we already fully downloaded the file.
if (
cost === 0 ||
(fileInfo && (fileInfo.completed || fileInfo.download_directory))
) {
return dispatch(doLoadVideo(uri));
}
if (cost > balance) {
dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS));
} else {
dispatch(doOpenModal(purchaseModalName));
return dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS));
}
return Promise.resolve();
return dispatch(doOpenModal(modals.AFFIRM_PURCHASE, { uri }));
};
}
@ -421,6 +411,22 @@ export function doFetchClaimListMine() {
};
}
export function doPlayUri(uri) {
return function(dispatch, getState) {
dispatch(doSetPlayingUri(uri));
dispatch(doPurchaseUri(uri));
};
}
export function doSetPlayingUri(uri) {
return function(dispatch, getState) {
dispatch({
type: types.SET_PLAYING_URI,
data: { uri },
});
};
}
export function doFetchChannelListMine() {
return function(dispatch, getState) {
dispatch({

View file

@ -13,7 +13,7 @@ import {
selectTotalDownloadProgress,
} from "selectors/file_info";
import { doCloseModal } from "actions/app";
import { doHistoryBack } from "actions/navigation";
import { doNavigate, doHistoryBack } from "actions/navigation";
import setProgressBar from "util/setProgressBar";
import batchActions from "util/batchActions";

View file

@ -1,5 +1,6 @@
import * as types from "constants/action_types";
import {
computePageFromPath,
selectPageTitle,
selectCurrentPage,
selectCurrentParams,
@ -20,9 +21,17 @@ export function doNavigate(path, params = {}, options = {}) {
url += "?" + toQueryString(params);
}
dispatch(doChangePath(url, options));
const state = getState(),
currentPage = selectCurrentPage(state),
nextPage = computePageFromPath(path),
scrollY = options.scrollY;
const pageTitle = selectPageTitle(getState()) + " - LBRY";
if (currentPage != nextPage) {
//I wasn't seeing it scroll to the proper position without this -- possibly because the page isn't fully rendered? Not sure - Jeremy
setTimeout(() => {
window.scrollTo(0, scrollY ? scrollY : 0);
}, 100);
}
dispatch({
type: types.HISTORY_NAVIGATE,
@ -45,31 +54,6 @@ export function doAuthNavigate(pathAfterAuth = null, params = {}) {
};
}
export function doChangePath(path, options = {}) {
return function(dispatch, getState) {
dispatch({
type: types.CHANGE_PATH,
data: {
path,
},
});
const state = getState();
const scrollY = options.scrollY;
//I wasn't seeing it scroll to the proper position without this -- possibly because the page isn't fully rendered? Not sure - Jeremy
setTimeout(() => {
window.scrollTo(0, scrollY ? scrollY : 0);
}, 100);
const currentPage = selectCurrentPage(state);
if (currentPage === "search") {
const params = selectCurrentParams(state);
dispatch(doSearch(params.query));
}
};
}
export function doHistoryTraverse(dispatch, state, modifier) {
const stack = selectHistoryStack(state),
index = selectHistoryIndex(state) + modifier;

View file

@ -1,10 +1,16 @@
import * as types from "constants/action_types";
import * as settings from "constants/settings";
import { doAlertError } from "actions/app";
import batchActions from "util/batchActions";
import lbry from "lbry";
import fs from "fs";
import http from "http";
const { remote } = require("electron");
const { extname } = require("path");
const { readdir } = remote.require("fs");
export function doFetchDaemonSettings() {
return function(dispatch, getState) {
lbry.settings_get().then(settings => {
@ -46,6 +52,27 @@ export function doSetClientSetting(key, value) {
};
}
export function doGetThemes() {
return function(dispatch, getState) {
const dir = `${remote.app.getAppPath()}/dist/themes`;
readdir(dir, (error, files) => {
if (!error) {
dispatch(
doSetClientSetting(
settings.THEMES,
files
.filter(file => extname(file) === ".css")
.map(file => file.replace(".css", ""))
)
);
} else {
dispatch(doAlertError(error));
}
});
};
}
export function doDownloadLanguage(langFile) {
return function(dispatch, getState) {
const destinationPath = app.i18n.directory + "/" + langFile;
@ -137,7 +164,7 @@ export function doDownloadLanguages() {
export function doChangeLanguage(language) {
return function(dispatch, getState) {
lbry.setClientSetting(settings.LANGUAGE, language);
dispatch(doSetClientSetting(settings.LANGUAGE, language));
app.i18n.setLocale(language);
};
}

View file

@ -5,7 +5,9 @@ import {
selectDraftTransactionAmount,
selectBalance,
} from "selectors/wallet";
import { doOpenModal } from "actions/app";
import { doOpenModal, doShowSnackBar } from "actions/app";
import { doNavigate } from "actions/navigation";
import * as modals from "constants/modal_types";
export function doUpdateBalance(balance) {
return {
@ -22,7 +24,7 @@ export function doFetchTransactions() {
type: types.FETCH_TRANSACTIONS_STARTED,
});
lbry.transaction_list().then(results => {
lbry.transaction_list({ include_tip_info: true }).then(results => {
dispatch({
type: types.FETCH_TRANSACTIONS_COMPLETED,
data: {
@ -83,8 +85,8 @@ export function doSendDraftTransaction() {
const balance = selectBalance(state);
const amount = selectDraftTransactionAmount(state);
if (balance - amount < 1) {
return dispatch(doOpenModal("insufficientBalance"));
if (balance - amount <= 0) {
return dispatch(doOpenModal(modals.INSUFFICIENT_BALANCE));
}
dispatch({
@ -96,13 +98,19 @@ export function doSendDraftTransaction() {
dispatch({
type: types.SEND_TRANSACTION_COMPLETED,
});
dispatch(doOpenModal("transactionSuccessful"));
dispatch(
doShowSnackBar({
message: __(`You sent ${amount} LBC`),
linkText: __("History"),
linkTarget: __("/wallet"),
})
);
} else {
dispatch({
type: types.SEND_TRANSACTION_FAILED,
data: { error: results },
});
dispatch(doOpenModal("transactionFailed"));
dispatch(doOpenModal(modals.TRANSACTION_FAILED));
}
};
@ -111,7 +119,7 @@ export function doSendDraftTransaction() {
type: types.SEND_TRANSACTION_FAILED,
data: { error: error.message },
});
dispatch(doOpenModal("transactionFailed"));
dispatch(doOpenModal(modals.TRANSACTION_FAILED));
};
lbry
@ -136,3 +144,55 @@ export function doSetDraftTransactionAddress(address) {
data: { address },
};
}
export function doSendSupport(amount, claim_id, uri) {
return function(dispatch, getState) {
const state = getState();
const balance = selectBalance(state);
if (balance - amount <= 0) {
return dispatch(doOpenModal(modals.INSUFFICIENT_BALANCE));
}
dispatch({
type: types.SUPPORT_TRANSACTION_STARTED,
});
const successCallback = results => {
if (results.txid) {
dispatch({
type: types.SUPPORT_TRANSACTION_COMPLETED,
});
dispatch(
doShowSnackBar({
message: __(`You sent ${amount} LBC as support, Mahalo!`),
linkText: __("History"),
linkTarget: __("/wallet"),
})
);
dispatch(doNavigate("/show", { uri }));
} else {
dispatch({
type: types.SUPPORT_TRANSACTION_FAILED,
data: { error: results.code },
});
dispatch(doOpenModal(modals.TRANSACTION_FAILED));
}
};
const errorCallback = error => {
dispatch({
type: types.SUPPORT_TRANSACTION_FAILED,
data: { error: error.code },
});
dispatch(doOpenModal(modals.TRANSACTION_FAILED));
};
lbry
.wallet_send({
claim_id: claim_id,
amount: amount,
})
.then(successCallback, errorCallback);
};
}

View file

@ -1,19 +1,14 @@
import store from "store.js";
import lbry from "./lbry.js";
import { remote } from "electron";
import * as settings from "constants/settings";
const env = ENV;
const config = {
...require(`./config/${env}`),
};
const language = lbry.getClientSetting(settings.LANGUAGE)
? lbry.getClientSetting(settings.LANGUAGE)
: "en";
const i18n = require("y18n")({
directory: remote.app.getAppPath() + "/locales",
updateFiles: false,
locale: language,
locale: "en",
});
const logs = [];
const app = {

View file

@ -2,10 +2,7 @@ import React from "react";
import { connect } from "react-redux";
import { selectPageTitle } from "selectors/navigation";
import { selectUser } from "selectors/user";
import {
doCheckUpgradeAvailable,
doAlertError,
} from "actions/app";
import { doCheckUpgradeAvailable, doAlertError } from "actions/app";
import { doRecordScroll } from "actions/navigation";
import { doFetchRewardedContent } from "actions/content";
import { doUpdateBalance } from "actions/wallet";

View file

@ -1,6 +1,7 @@
import React from "react";
import Router from "component/router/index";
import Header from "component/header";
import Theme from "component/theme";
import ModalRouter from "modal/modalRouter";
import lbry from "lbry";
@ -49,6 +50,7 @@ class App extends React.PureComponent {
render() {
return (
<div id="window">
<Theme />
<Header />
<div id="main-content">
<Router />

View file

@ -70,7 +70,7 @@ export class CreditAmount extends React.PureComponent {
showFree: React.PropTypes.bool,
showFullPrice: React.PropTypes.bool,
showPlus: React.PropTypes.bool,
look: React.PropTypes.oneOf(["indicator", "plain"]),
look: React.PropTypes.oneOf(["indicator", "plain", "fee"]),
};
static defaultProps = {
@ -78,7 +78,6 @@ export class CreditAmount extends React.PureComponent {
label: true,
showFree: false,
look: "indicator",
showFree: false,
showFullPrice: false,
showPlus: false,
};

View file

@ -1,6 +1,10 @@
import React from "react";
class DateTime extends React.PureComponent {
static SHOW_DATE = "date";
static SHOW_TIME = "time";
static SHOW_BOTH = "both";
componentWillMount() {
this.refreshDate(this.props);
}
@ -17,9 +21,20 @@ class DateTime extends React.PureComponent {
}
render() {
const { date } = this.props;
const { date, formatOptions } = this.props;
const show = this.props.show || DateTime.SHOW_BOTH;
return <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,52 +1,20 @@
import React from "react";
import { connect } from "react-redux";
import { selectPlatform, selectCurrentModal } from "selectors/app";
import {
makeSelectFileInfoForUri,
makeSelectDownloadingForUri,
makeSelectLoadingForUri,
} from "selectors/file_info";
import { makeSelectIsAvailableForUri } from "selectors/availability";
import { makeSelectFileInfoForUri } from "selectors/file_info";
import { makeSelectCostInfoForUri } from "selectors/cost_info";
import { doCloseModal, doOpenModal } from "actions/app";
import { doFetchAvailability } from "actions/availability";
import { doOpenFileInShell, doOpenFileInFolder } from "actions/file_info";
import { makeSelectClaimForUriIsMine } from "selectors/claims";
import { doPurchaseUri, doLoadVideo, doStartDownload } from "actions/content";
import { doOpenModal } from "actions/app";
import { makeSelectClaimIsMine } from "selectors/claims";
import FileActions from "./view";
const makeSelect = () => {
const selectFileInfoForUri = makeSelectFileInfoForUri();
const selectIsAvailableForUri = makeSelectIsAvailableForUri();
const selectDownloadingForUri = makeSelectDownloadingForUri();
const selectCostInfoForUri = makeSelectCostInfoForUri();
const selectLoadingForUri = makeSelectLoadingForUri();
const selectClaimForUriIsMine = makeSelectClaimForUriIsMine();
const select = (state, props) => ({
fileInfo: selectFileInfoForUri(state, props),
/*availability check is disabled due to poor performance, TBD if it dies forever or requires daemon fix*/
isAvailable: true, //selectIsAvailableForUri(state, props),
platform: selectPlatform(state),
modal: selectCurrentModal(state),
downloading: selectDownloadingForUri(state, props),
costInfo: selectCostInfoForUri(state, props),
loading: selectLoadingForUri(state, props),
claimIsMine: selectClaimForUriIsMine(state, props),
});
return select;
};
const perform = dispatch => ({
checkAvailability: uri => dispatch(doFetchAvailability(uri)),
closeModal: () => dispatch(doCloseModal()),
openInFolder: fileInfo => dispatch(doOpenFileInFolder(fileInfo)),
openInShell: fileInfo => dispatch(doOpenFileInShell(fileInfo)),
openModal: modal => dispatch(doOpenModal(modal)),
startDownload: uri => dispatch(doPurchaseUri(uri, "affirmPurchase")),
loadVideo: uri => dispatch(doLoadVideo(uri)),
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
const select = (state, props) => ({
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
/*availability check is disabled due to poor performance, TBD if it dies forever or requires daemon fix*/
costInfo: makeSelectCostInfoForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
});
export default connect(makeSelect, perform)(FileActions);
const perform = dispatch => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
});
export default connect(select, perform)(FileActions);

View file

@ -1,213 +1,40 @@
import React from "react";
import { Icon, BusyMessage } from "component/common";
import FilePrice from "component/filePrice";
import { Modal } from "modal/modal";
import Link from "component/link";
import { ToolTip } from "component/tooltip";
import { DropDownMenu, DropDownMenuItem } from "component/menu";
import ModalRemoveFile from "modal/modalRemoveFile";
import FileDownloadLink from "component/fileDownloadLink";
import * as modals from "constants/modal_types";
class FileActions extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
forceShowActions: false,
};
}
componentWillMount() {
this.checkAvailability(this.props.uri);
}
componentWillReceiveProps(nextProps) {
this.checkAvailability(nextProps.uri);
this.restartDownload(nextProps);
}
restartDownload(props) {
const { downloading, fileInfo, uri, restartDownload } = props;
if (
!downloading &&
fileInfo &&
!fileInfo.completed &&
fileInfo.written_bytes !== false &&
fileInfo.written_bytes < fileInfo.total_bytes
) {
restartDownload(uri, fileInfo.outpoint);
}
}
checkAvailability(uri) {
if (!this._uri || uri !== this._uri) {
this._uri = uri;
this.props.checkAvailability(uri);
}
}
onShowFileActionsRowClicked() {
this.setState({
forceShowActions: true,
});
}
onAffirmPurchase() {
this.props.closeModal();
this.props.loadVideo(this.props.uri);
}
render() {
const {
fileInfo,
isAvailable,
platform,
downloading,
uri,
openInFolder,
openInShell,
modal,
openModal,
closeModal,
startDownload,
costInfo,
loading,
claimIsMine,
} = this.props;
const { fileInfo, uri, openModal, claimIsMine } = this.props;
const metadata = fileInfo ? fileInfo.metadata : null,
openInFolderMessage = platform.startsWith("Mac")
? __("Open in Finder")
: __("Open in Folder"),
showMenu = fileInfo && Object.keys(fileInfo).length > 0,
title = metadata ? metadata.title : uri;
let content;
if (loading || downloading) {
const progress = fileInfo && fileInfo.written_bytes
? fileInfo.written_bytes / fileInfo.total_bytes * 100
: 0,
label = fileInfo
? progress.toFixed(0) + __("% complete")
: __("Connecting..."),
labelWithIcon = (
<span className="button__content">
<Icon icon="icon-download" />
<span>
{label}
</span>
</span>
);
content = (
<div className="faux-button-block file-actions__download-status-bar button-set-item">
<div
className="faux-button-block file-actions__download-status-bar-overlay"
style={{ width: progress + "%" }}
>
{labelWithIcon}
</div>
{labelWithIcon}
</div>
);
} else if (!fileInfo && isAvailable === undefined) {
content = <BusyMessage message={__("Checking availability")} />;
} else if (!fileInfo && !isAvailable && !this.state.forceShowActions) {
content = (
<div>
<div className="button-set-item empty">
{__("Content unavailable.")}
</div>
<ToolTip
label={__("Why?")}
body={__(
"The content on LBRY is hosted by its users. It appears there are no users connected that have this file at the moment."
)}
className="button-set-item"
/>
<Link
label={__("Try Anyway")}
onClick={this.onShowFileActionsRowClicked.bind(this)}
className="button-text button-set-item"
/>
</div>
);
} else if (fileInfo === null && !downloading) {
if (!costInfo) {
content = <BusyMessage message={__("Fetching cost info")} />;
} else {
content = (
<Link
button="text"
label={__("Download")}
icon="icon-download"
onClick={() => {
startDownload(uri);
}}
/>
);
}
} else if (fileInfo && fileInfo.download_path) {
content = (
<Link
label={__("Open")}
button="text"
icon="icon-folder-open"
onClick={() => openInShell(fileInfo)}
/>
);
} else if (!fileInfo) {
content = <BusyMessage message={__("Fetching file info")} />;
} else {
console.log("handle this case of file action props?");
}
const claimId = fileInfo ? fileInfo.claim_id : null,
showDelete = fileInfo && Object.keys(fileInfo).length > 0;
return (
<section className="file-actions">
{content}
{showMenu
? <div className="button-set-item">
<DropDownMenu>
<DropDownMenuItem
key={0}
onClick={() => openInFolder(fileInfo)}
label={openInFolderMessage}
/>
<DropDownMenuItem
key={1}
onClick={() => openModal(modals.CONFIRM_FILE_REMOVE)}
label={__("Remove...")}
/>
</DropDownMenu>
</div>
: ""}
<Modal
type="confirm"
isOpen={modal == "affirmPurchase"}
contentLabel={__("Confirm Purchase")}
onConfirmed={this.onAffirmPurchase.bind(this)}
onAborted={closeModal}
>
{__("This will purchase")} <strong>{title}</strong> {__("for")}{" "}
<strong>
<FilePrice uri={uri} showFullPrice={true} look="plain" />
</strong>{" "}
{__("credits")}.
</Modal>
<Modal
isOpen={modal == "timedOut"}
contentLabel={__("Download failed")}
onConfirmed={closeModal}
>
{__("LBRY was unable to download the stream")}{" "}{" "}
<strong>{title}</strong>.
</Modal>
{modal == modals.CONFIRM_FILE_REMOVE &&
<ModalRemoveFile
uri={uri}
outpoint={fileInfo.outpoint}
title={title}
<section className="card__actions">
{claimIsMine &&
<Link
button="text"
icon="icon-edit"
label={__("Edit")}
navigate="/publish"
navigateParams={{ id: claimId }}
/>}
<FileDownloadLink uri={uri} />
<Link
button="text"
icon="icon-gift"
label={__("Support")}
navigate="/show"
navigateParams={{ uri, tab: "tip" }}
/>
{showDelete &&
<Link
button="text"
icon="icon-trash"
label={__("Remove")}
className="card__action--right"
onClick={() => openModal(modals.CONFIRM_FILE_REMOVE, { uri })}
/>}
</section>
);

View file

@ -9,32 +9,23 @@ import {
} from "selectors/claims";
import { makeSelectFileInfoForUri } from "selectors/file_info";
import {
makeSelectIsResolvingForUri,
makeSelectIsUriResolving,
selectRewardContentClaimIds,
} from "selectors/content";
import FileCard from "./view";
const makeSelect = () => {
const selectClaimForUri = makeSelectClaimForUri();
const selectFileInfoForUri = makeSelectFileInfoForUri();
const selectMetadataForUri = makeSelectMetadataForUri();
const selectResolvingUri = makeSelectIsResolvingForUri();
const select = (state, props) => ({
claim: selectClaimForUri(state, props),
fileInfo: selectFileInfoForUri(state, props),
obscureNsfw: !selectShowNsfw(state),
metadata: selectMetadataForUri(state, props),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
isResolvingUri: selectResolvingUri(state, props),
});
return select;
};
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
});
const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)),
resolveUri: uri => dispatch(doResolveUri(uri)),
});
export default connect(makeSelect, perform)(FileCard);
export default connect(select, perform)(FileCard);

View file

@ -0,0 +1,16 @@
import React from "react";
import { connect } from "react-redux";
import {
makeSelectClaimForUri,
makeSelectContentTypeForUri,
makeSelectMetadataForUri,
} from "selectors/claims";
import FileDetails from "./view";
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
});
export default connect(select, null)(FileDetails);

View file

@ -0,0 +1,64 @@
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";
class FileDetails extends React.PureComponent {
render() {
const { claim, contentType, metadata, 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);
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>
</tbody>
</table>
<p>
<Link
href={`https://lbry.io/dmca?claim_id=${claim.claim_id}`}
label={__("report")}
/>
</p>
</div>
</div>
);
}
}
export default FileDetails;

View file

@ -0,0 +1,30 @@
import React from "react";
import { connect } from "react-redux";
import {
makeSelectFileInfoForUri,
makeSelectDownloadingForUri,
makeSelectLoadingForUri,
} from "selectors/file_info";
import { makeSelectCostInfoForUri } from "selectors/cost_info";
import { doFetchAvailability } from "actions/availability";
import { doOpenFileInShell } from "actions/file_info";
import { doPurchaseUri, doStartDownload } from "actions/content";
import FileDownloadLink from "./view";
import * as modals from "constants/modal_types";
const select = (state, props) => ({
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
/*availability check is disabled due to poor performance, TBD if it dies forever or requires daemon fix*/
downloading: makeSelectDownloadingForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
loading: makeSelectLoadingForUri(props.uri)(state),
});
const perform = dispatch => ({
checkAvailability: uri => dispatch(doFetchAvailability(uri)),
openInShell: fileInfo => dispatch(doOpenFileInShell(fileInfo)),
purchaseUri: uri => dispatch(doPurchaseUri(uri)),
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
});
export default connect(select, perform)(FileDownloadLink);

View file

@ -0,0 +1,104 @@
import React from "react";
import { Icon, BusyMessage } from "component/common";
import Link from "component/link";
class FileDownloadLink extends React.PureComponent {
componentWillMount() {
this.checkAvailability(this.props.uri);
}
componentWillReceiveProps(nextProps) {
this.checkAvailability(nextProps.uri);
this.restartDownload(nextProps);
}
restartDownload(props) {
const { downloading, fileInfo, uri, restartDownload } = props;
if (
!downloading &&
fileInfo &&
!fileInfo.completed &&
fileInfo.written_bytes !== false &&
fileInfo.written_bytes < fileInfo.total_bytes
) {
restartDownload(uri, fileInfo.outpoint);
}
}
checkAvailability(uri) {
if (!this._uri || uri !== this._uri) {
this._uri = uri;
this.props.checkAvailability(uri);
}
}
render() {
const {
fileInfo,
downloading,
uri,
openInShell,
purchaseUri,
costInfo,
loading,
} = this.props;
if (loading || downloading) {
const progress = fileInfo && fileInfo.written_bytes
? fileInfo.written_bytes / fileInfo.total_bytes * 100
: 0,
label = fileInfo
? progress.toFixed(0) + __("% complete")
: __("Connecting..."),
labelWithIcon = (
<span className="button__content">
<Icon icon="icon-download" />
<span>
{label}
</span>
</span>
);
return (
<div className="faux-button-block file-download button-set-item">
<div
className="faux-button-block file-download__overlay"
style={{ width: progress + "%" }}
>
{labelWithIcon}
</div>
{labelWithIcon}
</div>
);
} else if (fileInfo === null && !downloading) {
if (!costInfo) {
return <BusyMessage message={__("Fetching cost info")} />;
} else {
return (
<Link
button="text"
label={__("Download")}
icon="icon-download"
onClick={() => {
purchaseUri(uri);
}}
/>
);
}
} else if (fileInfo && fileInfo.download_path) {
return (
<Link
label={__("Open")}
button="text"
icon="icon-external-link-square"
onClick={() => openInShell(fileInfo)}
/>
);
}
return null;
}
}
export default FileDownloadLink;

View file

@ -88,6 +88,7 @@ class FileList extends React.PureComponent {
key={fileInfo.outpoint || fileInfo.claim_id}
uri={uri}
hidePrice={true}
showActions={true}
showEmpty={this.props.fileTileShowEmpty}
/>
);

View file

@ -49,7 +49,17 @@ const FileListSearchResults = props => {
class FileListSearch extends React.PureComponent {
componentWillMount() {
this.props.search(this.props.query);
this.doSearch(this.props);
}
componentWillReceiveProps(props) {
if (props.query != this.props.query) {
this.doSearch(props);
}
}
doSearch(props) {
this.props.search(props.query);
}
render() {

View file

@ -8,23 +8,15 @@ import {
import { makeSelectClaimForUri } from "selectors/claims";
import FilePrice from "./view";
const makeSelect = () => {
const selectCostInfoForUri = makeSelectCostInfoForUri();
const selectFetchingCostInfoForUri = makeSelectFetchingCostInfoForUri();
const selectClaim = makeSelectClaimForUri();
const select = (state, props) => ({
costInfo: selectCostInfoForUri(state, props),
fetching: selectFetchingCostInfoForUri(state, props),
claim: selectClaim(state, props),
});
return select;
};
const select = (state, props) => ({
costInfo: makeSelectCostInfoForUri(props.uri)(state),
fetching: makeSelectFetchingCostInfoForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
});
const perform = dispatch => ({
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
// cancelFetchCostInfo: (uri) => dispatch(doCancelFetchCostInfoForUri(uri))
});
export default connect(makeSelect, perform)(FilePrice);
export default connect(select, perform)(FilePrice);

View file

@ -9,32 +9,23 @@ import {
import { makeSelectFileInfoForUri } from "selectors/file_info";
import { selectShowNsfw } from "selectors/settings";
import {
makeSelectIsResolvingForUri,
makeSelectIsUriResolving,
selectRewardContentClaimIds,
} from "selectors/content";
import FileTile from "./view";
const makeSelect = () => {
const selectClaimForUri = makeSelectClaimForUri();
const selectFileInfoForUri = makeSelectFileInfoForUri();
const selectMetadataForUri = makeSelectMetadataForUri();
const selectResolvingUri = makeSelectIsResolvingForUri();
const select = (state, props) => ({
claim: selectClaimForUri(state, props),
fileInfo: selectFileInfoForUri(state, props),
obscureNsfw: !selectShowNsfw(state),
metadata: selectMetadataForUri(state, props),
isResolvingUri: selectResolvingUri(state, props),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
});
return select;
};
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
});
const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)),
resolveUri: uri => dispatch(doResolveUri(uri)),
});
export default connect(makeSelect, perform)(FileTile);
export default connect(select, perform)(FileTile);

View file

@ -1,6 +1,7 @@
import React from "react";
import lbryuri from "lbryuri.js";
import CardMedia from "component/cardMedia";
import FileActions from "component/fileActions";
import Link from "component/link";
import { TruncatedText } from "component/common.js";
import FilePrice from "component/filePrice";
@ -53,6 +54,7 @@ class FileTile extends React.PureComponent {
render() {
const {
claim,
showActions,
metadata,
isResolvingUri,
showEmpty,
@ -104,7 +106,7 @@ class FileTile extends React.PureComponent {
onMouseEnter={this.handleMouseOver.bind(this)}
onMouseLeave={this.handleMouseOut.bind(this)}
>
<Link onClick={onClick} className="card__link">
<div onClick={onClick} className="card__link">
<div className={"card__inner file-tile__row"}>
<CardMedia title={title} thumbnail={thumbnail} />
<div className="file-tile__content">
@ -116,14 +118,15 @@ class FileTile extends React.PureComponent {
<TruncatedText lines={1}>{title}</TruncatedText>
</h3>
</div>
<div className="card__content card__subtext">
<TruncatedText lines={3}>
{description}
</TruncatedText>
</div>
{description &&
<div className="card__content card__subtext">
<TruncatedText lines={!showActions ? 4 : 2}>
{description}
</TruncatedText>
</div>}
</div>
</div>
</Link>
</div>
{this.state.showNsfwHelp && <NsfwOverlay />}
</section>
);

View file

@ -74,11 +74,6 @@ export class FormRow extends React.PureComponent {
}
getOptions() {
if (!this._field || !this._field.getOptions) {
console.log(this);
console.log(this._field);
console.log(this._field.getOptions);
}
return this._field.getOptions();
}

View file

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

View file

@ -8,11 +8,11 @@ const Link = props => {
style,
label,
icon,
badge,
button,
disabled,
children,
navigate,
navigateParams,
doNavigate,
} = props;
@ -24,7 +24,7 @@ const Link = props => {
const onClick = !props.onClick && navigate
? () => {
doNavigate(navigate);
doNavigate(navigate, navigateParams || {});
}
: props.onClick;
@ -36,7 +36,6 @@ const Link = props => {
<span {...("button" in props ? { className: "button__content" } : {})}>
{"icon" in props ? <Icon icon={icon} fixed={true} /> : null}
{label ? <span className="link-label">{label}</span> : null}
{"badge" in props ? <span className="badge">{badge}</span> : null}
</span>
);
}

View file

@ -1,5 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import Link from "./view";
import LinkTransaction from "./view";
export default connect(null, null)(Link);
export default connect(null, null)(LinkTransaction);

View file

@ -19,6 +19,7 @@ class PublishForm extends React.PureComponent {
this._defaultPaidPrice = 0.01;
this.state = {
id: null,
rawName: "",
name: "",
bid: 10,
@ -48,6 +49,7 @@ class PublishForm extends React.PureComponent {
isFee: false,
customUrl: false,
source: null,
mode: "publish",
};
}
@ -116,7 +118,7 @@ class PublishForm extends React.PureComponent {
? { channel_name: this.state.channel }
: {}),
};
const { source } = this.state;
if (this.refs.file.getValue() !== "") {
@ -187,6 +189,14 @@ class PublishForm extends React.PureComponent {
return !!myClaims.find(claim => claim.name === name);
}
handleEditClaim() {
const claimInfo = this.claim() || this.myClaimInfo();
if (claimInfo) {
this.handlePrefillClaim(claimInfo);
}
}
topClaimIsMine() {
const myClaimInfo = this.myClaimInfo();
const { claimsByUri } = this.props;
@ -203,10 +213,10 @@ class PublishForm extends React.PureComponent {
}
myClaimInfo() {
const { name } = this.state;
const { id } = this.state;
return Object.values(this.props.myClaims).find(
claim => claim.name === name
claim => claim.claim_id === id
);
}
@ -226,6 +236,7 @@ class PublishForm extends React.PureComponent {
name: "",
uri: "",
prefillDone: false,
mode: "publish",
});
return;
@ -247,6 +258,7 @@ class PublishForm extends React.PureComponent {
rawName: rawName,
name: name,
prefillDone: false,
mode: "publish",
uri,
});
@ -261,9 +273,10 @@ class PublishForm extends React.PureComponent {
});
}
handlePrefillClicked() {
const claimInfo = this.myClaimInfo();
const { source } = claimInfo.value.stream;
handlePrefillClaim(claimInfo) {
const { claim_id, name, channel_name, amount } = claimInfo;
const { source, metadata } = claimInfo.value.stream;
const {
license,
licenseUrl,
@ -272,16 +285,21 @@ class PublishForm extends React.PureComponent {
description,
language,
nsfw,
} = claimInfo.value.stream.metadata;
} = metadata;
let newState = {
id: claim_id,
channel: channel_name || "anonymous",
bid: amount,
meta_title: title,
meta_thumbnail: thumbnail,
meta_description: description,
meta_language: language,
meta_nsfw: nsfw,
mode: "edit",
prefillDone: true,
bid: claimInfo.amount,
rawName: name,
name,
source,
};
@ -375,6 +393,7 @@ class PublishForm extends React.PureComponent {
handleChannelChange(channelName) {
this.setState({
mode: "publish",
channel: channelName,
});
const nameChanged = () => this.nameChanged(this.state.rawName);
@ -412,6 +431,13 @@ class PublishForm extends React.PureComponent {
componentWillMount() {
this.props.fetchClaimListMine();
this._updateChannelList();
const { id } = this.props.params;
this.setState({ id });
}
componentDidMount() {
this.handleEditClaim();
}
onFileChange() {
@ -436,37 +462,38 @@ class PublishForm extends React.PureComponent {
}
getNameBidHelpText() {
if (this.state.prefillDone) {
const { prefillDone, name, uri } = this.state;
const { resolvingUris } = this.props;
const claim = this.claim();
if (prefillDone) {
return __("Existing claim data was prefilled");
}
if (
this.state.uri &&
this.props.resolvingUris.indexOf(this.state.uri) !== -1 &&
this.claim() === undefined
) {
if (uri && resolvingUris.indexOf(uri) !== -1 && claim === undefined) {
return __("Checking...");
} else if (!this.state.name) {
} else if (!name) {
return __("Select a URL for this publish.");
} else if (!this.claim()) {
} else if (!claim) {
return __("This URL is unused.");
} else if (this.myClaimExists() && !this.state.prefillDone) {
} else if (this.myClaimExists() && !prefillDone) {
return (
<span>
{__("You already have a claim with this name.")}{" "}
<Link
label={__("Use data from my existing claim")}
onClick={() => this.handlePrefillClicked()}
label={__("Edit existing claim")}
onClick={() => this.handleEditClaim()}
/>
</span>
);
} else if (this.claim()) {
if (this.topClaimValue() === 1) {
} else if (claim) {
const topClaimValue = this.topClaimValue();
if (topClaimValue === 1) {
return (
<span>
{__(
'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>
);
@ -475,8 +502,8 @@ class PublishForm extends React.PureComponent {
<span>
{__(
'A deposit of at least "%s" credits is required to win "%s". However, you can still get a permanent URL for any amount.',
this.topClaimValue(),
this.state.name
topClaimValue,
name
)}
</span>
);
@ -493,10 +520,18 @@ class PublishForm extends React.PureComponent {
}
render() {
const { mode, submitting } = this.state;
const lbcInputHelp = __(
"This LBC remains yours and the deposit can be undone at any time."
);
let submitLabel = !submitting ? __("Publish") : __("Publishing...");
if (mode === "edit") {
submitLabel = !submitting ? __("Update") : __("Updating...");
}
return (
<main className="main--single-column">
<form
@ -839,9 +874,7 @@ class PublishForm extends React.PureComponent {
<div className="card-series-submit">
<Link
button="primary"
label={
!this.state.submitting ? __("Publish") : __("Publishing...")
}
label={submitLabel}
onClick={event => {
this.handleSubmit(event);
}}

View file

@ -46,7 +46,7 @@ const Router = props => {
search: <SearchPage params={params} />,
send: <SendCreditsPage params={params} />,
settings: <SettingsPage params={params} />,
show: <ShowPage params={params} />,
show: <ShowPage {...params} />,
wallet: <WalletPage params={params} />,
});
};

View file

@ -0,0 +1,10 @@
import React from "react";
import { connect } from "react-redux";
import { selectThemePath } from "selectors/settings.js";
import Theme from "./view";
const select = state => ({
themePath: selectThemePath(state),
});
export default connect(select, null)(Theme);

View file

@ -0,0 +1,20 @@
import React from "react";
const Theme = props => {
const { themePath } = props;
if (!themePath) {
return null;
}
return (
<link
href={themePath}
rel="stylesheet"
type="text/css"
media="screen,print"
/>
);
};
export default Theme;

View file

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

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,57 +1,82 @@
import React from "react";
import LinkTransaction from "component/linkTransaction";
import { CreditAmount } from "component/common";
import TransactionListItem from "./internal/TransactionListItem";
import FormField from "component/formField";
const TransactionList = props => {
const { emptyMessage, transactions } = props;
class TransactionList extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
filter: null,
};
}
handleFilterChanged(event) {
this.setState({
filter: event.target.value,
});
}
filterTransaction(transaction) {
const { filter } = this.state;
return !filter || filter == transaction.type;
}
render() {
const { emptyMessage, rewards, transactions } = this.props;
let transactionList = transactions.filter(
this.filterTransaction.bind(this)
);
if (!transactions || !transactions.length) {
return (
<div className="empty">
{emptyMessage || __("No transactions to list.")}
<div>
{(transactionList.length || this.state.filter) &&
<span className="sort-section">
{__("Filter")} {" "}
<FormField
type="select"
onChange={this.handleFilterChanged.bind(this)}
>
<option value="">{__("All")}</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="update">{__("Updates")}</option>
</FormField>
</span>}
{!transactionList.length &&
<div className="empty">
{emptyMessage || __("No transactions to list.")}
</div>}
{Boolean(transactionList.length) &&
<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]}
/>
)}
</tbody>
</table>}
</div>
);
}
return (
<table className="table-standard table-stretch">
<thead>
<tr>
<th>{__("Date")}</th>
<th>{__("Amount")}</th>
<th>{__("Transaction")}</th>
</tr>
</thead>
<tbody>
{transactions.map(item => {
return (
<tr key={item.id}>
<td>
{item.date
? item.date.toLocaleDateString() +
" " +
item.date.toLocaleTimeString()
: <span className="empty">
{__("(Transaction pending)")}
</span>}
</td>
<td>
<CreditAmount
amount={item.amount}
look="plain"
showPlus={true}
precision={8}
/>{" "}
</td>
<td>
<LinkTransaction id={item.id} />
</td>
</tr>
);
})}
</tbody>
</table>
);
};
}
export default TransactionList;

View file

@ -1,25 +1,19 @@
import React from "react";
import lbryuri from "lbryuri";
import { connect } from "react-redux";
import { makeSelectIsResolvingForUri } from "selectors/content";
import { doResolveUri } from "actions/content";
import { makeSelectIsUriResolving } from "selectors/content";
import { makeSelectClaimForUri } from "selectors/claims";
import UriIndicator from "./view";
const makeSelect = () => {
const selectClaim = makeSelectClaimForUri(),
selectIsResolving = makeSelectIsResolvingForUri();
const select = (state, props) => ({
claim: selectClaim(state, props),
isResolvingUri: selectIsResolving(state, props),
uri: lbryuri.normalize(props.uri),
});
return select;
};
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
uri: lbryuri.normalize(props.uri),
});
const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)),
});
export default connect(makeSelect, perform)(UriIndicator);
export default connect(select, perform)(UriIndicator);

View file

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

View file

@ -1,9 +1,8 @@
import React from "react";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import { doChangeVolume } from "actions/app";
import { selectCurrentModal, selectVolume } from "selectors/app";
import { doPurchaseUri, doLoadVideo } from "actions/content";
import { selectVolume } from "selectors/app";
import { doPlayUri, doSetPlayingUri } from "actions/content";
import {
makeSelectMetadataForUri,
makeSelectContentTypeForUri,
@ -16,35 +15,24 @@ import {
import { makeSelectCostInfoForUri } from "selectors/cost_info";
import { selectShowNsfw } from "selectors/settings";
import Video from "./view";
import { selectPlayingUri } from "selectors/content";
const makeSelect = () => {
const selectCostInfo = makeSelectCostInfoForUri();
const selectFileInfo = makeSelectFileInfoForUri();
const selectIsLoading = makeSelectLoadingForUri();
const selectIsDownloading = makeSelectDownloadingForUri();
const selectMetadata = makeSelectMetadataForUri();
const selectContentType = makeSelectContentTypeForUri();
const select = (state, props) => ({
costInfo: selectCostInfo(state, props),
fileInfo: selectFileInfo(state, props),
metadata: selectMetadata(state, props),
obscureNsfw: !selectShowNsfw(state),
modal: selectCurrentModal(state),
isLoading: selectIsLoading(state, props),
isDownloading: selectIsDownloading(state, props),
contentType: selectContentType(state, props),
volume: selectVolume(state, props),
});
return select;
};
const select = (state, props) => ({
costInfo: makeSelectCostInfoForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
isLoading: makeSelectLoadingForUri(props.uri)(state),
isDownloading: makeSelectDownloadingForUri(props.uri)(state),
playingUri: selectPlayingUri(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
volume: selectVolume(state),
});
const perform = dispatch => ({
loadVideo: uri => dispatch(doLoadVideo(uri)),
purchaseUri: uri => dispatch(doPurchaseUri(uri, "affirmPurchaseAndPlay")),
closeModal: () => dispatch(doCloseModal()),
play: uri => dispatch(doPlayUri(uri)),
cancelPlay: () => dispatch(doSetPlayingUri(null)),
changeVolume: volume => dispatch(doChangeVolume(volume)),
});
export default connect(makeSelect, perform)(Video);
export default connect(select, perform)(Video);

View file

@ -1,7 +1,5 @@
import React from "react";
import FilePrice from "component/filePrice";
import Link from "component/link";
import Modal from "modal/modal";
class VideoPlayButton extends React.PureComponent {
componentDidMount() {
@ -13,43 +11,22 @@ class VideoPlayButton extends React.PureComponent {
document.removeEventListener("keydown", this.keyDownListener);
}
onPurchaseConfirmed() {
this.props.closeModal();
this.props.startPlaying();
this.props.loadVideo(this.props.uri);
}
onKeyDown(event) {
if (
"input" !== event.target.tagName.toLowerCase() &&
"Space" === event.code
) {
event.preventDefault();
this.onWatchClick();
this.watch();
}
}
onWatchClick() {
this.props.purchaseUri(this.props.uri).then(() => {
if (!this.props.modal) {
this.props.startPlaying();
}
});
watch() {
this.props.play(this.props.uri);
}
render() {
const {
button,
label,
metadata,
metadata: { title },
uri,
modal,
closeModal,
isLoading,
fileInfo,
mediaType,
} = this.props;
const { button, label, isLoading, fileInfo, mediaType } = this.props;
/*
title={
@ -65,36 +42,14 @@ class VideoPlayButton extends React.PureComponent {
: "icon-folder-o";
return (
<div>
<Link
button={button ? button : null}
disabled={disabled}
label={label ? label : ""}
className="video__play-button"
icon={icon}
onClick={this.onWatchClick.bind(this)}
/>
<Modal
type="confirm"
isOpen={modal == "affirmPurchaseAndPlay"}
contentLabel={__("Confirm Purchase")}
onConfirmed={this.onPurchaseConfirmed.bind(this)}
onAborted={closeModal}
>
{__("This will purchase")} <strong>{title}</strong> {__("for")}{" "}
<strong>
<FilePrice uri={uri} showFullPrice={true} look="plain" />
</strong>{" "}
{__("credits")}.
</Modal>
<Modal
isOpen={modal == "timedOut"}
onConfirmed={closeModal}
contentLabel={__("Timed Out")}
>
{__("Sorry, your download timed out :(")}
</Modal>
</div>
<Link
button={button ? button : null}
disabled={disabled}
label={label ? label : ""}
className="video__play-button"
icon={icon}
onClick={() => this.watch()}
/>
);
}
}

View file

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

View file

@ -1,12 +1,10 @@
import React from "react";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import {
doSendDraftTransaction,
doSetDraftTransactionAmount,
doSetDraftTransactionAddress,
} from "actions/wallet";
import { selectCurrentModal } from "selectors/app";
import {
selectDraftTransactionAmount,
selectDraftTransactionAddress,
@ -16,14 +14,12 @@ import {
import WalletSend from "./view";
const select = state => ({
modal: selectCurrentModal(state),
address: selectDraftTransactionAddress(state),
amount: selectDraftTransactionAmount(state),
error: selectDraftTransactionError(state),
});
const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()),
sendToAddress: () => dispatch(doSendDraftTransaction()),
setAmount: event => dispatch(doSetDraftTransactionAmount(event.target.value)),
setAddress: event =>

View file

@ -1,20 +1,10 @@
import React from "react";
import Link from "component/link";
import Modal from "modal/modal";
import { FormRow } from "component/form";
import lbryuri from "lbryuri";
const WalletSend = props => {
const {
sendToAddress,
closeModal,
modal,
setAmount,
setAddress,
amount,
address,
error,
} = props;
const { sendToAddress, setAmount, setAddress, amount, address } = props;
return (
<section className="card">
@ -57,32 +47,6 @@ const WalletSend = props => {
</div>
</div>
</form>
{modal == "insufficientBalance" &&
<Modal
isOpen={true}
contentLabel={__("Insufficient balance")}
onConfirmed={closeModal}
>
{__(
"Insufficient balance: after this transaction you would have less than 1 LBC in your wallet."
)}
</Modal>}
{modal == "transactionSuccessful" &&
<Modal
isOpen={true}
contentLabel={__("Transaction successful")}
onConfirmed={closeModal}
>
{__("Your transaction was successfully placed in the queue.")}
</Modal>}
{modal == "transactionFailed" &&
<Modal
isOpen={true}
contentLabel={__("Transaction failed")}
onConfirmed={closeModal}
>
{error}
</Modal>}
</section>
);
};

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";
// Navigation
export const CHANGE_PATH = "CHANGE_PATH";
export const CHANGE_AFTER_AUTH_PATH = "CHANGE_AFTER_AUTH_PATH";
export const WINDOW_SCROLLED = "WINDOW_SCROLLED";
export const HISTORY_NAVIGATE = "HISTORY_NAVIGATE";
@ -40,8 +39,11 @@ export const SEND_TRANSACTION_STARTED = "SEND_TRANSACTION_STARTED";
export const SEND_TRANSACTION_COMPLETED = "SEND_TRANSACTION_COMPLETED";
export const SEND_TRANSACTION_FAILED = "SEND_TRANSACTION_FAILED";
export const FETCH_BLOCK_SUCCESS = "FETCH_BLOCK_SUCCESS";
export const SUPPORT_TRANSACTION_STARTED = "SUPPORT_TRANSACTION_STARTED";
export const SUPPORT_TRANSACTION_COMPLETED = "SUPPORT_TRANSACTION_COMPLETED";
export const SUPPORT_TRANSACTION_FAILED = "SUPPORT_TRANSACTION_FAILED";
// Content
// Claims
export const FETCH_FEATURED_CONTENT_STARTED = "FETCH_FEATURED_CONTENT_STARTED";
export const FETCH_FEATURED_CONTENT_COMPLETED =
"FETCH_FEATURED_CONTENT_COMPLETED";
@ -57,6 +59,20 @@ export const FETCH_CHANNEL_CLAIM_COUNT_COMPLETED =
export const FETCH_CLAIM_LIST_MINE_STARTED = "FETCH_CLAIM_LIST_MINE_STARTED";
export const FETCH_CLAIM_LIST_MINE_COMPLETED =
"FETCH_CLAIM_LIST_MINE_COMPLETED";
export const ABANDON_CLAIM_STARTED = "ABANDON_CLAIM_STARTED";
export const ABANDON_CLAIM_SUCCEEDED = "ABANDON_CLAIM_SUCCEEDED";
export const FETCH_CHANNEL_LIST_MINE_STARTED =
"FETCH_CHANNEL_LIST_MINE_STARTED";
export const FETCH_CHANNEL_LIST_MINE_COMPLETED =
"FETCH_CHANNEL_LIST_MINE_COMPLETED";
export const CREATE_CHANNEL_STARTED = "CREATE_CHANNEL_STARTED";
export const CREATE_CHANNEL_COMPLETED = "CREATE_CHANNEL_COMPLETED";
export const PUBLISH_STARTED = "PUBLISH_STARTED";
export const PUBLISH_COMPLETED = "PUBLISH_COMPLETED";
export const PUBLISH_FAILED = "PUBLISH_FAILED";
export const SET_PLAYING_URI = "PLAY_URI";
// Files
export const FILE_LIST_STARTED = "FILE_LIST_STARTED";
export const FILE_LIST_SUCCEEDED = "FILE_LIST_SUCCEEDED";
export const FETCH_FILE_INFO_STARTED = "FETCH_FILE_INFO_STARTED";
@ -73,17 +89,6 @@ export const PLAY_VIDEO_STARTED = "PLAY_VIDEO_STARTED";
export const FETCH_AVAILABILITY_STARTED = "FETCH_AVAILABILITY_STARTED";
export const FETCH_AVAILABILITY_COMPLETED = "FETCH_AVAILABILITY_COMPLETED";
export const FILE_DELETE = "FILE_DELETE";
export const ABANDON_CLAIM_STARTED = "ABANDON_CLAIM_STARTED";
export const ABANDON_CLAIM_SUCCEEDED = "ABANDON_CLAIM_SUCCEEDED";
export const FETCH_CHANNEL_LIST_MINE_STARTED =
"FETCH_CHANNEL_LIST_MINE_STARTED";
export const FETCH_CHANNEL_LIST_MINE_COMPLETED =
"FETCH_CHANNEL_LIST_MINE_COMPLETED";
export const CREATE_CHANNEL_STARTED = "CREATE_CHANNEL_STARTED";
export const CREATE_CHANNEL_COMPLETED = "CREATE_CHANNEL_COMPLETED";
export const PUBLISH_STARTED = "PUBLISH_STARTED";
export const PUBLISH_COMPLETED = "PUBLISH_COMPLETED";
export const PUBLISH_FAILED = "PUBLISH_FAILED";
// Search
export const SEARCH_STARTED = "SEARCH_STARTED";

View file

@ -1,5 +1,6 @@
export const CONFIRM_FILE_REMOVE = "confirmFileRemove";
export const INCOMPATIBLE_DAEMON = "incompatibleDaemon";
export const FILE_TIMEOUT = "file_timeout";
export const DOWNLOADING = "downloading";
export const ERROR = "error";
export const INSUFFICIENT_CREDITS = "insufficient_credits";
@ -7,5 +8,8 @@ export const UPGRADE = "upgrade";
export const WELCOME = "welcome";
export const FIRST_REWARD = "first_reward";
export const AUTHENTICATION_FAILURE = "auth_failure";
export const TRANSACTION_FAILED = "transaction_failed";
export const INSUFFICIENT_BALANCE = "insufficient_balance";
export const REWARD_APPROVAL_REQUIRED = "reward_approval_required";
export const AFFIRM_PURCHASE = "affirm_purchase";
export const CREDIT_INTRO = "credit_intro";

View file

@ -6,3 +6,5 @@ export const NEW_USER_ACKNOWLEDGED = "welcome_acknowledged";
export const LANGUAGE = "language";
export const SHOW_NSFW = "showNsfw";
export const SHOW_UNAVAILABLE = "showUnavailable";
export const THEME = "theme";
export const THEMES = "themes";

View file

@ -39,6 +39,8 @@ let lbry = {
customLighthouseServers: [],
showDeveloperMenu: false,
language: "en",
theme: "light",
themes: [],
},
};
@ -277,17 +279,6 @@ lbry.publishDeprecated = function(
);
};
lbry.getClientSettings = function() {
var outSettings = {};
for (let setting of Object.keys(lbry.defaultClientSettings)) {
var localStorageVal = localStorage.getItem("setting_" + setting);
outSettings[setting] = localStorageVal === null
? lbry.defaultClientSettings[setting]
: JSON.parse(localStorageVal);
}
return outSettings;
};
lbry.getClientSetting = function(setting) {
var localStorageVal = localStorage.getItem("setting_" + setting);
if (setting == "showDeveloperMenu") {
@ -298,12 +289,6 @@ lbry.getClientSetting = function(setting) {
: JSON.parse(localStorageVal);
};
lbry.setClientSettings = function(settings) {
for (let setting of Object.keys(settings)) {
lbry.setClientSetting(setting, settings[setting]);
}
};
lbry.setClientSetting = function(setting, value) {
return localStorage.setItem("setting_" + setting, JSON.stringify(value));
};

View file

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

View file

@ -0,0 +1,32 @@
import React from "react";
import FilePrice from "component/filePrice";
import { Modal } from "modal/modal";
class ModalAffirmPurchase extends React.PureComponent {
onAffirmPurchase() {
this.props.closeModal();
this.props.loadVideo(this.props.uri);
}
render() {
const { cancelPurchase, metadata: { title }, uri } = this.props;
return (
<Modal
type="confirm"
isOpen={true}
contentLabel={__("Confirm Purchase")}
onConfirmed={this.onAffirmPurchase.bind(this)}
onAborted={cancelPurchase}
>
{__("This will purchase")} <strong>{title}</strong> {__("for")}{" "}
<strong>
<FilePrice uri={uri} showFullPrice={true} look="plain" />
</strong>{" "}
{__("credits")}.
</Modal>
);
}
}
export default ModalAffirmPurchase;

View file

@ -9,4 +9,4 @@ const perform = dispatch => ({
close: () => dispatch(doCloseModal()),
});
export default connect(select, perform)(ModalAuthFailure);
export default connect(null, null)(ModalAuthFailure);

View file

@ -1,16 +1,10 @@
import React from "react";
import { connect } from "react-redux";
import { selectCurrentModal, selectModalExtraContent } from "selectors/app";
import { doCloseModal } from "actions/app";
import ModalError from "./view";
const select = state => ({
modal: selectCurrentModal(state),
error: selectModalExtraContent(state),
});
const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()),
});
export default connect(select, perform)(ModalError);
export default connect(null, perform)(ModalError);

View file

@ -4,7 +4,7 @@ import { ExpandableModal } from "modal/modal";
class ModalError extends React.PureComponent {
render() {
const { modal, closeModal, error } = this.props;
const { closeModal, error } = this.props;
const errorObj = typeof error === "string" ? { message: error } : error;
@ -33,7 +33,7 @@ class ModalError extends React.PureComponent {
return (
<ExpandableModal
isOpen={modal == "error"}
isOpen={true}
contentLabel={__("Error")}
className="error-modal"
overlayClassName="error-modal-overlay"

View file

@ -0,0 +1,15 @@
import React from "react";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import { makeSelectMetadataForUri } from "selectors/claims";
import ModalFileTimeout from "./view";
const select = state => ({
metadata: makeSelectMetadataForUri(props.uri)(state),
});
const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()),
});
export default connect(select, perform)(ModalFileTimeout);

View file

@ -0,0 +1,21 @@
import React from "react";
import { Modal } from "modal/modal";
class ModalFileTimeout extends React.PureComponent {
render() {
const { metadata: { title } } = this.props;
return (
<Modal
isOpen={true}
contentLabel={__("Download failed")}
onConfirmed={closeModal}
>
{__("LBRY was unable to download the stream")}{" "}
<strong>{title}</strong>.
</Modal>
);
}
}
export default ModalFileTimeout;

View file

@ -0,0 +1,17 @@
import React from "react";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import { doNavigate } from "actions/navigation";
import ModalInsufficientBalance from "./view";
const select = state => ({});
const perform = dispatch => ({
addBalance: () => {
dispatch(doNavigate("/wallet"));
dispatch(doCloseModal());
},
closeModal: () => dispatch(doCloseModal()),
});
export default connect(select, perform)(ModalInsufficientBalance);

View file

@ -0,0 +1,26 @@
import React from "react";
import { Modal } from "modal/modal";
class ModalInsufficientBalance extends React.PureComponent {
render() {
const { addBalance, closeModal } = this.props;
return (
<Modal
isOpen={true}
type="confirm"
contentLabel={__("Not enough credits")}
confirmButtonLabel={__("Get Credits")}
abortButtonLabel={__("Cancel")}
onAborted={closeModal}
onConfirmed={addBalance}
>
{__(
"Insufficient balance: after this transaction you would have less than 0 LBCs in your wallet."
)}
</Modal>
);
}
}
export default ModalInsufficientBalance;

View file

@ -2,19 +2,15 @@ import React from "react";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import { doDeleteFileAndGoBack } from "actions/file_info";
import { makeSelectClaimForUriIsMine } from "selectors/claims";
import { makeSelectTitleForUri, makeSelectClaimIsMine } from "selectors/claims";
import { makeSelectFileInfoForUri } from "selectors/file_info";
import ModalRemoveFile from "./view";
const makeSelect = () => {
const selectClaimForUriIsMine = makeSelectClaimForUriIsMine();
const select = (state, props) => ({
claimIsMine: selectClaimForUriIsMine(state, props),
});
return select;
};
const select = (state, props) => ({
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
});
const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()),
@ -23,4 +19,4 @@ const perform = dispatch => ({
},
});
export default connect(makeSelect, perform)(ModalRemoveFile);
export default connect(select, perform)(ModalRemoveFile);

View file

@ -25,7 +25,13 @@ class ModalRemoveFile extends React.PureComponent {
}
render() {
const { claimIsMine, closeModal, deleteFile, outpoint, title } = this.props;
const {
claimIsMine,
closeModal,
deleteFile,
fileInfo: { outpoint },
title,
} = this.props;
const { deleteChecked, abandonClaimChecked } = this.state;
return (

View file

@ -2,7 +2,7 @@ import React from "react";
import { connect } from "react-redux";
import { doOpenModal } from "actions/app";
import * as settings from "constants/settings";
import { selectCurrentModal } from "selectors/app";
import { selectCurrentModal, selectModalProps } from "selectors/app";
import { selectCurrentPage } from "selectors/navigation";
import { selectCostForCurrentPageUri } from "selectors/cost_info";
import { makeSelectClientSetting } from "selectors/settings";
@ -14,6 +14,7 @@ const select = (state, props) => ({
balance: selectBalance(state),
showPageCost: selectCostForCurrentPageUri(state),
modal: selectCurrentModal(state),
modalProps: selectModalProps(state),
page: selectCurrentPage(state),
isWelcomeAcknowledged: makeSelectClientSetting(
settings.NEW_USER_ACKNOWLEDGED

View file

@ -7,8 +7,13 @@ import ModalUpgrade from "modal/modalUpgrade";
import ModalWelcome from "modal/modalWelcome";
import ModalFirstReward from "modal/modalFirstReward";
import ModalRewardApprovalRequired from "modal/modalRewardApprovalRequired";
import * as modals from "constants/modal_types";
import ModalCreditIntro from "modal/modalCreditIntro";
import ModalRemoveFile from "modal/modalRemoveFile";
import ModalTransactionFailed from "modal/modalTransactionFailed";
import ModalInsufficientBalance from "modal/modalInsufficientBalance";
import ModalFileTimeout from "modal/modalFileTimeout";
import ModalAffirmPurchase from "modal/modalAffirmPurchase";
import * as modals from "constants/modal_types";
class ModalRouter extends React.PureComponent {
constructor(props) {
@ -29,7 +34,7 @@ class ModalRouter extends React.PureComponent {
}
showTransitionModals(props) {
const { modal, openModal, page } = props;
const { modal, modalProps, openModal, page } = props;
if (modal) {
return;
@ -96,27 +101,37 @@ class ModalRouter extends React.PureComponent {
}
render() {
const { modal } = this.props;
const { modal, modalProps } = this.props;
switch (modal) {
case modals.UPGRADE:
return <ModalUpgrade />;
return <ModalUpgrade {...modalProps} />;
case modals.DOWNLOADING:
return <ModalDownloading />;
return <ModalDownloading {...modalProps} />;
case modals.ERROR:
return <ModalError />;
return <ModalError {...modalProps} />;
case modals.FILE_TIMEOUT:
return <ModalFileTimeout {...modalProps} />;
case modals.INSUFFICIENT_CREDITS:
return <ModalInsufficientCredits />;
return <ModalInsufficientCredits {...modalProps} />;
case modals.WELCOME:
return <ModalWelcome />;
return <ModalWelcome {...modalProps} />;
case modals.FIRST_REWARD:
return <ModalFirstReward />;
return <ModalFirstReward {...modalProps} />;
case modals.AUTHENTICATION_FAILURE:
return <ModalAuthFailure />;
return <ModalAuthFailure {...modalProps} />;
case modals.CREDIT_INTRO:
return <ModalCreditIntro />;
return <ModalCreditIntro {...modalProps} />;
case modals.TRANSACTION_FAILED:
return <ModalTransactionFailed {...modalProps} />;
case modals.INSUFFICIENT_BALANCE:
return <ModalInsufficientBalance {...modalProps} />;
case modals.REWARD_APPROVAL_REQUIRED:
return <ModalRewardApprovalRequired />;
return <ModalRewardApprovalRequired {...modalProps} />;
case modals.CONFIRM_FILE_REMOVE:
return <ModalRemoveFile {...modalProps} />;
case modals.AFFIRM_PURCHASE:
return <ModalAffirmPurchase {...modalProps} />;
default:
return null;
}

View file

@ -0,0 +1,12 @@
import React from "react";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import ModalTransactionFailed from "./view";
const select = state => ({});
const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()),
});
export default connect(select, perform)(ModalTransactionFailed);

View file

@ -0,0 +1,20 @@
import React from "react";
import { Modal } from "modal/modal";
class ModalTransactionFailed extends React.PureComponent {
render() {
const { closeModal } = this.props;
return (
<Modal
isOpen={true}
contentLabel={__("Transaction failed")}
onConfirmed={closeModal}
>
{__("Something went wrong")}:
</Modal>
);
}
}
export default ModalTransactionFailed;

View file

@ -14,22 +14,16 @@ import { doNavigate } from "actions/navigation";
import { makeSelectTotalPagesForChannel } from "selectors/content";
import ChannelPage from "./view";
const makeSelect = () => {
const selectClaim = makeSelectClaimForUri(),
selectClaimsInChannel = makeSelectClaimsInChannelForCurrentPage(),
selectFetchingChannelClaims = makeSelectFetchingChannelClaims(),
selectTotalPagesForChannel = makeSelectTotalPagesForChannel();
const select = (state, props) => ({
claim: selectClaim(state, props),
claimsInChannel: selectClaimsInChannel(state, props),
fetching: selectFetchingChannelClaims(state, props),
totalPages: selectTotalPagesForChannel(state, props),
params: selectCurrentParams(state),
});
return select;
};
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
claimsInChannel: makeSelectClaimsInChannelForCurrentPage(
props.uri,
props.page
)(state),
fetching: makeSelectFetchingChannelClaims(props.uri)(state),
params: selectCurrentParams(state),
totalPages: makeSelectTotalPagesForChannel(props.uri)(state),
});
const perform = dispatch => ({
fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)),
@ -37,4 +31,4 @@ const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)),
});
export default connect(makeSelect, perform)(ChannelPage);
export default connect(select, perform)(ChannelPage);

View file

@ -7,20 +7,19 @@ import ReactPaginate from "react-paginate";
class ChannelPage extends React.PureComponent {
componentDidMount() {
const { uri, params, fetchClaims, fetchClaimCount } = this.props;
const { uri, page, fetchClaims, fetchClaimCount } = this.props;
fetchClaims(uri, params.page || 1);
fetchClaims(uri, page || 1);
fetchClaimCount(uri);
}
componentWillReceiveProps(nextProps) {
const { params, fetching, fetchClaims, fetchClaimCount } = this.props;
const nextParams = nextProps.params;
const { page, uri, fetching, fetchClaims, fetchClaimCount } = this.props;
if (fetching !== nextParams.page && params.page !== nextParams.page) {
fetchClaims(nextProps.uri, nextParams.page);
if (fetching !== nextProps.page && page !== nextProps.page) {
fetchClaims(nextProps.uri, nextProps.page);
}
if (nextProps.uri != this.props.uri) {
if (nextProps.uri != uri) {
fetchClaimCount(uri);
}
}
@ -38,10 +37,9 @@ class ChannelPage extends React.PureComponent {
claimsInChannel,
claim,
uri,
params,
page,
totalPages,
} = this.props;
const { page } = params;
let contentList;
if (claimsInChannel === undefined) {

View file

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

View file

@ -13,26 +13,18 @@ import {
import { makeSelectCostInfoForUri } from "selectors/cost_info";
import { selectShowNsfw } from "selectors/settings";
import FilePage from "./view";
import { makeSelectCurrentParam } from "../../selectors/navigation";
const makeSelect = () => {
const selectClaim = makeSelectClaimForUri(),
selectContentType = makeSelectContentTypeForUri(),
selectFileInfo = makeSelectFileInfoForUri(),
selectCostInfo = makeSelectCostInfoForUri(),
selectMetadata = makeSelectMetadataForUri();
const select = (state, props) => ({
claim: selectClaim(state, props),
contentType: selectContentType(state, props),
costInfo: selectCostInfo(state, props),
metadata: selectMetadata(state, props),
obscureNsfw: !selectShowNsfw(state),
fileInfo: selectFileInfo(state, props),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
});
return select;
};
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
contentType: makeSelectContentTypeForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
tab: makeSelectCurrentParam("tab")(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
});
const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)),
@ -40,4 +32,4 @@ const perform = dispatch => ({
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
});
export default connect(makeSelect, perform)(FilePage);
export default connect(select, perform)(FilePage);

View file

@ -1,45 +1,13 @@
import React from "react";
import ReactMarkdown from "react-markdown";
import lbry from "lbry.js";
import lbryuri from "lbryuri.js";
import Video from "component/video";
import { Thumbnail } from "component/common";
import FilePrice from "component/filePrice";
import FileActions from "component/fileActions";
import Link from "component/link";
import FileDetails from "component/fileDetails";
import UriIndicator from "component/uriIndicator";
import IconFeatured from "component/iconFeatured";
import DateTime from "component/dateTime";
const FormatItem = props => {
const {
publishedDate,
contentType,
claim: { height },
metadata: { language, license },
} = props;
const mediaType = lbry.getMediaType(contentType);
return (
<table className="table-standard">
<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>
);
};
import WalletSendTip from "component/walletSendTip";
class FilePage extends React.PureComponent {
componentDidMount() {
@ -69,34 +37,20 @@ class FilePage extends React.PureComponent {
fileInfo,
metadata,
contentType,
tab,
uri,
rewardedContentClaimIds,
} = this.props;
const showTipBox = tab == "tip";
if (!claim || !metadata) {
return (
<span className="empty">{__("Empty claim or metadata info.")}</span>
);
}
const {
txid,
nout,
channel_name: channelName,
has_signature: hasSignature,
signature_is_valid: signatureIsValid,
value,
} = claim;
const outpoint = txid + ":" + nout;
const title = metadata.title;
const channelClaimId = claim.value && claim.value.publisherSignature
? claim.value.publisherSignature.certificateId
: null;
const channelUri = signatureIsValid && hasSignature && channelName
? lbryuri.build({ channelName, claimId: channelClaimId }, false)
: null;
const uriIndicator = <UriIndicator uri={uri} />;
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
const mediaType = lbry.getMediaType(contentType);
const player = require("render-media");
@ -106,7 +60,7 @@ class FilePage extends React.PureComponent {
mediaType === "audio";
return (
<main className="main--single-column">
<div>
<section className="show-page-media">
{isPlayable
? <Video className="video-embedded" uri={uri} />
@ -116,54 +70,28 @@ class FilePage extends React.PureComponent {
</section>
<section className={"card " + (obscureNsfw ? "card--obscured " : "")}>
<div className="card__inner">
<div className="card__title-identity">
{!fileInfo || fileInfo.written_bytes <= 0
? <span style={{ float: "right" }}>
<FilePrice uri={lbryuri.normalize(uri)} />
{isRewardContent && <span>{" "}<IconFeatured /></span>}
</span>
: null}
<h1>{title}</h1>
<div className="card__subtitle">
{channelUri
? <Link
onClick={() =>
this.props.navigate("/show", { uri: channelUri })}
>
{uriIndicator}
</Link>
: uriIndicator}
</div>
<div className="card__actions">
<FileActions uri={uri} />
</div>
</div>
<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>
{metadata && claim
? <div className="card__content">
<FormatItem
metadata={metadata}
contentType={contentType}
claim={claim}
/>
</div>
: ""}
<div className="card__content">
<Link
href={`https://lbry.io/dmca?claim_id=${claim.claim_id}`}
label={__("report")}
className="button-text-help"
/>
{(!tab || tab === "details") &&
<div>
{" "} {" "}
<div className="card__title-identity">
{!fileInfo || fileInfo.written_bytes <= 0
? <span style={{ float: "right" }}>
<FilePrice uri={lbryuri.normalize(uri)} />
{isRewardContent && <span>{" "}<IconFeatured /></span>}
</span>
: null}
<h1>{title}</h1>
<div className="card__subtitle">
<UriIndicator uri={uri} link={true} />
</div>
</div>
<FileDetails uri={uri} />
</div>}
{tab === "tip" &&
<WalletSendTip claim_id={claim.claim_id} uri={uri} />}
</div>
</section>
</main>
</div>
);
}
}

View file

@ -1,14 +1,17 @@
import React from "react";
import { connect } from "react-redux";
import * as settings from "constants/settings";
import { doClearCache } from "actions/app";
import {
doSetDaemonSetting,
doSetClientSetting,
doGetThemes,
doSetTheme,
doChangeLanguage,
} from "actions/settings";
import {
makeSelectClientSetting,
selectDaemonSettings,
selectShowNsfw,
selectLanguages,
} from "selectors/settings";
import { selectCurrentLanguage } from "selectors/app";
@ -16,7 +19,10 @@ import SettingsPage from "./view";
const select = state => ({
daemonSettings: selectDaemonSettings(state),
showNsfw: selectShowNsfw(state),
showNsfw: makeSelectClientSetting(settings.SHOW_NSFW)(state),
showUnavailable: makeSelectClientSetting(settings.SHOW_UNAVAILABLE)(state),
theme: makeSelectClientSetting(settings.THEME)(state),
themes: makeSelectClientSetting(settings.THEMES)(state),
language: selectCurrentLanguage(state),
languages: selectLanguages(state),
});
@ -25,6 +31,7 @@ const perform = dispatch => ({
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
clearCache: () => dispatch(doClearCache()),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
getThemes: () => dispatch(doGetThemes()),
changeLanguage: newLanguage => dispatch(doChangeLanguage(newLanguage)),
});

View file

@ -6,20 +6,13 @@ import * as settings from "constants/settings";
import lbry from "lbry.js";
import Link from "component/link";
import FormFieldPrice from "component/formFieldPrice";
const { remote } = require("electron");
import { remote } from "electron";
class SettingsPage extends React.PureComponent {
constructor(props) {
super(props);
const { daemonSettings } = this.props || {};
this.state = {
// isMaxUpload: daemonSettings && daemonSettings.max_upload != 0,
// isMaxDownload: daemonSettings && daemonSettings.max_download != 0,
showUnavailable: lbry.getClientSetting(settings.SHOW_UNAVAILABLE),
language: lbry.getClientSetting(settings.LANGUAGE),
clearingCache: false,
};
}
@ -41,11 +34,6 @@ class SettingsPage extends React.PureComponent {
this.props.setDaemonSetting(name, value);
}
setClientSetting(name, value) {
lbry.setClientSetting(name, value);
this._onSettingSaveSuccess();
}
onRunOnStartChange(event) {
this.setDaemonSetting("run_on_startup", event.target.checked);
}
@ -66,6 +54,11 @@ class SettingsPage extends React.PureComponent {
this.setDaemonSetting("disable_max_key_fee", isDisabled);
}
onThemeChange(event) {
const { value } = event.target;
this.props.setClientSetting(settings.THEME, value);
}
// onMaxUploadPrefChange(isLimited) {
// if (!isLimited) {
// this.setDaemonSetting("max_upload", 0.0);
@ -101,10 +94,29 @@ class SettingsPage extends React.PureComponent {
this.forceUpdate();
}
onShowUnavailableChange(event) {}
onShowUnavailableChange(event) {
this.props.setClientSetting(
settings.SHOW_UNAVAILABLE,
event.target.checked
);
}
componentWillMount() {
this.props.getThemes();
}
componentDidMount() {}
render() {
const { daemonSettings, language, languages } = this.props;
const {
daemonSettings,
language,
languages,
showNsfw,
showUnavailable,
theme,
themes,
} = this.props;
if (!daemonSettings || Object.keys(daemonSettings).length === 0) {
return (
@ -209,7 +221,7 @@ class SettingsPage extends React.PureComponent {
<FormRow
type="checkbox"
onChange={this.onShowUnavailableChange.bind(this)}
defaultChecked={this.state.showUnavailable}
defaultChecked={showUnavailable}
label={__("Show unavailable content in search results")}
/>
</div>
@ -218,7 +230,7 @@ class SettingsPage extends React.PureComponent {
label={__("Show NSFW content")}
type="checkbox"
onChange={this.onShowNsfwChange.bind(this)}
defaultChecked={this.props.showNsfw}
defaultChecked={showNsfw}
helper={__(
"NSFW content may include nudity, intense sexuality, profanity, or other adult content. By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction. "
)}
@ -242,6 +254,27 @@ class SettingsPage extends React.PureComponent {
</div>
</section>
<section className="card">
<div className="card__content">
<h3>{__("Theme")}</h3>
</div>
<div className="card__content">
<FormField
type="select"
onChange={this.onThemeChange.bind(this)}
defaultValue={theme}
className="form-field__input--inline"
>
{themes.map((theme, index) =>
<option key={theme} value={theme}>
{theme}
</option>
)}
</FormField>
</div>
</section>
<section className="card">
<div className="card__content">
<h3>{__("Application Cache")}</h3>

View file

@ -2,23 +2,16 @@ import React from "react";
import { connect } from "react-redux";
import { doResolveUri } from "actions/content";
import { makeSelectClaimForUri } from "selectors/claims";
import { makeSelectIsResolvingForUri } from "selectors/content";
import { makeSelectIsUriResolving } from "selectors/content";
import ShowPage from "./view";
const makeSelect = () => {
const selectClaim = makeSelectClaimForUri(),
selectIsResolving = makeSelectIsResolvingForUri();
const select = (state, props) => ({
claim: selectClaim(state, props.params),
isResolvingUri: selectIsResolving(state, props.params),
});
return select;
};
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
});
const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)),
});
export default connect(makeSelect, perform)(ShowPage);
export default connect(select, perform)(ShowPage);

View file

@ -6,15 +6,13 @@ import FilePage from "page/file";
class ShowPage extends React.PureComponent {
componentWillMount() {
const { isResolvingUri, resolveUri, params } = this.props;
const { uri } = params;
const { isResolvingUri, resolveUri, uri } = this.props;
if (!isResolvingUri) resolveUri(uri);
}
componentWillReceiveProps(nextProps) {
const { isResolvingUri, resolveUri, claim, params } = nextProps;
const { uri } = params;
const { isResolvingUri, resolveUri, claim, uri } = nextProps;
if (!isResolvingUri && claim === undefined && uri) {
resolveUri(uri);
@ -22,8 +20,7 @@ class ShowPage extends React.PureComponent {
}
render() {
const { claim, params, isResolvingUri } = this.props;
const { uri } = params;
const { claim, isResolvingUri, uri } = this.props;
let innerContent = "";

View file

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

View file

@ -8,6 +8,8 @@ const win = remote.BrowserWindow.getFocusedWindow();
const reducers = {};
const defaultState = {
isLoaded: false,
modal: null,
modalProps: {},
platform: process.platform,
upgradeSkipped: sessionStorage.getItem("upgradeSkipped"),
daemonVersionMatched: null,
@ -76,14 +78,14 @@ reducers[types.UPDATE_VERSION] = function(state, action) {
reducers[types.OPEN_MODAL] = function(state, action) {
return Object.assign({}, state, {
modal: action.data.modal,
modalExtraContent: action.data.extraContent,
modalProps: action.data.modalProps || {},
});
};
reducers[types.CLOSE_MODAL] = function(state, action) {
return Object.assign({}, state, {
modal: undefined,
modalExtraContent: undefined,
modalProps: {},
});
};

View file

@ -1,6 +1,7 @@
import * as types from "constants/action_types";
const reducers = {};
const defaultState = {};
reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
@ -49,21 +50,23 @@ reducers[types.FETCH_CLAIM_LIST_MINE_COMPLETED] = function(state, action) {
.filter(claimId => Object.keys(abandoningById).indexOf(claimId) === -1)
);
claims.filter(claim => claim.category.match(/claim/)).forEach(claim => {
byId[claim.claim_id] = claim;
claims
.filter(claim => claim.category && claim.category.match(/claim/))
.forEach(claim => {
byId[claim.claim_id] = claim;
const pending = Object.values(pendingById).find(pendingClaim => {
return (
pendingClaim.name == claim.name &&
pendingClaim.channel_name == claim.channel_name
);
const pending = Object.values(pendingById).find(pendingClaim => {
return (
pendingClaim.name == claim.name &&
pendingClaim.channel_name == claim.channel_name
);
});
if (pending) {
delete pendingById[pending.claim_id];
}
});
if (pending) {
delete pendingById[pending.claim_id];
}
});
// Remove old timed out pending publishes
const old = Object.values(pendingById)
.filter(pendingClaim => Date.now() - pendingClaim.time >= 20 * 60 * 1000)

View file

@ -2,6 +2,7 @@ import * as types from "constants/action_types";
const reducers = {};
const defaultState = {
playingUri: null,
rewardedContentClaimIds: [],
channelPages: {},
};
@ -58,6 +59,12 @@ reducers[types.RESOLVE_URI_CANCELED] = reducers[
});
};
reducers[types.SET_PLAYING_URI] = (state, action) => {
return Object.assign({}, state, {
playingUri: action.data.uri,
});
};
// reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) {
// const channelPages = Object.assign({}, state.channelPages);
// const { uri, claims } = action.data;

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

View file

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

View file

@ -6,12 +6,15 @@ import lbry from "lbry";
const reducers = {};
const defaultState = {
clientSettings: {
showNsfw: lbry.getClientSetting("showNsfw"),
showNsfw: lbry.getClientSetting(settings.SHOW_NSFW),
showUnavailable: lbry.getClientSetting(settings.SHOW_UNAVAILABLE),
welcome_acknowledged: lbry.getClientSetting(settings.NEW_USER_ACKNOWLEDGED),
credit_intro_acknowledged: lbry.getClientSetting(
settings.CREDIT_INTRO_ACKNOWLEDGED
),
language: lbry.getClientSetting(settings.LANGUAGE),
theme: lbry.getClientSetting(settings.THEME),
themes: lbry.getClientSetting(settings.THEMES),
},
languages: {},
};

View file

@ -10,11 +10,12 @@ const buildDraftTransaction = () => ({
const defaultState = {
balance: undefined,
blocks: {},
transactions: [],
transactions: {},
fetchingTransactions: false,
receiveAddress: address,
gettingNewAddress: false,
draftTransaction: buildDraftTransaction(),
sendingSupport: false,
};
reducers[types.FETCH_TRANSACTIONS_STARTED] = function(state, action) {
@ -24,20 +25,16 @@ reducers[types.FETCH_TRANSACTIONS_STARTED] = function(state, action) {
};
reducers[types.FETCH_TRANSACTIONS_COMPLETED] = function(state, action) {
const oldTransactions = Object.assign({}, state.transactions);
const byId = Object.assign({}, oldTransactions.byId);
let byId = Object.assign({}, state.transactions);
const { transactions } = action.data;
transactions.forEach(transaction => {
byId[transaction.txid] = transaction;
});
const newTransactions = Object.assign({}, oldTransactions, {
byId: byId,
});
return Object.assign({}, state, {
transactions: newTransactions,
transactions: byId,
fetchingTransactions: false,
});
};
@ -125,6 +122,25 @@ reducers[types.SEND_TRANSACTION_FAILED] = function(state, action) {
});
};
reducers[types.SUPPORT_TRANSACTION_STARTED] = function(state, action) {
return Object.assign({}, state, {
sendingSupport: true,
});
};
reducers[types.SUPPORT_TRANSACTION_COMPLETED] = function(state, action) {
return Object.assign({}, state, {
sendingSupport: false,
});
};
reducers[types.SUPPORT_TRANSACTION_FAILED] = function(state, action) {
return Object.assign({}, state, {
error: action.data.error,
sendingSupport: false,
});
};
reducers[types.FETCH_BLOCK_SUCCESS] = (state, action) => {
const { block, block: { height } } = action.data,
blocks = Object.assign({}, state.blocks);

View file

@ -71,9 +71,9 @@ export const selectUpgradeDownloadItem = createSelector(
state => state.downloadItem
);
export const selectModalExtraContent = createSelector(
export const selectModalProps = createSelector(
_selectState,
state => state.modalExtraContent
state => state.modalProps
);
export const selectDaemonReady = createSelector(

View file

@ -7,14 +7,10 @@ export const selectAvailabilityByUri = createSelector(
state => state.byUri || {}
);
const selectAvailabilityForUri = (state, props) => {
return selectAvailabilityByUri(state)[props.uri];
};
export const makeSelectIsAvailableForUri = () => {
export const makeSelectIsAvailableForUri = uri => {
return createSelector(
selectAvailabilityForUri,
availability => (availability === undefined ? undefined : availability > 0)
selectAvailabilityByUri,
byUri => (!byUri || byUri[uri] === undefined ? undefined : byUri[uri] > 0)
);
};
@ -23,10 +19,9 @@ export const selectFetchingAvailability = createSelector(
state => state.fetching || {}
);
const selectFetchingAvailabilityForUri = (state, props) => {
return selectFetchingAvailability(state)[props.uri];
};
export const makeSelectFetchingAvailabilityForUri = () => {
return createSelector(selectFetchingAvailabilityForUri, fetching => fetching);
export const makeSelectFetchingAvailabilityForUri = uri => {
return createSelector(
selectFetchingAvailability,
byUri => byUri && byUri[uri]
);
};

View file

@ -40,25 +40,24 @@ export const selectAllClaimsByChannel = createSelector(
state => state.claimsByChannel || {}
);
const selectClaimForUri = (state, props) => {
const uri = lbryuri.normalize(props.uri);
return selectClaimsByUri(state)[uri];
export const makeSelectClaimForUri = uri => {
return createSelector(
selectClaimsByUri,
claims => claims && claims[lbryuri.normalize(uri)]
);
};
export const makeSelectClaimForUri = () => {
return createSelector(selectClaimForUri, claim => claim);
};
const selectClaimForUriIsMine = (state, props) => {
const uri = lbryuri.normalize(props.uri);
const claim = selectClaimsByUri(state)[uri];
const myClaims = selectMyClaimsRaw(state);
return myClaims.has(claim.claim_id);
};
export const makeSelectClaimForUriIsMine = () => {
return createSelector(selectClaimForUriIsMine, isMine => isMine);
export const makeSelectClaimIsMine = rawUri => {
const uri = lbryuri.normalize(rawUri);
return createSelector(
selectClaimsByUri,
selectMyClaimsRaw,
(claims, myClaims) =>
claims &&
claims[uri] &&
claims[uri].claim_id &&
myClaims.has(claims[uri].claim_id)
);
};
export const selectAllFetchingChannelClaims = createSelector(
@ -66,86 +65,53 @@ export const selectAllFetchingChannelClaims = createSelector(
state => state.fetchingChannelClaims || {}
);
const selectFetchingChannelClaims = (state, props) => {
const allFetchingChannelClaims = selectAllFetchingChannelClaims(state);
return allFetchingChannelClaims[props.uri];
};
export const makeSelectFetchingChannelClaims = (state, props) => {
return createSelector(selectFetchingChannelClaims, fetching => fetching);
};
export const selectClaimsInChannelForUri = (state, props) => {
const byId = selectClaimsById(state);
const byChannel = selectAllClaimsByChannel(state)[props.uri] || {};
const claimIds = byChannel["all"];
if (!claimIds) return claimIds;
const claims = [];
claimIds.forEach(claimId => claims.push(byId[claimId]));
return claims;
};
export const makeSelectClaimsInChannelForUri = () => {
return createSelector(selectClaimsInChannelForUri, claims => claims);
};
export const selectClaimsInChannelForCurrentPage = (state, props) => {
const byId = selectClaimsById(state);
const byChannel = selectAllClaimsByChannel(state)[props.uri] || {};
const params = selectCurrentParams(state);
const page = params && params.page ? params.page : 1;
const claimIds = byChannel[page];
if (!claimIds) return claimIds;
const claims = [];
claimIds.forEach(claimId => claims.push(byId[claimId]));
return claims;
};
export const makeSelectClaimsInChannelForCurrentPage = () => {
return createSelector(selectClaimsInChannelForCurrentPage, claims => claims);
};
const selectMetadataForUri = (state, props) => {
const claim = selectClaimForUri(state, props);
const metadata =
claim && claim.value && claim.value.stream && claim.value.stream.metadata;
const value = metadata ? metadata : claim === undefined ? undefined : null;
return value;
};
export const makeSelectMetadataForUri = () => {
return createSelector(selectMetadataForUri, metadata => metadata);
};
const selectSourceForUri = (state, props) => {
const claim = selectClaimForUri(state, props);
const source =
claim && claim.value && claim.value.stream && claim.value.stream.source;
return source ? source : claim === undefined ? undefined : null;
};
export const makeSelectSourceForUri = () => {
return createSelector(selectSourceForUri, source => source);
};
export const makeSelectContentTypeForUri = () => {
export const makeSelectFetchingChannelClaims = uri => {
return createSelector(
selectSourceForUri,
source => (source ? source.contentType : source)
selectAllFetchingChannelClaims,
fetching => fetching && fetching[uri]
);
};
export const makeSelectClaimsInChannelForCurrentPage = (uri, page = 1) => {
return createSelector(
selectClaimsById,
selectAllClaimsByChannel,
(byId, allClaims) => {
const byChannel = allClaims[uri] || {};
const claimIds = byChannel[page];
if (!claimIds) return claimIds;
return claimIds.map(claimId => byId[claimId]);
}
);
};
export const makeSelectMetadataForUri = uri => {
return createSelector(makeSelectClaimForUri(uri), claim => {
const metadata =
claim && claim.value && claim.value.stream && claim.value.stream.metadata;
const value = metadata ? metadata : claim === undefined ? undefined : null;
return value;
});
};
export const makeSelectTitleForUri = uri => {
return createSelector(
makeSelectMetadataForUri(uri),
metadata => metadata && metadata.title
);
};
export const makeSelectContentTypeForUri = uri => {
return createSelector(makeSelectClaimForUri(uri), claim => {
const source =
claim && claim.value && claim.value.stream && claim.value.stream.source;
return source ? source.contentType : undefined;
});
};
export const selectIsFetchingClaimListMine = createSelector(
_selectState,
state => !!state.isFetchingClaimListMine

View file

@ -17,12 +17,16 @@ export const selectResolvingUris = createSelector(
state => state.resolvingUris || []
);
const selectResolvingUri = (state, props) => {
return selectResolvingUris(state).indexOf(props.uri) != -1;
};
export const selectPlayingUri = createSelector(
_selectState,
state => state.playingUri
);
export const makeSelectIsResolvingForUri = () => {
return createSelector(selectResolvingUri, resolving => resolving);
export const makeSelectIsUriResolving = uri => {
return createSelector(
selectResolvingUris,
resolvingUris => resolvingUris && resolvingUris.indexOf(uri) != -1
);
};
export const selectChannelPages = createSelector(
@ -30,12 +34,8 @@ export const selectChannelPages = createSelector(
state => state.channelPages || {}
);
const selectTotalPagesForChannel = (state, props) => {
return selectChannelPages(state)[props.uri];
};
export const makeSelectTotalPagesForChannel = () => {
return createSelector(selectTotalPagesForChannel, totalPages => totalPages);
export const makeSelectTotalPagesForChannel = uri => {
return createSelector(selectChannelPages, byUri => byUri && byUri[uri]);
};
export const selectRewardContentClaimIds = createSelector(

View file

@ -8,12 +8,11 @@ export const selectAllCostInfoByUri = createSelector(
state => state.byUri || {}
);
export const selectCostInfoForUri = (state, props) => {
return selectAllCostInfoByUri(state)[props.uri];
};
export const makeSelectCostInfoForUri = () => {
return createSelector(selectCostInfoForUri, costInfo => costInfo);
export const makeSelectCostInfoForUri = uri => {
return createSelector(
selectAllCostInfoByUri,
costInfos => costInfos && costInfos[uri]
);
};
export const selectCostForCurrentPageUri = createSelector(
@ -28,10 +27,9 @@ export const selectFetchingCostInfo = createSelector(
state => state.fetching || {}
);
const selectFetchingCostInfoForUri = (state, props) => {
return selectFetchingCostInfo(state)[props.uri];
};
export const makeSelectFetchingCostInfoForUri = () => {
return createSelector(selectFetchingCostInfoForUri, fetching => !!fetching);
export const makeSelectFetchingCostInfoForUri = uri => {
return createSelector(
selectFetchingCostInfo,
fetchingByUri => fetchingByUri && fetchingByUri[uri]
);
};

View file

@ -26,17 +26,17 @@ export const selectIsFetchingFileListDownloadedOrPublished = createSelector(
isFetchingFileList || isFetchingClaimListMine
);
export const selectFileInfoForUri = (state, props) => {
const claims = selectClaimsByUri(state),
claim = claims[props.uri],
byOutpoint = selectFileInfosByOutpoint(state),
outpoint = claim ? `${claim.txid}:${claim.nout}` : undefined;
export const makeSelectFileInfoForUri = uri => {
return createSelector(
selectClaimsByUri,
selectFileInfosByOutpoint,
(claims, byOutpoint) => {
const claim = claims[uri],
outpoint = claim ? `${claim.txid}:${claim.nout}` : undefined;
return outpoint ? byOutpoint[outpoint] : undefined;
};
export const makeSelectFileInfoForUri = () => {
return createSelector(selectFileInfoForUri, fileInfo => fileInfo);
return outpoint ? byOutpoint[outpoint] : undefined;
}
);
};
export const selectDownloadingByOutpoint = createSelector(
@ -44,19 +44,14 @@ export const selectDownloadingByOutpoint = createSelector(
state => state.downloadingByOutpoint || {}
);
const selectDownloadingForUri = (state, props) => {
const byOutpoint = selectDownloadingByOutpoint(state);
const fileInfo = selectFileInfoForUri(state, props);
if (!fileInfo) return false;
return byOutpoint[fileInfo.outpoint];
};
export const makeSelectDownloadingForUri = () => {
export const makeSelectDownloadingForUri = uri => {
return createSelector(
selectDownloadingForUri,
downloadingForUri => !!downloadingForUri
selectDownloadingByOutpoint,
makeSelectFileInfoForUri(uri),
(byOutpoint, fileInfo) => {
if (!fileInfo) return false;
return byOutpoint[fileInfo.outpoint];
}
);
};
@ -65,13 +60,8 @@ export const selectUrisLoading = createSelector(
state => state.urisLoading || {}
);
const selectLoadingForUri = (state, props) => {
const byUri = selectUrisLoading(state);
return byUri[props.uri];
};
export const makeSelectLoadingForUri = () => {
return createSelector(selectLoadingForUri, loading => !!loading);
export const makeSelectLoadingForUri = uri => {
return createSelector(selectUrisLoading, byUri => byUri && byUri[uri]);
};
export const selectFileInfosPendingPublish = createSelector(

View file

@ -10,8 +10,11 @@ export const selectCurrentPath = createSelector(
state => state.currentPath
);
export const computePageFromPath = path =>
path.replace(/^\//, "").split("?")[0];
export const selectCurrentPage = createSelector(selectCurrentPath, path => {
return path.replace(/^\//, "").split("?")[0];
return computePageFromPath(path);
});
export const selectCurrentParams = createSelector(selectCurrentPath, path => {
@ -21,6 +24,13 @@ export const selectCurrentParams = createSelector(selectCurrentPath, path => {
return parseQueryParams(path.split("?")[1]);
});
export const makeSelectCurrentParam = param => {
return createSelector(
selectCurrentParams,
params => (params ? params[param] : undefined)
);
};
export const selectHeaderLinks = createSelector(selectCurrentPage, page => {
// This contains intentional fall throughs
switch (page) {
@ -80,7 +90,7 @@ export const selectPageTitle = createSelector(
case "start":
return __("Start");
case "publish":
return __("Publish");
return params.id ? __("Edit") : __("Publish");
case "help":
return __("Help");
case "developer":

View file

@ -19,6 +19,15 @@ export const selectClaimedRewards = createSelector(
byId => Object.values(byId) || []
);
export const selectClaimedRewardsByTransactionId = createSelector(
selectClaimedRewards,
rewards =>
rewards.reduce((map, reward) => {
map[reward.transaction_id] = reward;
return map;
}, {})
);
export const selectUnclaimedRewards = createSelector(
selectUnclaimedRewardsByType,
byType =>

View file

@ -1,11 +1,16 @@
import { createSelector } from "reselect";
import { selectPageTitle, selectCurrentPage } from "selectors/navigation";
import {
selectPageTitle,
selectCurrentPage,
selectCurrentParams,
} from "selectors/navigation";
export const _selectState = state => state.search || {};
export const selectSearchQuery = createSelector(
_selectState,
state => state.query
selectCurrentPage,
selectCurrentParams,
(page, params) => (page === "search" ? params && params.query : null)
);
export const selectIsSearching = createSelector(
@ -36,45 +41,49 @@ export const selectWunderBarAddress = createSelector(
(page, title, query) => (page != "search" ? title : query ? query : title)
);
export const selectWunderBarIcon = createSelector(selectCurrentPage, page => {
switch (page) {
case "auth":
return "icon-user";
case "search":
return "icon-search";
case "settings":
return "icon-gear";
case "help":
return "icon-question";
case "report":
return "icon-file";
case "downloaded":
return "icon-folder";
case "published":
return "icon-folder";
case "history":
return "icon-history";
case "send":
return "icon-send";
case "rewards":
return "icon-rocket";
case "invite":
return "icon-envelope-open";
case "address":
case "receive":
return "icon-address-book";
case "wallet":
case "backup":
return "icon-bank";
case "show":
return "icon-file";
case "publish":
return "icon-upload";
case "developer":
return "icon-code";
case "discover":
return "icon-home";
default:
return "icon-file";
export const selectWunderBarIcon = createSelector(
selectCurrentPage,
selectCurrentParams,
(page, params) => {
switch (page) {
case "auth":
return "icon-user";
case "search":
return "icon-search";
case "settings":
return "icon-gear";
case "help":
return "icon-question";
case "report":
return "icon-file";
case "downloaded":
return "icon-folder";
case "published":
return "icon-folder";
case "history":
return "icon-history";
case "send":
return "icon-send";
case "rewards":
return "icon-rocket";
case "invite":
return "icon-envelope-open";
case "address":
case "receive":
return "icon-address-book";
case "wallet":
case "backup":
return "icon-bank";
case "show":
return "icon-file";
case "publish":
return params.id ? __("icon-pencil") : __("icon-upload");
case "developer":
return "icon-code";
case "discover":
return "icon-home";
default:
return "icon-file";
}
}
});
);

View file

@ -1,3 +1,4 @@
import * as settings from "constants/settings";
import { createSelector } from "reselect";
const _selectState = state => state.settings || {};
@ -24,12 +25,15 @@ export const selectSettingsIsGenerous = createSelector(
settings => settings && settings.is_generous_host
);
export const selectShowNsfw = createSelector(
selectClientSettings,
clientSettings => !!clientSettings.showNsfw
);
//refactor me
export const selectShowNsfw = makeSelectClientSetting(settings.SHOW_NSFW);
export const selectLanguages = createSelector(
_selectState,
state => state.languages || {}
);
export const selectThemePath = createSelector(
makeSelectClientSetting(settings.THEME),
theme => "themes/" + (theme || "light") + ".css"
);

View file

@ -7,30 +7,70 @@ export const selectBalance = createSelector(
state => state.balance
);
export const selectTransactions = createSelector(
_selectState,
state => state.transactions || {}
);
export const selectTransactionsById = createSelector(
selectTransactions,
transactions => transactions.byId || {}
_selectState,
state => state.transactions
);
export const selectTransactionItems = createSelector(
selectTransactionsById,
byId => {
const transactionItems = [];
const txids = Object.keys(byId);
txids.forEach(txid => {
const items = [];
Object.keys(byId).forEach(txid => {
const tx = byId[txid];
transactionItems.push({
id: txid,
date: tx.timestamp ? new Date(parseInt(tx.timestamp) * 1000) : null,
amount: parseFloat(tx.value),
});
//ignore dust/fees
if (Math.abs(tx.amount) === Math.abs(tx.fee)) {
return;
}
let append = [];
append.push(
...tx.claim_info.map(item =>
Object.assign({}, item, {
type: item.claim_name[0] === "@" ? "channel" : "publish",
})
)
);
append.push(
...tx.support_info.map(item =>
Object.assign({}, item, { type: !item.is_tip ? "support" : "tip" })
)
);
append.push(
...tx.update_info.map(item =>
Object.assign({}, item, { type: "update" })
)
);
if (!append.length) {
append.push(
Object.assign({}, tx, {
type: tx.value < 0 ? "spend" : "receive",
})
);
}
items.push(
...append.map(item => {
const amount = parseFloat(item.value || -1 * item.amount); //it's value on a transaction, amount on an outpoint (which has the sign the opposite way)
return {
txid: txid,
date: tx.timestamp ? new Date(parseInt(tx.timestamp) * 1000) : null,
amount: amount,
fee: amount < 0 ? -1 * tx.fee / append.length : 0,
claim_id: item.claim_id,
claim_name: item.claim_name,
type: item.type || "send",
nout: item.nout,
};
})
);
});
return transactionItems.reverse();
return items.reverse();
}
);
@ -57,6 +97,11 @@ export const selectIsFetchingTransactions = createSelector(
state => state.fetchingTransactions
);
export const selectIsSendingSupport = createSelector(
_selectState,
state => state.sendingSupport
);
export const selectReceiveAddress = createSelector(
_selectState,
state => state.receiveAddress

View file

@ -1,195 +0,0 @@
@charset "UTF-8";
$spacing-vertical: 24px;
$padding-button: $spacing-vertical * 2/3;
$padding-text-link: 0px;
$color-primary: #155B4A;
$color-primary-light: saturate(lighten($color-primary, 50%), 20%);
$color-light-alt: hsl(hue($color-primary), 15, 85);
$color-text-dark: #000;
$color-black-transparent: rgba(32,32,32,0.9);
$color-help: rgba(0,0,0,.6);
$color-notice: #8a6d3b;
$color-error: #a94442;
$color-load-screen-text: #c3c3c3;
$color-canvas: #f5f5f5;
$color-bg: #ffffff;
$color-bg-alt: #D9D9D9;
$color-money: #216C2A;
$color-meta-light: #505050;
$color-form-border: rgba(160,160,160,.5);
$font-size: 16px;
$font-line-height: 1.3333;
$mobile-width-threshold: 801px;
$max-content-width: 1000px;
$max-text-width: 660px;
$width-page-constrained: 800px;
$width-input-text: 330px;
$height-button: $spacing-vertical * 1.5;
$height-header: $spacing-vertical * 2.5;
$height-video-embedded: $width-page-constrained * 9 / 16;
$box-shadow-layer: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
$box-shadow-focus: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12);
$transition-standard: .225s ease;
$blur-intensity-nsfw: 20px;
@mixin clearfix()
{
&:before, &:after
{
content: " ";
display: table;
}
&:after
{
clear: both;
}
}
@mixin border-radius($radius)
{
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
-ms-border-radius: $radius;
border-radius: $radius;
}
@mixin placeholder-color($color) {
/*do not group these it breaks because CSS*/
&:-moz-placeholder {
color: $color;
}
&::-moz-placeholder {
color: $color;
}
&:-ms-input-placeholder {
color: $color;
}
&::-webkit-input-placeholder {
color: $color;
}
}
@mixin display-flex()
{
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
}
@mixin flex($columns)
{
-webkit-flex: $columns;
-moz-flex: $columns;
-ms-flex: $columns;
flex: $columns;
}
@mixin flex-flow($flow) {
-webkit-flex-flow: $flow;
-moz-flex-flow: $flow;
-ms-flex-flow: $flow;
flex-flow: $flow;
}
@mixin flex-direction($direction) {
-webkit-flex-direction: $direction;
-moz-flex-direction: $direction;
-ms-flex-direction: $direction;
flex-direction: $direction;
}
@mixin absolute-center()
{
@include display-flex();
-webkit-box-align: center;
-moz-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
-webkit-box-pack: center;
-moz-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
}
@mixin linear-gradient($from-color, $to-color) {
background-color: $to-color; /* Fallback Color */
background-image: -webkit-linear-gradient(top, $from-color, $to-color); /* Chrome 10+, Saf5.1+, iOS 5+ */
background-image: -moz-linear-gradient(top, $from-color, $to-color); /* FF3.6 */
background-image: -ms-linear-gradient(top, $from-color, $to-color); /* IE10 */
background-image: linear-gradient(top, $from-color, $to-color);
}
@mixin box-sizing( $type: border-box ) {
-webkit-box-sizing: $type;
-moz-box-sizing: $type;
-o-box-sizing: $type;
-ms-box-sizing: $type;
box-sizing: $type;
}
@mixin background-size ($size) {
-webkit-background-size: $size;
-moz-background-size: $size;
-o-background-size: $size;
background-size: $size;
}
@mixin placeholder {
&::-webkit-input-placeholder {@content}
&:-moz-placeholder {@content}
&:-ms-input-placeholder {@content}
}
@mixin offscreen() {
position: absolute;
left: -9999px;
top:auto;
width:1px;
height:1px;
overflow:hidden;
}
@mixin text-link($color: $color-primary, $hover-opacity: 0.70) {
.icon
{
&:first-child {
padding-right: 5px;
}
&:last-child:not(:only-child) {
padding-left: 5px;
}
}
&:not(.no-underline) {
text-decoration: underline;
.icon {
text-decoration: none;
}
}
&:hover
{
opacity: $hover-opacity;
transition: opacity $transition-standard;
text-decoration: underline;
.icon {
text-decoration: none;
}
}
color: $color;
cursor: pointer;
}

View file

@ -1,40 +1,42 @@
@import "global";
html
{
height: 100%;
font-size: $font-size;
font-size: var(--font-size);
}
body
{
color: var(--text-color);
font-family: 'Source Sans Pro', sans-serif;
line-height: $font-line-height;
line-height: var(--font-line-height);
}
/* Custom text selection */
*::selection {
background: var(--text-selection-bg);
color: var(--text-selection-color);
}
#window
{
min-height: 100vh;
background: $color-canvas;
background: var(--window-bg);
}
.badge
{
background: $color-money;
display: inline-block;
padding: 2px;
color: white;
border-radius: 2px;
}
.credit-amount--indicator
{
font-weight: bold;
color: $color-money;
color: var(--color-money);
}
.credit-amount--fee
{
font-size: 0.9em;
color: var(--color-meta-light);
}
#main-content
{
padding: $spacing-vertical;
margin-top: $height-header;
margin-top: var(--header-height);
display: flex;
flex-direction: column;
main {
@ -47,7 +49,8 @@ body
width: $width-page-constrained;
}
}
main.main--refreshing {
.reloading {
&:before {
$width: 30px;
position: absolute;
@ -87,7 +90,7 @@ sub { top: 0.4em; }
code {
font: 0.8em Consolas, 'Lucida Console', 'Source Sans', monospace;
background-color: #eee;
background-color: var(--color-bg-alt);
}
p
@ -136,18 +139,18 @@ p
.help {
font-size: .85em;
color: $color-help;
color: var(--color-help);
}
.meta
{
font-size: 0.9em;
color: $color-meta-light;
color: var(--color-meta-light);
}
.empty
{
color: $color-meta-light;
color:var(--color-meta-light);
font-style: italic;
}
@ -167,7 +170,7 @@ p
text-align: right;
line-height: 1;
font-size: 0.85em;
color: $color-help;
color: var(--color-help);
}
section.section-spaced {

View file

@ -1,5 +1,3 @@
@import "global";
@font-face {
font-family: 'FontAwesome';
src: url('../font/fontawesome-webfont.eot?v=4.7.0');

Some files were not shown because too many files have changed in this diff Show more