[WIP] Support claim UI implementation #406

Closed
akinwale wants to merge 5 commits from ui-claim-support into master
8 changed files with 309 additions and 3 deletions
Showing only changes of commit 3257d87a68 - Show all commits

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

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/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);

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";
@ -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>
); );
} }

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

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

View 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
);

View file

@ -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();

View file

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