diff --git a/CHANGELOG.md b/CHANGELOG.md index 10c3f05b7..01532c8b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Web UI version numbers should always match the corresponding version of LBRY App * Added a loading message to file actions * URL is auto suggested in Publish Page * Added infinite scroll to channel pages + * Added claim support button and inline form to file page. ### Changed * Publishing revamped. Editing claims is much easier. diff --git a/ui/js/actions/claims.js b/ui/js/actions/claims.js new file mode 100644 index 000000000..8ac1d7548 --- /dev/null +++ b/ui/js/actions/claims.js @@ -0,0 +1,68 @@ +import * as types from "constants/action_types"; +import lbry from "lbry"; +import { selectClaimSupport, selectClaimSupportAmount } from "selectors/claims"; +import { selectBalance } from "selectors/wallet"; +import { doOpenModal } from "actions/app"; + +export function doClaimNewSupport() { + return function(dispatch, getState) { + const state = getState(); + const claimSupport = selectClaimSupport(state); + const balance = selectBalance(state); + const amount = selectClaimSupportAmount(state); + + if (balance - amount < 1) { + return dispatch(doOpenModal("insufficientBalance")); + } + + dispatch({ + type: types.CLAIM_SUPPORT_STARTED, + }); + + const successCallback = results => { + // txid hash present indicates successful request + if (results.txid && results.txid.length > 0) { + dispatch({ + type: types.CLAIM_SUPPORT_COMPLETED, + }); + dispatch(doOpenModal("transactionSuccessful")); + } else { + dispatch({ + type: types.CLAIM_SUPPORT_FAILED, + data: { error: results }, + }); + dispatch(doOpenModal("transactionFailed")); + } + }; + + const errorCallback = error => { + dispatch({ + type: types.CLAIM_SUPPORT_FAILED, + data: { error: error.message }, + }); + dispatch(doOpenModal("transactionFailed")); + }; + + lbry + .claim_new_support({ + name: claimSupport.name, + claim_id: claimSupport.claim_id, + amount: claimSupport.amount, + }) + .then(successCallback, errorCallback); + }; +} + +export function doSetClaimSupportAmount(amount) { + return { + type: types.SET_CLAIM_SUPPORT_AMOUNT, + data: { amount }, + }; +} + +export function doSetClaimSupportClaim(claim_id, name) { + return { + type: types.SET_CLAIM_SUPPORT_CLAIM, + data: { claim_id, name }, + }; +} diff --git a/ui/js/component/fileActions/index.js b/ui/js/component/fileActions/index.js index 4311823d1..d1d2b9f9d 100644 --- a/ui/js/component/fileActions/index.js +++ b/ui/js/component/fileActions/index.js @@ -6,6 +6,7 @@ import { makeSelectDownloadingForUri, makeSelectLoadingForUri, } from "selectors/file_info"; + import { makeSelectIsAvailableForUri } from "selectors/availability"; import { selectCurrentModal } from "selectors/app"; import { makeSelectCostInfoForUri } from "selectors/cost_info"; @@ -15,8 +16,16 @@ import { doOpenFileInShell, doOpenFileInFolder } from "actions/file_info"; import { makeSelectClaimForUriIsMine } from "selectors/claims"; import { doPurchaseUri, doLoadVideo, doStartDownload } from "actions/content"; import FileActions from "./view"; +import { makeSelectClaimForUri } from "selectors/claims"; +import { + doClaimNewSupport, + doSetClaimSupportAmount, + doSetClaimSupportClaim, +} from "actions/claims"; +import { selectClaimSupportAmount } from "selectors/claims"; const makeSelect = () => { + const selectClaim = makeSelectClaimForUri(); const selectFileInfoForUri = makeSelectFileInfoForUri(); const selectIsAvailableForUri = makeSelectIsAvailableForUri(); const selectDownloadingForUri = makeSelectDownloadingForUri(); @@ -25,6 +34,7 @@ const makeSelect = () => { const selectClaimForUriIsMine = makeSelectClaimForUriIsMine(); const select = (state, props) => ({ + claim: selectClaim(state, props), fileInfo: selectFileInfoForUri(state, props), /*availability check is disabled due to poor performance, TBD if it dies forever or requires daemon fix*/ isAvailable: true, //selectIsAvailableForUri(state, props), @@ -34,6 +44,7 @@ const makeSelect = () => { costInfo: selectCostInfoForUri(state, props), loading: selectLoadingForUri(state, props), claimIsMine: selectClaimForUriIsMine(state, props), + amount: selectClaimSupportAmount(state), }); return select; @@ -48,6 +59,10 @@ const perform = dispatch => ({ startDownload: uri => dispatch(doPurchaseUri(uri, "affirmPurchase")), loadVideo: uri => dispatch(doLoadVideo(uri)), restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)), + claimNewSupport: () => dispatch(doClaimNewSupport()), + setAmount: event => dispatch(doSetClaimSupportAmount(event.target.value)), + setClaimSupport: (claim_id, name) => + dispatch(doSetClaimSupportClaim(claim_id, name)), }); export default connect(makeSelect, perform)(FileActions); diff --git a/ui/js/component/fileActions/view.jsx b/ui/js/component/fileActions/view.jsx index df624a345..344e5fe48 100644 --- a/ui/js/component/fileActions/view.jsx +++ b/ui/js/component/fileActions/view.jsx @@ -1,6 +1,7 @@ import React from "react"; import { Icon, BusyMessage } from "component/common"; import FilePrice from "component/filePrice"; +import { FormField } from "component/form"; import { Modal } from "component/modal"; import Link from "component/link"; import { ToolTip } from "component/tooltip"; @@ -57,6 +58,58 @@ class FileActions extends React.PureComponent { this.props.loadVideo(this.props.uri); } + onSupportClaimClicked(event) { + if (this.state.showSupportClaimForm) { + return; + } + + let button; + let parentCard; + if ("a" === event.target.tagName.toLowerCase()) { + button = event.target; + } + + let parent = event.target.parentElement; + do { + if (!button && "a" === parent.tagName.toLowerCase()) { + button = parent; + } + + if ("card" === parent.className.trim()) { + parentCard = parent; + } + parent = parent.parentElement; + } while (parent && !parentCard); + + this.setState({ + showSupportClaimForm: true, + supportClaimLinkOffset: button && parentCard + ? button.getBoundingClientRect().left - + parentCard.getBoundingClientRect().left - + 12 /* left pad + icon */ + : 0, + }); + } + + sendSupportClaim() { + this.setState({ supportInProgress: true }); + const { claim, setClaimSupport, claimNewSupport } = this.props; + setClaimSupport(claim.claim_id, claim.name); + claimNewSupport(); + } + + onClaimSupportSuccessful() { + this.setState({ + showSupportClaimForm: false, + }); + this.onClaimSupportCompleted(); + } + + onClaimSupportCompleted() { + this.props.closeModal(); + this.setState({ supportInProgress: false }); + } + render() { const { fileInfo, @@ -73,6 +126,8 @@ class FileActions extends React.PureComponent { costInfo, loading, claimIsMine, + amount, + setAmount, } = this.props; const metadata = fileInfo ? fileInfo.metadata : null, @@ -180,6 +235,45 @@ class FileActions extends React.PureComponent { /> : ""} + + {this.state.showSupportClaimForm && +
+
+ +
+ 0.0) || this.state.supportInProgress + } + /> + this.setState({ showSupportClaimForm: false })} + /> +
+ +
} } + {modal == "insufficientBalance" && + + {__( + "Insufficient balance: after supporting this claim, you would have less than 1 LBC in your wallet." + )} + } + {modal == "transactionSuccessful" && + + {__( + "Your claim support transaction was successfully placed in the queue." + )} + } + {modal == "transactionFailed" && + + {__("Something went wrong")}: + } ); } diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 71d6d8072..439e10fb2 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -112,5 +112,11 @@ export const CLAIM_REWARD_STARTED = "CLAIM_REWARD_STARTED"; export const CLAIM_REWARD_SUCCESS = "CLAIM_REWARD_SUCCESS"; export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE"; export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR"; -export const FETCH_REWARD_CONTENT_COMPLETED = - "FETCH_REWARD_CONTENT_COMPLETED"; +export const FETCH_REWARD_CONTENT_COMPLETED = "FETCH_REWARD_CONTENT_COMPLETED"; + +// File Actions +export const SET_CLAIM_SUPPORT_CLAIM = "SET_CLAIM_SUPPORT_CLAIM"; +export const SET_CLAIM_SUPPORT_AMOUNT = "SET_CLAIM_SUPPORT_AMOUNT"; +export const CLAIM_SUPPORT_STARTED = "CLAIM_SUPPORT_STARTED"; +export const CLAIM_SUPPORT_COMPLETED = "CLAIM_SUPPORT_COMPLETED"; +export const CLAIM_SUPPORT_FAILED = "CLAIM_SUPPORT_FAILED"; diff --git a/ui/js/reducers/claims.js b/ui/js/reducers/claims.js index c70fd45e0..3ad236687 100644 --- a/ui/js/reducers/claims.js +++ b/ui/js/reducers/claims.js @@ -2,7 +2,14 @@ import * as types from "constants/action_types"; import lbryuri from "lbryuri"; const reducers = {}; -const defaultState = {}; +const buildClaimSupport = () => ({ + name: undefined, + amount: undefined, + claim_id: undefined, +}); +const defaultState = { + claimSupport: buildClaimSupport(), +}; reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) { const { uri, certificate, claim } = action.data; @@ -190,6 +197,56 @@ reducers[types.CREATE_CHANNEL_COMPLETED] = function(state, action) { }); }; +reducers[types.SET_CLAIM_SUPPORT_CLAIM] = function(state, action) { + const oldClaimSupport = state.claimSupport; + const newClaimSupport = Object.assign({}, oldClaimSupport, { + claim_id: action.data.claim_id, + name: action.data.name, + }); + + return Object.assign({}, state, { + claimSupport: newClaimSupport, + }); +}; + +reducers[types.SET_CLAIM_SUPPORT_AMOUNT] = function(state, action) { + const oldClaimSupport = state.claimSupport; + const newClaimSupport = Object.assign({}, oldClaimSupport, { + amount: parseFloat(action.data.amount), + }); + + return Object.assign({}, state, { + claimSupport: newClaimSupport, + }); +}; + +reducers[types.CLAIM_SUPPORT_STARTED] = function(state, action) { + const newClaimSupport = Object.assign({}, state.claimSupport, { + sending: true, + }); + + return Object.assign({}, state, { + claimSupport: newClaimSupport, + }); +}; + +reducers[types.CLAIM_SUPPORT_COMPLETED] = function(state, action) { + return Object.assign({}, state, { + draftTransaction: buildClaimSupport(), + }); +}; + +reducers[types.CLAIM_SUPPORT_FAILED] = function(state, action) { + const newClaimSupport = Object.assign({}, state.claimSupport, { + sending: false, + error: action.data.error, + }); + + return Object.assign({}, state, { + claimSupport: newClaimSupport, + }); +}; + export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; if (handler) return handler(state, action); diff --git a/ui/js/selectors/claims.js b/ui/js/selectors/claims.js index 2c4b4b511..065b7475c 100644 --- a/ui/js/selectors/claims.js +++ b/ui/js/selectors/claims.js @@ -215,3 +215,13 @@ export const selectMyChannelClaims = createSelector( return claims; } ); + +export const selectClaimSupport = createSelector( + _selectState, + state => state.claimSupport || {} +); + +export const selectClaimSupportAmount = createSelector( + selectClaimSupport, + claimSupport => claimSupport.amount +); diff --git a/ui/scss/component/_file-actions.scss b/ui/scss/component/_file-actions.scss index 4eda16b51..27e231358 100644 --- a/ui/scss/component/_file-actions.scss +++ b/ui/scss/component/_file-actions.scss @@ -29,4 +29,13 @@ $color-download: #444; z-index: 1; top: 0px; left: 0px; +} +.file-actions__support_claim { + position: relative; + display: block; + width: 50%; +} +.file-actions__inline-buttons { + display: inline-block; + margin-left: $spacing-vertical } \ No newline at end of file