[WIP] Support claim UI implementation #406
8 changed files with 291 additions and 3 deletions
|
@ -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.
|
||||
|
|
68
ui/js/actions/claims.js
Normal file
68
ui/js/actions/claims.js
Normal 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 },
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
|||
/>
|
||||
</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
|
||||
type="confirm"
|
||||
isOpen={modal == "affirmPurchase"}
|
||||
|
@ -206,6 +300,34 @@ class FileActions extends React.PureComponent {
|
|||
outpoint={fileInfo.outpoint}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue