Paid content on lbry.tv #4197

Merged
neb-b merged 28 commits from paid-content into master 2020-05-21 17:38:29 +02:00
64 changed files with 968 additions and 354 deletions

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

@ -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)) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -51,6 +51,10 @@
> .card { > .card {
width: 100%; width: 100%;
} }
@media (max-width: $breakpoint-small) {
padding: 0 var(--spacing-medium);
}
} }
.main--launching { .main--launching {

View file

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

View file

@ -65,9 +65,11 @@
.modal--card-internal { .modal--card-internal {
padding: 0; padding: 0;
border: none;
.card { .card {
margin: 0; margin: 0;
border: none;
} }
} }

View file

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

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

View file

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

View file

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

View file

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