diff --git a/CHANGELOG.md b/CHANGELOG.md index 156b5190e..29596e25b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ui/js/component/common.js b/ui/js/component/common.js index 8af8733c2..23b195bc6 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -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 ( diff --git a/ui/js/component/dateTime/view.jsx b/ui/js/component/dateTime/view.jsx index 72d47c130..747286daa 100644 --- a/ui/js/component/dateTime/view.jsx +++ b/ui/js/component/dateTime/view.jsx @@ -1,6 +1,10 @@ import React from "react"; class DateTime extends React.PureComponent { + static SHOW_DATE = "date"; + static SHOW_TIME = "time"; + static SHOW_BOTH = "both"; + componentWillMount() { this.refreshDate(this.props); } @@ -17,9 +21,20 @@ class DateTime extends React.PureComponent { } render() { - const { date } = this.props; + const { date, formatOptions } = this.props; + const show = this.props.show || DateTime.SHOW_BOTH; - return {date && date.toLocaleString()}; + return ( + + {date && + (show == DateTime.SHOW_BOTH || show === DateTime.SHOW_DATE) && + date.toLocaleDateString()} + {show == DateTime.SHOW_BOTH && " "} + {date && + (show == DateTime.SHOW_BOTH || show === DateTime.SHOW_TIME) && + date.toLocaleTimeString()} + + ); } } diff --git a/ui/js/component/linkTransaction/index.js b/ui/js/component/linkTransaction/index.js index 601927420..9983f1bfc 100644 --- a/ui/js/component/linkTransaction/index.js +++ b/ui/js/component/linkTransaction/index.js @@ -1,5 +1,5 @@ import React from "react"; import { connect } from "react-redux"; -import Link from "./view"; +import LinkTransaction from "./view"; -export default connect(null, null)(Link); +export default connect(null, null)(LinkTransaction); diff --git a/ui/js/component/transactionList/index.js b/ui/js/component/transactionList/index.js index 0b4d0e1af..5a6c10b6b 100644 --- a/ui/js/component/transactionList/index.js +++ b/ui/js/component/transactionList/index.js @@ -1,8 +1,13 @@ import React from "react"; import { connect } from "react-redux"; import { doNavigate } from "actions/navigation"; +import { selectClaimedRewardsByTransactionId } from "selectors/rewards"; import TransactionList from "./view"; +const select = state => ({ + rewards: selectClaimedRewardsByTransactionId(state), +}); + const perform = dispatch => ({ navigate: (path, params) => dispatch(doNavigate(path, params)), }); diff --git a/ui/js/component/transactionList/internal/TransactionListBody.jsx b/ui/js/component/transactionList/internal/TransactionListBody.jsx deleted file mode 100644 index 6ad795f05..000000000 --- a/ui/js/component/transactionList/internal/TransactionListBody.jsx +++ /dev/null @@ -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 ( - this.props.navigate(uri)}> - {claim_name} - - ); - } - - filterList(transaction) { - if (this.props.filter == "claim") { - return transaction.claim_info.length > 0; - } else if (this.props.filter == "support") { - return transaction.support_info.length > 0; - } else if (this.props.filter == "update") { - return transaction.update_info.length > 0; - } else { - return transaction; - } - } - - renderBody(transaction) { - const txid = transaction.id; - const date = transaction.date; - const fee = transaction.fee; - const filter = this.props.filter; - const options = { - weekday: "short", - year: "2-digit", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }; - - if (filter == "tipSupport") - transaction["tipSupport_info"] = transaction["support_info"].filter( - tx => tx.is_tip - ); - - return filter != "unfiltered" - ? transaction[`${filter}_info`].map(item => { - return ( - - - {date - ? date.toLocaleDateString("en-US", options) - : - {__("(Transaction pending)")} - } - - - -
- - - - {this.getClaimLink(item.claim_name, item.claim_id)} - - - - - - ); - }) - : - - {date - ? date.toLocaleDateString("en-US", options) - : - {__("(Transaction pending)")} - } - - - -
- - - - - - ; - } - - removeFeeTx(transaction) { - if (this.props.filter == "unfiltered") - return Math.abs(transaction.amount) != Math.abs(transaction.fee); - else return true; - } - - render() { - const { transactions, filter } = this.props; - let transactionList = transactions - .filter(this.filterList, this) - .filter(this.removeFeeTx, this) - .map(this.renderBody, this); - - if (transactionList.length == 0) { - return ( - - - - {__("There are no transactions of this type.")} - - - - ); - } - - return ( - - {transactionList} - - ); - } -} - -export default TransactionTableBody; diff --git a/ui/js/component/transactionList/internal/TransactionListHeader.jsx b/ui/js/component/transactionList/internal/TransactionListHeader.jsx deleted file mode 100644 index 52f735fb9..000000000 --- a/ui/js/component/transactionList/internal/TransactionListHeader.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -class TransactionTableHeader extends React.PureComponent { - render() { - const { filter } = this.props; - return ( - - - {__("Date")} - {__("Amount(Fee)")} - {filter != "unfiltered" && {__("Claim Name")} } - {__("Transaction")} - - - ); - } -} - -export default TransactionTableHeader; diff --git a/ui/js/component/transactionList/internal/TransactionListItem.jsx b/ui/js/component/transactionList/internal/TransactionListItem.jsx new file mode 100644 index 000000000..2a4ba28ae --- /dev/null +++ b/ui/js/component/transactionList/internal/TransactionListItem.jsx @@ -0,0 +1,78 @@ +import React from "react"; +import LinkTransaction from "component/linkTransaction"; +import { CreditAmount } from "component/common"; +import DateTime from "component/dateTime"; +import Link from "component/link"; +import lbryuri from "lbryuri"; + +class TransactionListItem extends React.PureComponent { + render() { + const { reward, transaction } = this.props; + const { + amount, + claim_id: claimId, + claim_name: name, + date, + fee, + txid, + type, + } = transaction; + + return ( + + + {date + ?
+ +
+ +
+
+ : + {__("(Transaction pending)")} + } + + + +
+ {fee != 0 && + } + + + {type} + + + {reward && + + {__("Reward: %s", reward.reward_title)} + } + {name && + claimId && + + {name} + } + + + + + + ); + } +} + +export default TransactionListItem; diff --git a/ui/js/component/transactionList/view.jsx b/ui/js/component/transactionList/view.jsx index ee78e2acc..beb2f8514 100644 --- a/ui/js/component/transactionList/view.jsx +++ b/ui/js/component/transactionList/view.jsx @@ -1,6 +1,5 @@ import React from "react"; -import TransactionTableHeader from "./internal/TransactionListHeader"; -import TransactionTableBody from "./internal/TransactionListBody"; +import TransactionListItem from "./internal/TransactionListItem"; import FormField from "component/formField"; class TransactionList extends React.PureComponent { @@ -8,7 +7,7 @@ class TransactionList extends React.PureComponent { super(props); this.state = { - filter: "unfiltered", + filter: null, }; } @@ -18,45 +17,63 @@ class TransactionList extends React.PureComponent { }); } - handleClaimNameClicked(uri) { - this.props.navigate("/show", { uri }); + filterTransaction(transaction) { + const { filter } = this.state; + + return !filter || filter == transaction.type; } render() { - const { emptyMessage, transactions } = this.props; - const { filter } = this.state; + const { emptyMessage, rewards, transactions } = this.props; - if (!transactions || !transactions.length) { - return ( -
- {emptyMessage || __("No transactions to list.")} -
- ); - } + let transactionList = transactions.filter( + this.filterTransaction.bind(this) + ); return (
- - {__("Filter")} {" "} - - - - - - - - - - - -
+ {(transactionList.length || this.state.filter) && + + {__("Filter")} {" "} + + + + + + + + + + + } + {!transactionList.length && +
+ {emptyMessage || __("No transactions to list.")} +
} + {Boolean(transactionList.length) && + + + + + + + + + + + + {transactionList.map(t => + + )} + +
{__("Date")}{__("Amount (Fee)")}{__("Type")} {__("Details")} {__("Transaction")}
}
); } diff --git a/ui/js/page/discover/view.jsx b/ui/js/page/discover/view.jsx index 5f4138cbf..bc86a9597 100644 --- a/ui/js/page/discover/view.jsx +++ b/ui/js/page/discover/view.jsx @@ -215,11 +215,7 @@ class DiscoverPage extends React.PureComponent { failedToLoad = !fetchingFeaturedUris && !hasContent; return ( -
+
{!hasContent && fetchingFeaturedUris && } diff --git a/ui/js/page/transactionHistory/view.jsx b/ui/js/page/transactionHistory/view.jsx index 8b9f1a90b..2f3a75715 100644 --- a/ui/js/page/transactionHistory/view.jsx +++ b/ui/js/page/transactionHistory/view.jsx @@ -10,18 +10,26 @@ class TransactionHistoryPage extends React.PureComponent { render() { const { fetchingTransactions, transactions } = this.props; + return (
-
+

{__("Transaction History")}

- {fetchingTransactions && - } - {!fetchingTransactions && - } + {fetchingTransactions && !transactions.length + ? + : ""} + {transactions && transactions.length + ? + : ""}
diff --git a/ui/js/reducers/wallet.js b/ui/js/reducers/wallet.js index 7cc53b54a..a4e25b964 100644 --- a/ui/js/reducers/wallet.js +++ b/ui/js/reducers/wallet.js @@ -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, }); }; diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index c2a646563..f9fbc4fe4 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -19,6 +19,15 @@ export const selectClaimedRewards = createSelector( byId => Object.values(byId) || [] ); +export const selectClaimedRewardsByTransactionId = createSelector( + selectClaimedRewards, + rewards => + rewards.reduce((map, reward) => { + map[reward.transaction_id] = reward; + return map; + }, {}) +); + export const selectUnclaimedRewards = createSelector( selectUnclaimedRewardsByType, byType => diff --git a/ui/js/selectors/wallet.js b/ui/js/selectors/wallet.js index 7469c2f48..fbf63770f 100644 --- a/ui/js/selectors/wallet.js +++ b/ui/js/selectors/wallet.js @@ -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(); } ); diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index 0f4b13be5..90b3212bc 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -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; diff --git a/ui/scss/component/_table.scss b/ui/scss/component/_table.scss index 2e2b664a1..59edeb480 100644 --- a/ui/scss/component/_table.scss +++ b/ui/scss/component/_table.scss @@ -60,3 +60,11 @@ table.table-standard { table.table-stretch { width: 100%; } + +table.table-transactions { + td:nth-of-type(1) { width: 15%; } + td:nth-of-type(2) { width: 15%; } + td:nth-of-type(3) { width: 15%; } + td:nth-of-type(4) { width: 40%; } + td:nth-of-type(5) { width: 15%; } +} \ No newline at end of file