use txo list for wallet page:

paginated
enable revoking
filtering

txo pagination changes

move constants

remove fetchTransactions() calls

review changes

final changes
This commit is contained in:
jessop 2020-04-10 13:31:36 -04:00 committed by Sean Yesmunt
parent a6000ecf0b
commit fdd20ef350
30 changed files with 617 additions and 556 deletions

View file

@ -130,7 +130,7 @@
"imagesloaded": "^4.1.4", "imagesloaded": "^4.1.4",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git", "lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#677dd256437f4cefa3eba96f0a28a814d07736c8", "lbry-redux": "lbryio/lbry-redux#c51483f4171f0056da65193f979a483248a68297",
"lbryinc": "lbryio/lbryinc#12aefaa14343d2f3eac01f2683701f58e53f1848", "lbryinc": "lbryio/lbryinc#12aefaa14343d2f3eac01f2683701f58e53f1848",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",

View file

@ -1128,5 +1128,48 @@
"%message% hihi": "%message% hihi", "%message% hihi": "%message% hihi",
"How much would you like to unlock?": "How much would you like to unlock?", "How much would you like to unlock?": "How much would you like to unlock?",
"A prudent choice": "A prudent choice", "A prudent choice": "A prudent choice",
"Join": "Join" "Join": "Join",
"File Details": "File Details",
"You can unlock all or some of this LBC at any time.": "You can unlock all or some of this LBC at any time.",
"Keeping it locked improves the trust and discoverability of your content.": "Keeping it locked improves the trust and discoverability of your content.",
"It's usually only worth unlocking what you intend to use immediately. %learn_more%": "It's usually only worth unlocking what you intend to use immediately. %learn_more%",
"%amount% available to unlock": "%amount% available to unlock",
"%message% hihi": "%message% hihi",
"Comrade Yrbl, we have a problem": "Comrade Yrbl, we have a problem",
"How much would you like to unlock?": "How much would you like to unlock?",
"A prudent choice": "A prudent choice",
"Preparing your content": "Preparing your content",
"New History": "New History",
"Share on LinkedIn": "Share on LinkedIn",
"Embed this content": "Embed this content",
"More actions": "More actions",
"music": "music",
"Transactions": "Transactions",
"Tx Type": "Tx Type",
"Payment": "Payment",
"Stream": "Stream",
"Tips": "Tips",
"Done!": "Done!",
"Publish to %uri%": "Publish to %uri%",
"Sent": "Sent",
"Received": "Received",
"active": "active",
"spent": "spent",
"all": "all",
"Payment Type": "Payment Type",
"Purchase": "Purchase",
"No transactions.": "No transactions.",
"Active": "Active",
"Historical": "Historical",
"Reposts": "Reposts",
"lbry": "lbry",
"Default": "Default",
"chillstep": "chillstep",
"economics": "economics",
"education": "education",
"linux": "linux",
"math": "math",
"news": "news",
"science": "science",
"Amount (LBC)": "Amount (LBC)"
} }

View file

