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] ## [Unreleased]
### Added ### Added
* Added a tipping button to send LBRY Credits to the publisher * Added a tipping button to send LBRY Credits to a creator.
* Added edit button on published content / improved UX for editing claims. * 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. * File pages now show the time of a publish.
* The "auth token" displayable on Help offers security warning * The "auth token" displayable on Help offers security warning
* Added a new component for rendering dates and times. This component can render the date and time of a block height, as well. * Added a new component for rendering dates and times. This component can render the date and time of a block height, as well.
### Changed ### Changed
* *
*
### Fixed ### Fixed
* URLs on cards no longer wrap and show an ellipsis if longer than one line * 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, showFree: React.PropTypes.bool,
showFullPrice: React.PropTypes.bool, showFullPrice: React.PropTypes.bool,
showPlus: React.PropTypes.bool, showPlus: React.PropTypes.bool,
look: React.PropTypes.oneOf(["indicator", "plain"]), look: React.PropTypes.oneOf(["indicator", "plain", "fee"]),
fee: React.PropTypes.bool, fee: React.PropTypes.bool,
}; };
@ -79,7 +79,6 @@ export class CreditAmount extends React.PureComponent {
label: true, label: true,
showFree: false, showFree: false,
look: "indicator", look: "indicator",
showFree: false,
showFullPrice: false, showFullPrice: false,
showPlus: false, showPlus: false,
fee: false, fee: false,
@ -119,10 +118,7 @@ export class CreditAmount extends React.PureComponent {
return ( return (
<span <span
className={`credit-amount credit-amount--${this.props.look} ${this.props className={`credit-amount credit-amount--${this.props.look}`}
.fee
? " meta"
: ""}`}
title={fullPrice} title={fullPrice}
> >
<span> <span>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ const buildDraftTransaction = () => ({
const defaultState = { const defaultState = {
balance: undefined, balance: undefined,
blocks: {}, blocks: {},
transactions: [], transactions: {},
fetchingTransactions: false, fetchingTransactions: false,
receiveAddress: address, receiveAddress: address,
gettingNewAddress: false, gettingNewAddress: false,
@ -25,20 +25,16 @@ reducers[types.FETCH_TRANSACTIONS_STARTED] = function(state, action) {
}; };
reducers[types.FETCH_TRANSACTIONS_COMPLETED] = function(state, action) { reducers[types.FETCH_TRANSACTIONS_COMPLETED] = function(state, action) {
const oldTransactions = Object.assign({}, state.transactions); let byId = Object.assign({}, state.transactions);
const byId = Object.assign({}, oldTransactions.byId);
const { transactions } = action.data; const { transactions } = action.data;
transactions.forEach(transaction => { transactions.forEach(transaction => {
byId[transaction.txid] = transaction; byId[transaction.txid] = transaction;
}); });
const newTransactions = Object.assign({}, oldTransactions, {
byId: byId,
});
return Object.assign({}, state, { return Object.assign({}, state, {
transactions: newTransactions, transactions: byId,
fetchingTransactions: false, fetchingTransactions: false,
}); });
}; };

View file

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

View file

@ -7,34 +7,70 @@ export const selectBalance = createSelector(
state => state.balance state => state.balance
); );
export const selectTransactions = createSelector(
_selectState,
state => state.transactions || {}
);
export const selectTransactionsById = createSelector( export const selectTransactionsById = createSelector(
selectTransactions, _selectState,
transactions => transactions.byId || {} state => state.transactions
); );
export const selectTransactionItems = createSelector( export const selectTransactionItems = createSelector(
selectTransactionsById, selectTransactionsById,
byId => { byId => {
const transactionItems = []; const items = [];
const txids = Object.keys(byId);
txids.forEach(txid => { Object.keys(byId).forEach(txid => {
const tx = byId[txid]; const tx = byId[txid];
transactionItems.push({
id: txid, //ignore dust/fees
date: tx.timestamp ? new Date(parseInt(tx.timestamp) * 1000) : null, if (Math.abs(tx.amount) === Math.abs(tx.fee)) {
amount: parseFloat(tx.value), return;
claim_info: tx.claim_info, }
support_info: tx.support_info,
update_info: tx.update_info, let append = [];
fee: tx.fee,
}); 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; font-weight: bold;
color: var(--color-money); color: var(--color-money);
} }
.credit-amount--fee
{
font-size: 0.9em;
color: var(--color-meta-light);
}
#main-content #main-content
{ {
@ -44,7 +49,8 @@ body
width: $width-page-constrained; width: $width-page-constrained;
} }
} }
main.main--refreshing {
.reloading {
&:before { &:before {
$width: 30px; $width: 30px;
position: absolute; position: absolute;

View file

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