[WIP] Support claim UI implementation #406

Closed
akinwale wants to merge 5 commits from ui-claim-support into master
8 changed files with 291 additions and 3 deletions

View file

@ -16,6 +16,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
* Added a loading message to file actions * Added a loading message to file actions
* URL is auto suggested in Publish Page * URL is auto suggested in Publish Page
* Added infinite scroll to channel pages * Added infinite scroll to channel pages
* Added claim support button and inline form to file page.
### Changed ### Changed
* Publishing revamped. Editing claims is much easier. * Publishing revamped. Editing claims is much easier.

68
ui/js/actions/claims.js Normal file
View file

@ -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 },
};
}

View file

@ -6,6 +6,7 @@ import {
makeSelectDownloadingForUri, makeSelectDownloadingForUri,
makeSelectLoadingForUri, makeSelectLoadingForUri,
} from "selectors/file_info"; } from "selectors/file_info";
import { makeSelectIsAvailableForUri } from "selectors/availability"; import { makeSelectIsAvailableForUri } from "selectors/availability";
import { selectCurrentModal } from "selectors/app"; import { selectCurrentModal } from "selectors/app";
import { makeSelectCostInfoForUri } from "selectors/cost_info"; import { makeSelectCostInfoForUri } from "selectors/cost_info";
@ -15,8 +16,16 @@ import { doOpenFileInShell, doOpenFileInFolder } from "actions/file_info";
import { makeSelectClaimForUriIsMine } from "selectors/claims"; import { makeSelectClaimForUriIsMine } from "selectors/claims";
import { doPurchaseUri, doLoadVideo, doStartDownload } from "actions/content"; import { doPurchaseUri, doLoadVideo, doStartDownload } from "actions/content";
import FileActions from "./view"; import FileActions from "./view";
import { makeSelectClaimForUri } from "selectors/claims";
import {
doClaimNewSupport,
doSetClaimSupportAmount,
doSetClaimSupportClaim,
} from "actions/claims";
import { selectClaimSupportAmount } from "selectors/claims";
const makeSelect = () => { const makeSelect = () => {
const selectClaim = makeSelectClaimForUri();
const selectFileInfoForUri = makeSelectFileInfoForUri(); const selectFileInfoForUri = makeSelectFileInfoForUri();
const selectIsAvailableForUri = makeSelectIsAvailableForUri(); const selectIsAvailableForUri = makeSelectIsAvailableForUri();
const selectDownloadingForUri = makeSelectDownloadingForUri(); const selectDownloadingForUri = makeSelectDownloadingForUri();
@ -25,6 +34,7 @@ const makeSelect = () => {
const selectClaimForUriIsMine = makeSelectClaimForUriIsMine(); const selectClaimForUriIsMine = makeSelectClaimForUriIsMine();
const select = (state, props) => ({ const select = (state, props) => ({
claim: selectClaim(state, props),
fileInfo: selectFileInfoForUri(state, props), fileInfo: selectFileInfoForUri(state, props),
/*availability check is disabled due to poor performance, TBD if it dies forever or requires daemon fix*/ /*availability check is disabled due to poor performance, TBD if it dies forever or requires daemon fix*/
isAvailable: true, //selectIsAvailableForUri(state, props), isAvailable: true, //selectIsAvailableForUri(state, props),
@ -34,6 +44,7 @@ const makeSelect = () => {
costInfo: selectCostInfoForUri(state, props), costInfo: selectCostInfoForUri(state, props),
loading: selectLoadingForUri(state, props), loading: selectLoadingForUri(state, props),
claimIsMine: selectClaimForUriIsMine(state, props), claimIsMine: selectClaimForUriIsMine(state, props),
amount: selectClaimSupportAmount(state),
}); });
return select; return select;
@ -48,6 +59,10 @@ const perform = dispatch => ({
startDownload: uri => dispatch(doPurchaseUri(uri, "affirmPurchase")), startDownload: uri => dispatch(doPurchaseUri(uri, "affirmPurchase")),
loadVideo: uri => dispatch(doLoadVideo(uri)), loadVideo: uri => dispatch(doLoadVideo(uri)),
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)), 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); export default connect(makeSelect, perform)(FileActions);

View file

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { Icon, BusyMessage } from "component/common"; import { Icon, BusyMessage } from "component/common";
import FilePrice from "component/filePrice"; import FilePrice from "component/filePrice";
import { FormField } from "component/form";
import { Modal } from "component/modal"; import { Modal } from "component/modal";
import Link from "component/link"; import Link from "component/link";
import { ToolTip } from "component/tooltip"; import { ToolTip } from "component/tooltip";
@ -57,6 +58,58 @@ class FileActions extends React.PureComponent {
this.props.loadVideo(this.props.uri); 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() { render() {
const { const {
fileInfo, fileInfo,
@ -73,6 +126,8 @@ class FileActions extends React.PureComponent {
costInfo, costInfo,
loading, loading,
claimIsMine, claimIsMine,
amount,
setAmount,
} = this.props; } = this.props;
const metadata = fileInfo ? fileInfo.metadata : null, const metadata = fileInfo ? fileInfo.metadata : null,
@ -180,6 +235,45 @@ class FileActions extends React.PureComponent {
/> />
</DropDownMenu> </DropDownMenu>
: ""} : ""}
<Link
label={__("Support Claim")}
button="text"
style={{ position: "relative" }}
icon="icon-life-ring"
onClick={this.onSupportClaimClicked.bind(this)}
/>
{this.state.showSupportClaimForm &&
<div
className="file-actions__support_claim"
style={{ marginLeft: this.state.supportClaimLinkOffset + "px" }}
>
<form onSubmit={this.sendSupportClaim.bind(this)}>
<FormField
type="number"
min="0.01"
placeholder="0.01"
step="0.01"
postfix="LBC"
className="form-field__input--inline"
onChange={setAmount}
/>
<div className="file-actions__inline-buttons">
<Link
button="primary"
label={__("Confirm")}
onClick={this.sendSupportClaim.bind(this)}
disabled={
!(parseFloat(amount) > 0.0) || this.state.supportInProgress
}
/>
<Link
button="cancel"
label={__("Cancel")}
onClick={() => this.setState({ showSupportClaimForm: false })}
/>
</div>
</form>
</div>}
<Modal <Modal
type="confirm" type="confirm"
isOpen={modal == "affirmPurchase"} isOpen={modal == "affirmPurchase"}
@ -206,6 +300,34 @@ class FileActions extends React.PureComponent {
outpoint={fileInfo.outpoint} outpoint={fileInfo.outpoint}
title={title} title={title}
/>} />}
{modal == "insufficientBalance" &&
<Modal
isOpen={true}
contentLabel={__("Insufficient balance")}
onConfirmed={this.onClaimSupportCompleted.bind(this)}
>
{__(
"Insufficient balance: after supporting this claim, you would have less than 1 LBC in your wallet."
)}
</Modal>}
{modal == "transactionSuccessful" &&
<Modal
isOpen={true}
contentLabel={__("Transaction successful")}
onConfirmed={this.onClaimSupportSuccessful.bind(this)}
>
{__(
"Your claim support transaction was successfully placed in the queue."
)}
</Modal>}
{modal == "transactionFailed" &&
<Modal
isOpen={true}
contentLabel={__("Transaction failed")}
onConfirmed={this.onClaimSupportCompleted.bind(this)}
>
{__("Something went wrong")}:
</Modal>}
</section> </section>
); );
} }

View file

@ -112,5 +112,11 @@ export const CLAIM_REWARD_STARTED = "CLAIM_REWARD_STARTED";
export const CLAIM_REWARD_SUCCESS = "CLAIM_REWARD_SUCCESS"; export const CLAIM_REWARD_SUCCESS = "CLAIM_REWARD_SUCCESS";
export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE"; export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE";
export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR"; export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR";
export const FETCH_REWARD_CONTENT_COMPLETED = export const FETCH_REWARD_CONTENT_COMPLETED = "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";

View file

@ -2,7 +2,14 @@ import * as types from "constants/action_types";
import lbryuri from "lbryuri"; import lbryuri from "lbryuri";
const reducers = {}; 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) { reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
const { uri, certificate, claim } = action.data; 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) { export default function reducer(state = defaultState, action) {
const handler = reducers[action.type]; const handler = reducers[action.type];
if (handler) return handler(state, action); if (handler) return handler(state, action);

View file

@ -215,3 +215,13 @@ export const selectMyChannelClaims = createSelector(
return claims; return claims;
} }
); );
export const selectClaimSupport = createSelector(
_selectState,
state => state.claimSupport || {}
);
export const selectClaimSupportAmount = createSelector(
selectClaimSupport,
claimSupport => claimSupport.amount
);

View file

@ -29,4 +29,13 @@ $color-download: #444;
z-index: 1; z-index: 1;
top: 0px; top: 0px;
left: 0px; left: 0px;
}
.file-actions__support_claim {
position: relative;
display: block;
width: 50%;
}
.file-actions__inline-buttons {
display: inline-block;
margin-left: $spacing-vertical
} }