@ -11,7 +11,7 @@ import {
doUserSetReferrer, doUserSetReferrer,
selectUserVerifiedEmail, selectUserVerifiedEmail,
} from 'lbryinc'; } from 'lbryinc';
import { doFetchTransactions, doFetchChannelListMine } from 'lbry-redux'; import { doFetchChannelListMine } from 'lbry-redux';
import { makeSelectClientSetting, selectLoadedLanguages, selectThemePath } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectLoadedLanguages, selectThemePath } from 'redux/selectors/settings';
import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app'; import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app';
import { doSetLanguage } from 'redux/actions/settings'; import { doSetLanguage } from 'redux/actions/settings';
@ -41,7 +41,6 @@ const select = state => ({
const perform = dispatch => ({ const perform = dispatch => ({
analyticsTagSync: () => dispatch(doAnalyticsTagSync()), analyticsTagSync: () => dispatch(doAnalyticsTagSync()),
fetchTransactions: (page, pageSize) => dispatch(doFetchTransactions(page, pageSize)),
fetchAccessToken: () => dispatch(doFetchAccessToken()), fetchAccessToken: () => dispatch(doFetchAccessToken()),
fetchChannelListMine: () => dispatch(doFetchChannelListMine()), fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
setLanguage: language => dispatch(doSetLanguage(language)), setLanguage: language => dispatch(doSetLanguage(language)),

View file

@ -3,7 +3,7 @@ import * as PAGES from 'constants/pages';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import analytics from 'analytics'; import analytics from 'analytics';
import { buildURI, parseURI, TX_LIST } from 'lbry-redux'; import { buildURI, parseURI } from 'lbry-redux';
import Router from 'component/router/index'; import Router from 'component/router/index';
import ModalRouter from 'modal/modalRouter'; import ModalRouter from 'modal/modalRouter';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
@ -54,7 +54,6 @@ type Props = {
length: number, length: number,
push: string => void, push: string => void,
}, },
fetchTransactions: (number, number) => void,
fetchAccessToken: () => void, fetchAccessToken: () => void,
fetchChannelListMine: () => void, fetchChannelListMine: () => void,
signIn: () => void, signIn: () => void,
@ -78,7 +77,6 @@ type Props = {
function App(props: Props) { function App(props: Props) {
const { const {
theme, theme,
fetchTransactions,
user, user,
fetchAccessToken, fetchAccessToken,
fetchChannelListMine, fetchChannelListMine,
@ -178,10 +176,9 @@ function App(props: Props) {
fetchAccessToken(); fetchAccessToken();
// @if TARGET='app' // @if TARGET='app'
fetchTransactions(1, TX_LIST.LATEST_PAGE_SIZE);
fetchChannelListMine(); // This needs to be done for web too... fetchChannelListMine(); // This needs to be done for web too...
// @endif // @endif
}, [appRef, fetchAccessToken, fetchChannelListMine, fetchTransactions]); }, [appRef, fetchAccessToken, fetchChannelListMine]);
useEffect(() => { useEffect(() => {
// $FlowFixMe // $FlowFixMe

View file

@ -10,6 +10,7 @@ import * as ICONS from 'constants/icons';
type Props = { type Props = {
title?: string | Node, title?: string | Node,
subtitle?: string | Node, subtitle?: string | Node,
titleActions?: string | Node,
body?: string | Node, body?: string | Node,
actions?: string | Node, actions?: string | Node,
icon?: string, icon?: string,
@ -24,6 +25,7 @@ export default function Card(props: Props) {
const { const {
title, title,
subtitle, subtitle,
titleActions,
body, body,
actions, actions,
icon, icon,
@ -48,8 +50,10 @@ export default function Card(props: Props) {
{subtitle && <div className="card__subtitle">{subtitle}</div>} {subtitle && <div className="card__subtitle">{subtitle}</div>}
</div> </div>
</div> </div>
<div>
{titleActions && <div className="card__title-actions">{titleActions}</div>}
{expandable && ( {expandable && (
<div className="card__expand-btn"> <div className="card__title-actions">
<Button <Button
button={'alt'} button={'alt'}
aria-label={__('More')} aria-label={__('More')}
@ -59,6 +63,7 @@ export default function Card(props: Props) {
</div> </div>
)} )}
</div> </div>
</div>
)} )}
{(!expandable || (expandable && expanded)) && ( {(!expandable || (expandable && expanded)) && (
<> <>

View file

@ -16,7 +16,6 @@ import InvitedPage from 'page/invited';
import RewardsPage from 'page/rewards'; import RewardsPage from 'page/rewards';
import FileListDownloaded from 'page/fileListDownloaded'; import FileListDownloaded from 'page/fileListDownloaded';
import FileListPublished from 'page/fileListPublished'; import FileListPublished from 'page/fileListPublished';
import TransactionHistoryPage from 'page/transactionHistory';
import InvitePage from 'page/invite'; import InvitePage from 'page/invite';
import SearchPage from 'page/search'; import SearchPage from 'page/search';
import LibraryPage from 'page/library'; import LibraryPage from 'page/library';
@ -183,9 +182,9 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.CREATOR_DASHBOARD}`} component={CreatorDashboard} /> <PrivateRoute {...props} path={`/$/${PAGES.CREATOR_DASHBOARD}`} component={CreatorDashboard} />
<PrivateRoute {...props} path={`/$/${PAGES.PUBLISH}`} component={PublishPage} /> <PrivateRoute {...props} path={`/$/${PAGES.PUBLISH}`} component={PublishPage} />
<PrivateRoute {...props} path={`/$/${PAGES.REPORT}`} component={ReportPage} /> <PrivateRoute {...props} path={`/$/${PAGES.REPORT}`} component={ReportPage} />
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS}`} component={RewardsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS}`} exact component={RewardsPage} /> <PrivateRoute {...props} path={`/$/${PAGES.REWARDS}`} exact component={RewardsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS_VERIFY}`} component={RewardsVerifyPage} /> <PrivateRoute {...props} path={`/$/${PAGES.REWARDS_VERIFY}`} component={RewardsVerifyPage} />
<PrivateRoute {...props} path={`/$/${PAGES.TRANSACTIONS}`} component={TransactionHistoryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} /> <PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} /> <PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} />
<PrivateRoute {...props} path={`/$/${PAGES.BLOCKED}`} component={ListBlockedPage} /> <PrivateRoute {...props} path={`/$/${PAGES.BLOCKED}`} component={ListBlockedPage} />

View file

@ -1,34 +0,0 @@
import { connect } from 'react-redux';
import { selectClaimedRewardsByTransactionId } from 'lbryinc';
import { doOpenModal } from 'redux/actions/app';
import {
selectAllMyClaimsByOutpoint,
selectSupportsByOutpoint,
selectTransactionListFilter,
doSetTransactionListFilter,
selectIsFetchingTransactions,
selectTransactionItems,
} from 'lbry-redux';
import { withRouter } from 'react-router';
import TransactionList from './view';
const select = state => ({
rewards: selectClaimedRewardsByTransactionId(state),
mySupports: selectSupportsByOutpoint(state),
myClaims: selectAllMyClaimsByOutpoint(state),
filterSetting: selectTransactionListFilter(state),
loading: selectIsFetchingTransactions(state),
allTransactions: selectTransactionItems(state),
});
const perform = dispatch => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
setTransactionFilter: filterSetting => dispatch(doSetTransactionListFilter(filterSetting)),
});
export default withRouter(
connect(
select,
perform
)(TransactionList)
);

View file

@ -1,114 +0,0 @@
// @flow
import * as icons from 'constants/icons';
import React from 'react';
import { FormField } from 'component/common/form';
import Button from 'component/button';
import FileExporter from 'component/common/file-exporter';
import { TRANSACTIONS, TX_LIST } from 'lbry-redux';
import * as PAGES from 'constants/pages';
import TransactionListTable from 'component/transactionListTable';
import RefreshTransactionButton from 'component/transactionRefreshButton';
import Paginate from 'component/common/paginate';
type Props = {
emptyMessage: ?string,
filterSetting: string,
loading: boolean,
myClaims: any,
setTransactionFilter: string => void,
slim?: boolean,
title: string,
allTransactions: Array<Transaction>,
transactions: Array<Transaction>,
transactionCount?: number,
history: { replace: string => void },
};
function TransactionList(props: Props) {
const {
emptyMessage,
slim,
filterSetting,
title,
transactions,
loading,
history,
transactionCount,
allTransactions,
} = props;
// Flow offers little support for Object.values() typing.
// https://github.com/facebook/flow/issues/2221
// $FlowFixMe
const transactionTypes: Array<string> = Object.values(TRANSACTIONS);
function capitalize(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function handleFilterChanged(event: SyntheticInputEvent<*>) {
props.setTransactionFilter(event.target.value);
history.replace(`/$/${PAGES.TRANSACTIONS}`); //
}
return (
<React.Fragment>
<header className="table__header">
<div className="table__header-text--between">
<h2 className="card__title">{title}</h2>
<div className="card__actions--inline">
<RefreshTransactionButton slim={slim} />
{/* @if TARGET='app' */}
{!slim && (
<FileExporter
data={allTransactions}
label={__('Export')}
title={__('Export Transactions')}
filters={['nout']}
defaultPath={'lbry-transactions-history'}
disabled={!transactions.length}
/>
)}
{/* @endif */}
{!slim && (
<FormField
type="select"
name="file-sort"
value={filterSetting || TRANSACTIONS.ALL}
onChange={handleFilterChanged}
postfix={
<Button
button="link"
icon={icons.HELP}
href="https://lbry.com/faq/transaction-types"
title={__('Help')}
/>
}
>
{transactionTypes.map(tt => (
<option key={tt} value={tt}>
{__(`${capitalize(tt)}`)}
</option>
))}
</FormField>
)}
{slim && <Button button="primary" navigate={`/$/${PAGES.TRANSACTIONS}`} label={__('Full History')} />}
</div>
</div>
</header>
{((loading && !transactions.length) || !transactions.length) && (
<h2 className="main--empty empty">{loading ? __('Loading') : emptyMessage || __('No transactions.')}</h2>
)}
{!!transactions && !!transactions.length && <TransactionListTable transactionList={transactions} />}
{!slim && !!transactionCount && (
<Paginate
onPageChange={page => history.replace(`/$/${PAGES.TRANSACTIONS}?page=${Number(page)}`)}
totalPages={Math.ceil(transactionCount / TX_LIST.PAGE_SIZE)}
/>
)}
</React.Fragment>
);
}
export default TransactionList;

View file

@ -1,25 +0,0 @@
import { connect } from 'react-redux';
import {
doFetchTransactions,
makeSelectLatestTransactions,
doFetchClaimListMine,
selectMyClaimsWithoutChannels,
} from 'lbry-redux';
import TransactionListRecent from './view';
const select = state => {
return {
transactions: makeSelectLatestTransactions(state),
myClaims: selectMyClaimsWithoutChannels(state),
};
};
const perform = dispatch => ({
fetchTransactions: (page, pageSize) => dispatch(doFetchTransactions(page, pageSize)),
fetchClaimListMine: () => dispatch(doFetchClaimListMine()),
});
export default connect(
select,
perform
)(TransactionListRecent);

View file

@ -1,41 +0,0 @@
// @flow
import React from 'react';
import TransactionList from 'component/transactionList';
import { TX_LIST } from 'lbry-redux';
type Props = {
fetchTransactions: (number, number) => void,
fetchClaimListMine: () => void,
fetchingTransactions: boolean,
hasTransactions: boolean,
transactions: Array<Transaction>,
myClaims: ?Array<StreamClaim>,
};
function TransactionListRecent(props: Props) {
const { transactions, fetchTransactions, myClaims, fetchClaimListMine } = props;
React.useEffect(() => {
fetchTransactions(1, TX_LIST.LATEST_PAGE_SIZE);
}, [fetchTransactions]);
const myClaimsString = myClaims && myClaims.map(channel => channel.permanent_url).join(',');
React.useEffect(() => {
if (myClaimsString === '') {
fetchClaimListMine();
}
}, [myClaimsString, fetchClaimListMine]);
return (
<section className="card">
<TransactionList
slim
title={__('Latest Transactions')}
transactions={transactions}
emptyMessage={__("Looks like you don't have any transactions.")}
/>
</section>
);
}
export default TransactionListRecent;

View file

@ -1,27 +1,16 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectClaimedRewardsByTransactionId } from 'lbryinc'; import { selectClaimedRewardsByTransactionId } from 'lbryinc';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import { import { selectIsFetchingTxos } from 'lbry-redux';
selectAllMyClaimsByOutpoint,
selectSupportsByOutpoint,
selectTransactionListFilter,
selectIsFetchingTransactions,
} from 'lbry-redux';
import TransactionListTable from './view'; import TransactionListTable from './view';
const select = state => ({ const select = state => ({
rewards: selectClaimedRewardsByTransactionId(state), rewards: selectClaimedRewardsByTransactionId(state),
mySupports: selectSupportsByOutpoint(state), loading: selectIsFetchingTxos(state),
myClaims: selectAllMyClaimsByOutpoint(state),
filterSetting: selectTransactionListFilter(state),
loading: selectIsFetchingTransactions(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
}); });
export default connect( export default connect(select, perform)(TransactionListTable);
select,
perform
)(TransactionListTable);

View file

@ -1,108 +0,0 @@
// @flow
import * as TXN_TYPES from 'constants/transaction_types';
import * as ICONS from 'constants/icons';
import React from 'react';
import ButtonTransaction from 'component/common/transaction-link';
import CreditAmount from 'component/common/credit-amount';
import DateTime from 'component/dateTime';
import Button from 'component/button';
import { buildURI, parseURI } from 'lbry-redux';
type Props = {
transaction: Transaction,
revokeClaim: (string, number) => void,
isRevokeable: boolean,
reward: ?{
reward_title: string,
},
};
class TransactionListItem extends React.PureComponent<Props> {
constructor() {
super();
(this: any).abandonClaim = this.abandonClaim.bind(this);
}
getLink(type: string) {
if (type === TXN_TYPES.TIP) {
return <Button button="secondary" icon={ICONS.UNLOCK} onClick={this.abandonClaim} title={__('Unlock Tip')} />;
}
const abandonTitle = type === TXN_TYPES.SUPPORT ? 'Abandon Support' : 'Abandon Claim';
return <Button button="secondary" icon={ICONS.DELETE} onClick={this.abandonClaim} title={__(abandonTitle)} />;
}
abandonClaim() {
const { txid, nout } = this.props.transaction;
this.props.revokeClaim(txid, nout);
}
capitalize = (string: ?string) => string && string.charAt(0).toUpperCase() + string.slice(1);
render() {
const { reward, transaction, isRevokeable } = this.props;
const { amount, claim_id: claimId, claim_name: name, date, fee, txid, type } = transaction;
// Ensure the claim name exists and is valid
let uri;
let claimName;
try {
if (name.startsWith('@')) {
({ claimName } = parseURI(name));
uri = buildURI({ channelName: claimName, channelClaimId: claimId });
} else {
({ claimName } = parseURI(name));
uri = buildURI({ streamName: claimName, streamClaimId: claimId });
}
} catch (e) {}
const dateFormat = {
month: 'short',
day: 'numeric',
year: 'numeric',
};
const forClaim = name && claimId;
return (
<tr>
<td>
{date ? (
<div>
<DateTime date={date} show={DateTime.SHOW_DATE} formatOptions={dateFormat} />
<div className="table__item-label">
<DateTime date={date} show={DateTime.SHOW_TIME} />
</div>
</div>
) : (
<span className="empty">{__('Pending')}</span>
)}
</td>
<td className="table__item--actionable">
<span>{this.capitalize(type)}</span> {isRevokeable && this.getLink(type)}
</td>
<td>
{forClaim && <Button button="link" navigate={uri} label={claimName} disabled={!date} />}
{!forClaim && reward && <span>{reward.reward_title}</span>}
</td>
<td>
<ButtonTransaction id={txid} />
</td>
<td className="table__item--align-right">
<CreditAmount badge={false} showPlus amount={amount} precision={8} />
<br />
{fee !== 0 && (
<span className="table__item-label">
<CreditAmount badge={false} fee amount={fee} precision={8} />
</span>
)}
</td>
</tr>
);
}
}
export default TransactionListItem;

View file

@ -0,0 +1,147 @@
// @flow
import * as ICONS from 'constants/icons';
import React from 'react';
import ButtonTransaction from 'component/common/transaction-link';
import CreditAmount from 'component/common/credit-amount';
import DateTime from 'component/dateTime';
import Button from 'component/button';
import { buildURI, parseURI, TXO_LIST as TXO } from 'lbry-redux';
type Props = {
txo: Txo,
revokeClaim: (Txo, () => void) => void,
isRevokeable: boolean,
reward: ?{
reward_title: string,
},
};
type State = {
disabled: boolean,
};
class TxoListItem extends React.PureComponent<Props, State> {
constructor() {
super();
this.state = { disabled: false };
(this: any).abandonClaim = this.abandonClaim.bind(this);
(this: any).getLink = this.getLink.bind(this);
}
getLink(type: string) {
if (type === TXO.SUPPORT) {
return (
<Button
disabled={this.state.disabled}
button="secondary"
icon={ICONS.UNLOCK}
onClick={this.abandonClaim}
title={__('Unlock Tip')}
/>
);
}
const abandonTitle = type === TXO.SUPPORT ? 'Abandon Support' : 'Abandon Claim';
return (
<Button
disabled={this.state.disabled}
button="secondary"
icon={ICONS.DELETE}
onClick={this.abandonClaim}
title={__(abandonTitle)}
/>
);
}
abandonClaim() {
this.props.revokeClaim(this.props.txo, () => this.setState({ disabled: true }));
// this.setState({ disabled: true }); // just flag the item disabled
}
capitalize = (string: string) => string && string.charAt(0).toUpperCase() + string.slice(1);
render() {
const { reward, txo, isRevokeable } = this.props;
const {
amount,
claim_id: claimId,
normalized_name: txoListName,
timestamp,
txid,
type,
value_type: valueType,
is_my_input: isMyInput, // no transaction
is_my_output: isMyOutput,
} = txo;
const name = txoListName;
const isMinus = (type === 'support' || type === 'payment' || type === 'other') && isMyInput && !isMyOutput;
const isTip = type === 'support' && ((isMyInput && !isMyOutput) || (!isMyInput && isMyOutput));
const date = new Date(timestamp * 1000);
// Ensure the claim name exists and is valid
let uri;
let claimName;
try {
if (name.startsWith('@')) {
({ claimName } = parseURI(name));
uri = buildURI({ channelName: claimName, channelClaimId: claimId });
} else {
({ claimName } = parseURI(name));
uri = buildURI({ streamName: claimName, streamClaimId: claimId });
}
} catch (e) {}
const dateFormat = {
month: 'short',
day: 'numeric',
year: 'numeric',
};
const forClaim = name && claimId;
return (
<tr>
<td>
{timestamp ? (
<div>
<DateTime date={date} show={DateTime.SHOW_DATE} formatOptions={dateFormat} />
<div className="table__item-label">
<DateTime date={date} show={DateTime.SHOW_TIME} />
</div>
</div>
) : (
<span className="empty">{__('Pending')}</span>
)}
</td>
<td className="table__item--actionable">
<span>
{(isTip && __(this.capitalize('tip'))) ||
(valueType && __(this.capitalize(valueType))) ||
(type && __(this.capitalize(type)))}
</span>{' '}
{isRevokeable && this.getLink(type)}
</td>
<td>
{forClaim && <Button button="link" navigate={uri} label={claimName} disabled={!date} />}
{!forClaim && reward && <span>{reward.reward_title}</span>}
</td>
<td>
<ButtonTransaction id={txid} />
</td>
<td className="table__item--align-right">
<CreditAmount
badge={false}
showPlus={isMinus}
amount={isMinus ? Number(0 - amount) : Number(amount)}
precision={8}
showLBC={false}
/>
</td>
</tr>
);
}
}
export default TxoListItem;

View file

@ -1,38 +1,32 @@
// @flow // @flow
import * as MODALS from 'constants/modal_types'; import * as MODALS from 'constants/modal_types';
import React from 'react'; import React from 'react';
import TransactionListItem from './internal/transaction-list-item'; import TxoListItem from './internal/txo-list-item';
import Spinner from 'component/spinner';
type Props = { type Props = {
emptyMessage: ?string, emptyMessage: ?string,
loading: boolean, loading: boolean,
mySupports: {}, openModal: (id: string, { tx: Txo }) => void,
myClaims: any,
openModal: (id: string, { nout: number, txid: string }) => void,
rewards: {}, rewards: {},
transactionList: Array<any>, txos: Array<Txo>,
}; };
function TransactionListTable(props: Props) { function TransactionListTable(props: Props) {
const { emptyMessage, rewards, loading, transactionList } = props; const { emptyMessage, rewards, loading, txos } = props;
const REVOCABLE_TYPES = ['channel', 'stream', 'repost', 'support', 'claim'];
function isRevokeable(txid: string, nout: number) { function revokeClaim(tx: any, cb: () => void) {
const outpoint = `${txid}:${nout}`; props.openModal(MODALS.CONFIRM_CLAIM_REVOKE, { tx, cb });
const { mySupports, myClaims } = props;
return !!mySupports[outpoint] || myClaims.has(outpoint);
}
function revokeClaim(txid: string, nout: number) {
props.openModal(MODALS.CONFIRM_CLAIM_REVOKE, { txid, nout });
} }
return ( return (
<React.Fragment> <React.Fragment>
{!loading && !transactionList.length && ( {!loading && !txos.length && <h2 className="main--empty empty">{emptyMessage || __('No transactions.')}</h2>}
<h2 className="main--empty empty">{emptyMessage || __('No transactions.')}</h2> {loading && (
<h2 className="main--empty empty">
<Spinner delayed />
</h2>
)} )}
{!!transactionList.length && ( {!loading && !!txos.length && (
<div className="table__wrapper"> <div className="table__wrapper">
<table className="table table--transactions"> <table className="table table--transactions">
<thead> <thead>
@ -41,16 +35,17 @@ function TransactionListTable(props: Props) {
<th>{__('Type')} </th> <th>{__('Type')} </th>
<th>{__('Details')} </th> <th>{__('Details')} </th>
<th>{__('Transaction')}</th> <th>{__('Transaction')}</th>
<th className="table__item--align-right">{__('Amount')}</th> <th className="table__item--align-right">{__('Amount (LBC)')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{transactionList.map(t => ( {txos &&
<TransactionListItem txos.map((t, i) => (
key={`${t.txid}:${t.nout}`} <TxoListItem
transaction={t} key={`${t.txid}:${t.nout}-${i}`}
txo={t}
reward={rewards && rewards[t.txid]} reward={rewards && rewards[t.txid]}
isRevokeable={isRevokeable(t.txid, t.nout)} isRevokeable={t.is_my_output && !t.is_spent && REVOCABLE_TYPES.includes(t.type)}
revokeClaim={revokeClaim} revokeClaim={revokeClaim}
/> />
))} ))}

View file

@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import { doFetchTransactions, selectIsFetchingTransactions } from 'lbry-redux';
import RefreshTransactionButton from './view';
const select = state => ({
fetchingTransactions: selectIsFetchingTransactions(state),
});
const perform = dispatch => ({
fetchTransactions: (page, pageSize) => dispatch(doFetchTransactions(page, pageSize)),
});
export default connect(
select,
perform
)(RefreshTransactionButton);

View file

@ -1,53 +0,0 @@
// @flow
import React, { PureComponent } from 'react';
import Button from 'component/button';
import { TX_LIST } from 'lbry-redux';
type Props = {
fetchTransactions: (?number, ?number) => void,
fetchingTransactions: boolean,
slim: boolean,
};
type State = {
label: string,
disabled: boolean,
};
class TransactionRefreshButton extends PureComponent<Props, State> {
constructor() {
super();
this.state = { label: __('Refresh'), disabled: false };
(this: any).handleClick = this.handleClick.bind(this);
}
handleClick() {
const { fetchTransactions, slim } = this.props;
// The fetchTransactions call will be super fast most of the time.
// Instead of showing a loading spinner for 100ms, change the label and show as "Refreshed!"
if (slim) {
fetchTransactions(1, TX_LIST.LATEST_PAGE_SIZE);
} else {
fetchTransactions();
}
this.setState({ label: __('Refreshed!'), disabled: true });
setTimeout(() => {
this.setState({ label: __('Refresh'), disabled: false });
}, 2000);
}
render() {
const { fetchingTransactions } = this.props;
const { label, disabled } = this.state;
return (
<Button button="secondary" label={label} onClick={this.handleClick} disabled={disabled || fetchingTransactions} />
);
}
}
export default TransactionRefreshButton;

View file

@ -0,0 +1,29 @@
import { connect } from 'react-redux';
import { doOpenModal } from 'redux/actions/app';
import {
selectIsFetchingTxos,
selectFetchingTxosError,
selectTxoPage,
selectTxoPageNumber,
selectTxoItemCount,
doFetchTxoPage,
doUpdateTxoPageParams,
} from 'lbry-redux';
import { withRouter } from 'react-router';
import TxoList from './view';
const select = state => ({
txoFetchError: selectFetchingTxosError(state),
txoPage: selectTxoPage(state),
txoPageNumber: selectTxoPageNumber(state),
txoItemCount: selectTxoItemCount(state),
loading: selectIsFetchingTxos(state),
});
const perform = dispatch => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
fetchTxoPage: () => dispatch(doFetchTxoPage()),
updateTxoPageParams: params => dispatch(doUpdateTxoPageParams(params)),
});
export default withRouter(connect(select, perform)(TxoList));

View file

@ -0,0 +1,260 @@
// @flow
import React, { useEffect } from 'react';
import { withRouter } from 'react-router';
import { TXO_LIST as TXO } from 'lbry-redux';
import TransactionListTable from 'component/transactionListTable';
import Paginate from 'component/common/paginate';
import { FormField } from 'component/common/form-components/form-field';
import Button from 'component/button';
import Card from 'component/common/card';
import { toCapitalCase } from 'util/string';
import classnames from 'classnames';
type Props = {
search: string,
history: { action: string, push: string => void, replace: string => void },
txoPage: Array<Transaction>,
txoPageNumber: string,
txoItemCount: number,
fetchTxoPage: () => void,
updateTxoPageParams: any => void,
};
type Delta = {
dkey: string,
value: string,
};
function TxoList(props: Props) {
const { search, txoPage, txoItemCount, fetchTxoPage, updateTxoPageParams, history } = props;
console.log('txoPage', txoPage);
// parse urlParams
const urlParams = new URLSearchParams(search);
const page = urlParams.get(TXO.PAGE) || String(1);
const pageSize = urlParams.get(TXO.PAGE_SIZE) || String(TXO.PAGE_SIZE_DEFAULT);
const type = urlParams.get(TXO.TYPE);
const subtype = urlParams.get(TXO.SUB_TYPE);
const active = urlParams.get(TXO.ACTIVE) || TXO.ACTIVE;
const currentUrlParams = {
page,
pageSize,
active,
type,
subtype,
};
// build new json params
const params = {};
if (currentUrlParams.type) {
if (currentUrlParams.type === TXO.SENT) {
params[TXO.IS_MY_INPUT] = true;
params[TXO.IS_NOT_MY_OUTPUT] = true;
if (currentUrlParams.subtype === TXO.TIP) {
params[TXO.TX_TYPE] = TXO.SUPPORT;
} else if (currentUrlParams.subtype === TXO.PURCHASE) {
params[TXO.TX_TYPE] = TXO.PURCHASE;
} else if (currentUrlParams.subtype === TXO.PURCHASE) {
params[TXO.TX_TYPE] = TXO.OTHER;
} else {
params[TXO.TX_TYPE] = [TXO.OTHER, TXO.PURCHASE, TXO.SUPPORT];
}
} else if (currentUrlParams.type === TXO.RECEIVED) {
params[TXO.IS_MY_OUTPUT] = true;
params[TXO.IS_NOT_MY_INPUT] = true;
if (currentUrlParams.subtype === TXO.TIP) {
params[TXO.TX_TYPE] = TXO.SUPPORT;
} else if (currentUrlParams.subtype === TXO.PURCHASE) {
params[TXO.TX_TYPE] = TXO.PURCHASE;
} else if (currentUrlParams.subtype === TXO.PURCHASE) {
params[TXO.TX_TYPE] = TXO.OTHER;
} else {
params[TXO.TX_TYPE] = [TXO.OTHER, TXO.PURCHASE, TXO.SUPPORT];
}
} else if (currentUrlParams.type === TXO.SUPPORT) {
params[TXO.TX_TYPE] = TXO.SUPPORT;
params[TXO.IS_MY_INPUT] = true;
params[TXO.IS_MY_OUTPUT] = true;
} else if (currentUrlParams.type === TXO.CHANNEL || currentUrlParams.type === TXO.REPOST) {
params[TXO.TX_TYPE] = currentUrlParams.type;
} else if (currentUrlParams.type === TXO.PUBLISH) {
params[TXO.TX_TYPE] = TXO.STREAM;
}
}
if (currentUrlParams.active) {
if (currentUrlParams.active === 'spent') {
params[TXO.IS_SPENT] = true;
} else if (currentUrlParams.active === 'active') {
params[TXO.IS_NOT_SPENT] = true;
}
}
if (currentUrlParams.page) params[TXO.PAGE] = Number(page);
if (currentUrlParams.pageSize) params[TXO.PAGE_SIZE] = Number(pageSize);
function handleChange(delta: Delta) {
const url = updateUrl(delta);
history.push(url);
}
function updateUrl(delta: Delta) {
const newUrlParams = new URLSearchParams();
switch (delta.dkey) {
case TXO.PAGE:
if (currentUrlParams.type) {
newUrlParams.set(TXO.TYPE, currentUrlParams.type);
}
if (currentUrlParams.subtype) {
newUrlParams.set(TXO.SUB_TYPE, currentUrlParams.subtype);
}
if (currentUrlParams.active) {
newUrlParams.set(TXO.ACTIVE, currentUrlParams.active);
}
newUrlParams.set(TXO.PAGE, delta.value);
break;
case TXO.TYPE:
newUrlParams.set(TXO.TYPE, delta.value);
if (delta.value === TXO.SENT || delta.value === TXO.RECEIVED) {
if (currentUrlParams.subtype) {
newUrlParams.set(TXO.SUB_TYPE, currentUrlParams.subtype);
} else {
newUrlParams.set(TXO.SUB_TYPE, 'all');
}
}
if (currentUrlParams.active) {
newUrlParams.set(TXO.ACTIVE, currentUrlParams.active);
}
newUrlParams.set(TXO.PAGE, String(1));
newUrlParams.set(TXO.PAGE_SIZE, currentUrlParams.pageSize);
break;
case TXO.SUB_TYPE:
if (currentUrlParams.type) {
newUrlParams.set(TXO.TYPE, currentUrlParams.type);
}
if (currentUrlParams.active) {
newUrlParams.set(TXO.ACTIVE, currentUrlParams.active);
}
newUrlParams.set(TXO.SUB_TYPE, delta.value);
newUrlParams.set(TXO.PAGE, String(1));
newUrlParams.set(TXO.PAGE_SIZE, currentUrlParams.pageSize);
break;
case TXO.ACTIVE:
if (currentUrlParams.type) {
newUrlParams.set(TXO.TYPE, currentUrlParams.type);
}
if (currentUrlParams.subtype) {
newUrlParams.set(TXO.SUB_TYPE, currentUrlParams.subtype);
}
newUrlParams.set(TXO.ACTIVE, delta.value);
newUrlParams.set(TXO.PAGE, String(1));
newUrlParams.set(TXO.PAGE_SIZE, currentUrlParams.pageSize);
break;
}
return `?${newUrlParams.toString()}`;
}
const paramsString = JSON.stringify(params);
useEffect(() => {
if (paramsString && updateTxoPageParams) {
const params = JSON.parse(paramsString);
updateTxoPageParams(params);
}
}, [paramsString, updateTxoPageParams]);
return (
<Card
title={__(`Transactions`)}
titleActions={
<div className="card__actions--inline">
<Button button="secondary" label={__('Refresh')} onClick={() => fetchTxoPage()} />
</div>
}
isBodyTable
body={
<div>
<div className="card__body-actions">
<div className="card__actions">
{/* show the main drop down */}
<div>
<FormField
type="select"
name="type"
label={__('Type')}
value={type || 'all'}
onChange={e => handleChange({ dkey: TXO.TYPE, value: e.target.value })}
>
{Object.values(TXO.DROPDOWN_TYPES).map(v => {
const stringV = String(v);
return (
<option key={stringV} value={stringV}>
{stringV && __(toCapitalCase(stringV))}
</option>
);
})}
</FormField>
</div>
{/* show the subtypes drop down */}
{(type === TXO.SENT || type === TXO.RECEIVED) && (
<div>
<FormField
type="select"
name="subtype"
label={__('Payment Type')}
value={subtype || 'all'}
onChange={e => handleChange({ dkey: TXO.SUB_TYPE, value: e.target.value })}
>
{Object.values(TXO.DROPDOWN_SUBTYPES).map(v => {
const stringV = String(v);
return (
<option key={stringV} value={stringV}>
{stringV && __(toCapitalCase(stringV))}
</option>
);
})}
</FormField>
</div>
)}
<div>
<fieldset-section>
<label>Status</label>
<div className={'txo__radios'}>
<Button
button="alt"
onClick={e => handleChange({ dkey: TXO.ACTIVE, value: 'active' })}
className={classnames(`button-toggle`, {
'button-toggle--active': active === TXO.ACTIVE,
})}
label={__(toCapitalCase('active'))}
/>
<Button
button="alt"
onClick={e => handleChange({ dkey: TXO.ACTIVE, value: 'spent' })}
className={classnames(`button-toggle`, {
'button-toggle--active': active === 'spent',
})}
label={__(toCapitalCase('historical'))}
/>
<Button
button="alt"
onClick={e => handleChange({ dkey: TXO.ACTIVE, value: 'all' })}
className={classnames(`button-toggle`, {
'button-toggle--active': active === 'all',
})}
label={__(toCapitalCase('all'))}
/>
</div>
</fieldset-section>
</div>
</div>
{/* show active/spent */}
</div>
<TransactionListTable txos={txoPage} />
<Paginate totalPages={Math.ceil(txoItemCount / Number(pageSize))} />
</div>
}
/>
);
}
export default withRouter(TxoList);

View file

@ -21,7 +21,6 @@ exports.SETTINGS = 'settings';
exports.SHOW = 'show'; exports.SHOW = 'show';
exports.ACCOUNT = 'account'; exports.ACCOUNT = 'account';
exports.SEARCH = 'search'; exports.SEARCH = 'search';
exports.TRANSACTIONS = 'transactions';
exports.TAGS_FOLLOWING = 'tags'; exports.TAGS_FOLLOWING = 'tags';
exports.DEPRECATED__TAGS_FOLLOWING = 'following/tags'; exports.DEPRECATED__TAGS_FOLLOWING = 'following/tags';
exports.TAGS_FOLLOWING_MANAGE = 'tags/manage'; exports.TAGS_FOLLOWING_MANAGE = 'tags/manage';

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import { doAbandonClaim, selectTransactionItems } from 'lbry-redux'; import { doAbandonTxo, selectTransactionItems } from 'lbry-redux';
import ModalRevokeClaim from './view'; import ModalRevokeClaim from './view';
const select = state => ({ const select = state => ({
@ -9,10 +9,7 @@ const select = state => ({
const perform = dispatch => ({ const perform = dispatch => ({
closeModal: () => dispatch(doHideModal()), closeModal: () => dispatch(doHideModal()),
abandonClaim: (txid, nout) => dispatch(doAbandonClaim(txid, nout)), abandonTxo: (txo, cb) => dispatch(doAbandonTxo(txo, cb)),
}); });
export default connect( export default connect(select, perform)(ModalRevokeClaim);
select,
perform
)(ModalRevokeClaim);

View file

@ -6,15 +6,14 @@ import * as txnTypes from 'constants/transaction_types';
type Props = { type Props = {
closeModal: () => void, closeModal: () => void,
abandonClaim: (string, number) => void, abandonTxo: (Txo, () => void) => void,
txid: string, tx: Txo,
nout: number, cb: () => void,
transactionItems: Array<Transaction>,
}; };
export default function ModalRevokeClaim(props: Props) { export default function ModalRevokeClaim(props: Props) {
const { transactionItems, txid, nout, closeModal } = props; const { tx, closeModal, abandonTxo, cb } = props;
const { type, claim_name: name } = transactionItems.find(claim => claim.txid === txid && claim.nout === nout) || {}; const { value_type: valueType, type, normalized_name: name } = tx;
const [channelName, setChannelName] = useState(''); const [channelName, setChannelName] = useState('');
function getButtonLabel(type: string) { function getButtonLabel(type: string) {
@ -49,7 +48,11 @@ export default function ModalRevokeClaim(props: Props) {
</p> </p>
</React.Fragment> </React.Fragment>
); );
} else if (type === txnTypes.CHANNEL || (type === txnTypes.UPDATE && name.startsWith('@'))) { } else if (
valueType === txnTypes.CHANNEL ||
type === txnTypes.CHANNEL ||
(type === txnTypes.UPDATE && name.startsWith('@'))
) {
return ( return (
<React.Fragment> <React.Fragment>
<p> <p>
@ -77,10 +80,8 @@ export default function ModalRevokeClaim(props: Props) {
} }
function revokeClaim() { function revokeClaim() {
const { txid, nout } = props; abandonTxo(tx, cb);
closeModal();
props.closeModal();
props.abandonClaim(txid, nout);
} }
return ( return (
@ -92,7 +93,7 @@ export default function ModalRevokeClaim(props: Props) {
confirmButtonLabel={getButtonLabel(type)} confirmButtonLabel={getButtonLabel(type)}
onConfirmed={revokeClaim} onConfirmed={revokeClaim}
onAborted={closeModal} onAborted={closeModal}
confirmButtonDisabled={type === txnTypes.CHANNEL && name !== channelName} confirmButtonDisabled={valueType === txnTypes.CHANNEL && name !== channelName}
> >
<section>{getMsgBody(type, name)}</section> <section>{getMsgBody(type, name)}</section>
</Modal> </Modal>

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import { doAbandonClaim, selectTransactionItems } from 'lbry-redux'; import { selectTransactionItems } from 'lbry-redux';
import ModalSupportsLiquidate from './view'; import ModalSupportsLiquidate from './view';
const select = state => ({ const select = state => ({
@ -9,7 +9,6 @@ const select = state => ({
const perform = dispatch => ({ const perform = dispatch => ({
closeModal: () => dispatch(doHideModal()), closeModal: () => dispatch(doHideModal()),
abandonClaim: (txid, nout) => dispatch(doAbandonClaim(txid, nout)),
}); });
export default connect(select, perform)(ModalSupportsLiquidate); export default connect(select, perform)(ModalSupportsLiquidate);

View file

@ -1,27 +0,0 @@
import { connect } from 'react-redux';
import { doFetchTransactions, makeSelectFilteredTransactionsForPage, selectFilteredTransactionCount } from 'lbry-redux';
import { withRouter } from 'react-router';
import TransactionHistoryPage from './view';
const select = (state, props) => {
const { search } = props.location;
const urlParams = new URLSearchParams(search);
const page = Number(urlParams.get('page')) || 1;
return {
page,
filteredTransactionPage: makeSelectFilteredTransactionsForPage(page)(state),
filteredTransactionsCount: selectFilteredTransactionCount(state),
};
};
const perform = dispatch => ({
fetchTransactions: () => dispatch(doFetchTransactions()),
});
export default withRouter(
connect(
select,
perform
)(TransactionHistoryPage)
);

View file

@ -1,37 +0,0 @@
// @flow
import React from 'react';
import TransactionList from 'component/transactionList';
import Page from 'component/page';
type Props = {
fetchTransactions: () => void,
fetchingTransactions: boolean,
filteredTransactionPage: Array<{}>,
filteredTransactionsCount: number,
};
class TransactionHistoryPage extends React.PureComponent<Props> {
componentDidMount() {
const { fetchTransactions } = this.props;
fetchTransactions();
}
render() {
const { filteredTransactionPage, filteredTransactionsCount } = this.props;
return (
<Page>
<section className="card">
<TransactionList
transactions={filteredTransactionPage}
transactionCount={filteredTransactionsCount}
title={__('Transaction History')}
/>
</section>
</Page>
);
}
}
export default TransactionHistoryPage;

View file

@ -1,13 +1,25 @@
// @flow
import React from 'react'; import React from 'react';
import { withRouter } from 'react-router';
import WalletBalance from 'component/walletBalance'; import WalletBalance from 'component/walletBalance';
import TransactionListRecent from 'component/transactionListRecent'; import TxoList from 'component/txoList';
import Page from 'component/page'; import Page from 'component/page';
const WalletPage = () => ( type Props = {
history: { action: string, push: string => void, replace: string => void },
location: { search: string, pathname: string },
};
const WalletPage = (props: Props) => {
const { location } = props;
const { search } = location;
return (
<Page> <Page>
<WalletBalance /> <WalletBalance />
<TransactionListRecent /> <TxoList search={search} />
</Page> </Page>
); );
};
export default WalletPage; export default withRouter(WalletPage);

View file

@ -44,6 +44,7 @@
@import 'component/syntax-highlighter'; @import 'component/syntax-highlighter';
@import 'component/table'; @import 'component/table';
@import 'component/tabs'; @import 'component/tabs';
@import 'component/txo-list';
@import 'component/tags'; @import 'component/tags';
@import 'component/wunderbar'; @import 'component/wunderbar';
@import 'component/yrbl'; @import 'component/yrbl';

View file

@ -61,6 +61,14 @@
> *:not(:last-child) { > *:not(:last-child) {
margin-right: var(--spacing-medium); margin-right: var(--spacing-medium);
} }
@media (max-width: $breakpoint-small) {
> * {
padding-bottom: var(--spacing-medium);
}
flex-flow: wrap;
justify-content: space-between;
}
} }
.card__section--flex { .card__section--flex {
@ -129,16 +137,13 @@
} }
.card__title { .card__title {
display: flex; display: block;
flex-wrap: wrap;
align-items: center; align-items: center;
font-size: var(--font-title); font-size: var(--font-title);
font-weight: var(--font-weight-light); font-weight: var(--font-weight-light);
& > *:not(:last-child) { & > *:not(:last-child) {
margin-right: var(--spacing-medium); margin-right: var(--spacing-medium);
} }
/* .badge rule inherited from file page prices, should be refactored */ /* .badge rule inherited from file page prices, should be refactored */
.badge { .badge {
float: right; float: right;
@ -147,6 +152,14 @@
} }
} }
.card__title-actions {
padding: var(--spacing-medium);
@media (max-width: $breakpoint-small) {
padding: 0;
}
}
.card__title.card__title--deprecated { .card__title.card__title--deprecated {
margin-bottom: var(--spacing-small); margin-bottom: var(--spacing-small);
} }
@ -187,6 +200,7 @@
.card__body { .card__body {
padding: var(--spacing-large); padding: var(--spacing-large);
border-top: 1px solid var(--color-border);
&:not(.card__body--no-title) { &:not(.card__body--no-title) {
padding-top: 0; padding-top: 0;
} }
@ -207,6 +221,19 @@
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
} }
.card__body-actions {
padding: var(--spacing-large);
@media (max-width: $breakpoint-small) {
padding: var(--spacing-small);
}
}
.card__header-actions {
border-top: 1px solid var(--color-border);
padding: var(--spacing-large);
width: 100%;
}
.card__body--with-icon, .card__body--with-icon,
.card__main-actions--with-icon { .card__main-actions--with-icon {
padding-left: 7.5rem; padding-left: 7.5rem;

View file

@ -38,6 +38,12 @@
} }
.table--transactions { .table--transactions {
td:nth-of-type(1) {
width: 20%;
}
td:nth-of-type(2) {
width: 15%;
}
td:nth-of-type(3) { td:nth-of-type(3) {
// Only add ellipsis to the links in the table // Only add ellipsis to the links in the table
// We still want to show the entire message if a TX includes one // We still want to show the entire message if a TX includes one
@ -47,6 +53,13 @@
vertical-align: bottom; vertical-align: bottom;
display: inline-block; display: inline-block;
} }
width: 35%;
}
td:nth-of-type(4) {
width: 15%;
}
td:nth-of-type(5) {
width: 15%;
} }
} }

View file

@ -0,0 +1,4 @@
.txo__radios {
display: flex;
flex-direction: row;
}

View file

@ -6139,9 +6139,9 @@ lazy-val@^1.0.4:
yargs "^13.2.2" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#677dd256437f4cefa3eba96f0a28a814d07736c8: lbry-redux@lbryio/lbry-redux#c51483f4171f0056da65193f979a483248a68297:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/677dd256437f4cefa3eba96f0a28a814d07736c8" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/c51483f4171f0056da65193f979a483248a68297"
dependencies: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"