diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d8e64203..fbc90574f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Web UI version numbers should always match the corresponding version of LBRY App ## [Unreleased] ### Added + * Now you can revoke your claims from the txns list itself.(#581) * The app now closes to the system tray unless specifically requested to quit. (#374) * Added new window menu options for reloading and help. * Rewards are now marked in transaction history (#660) diff --git a/ui/js/actions/content.js b/ui/js/actions/content.js index a87e39e2e..6347536c0 100644 --- a/ui/js/actions/content.js +++ b/ui/js/actions/content.js @@ -4,7 +4,7 @@ import lbry from "lbry"; import lbryio from "lbryio"; import lbryuri from "lbryuri"; import { makeSelectClientSetting } from "selectors/settings"; -import { selectBalance } from "selectors/wallet"; +import { selectBalance, selectTransactionItems } from "selectors/wallet"; import { makeSelectFileInfoForUri, selectDownloadingByOutpoint, @@ -495,3 +495,46 @@ export function doPublish(params) { }); }; } + +export function doAbandonClaim(txid, nout) { + return function(dispatch, getState) { + const state = getState(); + const transactionItems = selectTransactionItems(state); + const { claim_id: claimId, claim_name: name } = transactionItems.find( + claim => claim.txid == txid && claim.nout == nout + ); + + dispatch({ + type: types.ABANDON_CLAIM_STARTED, + data: { + claimId: claimId, + }, + }); + + const errorCallback = error => { + dispatch(doOpenModal(modals.TRANSACTION_FAILED)); + }; + + const successCallback = results => { + if (results.txid) { + dispatch({ + type: types.ABANDON_CLAIM_SUCCEEDED, + data: { + claimId: claimId, + }, + }); + dispatch(doResolveUri(lbryuri.build({ name, claimId }))); + dispatch(doFetchClaimListMine()); + } else { + dispatch(doOpenModal(modals.TRANSACTION_FAILED)); + } + }; + + lbry + .claim_abandon({ + txid: txid, + nout: nout, + }) + .then(successCallback, errorCallback); + }; +} diff --git a/ui/js/actions/file_info.js b/ui/js/actions/file_info.js index 49c949e5c..ef9a742d5 100644 --- a/ui/js/actions/file_info.js +++ b/ui/js/actions/file_info.js @@ -1,6 +1,6 @@ import * as types from "constants/action_types"; import lbry from "lbry"; -import { doFetchClaimListMine } from "actions/content"; +import { doFetchClaimListMine, doAbandonClaim } from "actions/content"; import { selectClaimsByUri, selectIsFetchingClaimListMine, @@ -102,20 +102,10 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) { const fileInfo = byOutpoint[outpoint]; if (fileInfo) { - dispatch({ - type: types.ABANDON_CLAIM_STARTED, - data: { - claimId: fileInfo.claim_id, - }, - }); + txid = fileInfo.outpoint.slice(0, -2); + nout = fileInfo.outpoint.slice(-1); - const success = dispatch({ - type: types.ABANDON_CLAIM_SUCCEEDED, - data: { - claimId: fileInfo.claim_id, - }, - }); - lbry.claim_abandon({ claim_id: fileInfo.claim_id }).then(success); + dispatch(doAbandonClaim(txid, nout)); } } diff --git a/ui/js/component/transactionList/index.js b/ui/js/component/transactionList/index.js index f6ffc976d..48ea3bc05 100644 --- a/ui/js/component/transactionList/index.js +++ b/ui/js/component/transactionList/index.js @@ -1,15 +1,19 @@ import React from "react"; import { connect } from "react-redux"; import { doNavigate } from "actions/navigation"; +import { doOpenModal } from "actions/app"; import { selectClaimedRewardsByTransactionId } from "selectors/rewards"; +import { selectAllMyClaimsByOutpoint } from "selectors/claims"; import TransactionList from "./view"; const select = state => ({ rewards: selectClaimedRewardsByTransactionId(state), + myClaims: selectAllMyClaimsByOutpoint(state), }); const perform = dispatch => ({ navigate: (path, params) => dispatch(doNavigate(path, params)), + openModal: (modal, props) => dispatch(doOpenModal(modal, props)), }); export default connect(select, perform)(TransactionList); diff --git a/ui/js/component/transactionList/internal/TransactionListItem.jsx b/ui/js/component/transactionList/internal/TransactionListItem.jsx index 22b35cb12..35cd33247 100644 --- a/ui/js/component/transactionList/internal/TransactionListItem.jsx +++ b/ui/js/component/transactionList/internal/TransactionListItem.jsx @@ -4,14 +4,41 @@ import { CreditAmount } from "component/common"; import DateTime from "component/dateTime"; import Link from "component/link"; import lbryuri from "lbryuri"; +import * as txnTypes from "constants/transaction_types"; class TransactionListItem extends React.PureComponent { + abandonClaim() { + const { txid, nout } = this.props.transaction; + + this.props.revokeClaim(txid, nout); + } + + getLink(type) { + if (type == txnTypes.TIP) { + return ( + + ); + } else { + return ( + + ); + } + } + capitalize(string) { return string.charAt(0).toUpperCase() + string.slice(1); } render() { - const { reward, transaction } = this.props; + const { reward, transaction, isRevokeable } = this.props; const { amount, claim_id: claimId, @@ -20,6 +47,7 @@ class TransactionListItem extends React.PureComponent { fee, txid, type, + nout, } = transaction; const dateFormat = { @@ -43,7 +71,7 @@ class TransactionListItem extends React.PureComponent { : - {__("(Transaction pending)")} + {__("Pending")} } @@ -64,7 +92,9 @@ class TransactionListItem extends React.PureComponent { />} - {this.capitalize(type)} + {this.capitalize(type)}{" "} + {isRevokeable && this.getLink(type)} + {reward && diff --git a/ui/js/component/transactionList/view.jsx b/ui/js/component/transactionList/view.jsx index beb2f8514..f736320bf 100644 --- a/ui/js/component/transactionList/view.jsx +++ b/ui/js/component/transactionList/view.jsx @@ -1,6 +1,9 @@ import React from "react"; import TransactionListItem from "./internal/TransactionListItem"; import FormField from "component/formField"; +import Link from "component/link"; +import * as icons from "constants/icons"; +import * as modals from "constants/modal_types"; class TransactionList extends React.PureComponent { constructor(props) { @@ -23,6 +26,16 @@ class TransactionList extends React.PureComponent { return !filter || filter == transaction.type; } + isRevokeable(txid, nout) { + // a claim/support/update is revokable if it + // is in my claim list(claim_list_mine) + return this.props.myClaims.has(`${txid}:${nout}`); + } + + revokeClaim(txid, nout) { + this.props.openModal(modals.CONFIRM_CLAIM_REVOKE, { txid, nout }); + } + render() { const { emptyMessage, rewards, transactions } = this.props; @@ -48,6 +61,11 @@ class TransactionList extends React.PureComponent { + {" "} + } {!transactionList.length &&
@@ -70,6 +88,8 @@ class TransactionList extends React.PureComponent { key={`${t.txid}:${t.nout}`} transaction={t} reward={rewards && rewards[t.txid]} + isRevokeable={this.isRevokeable(t.txid, t.nout)} + revokeClaim={this.revokeClaim.bind(this)} /> )} diff --git a/ui/js/component/video/view.jsx b/ui/js/component/video/view.jsx index 31f0d9e23..f65a63204 100644 --- a/ui/js/component/video/view.jsx +++ b/ui/js/component/video/view.jsx @@ -73,9 +73,7 @@ class Video extends React.PureComponent { "It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds." ); } else if (isLoading) { - loadStatusMessage = __( - "Requesting stream..." - ); + loadStatusMessage = __("Requesting stream..."); } else if (isDownloading) { loadStatusMessage = __("Downloading stream... not long left now!"); } diff --git a/ui/js/constants/icons.js b/ui/js/constants/icons.js index ce2f54e22..d28d7591e 100644 --- a/ui/js/constants/icons.js +++ b/ui/js/constants/icons.js @@ -2,3 +2,4 @@ export const FEATURED = "rocket"; export const LOCAL = "folder"; export const FILE = "file"; export const HISTORY = "history"; +export const HELP_CIRCLE = "question-circle"; diff --git a/ui/js/constants/modal_types.js b/ui/js/constants/modal_types.js index 9e82be50a..d68349d44 100644 --- a/ui/js/constants/modal_types.js +++ b/ui/js/constants/modal_types.js @@ -13,3 +13,4 @@ export const INSUFFICIENT_BALANCE = "insufficient_balance"; export const REWARD_APPROVAL_REQUIRED = "reward_approval_required"; export const AFFIRM_PURCHASE = "affirm_purchase"; export const CREDIT_INTRO = "credit_intro"; +export const CONFIRM_CLAIM_REVOKE = "confirmClaimRevoke"; diff --git a/ui/js/constants/transaction_types.js b/ui/js/constants/transaction_types.js new file mode 100644 index 000000000..89530f9f0 --- /dev/null +++ b/ui/js/constants/transaction_types.js @@ -0,0 +1 @@ +export const TIP = "tip"; diff --git a/ui/js/modal/modalRevokeClaim/index.js b/ui/js/modal/modalRevokeClaim/index.js new file mode 100644 index 000000000..766095b64 --- /dev/null +++ b/ui/js/modal/modalRevokeClaim/index.js @@ -0,0 +1,17 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doCloseModal } from "actions/app"; +import { doAbandonClaim } from "actions/content"; +import { selectTransactionItems } from "selectors/wallet"; +import ModalRevokeClaim from "./view"; + +const select = state => ({ + transactionItems: selectTransactionItems(state), +}); + +const perform = dispatch => ({ + closeModal: () => dispatch(doCloseModal()), + abandonClaim: (txid, nout) => dispatch(doAbandonClaim(txid, nout)), +}); + +export default connect(select, perform)(ModalRevokeClaim); diff --git a/ui/js/modal/modalRevokeClaim/view.jsx b/ui/js/modal/modalRevokeClaim/view.jsx new file mode 100644 index 000000000..06f1362f8 --- /dev/null +++ b/ui/js/modal/modalRevokeClaim/view.jsx @@ -0,0 +1,86 @@ +import React from "react"; +import { Modal } from "modal/modal"; +import * as txnTypes from "constants/transaction_types"; + +class ModalRevokeClaim extends React.PureComponent { + constructor(props) { + super(props); + } + + revokeClaim() { + const { txid, nout } = this.props; + + this.props.closeModal(); + this.props.abandonClaim(txid, nout); + } + + getButtonLabel(type) { + if (type == txnTypes.TIP) { + return "Confirm Tip Unlock"; + } else { + return "Confirm Claim Revoke"; + } + } + + getMsgBody(type) { + if (type == txnTypes.TIP) { + return ( +
+

{__("Confirm Tip Unlock")}

+

+ {__("Are you sure you want to unlock these credits?")} +
+
+ {__( + "These credits are permanently yours and can be\ + unlocked at any time. Unlocking them allows you to\ + spend them, but can hurt the performance of your\ + content in lookups and search results. It is\ + recommended you leave tips locked until you\ + need or want to spend them." + )} +

+
+ ); + } else { + return ( +
+

{__("Confirm Claim Revoke")}

+

+ {__("Are you sure want to revoke this claim?")} +
+
+ {__( + "This will prevent others from resolving and\ + accessing the content you published. It will return\ + the LBC to your spendable balance, less a small\ + transaction fee." + )} +

+
+ ); + } + } + + render() { + const { transactionItems, txid, nout, closeModal } = this.props; + const { type } = transactionItems.find( + claim => claim.txid == txid && claim.nout == nout + ); + + return ( + + {this.getMsgBody(type)} + + ); + } +} + +export default ModalRevokeClaim; diff --git a/ui/js/modal/modalRouter/view.jsx b/ui/js/modal/modalRouter/view.jsx index 8d2bf912e..d7beb7d3d 100644 --- a/ui/js/modal/modalRouter/view.jsx +++ b/ui/js/modal/modalRouter/view.jsx @@ -13,6 +13,7 @@ import ModalTransactionFailed from "modal/modalTransactionFailed"; import ModalInsufficientBalance from "modal/modalInsufficientBalance"; import ModalFileTimeout from "modal/modalFileTimeout"; import ModalAffirmPurchase from "modal/modalAffirmPurchase"; +import ModalRevokeClaim from "modal/modalRevokeClaim"; import * as modals from "constants/modal_types"; class ModalRouter extends React.PureComponent { @@ -132,6 +133,8 @@ class ModalRouter extends React.PureComponent { return ; case modals.AFFIRM_PURCHASE: return ; + case modals.CONFIRM_CLAIM_REVOKE: + return ; default: return null; } diff --git a/ui/js/reducers/claims.js b/ui/js/reducers/claims.js index a3e4ae2b4..270790d8f 100644 --- a/ui/js/reducers/claims.js +++ b/ui/js/reducers/claims.js @@ -45,11 +45,6 @@ reducers[types.FETCH_CLAIM_LIST_MINE_COMPLETED] = function(state, action) { const byId = Object.assign({}, state.byId); const pendingById = Object.assign({}, state.pendingById); const abandoningById = Object.assign({}, state.abandoningById); - const myClaims = new Set( - claims - .map(claim => claim.claim_id) - .filter(claimId => Object.keys(abandoningById).indexOf(claimId) === -1) - ); claims .filter(claim => claim.category && claim.category.match(/claim/)) @@ -77,7 +72,7 @@ reducers[types.FETCH_CLAIM_LIST_MINE_COMPLETED] = function(state, action) { return Object.assign({}, state, { isFetchingClaimListMine: false, - myClaims: myClaims, + myClaims: claims, byId, pendingById, }); @@ -158,10 +153,8 @@ reducers[types.ABANDON_CLAIM_STARTED] = function(state, action) { reducers[types.ABANDON_CLAIM_SUCCEEDED] = function(state, action) { const { claimId } = action.data; - const myClaims = new Set(state.myClaims); const byId = Object.assign({}, state.byId); const claimsByUri = Object.assign({}, state.claimsByUri); - const uris = []; Object.keys(claimsByUri).forEach(uri => { if (claimsByUri[uri] === claimId) { @@ -170,10 +163,8 @@ reducers[types.ABANDON_CLAIM_SUCCEEDED] = function(state, action) { }); delete byId[claimId]; - myClaims.delete(claimId); return Object.assign({}, state, { - myClaims, byId, claimsByUri, }); diff --git a/ui/js/selectors/claims.js b/ui/js/selectors/claims.js index c90b78843..c29522dcf 100644 --- a/ui/js/selectors/claims.js +++ b/ui/js/selectors/claims.js @@ -52,7 +52,7 @@ export const makeSelectClaimIsMine = rawUri => { const uri = lbryuri.normalize(rawUri); return createSelector( selectClaimsByUri, - selectMyClaimsRaw, + selectMyActiveClaims, (claims, myClaims) => claims && claims[uri] && @@ -123,19 +123,31 @@ export const selectIsFetchingClaimListMine = createSelector( export const selectMyClaimsRaw = createSelector( _selectState, - state => new Set(state.myClaims) + state => state.myClaims ); export const selectAbandoningIds = createSelector(_selectState, state => Object.keys(state.abandoningById || {}) ); +export const selectMyActiveClaims = createSelector( + selectMyClaimsRaw, + selectAbandoningIds, + (claims, abandoningIds) => + new Set( + claims && + claims + .map(claim => claim.claim_id) + .filter(claimId => Object.keys(abandoningIds).indexOf(claimId) === -1) + ) +); + export const selectPendingClaims = createSelector(_selectState, state => Object.values(state.pendingById || {}) ); export const selectMyClaims = createSelector( - selectMyClaimsRaw, + selectMyActiveClaims, selectClaimsById, selectAbandoningIds, selectPendingClaims, @@ -157,6 +169,11 @@ export const selectMyClaimsWithoutChannels = createSelector( myClaims => myClaims.filter(claim => !claim.name.match(/^@/)) ); +export const selectAllMyClaimsByOutpoint = createSelector( + selectMyClaimsRaw, + claims => new Set(claims.map(claim => `${claim.txid}:${claim.nout}`)) +); + export const selectMyClaimsOutpoints = createSelector( selectMyClaims, myClaims => {