[WIP] Support claim UI implementation #406
8 changed files with 309 additions and 3 deletions
71
ui/js/actions/file_actions.js
Normal file
71
ui/js/actions/file_actions.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import * as types from "constants/action_types";
|
||||||
|
import lbry from "lbry";
|
||||||
|
import {
|
||||||
|
selectClaimSupport,
|
||||||
|
selectClaimSupportAmount,
|
||||||
|
} from "selectors/file_actions";
|
||||||
|
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,
|
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/file_actions";
|
||||||
|
import { selectClaimSupportAmount } from "selectors/file_actions";
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
@ -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";
|
||||||
|
@ -33,7 +34,7 @@ class FileActions extends React.PureComponent {
|
||||||
fileInfo &&
|
fileInfo &&
|
||||||
!fileInfo.completed &&
|
!fileInfo.completed &&
|
||||||
fileInfo.written_bytes !== false &&
|
fileInfo.written_bytes !== false &&
|
||||||
fileInfo.written_bytes < fileInfo.total_bytes
|
fileInfo.fwritten_bytes < fileInfo.total_bytes
|
||||||
) {
|
) {
|
||||||
restartDownload(uri, fileInfo.outpoint);
|
restartDownload(uri, fileInfo.outpoint);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
68
ui/js/reducers/file_actions.js
Normal file
68
ui/js/reducers/file_actions.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import * as types from "constants/action_types";
|
||||||
|
|
||||||
|
const reducers = {};
|
||||||
|
const buildClaimSupport = () => ({
|
||||||
|
name: undefined,
|
||||||
|
amount: undefined,
|
||||||
|
claim_id: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
claimSupport: buildClaimSupport(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
return state;
|
||||||
|
}
|
13
ui/js/selectors/file_actions.js
Normal file
13
ui/js/selectors/file_actions.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { createSelector } from "reselect";
|
||||||
|
|
||||||
|
export const _selectState = state => state.fileActions || {};
|
||||||
|
|
||||||
|
export const selectClaimSupport = createSelector(
|
||||||
|
_selectState,
|
||||||
|
state => state.claimSupport || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectClaimSupportAmount = createSelector(
|
||||||
|
selectClaimSupport,
|
||||||
|
claimSupport => claimSupport.amount
|
||||||
|
);
|
|
@ -10,6 +10,7 @@ import searchReducer from "reducers/search";
|
||||||
import settingsReducer from "reducers/settings";
|
import settingsReducer from "reducers/settings";
|
||||||
import userReducer from "reducers/user";
|
import userReducer from "reducers/user";
|
||||||
import walletReducer from "reducers/wallet";
|
import walletReducer from "reducers/wallet";
|
||||||
|
import fileActionsReducer from "reducers/file_actions";
|
||||||
import { persistStore, autoRehydrate } from "redux-persist";
|
import { persistStore, autoRehydrate } from "redux-persist";
|
||||||
import createCompressor from "redux-persist-transform-compress";
|
import createCompressor from "redux-persist-transform-compress";
|
||||||
import createFilter from "redux-persist-transform-filter";
|
import createFilter from "redux-persist-transform-filter";
|
||||||
|
@ -65,6 +66,7 @@ const reducers = redux.combineReducers({
|
||||||
settings: settingsReducer,
|
settings: settingsReducer,
|
||||||
wallet: walletReducer,
|
wallet: walletReducer,
|
||||||
user: userReducer,
|
user: userReducer,
|
||||||
|
fileActions: fileActionsReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bulkThunk = createBulkThunkMiddleware();
|
const bulkThunk = createBulkThunkMiddleware();
|
||||||
|
|
|
@ -30,3 +30,12 @@ $color-download: #444;
|
||||||
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 * 1
|
||||||
|
}
|
Loading…
Reference in a new issue