Paid content on lbry.tv #4197
64 changed files with 968 additions and 354 deletions
|
@ -31,8 +31,8 @@
|
||||||
"koa-logger": "^3.2.1",
|
"koa-logger": "^3.2.1",
|
||||||
"koa-send": "^5.0.0",
|
"koa-send": "^5.0.0",
|
||||||
"koa-static": "^5.0.0",
|
"koa-static": "^5.0.0",
|
||||||
"lbry-redux": "lbryio/lbry-redux#87ae7faf1c1d5ffa86feb578899596f6ea2a5fd9",
|
"lbry-redux": "lbryio/lbry-redux#f6e5b69e5aa337d50503a2f5ebb5efe4eda4ac57",
|
||||||
"lbryinc": "lbryio/lbryinc#0addc624db54000b0447f4539f91f5758d26eef3",
|
"lbryinc": "lbryio/lbryinc#6a52f8026cdc7cd56d200fb5c46f852e0139bbeb",
|
||||||
"mysql": "^2.17.1",
|
"mysql": "^2.17.1",
|
||||||
"node-fetch": "^2.6.0"
|
"node-fetch": "^2.6.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -3333,17 +3333,17 @@ latest-version@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
package-json "^4.0.0"
|
package-json "^4.0.0"
|
||||||
|
|
||||||
lbry-redux@lbryio/lbry-redux#87ae7faf1c1d5ffa86feb578899596f6ea2a5fd9:
|
lbry-redux@lbryio/lbry-redux#f6e5b69e5aa337d50503a2f5ebb5efe4eda4ac57:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/87ae7faf1c1d5ffa86feb578899596f6ea2a5fd9"
|
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/f6e5b69e5aa337d50503a2f5ebb5efe4eda4ac57"
|
||||||
dependencies:
|
dependencies:
|
||||||
proxy-polyfill "0.1.6"
|
proxy-polyfill "0.1.6"
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
uuid "^3.3.2"
|
uuid "^3.3.2"
|
||||||
|
|
||||||
lbryinc@lbryio/lbryinc#0addc624db54000b0447f4539f91f5758d26eef3:
|
lbryinc@lbryio/lbryinc#6a52f8026cdc7cd56d200fb5c46f852e0139bbeb:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/0addc624db54000b0447f4539f91f5758d26eef3"
|
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/6a52f8026cdc7cd56d200fb5c46f852e0139bbeb"
|
||||||
dependencies:
|
dependencies:
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
"@babel/register": "^7.0.0",
|
"@babel/register": "^7.0.0",
|
||||||
"@exponent/electron-cookies": "^2.0.0",
|
"@exponent/electron-cookies": "^2.0.0",
|
||||||
"@hot-loader/react-dom": "^16.8",
|
"@hot-loader/react-dom": "^16.8",
|
||||||
"@lbry/components": "^4.1.5",
|
"@lbry/components": "^4.2.2",
|
||||||
"@reach/menu-button": "0.7.4",
|
"@reach/menu-button": "0.7.4",
|
||||||
"@reach/rect": "^0.2.1",
|
"@reach/rect": "^0.2.1",
|
||||||
"@reach/tabs": "^0.1.5",
|
"@reach/tabs": "^0.1.5",
|
||||||
|
@ -132,8 +132,8 @@
|
||||||
"imagesloaded": "^4.1.4",
|
"imagesloaded": "^4.1.4",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||||
"lbry-redux": "lbryio/lbry-redux#cd9c15567f2934ddc82de364d88b378ff04d5571",
|
"lbry-redux": "lbryio/lbry-redux#aa2cfa789670e899824d3d3ac1ae677172a7ad4e",
|
||||||
"lbryinc": "lbryio/lbryinc#cc62a4eec10845cc0b31da7d0f27287cfa7c4866",
|
"lbryinc": "lbryio/lbryinc#6a52f8026cdc7cd56d200fb5c46f852e0139bbeb",
|
||||||
"lint-staged": "^7.0.2",
|
"lint-staged": "^7.0.2",
|
||||||
"localforage": "^1.7.1",
|
"localforage": "^1.7.1",
|
||||||
"lodash-es": "^4.17.14",
|
"lodash-es": "^4.17.14",
|
||||||
|
@ -211,7 +211,7 @@
|
||||||
"yarn": "^1.3"
|
"yarn": "^1.3"
|
||||||
},
|
},
|
||||||
"lbrySettings": {
|
"lbrySettings": {
|
||||||
"lbrynetDaemonVersion": "0.72.0",
|
"lbrynetDaemonVersion": "0.74.0",
|
||||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||||
"lbrynetDaemonDir": "static/daemon",
|
"lbrynetDaemonDir": "static/daemon",
|
||||||
"lbrynetDaemonFileName": "lbrynet"
|
"lbrynetDaemonFileName": "lbrynet"
|
||||||
|
|
|
@ -30,7 +30,7 @@ if (isProduction) {
|
||||||
|
|
||||||
type Analytics = {
|
type Analytics = {
|
||||||
error: string => Promise<any>,
|
error: string => Promise<any>,
|
||||||
sentryError: ({}, {}) => Promise<any>,
|
sentryError: ({} | string, {}) => Promise<any>,
|
||||||
pageView: string => void,
|
pageView: string => void,
|
||||||
setUser: Object => void,
|
setUser: Object => void,
|
||||||
toggleInternal: (boolean, ?boolean) => void,
|
toggleInternal: (boolean, ?boolean) => void,
|
||||||
|
@ -45,6 +45,7 @@ type Analytics = {
|
||||||
emailVerifiedEvent: () => void,
|
emailVerifiedEvent: () => void,
|
||||||
rewardEligibleEvent: () => void,
|
rewardEligibleEvent: () => void,
|
||||||
startupEvent: () => void,
|
startupEvent: () => void,
|
||||||
|
purchaseEvent: number => void,
|
||||||
readyEvent: number => void,
|
readyEvent: number => void,
|
||||||
openUrlEvent: string => void,
|
openUrlEvent: string => void,
|
||||||
};
|
};
|
||||||
|
@ -217,6 +218,9 @@ const analytics: Analytics = {
|
||||||
sendGaEvent('Startup', 'App-Ready');
|
sendGaEvent('Startup', 'App-Ready');
|
||||||
sendGaTimingEvent('Startup', 'App-Ready', timeToReady);
|
sendGaTimingEvent('Startup', 'App-Ready', timeToReady);
|
||||||
},
|
},
|
||||||
|
purchaseEvent: (purchaseInt: number) => {
|
||||||
|
sendGaEvent('Purchase', 'Purchase-Complete', undefined, purchaseInt);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function sendGaEvent(category, action, label, value) {
|
function sendGaEvent(category, action, label, value) {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content';
|
import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content';
|
||||||
|
import { makeSelectClaimWasPurchased } from 'lbry-redux';
|
||||||
import ClaimInsufficientCredits from './view';
|
import ClaimInsufficientCredits from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
isInsufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
isInsufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||||
|
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select)(ClaimInsufficientCredits);
|
export default connect(select)(ClaimInsufficientCredits);
|
||||||
|
|
|
@ -7,12 +7,13 @@ type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
fileInfo: FileListItem,
|
fileInfo: FileListItem,
|
||||||
isInsufficientCredits: boolean,
|
isInsufficientCredits: boolean,
|
||||||
|
claimWasPurchased: boolea,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimInsufficientCredits(props: Props) {
|
function ClaimInsufficientCredits(props: Props) {
|
||||||
const { isInsufficientCredits, fileInfo } = props;
|
const { isInsufficientCredits, fileInfo, claimWasPurchased } = props;
|
||||||
|
|
||||||
if (fileInfo || !isInsufficientCredits) {
|
if (fileInfo || !isInsufficientCredits || claimWasPurchased) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ type Props = {
|
||||||
followedTags?: Array<Tag>,
|
followedTags?: Array<Tag>,
|
||||||
injectedItem: ?Node,
|
injectedItem: ?Node,
|
||||||
infiniteScroll?: Boolean,
|
infiniteScroll?: Boolean,
|
||||||
|
feeAmount?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimListDiscover(props: Props) {
|
function ClaimListDiscover(props: Props) {
|
||||||
|
@ -92,6 +93,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
infiniteScroll = true,
|
infiniteScroll = true,
|
||||||
followedTags,
|
followedTags,
|
||||||
injectedItem,
|
injectedItem,
|
||||||
|
feeAmount,
|
||||||
} = props;
|
} = props;
|
||||||
const didNavigateForward = history.action === 'PUSH';
|
const didNavigateForward = history.action === 'PUSH';
|
||||||
const { search } = location;
|
const { search } = location;
|
||||||
|
@ -113,14 +115,17 @@ function ClaimListDiscover(props: Props) {
|
||||||
const streamTypeParam =
|
const streamTypeParam =
|
||||||
streamType || (CS.FILE_TYPES.includes(contentTypeParam) && contentTypeParam) || defaultStreamType || null;
|
streamType || (CS.FILE_TYPES.includes(contentTypeParam) && contentTypeParam) || defaultStreamType || null;
|
||||||
const durationParam = urlParams.get(CS.DURATION_KEY) || null;
|
const durationParam = urlParams.get(CS.DURATION_KEY) || null;
|
||||||
|
const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY);
|
||||||
|
const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds;
|
||||||
|
const feeAmountParam = urlParams.get('fee_amount') || feeAmount || CS.FEE_AMOUNT_ANY;
|
||||||
const showDuration = !(claimType && claimType === CS.CLAIM_CHANNEL);
|
const showDuration = !(claimType && claimType === CS.CLAIM_CHANNEL);
|
||||||
const isFiltered = () =>
|
const isFiltered = () =>
|
||||||
Boolean(
|
Boolean(
|
||||||
urlParams.get(CS.FRESH_KEY) ||
|
urlParams.get(CS.FRESH_KEY) ||
|
||||||
urlParams.get(CS.CONTENT_KEY) ||
|
urlParams.get(CS.CONTENT_KEY) ||
|
||||||
urlParams.get(CS.DURATION_KEY) ||
|
urlParams.get(CS.DURATION_KEY) ||
|
||||||
urlParams.get(CS.TAGS_KEY)
|
urlParams.get(CS.TAGS_KEY) ||
|
||||||
|
urlParams.get(CS.FEE_AMOUNT_KEY)
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -143,6 +148,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
duration?: string,
|
duration?: string,
|
||||||
reposted_claim_id?: string,
|
reposted_claim_id?: string,
|
||||||
stream_types?: any,
|
stream_types?: any,
|
||||||
|
fee_amount?: string,
|
||||||
} = {
|
} = {
|
||||||
page_size: pageSize || CS.PAGE_SIZE,
|
page_size: pageSize || CS.PAGE_SIZE,
|
||||||
page,
|
page,
|
||||||
|
@ -151,10 +157,10 @@ function ClaimListDiscover(props: Props) {
|
||||||
// no_totals makes it so the sdk doesn't have to calculate total number pages for pagination
|
// no_totals makes it so the sdk doesn't have to calculate total number pages for pagination
|
||||||
// it's faster, but we will need to remove it if we start using total_pages
|
// it's faster, but we will need to remove it if we start using total_pages
|
||||||
no_totals: true,
|
no_totals: true,
|
||||||
channel_ids: channelIds || [],
|
channel_ids: channelIdsParam || [],
|
||||||
not_channel_ids:
|
not_channel_ids:
|
||||||
// If channelIds were passed in, we don't need not_channel_ids
|
// If channelIdsParam were passed in, we don't need not_channel_ids
|
||||||
!channelIds && hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [],
|
!channelIdsParam && hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [],
|
||||||
not_tags: !showNsfw ? MATURE_TAGS : [],
|
not_tags: !showNsfw ? MATURE_TAGS : [],
|
||||||
order_by:
|
order_by:
|
||||||
orderParam === CS.ORDER_BY_TRENDING
|
orderParam === CS.ORDER_BY_TRENDING
|
||||||
|
@ -212,6 +218,10 @@ function ClaimListDiscover(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (feeAmountParam) {
|
||||||
|
options.fee_amount = feeAmountParam;
|
||||||
|
}
|
||||||
|
|
||||||
if (durationParam) {
|
if (durationParam) {
|
||||||
if (durationParam === CS.DURATION_SHORT) {
|
if (durationParam === CS.DURATION_SHORT) {
|
||||||
options.duration = '<=1800';
|
options.duration = '<=1800';
|
||||||
|
@ -310,6 +320,15 @@ function ClaimListDiscover(props: Props) {
|
||||||
history.push(url);
|
history.push(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAdvancedReset() {
|
||||||
|
const newUrlParams = new URLSearchParams(search);
|
||||||
|
newUrlParams.delete('claim_type');
|
||||||
|
newUrlParams.delete('channel_ids');
|
||||||
|
const newSearch = `?${newUrlParams.toString()}`;
|
||||||
|
|
||||||
|
history.push(newSearch);
|
||||||
|
}
|
||||||
|
|
||||||
function getParamFromTags(t) {
|
function getParamFromTags(t) {
|
||||||
if (t === CS.TAGS_ALL || t === CS.TAGS_FOLLOWED) {
|
if (t === CS.TAGS_ALL || t === CS.TAGS_FOLLOWED) {
|
||||||
return t;
|
return t;
|
||||||
|
@ -319,7 +338,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUrl(delta) {
|
function buildUrl(delta) {
|
||||||
const newUrlParams = new URLSearchParams();
|
const newUrlParams = new URLSearchParams(location.search);
|
||||||
CS.KEYS.forEach(k => {
|
CS.KEYS.forEach(k => {
|
||||||
// $FlowFixMe append() can't take null as second arg, but get() can return null
|
// $FlowFixMe append() can't take null as second arg, but get() can return null
|
||||||
if (urlParams.get(k) !== null) newUrlParams.append(k, urlParams.get(k));
|
if (urlParams.get(k) !== null) newUrlParams.append(k, urlParams.get(k));
|
||||||
|
@ -370,6 +389,13 @@ function ClaimListDiscover(props: Props) {
|
||||||
newUrlParams.set(CS.TAGS_KEY, delta.value);
|
newUrlParams.set(CS.TAGS_KEY, delta.value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case CS.FEE_AMOUNT_KEY:
|
||||||
|
if (delta.value === CS.FEE_AMOUNT_ANY) {
|
||||||
|
newUrlParams.delete(CS.FEE_AMOUNT_KEY);
|
||||||
|
} else {
|
||||||
|
newUrlParams.set(CS.FEE_AMOUNT_KEY, delta.value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return `?${newUrlParams.toString()}`;
|
return `?${newUrlParams.toString()}`;
|
||||||
}
|
}
|
||||||
|
@ -485,7 +511,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{CS.CONTENT_TYPES.map(type => {
|
{CS.CONTENT_TYPES.map(type => {
|
||||||
if (type !== CS.CLAIM_CHANNEL || (type === CS.CLAIM_CHANNEL && !channelIds)) {
|
if (type !== CS.CLAIM_CHANNEL || (type === CS.CLAIM_CHANNEL && !channelIdsParam)) {
|
||||||
return (
|
return (
|
||||||
<option key={type} value={type}>
|
<option key={type} value={type}>
|
||||||
{/* i18fixme */}
|
{/* i18fixme */}
|
||||||
|
@ -505,6 +531,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* DURATIONS FIELD */}
|
{/* DURATIONS FIELD */}
|
||||||
{showDuration && (
|
{showDuration && (
|
||||||
<div className={'claim-search__input-container'}>
|
<div className={'claim-search__input-container'}>
|
||||||
|
@ -541,6 +568,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TAGS FIELD */}
|
{/* TAGS FIELD */}
|
||||||
{!tags && (
|
{!tags && (
|
||||||
<div className={'claim-search__input-container'}>
|
<div className={'claim-search__input-container'}>
|
||||||
|
@ -585,6 +613,38 @@ function ClaimListDiscover(props: Props) {
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* PAID FIELD */}
|
||||||
|
<div className={'claim-search__input-container'}>
|
||||||
|
<FormField
|
||||||
|
className={classnames('claim-search__dropdown', {
|
||||||
|
'claim-search__dropdown--selected':
|
||||||
|
feeAmountParam === CS.FEE_AMOUNT_ONLY_FREE || feeAmountParam === CS.FEE_AMOUNT_ONLY_PAID,
|
||||||
|
})}
|
||||||
|
label={__('Price')}
|
||||||
|
type="select"
|
||||||
|
name="paidcontent"
|
||||||
|
value={feeAmountParam}
|
||||||
|
onChange={e =>
|
||||||
|
handleChange({
|
||||||
|
key: CS.FEE_AMOUNT_KEY,
|
||||||
|
value: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value={CS.FEE_AMOUNT_ANY}>Anything</option>
|
||||||
|
<option value={CS.FEE_AMOUNT_ONLY_FREE}>Free</option>
|
||||||
|
<option value={CS.FEE_AMOUNT_ONLY_PAID}>Paid</option>
|
||||||
|
))}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{channelIdsInUrl && (
|
||||||
|
<div className={'claim-search__input-container'}>
|
||||||
|
<label>{__('Advanced Filters from URL')}</label>
|
||||||
|
<Button button="alt" label={__('Clear')} onClick={handleAdvancedReset} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -13,10 +13,11 @@ import {
|
||||||
doFileGet,
|
doFileGet,
|
||||||
makeSelectReflectingClaimForUri,
|
makeSelectReflectingClaimForUri,
|
||||||
makeSelectClaimWasPurchased,
|
makeSelectClaimWasPurchased,
|
||||||
|
makeSelectStreamingUrlForUri,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import { makeSelectHasVisitedUri, makeSelectStreamingUrlForUriWebProxy } from 'redux/selectors/content';
|
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
|
||||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
import ClaimPreview from './view';
|
import ClaimPreview from './view';
|
||||||
|
|
||||||
|
@ -36,7 +37,7 @@ const select = (state, props) => ({
|
||||||
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
|
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
|
||||||
channelIsBlocked: props.uri && selectChannelIsBlocked(props.uri)(state),
|
channelIsBlocked: props.uri && selectChannelIsBlocked(props.uri)(state),
|
||||||
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
|
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
|
||||||
streamingUrl: props.uri && makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
|
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
|
||||||
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
|
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
|
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
|
||||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
prefixUris?: Array<string>,
|
prefixUris?: Array<string>,
|
||||||
|
@ -23,6 +24,7 @@ type Props = {
|
||||||
releaseTime?: string,
|
releaseTime?: string,
|
||||||
claimType?: Array<string>,
|
claimType?: Array<string>,
|
||||||
timestamp?: string,
|
timestamp?: string,
|
||||||
|
feeAmount?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimTilesDiscover(props: Props) {
|
function ClaimTilesDiscover(props: Props) {
|
||||||
|
@ -42,7 +44,12 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
claimType,
|
claimType,
|
||||||
prefixUris,
|
prefixUris,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
feeAmount,
|
||||||
} = props;
|
} = props;
|
||||||
|
const { location } = useHistory();
|
||||||
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
const feeAmountInUrl = urlParams.get('fee_amount');
|
||||||
|
const feeAmountParam = feeAmountInUrl || feeAmount;
|
||||||
const [hasSearched, setHasSearched] = React.useState(false);
|
const [hasSearched, setHasSearched] = React.useState(false);
|
||||||
const options: {
|
const options: {
|
||||||
page_size: number,
|
page_size: number,
|
||||||
|
@ -56,6 +63,7 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
release_time?: string,
|
release_time?: string,
|
||||||
claim_type?: Array<string>,
|
claim_type?: Array<string>,
|
||||||
timestamp?: string,
|
timestamp?: string,
|
||||||
|
fee_amount?: string,
|
||||||
} = {
|
} = {
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
claim_type: claimType || undefined,
|
claim_type: claimType || undefined,
|
||||||
|
@ -76,6 +84,10 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
options.release_time = releaseTime;
|
options.release_time = releaseTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (feeAmountParam) {
|
||||||
|
options.fee_amount = feeAmountParam;
|
||||||
|
}
|
||||||
|
|
||||||
// https://github.com/lbryio/lbry-desktop/issues/3774
|
// https://github.com/lbryio/lbry-desktop/issues/3774
|
||||||
if (hideReposts) {
|
if (hideReposts) {
|
||||||
if (Array.isArray(options.claim_type)) {
|
if (Array.isArray(options.claim_type)) {
|
||||||
|
|
|
@ -15,6 +15,7 @@ type Props = {
|
||||||
icon?: string,
|
icon?: string,
|
||||||
className?: string,
|
className?: string,
|
||||||
isPageTitle?: boolean,
|
isPageTitle?: boolean,
|
||||||
|
noTitleWrap?: boolean,
|
||||||
isBodyList?: boolean,
|
isBodyList?: boolean,
|
||||||
defaultExpand?: boolean,
|
defaultExpand?: boolean,
|
||||||
nag?: Node,
|
nag?: Node,
|
||||||
|
@ -31,6 +32,7 @@ export default function Card(props: Props) {
|
||||||
className,
|
className,
|
||||||
isPageTitle = false,
|
isPageTitle = false,
|
||||||
isBodyList = false,
|
isBodyList = false,
|
||||||
|
noTitleWrap = false,
|
||||||
defaultExpand,
|
defaultExpand,
|
||||||
nag,
|
nag,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -40,8 +42,12 @@ export default function Card(props: Props) {
|
||||||
return (
|
return (
|
||||||
<section className={classnames(className, 'card')}>
|
<section className={classnames(className, 'card')}>
|
||||||
{(title || subtitle) && (
|
{(title || subtitle) && (
|
||||||
<div className="card__header--between">
|
<div
|
||||||
<div className="card__section--flex">
|
className={classnames('card__header--between', {
|
||||||
|
'card__header--nowrap': noTitleWrap,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={classnames('card__title-section', { 'card__title-section--body-list': isBodyList })}>
|
||||||
{icon && <Icon sectionIcon icon={icon} />}
|
{icon && <Icon sectionIcon icon={icon} />}
|
||||||
<div>
|
<div>
|
||||||
{isPageTitle && <h1 className="card__title">{title}</h1>}
|
{isPageTitle && <h1 className="card__title">{title}</h1>}
|
||||||
|
|
|
@ -82,7 +82,7 @@ class CreditAmount extends React.PureComponent<Props> {
|
||||||
'badge--free': badge && isFree,
|
'badge--free': badge && isFree,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span>{amountText}</span>
|
<span className="credit-amount">{amountText}</span>
|
||||||
|
|
||||||
{isEstimate ? (
|
{isEstimate ? (
|
||||||
<span className="credit-amount__estimate" title={__('This is an estimate and does not include data fees')}>
|
<span className="credit-amount__estimate" title={__('This is an estimate and does not include data fees')}>
|
||||||
|
|
|
@ -8,7 +8,6 @@ import useIsMobile from 'effects/use-is-mobile';
|
||||||
const PAGINATE_PARAM = 'page';
|
const PAGINATE_PARAM = 'page';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
loading: boolean,
|
|
||||||
totalPages: number,
|
totalPages: number,
|
||||||
location: { search: string },
|
location: { search: string },
|
||||||
history: { push: string => void },
|
history: { push: string => void },
|
||||||
|
@ -16,7 +15,7 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function Paginate(props: Props) {
|
function Paginate(props: Props) {
|
||||||
const { totalPages = 1, loading, location, history, onPageChange } = props;
|
const { totalPages = 1, location, history, onPageChange } = props;
|
||||||
const { search } = location;
|
const { search } = location;
|
||||||
const [textValue, setTextValue] = React.useState('');
|
const [textValue, setTextValue] = React.useState('');
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
|
@ -45,7 +44,7 @@ function Paginate(props: Props) {
|
||||||
return (
|
return (
|
||||||
// Hide the paginate controls if we are loading or there is only one page
|
// Hide the paginate controls if we are loading or there is only one page
|
||||||
// It should still be rendered to trigger the onPageChange callback
|
// It should still be rendered to trigger the onPageChange callback
|
||||||
<Form style={totalPages <= 1 || loading ? { display: 'none' } : null} onSubmit={handlePaginateKeyUp}>
|
<Form style={totalPages <= 1 ? { display: 'none' } : null} onSubmit={handlePaginateKeyUp}>
|
||||||
<fieldset-group class="fieldset-group--smushed fieldgroup--paginate">
|
<fieldset-group class="fieldset-group--smushed fieldgroup--paginate">
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
<ReactPaginate
|
<ReactPaginate
|
||||||
|
|
|
@ -5,6 +5,8 @@ import {
|
||||||
makeSelectLoadingForUri,
|
makeSelectLoadingForUri,
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
|
makeSelectClaimWasPurchased,
|
||||||
|
makeSelectStreamingUrlForUri,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
import { doOpenModal, doAnalyticsView } from 'redux/actions/app';
|
import { doOpenModal, doAnalyticsView } from 'redux/actions/app';
|
||||||
|
@ -18,16 +20,14 @@ const select = (state, props) => ({
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
|
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||||
|
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
pause: () => dispatch(doSetPlayingUri(null)),
|
pause: () => dispatch(doSetPlayingUri(null)),
|
||||||
download: uri => dispatch(doPlayUri(uri, false, true, () => dispatch(doAnalyticsView(uri)))),
|
download: uri => dispatch(doPlayUri(uri, false, true, () => dispatch(doAnalyticsView(uri)))),
|
||||||
triggerViewEvent: uri => dispatch(doAnalyticsView(uri)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(FileDownloadLink);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(FileDownloadLink);
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import * as ICONS from 'constants/icons';
|
||||||
import * as MODALS from 'constants/modal_types';
|
import * as MODALS from 'constants/modal_types';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { generateDownloadUrl } from 'util/lbrytv';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -15,12 +14,12 @@ type Props = {
|
||||||
openModal: (id: string, { path: string }) => void,
|
openModal: (id: string, { path: string }) => void,
|
||||||
pause: () => void,
|
pause: () => void,
|
||||||
download: string => void,
|
download: string => void,
|
||||||
triggerViewEvent: string => void,
|
|
||||||
costInfo: ?{ cost: string },
|
costInfo: ?{ cost: string },
|
||||||
buttonType: ?string,
|
buttonType: ?string,
|
||||||
showLabel: ?boolean,
|
showLabel: ?boolean,
|
||||||
hideOpenButton: boolean,
|
hideOpenButton: boolean,
|
||||||
hideDownloadStatus: boolean,
|
hideDownloadStatus: boolean,
|
||||||
|
streamingUrl: ?string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function FileDownloadLink(props: Props) {
|
function FileDownloadLink(props: Props) {
|
||||||
|
@ -34,39 +33,43 @@ function FileDownloadLink(props: Props) {
|
||||||
download,
|
download,
|
||||||
uri,
|
uri,
|
||||||
claim,
|
claim,
|
||||||
triggerViewEvent,
|
|
||||||
costInfo,
|
|
||||||
buttonType = 'alt',
|
buttonType = 'alt',
|
||||||
showLabel = false,
|
showLabel = false,
|
||||||
hideOpenButton = false,
|
hideOpenButton = false,
|
||||||
hideDownloadStatus = false,
|
hideDownloadStatus = false,
|
||||||
|
streamingUrl,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [viewEventSent, setViewEventSent] = useState(false);
|
const [didClickDownloadButton, setDidClickDownloadButton] = useState(false);
|
||||||
|
const fileName = claim && claim.value && claim.value.source && claim.value.source.name;
|
||||||
|
|
||||||
const cost = costInfo ? Number(costInfo.cost) : 0;
|
React.useEffect(() => {
|
||||||
const isPaidContent = cost > 0;
|
if (didClickDownloadButton && streamingUrl) {
|
||||||
if (!claim || (IS_WEB && isPaidContent)) {
|
let element = document.createElement('a');
|
||||||
|
element.setAttribute('href', `${streamingUrl}?download=true`);
|
||||||
|
element.setAttribute('download', fileName);
|
||||||
|
element.style.display = 'none';
|
||||||
|
// $FlowFixMe
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.click();
|
||||||
|
// $FlowFixMe
|
||||||
|
document.body.removeChild(element);
|
||||||
|
|
||||||
|
setDidClickDownloadButton(false);
|
||||||
|
}
|
||||||
|
}, [streamingUrl, didClickDownloadButton]);
|
||||||
|
|
||||||
|
function handleDownload(e) {
|
||||||
|
setDidClickDownloadButton(true);
|
||||||
|
e.preventDefault();
|
||||||
|
download(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!claim) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, claim_id: claimId, value } = claim;
|
// @if TARGET='app'
|
||||||
const fileName = value && value.source && value.source.name;
|
|
||||||
const downloadUrl = generateDownloadUrl(name, claimId);
|
|
||||||
|
|
||||||
function handleDownload(e) {
|
|
||||||
// @if TARGET='app'
|
|
||||||
e.preventDefault();
|
|
||||||
download(uri);
|
|
||||||
// @endif;
|
|
||||||
// @if TARGET='web'
|
|
||||||
if (!viewEventSent) {
|
|
||||||
triggerViewEvent(uri);
|
|
||||||
}
|
|
||||||
setViewEventSent(true);
|
|
||||||
// @endif;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (downloading || loading) {
|
if (downloading || loading) {
|
||||||
const progress = fileInfo && fileInfo.written_bytes > 0 ? (fileInfo.written_bytes / fileInfo.total_bytes) * 100 : 0;
|
const progress = fileInfo && fileInfo.written_bytes > 0 ? (fileInfo.written_bytes / fileInfo.total_bytes) * 100 : 0;
|
||||||
const label =
|
const label =
|
||||||
|
@ -74,6 +77,7 @@ function FileDownloadLink(props: Props) {
|
||||||
|
|
||||||
return hideDownloadStatus ? null : <span className="download-text">{label}</span>;
|
return hideDownloadStatus ? null : <span className="download-text">{label}</span>;
|
||||||
}
|
}
|
||||||
|
// @endif
|
||||||
|
|
||||||
if (fileInfo && fileInfo.download_path && fileInfo.completed) {
|
if (fileInfo && fileInfo.download_path && fileInfo.completed) {
|
||||||
const openLabel = __('Open file');
|
const openLabel = __('Open file');
|
||||||
|
@ -91,7 +95,7 @@ function FileDownloadLink(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = IS_WEB ? __('Download') : __('Download to your Library');
|
const label = __('Download');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -100,10 +104,6 @@ function FileDownloadLink(props: Props) {
|
||||||
icon={ICONS.DOWNLOAD}
|
icon={ICONS.DOWNLOAD}
|
||||||
label={showLabel ? label : null}
|
label={showLabel ? label : null}
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
// @if TARGET='web'
|
|
||||||
download={fileName}
|
|
||||||
href={downloadUrl}
|
|
||||||
// @endif
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectClaimForUri } from 'lbry-redux';
|
import { makeSelectClaimForUri, makeSelectClaimWasPurchased, makeSelectClaimIsMine } from 'lbry-redux';
|
||||||
import { makeSelectCostInfoForUri, doFetchCostInfoForUri, makeSelectFetchingCostInfoForUri } from 'lbryinc';
|
import { makeSelectCostInfoForUri, doFetchCostInfoForUri, makeSelectFetchingCostInfoForUri } from 'lbryinc';
|
||||||
import FilePrice from './view';
|
import FilePrice from './view';
|
||||||
|
|
||||||
|
@ -7,14 +7,12 @@ const select = (state, props) => ({
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
fetching: makeSelectFetchingCostInfoForUri(props.uri)(state),
|
fetching: makeSelectFetchingCostInfoForUri(props.uri)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||||
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||||
// cancelFetchCostInfo: (uri) => dispatch(doCancelFetchCostInfoForUri(uri))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(FilePrice);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(FilePrice);
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
showFullPrice: boolean,
|
showFullPrice: boolean,
|
||||||
|
@ -9,12 +12,13 @@ type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
fetching: boolean,
|
fetching: boolean,
|
||||||
claim: ?{},
|
claim: ?{},
|
||||||
|
claimWasPurchased: boolean,
|
||||||
|
claimIsMine: boolean,
|
||||||
|
type?: string,
|
||||||
// below props are just passed to <CreditAmount />
|
// below props are just passed to <CreditAmount />
|
||||||
badge?: boolean,
|
|
||||||
inheritStyle?: boolean,
|
inheritStyle?: boolean,
|
||||||
showLBC?: boolean,
|
showLBC?: boolean,
|
||||||
hideFree?: boolean, // hide the file price if it's free
|
hideFree?: boolean, // hide the file price if it's free
|
||||||
className?: string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class FilePrice extends React.PureComponent<Props> {
|
class FilePrice extends React.PureComponent<Props> {
|
||||||
|
@ -39,23 +43,35 @@ class FilePrice extends React.PureComponent<Props> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { costInfo, showFullPrice, badge, inheritStyle, showLBC, hideFree, className } = this.props;
|
const { costInfo, showFullPrice, showLBC, hideFree, claimWasPurchased, type, claimIsMine } = this.props;
|
||||||
if (costInfo && (!costInfo.cost || (!costInfo.cost && hideFree))) {
|
|
||||||
|
if (claimIsMine || !costInfo || !costInfo.cost || (!costInfo.cost && hideFree)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return costInfo ? (
|
return claimWasPurchased ? (
|
||||||
|
<span
|
||||||
|
className={classnames('file-price__key', {
|
||||||
|
'file-price__key--filepage': type === 'filepage',
|
||||||
|
'file-price__key--modal': type === 'modal',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Icon icon={ICONS.PURCHASED} size={type === 'filepage' ? 22 : undefined} />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<CreditAmount
|
<CreditAmount
|
||||||
|
className={classnames('file-price', {
|
||||||
|
'file-price--filepage': type === 'filepage',
|
||||||
|
'file-price--modal': type === 'modal',
|
||||||
|
})}
|
||||||
showFree
|
showFree
|
||||||
badge={badge}
|
badge={false}
|
||||||
inheritStyle={inheritStyle}
|
|
||||||
showLBC={showLBC}
|
showLBC={showLBC}
|
||||||
amount={costInfo.cost}
|
amount={costInfo.cost}
|
||||||
isEstimate={!costInfo.includesData}
|
isEstimate={!costInfo.includesData}
|
||||||
showFullPrice={showFullPrice}
|
showFullPrice={showFullPrice}
|
||||||
className={className}
|
|
||||||
/>
|
/>
|
||||||
) : null;
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectFilePartlyDownloaded, makeSelectClaimIsMine, makeSelectClaimWasPurchased } from 'lbry-redux';
|
import { makeSelectFilePartlyDownloaded, makeSelectClaimIsMine } from 'lbry-redux';
|
||||||
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions';
|
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions';
|
||||||
import FileProperties from './view';
|
import FileProperties from './view';
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ const select = (state, props) => ({
|
||||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||||
isNew: makeSelectIsNew(props.uri)(state),
|
isNew: makeSelectIsNew(props.uri)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, null)(FileProperties);
|
export default connect(select, null)(FileProperties);
|
||||||
|
|
|
@ -14,11 +14,10 @@ type Props = {
|
||||||
isSubscribed: boolean,
|
isSubscribed: boolean,
|
||||||
isNew: boolean,
|
isNew: boolean,
|
||||||
small: boolean,
|
small: boolean,
|
||||||
claimWasPurchased: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileProperties(props: Props) {
|
export default function FileProperties(props: Props) {
|
||||||
const { uri, downloaded, claimIsMine, claimWasPurchased, isSubscribed, small = false } = props;
|
const { uri, downloaded, claimIsMine, isSubscribed, small = false } = props;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames('file-properties', {
|
className={classnames('file-properties', {
|
||||||
|
@ -29,13 +28,8 @@ export default function FileProperties(props: Props) {
|
||||||
<FileType uri={uri} />
|
<FileType uri={uri} />
|
||||||
{isSubscribed && <Icon tooltip icon={ICONS.SUBSCRIBE} />}
|
{isSubscribed && <Icon tooltip icon={ICONS.SUBSCRIBE} />}
|
||||||
{!claimIsMine && downloaded && <Icon tooltip icon={ICONS.LIBRARY} />}
|
{!claimIsMine && downloaded && <Icon tooltip icon={ICONS.LIBRARY} />}
|
||||||
{claimWasPurchased ? (
|
|
||||||
<span className="file-properties__purchased">
|
<FilePrice hideFree uri={uri} />
|
||||||
<Icon icon={ICONS.PURCHASED} />
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<FilePrice hideFree uri={uri} badge={false} className="file-properties__not-purchased" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,11 @@ import {
|
||||||
makeSelectThumbnailForUri,
|
makeSelectThumbnailForUri,
|
||||||
makeSelectContentTypeForUri,
|
makeSelectContentTypeForUri,
|
||||||
makeSelectDownloadPathForUri,
|
makeSelectDownloadPathForUri,
|
||||||
|
makeSelectStreamingUrlForUri,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import {
|
import { makeSelectFileRenderModeForUri, makeSelectFileExtensionForUri } from 'redux/selectors/content';
|
||||||
makeSelectFileRenderModeForUri,
|
|
||||||
makeSelectFileExtensionForUri,
|
|
||||||
makeSelectStreamingUrlForUriWebProxy,
|
|
||||||
} from 'redux/selectors/content';
|
|
||||||
import FileRender from './view';
|
import FileRender from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
@ -23,7 +20,7 @@ const select = (state, props) => {
|
||||||
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
contentType: makeSelectContentTypeForUri(props.uri)(state),
|
||||||
downloadPath: makeSelectDownloadPathForUri(props.uri)(state),
|
downloadPath: makeSelectDownloadPathForUri(props.uri)(state),
|
||||||
fileExtension: makeSelectFileExtensionForUri(props.uri)(state),
|
fileExtension: makeSelectFileExtensionForUri(props.uri)(state),
|
||||||
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
|
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
autoplay: autoplay,
|
autoplay: autoplay,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,31 +7,28 @@ import Button from 'component/button';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
isFree: boolean,
|
|
||||||
renderMode: string,
|
renderMode: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileRenderDownload(props: Props) {
|
export default function FileRenderDownload(props: Props) {
|
||||||
const { uri, renderMode, isFree } = props;
|
const { uri, renderMode } = props;
|
||||||
|
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
if (RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode)) {
|
if (RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode)) {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={isFree ? __('Download or Get the App') : __('Get the App')}
|
title={__('Download or Get the App')}
|
||||||
subtitle={
|
subtitle={
|
||||||
<p>
|
<p>
|
||||||
{isFree
|
{__(
|
||||||
? __(
|
'This content can be downloaded from lbry.tv, but not displayed. It will display in LBRY Desktop, an app for desktop computers.'
|
||||||
'This content can be downloaded from lbry.tv, but not displayed. It will display in LBRY Desktop, an app for desktop computers.'
|
)}
|
||||||
)
|
|
||||||
: __('Paid content requires a full LBRY app.')}
|
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
{isFree && <FileDownloadLink uri={uri} buttonType="primary" showLabel />}
|
<FileDownloadLink uri={uri} buttonType="primary" showLabel />
|
||||||
<Button button={!isFree ? 'primary' : 'link'} label={__('Get the App')} href="https://lbry.com/get" />
|
<Button button={'link'} label={__('Get the App')} href="https://lbry.com/get" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectFileInfoForUri, makeSelectTitleForUri } from 'lbry-redux';
|
import { makeSelectFileInfoForUri, makeSelectTitleForUri, makeSelectStreamingUrlForUri } from 'lbry-redux';
|
||||||
import {
|
import {
|
||||||
makeSelectIsPlayerFloating,
|
makeSelectIsPlayerFloating,
|
||||||
selectFloatingUri,
|
selectFloatingUri,
|
||||||
selectPlayingUri,
|
selectPlayingUri,
|
||||||
makeSelectFileRenderModeForUri,
|
makeSelectFileRenderModeForUri,
|
||||||
makeSelectStreamingUrlForUriWebProxy,
|
|
||||||
} from 'redux/selectors/content';
|
} from 'redux/selectors/content';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { doCloseFloatingPlayer, doSetPlayingUri } from 'redux/actions/content';
|
import { doCloseFloatingPlayer, doSetPlayingUri } from 'redux/actions/content';
|
||||||
|
@ -22,7 +21,7 @@ const select = (state, props) => {
|
||||||
title: makeSelectTitleForUri(uri)(state),
|
title: makeSelectTitleForUri(uri)(state),
|
||||||
fileInfo: makeSelectFileInfoForUri(uri)(state),
|
fileInfo: makeSelectFileInfoForUri(uri)(state),
|
||||||
isFloating: makeSelectIsPlayerFloating(props.location)(state),
|
isFloating: makeSelectIsPlayerFloating(props.location)(state),
|
||||||
streamingUrl: makeSelectStreamingUrlForUriWebProxy(uri)(state),
|
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
|
||||||
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||||
renderMode: makeSelectFileRenderModeForUri(uri)(state),
|
renderMode: makeSelectFileRenderModeForUri(uri)(state),
|
||||||
};
|
};
|
||||||
|
|
|
@ -38,6 +38,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
renderMode,
|
renderMode,
|
||||||
setPlayingUri,
|
setPlayingUri,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [fileViewerRect, setFileViewerRect] = useState();
|
const [fileViewerRect, setFileViewerRect] = useState();
|
||||||
const [desktopPlayStartTime, setDesktopPlayStartTime] = useState();
|
const [desktopPlayStartTime, setDesktopPlayStartTime] = useState();
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { makeSelectFileInfoForUri, makeSelectThumbnailForUri, makeSelectClaimForUri } from 'lbry-redux';
|
import {
|
||||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
makeSelectFileInfoForUri,
|
||||||
|
makeSelectThumbnailForUri,
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectStreamingUrlForUri,
|
||||||
|
makeSelectClaimWasPurchased,
|
||||||
|
} from 'lbry-redux';
|
||||||
|
import { makeSelectCostInfoForUri, selectUserVerifiedEmail } from 'lbryinc';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import {
|
import {
|
||||||
|
@ -10,10 +16,10 @@ import {
|
||||||
makeSelectShouldObscurePreview,
|
makeSelectShouldObscurePreview,
|
||||||
selectPlayingUri,
|
selectPlayingUri,
|
||||||
makeSelectInsufficientCreditsForUri,
|
makeSelectInsufficientCreditsForUri,
|
||||||
makeSelectStreamingUrlForUriWebProxy,
|
|
||||||
makeSelectFileRenderModeForUri,
|
makeSelectFileRenderModeForUri,
|
||||||
} from 'redux/selectors/content';
|
} from 'redux/selectors/content';
|
||||||
import FileRenderInitiator from './view';
|
import FileRenderInitiator from './view';
|
||||||
|
import { doAnaltyicsPurchaseEvent } from 'redux/actions/app';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
|
@ -22,20 +28,20 @@ const select = (state, props) => ({
|
||||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||||
playingUri: selectPlayingUri(state),
|
playingUri: selectPlayingUri(state),
|
||||||
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||||
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
|
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
||||||
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||||
|
authenticated: selectUserVerifiedEmail(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
play: uri => {
|
play: uri => {
|
||||||
dispatch(doSetPlayingUri(uri));
|
dispatch(doSetPlayingUri(uri));
|
||||||
// @if TARGET='app'
|
dispatch(doPlayUri(uri, undefined, undefined, fileInfo => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
|
||||||
dispatch(doPlayUri(uri));
|
|
||||||
// @endif
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ type Props = {
|
||||||
fileInfo: FileListItem,
|
fileInfo: FileListItem,
|
||||||
uri: string,
|
uri: string,
|
||||||
history: { push: string => void },
|
history: { push: string => void },
|
||||||
|
location: { search: ?string, pathname: string },
|
||||||
obscurePreview: boolean,
|
obscurePreview: boolean,
|
||||||
insufficientCredits: boolean,
|
insufficientCredits: boolean,
|
||||||
thumbnail?: string,
|
thumbnail?: string,
|
||||||
|
@ -29,6 +30,8 @@ type Props = {
|
||||||
inline: boolean,
|
inline: boolean,
|
||||||
renderMode: string,
|
renderMode: string,
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
|
claimWasPurchased: boolean,
|
||||||
|
authenticated: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileRenderInitiator(props: Props) {
|
export default function FileRenderInitiator(props: Props) {
|
||||||
|
@ -40,11 +43,14 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
obscurePreview,
|
obscurePreview,
|
||||||
insufficientCredits,
|
insufficientCredits,
|
||||||
history,
|
history,
|
||||||
|
location,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
autoplay,
|
autoplay,
|
||||||
renderMode,
|
renderMode,
|
||||||
hasCostInfo,
|
hasCostInfo,
|
||||||
costInfo,
|
costInfo,
|
||||||
|
claimWasPurchased,
|
||||||
|
authenticated,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const cost = costInfo && costInfo.cost;
|
const cost = costInfo && costInfo.cost;
|
||||||
|
@ -52,6 +58,10 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
const fileStatus = fileInfo && fileInfo.status;
|
const fileStatus = fileInfo && fileInfo.status;
|
||||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
||||||
|
|
||||||
|
function doAuthRedirect() {
|
||||||
|
history.push(`/$/${PAGES.AUTH}?redirect=${encodeURIComponent(location.pathname)}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Wrap this in useCallback because we need to use it to the keyboard effect
|
// Wrap this in useCallback because we need to use it to the keyboard effect
|
||||||
// If we don't a new instance will be created for every render and react will think the dependencies have changed, which will add/remove the listener for every render
|
// If we don't a new instance will be created for every render and react will think the dependencies have changed, which will add/remove the listener for every render
|
||||||
const viewFile = useCallback(
|
const viewFile = useCallback(
|
||||||
|
@ -99,13 +109,13 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAppNag = IS_WEB && (!isFree || RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode));
|
const showAppNag = IS_WEB && RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode);
|
||||||
|
const disabled = showAppNag || (!fileInfo && insufficientCredits && !claimWasPurchased);
|
||||||
const disabled = showAppNag || (!fileInfo && insufficientCredits);
|
const shouldRedirect = IS_WEB && !authenticated && !isFree;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={disabled ? undefined : viewFile}
|
onClick={disabled ? undefined : shouldRedirect ? doAuthRedirect : viewFile}
|
||||||
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||||
className={classnames('content__cover', {
|
className={classnames('content__cover', {
|
||||||
'content__cover--disabled': disabled,
|
'content__cover--disabled': disabled,
|
||||||
|
@ -121,7 +131,7 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
href="https://lbry.com/get"
|
href="https://lbry.com/get"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{insufficientCredits && !showAppNag && (
|
{!claimWasPurchased && insufficientCredits && !showAppNag && (
|
||||||
<Nag
|
<Nag
|
||||||
type="helpful"
|
type="helpful"
|
||||||
inline
|
inline
|
||||||
|
@ -132,6 +142,7 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
)}
|
)}
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<Button
|
<Button
|
||||||
|
requiresAuth={IS_WEB}
|
||||||
onClick={viewFile}
|
onClick={viewFile}
|
||||||
iconSize={30}
|
iconSize={30}
|
||||||
title={isPlayable ? __('Play') : __('View')}
|
title={isPlayable ? __('Play') : __('View')}
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectFileInfoForUri } from 'lbry-redux';
|
import { makeSelectFileInfoForUri, makeSelectStreamingUrlForUri } from 'lbry-redux';
|
||||||
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
|
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
|
||||||
import {
|
import { makeSelectFileRenderModeForUri, makeSelectIsPlaying } from 'redux/selectors/content';
|
||||||
makeSelectFileRenderModeForUri,
|
|
||||||
makeSelectIsPlaying,
|
|
||||||
makeSelectStreamingUrlForUriWebProxy,
|
|
||||||
} from 'redux/selectors/content';
|
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { doAnalyticsView } from 'redux/actions/app';
|
import { doAnalyticsView } from 'redux/actions/app';
|
||||||
import FileRenderInline from './view';
|
import FileRenderInline from './view';
|
||||||
|
@ -13,7 +9,7 @@ import FileRenderInline from './view';
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||||
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
|
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -20,10 +20,10 @@ function FileTitle(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
isPageTitle
|
isPageTitle
|
||||||
|
noTitleWrap
|
||||||
title={
|
title={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{title}
|
{title}
|
||||||
<FilePrice badge uri={normalizeURI(uri)} />
|
|
||||||
{nsfw && (
|
{nsfw && (
|
||||||
<span className="media__title-badge">
|
<span className="media__title-badge">
|
||||||
<span className="badge badge--tag-mature">{__('Mature')}</span>
|
<span className="badge badge--tag-mature">{__('Mature')}</span>
|
||||||
|
@ -31,14 +31,23 @@ function FileTitle(props: Props) {
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
|
titleActions={<FilePrice uri={normalizeURI(uri)} type="filepage" />}
|
||||||
body={
|
body={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<ClaimInsufficientCredits uri={uri} />
|
<ClaimInsufficientCredits uri={uri} />
|
||||||
<FileSubtitle uri={uri} />
|
<FileSubtitle uri={uri} />
|
||||||
<FileAuthor uri={uri} />
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
actions={<FileActions uri={uri} />}
|
actions={
|
||||||
|
<div>
|
||||||
|
<div className="section">
|
||||||
|
<FileActions uri={uri} />
|
||||||
|
</div>
|
||||||
|
<div className="section">
|
||||||
|
<FileAuthor uri={uri} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ class FileValues extends PureComponent<Props> {
|
||||||
return <span className="empty">{__('Empty claim or metadata info.')}</span>;
|
return <span className="empty">{__('Empty claim or metadata info.')}</span>;
|
||||||
}
|
}
|
||||||
const supportsAmount = claim && claim.meta && claim.meta.support_amount && Number(claim.meta.support_amount);
|
const supportsAmount = claim && claim.meta && claim.meta.support_amount && Number(claim.meta.support_amount);
|
||||||
|
const purchaseReceipt = claim && claim.purchase_receipt;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Card
|
<Card
|
||||||
|
@ -37,6 +39,18 @@ class FileValues extends PureComponent<Props> {
|
||||||
actions={
|
actions={
|
||||||
<table className="table table--condensed table--fixed table--lbc-details">
|
<table className="table table--condensed table--fixed table--lbc-details">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{purchaseReceipt && (
|
||||||
|
<tr>
|
||||||
|
<td> {__('Purchase Amount')}</td>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
button="link"
|
||||||
|
href={`https://explorer.lbry.com/tx/${purchaseReceipt.txid}`}
|
||||||
|
label={<CreditAmount badge={false} amount={Number(purchaseReceipt.amount)} precision={2} />}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td> {__('Original Publish Amount')}</td>
|
<td> {__('Original Publish Amount')}</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -49,7 +63,6 @@ class FileValues extends PureComponent<Props> {
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{' '}
|
|
||||||
{__('Supports and Tips')}
|
{__('Supports and Tips')}
|
||||||
<HelpLink href="https://lbry.com/faq/tipping" />
|
<HelpLink href="https://lbry.com/faq/tipping" />
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||||
import { selectFollowedTags } from 'lbry-redux';
|
import { selectFollowedTags, selectPurchaseUriSuccess, doClearPurchasedUriSuccess } from 'lbry-redux';
|
||||||
import { selectUploadCount, selectUserVerifiedEmail } from 'lbryinc';
|
import { selectUploadCount, selectUserVerifiedEmail } from 'lbryinc';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { doSignOut } from 'redux/actions/app';
|
import { doSignOut } from 'redux/actions/app';
|
||||||
|
@ -13,11 +13,10 @@ const select = state => ({
|
||||||
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
|
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
|
||||||
uploadCount: selectUploadCount(state),
|
uploadCount: selectUploadCount(state),
|
||||||
email: selectUserVerifiedEmail(state),
|
email: selectUserVerifiedEmail(state),
|
||||||
|
purchaseSuccess: selectPurchaseUriSuccess(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, {
|
||||||
select,
|
doSignOut,
|
||||||
{
|
doClearPurchasedUriSuccess,
|
||||||
doSignOut,
|
})(SideNavigation);
|
||||||
}
|
|
||||||
)(SideNavigation);
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Tag from 'component/tag';
|
||||||
import StickyBox from 'react-sticky-box/dist/esnext';
|
import StickyBox from 'react-sticky-box/dist/esnext';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
import classnames from 'classnames';
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
// import Ads from 'lbrytv/component/ads';
|
// import Ads from 'lbrytv/component/ads';
|
||||||
// @endif
|
// @endif
|
||||||
|
@ -24,6 +25,8 @@ type Props = {
|
||||||
expanded: boolean,
|
expanded: boolean,
|
||||||
doSignOut: () => void,
|
doSignOut: () => void,
|
||||||
location: { pathname: string },
|
location: { pathname: string },
|
||||||
|
purchaseSuccess: boolean,
|
||||||
|
doClearPurchasedUriSuccess: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function SideNavigation(props: Props) {
|
function SideNavigation(props: Props) {
|
||||||
|
@ -36,9 +39,12 @@ function SideNavigation(props: Props) {
|
||||||
sticky = true,
|
sticky = true,
|
||||||
expanded = false,
|
expanded = false,
|
||||||
location,
|
location,
|
||||||
|
purchaseSuccess,
|
||||||
|
doClearPurchasedUriSuccess,
|
||||||
} = props;
|
} = props;
|
||||||
const { pathname } = location;
|
const { pathname } = location;
|
||||||
const isAuthenticated = Boolean(email);
|
const isAuthenticated = Boolean(email);
|
||||||
|
const [pulseLibrary, setPulseLibrary] = React.useState(false);
|
||||||
const [sideInformation, setSideInformation] = usePersistedState(
|
const [sideInformation, setSideInformation] = usePersistedState(
|
||||||
'side-navigation:information',
|
'side-navigation:information',
|
||||||
getSideInformation(pathname)
|
getSideInformation(pathname)
|
||||||
|
@ -65,6 +71,19 @@ function SideNavigation(props: Props) {
|
||||||
setSideInformation(sideInfo);
|
setSideInformation(sideInfo);
|
||||||
}, [pathname, setSideInformation]);
|
}, [pathname, setSideInformation]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (purchaseSuccess) {
|
||||||
|
setPulseLibrary(true);
|
||||||
|
|
||||||
|
let timeout = setTimeout(() => {
|
||||||
|
setPulseLibrary(false);
|
||||||
|
doClearPurchasedUriSuccess();
|
||||||
|
}, 2500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}, [setPulseLibrary, purchaseSuccess, doClearPurchasedUriSuccess]);
|
||||||
|
|
||||||
function buildLink(path, label, icon, onClick, requiresAuth = false) {
|
function buildLink(path, label, icon, onClick, requiresAuth = false) {
|
||||||
return {
|
return {
|
||||||
navigate: path ? `$/${path}` : '/',
|
navigate: path ? `$/${path}` : '/',
|
||||||
|
@ -115,11 +134,9 @@ function SideNavigation(props: Props) {
|
||||||
{
|
{
|
||||||
...buildLink(PAGES.DISCOVER, __('All Content'), ICONS.DISCOVER),
|
...buildLink(PAGES.DISCOVER, __('All Content'), ICONS.DISCOVER),
|
||||||
},
|
},
|
||||||
// @if TARGET='app'
|
|
||||||
{
|
{
|
||||||
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
|
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
|
||||||
},
|
},
|
||||||
// @endif
|
|
||||||
{
|
{
|
||||||
...(expanded ? { ...buildLink(PAGES.SETTINGS, __('Settings'), ICONS.SETTINGS) } : {}),
|
...(expanded ? { ...buildLink(PAGES.SETTINGS, __('Settings'), ICONS.SETTINGS) } : {}),
|
||||||
},
|
},
|
||||||
|
@ -127,7 +144,14 @@ function SideNavigation(props: Props) {
|
||||||
linkProps =>
|
linkProps =>
|
||||||
linkProps.navigate && (
|
linkProps.navigate && (
|
||||||
<li key={linkProps.navigate}>
|
<li key={linkProps.navigate}>
|
||||||
<Button {...linkProps} className="navigation-link" activeClass="navigation-link--active" />
|
<Button
|
||||||
|
{...linkProps}
|
||||||
|
icon={pulseLibrary && linkProps.icon === ICONS.LIBRARY ? ICONS.PURCHASED : linkProps.icon}
|
||||||
|
className={classnames('navigation-link', {
|
||||||
|
'navigation-link--pulse': linkProps.icon === ICONS.LIBRARY && pulseLibrary,
|
||||||
|
})}
|
||||||
|
activeClass="navigation-link--active"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -17,6 +17,7 @@ export type Player = {
|
||||||
dispose: () => void,
|
dispose: () => void,
|
||||||
currentTime: (?number) => number,
|
currentTime: (?number) => number,
|
||||||
ended: () => boolean,
|
ended: () => boolean,
|
||||||
|
error: () => any,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
|
@ -163,6 +163,13 @@ function VideoViewer(props: Props) {
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
handlePosition(player);
|
handlePosition(player);
|
||||||
});
|
});
|
||||||
|
player.on('error', function() {
|
||||||
|
const error = player.error();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
analytics.sentryError('Video.js error', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
player.on('volumechange', () => {
|
player.on('volumechange', () => {
|
||||||
if (player && player.volume() !== volume) {
|
if (player && player.volume() !== volume) {
|
||||||
changeVolume(player.volume());
|
changeVolume(player.volume());
|
||||||
|
|
|
@ -6,10 +6,15 @@ export const DURATION_KEY = 'duration';
|
||||||
export const TAGS_KEY = 't';
|
export const TAGS_KEY = 't';
|
||||||
export const CONTENT_KEY = 'content';
|
export const CONTENT_KEY = 'content';
|
||||||
export const REPOSTED_URI_KEY = 'reposted_uri';
|
export const REPOSTED_URI_KEY = 'reposted_uri';
|
||||||
|
export const CHANNEL_IDS_KEY = 'channel_ids';
|
||||||
export const TAGS_ALL = 'tags_any';
|
export const TAGS_ALL = 'tags_any';
|
||||||
export const TAGS_FOLLOWED = 'tags_followed';
|
export const TAGS_FOLLOWED = 'tags_followed';
|
||||||
|
|
||||||
|
export const FEE_AMOUNT_KEY = 'fee_amount';
|
||||||
|
export const FEE_AMOUNT_ANY = '>=0';
|
||||||
|
export const FEE_AMOUNT_ONLY_PAID = '>0';
|
||||||
|
export const FEE_AMOUNT_ONLY_FREE = '0';
|
||||||
|
|
||||||
export const FRESH_DAY = 'day';
|
export const FRESH_DAY = 'day';
|
||||||
export const FRESH_WEEK = 'week';
|
export const FRESH_WEEK = 'week';
|
||||||
export const FRESH_MONTH = 'month';
|
export const FRESH_MONTH = 'month';
|
||||||
|
@ -39,6 +44,7 @@ export const FILE_IMAGE = 'image';
|
||||||
export const FILE_MODEL = 'model';
|
export const FILE_MODEL = 'model';
|
||||||
export const FILE_TYPES = [FILE_VIDEO, FILE_AUDIO, FILE_DOCUMENT, FILE_IMAGE, FILE_MODEL, FILE_BINARY];
|
export const FILE_TYPES = [FILE_VIDEO, FILE_AUDIO, FILE_DOCUMENT, FILE_IMAGE, FILE_MODEL, FILE_BINARY];
|
||||||
|
|
||||||
|
export const CLAIM_TYPE = 'claim_type';
|
||||||
export const CLAIM_CHANNEL = 'channel';
|
export const CLAIM_CHANNEL = 'channel';
|
||||||
export const CLAIM_STREAM = 'stream';
|
export const CLAIM_STREAM = 'stream';
|
||||||
export const CLAIM_REPOST = 'repost';
|
export const CLAIM_REPOST = 'repost';
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
doAuthTokenRefresh,
|
doAuthTokenRefresh,
|
||||||
} from 'util/saved-passwords';
|
} from 'util/saved-passwords';
|
||||||
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
|
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
|
||||||
|
import { LBRY_TV_API } from 'config';
|
||||||
|
|
||||||
// Import our app styles
|
// Import our app styles
|
||||||
// If a style is not necessary for the initial page load, it should be removed from `all.scss`
|
// If a style is not necessary for the initial page load, it should be removed from `all.scss`
|
||||||
|
@ -56,7 +57,11 @@ if (process.env.SDK_API_URL) {
|
||||||
console.warn('SDK_API_URL env var is deprecated. Use SDK_API_HOST instead');
|
console.warn('SDK_API_URL env var is deprecated. Use SDK_API_HOST instead');
|
||||||
}
|
}
|
||||||
|
|
||||||
const sdkAPIHost = process.env.SDK_API_HOST || process.env.SDK_API_URL || `https://api.lbry.tv`;
|
let sdkAPIHost = process.env.SDK_API_HOST || process.env.SDK_API_URL;
|
||||||
|
// @if TARGET='web'
|
||||||
|
sdkAPIHost = LBRY_TV_API;
|
||||||
|
// @endif
|
||||||
|
|
||||||
export const SDK_API_PATH = `${sdkAPIHost}/api/v1`;
|
export const SDK_API_PATH = `${sdkAPIHost}/api/v1`;
|
||||||
const proxyURL = `${SDK_API_PATH}/proxy`;
|
const proxyURL = `${SDK_API_PATH}/proxy`;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doSetPlayingUri, doPlayUri } from 'redux/actions/content';
|
import { doSetPlayingUri, doPlayUri } from 'redux/actions/content';
|
||||||
import { doHideModal } from 'redux/actions/app';
|
import { doHideModal, doAnaltyicsPurchaseEvent } from 'redux/actions/app';
|
||||||
import { makeSelectMetadataForUri } from 'lbry-redux';
|
import { makeSelectMetadataForUri } from 'lbry-redux';
|
||||||
import ModalAffirmPurchase from './view';
|
import ModalAffirmPurchase from './view';
|
||||||
|
|
||||||
|
@ -9,18 +9,13 @@ const select = (state, props) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
analyticsPurchaseEvent: fileInfo => dispatch(doAnaltyicsPurchaseEvent(fileInfo)),
|
||||||
cancelPurchase: () => {
|
cancelPurchase: () => {
|
||||||
dispatch(doSetPlayingUri(null));
|
dispatch(doSetPlayingUri(null));
|
||||||
dispatch(doHideModal());
|
dispatch(doHideModal());
|
||||||
},
|
},
|
||||||
closeModal: () => dispatch(doHideModal()),
|
closeModal: () => dispatch(doHideModal()),
|
||||||
loadVideo: uri => {
|
loadVideo: (uri, onSuccess) => dispatch(doPlayUri(uri, true, undefined, onSuccess)),
|
||||||
dispatch(doSetPlayingUri(uri));
|
|
||||||
dispatch(doPlayUri(uri, true));
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(ModalAffirmPurchase);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(ModalAffirmPurchase);
|
|
||||||
|
|
|
@ -1,64 +1,105 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
import FilePrice from 'component/filePrice';
|
import FilePrice from 'component/filePrice';
|
||||||
import { Modal } from 'modal/modal';
|
import { Modal } from 'modal/modal';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
|
||||||
|
// This number is tied to transitions in scss/purchase.scss
|
||||||
|
const ANIMATION_LENGTH = 2500;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
closeModal: () => void,
|
closeModal: () => void,
|
||||||
loadVideo: string => void,
|
loadVideo: (string, (GetResponse) => void) => void,
|
||||||
uri: string,
|
uri: string,
|
||||||
cancelPurchase: () => void,
|
cancelPurchase: () => void,
|
||||||
metadata: StreamMetadata,
|
metadata: StreamMetadata,
|
||||||
|
analyticsPurchaseEvent: GetResponse => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
class ModalAffirmPurchase extends React.PureComponent<Props> {
|
function ModalAffirmPurchase(props: Props) {
|
||||||
constructor() {
|
const {
|
||||||
super();
|
cancelPurchase,
|
||||||
|
closeModal,
|
||||||
|
loadVideo,
|
||||||
|
metadata: { title },
|
||||||
|
uri,
|
||||||
|
analyticsPurchaseEvent,
|
||||||
|
} = props;
|
||||||
|
const [success, setSuccess] = React.useState(false);
|
||||||
|
const [purchasing, setPurchasing] = React.useState(false);
|
||||||
|
|
||||||
(this: any).onAffirmPurchase = this.onAffirmPurchase.bind(this);
|
const modalTitle = __('Confirm Purchase');
|
||||||
|
|
||||||
|
function onAffirmPurchase() {
|
||||||
|
setPurchasing(true);
|
||||||
|
loadVideo(uri, fileInfo => {
|
||||||
|
setPurchasing(false);
|
||||||
|
setSuccess(true);
|
||||||
|
analyticsPurchaseEvent(fileInfo);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onAffirmPurchase() {
|
React.useEffect(() => {
|
||||||
this.props.closeModal();
|
let timeout;
|
||||||
this.props.loadVideo(this.props.uri);
|
if (success) {
|
||||||
}
|
timeout = setTimeout(() => {
|
||||||
|
closeModal();
|
||||||
|
setSuccess(false);
|
||||||
|
}, ANIMATION_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
return () => {
|
||||||
const {
|
if (timeout) {
|
||||||
cancelPurchase,
|
clearTimeout(timeout);
|
||||||
metadata: { title },
|
}
|
||||||
uri,
|
};
|
||||||
} = this.props;
|
}, [success, uri]);
|
||||||
|
|
||||||
const modalTitle = __('Confirm Purchase');
|
return (
|
||||||
|
<Modal type="card" isOpen contentLabel={modalTitle} onAborted={cancelPurchase}>
|
||||||
return (
|
<Card
|
||||||
<Modal type="card" isOpen contentLabel={modalTitle} onAborted={cancelPurchase}>
|
title={modalTitle}
|
||||||
<Card
|
subtitle={
|
||||||
title={modalTitle}
|
<div className={classnames('purchase-stuff', { 'purchase-stuff--purchased': success })}>
|
||||||
subtitle={
|
<div>
|
||||||
<I18nMessage
|
{success && (
|
||||||
tokens={{
|
<div className="purchase-stuff__text--purchased">
|
||||||
claim_title: <strong>{title ? `"${title}"` : uri}</strong>,
|
{__('Purchased!')}
|
||||||
amount: <FilePrice uri={uri} showFullPrice inheritStyle />,
|
<div className="purchase_stuff__subtext--purchased">
|
||||||
}}
|
{__('This content will now be in your Library.')}
|
||||||
>
|
</div>
|
||||||
This will purchase %claim_title% for %amount%.
|
</div>
|
||||||
</I18nMessage>
|
)}
|
||||||
}
|
{/* Keep this message rendered but hidden so the width doesn't change */}
|
||||||
actions={
|
<I18nMessage
|
||||||
<div className="section__actions">
|
tokens={{
|
||||||
<Button button="primary" label={__('Confirm')} onClick={this.onAffirmPurchase} />
|
claim_title: <strong>{title ? `"${title}"` : uri}</strong>,
|
||||||
<Button button="link" label={__('Cancel')} onClick={cancelPurchase} />
|
}}
|
||||||
|
>
|
||||||
|
Are you sure you want to purchase %claim_title%?
|
||||||
|
</I18nMessage>
|
||||||
</div>
|
</div>
|
||||||
}
|
<div>
|
||||||
/>
|
<FilePrice uri={uri} showFullPrice type="modal" />
|
||||||
</Modal>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
}
|
||||||
|
actions={
|
||||||
|
<div className="section__actions" style={success ? { visibility: 'hidden' } : undefined}>
|
||||||
|
<Button
|
||||||
|
button="primary"
|
||||||
|
label={purchasing ? __('Purchasing') : __('Purchase')}
|
||||||
|
onClick={onAffirmPurchase}
|
||||||
|
/>
|
||||||
|
<Button button="link" label={__('Cancel')} onClick={cancelPurchase} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ModalAffirmPurchase;
|
export default ModalAffirmPurchase;
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as MODALS from 'constants/modal_types';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import ClaimList from 'component/claimList';
|
import ClaimList from 'component/claimList';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import YoutubeTransferStatus from 'component/youtubeTransferStatus';
|
import YoutubeTransferStatus from 'component/youtubeTransferStatus';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import * as MODALS from 'constants/modal_types';
|
import Card from 'component/common/card';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
channels: Array<ChannelClaim>,
|
channels: Array<ChannelClaim>,
|
||||||
|
@ -40,12 +42,19 @@ export default function ChannelsPage(props: Props) {
|
||||||
{hasYoutubeChannels && <YoutubeTransferStatus hideChannelLink />}
|
{hasYoutubeChannels && <YoutubeTransferStatus hideChannelLink />}
|
||||||
|
|
||||||
{channels && Boolean(channels.length) && (
|
{channels && Boolean(channels.length) && (
|
||||||
<ClaimList
|
<Card
|
||||||
header={__('Your Channels')}
|
title={__('Your Channels')}
|
||||||
loading={fetchingChannels}
|
titleActions={
|
||||||
uris={channels.map(channel => channel.permanent_url)}
|
<Button
|
||||||
headerAltControls={
|
button="secondary"
|
||||||
<Button button="link" label={__('New Channel')} onClick={() => openModal(MODALS.CREATE_CHANNEL)} />
|
icon={ICONS.CHANNEL}
|
||||||
|
label={__('New Channel')}
|
||||||
|
onClick={() => openModal(MODALS.CREATE_CHANNEL)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
isBodyList
|
||||||
|
body={
|
||||||
|
<ClaimList isCardBody loading={fetchingChannels} uris={channels.map(channel => channel.permanent_url)} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -33,7 +33,7 @@ function ChannelsFollowingPage(props: Props) {
|
||||||
meta={
|
meta={
|
||||||
<Button
|
<Button
|
||||||
icon={ICONS.SEARCH}
|
icon={ICONS.SEARCH}
|
||||||
button="alt"
|
button="secondary"
|
||||||
label={__('Discover Channels')}
|
label={__('Discover Channels')}
|
||||||
navigate={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`}
|
navigate={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Page from 'component/page';
|
||||||
import ClaimListDiscover from 'component/claimListDiscover';
|
import ClaimListDiscover from 'component/claimListDiscover';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import useHover from 'effects/use-hover';
|
import useHover from 'effects/use-hover';
|
||||||
|
import useIsMobile from 'effects/use-is-mobile';
|
||||||
import analytics from 'analytics';
|
import analytics from 'analytics';
|
||||||
import HiddenNsfw from 'component/common/hidden-nsfw';
|
import HiddenNsfw from 'component/common/hidden-nsfw';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
|
@ -33,6 +34,7 @@ function DiscoverPage(props: Props) {
|
||||||
} = props;
|
} = props;
|
||||||
const buttonRef = useRef();
|
const buttonRef = useRef();
|
||||||
const isHovering = useHover(buttonRef);
|
const isHovering = useHover(buttonRef);
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const claimType = urlParams.get('claim_type');
|
const claimType = urlParams.get('claim_type');
|
||||||
|
@ -94,7 +96,8 @@ function DiscoverPage(props: Props) {
|
||||||
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
|
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
|
||||||
injectedItem={!isAuthenticated && IS_WEB && <Ads type="video" />}
|
injectedItem={!isAuthenticated && IS_WEB && <Ads type="video" />}
|
||||||
meta={
|
meta={
|
||||||
tag && (
|
tag &&
|
||||||
|
!isMobile && (
|
||||||
<Button
|
<Button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
button="alt"
|
button="alt"
|
||||||
|
|
|
@ -3,7 +3,6 @@ import {
|
||||||
makeSelectSearchDownloadUrlsForPage,
|
makeSelectSearchDownloadUrlsForPage,
|
||||||
selectDownloadUrlsCount,
|
selectDownloadUrlsCount,
|
||||||
selectIsFetchingFileList,
|
selectIsFetchingFileList,
|
||||||
doPurchaseList,
|
|
||||||
makeSelectMyPurchasesForPage,
|
makeSelectMyPurchasesForPage,
|
||||||
selectIsFetchingMyPurchases,
|
selectIsFetchingMyPurchases,
|
||||||
selectMyPurchasesCount,
|
selectMyPurchasesCount,
|
||||||
|
@ -30,8 +29,4 @@ const select = (state, props) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(
|
export default withRouter(connect(select)(FileListDownloaded));
|
||||||
connect(select, {
|
|
||||||
doPurchaseList,
|
|
||||||
})(FileListDownloaded)
|
|
||||||
);
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { FormField } from 'component/common/form-components/form-field';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import Yrbl from 'component/yrbl';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fetchingFileList: boolean,
|
fetchingFileList: boolean,
|
||||||
|
@ -38,7 +39,6 @@ function FileListDownloaded(props: Props) {
|
||||||
myDownloads,
|
myDownloads,
|
||||||
fetchingFileList,
|
fetchingFileList,
|
||||||
fetchingMyPurchases,
|
fetchingMyPurchases,
|
||||||
doPurchaseList,
|
|
||||||
} = props;
|
} = props;
|
||||||
const loading = fetchingFileList || fetchingMyPurchases;
|
const loading = fetchingFileList || fetchingMyPurchases;
|
||||||
const [viewMode, setViewMode] = React.useState(VIEW_PURCHASES);
|
const [viewMode, setViewMode] = React.useState(VIEW_PURCHASES);
|
||||||
|
@ -52,10 +52,6 @@ function FileListDownloaded(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
doPurchaseList();
|
|
||||||
}, [doPurchaseList]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
|
@ -63,7 +59,7 @@ function FileListDownloaded(props: Props) {
|
||||||
<Button
|
<Button
|
||||||
icon={ICONS.LIBRARY}
|
icon={ICONS.LIBRARY}
|
||||||
button="alt"
|
button="alt"
|
||||||
label={__('All Downloads')}
|
label={__('Downloads')}
|
||||||
className={classnames(`button-toggle`, {
|
className={classnames(`button-toggle`, {
|
||||||
'button-toggle--active': viewMode === VIEW_DOWNLOADS,
|
'button-toggle--active': viewMode === VIEW_DOWNLOADS,
|
||||||
})}
|
})}
|
||||||
|
@ -72,7 +68,7 @@ function FileListDownloaded(props: Props) {
|
||||||
<Button
|
<Button
|
||||||
icon={ICONS.PURCHASED}
|
icon={ICONS.PURCHASED}
|
||||||
button="alt"
|
button="alt"
|
||||||
label={__('Your Purchases')}
|
label={__('Purchases')}
|
||||||
className={classnames(`button-toggle`, {
|
className={classnames(`button-toggle`, {
|
||||||
'button-toggle--active': viewMode === VIEW_PURCHASES,
|
'button-toggle--active': viewMode === VIEW_PURCHASES,
|
||||||
})}
|
})}
|
||||||
|
@ -97,29 +93,46 @@ function FileListDownloaded(props: Props) {
|
||||||
}
|
}
|
||||||
isBodyList
|
isBodyList
|
||||||
body={
|
body={
|
||||||
<div>
|
IS_WEB && viewMode === VIEW_DOWNLOADS ? (
|
||||||
<ClaimList
|
<div className="main--empty">
|
||||||
isCardBody
|
<Yrbl
|
||||||
renderProperties={() => null}
|
title={__('Try Out the App!')}
|
||||||
empty={
|
subtitle={
|
||||||
viewMode === VIEW_PURCHASES && !query ? (
|
<>
|
||||||
<div>{__("You haven't purchased anything yet silly goose.")}</div>
|
<p className="section__subtitle">
|
||||||
) : (
|
{__("Download the app to track files you've viewed and downloaded.")}
|
||||||
__('No results for %query%', { query })
|
</p>
|
||||||
)
|
<div className="section__actions">
|
||||||
}
|
<Button button="primary" label={__('Get The App')} href="https://lbry.com/get" />
|
||||||
uris={viewMode === VIEW_PURCHASES ? myPurchases : myDownloads}
|
</div>
|
||||||
loading={loading}
|
</>
|
||||||
/>
|
}
|
||||||
{!query && (
|
|
||||||
<Paginate
|
|
||||||
loading={loading}
|
|
||||||
totalPages={Math.ceil(
|
|
||||||
Number(viewMode === VIEW_PURCHASES ? myPurchasesCount : downloadedUrlsCount) / Number(PAGE_SIZE)
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div>
|
||||||
|
<ClaimList
|
||||||
|
isCardBody
|
||||||
|
renderProperties={() => null}
|
||||||
|
empty={
|
||||||
|
viewMode === VIEW_PURCHASES && !query ? (
|
||||||
|
<div>{__('No purchases found.')}</div>
|
||||||
|
) : (
|
||||||
|
__('No results for %query%', { query })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
uris={viewMode === VIEW_PURCHASES ? myPurchases : myDownloads}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
{!query && (
|
||||||
|
<Paginate
|
||||||
|
totalPages={Math.ceil(
|
||||||
|
Number(viewMode === VIEW_PURCHASES ? myPurchasesCount : downloadedUrlsCount) / Number(PAGE_SIZE)
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,6 +11,64 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import { parseURI } from 'lbry-redux';
|
import { parseURI } from 'lbry-redux';
|
||||||
import { toCapitalCase } from 'util/string';
|
import { toCapitalCase } from 'util/string';
|
||||||
|
|
||||||
|
const PAID_BETA_CHANNEL_IDS_KEY = [
|
||||||
|
'4ee7cfaf1fc50a6df858ed0b99c278d633bccca9',
|
||||||
|
'5af39f818f668d8c00943c9326c5201c4fe3c423',
|
||||||
|
'cda9c4e92f19d6fe0764524a2012056e06ca2055',
|
||||||
|
'760da3ba3dd85830a843beaaed543a89b7a367e7',
|
||||||
|
'40c36948f0da072dcba3e4833e90f71e16de78be',
|
||||||
|
'e8f68563d242f6ac9784dcbc41dd86c28a9391d6',
|
||||||
|
'7236fc5d2783ea7314d9076ae6c8a250e3992d1a',
|
||||||
|
'8627af93c1a1219150f06b698f4b33e6ed2f1c1e',
|
||||||
|
'c5b0b17838df2f6c31162f64d55f60f34ae8bfc6',
|
||||||
|
'f576d5dba905fc179de880c3fe3eb3281ea74f59',
|
||||||
|
'97dd77c93c9603cbb2583f3589f7f5a6c92baa43',
|
||||||
|
'f399d873e0c37cf24de9569b5f22bbb30a5c6709',
|
||||||
|
'dba870d0620d41b2b9a152c961e0c06cf875ccfc',
|
||||||
|
'ca1fd651c9d14bf2e5088bb2aa0146ee7aeb2ae0',
|
||||||
|
'50ad846a4b1543b847bf3fdafb7b45f6b2f5844c',
|
||||||
|
'e09ff5abe9fb44dd0dd0576894a6db60a6211603',
|
||||||
|
'7b6f7517f6b816827d076fa0eaad550aa315a4e7',
|
||||||
|
'2068452c41d8da3bd68961335da0072a99258a1a',
|
||||||
|
'3645cf2f5d0bdac0523f945be1c3ff60758f7845',
|
||||||
|
'4da85b12244839d6368b9290f1619ff9514ab2a8',
|
||||||
|
'4ad942982e43326c7700b1b6443049b3cfd82161',
|
||||||
|
'55304f219244abf82f684f759cc0c7769242f3b4',
|
||||||
|
'8f42e5b592bb7f7a03f4a94a86a41b1236bb099f',
|
||||||
|
'e2a014d885a48f5be2dc6409610996337312facb',
|
||||||
|
'c18996ca488753f714d36d4654715927c1d7f9c2',
|
||||||
|
'ebc4214424cfa683a7046e1f794fea1e44788d84',
|
||||||
|
'06b6d6d6a893fb589ec2ded948f5122856921ed5',
|
||||||
|
'07e4546674268fc0222b2ca22d31d0549dc217ee',
|
||||||
|
'060940e41973d4f7f16d72a2733138e931c35f41',
|
||||||
|
'f8d6eccd887c9cebd36b1d42aa349279b7f5c3ed',
|
||||||
|
'68098b8426f967b8d04cc566348b5c128823219e',
|
||||||
|
'2bfe6cdb24a21bdc1b76fb7c416edd50e9e85945',
|
||||||
|
'1f9bb08bfa2259629f4aaa9ed40f97e9a41b6fa1',
|
||||||
|
'2f20148495612946675fe1c8ea99171e4d950b81',
|
||||||
|
'bc6938fa1e09e840056c2e831abf9664f397c472',
|
||||||
|
'2a6194792beac5130641e932b5ac6e5a99b5ca4f',
|
||||||
|
'185ba2bd547a5e4a77d29fe6c1484f47db5e058f',
|
||||||
|
'29cc7f6081268eaa5b3f2946e0cd0b952a94812c',
|
||||||
|
'ffdc62ac2f7549398d3aca9d2119e83d80d588d5',
|
||||||
|
'd7a4d2808074b0c55d6b239f69d90e7a4930f943',
|
||||||
|
'd58aa4a0b2f6c2504c3abce8de3f1afb71800acc',
|
||||||
|
'77ae23dc7eb8a75609881d4548a79e4935a89d37',
|
||||||
|
'f79bce8a60fbece671f6265adc39f6469f3b9b8c',
|
||||||
|
'051995fdf0af634e4911704057a551e9392e62b1',
|
||||||
|
'b0e489f986c345aef23c4a48d91cbcf5a6fdb9ac',
|
||||||
|
'825aa21c8c0bda4ded3e69a69238763c8cfcc13b',
|
||||||
|
'49389450b1241f5d8f4c8c4271a3eb56bba33965',
|
||||||
|
'f3b9973e1725ecb50da3e6fa4d47343c98ef0382',
|
||||||
|
'321b33d22c8e24ef207e3f357a4573f6a56611f3',
|
||||||
|
'20d694ada07e740c6fa43a8c324cb7d6e362b5ee',
|
||||||
|
'cf7792c2a37d0d76aaaff84aff0b99a8c791429d',
|
||||||
|
'8316ac90764fedf3147799b7b81a6575a9cc398e',
|
||||||
|
'8972a1bd06de5186e5e89292b05aac8aaa817791',
|
||||||
|
'5da63df97c8255ae94a88940695b8471657dd5a1',
|
||||||
|
'f3da2196b5151570d980b34d311ee0973225a68e',
|
||||||
|
];
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
followedTags: Array<Tag>,
|
followedTags: Array<Tag>,
|
||||||
|
@ -31,6 +89,20 @@ function HomePage(props: Props) {
|
||||||
const showIndividualTags = showPersonalizedTags && followedTags.length < 5;
|
const showIndividualTags = showPersonalizedTags && followedTags.length < 5;
|
||||||
let rowData: Array<RowDataItem> = [];
|
let rowData: Array<RowDataItem> = [];
|
||||||
|
|
||||||
|
const lbrytvPaidBetaRow = {
|
||||||
|
title: '#lbrytvpaidbeta',
|
||||||
|
link: `/$/${PAGES.DISCOVER}?${CS.TAGS_KEY}=lbrytvpaidbeta&fee_amount=>0&${CS.CLAIM_TYPE}=${CS.CLAIM_STREAM}&${
|
||||||
|
CS.CHANNEL_IDS_KEY
|
||||||
|
}=${PAID_BETA_CHANNEL_IDS_KEY.join(',')}`,
|
||||||
|
options: {
|
||||||
|
feeAmount: '>0',
|
||||||
|
claimType: ['stream'],
|
||||||
|
tags: ['lbrytvpaidbeta'],
|
||||||
|
pageSize: 8,
|
||||||
|
channelIds: PAID_BETA_CHANNEL_IDS_KEY,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// if you are following channels, always show that first
|
// if you are following channels, always show that first
|
||||||
if (showPersonalizedChannels) {
|
if (showPersonalizedChannels) {
|
||||||
let releaseTime = `>${Math.floor(
|
let releaseTime = `>${Math.floor(
|
||||||
|
@ -93,6 +165,10 @@ function HomePage(props: Props) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authenticated) {
|
||||||
|
rowData.push(lbrytvPaidBetaRow);
|
||||||
|
}
|
||||||
|
|
||||||
rowData.push({
|
rowData.push({
|
||||||
title: 'Top Content from Today',
|
title: 'Top Content from Today',
|
||||||
link: `/$/${PAGES.DISCOVER}?${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TOP}&${CS.FRESH_KEY}=${CS.FRESH_DAY}`,
|
link: `/$/${PAGES.DISCOVER}?${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TOP}&${CS.FRESH_KEY}=${CS.FRESH_DAY}`,
|
||||||
|
@ -128,6 +204,10 @@ function HomePage(props: Props) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!authenticated) {
|
||||||
|
rowData.push(lbrytvPaidBetaRow);
|
||||||
|
}
|
||||||
|
|
||||||
rowData.push({
|
rowData.push({
|
||||||
title: 'Trending Classics',
|
title: 'Trending Classics',
|
||||||
link: `/$/${PAGES.DISCOVER}?${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TRENDING}&${CS.FRESH_KEY}=${CS.FRESH_WEEK}`,
|
link: `/$/${PAGES.DISCOVER}?${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TRENDING}&${CS.FRESH_KEY}=${CS.FRESH_WEEK}`,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
selectIsFetchingFileList,
|
selectIsFetchingFileList,
|
||||||
selectMyPurchases,
|
selectMyPurchases,
|
||||||
selectIsFetchingMyPurchases,
|
selectIsFetchingMyPurchases,
|
||||||
|
doPurchaseList,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import LibraryPage from './view';
|
import LibraryPage from './view';
|
||||||
|
|
||||||
|
@ -14,4 +15,6 @@ const select = state => ({
|
||||||
fetchingMyPurchases: selectIsFetchingMyPurchases(state),
|
fetchingMyPurchases: selectIsFetchingMyPurchases(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select)(LibraryPage);
|
export default connect(select, {
|
||||||
|
doPurchaseList,
|
||||||
|
})(LibraryPage);
|
||||||
|
|
|
@ -5,19 +5,28 @@ import Page from 'component/page';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import DownloadList from 'page/fileListDownloaded';
|
import DownloadList from 'page/fileListDownloaded';
|
||||||
import Yrbl from 'component/yrbl';
|
import Yrbl from 'component/yrbl';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
allDownloadedUrlsCount: number,
|
allDownloadedUrlsCount: number,
|
||||||
myPurchases: Array<string>,
|
myPurchases: Array<string>,
|
||||||
fetchingMyPurchases: boolean,
|
fetchingMyPurchases: boolean,
|
||||||
fetchingFileList: boolean,
|
fetchingFileList: boolean,
|
||||||
|
doPurchaseList: number => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function LibraryPage(props: Props) {
|
function LibraryPage(props: Props) {
|
||||||
const { allDownloadedUrlsCount, myPurchases, fetchingMyPurchases, fetchingFileList } = props;
|
const { allDownloadedUrlsCount, myPurchases, fetchingMyPurchases, fetchingFileList, doPurchaseList } = props;
|
||||||
const hasDownloads = allDownloadedUrlsCount > 0 || (myPurchases && myPurchases.length);
|
const { location } = useHistory();
|
||||||
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
const page = Number(urlParams.get('page')) || 1;
|
||||||
|
const hasDownloads = allDownloadedUrlsCount > 0 || (myPurchases && myPurchases.length > 0);
|
||||||
const loading = fetchingFileList || fetchingMyPurchases;
|
const loading = fetchingFileList || fetchingMyPurchases;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
doPurchaseList(page);
|
||||||
|
}, [doPurchaseList, page]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
{loading && !hasDownloads && (
|
{loading && !hasDownloads && (
|
||||||
|
@ -29,10 +38,12 @@ function LibraryPage(props: Props) {
|
||||||
{!loading && !hasDownloads && (
|
{!loading && !hasDownloads && (
|
||||||
<div className="main--empty">
|
<div className="main--empty">
|
||||||
<Yrbl
|
<Yrbl
|
||||||
title={__("You haven't downloaded anything from LBRY yet")}
|
title={
|
||||||
|
IS_WEB ? __("You haven't purchased anything yet") : __("You haven't downloaded anything from LBRY yet")
|
||||||
|
}
|
||||||
subtitle={
|
subtitle={
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
<Button button="primary" navigate="/" label={__('Explore new content')} />
|
<Button button="primary" navigate="/" label={__('Explore New Content')} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -49,7 +49,4 @@ const perform = dispatch => ({
|
||||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(ShowPage);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(ShowPage);
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ function DiscoverPage() {
|
||||||
defaultTags={CS.TAGS_FOLLOWED}
|
defaultTags={CS.TAGS_FOLLOWED}
|
||||||
meta={
|
meta={
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="secondary"
|
||||||
icon={ICONS.EDIT}
|
icon={ICONS.EDIT}
|
||||||
label={__('Manage')}
|
label={__('Manage')}
|
||||||
requiresAuth={IS_WEB}
|
requiresAuth={IS_WEB}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
commentReducer,
|
commentReducer,
|
||||||
blockedReducer,
|
blockedReducer,
|
||||||
publishReducer,
|
publishReducer,
|
||||||
fileReducer,
|
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import {
|
import {
|
||||||
userReducer,
|
userReducer,
|
||||||
|
@ -39,7 +38,6 @@ export default history =>
|
||||||
content: contentReducer,
|
content: contentReducer,
|
||||||
costInfo: costInfoReducer,
|
costInfo: costInfoReducer,
|
||||||
fileInfo: fileInfoReducer,
|
fileInfo: fileInfoReducer,
|
||||||
file: fileReducer,
|
|
||||||
homepage: homepageReducer,
|
homepage: homepageReducer,
|
||||||
notifications: notificationsReducer,
|
notifications: notificationsReducer,
|
||||||
publish: publishReducer,
|
publish: publishReducer,
|
||||||
|
|
|
@ -43,7 +43,7 @@ import {
|
||||||
selectAllowAnalytics,
|
selectAllowAnalytics,
|
||||||
} from 'redux/selectors/app';
|
} from 'redux/selectors/app';
|
||||||
// import { selectDaemonSettings } from 'redux/selectors/settings';
|
// import { selectDaemonSettings } from 'redux/selectors/settings';
|
||||||
import { doAuthenticate, doGetSync } from 'lbryinc';
|
import { doAuthenticate, doGetSync, doClaimRewardType, rewards as REWARDS } from 'lbryinc';
|
||||||
import { lbrySettings as config, version as appVersion } from 'package.json';
|
import { lbrySettings as config, version as appVersion } from 'package.json';
|
||||||
import analytics, { SHARE_INTERNAL } from 'analytics';
|
import analytics, { SHARE_INTERNAL } from 'analytics';
|
||||||
import { doSignOutCleanup, deleteSavedPassword, getSavedPassword } from 'util/saved-passwords';
|
import { doSignOutCleanup, deleteSavedPassword, getSavedPassword } from 'util/saved-passwords';
|
||||||
|
@ -464,6 +464,33 @@ export function doAnalyticsTagSync() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doAnaltyicsPurchaseEvent(fileInfo) {
|
||||||
|
return dispatch => {
|
||||||
|
let purchasePrice = fileInfo.purchase_receipt && fileInfo.purchase_receipt.amount;
|
||||||
|
if (purchasePrice) {
|
||||||
|
const purchaseInt = Number(Number(purchasePrice).toFixed(0));
|
||||||
|
analytics.purchaseEvent(purchaseInt);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const contentFeeTxid = fileInfo.content_fee && fileInfo.content_fee.txid;
|
||||||
|
const purchaseReceiptTxid = fileInfo.purchase_receipt && fileInfo.purchase_receipt.txid;
|
||||||
|
// These aren't guaranteed to exist
|
||||||
|
const txid = contentFeeTxid || purchaseReceiptTxid;
|
||||||
|
|
||||||
|
if (txid) {
|
||||||
|
dispatch(
|
||||||
|
doClaimRewardType(REWARDS.TYPE_PAID_CONTENT, {
|
||||||
|
failSilently: true,
|
||||||
|
params: { transaction_id: txid },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Give it some time to get into the mempool
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function doSignIn() {
|
export function doSignIn() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
|
|
|
@ -20,6 +20,8 @@ import {
|
||||||
makeSelectUriIsStreamable,
|
makeSelectUriIsStreamable,
|
||||||
selectDownloadingByOutpoint,
|
selectDownloadingByOutpoint,
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
|
makeSelectClaimIsMine,
|
||||||
|
makeSelectClaimWasPurchased,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
|
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
|
||||||
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
||||||
|
@ -157,7 +159,7 @@ export function doCloseFloatingPlayer() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolean, cb: ?() => void) {
|
export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolean, cb: ?(GetResponse) => void) {
|
||||||
return (dispatch: Dispatch, getState: () => any) => {
|
return (dispatch: Dispatch, getState: () => any) => {
|
||||||
function onSuccess(fileInfo) {
|
function onSuccess(fileInfo) {
|
||||||
if (saveFile) {
|
if (saveFile) {
|
||||||
|
@ -165,7 +167,7 @@ export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolea
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cb) {
|
if (cb) {
|
||||||
cb();
|
cb(fileInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,19 +183,22 @@ export function doPlayUri(
|
||||||
) {
|
) {
|
||||||
return (dispatch: Dispatch, getState: () => any) => {
|
return (dispatch: Dispatch, getState: () => any) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
const isMine = makeSelectClaimIsMine(uri)(state);
|
||||||
const fileInfo = makeSelectFileInfoForUri(uri)(state);
|
const fileInfo = makeSelectFileInfoForUri(uri)(state);
|
||||||
const uriIsStreamable = makeSelectUriIsStreamable(uri)(state);
|
const uriIsStreamable = makeSelectUriIsStreamable(uri)(state);
|
||||||
const downloadingByOutpoint = selectDownloadingByOutpoint(state);
|
const downloadingByOutpoint = selectDownloadingByOutpoint(state);
|
||||||
|
const claimWasPurchased = makeSelectClaimWasPurchased(uri)(state);
|
||||||
const alreadyDownloaded = fileInfo && (fileInfo.completed || (fileInfo.blobs_remaining === 0 && uriIsStreamable));
|
const alreadyDownloaded = fileInfo && (fileInfo.completed || (fileInfo.blobs_remaining === 0 && uriIsStreamable));
|
||||||
const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
|
const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
|
||||||
if (alreadyDownloading || alreadyDownloaded) {
|
|
||||||
|
if (!IS_WEB && (alreadyDownloading || alreadyDownloaded)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const daemonSettings = selectDaemonSettings(state);
|
const daemonSettings = selectDaemonSettings(state);
|
||||||
const costInfo = makeSelectCostInfoForUri(uri)(state);
|
const costInfo = makeSelectCostInfoForUri(uri)(state);
|
||||||
const cost = (costInfo && Number(costInfo.cost)) || 0;
|
const cost = (costInfo && Number(costInfo.cost)) || 0;
|
||||||
const saveFile = !uriIsStreamable ? true : daemonSettings.save_files || saveFileOverride || cost > 0;
|
const saveFile = !IS_WEB && (!uriIsStreamable ? true : daemonSettings.save_files || saveFileOverride || cost > 0);
|
||||||
const instantPurchaseEnabled = makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state);
|
const instantPurchaseEnabled = makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state);
|
||||||
const instantPurchaseMax = makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state);
|
const instantPurchaseMax = makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state);
|
||||||
|
|
||||||
|
@ -203,7 +208,12 @@ export function doPlayUri(
|
||||||
|
|
||||||
function attemptPlay(instantPurchaseMax = null) {
|
function attemptPlay(instantPurchaseMax = null) {
|
||||||
// If you have a file_list entry, you have already purchased the file
|
// If you have a file_list entry, you have already purchased the file
|
||||||
if (!fileInfo && (!instantPurchaseMax || !instantPurchaseEnabled || cost > instantPurchaseMax)) {
|
if (
|
||||||
|
!isMine &&
|
||||||
|
!fileInfo &&
|
||||||
|
!claimWasPurchased &&
|
||||||
|
(!instantPurchaseMax || !instantPurchaseEnabled || cost > instantPurchaseMax)
|
||||||
|
) {
|
||||||
dispatch(doOpenModal(MODALS.AFFIRM_PURCHASE, { uri }));
|
dispatch(doOpenModal(MODALS.AFFIRM_PURCHASE, { uri }));
|
||||||
} else {
|
} else {
|
||||||
beginGetFile();
|
beginGetFile();
|
||||||
|
|
|
@ -285,6 +285,14 @@ reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (state, action) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
modal: null,
|
||||||
|
modalProps: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default function reducer(state: AppState = defaultState, action: any) {
|
export default function reducer(state: AppState = defaultState, action: any) {
|
||||||
const handler = reducers[action.type];
|
const handler = reducers[action.type];
|
||||||
if (handler) return handler(state, action);
|
if (handler) return handler(state, action);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import { ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
|
||||||
|
|
||||||
const reducers = {};
|
const reducers = {};
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
|
@ -89,6 +90,13 @@ reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => {
|
||||||
|
|
||||||
reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = state => ({ ...state, history: [] });
|
reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = state => ({ ...state, history: [] });
|
||||||
|
|
||||||
|
reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
playingUri: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
makeSelectClaimIsNsfw,
|
makeSelectClaimIsNsfw,
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
makeSelectRecommendedContentForUri,
|
makeSelectRecommendedContentForUri,
|
||||||
makeSelectStreamingUrlForUri,
|
|
||||||
makeSelectMediaTypeForUri,
|
makeSelectMediaTypeForUri,
|
||||||
selectBalance,
|
selectBalance,
|
||||||
selectBlockedChannels,
|
selectBlockedChannels,
|
||||||
|
@ -21,9 +20,6 @@ import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { FORCE_CONTENT_TYPE_PLAYER, FORCE_CONTENT_TYPE_COMIC } from 'constants/claim';
|
import { FORCE_CONTENT_TYPE_PLAYER, FORCE_CONTENT_TYPE_COMIC } from 'constants/claim';
|
||||||
// @if TARGET='web'
|
|
||||||
import { generateStreamUrl } from 'util/lbrytv';
|
|
||||||
// @endif
|
|
||||||
|
|
||||||
const RECENT_HISTORY_AMOUNT = 10;
|
const RECENT_HISTORY_AMOUNT = 10;
|
||||||
const HISTORY_ITEMS_PER_PAGE = 50;
|
const HISTORY_ITEMS_PER_PAGE = 50;
|
||||||
|
@ -167,16 +163,6 @@ export const makeSelectFileExtensionForUri = (uri: string) =>
|
||||||
return fileName && path.extname(fileName).substring(1);
|
return fileName && path.extname(fileName).substring(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
let makeSelectStreamingUrlForUriWebProxy;
|
|
||||||
// @if TARGET='web'
|
|
||||||
makeSelectStreamingUrlForUriWebProxy = (uri: string) =>
|
|
||||||
createSelector(makeSelectClaimForUri(uri), claim => (claim ? generateStreamUrl(claim.name, claim.claim_id) : null));
|
|
||||||
// @endif
|
|
||||||
// @if TARGET='app'
|
|
||||||
makeSelectStreamingUrlForUriWebProxy = (uri: string) => createSelector(makeSelectStreamingUrlForUri(uri), url => url);
|
|
||||||
// @endif
|
|
||||||
export { makeSelectStreamingUrlForUriWebProxy };
|
|
||||||
|
|
||||||
export const makeSelectFileRenderModeForUri = (uri: string) =>
|
export const makeSelectFileRenderModeForUri = (uri: string) =>
|
||||||
createSelector(
|
createSelector(
|
||||||
makeSelectContentTypeForUri(uri),
|
makeSelectContentTypeForUri(uri),
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
@import 'component/nag';
|
@import 'component/nag';
|
||||||
@import 'component/navigation';
|
@import 'component/navigation';
|
||||||
@import 'component/pagination';
|
@import 'component/pagination';
|
||||||
|
@import 'component/purchase';
|
||||||
@import 'component/placeholder';
|
@import 'component/placeholder';
|
||||||
@import 'component/search';
|
@import 'component/search';
|
||||||
@import 'component/claim-search';
|
@import 'component/claim-search';
|
||||||
|
|
|
@ -75,7 +75,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__section--flex {
|
.card__title-section {
|
||||||
@extend .section__flex;
|
@extend .section__flex;
|
||||||
padding: var(--spacing-medium) var(--spacing-large);
|
padding: var(--spacing-medium) var(--spacing-large);
|
||||||
|
|
||||||
|
@ -84,6 +84,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card__title-section--body-list {
|
||||||
|
padding: var(--spacing-medium);
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.card__actions--inline {
|
.card__actions--inline {
|
||||||
@extend .card__actions;
|
@extend .card__actions;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
@ -165,6 +173,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__title-actions {
|
.card__title-actions {
|
||||||
|
align-self: flex-start;
|
||||||
padding: var(--spacing-medium);
|
padding: var(--spacing-medium);
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
|
@ -202,6 +211,12 @@
|
||||||
.card__header--between {
|
.card__header--between {
|
||||||
@extend .card__header;
|
@extend .card__header;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__header--nowrap {
|
||||||
|
@extend .card__header--between;
|
||||||
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__subtitle {
|
.card__subtitle {
|
||||||
|
|
|
@ -4,17 +4,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim-list--card-body {
|
|
||||||
.claim-preview {
|
|
||||||
padding: 0 var(--spacing-medium);
|
|
||||||
padding-right: 0;
|
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.claim-list__header {
|
.claim-list__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -433,13 +422,17 @@
|
||||||
bottom: var(--spacing-miniscule);
|
bottom: var(--spacing-miniscule);
|
||||||
right: var(--spacing-miniscule);
|
right: var(--spacing-miniscule);
|
||||||
background-color: var(--color-black);
|
background-color: var(--color-black);
|
||||||
padding: 0.2rem;
|
padding: 0.3rem;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
.file-properties {
|
.file-properties {
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-price {
|
||||||
|
padding: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,14 +33,28 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim-search__input-container {
|
.claim-search__input-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: var(--font-body);
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(:first-of-type) {
|
&:not(:first-of-type) {
|
||||||
padding-left: var(--spacing-medium);
|
padding-left: var(--spacing-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
|
padding-left: 0px;
|
||||||
|
|
||||||
&:not(:first-of-type) {
|
&:not(:first-of-type) {
|
||||||
margin-top: var(--spacing-small);
|
margin-top: var(--spacing-small);
|
||||||
}
|
}
|
||||||
padding-left: 0px;
|
|
||||||
&:not(:first-of-type) {
|
&:not(:first-of-type) {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-properties--large {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: var(--spacing-large);
|
||||||
|
margin-left: 0;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin-top: var(--spacing-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.file-properties--small {
|
.file-properties--small {
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
@ -34,64 +44,3 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-properties__purchased {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: var(--spacing-xsmall);
|
|
||||||
color: var(--color-gray-5);
|
|
||||||
|
|
||||||
span,
|
|
||||||
svg {
|
|
||||||
position: relative;
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
content: '';
|
|
||||||
left: -0.4rem;
|
|
||||||
right: -5rem;
|
|
||||||
height: 1.75rem;
|
|
||||||
transform: skew(20deg);
|
|
||||||
background-color: var(--color-purchased);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-properties__not-purchased {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--color-purchased-text);
|
|
||||||
|
|
||||||
span {
|
|
||||||
position: relative;
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
content: '';
|
|
||||||
left: 0;
|
|
||||||
right: -5rem;
|
|
||||||
height: 1.75rem;
|
|
||||||
background-color: var(--color-purchased-alt);
|
|
||||||
border: 2px solid var(--color-purchased);
|
|
||||||
transform: skew(20deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-properties--large {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: var(--spacing-large);
|
|
||||||
margin-left: 0;
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
margin-top: var(--spacing-small);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -51,6 +51,10 @@
|
||||||
> .card {
|
> .card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
padding: 0 var(--spacing-medium);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main--launching {
|
.main--launching {
|
||||||
|
|
|
@ -67,7 +67,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: var(--spacing-small);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.media__info-text {
|
.media__info-text {
|
||||||
|
|
|
@ -65,9 +65,11 @@
|
||||||
|
|
||||||
.modal--card-internal {
|
.modal--card-internal {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,3 +64,24 @@
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navigation-link--pulse {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: shadow-pulse 2.5s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shadow-pulse {
|
||||||
|
0% {
|
||||||
|
background-color: rgba(37, 119, 97, 0.2);
|
||||||
|
box-shadow: 0 0 0 0px rgba(37, 119, 97, 0.2);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: rgba(37, 119, 97, 0);
|
||||||
|
box-shadow: 0 0 0 35px rgba(37, 119, 97, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
265
ui/scss/component/_purchase.scss
Normal file
265
ui/scss/component/_purchase.scss
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
.file-price {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-purchased-text);
|
||||||
|
|
||||||
|
.credit-amount,
|
||||||
|
.icon {
|
||||||
|
position: relative;
|
||||||
|
margin-left: var(--spacing-medium);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
left: 0;
|
||||||
|
width: 250%;
|
||||||
|
height: 160%;
|
||||||
|
transform: skew(15deg);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--color-purchased-alt);
|
||||||
|
border: 2px solid var(--color-purchased);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-price__key {
|
||||||
|
@extend .file-price;
|
||||||
|
color: var(--color-gray-5);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: var(--color-purchased);
|
||||||
|
height: 180%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-price--filepage {
|
||||||
|
font-size: var(--font-body);
|
||||||
|
top: calc(var(--spacing-miniscule) * -1);
|
||||||
|
margin-left: var(--spacing-medium);
|
||||||
|
|
||||||
|
.credit-amount {
|
||||||
|
margin: 0 var(--spacing-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
height: 250%;
|
||||||
|
left: calc(var(--spacing-medium) * -1);
|
||||||
|
border-radius: 0;
|
||||||
|
border-bottom-left-radius: var(--border-radius);
|
||||||
|
border-width: 5px;
|
||||||
|
border-top-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
padding: var(--spacing-small);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
height: 140%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-price__key--filepage {
|
||||||
|
@extend .file-price--filepage;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
height: 300%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin: 0 var(--spacing-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
&::before {
|
||||||
|
top: calc(-1 * var(--spacing-small));
|
||||||
|
height: 110%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
top: calc(-1 * var(--spacing-small));
|
||||||
|
margin: 0 var(--spacing-xsmall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-price--modal {
|
||||||
|
border: 5px solid var(--color-purchased);
|
||||||
|
|
||||||
|
.credit-amount {
|
||||||
|
margin: 0 var(--spacing-medium);
|
||||||
|
margin-left: var(--spacing-large);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-price--modal {
|
||||||
|
font-size: var(--font-body);
|
||||||
|
height: 4rem;
|
||||||
|
background-color: var(--color-purchased-alt);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transform: skew(15deg);
|
||||||
|
|
||||||
|
.icon,
|
||||||
|
.credit-amount {
|
||||||
|
transform: skew(-15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit-amount {
|
||||||
|
font-size: var(--font-large);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-price__key--modal {
|
||||||
|
@extend .file-price--modal;
|
||||||
|
top: var(--spacing-medium);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 100%;
|
||||||
|
width: auto;
|
||||||
|
left: calc(var(--spacing-xlarge) * 1.5);
|
||||||
|
animation: moveKey 2.5s 1 ease-out;
|
||||||
|
overflow: visible;
|
||||||
|
stroke: var(--color-black);
|
||||||
|
|
||||||
|
g {
|
||||||
|
animation: turnKey 2.5s 1 ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
transform: skew(15deg);
|
||||||
|
animation: expand 2.5s 1 ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase-stuff {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> *:first-child {
|
||||||
|
width: 60%;
|
||||||
|
padding-right: var(--spacing-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-price,
|
||||||
|
.file-price__key {
|
||||||
|
width: 900%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-price__key {
|
||||||
|
background-color: transparent;
|
||||||
|
border-width: 0;
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
margin-top: calc(var(--spacing-xlarge) * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase-stuff__text--purchased {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: var(--font-title);
|
||||||
|
color: var(--color-black);
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--spacing-xlarge) * 1.6);
|
||||||
|
left: var(--spacing-large);
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 9999999999;
|
||||||
|
animation: display 1s 1 ease-in;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase_stuff__subtext--purchased {
|
||||||
|
color: var(--color-black);
|
||||||
|
font-size: var(--font-body);
|
||||||
|
margin-top: var(--spacing-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes display {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes expand {
|
||||||
|
0% {
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
height: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
height: 30rem;
|
||||||
|
left: -200%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes moveKey {
|
||||||
|
0% {
|
||||||
|
max-height: 3rem;
|
||||||
|
left: -20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
left: calc(var(--spacing-xlarge) * 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
left: calc(var(--spacing-xlarge) * 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
left: calc(var(--spacing-xlarge) * 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
max-height: 10rem;
|
||||||
|
opacity: 1;
|
||||||
|
left: 30rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes turnKey {
|
||||||
|
0% {
|
||||||
|
transform: rotate3d(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: rotate3d(0, 0, 1, 45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
transform: rotate3d(1, 0, 1, 45deg);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: rotate3d(0, 0, 1, 45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate3d(0, 0, 1, 45deg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,10 @@
|
||||||
|
|
||||||
~ .section {
|
~ .section {
|
||||||
margin-top: var(--spacing-large);
|
margin-top: var(--spacing-large);
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
margin-top: var(--spacing-medium);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,8 +44,8 @@
|
||||||
--color-notice: #58563b;
|
--color-notice: #58563b;
|
||||||
--color-error: #61373f;
|
--color-error: #61373f;
|
||||||
--color-purchased: #ffd580;
|
--color-purchased: #ffd580;
|
||||||
--color-purchased-alt: #ffd5804a;
|
--color-purchased-alt: var(--color-purchased);
|
||||||
--color-purchased-text: #eeeeee;
|
--color-purchased-text: var(--color-gray-5);
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
--color-text: #eeeeee;
|
--color-text: #eeeeee;
|
||||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -826,10 +826,10 @@
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.18.0"
|
scheduler "^0.18.0"
|
||||||
|
|
||||||
"@lbry/components@^4.1.5":
|
"@lbry/components@^4.2.2":
|
||||||
version "4.1.5"
|
version "4.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.1.5.tgz#eb6a74c6e17ee9ed46cbb9c590aca10847d18f38"
|
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.2.2.tgz#023b8224e180b69cd8b5d77242441742bca26d0f"
|
||||||
integrity sha512-T/cR5tZvikI/dAE2euJP07+/yi+yO1wkE/4djT8EhCJITF+SZ0CjHG8auNYQ/zQw7H5KQnh/WMYaRVxO54FKLA==
|
integrity sha512-CziwuALDiv/DXT5zwkVK8cfF914WyOqKg8GkqIk/f9vc7VXZx9OlRfBadnpGDrezrjjHn7onwOreQrwzB5PcNA==
|
||||||
|
|
||||||
"@mapbox/hast-util-table-cell-style@^0.1.3":
|
"@mapbox/hast-util-table-cell-style@^0.1.3":
|
||||||
version "0.1.3"
|
version "0.1.3"
|
||||||
|
@ -6178,17 +6178,17 @@ lazy-val@^1.0.4:
|
||||||
yargs "^13.2.2"
|
yargs "^13.2.2"
|
||||||
zstd-codec "^0.1.1"
|
zstd-codec "^0.1.1"
|
||||||
|
|
||||||
lbry-redux@lbryio/lbry-redux#cd9c15567f2934ddc82de364d88b378ff04d5571:
|
lbry-redux@lbryio/lbry-redux#aa2cfa789670e899824d3d3ac1ae677172a7ad4e:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/cd9c15567f2934ddc82de364d88b378ff04d5571"
|
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/aa2cfa789670e899824d3d3ac1ae677172a7ad4e"
|
||||||
dependencies:
|
dependencies:
|
||||||
proxy-polyfill "0.1.6"
|
proxy-polyfill "0.1.6"
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
uuid "^3.3.2"
|
uuid "^3.3.2"
|
||||||
|
|
||||||
lbryinc@lbryio/lbryinc#cc62a4eec10845cc0b31da7d0f27287cfa7c4866:
|
lbryinc@lbryio/lbryinc#6a52f8026cdc7cd56d200fb5c46f852e0139bbeb:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/cc62a4eec10845cc0b31da7d0f27287cfa7c4866"
|
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/6a52f8026cdc7cd56d200fb5c46f852e0139bbeb"
|
||||||
dependencies:
|
dependencies:
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue