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-send": "^5.0.0",
"koa-static": "^5.0.0",
"lbry-redux": "lbryio/lbry-redux#87ae7faf1c1d5ffa86feb578899596f6ea2a5fd9",
"lbryinc": "lbryio/lbryinc#0addc624db54000b0447f4539f91f5758d26eef3",
"lbry-redux": "lbryio/lbry-redux#f6e5b69e5aa337d50503a2f5ebb5efe4eda4ac57",
"lbryinc": "lbryio/lbryinc#6a52f8026cdc7cd56d200fb5c46f852e0139bbeb",
"mysql": "^2.17.1",
"node-fetch": "^2.6.0"
},

View file

@ -3333,17 +3333,17 @@ latest-version@^3.0.0:
dependencies:
package-json "^4.0.0"
lbry-redux@lbryio/lbry-redux#87ae7faf1c1d5ffa86feb578899596f6ea2a5fd9:
lbry-redux@lbryio/lbry-redux#f6e5b69e5aa337d50503a2f5ebb5efe4eda4ac57:
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:
proxy-polyfill "0.1.6"
reselect "^3.0.0"
uuid "^3.3.2"
lbryinc@lbryio/lbryinc#0addc624db54000b0447f4539f91f5758d26eef3:
lbryinc@lbryio/lbryinc#6a52f8026cdc7cd56d200fb5c46f852e0139bbeb:
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:
reselect "^3.0.0"

View file

@ -70,7 +70,7 @@
"@babel/register": "^7.0.0",
"@exponent/electron-cookies": "^2.0.0",
"@hot-loader/react-dom": "^16.8",
"@lbry/components": "^4.1.5",
"@lbry/components": "^4.2.2",
"@reach/menu-button": "0.7.4",
"@reach/rect": "^0.2.1",
"@reach/tabs": "^0.1.5",
@ -132,8 +132,8 @@
"imagesloaded": "^4.1.4",
"json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#cd9c15567f2934ddc82de364d88b378ff04d5571",
"lbryinc": "lbryio/lbryinc#cc62a4eec10845cc0b31da7d0f27287cfa7c4866",
"lbry-redux": "lbryio/lbry-redux#aa2cfa789670e899824d3d3ac1ae677172a7ad4e",
"lbryinc": "lbryio/lbryinc#6a52f8026cdc7cd56d200fb5c46f852e0139bbeb",
"lint-staged": "^7.0.2",
"localforage": "^1.7.1",
"lodash-es": "^4.17.14",
@ -211,7 +211,7 @@
"yarn": "^1.3"
},
"lbrySettings": {
"lbrynetDaemonVersion": "0.72.0",
"lbrynetDaemonVersion": "0.74.0",
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
"lbrynetDaemonDir": "static/daemon",
"lbrynetDaemonFileName": "lbrynet"

View file

@ -30,7 +30,7 @@ if (isProduction) {
type Analytics = {
error: string => Promise<any>,
sentryError: ({}, {}) => Promise<any>,
sentryError: ({} | string, {}) => Promise<any>,
pageView: string => void,
setUser: Object => void,
toggleInternal: (boolean, ?boolean) => void,
@ -45,6 +45,7 @@ type Analytics = {
emailVerifiedEvent: () => void,
rewardEligibleEvent: () => void,
startupEvent: () => void,
purchaseEvent: number => void,
readyEvent: number => void,
openUrlEvent: string => void,
};
@ -217,6 +218,9 @@ const analytics: Analytics = {
sendGaEvent('Startup', 'App-Ready');
sendGaTimingEvent('Startup', 'App-Ready', timeToReady);
},
purchaseEvent: (purchaseInt: number) => {
sendGaEvent('Purchase', 'Purchase-Complete', undefined, purchaseInt);
},
};
function sendGaEvent(category, action, label, value) {

View file

@ -1,9 +1,11 @@
import { connect } from 'react-redux';
import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content';
import { makeSelectClaimWasPurchased } from 'lbry-redux';
import ClaimInsufficientCredits from './view';
const select = (state, props) => ({
isInsufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
});
export default connect(select)(ClaimInsufficientCredits);

View file

@ -7,12 +7,13 @@ type Props = {
uri: string,
fileInfo: FileListItem,
isInsufficientCredits: boolean,
claimWasPurchased: boolea,
};
function ClaimInsufficientCredits(props: Props) {
const { isInsufficientCredits, fileInfo } = props;
const { isInsufficientCredits, fileInfo, claimWasPurchased } = props;
if (fileInfo || !isInsufficientCredits) {
if (fileInfo || !isInsufficientCredits || claimWasPurchased) {
return null;
}

View file

@ -55,6 +55,7 @@ type Props = {
followedTags?: Array<Tag>,
injectedItem: ?Node,
infiniteScroll?: Boolean,
feeAmount?: string,
};
function ClaimListDiscover(props: Props) {
@ -92,6 +93,7 @@ function ClaimListDiscover(props: Props) {
infiniteScroll = true,
followedTags,
injectedItem,
feeAmount,
} = props;
const didNavigateForward = history.action === 'PUSH';
const { search } = location;
@ -113,14 +115,17 @@ function ClaimListDiscover(props: Props) {
const streamTypeParam =
streamType || (CS.FILE_TYPES.includes(contentTypeParam) && contentTypeParam) || defaultStreamType || 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 isFiltered = () =>
Boolean(
urlParams.get(CS.FRESH_KEY) ||
urlParams.get(CS.CONTENT_KEY) ||
urlParams.get(CS.DURATION_KEY) ||
urlParams.get(CS.TAGS_KEY)
urlParams.get(CS.TAGS_KEY) ||
urlParams.get(CS.FEE_AMOUNT_KEY)
);
useEffect(() => {
@ -143,6 +148,7 @@ function ClaimListDiscover(props: Props) {
duration?: string,
reposted_claim_id?: string,
stream_types?: any,
fee_amount?: string,
} = {
page_size: pageSize || CS.PAGE_SIZE,
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
// it's faster, but we will need to remove it if we start using total_pages
no_totals: true,
channel_ids: channelIds || [],
channel_ids: channelIdsParam || [],
not_channel_ids:
// If channelIds were passed in, we don't need not_channel_ids
!channelIds && hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [],
// If channelIdsParam were passed in, we don't need not_channel_ids
!channelIdsParam && hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [],
not_tags: !showNsfw ? MATURE_TAGS : [],
order_by:
orderParam === CS.ORDER_BY_TRENDING
@ -212,6 +218,10 @@ function ClaimListDiscover(props: Props) {
}
}
if (feeAmountParam) {
options.fee_amount = feeAmountParam;
}
if (durationParam) {
if (durationParam === CS.DURATION_SHORT) {
options.duration = '<=1800';
@ -310,6 +320,15 @@ function ClaimListDiscover(props: Props) {
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) {
if (t === CS.TAGS_ALL || t === CS.TAGS_FOLLOWED) {
return t;
@ -319,7 +338,7 @@ function ClaimListDiscover(props: Props) {
}
function buildUrl(delta) {
const newUrlParams = new URLSearchParams();
const newUrlParams = new URLSearchParams(location.search);
CS.KEYS.forEach(k => {
// $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));
@ -370,6 +389,13 @@ function ClaimListDiscover(props: Props) {
newUrlParams.set(CS.TAGS_KEY, delta.value);
}
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()}`;
}
@ -485,7 +511,7 @@ function ClaimListDiscover(props: Props) {
}
>
{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 (
<option key={type} value={type}>
{/* i18fixme */}
@ -505,6 +531,7 @@ function ClaimListDiscover(props: Props) {
</FormField>
</div>
)}
{/* DURATIONS FIELD */}
{showDuration && (
<div className={'claim-search__input-container'}>
@ -541,6 +568,7 @@ function ClaimListDiscover(props: Props) {
</FormField>
</div>
)}
{/* TAGS FIELD */}
{!tags && (
<div className={'claim-search__input-container'}>
@ -585,6 +613,38 @@ function ClaimListDiscover(props: Props) {
</FormField>
</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>
</>
)}

View file

@ -13,10 +13,11 @@ import {
doFileGet,
makeSelectReflectingClaimForUri,
makeSelectClaimWasPurchased,
makeSelectStreamingUrlForUri,
} from 'lbry-redux';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
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 ClaimPreview from './view';
@ -36,7 +37,7 @@ const select = (state, props) => ({
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
channelIsBlocked: props.uri && selectChannelIsBlocked(props.uri)(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),
});

View file

@ -2,6 +2,7 @@
import React from 'react';
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
import ClaimPreviewTile from 'component/claimPreviewTile';
import { useHistory } from 'react-router';
type Props = {
prefixUris?: Array<string>,
@ -23,6 +24,7 @@ type Props = {
releaseTime?: string,
claimType?: Array<string>,
timestamp?: string,
feeAmount?: string,
};
function ClaimTilesDiscover(props: Props) {
@ -42,7 +44,12 @@ function ClaimTilesDiscover(props: Props) {
claimType,
prefixUris,
timestamp,
feeAmount,
} = 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 options: {
page_size: number,
@ -56,6 +63,7 @@ function ClaimTilesDiscover(props: Props) {
release_time?: string,
claim_type?: Array<string>,
timestamp?: string,
fee_amount?: string,
} = {
page_size: pageSize,
claim_type: claimType || undefined,
@ -76,6 +84,10 @@ function ClaimTilesDiscover(props: Props) {
options.release_time = releaseTime;
}
if (feeAmountParam) {
options.fee_amount = feeAmountParam;
}
// https://github.com/lbryio/lbry-desktop/issues/3774
if (hideReposts) {
if (Array.isArray(options.claim_type)) {

View file

@ -15,6 +15,7 @@ type Props = {
icon?: string,
className?: string,
isPageTitle?: boolean,
noTitleWrap?: boolean,
isBodyList?: boolean,
defaultExpand?: boolean,
nag?: Node,
@ -31,6 +32,7 @@ export default function Card(props: Props) {
className,
isPageTitle = false,
isBodyList = false,
noTitleWrap = false,
defaultExpand,
nag,
} = props;
@ -40,8 +42,12 @@ export default function Card(props: Props) {
return (
<section className={classnames(className, 'card')}>
{(title || subtitle) && (
<div className="card__header--between">
<div className="card__section--flex">
<div
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} />}
<div>
{isPageTitle && <h1 className="card__title">{title}</h1>}

View file

@ -82,7 +82,7 @@ class CreditAmount extends React.PureComponent<Props> {
'badge--free': badge && isFree,
})}
>
<span>{amountText}</span>
<span className="credit-amount">{amountText}</span>
{isEstimate ? (
<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';
type Props = {
loading: boolean,
totalPages: number,
location: { search: string },
history: { push: string => void },
@ -16,7 +15,7 @@ type Props = {
};
function Paginate(props: Props) {
const { totalPages = 1, loading, location, history, onPageChange } = props;
const { totalPages = 1, location, history, onPageChange } = props;
const { search } = location;
const [textValue, setTextValue] = React.useState('');
const urlParams = new URLSearchParams(search);
@ -45,7 +44,7 @@ function Paginate(props: Props) {
return (
// Hide the paginate controls if we are loading or there is only one page
// 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-section>
<ReactPaginate

View file

@ -5,6 +5,8 @@ import {
makeSelectLoadingForUri,
makeSelectClaimIsMine,
makeSelectClaimForUri,
makeSelectClaimWasPurchased,
makeSelectStreamingUrlForUri,
} from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { doOpenModal, doAnalyticsView } from 'redux/actions/app';
@ -18,16 +20,14 @@ const select = (state, props) => ({
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
});
const perform = dispatch => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
pause: () => dispatch(doSetPlayingUri(null)),
download: uri => dispatch(doPlayUri(uri, false, true, () => dispatch(doAnalyticsView(uri)))),
triggerViewEvent: uri => dispatch(doAnalyticsView(uri)),
});
export default connect(
select,
perform
)(FileDownloadLink);
export default connect(select, perform)(FileDownloadLink);

View file

@ -3,7 +3,6 @@ import * as ICONS from 'constants/icons';
import * as MODALS from 'constants/modal_types';
import React, { useState } from 'react';
import Button from 'component/button';
import { generateDownloadUrl } from 'util/lbrytv';
type Props = {
uri: string,
@ -15,12 +14,12 @@ type Props = {
openModal: (id: string, { path: string }) => void,
pause: () => void,
download: string => void,
triggerViewEvent: string => void,
costInfo: ?{ cost: string },
buttonType: ?string,
showLabel: ?boolean,
hideOpenButton: boolean,
hideDownloadStatus: boolean,
streamingUrl: ?string,
};
function FileDownloadLink(props: Props) {
@ -34,39 +33,43 @@ function FileDownloadLink(props: Props) {
download,
uri,
claim,
triggerViewEvent,
costInfo,
buttonType = 'alt',
showLabel = false,
hideOpenButton = false,
hideDownloadStatus = false,
streamingUrl,
} = 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;
const isPaidContent = cost > 0;
if (!claim || (IS_WEB && isPaidContent)) {
React.useEffect(() => {
if (didClickDownloadButton && streamingUrl) {
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;
}
const { name, claim_id: claimId, value } = claim;
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 TARGET='app'
if (downloading || loading) {
const progress = fileInfo && fileInfo.written_bytes > 0 ? (fileInfo.written_bytes / fileInfo.total_bytes) * 100 : 0;
const label =
@ -74,6 +77,7 @@ function FileDownloadLink(props: Props) {
return hideDownloadStatus ? null : <span className="download-text">{label}</span>;
}
// @endif
if (fileInfo && fileInfo.download_path && fileInfo.completed) {
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 (
<Button
@ -100,10 +104,6 @@ function FileDownloadLink(props: Props) {
icon={ICONS.DOWNLOAD}
label={showLabel ? label : null}
onClick={handleDownload}
// @if TARGET='web'
download={fileName}
href={downloadUrl}
// @endif
/>
);
}

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri } from 'lbry-redux';
import { makeSelectClaimForUri, makeSelectClaimWasPurchased, makeSelectClaimIsMine } from 'lbry-redux';
import { makeSelectCostInfoForUri, doFetchCostInfoForUri, makeSelectFetchingCostInfoForUri } from 'lbryinc';
import FilePrice from './view';
@ -7,14 +7,12 @@ const select = (state, props) => ({
costInfo: makeSelectCostInfoForUri(props.uri)(state),
fetching: makeSelectFetchingCostInfoForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
});
const perform = dispatch => ({
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
// cancelFetchCostInfo: (uri) => dispatch(doCancelFetchCostInfoForUri(uri))
});
export default connect(
select,
perform
)(FilePrice);
export default connect(select, perform)(FilePrice);

View file

@ -1,6 +1,9 @@
// @flow
import * as ICONS from 'constants/icons';
import React from 'react';
import classnames from 'classnames';
import CreditAmount from 'component/common/credit-amount';
import Icon from 'component/common/icon';
type Props = {
showFullPrice: boolean,
@ -9,12 +12,13 @@ type Props = {
uri: string,
fetching: boolean,
claim: ?{},
claimWasPurchased: boolean,
claimIsMine: boolean,
type?: string,
// below props are just passed to <CreditAmount />
badge?: boolean,
inheritStyle?: boolean,
showLBC?: boolean,
hideFree?: boolean, // hide the file price if it's free
className?: string,
};
class FilePrice extends React.PureComponent<Props> {
@ -39,23 +43,35 @@ class FilePrice extends React.PureComponent<Props> {
};
render() {
const { costInfo, showFullPrice, badge, inheritStyle, showLBC, hideFree, className } = this.props;
if (costInfo && (!costInfo.cost || (!costInfo.cost && hideFree))) {
const { costInfo, showFullPrice, showLBC, hideFree, claimWasPurchased, type, claimIsMine } = this.props;
if (claimIsMine || !costInfo || !costInfo.cost || (!costInfo.cost && hideFree)) {
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
className={classnames('file-price', {
'file-price--filepage': type === 'filepage',
'file-price--modal': type === 'modal',
})}
showFree
badge={badge}
inheritStyle={inheritStyle}
badge={false}
showLBC={showLBC}
amount={costInfo.cost}
isEstimate={!costInfo.includesData}
showFullPrice={showFullPrice}
className={className}
/>
) : null;
);
}
}

View file

@ -1,5 +1,5 @@
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 FileProperties from './view';
@ -8,7 +8,6 @@ const select = (state, props) => ({
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
isNew: makeSelectIsNew(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
});
export default connect(select, null)(FileProperties);

View file

@ -14,11 +14,10 @@ type Props = {
isSubscribed: boolean,
isNew: boolean,
small: boolean,
claimWasPurchased: boolean,
};
export default function FileProperties(props: Props) {
const { uri, downloaded, claimIsMine, claimWasPurchased, isSubscribed, small = false } = props;
const { uri, downloaded, claimIsMine, isSubscribed, small = false } = props;
return (
<div
className={classnames('file-properties', {
@ -29,13 +28,8 @@ export default function FileProperties(props: Props) {
<FileType uri={uri} />
{isSubscribed && <Icon tooltip icon={ICONS.SUBSCRIBE} />}
{!claimIsMine && downloaded && <Icon tooltip icon={ICONS.LIBRARY} />}
{claimWasPurchased ? (
<span className="file-properties__purchased">
<Icon icon={ICONS.PURCHASED} />
</span>
) : (
<FilePrice hideFree uri={uri} badge={false} className="file-properties__not-purchased" />
)}
<FilePrice hideFree uri={uri} />
</div>
);
}

View file

@ -4,14 +4,11 @@ import {
makeSelectThumbnailForUri,
makeSelectContentTypeForUri,
makeSelectDownloadPathForUri,
makeSelectStreamingUrlForUri,
} from 'lbry-redux';
import * as SETTINGS from 'constants/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import {
makeSelectFileRenderModeForUri,
makeSelectFileExtensionForUri,
makeSelectStreamingUrlForUriWebProxy,
} from 'redux/selectors/content';
import { makeSelectFileRenderModeForUri, makeSelectFileExtensionForUri } from 'redux/selectors/content';
import FileRender from './view';
const select = (state, props) => {
@ -23,7 +20,7 @@ const select = (state, props) => {
contentType: makeSelectContentTypeForUri(props.uri)(state),
downloadPath: makeSelectDownloadPathForUri(props.uri)(state),
fileExtension: makeSelectFileExtensionForUri(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
autoplay: autoplay,
};

View file

@ -7,31 +7,28 @@ import Button from 'component/button';
type Props = {
uri: string,
isFree: boolean,
renderMode: string,
};
export default function FileRenderDownload(props: Props) {
const { uri, renderMode, isFree } = props;
const { uri, renderMode } = props;
// @if TARGET='web'
if (RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode)) {
return (
<Card
title={isFree ? __('Download or Get the App') : __('Get the App')}
title={__('Download or Get the App')}
subtitle={
<p>
{isFree
? __(
'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.')}
{__(
'This content can be downloaded from lbry.tv, but not displayed. It will display in LBRY Desktop, an app for desktop computers.'
)}
</p>
}
actions={
<div className="section__actions">
{isFree && <FileDownloadLink uri={uri} buttonType="primary" showLabel />}
<Button button={!isFree ? 'primary' : 'link'} label={__('Get the App')} href="https://lbry.com/get" />
<FileDownloadLink uri={uri} buttonType="primary" showLabel />
<Button button={'link'} label={__('Get the App')} href="https://lbry.com/get" />
</div>
}
/>

View file

@ -1,12 +1,11 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { makeSelectFileInfoForUri, makeSelectTitleForUri } from 'lbry-redux';
import { makeSelectFileInfoForUri, makeSelectTitleForUri, makeSelectStreamingUrlForUri } from 'lbry-redux';
import {
makeSelectIsPlayerFloating,
selectFloatingUri,
selectPlayingUri,
makeSelectFileRenderModeForUri,
makeSelectStreamingUrlForUriWebProxy,
} from 'redux/selectors/content';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doCloseFloatingPlayer, doSetPlayingUri } from 'redux/actions/content';
@ -22,7 +21,7 @@ const select = (state, props) => {
title: makeSelectTitleForUri(uri)(state),
fileInfo: makeSelectFileInfoForUri(uri)(state),
isFloating: makeSelectIsPlayerFloating(props.location)(state),
streamingUrl: makeSelectStreamingUrlForUriWebProxy(uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
renderMode: makeSelectFileRenderModeForUri(uri)(state),
};

View file

@ -38,6 +38,7 @@ export default function FileRenderFloating(props: Props) {
renderMode,
setPlayingUri,
} = props;
const isMobile = useIsMobile();
const [fileViewerRect, setFileViewerRect] = useState();
const [desktopPlayStartTime, setDesktopPlayStartTime] = useState();

View file

@ -1,8 +1,14 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
import { makeSelectFileInfoForUri, makeSelectThumbnailForUri, makeSelectClaimForUri } from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import {
makeSelectFileInfoForUri,
makeSelectThumbnailForUri,
makeSelectClaimForUri,
makeSelectStreamingUrlForUri,
makeSelectClaimWasPurchased,
} from 'lbry-redux';
import { makeSelectCostInfoForUri, selectUserVerifiedEmail } from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { withRouter } from 'react-router';
import {
@ -10,10 +16,10 @@ import {
makeSelectShouldObscurePreview,
selectPlayingUri,
makeSelectInsufficientCreditsForUri,
makeSelectStreamingUrlForUriWebProxy,
makeSelectFileRenderModeForUri,
} from 'redux/selectors/content';
import FileRenderInitiator from './view';
import { doAnaltyicsPurchaseEvent } from 'redux/actions/app';
const select = (state, props) => ({
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
@ -22,20 +28,20 @@ const select = (state, props) => ({
isPlaying: makeSelectIsPlaying(props.uri)(state),
playingUri: selectPlayingUri(state),
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
authenticated: selectUserVerifiedEmail(state),
});
const perform = dispatch => ({
play: uri => {
dispatch(doSetPlayingUri(uri));
// @if TARGET='app'
dispatch(doPlayUri(uri));
// @endif
dispatch(doPlayUri(uri, undefined, undefined, fileInfo => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
},
});

View file

@ -20,6 +20,7 @@ type Props = {
fileInfo: FileListItem,
uri: string,
history: { push: string => void },
location: { search: ?string, pathname: string },
obscurePreview: boolean,
insufficientCredits: boolean,
thumbnail?: string,
@ -29,6 +30,8 @@ type Props = {
inline: boolean,
renderMode: string,
claim: StreamClaim,
claimWasPurchased: boolean,
authenticated: boolean,
};
export default function FileRenderInitiator(props: Props) {
@ -40,11 +43,14 @@ export default function FileRenderInitiator(props: Props) {
obscurePreview,
insufficientCredits,
history,
location,
thumbnail,
autoplay,
renderMode,
hasCostInfo,
costInfo,
claimWasPurchased,
authenticated,
} = props;
const cost = costInfo && costInfo.cost;
@ -52,6 +58,10 @@ export default function FileRenderInitiator(props: Props) {
const fileStatus = fileInfo && fileInfo.status;
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
// 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(
@ -99,13 +109,13 @@ export default function FileRenderInitiator(props: Props) {
return null;
}
const showAppNag = IS_WEB && (!isFree || RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode));
const disabled = showAppNag || (!fileInfo && insufficientCredits);
const showAppNag = IS_WEB && RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode);
const disabled = showAppNag || (!fileInfo && insufficientCredits && !claimWasPurchased);
const shouldRedirect = IS_WEB && !authenticated && !isFree;
return (
<div
onClick={disabled ? undefined : viewFile}
onClick={disabled ? undefined : shouldRedirect ? doAuthRedirect : viewFile}
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
className={classnames('content__cover', {
'content__cover--disabled': disabled,
@ -121,7 +131,7 @@ export default function FileRenderInitiator(props: Props) {
href="https://lbry.com/get"
/>
)}
{insufficientCredits && !showAppNag && (
{!claimWasPurchased && insufficientCredits && !showAppNag && (
<Nag
type="helpful"
inline
@ -132,6 +142,7 @@ export default function FileRenderInitiator(props: Props) {
)}
{!disabled && (
<Button
requiresAuth={IS_WEB}
onClick={viewFile}
iconSize={30}
title={isPlayable ? __('Play') : __('View')}

View file

@ -1,11 +1,7 @@
import { connect } from 'react-redux';
import { makeSelectFileInfoForUri } from 'lbry-redux';
import { makeSelectFileInfoForUri, makeSelectStreamingUrlForUri } from 'lbry-redux';
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
import {
makeSelectFileRenderModeForUri,
makeSelectIsPlaying,
makeSelectStreamingUrlForUriWebProxy,
} from 'redux/selectors/content';
import { makeSelectFileRenderModeForUri, makeSelectIsPlaying } from 'redux/selectors/content';
import { withRouter } from 'react-router';
import { doAnalyticsView } from 'redux/actions/app';
import FileRenderInline from './view';
@ -13,7 +9,7 @@ import FileRenderInline from './view';
const select = (state, props) => ({
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
isPlaying: makeSelectIsPlaying(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
});

View file

@ -20,10 +20,10 @@ function FileTitle(props: Props) {
return (
<Card
isPageTitle
noTitleWrap
title={
<React.Fragment>
{title}
<FilePrice badge uri={normalizeURI(uri)} />
{nsfw && (
<span className="media__title-badge">
<span className="badge badge--tag-mature">{__('Mature')}</span>
@ -31,14 +31,23 @@ function FileTitle(props: Props) {
)}
</React.Fragment>
}
titleActions={<FilePrice uri={normalizeURI(uri)} type="filepage" />}
body={
<React.Fragment>
<ClaimInsufficientCredits uri={uri} />
<FileSubtitle uri={uri} />
<FileAuthor uri={uri} />
</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>;
}
const supportsAmount = claim && claim.meta && claim.meta.support_amount && Number(claim.meta.support_amount);
const purchaseReceipt = claim && claim.purchase_receipt;
return (
<Fragment>
<Card
@ -37,6 +39,18 @@ class FileValues extends PureComponent<Props> {
actions={
<table className="table table--condensed table--fixed table--lbc-details">
<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>
<td> {__('Original Publish Amount')}</td>
<td>
@ -49,7 +63,6 @@ class FileValues extends PureComponent<Props> {
</tr>
<tr>
<td>
{' '}
{__('Supports and Tips')}
<HelpLink href="https://lbry.com/faq/tipping" />
</td>

View file

@ -1,7 +1,7 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { selectFollowedTags } from 'lbry-redux';
import { selectFollowedTags, selectPurchaseUriSuccess, doClearPurchasedUriSuccess } from 'lbry-redux';
import { selectUploadCount, selectUserVerifiedEmail } from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSignOut } from 'redux/actions/app';
@ -13,11 +13,10 @@ const select = state => ({
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
uploadCount: selectUploadCount(state),
email: selectUserVerifiedEmail(state),
purchaseSuccess: selectPurchaseUriSuccess(state),
});
export default connect(
select,
{
doSignOut,
}
)(SideNavigation);
export default connect(select, {
doSignOut,
doClearPurchasedUriSuccess,
})(SideNavigation);

View file

@ -8,6 +8,7 @@ import Tag from 'component/tag';
import StickyBox from 'react-sticky-box/dist/esnext';
import Spinner from 'component/spinner';
import usePersistedState from 'effects/use-persisted-state';
import classnames from 'classnames';
// @if TARGET='web'
// import Ads from 'lbrytv/component/ads';
// @endif
@ -24,6 +25,8 @@ type Props = {
expanded: boolean,
doSignOut: () => void,
location: { pathname: string },
purchaseSuccess: boolean,
doClearPurchasedUriSuccess: () => void,
};
function SideNavigation(props: Props) {
@ -36,9 +39,12 @@ function SideNavigation(props: Props) {
sticky = true,
expanded = false,
location,
purchaseSuccess,
doClearPurchasedUriSuccess,
} = props;
const { pathname } = location;
const isAuthenticated = Boolean(email);
const [pulseLibrary, setPulseLibrary] = React.useState(false);
const [sideInformation, setSideInformation] = usePersistedState(
'side-navigation:information',
getSideInformation(pathname)
@ -65,6 +71,19 @@ function SideNavigation(props: Props) {
setSideInformation(sideInfo);
}, [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) {
return {
navigate: path ? `$/${path}` : '/',
@ -115,11 +134,9 @@ function SideNavigation(props: Props) {
{
...buildLink(PAGES.DISCOVER, __('All Content'), ICONS.DISCOVER),
},
// @if TARGET='app'
{
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
},
// @endif
{
...(expanded ? { ...buildLink(PAGES.SETTINGS, __('Settings'), ICONS.SETTINGS) } : {}),
},
@ -127,7 +144,14 @@ function SideNavigation(props: Props) {
linkProps =>
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>
)
)}

View file

@ -17,6 +17,7 @@ export type Player = {
dispose: () => void,
currentTime: (?number) => number,
ended: () => boolean,
error: () => any,
};
type Props = {

View file

@ -163,6 +163,13 @@ function VideoViewer(props: Props) {
setIsPlaying(false);
handlePosition(player);
});
player.on('error', function() {
const error = player.error();
if (error) {
analytics.sentryError('Video.js error', error);
}
});
player.on('volumechange', () => {
if (player && player.volume() !== volume) {
changeVolume(player.volume());

View file

@ -6,10 +6,15 @@ export const DURATION_KEY = 'duration';
export const TAGS_KEY = 't';
export const CONTENT_KEY = 'content';
export const REPOSTED_URI_KEY = 'reposted_uri';
export const CHANNEL_IDS_KEY = 'channel_ids';
export const TAGS_ALL = 'tags_any';
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_WEEK = 'week';
export const FRESH_MONTH = 'month';
@ -39,6 +44,7 @@ export const FILE_IMAGE = 'image';
export const FILE_MODEL = 'model';
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_STREAM = 'stream';
export const CLAIM_REPOST = 'repost';

View file

@ -31,6 +31,7 @@ import {
doAuthTokenRefresh,
} from 'util/saved-passwords';
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
import { LBRY_TV_API } from 'config';
// Import our app styles
// 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');
}
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`;
const proxyURL = `${SDK_API_PATH}/proxy`;

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
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 ModalAffirmPurchase from './view';
@ -9,18 +9,13 @@ const select = (state, props) => ({
});
const perform = dispatch => ({
analyticsPurchaseEvent: fileInfo => dispatch(doAnaltyicsPurchaseEvent(fileInfo)),
cancelPurchase: () => {
dispatch(doSetPlayingUri(null));
dispatch(doHideModal());
},
closeModal: () => dispatch(doHideModal()),
loadVideo: uri => {
dispatch(doSetPlayingUri(uri));
dispatch(doPlayUri(uri, true));
},
loadVideo: (uri, onSuccess) => dispatch(doPlayUri(uri, true, undefined, onSuccess)),
});
export default connect(
select,
perform
)(ModalAffirmPurchase);
export default connect(select, perform)(ModalAffirmPurchase);

View file

@ -1,64 +1,105 @@
// @flow
import React from 'react';
import classnames from 'classnames';
import FilePrice from 'component/filePrice';
import { Modal } from 'modal/modal';
import Card from 'component/common/card';
import I18nMessage from 'component/i18nMessage';
import Button from 'component/button';
// This number is tied to transitions in scss/purchase.scss
const ANIMATION_LENGTH = 2500;
type Props = {
closeModal: () => void,
loadVideo: string => void,
loadVideo: (string, (GetResponse) => void) => void,
uri: string,
cancelPurchase: () => void,
metadata: StreamMetadata,
analyticsPurchaseEvent: GetResponse => void,
};
class ModalAffirmPurchase extends React.PureComponent<Props> {
constructor() {
super();
function ModalAffirmPurchase(props: Props) {
const {
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() {
this.props.closeModal();
this.props.loadVideo(this.props.uri);
}
React.useEffect(() => {
let timeout;
if (success) {
timeout = setTimeout(() => {
closeModal();
setSuccess(false);
}, ANIMATION_LENGTH);
}
render() {
const {
cancelPurchase,
metadata: { title },
uri,
} = this.props;
return () => {
if (timeout) {
clearTimeout(timeout);
}
};
}, [success, uri]);
const modalTitle = __('Confirm Purchase');
return (
<Modal type="card" isOpen contentLabel={modalTitle} onAborted={cancelPurchase}>
<Card
title={modalTitle}
subtitle={
<I18nMessage
tokens={{
claim_title: <strong>{title ? `"${title}"` : uri}</strong>,
amount: <FilePrice uri={uri} showFullPrice inheritStyle />,
}}
>
This will purchase %claim_title% for %amount%.
</I18nMessage>
}
actions={
<div className="section__actions">
<Button button="primary" label={__('Confirm')} onClick={this.onAffirmPurchase} />
<Button button="link" label={__('Cancel')} onClick={cancelPurchase} />
return (
<Modal type="card" isOpen contentLabel={modalTitle} onAborted={cancelPurchase}>
<Card
title={modalTitle}
subtitle={
<div className={classnames('purchase-stuff', { 'purchase-stuff--purchased': success })}>
<div>
{success && (
<div className="purchase-stuff__text--purchased">
{__('Purchased!')}
<div className="purchase_stuff__subtext--purchased">
{__('This content will now be in your Library.')}
</div>
</div>
)}
{/* Keep this message rendered but hidden so the width doesn't change */}
<I18nMessage
tokens={{
claim_title: <strong>{title ? `"${title}"` : uri}</strong>,
}}
>
Are you sure you want to purchase %claim_title%?
</I18nMessage>
</div>
}
/>
</Modal>
);
}
<div>
<FilePrice uri={uri} showFullPrice type="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;

View file

@ -1,11 +1,13 @@
// @flow
import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons';
import React, { useEffect } from 'react';
import ClaimList from 'component/claimList';
import Page from 'component/page';
import Button from 'component/button';
import YoutubeTransferStatus from 'component/youtubeTransferStatus';
import Spinner from 'component/spinner';
import * as MODALS from 'constants/modal_types';
import Card from 'component/common/card';
type Props = {
channels: Array<ChannelClaim>,
@ -40,12 +42,19 @@ export default function ChannelsPage(props: Props) {
{hasYoutubeChannels && <YoutubeTransferStatus hideChannelLink />}
{channels && Boolean(channels.length) && (
<ClaimList
header={__('Your Channels')}
loading={fetchingChannels}
uris={channels.map(channel => channel.permanent_url)}
headerAltControls={
<Button button="link" label={__('New Channel')} onClick={() => openModal(MODALS.CREATE_CHANNEL)} />
<Card
title={__('Your Channels')}
titleActions={
<Button
button="secondary"
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={
<Button
icon={ICONS.SEARCH}
button="alt"
button="secondary"
label={__('Discover Channels')}
navigate={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`}
/>

View file

@ -5,6 +5,7 @@ import Page from 'component/page';
import ClaimListDiscover from 'component/claimListDiscover';
import Button from 'component/button';
import useHover from 'effects/use-hover';
import useIsMobile from 'effects/use-is-mobile';
import analytics from 'analytics';
import HiddenNsfw from 'component/common/hidden-nsfw';
import Icon from 'component/common/icon';
@ -33,6 +34,7 @@ function DiscoverPage(props: Props) {
} = props;
const buttonRef = useRef();
const isHovering = useHover(buttonRef);
const isMobile = useIsMobile();
const urlParams = new URLSearchParams(search);
const claimType = urlParams.get('claim_type');
@ -94,7 +96,8 @@ function DiscoverPage(props: Props) {
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
injectedItem={!isAuthenticated && IS_WEB && <Ads type="video" />}
meta={
tag && (
tag &&
!isMobile && (
<Button
ref={buttonRef}
button="alt"

View file

@ -3,7 +3,6 @@ import {
makeSelectSearchDownloadUrlsForPage,
selectDownloadUrlsCount,
selectIsFetchingFileList,
doPurchaseList,
makeSelectMyPurchasesForPage,
selectIsFetchingMyPurchases,
selectMyPurchasesCount,
@ -30,8 +29,4 @@ const select = (state, props) => {
};
};
export default withRouter(
connect(select, {
doPurchaseList,
})(FileListDownloaded)
);
export default withRouter(connect(select)(FileListDownloaded));

View file

@ -11,6 +11,7 @@ import { FormField } from 'component/common/form-components/form-field';
import { withRouter } from 'react-router';
import Card from 'component/common/card';
import classnames from 'classnames';
import Yrbl from 'component/yrbl';
type Props = {
fetchingFileList: boolean,
@ -38,7 +39,6 @@ function FileListDownloaded(props: Props) {
myDownloads,
fetchingFileList,
fetchingMyPurchases,
doPurchaseList,
} = props;
const loading = fetchingFileList || fetchingMyPurchases;
const [viewMode, setViewMode] = React.useState(VIEW_PURCHASES);
@ -52,10 +52,6 @@ function FileListDownloaded(props: Props) {
}
}
React.useEffect(() => {
doPurchaseList();
}, [doPurchaseList]);
return (
<Card
title={
@ -63,7 +59,7 @@ function FileListDownloaded(props: Props) {
<Button
icon={ICONS.LIBRARY}
button="alt"
label={__('All Downloads')}
label={__('Downloads')}
className={classnames(`button-toggle`, {
'button-toggle--active': viewMode === VIEW_DOWNLOADS,
})}
@ -72,7 +68,7 @@ function FileListDownloaded(props: Props) {
<Button
icon={ICONS.PURCHASED}
button="alt"
label={__('Your Purchases')}
label={__('Purchases')}
className={classnames(`button-toggle`, {
'button-toggle--active': viewMode === VIEW_PURCHASES,
})}
@ -97,29 +93,46 @@ function FileListDownloaded(props: Props) {
}
isBodyList
body={
<div>
<ClaimList
isCardBody
renderProperties={() => null}
empty={
viewMode === VIEW_PURCHASES && !query ? (
<div>{__("You haven't purchased anything yet silly goose.")}</div>
) : (
__('No results for %query%', { query })
)
}
uris={viewMode === VIEW_PURCHASES ? myPurchases : myDownloads}
loading={loading}
/>
{!query && (
<Paginate
loading={loading}
totalPages={Math.ceil(
Number(viewMode === VIEW_PURCHASES ? myPurchasesCount : downloadedUrlsCount) / Number(PAGE_SIZE)
)}
IS_WEB && viewMode === VIEW_DOWNLOADS ? (
<div className="main--empty">
<Yrbl
title={__('Try Out the App!')}
subtitle={
<>
<p className="section__subtitle">
{__("Download the app to track files you've viewed and downloaded.")}
</p>
<div className="section__actions">
<Button button="primary" label={__('Get The App')} href="https://lbry.com/get" />
</div>
</>
}
/>
)}
</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 { 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 = {
authenticated: boolean,
followedTags: Array<Tag>,
@ -31,6 +89,20 @@ function HomePage(props: Props) {
const showIndividualTags = showPersonalizedTags && followedTags.length < 5;
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 (showPersonalizedChannels) {
let releaseTime = `>${Math.floor(
@ -93,6 +165,10 @@ function HomePage(props: Props) {
});
}
if (authenticated) {
rowData.push(lbrytvPaidBetaRow);
}
rowData.push({
title: 'Top Content from Today',
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({
title: 'Trending Classics',
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,
selectMyPurchases,
selectIsFetchingMyPurchases,
doPurchaseList,
} from 'lbry-redux';
import LibraryPage from './view';
@ -14,4 +15,6 @@ const select = 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 DownloadList from 'page/fileListDownloaded';
import Yrbl from 'component/yrbl';
import { useHistory } from 'react-router';
type Props = {
allDownloadedUrlsCount: number,
myPurchases: Array<string>,
fetchingMyPurchases: boolean,
fetchingFileList: boolean,
doPurchaseList: number => void,
};
function LibraryPage(props: Props) {
const { allDownloadedUrlsCount, myPurchases, fetchingMyPurchases, fetchingFileList } = props;
const hasDownloads = allDownloadedUrlsCount > 0 || (myPurchases && myPurchases.length);
const { allDownloadedUrlsCount, myPurchases, fetchingMyPurchases, fetchingFileList, doPurchaseList } = props;
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;
React.useEffect(() => {
doPurchaseList(page);
}, [doPurchaseList, page]);
return (
<Page>
{loading && !hasDownloads && (
@ -29,10 +38,12 @@ function LibraryPage(props: Props) {
{!loading && !hasDownloads && (
<div className="main--empty">
<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={
<div className="section__actions">
<Button button="primary" navigate="/" label={__('Explore new content')} />
<Button button="primary" navigate="/" label={__('Explore New Content')} />
</div>
}
/>

View file

@ -49,7 +49,4 @@ const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)),
});
export default connect(
select,
perform
)(ShowPage);
export default connect(select, perform)(ShowPage);

View file

@ -22,7 +22,7 @@ function DiscoverPage() {
defaultTags={CS.TAGS_FOLLOWED}
meta={
<Button
button="alt"
button="secondary"
icon={ICONS.EDIT}
label={__('Manage')}
requiresAuth={IS_WEB}

View file

@ -10,7 +10,6 @@ import {
commentReducer,
blockedReducer,
publishReducer,
fileReducer,
} from 'lbry-redux';
import {
userReducer,
@ -39,7 +38,6 @@ export default history =>
content: contentReducer,
costInfo: costInfoReducer,
fileInfo: fileInfoReducer,
file: fileReducer,
homepage: homepageReducer,
notifications: notificationsReducer,
publish: publishReducer,

View file

@ -43,7 +43,7 @@ import {
selectAllowAnalytics,
} from 'redux/selectors/app';
// 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 analytics, { SHARE_INTERNAL } from 'analytics';
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() {
return (dispatch, getState) => {
// @if TARGET='web'

View file

@ -20,6 +20,8 @@ import {
makeSelectUriIsStreamable,
selectDownloadingByOutpoint,
makeSelectClaimForUri,
makeSelectClaimIsMine,
makeSelectClaimWasPurchased,
} from 'lbry-redux';
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
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) => {
function onSuccess(fileInfo) {
if (saveFile) {
@ -165,7 +167,7 @@ export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolea
}
if (cb) {
cb();
cb(fileInfo);
}
}
@ -181,19 +183,22 @@ export function doPlayUri(
) {
return (dispatch: Dispatch, getState: () => any) => {
const state = getState();
const isMine = makeSelectClaimIsMine(uri)(state);
const fileInfo = makeSelectFileInfoForUri(uri)(state);
const uriIsStreamable = makeSelectUriIsStreamable(uri)(state);
const downloadingByOutpoint = selectDownloadingByOutpoint(state);
const claimWasPurchased = makeSelectClaimWasPurchased(uri)(state);
const alreadyDownloaded = fileInfo && (fileInfo.completed || (fileInfo.blobs_remaining === 0 && uriIsStreamable));
const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
if (alreadyDownloading || alreadyDownloaded) {
if (!IS_WEB && (alreadyDownloading || alreadyDownloaded)) {
return;
}
const daemonSettings = selectDaemonSettings(state);
const costInfo = makeSelectCostInfoForUri(uri)(state);
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 instantPurchaseMax = makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_MAX)(state);
@ -203,7 +208,12 @@ export function doPlayUri(
function attemptPlay(instantPurchaseMax = null) {
// 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 }));
} else {
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) {
const handler = reducers[action.type];
if (handler) return handler(state, action);

View file

@ -1,4 +1,5 @@
import * as ACTIONS from 'constants/action_types';
import { ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
const reducers = {};
const defaultState = {
@ -89,6 +90,13 @@ reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => {
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) {
const handler = reducers[action.type];
if (handler) return handler(state, action);

View file

@ -7,7 +7,6 @@ import {
makeSelectClaimIsNsfw,
makeSelectClaimIsMine,
makeSelectRecommendedContentForUri,
makeSelectStreamingUrlForUri,
makeSelectMediaTypeForUri,
selectBalance,
selectBlockedChannels,
@ -21,9 +20,6 @@ import { selectShowMatureContent } from 'redux/selectors/settings';
import * as RENDER_MODES from 'constants/file_render_modes';
import path from 'path';
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 HISTORY_ITEMS_PER_PAGE = 50;
@ -167,16 +163,6 @@ export const makeSelectFileExtensionForUri = (uri: string) =>
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) =>
createSelector(
makeSelectContentTypeForUri(uri),

View file

@ -33,6 +33,7 @@
@import 'component/nag';
@import 'component/navigation';
@import 'component/pagination';
@import 'component/purchase';
@import 'component/placeholder';
@import 'component/search';
@import 'component/claim-search';

View file

@ -75,7 +75,7 @@
}
}
.card__section--flex {
.card__title-section {
@extend .section__flex;
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 {
@extend .card__actions;
margin-top: 0;
@ -165,6 +173,7 @@
}
.card__title-actions {
align-self: flex-start;
padding: var(--spacing-medium);
@media (max-width: $breakpoint-small) {
@ -202,6 +211,12 @@
.card__header--between {
@extend .card__header;
justify-content: space-between;
flex-wrap: wrap;
}
.card__header--nowrap {
@extend .card__header--between;
flex-wrap: nowrap;
}
.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 {
display: flex;
flex-wrap: wrap;
@ -433,13 +422,17 @@
bottom: var(--spacing-miniscule);
right: var(--spacing-miniscule);
background-color: var(--color-black);
padding: 0.2rem;
padding: 0.3rem;
border-radius: var(--border-radius);
.file-properties {
color: var(--color-white);
}
.file-price {
padding: 0.1rem;
}
@media (max-width: $breakpoint-small) {
display: none;
}

View file

@ -33,14 +33,28 @@
}
.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) {
padding-left: var(--spacing-medium);
}
@media (max-width: $breakpoint-small) {
padding-left: 0px;
&:not(:first-of-type) {
margin-top: var(--spacing-small);
}
padding-left: 0px;
&:not(:first-of-type) {
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 {
font-size: var(--font-xsmall);
line-height: 1.2;
@ -34,64 +44,3 @@
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 {
width: 100%;
}
@media (max-width: $breakpoint-small) {
padding: 0 var(--spacing-medium);
}
}
.main--launching {

View file

@ -67,7 +67,6 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-small);
}
.media__info-text {

View file

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

View file

@ -64,3 +64,24 @@
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 {
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-error: #61373f;
--color-purchased: #ffd580;
--color-purchased-alt: #ffd5804a;
--color-purchased-text: #eeeeee;
--color-purchased-alt: var(--color-purchased);
--color-purchased-text: var(--color-gray-5);
// Text
--color-text: #eeeeee;

View file

@ -826,10 +826,10 @@
prop-types "^15.6.2"
scheduler "^0.18.0"
"@lbry/components@^4.1.5":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.1.5.tgz#eb6a74c6e17ee9ed46cbb9c590aca10847d18f38"
integrity sha512-T/cR5tZvikI/dAE2euJP07+/yi+yO1wkE/4djT8EhCJITF+SZ0CjHG8auNYQ/zQw7H5KQnh/WMYaRVxO54FKLA==
"@lbry/components@^4.2.2":
version "4.2.2"
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.2.2.tgz#023b8224e180b69cd8b5d77242441742bca26d0f"
integrity sha512-CziwuALDiv/DXT5zwkVK8cfF914WyOqKg8GkqIk/f9vc7VXZx9OlRfBadnpGDrezrjjHn7onwOreQrwzB5PcNA==
"@mapbox/hast-util-table-cell-style@^0.1.3":
version "0.1.3"
@ -6178,17 +6178,17 @@ lazy-val@^1.0.4:
yargs "^13.2.2"
zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#cd9c15567f2934ddc82de364d88b378ff04d5571:
lbry-redux@lbryio/lbry-redux#aa2cfa789670e899824d3d3ac1ae677172a7ad4e:
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:
proxy-polyfill "0.1.6"
reselect "^3.0.0"
uuid "^3.3.2"
lbryinc@lbryio/lbryinc#cc62a4eec10845cc0b31da7d0f27287cfa7c4866:
lbryinc@lbryio/lbryinc#6a52f8026cdc7cd56d200fb5c46f852e0139bbeb:
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:
reselect "^3.0.0"