transaction refactor / cleanup / improvement

This commit is contained in:
Jeremy Kauffman 2017-09-17 20:52:57 -04:00
parent f68136bd79
commit 65f65f1aea
16 changed files with 257 additions and 259 deletions

View file

@ -8,15 +8,15 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased]
### Added
* Added a tipping button to send LBRY Credits to the publisher
* Added edit button on published content / improved UX for editing claims.
* Added a tipping button to send LBRY Credits to a creator.
* Added an edit button on published content. Significantly improved UX for editing claims.
* 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
*
*
### Fixed
* URLs on cards no longer wrap and show an ellipsis if longer than one line

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"]),
fee: React.PropTypes.bool,
};
@ -79,7 +79,6 @@ export class CreditAmount extends React.PureComponent {
label: true,
showFree: false,
look: "indicator",
showFree: false,
showFullPrice: false,
showPlus: false,
fee: false,
@ -119,10 +118,7 @@ export class CreditAmount extends React.PureComponent {
return (
<span
className={`credit-amount credit-amount--${this.props.look} ${this.props
.fee
? " meta"
: ""}`}
className={`credit-amount credit-amount--${this.props.look}`}
title={fullPrice}
>
<span>

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

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

View file

@ -1,153 +0,0 @@
import React from "react";
import LinkTransaction from "component/linkTransaction";
import { CreditAmount } from "component/common";
class TransactionTableBody extends React.PureComponent {
constructor(props) {
super(props);
}
getClaimLink(claim_name, claim_id) {
let uri = `lbry://${claim_name}#${claim_id}`;
return (
<a className="button-text" onClick={() => this.props.navigate(uri)}>
{claim_name}
</a>
);
}
filterList(transaction) {
if (this.props.filter == "claim") {
return transaction.claim_info.length > 0;
} else if (this.props.filter == "support") {
return transaction.support_info.length > 0;
} else if (this.props.filter == "update") {
return transaction.update_info.length > 0;
} else {
return transaction;
}
}
renderBody(transaction) {
const txid = transaction.id;
const date = transaction.date;
const fee = transaction.fee;
const filter = this.props.filter;
const options = {
weekday: "short",
year: "2-digit",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
if (filter == "tipSupport")
transaction["tipSupport_info"] = transaction["support_info"].filter(
tx => tx.is_tip
);
return filter != "unfiltered"
? transaction[`${filter}_info`].map(item => {
return (
<tr key={`${txid}:${item.nout}`}>
<td>
{date
? date.toLocaleDateString("en-US", options)
: <span className="empty">
{__("(Transaction pending)")}
</span>}
</td>
<td>
<CreditAmount
amount={item.amount}
look="plain"
label={false}
showPlus={true}
precision={8}
/>
<br />
<CreditAmount
amount={fee}
look="plain"
fee={true}
label={false}
precision={8}
/>
</td>
<td>
{this.getClaimLink(item.claim_name, item.claim_id)}
</td>
<td>
<LinkTransaction id={txid} />
</td>
</tr>
);
})
: <tr key={txid}>
<td>
{date
? date.toLocaleDateString("en-US", options)
: <span className="empty">
{__("(Transaction pending)")}
</span>}
</td>
<td>
<CreditAmount
amount={transaction.amount}
look="plain"
label={false}
showPlus={true}
precision={8}
/>
<br />
<CreditAmount
amount={fee}
look="plain"
fee={true}
label={false}
precision={8}
/>
</td>
<td>
<LinkTransaction id={txid} />
</td>
</tr>;
}
removeFeeTx(transaction) {
if (this.props.filter == "unfiltered")
return Math.abs(transaction.amount) != Math.abs(transaction.fee);
else return true;
}
render() {
const { transactions, filter } = this.props;
let transactionList = transactions
.filter(this.filterList, this)
.filter(this.removeFeeTx, this)
.map(this.renderBody, this);
if (transactionList.length == 0) {
return (
<tbody>
<tr>
<td className="empty" colSpan="3">
{__("There are no transactions of this type.")}
</td>
</tr>
</tbody>
);
}
return (
<tbody>
{transactionList}
</tbody>
);
}
}
export default TransactionTableBody;

View file

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

View file

@ -0,0 +1,78 @@
import React from "react";
import LinkTransaction from "component/linkTransaction";
import { CreditAmount } from "component/common";
import DateTime from "component/dateTime";
import Link from "component/link";
import lbryuri from "lbryuri";
class TransactionListItem extends React.PureComponent {
render() {
const { reward, transaction } = this.props;
const {
amount,
claim_id: claimId,
claim_name: name,
date,
fee,
txid,
type,
} = transaction;
return (
<tr>
<td>
{date
? <div>
<DateTime date={date} show={DateTime.SHOW_DATE} />
<div className="meta">
<DateTime date={date} show={DateTime.SHOW_TIME} />
</div>
</div>
: <span className="empty">
{__("(Transaction pending)")}
</span>}
</td>
<td>
<CreditAmount
amount={amount}
look="plain"
label={false}
showPlus={true}
precision={8}
/>
<br />
{fee != 0 &&
<CreditAmount
amount={fee}
look="fee"
label={false}
precision={8}
/>}
</td>
<td>
{type}
</td>
<td>
{reward &&
<Link navigate="/rewards">
{__("Reward: %s", reward.reward_title)}
</Link>}
{name &&
claimId &&
<Link
className="button-text"
navigate="/show"
navigateParams={{ uri: lbryuri.build({ name, claimId }) }}
>
{name}
</Link>}
</td>
<td>
<LinkTransaction id={txid} />
</td>
</tr>
);
}
}
export default TransactionListItem;

View file

@ -1,6 +1,5 @@
import React from "react";
import TransactionTableHeader from "./internal/TransactionListHeader";
import TransactionTableBody from "./internal/TransactionListBody";
import TransactionListItem from "./internal/TransactionListItem";
import FormField from "component/formField";
class TransactionList extends React.PureComponent {
@ -8,7 +7,7 @@ class TransactionList extends React.PureComponent {
super(props);
this.state = {
filter: "unfiltered",
filter: null,
};
}
@ -18,45 +17,63 @@ class TransactionList extends React.PureComponent {
});
}
handleClaimNameClicked(uri) {
this.props.navigate("/show", { uri });
filterTransaction(transaction) {
const { filter } = this.state;
return !filter || filter == transaction.type;
}
render() {
const { emptyMessage, transactions } = this.props;
const { filter } = this.state;
const { emptyMessage, rewards, transactions } = this.props;
if (!transactions || !transactions.length) {
return (
<div className="empty">
{emptyMessage || __("No transactions to list.")}
</div>
);
}
let transactionList = transactions.filter(
this.filterTransaction.bind(this)
);
return (
<div>
<span className="sort-section">
{__("Filter")} {" "}
<FormField
type="select"
onChange={this.handleFilterChanged.bind(this)}
>
<option value="unfiltered">{__("All")}</option>
<option value="claim">{__("Publishes")}</option>
<option value="support">{__("Supports")}</option>
<option value="tipSupport">{__("Tips")}</option>
<option value="update">{__("Updates")}</option>
</FormField>
</span>
<table className="table-standard table-stretch">
<TransactionTableHeader filter={filter} />
<TransactionTableBody
transactions={transactions}
filter={filter}
navigate={this.handleClaimNameClicked.bind(this)}
/>
</table>
{(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>
);
}

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

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

@ -10,7 +10,7 @@ const buildDraftTransaction = () => ({
const defaultState = {
balance: undefined,
blocks: {},
transactions: [],
transactions: {},
fetchingTransactions: false,
receiveAddress: address,
gettingNewAddress: false,
@ -25,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,
});
};

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

@ -7,34 +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),
claim_info: tx.claim_info,
support_info: tx.support_info,
update_info: tx.update_info,
fee: tx.fee,
});
//ignore dust/fees
if (Math.abs(tx.amount) === Math.abs(tx.fee)) {
return;
}
let append = [];
append.push(
...tx.claim_info.map(item =>
Object.assign({}, 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();
}
);

View file

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

View file

@ -60,3 +60,11 @@ table.table-standard {
table.table-stretch {
width: 100%;
}
table.table-transactions {
td:nth-of-type(1) { width: 15%; }
td:nth-of-type(2) { width: 15%; }
td:nth-of-type(3) { width: 15%; }
td:nth-of-type(4) { width: 40%; }
td:nth-of-type(5) { width: 15%; }
}