Playlist fall out fixes (#7032)

* Add snack bar notification

* Fix and improve code

* Better handle paid content on playlists

* Fix menu options that show for unauth users
This commit is contained in:
saltrafael 2021-09-10 14:27:21 -03:00 committed by GitHub
parent f6683d3c49
commit e8d8dfa76b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 391 additions and 276 deletions

View file

@ -15,8 +15,11 @@ type Props = {
nextRecommendedClaim: ?StreamClaim, nextRecommendedClaim: ?StreamClaim,
nextRecommendedUri: string, nextRecommendedUri: string,
modal: { id: string, modalProps: {} }, modal: { id: string, modalProps: {} },
skipPaid: boolean,
doNavigate: () => void, doNavigate: () => void,
doReplay: () => void, doReplay: () => void,
doPrevious: () => void,
onCanceled: () => void,
}; };
function AutoplayCountdown(props: Props) { function AutoplayCountdown(props: Props) {
@ -25,8 +28,11 @@ function AutoplayCountdown(props: Props) {
nextRecommendedClaim, nextRecommendedClaim,
history: { push }, history: { push },
modal, modal,
skipPaid,
doNavigate, doNavigate,
doReplay, doReplay,
doPrevious,
onCanceled,
} = props; } = props;
const nextTitle = nextRecommendedClaim && nextRecommendedClaim.value && nextRecommendedClaim.value.title; const nextTitle = nextRecommendedClaim && nextRecommendedClaim.value && nextRecommendedClaim.value.title;
@ -68,6 +74,7 @@ function AutoplayCountdown(props: Props) {
interval = setInterval(() => { interval = setInterval(() => {
const newTime = timer - 1; const newTime = timer - 1;
if (newTime === 0) { if (newTime === 0) {
if (skipPaid) setTimer(countdownTime);
doNavigate(); doNavigate();
} else { } else {
setTimer(timer - 1); setTimer(timer - 1);
@ -78,7 +85,7 @@ function AutoplayCountdown(props: Props) {
return () => { return () => {
clearInterval(interval); clearInterval(interval);
}; };
}, [timer, doNavigate, push, timerCanceled, isTimerPaused, nextRecommendedUri]); }, [timer, doNavigate, push, timerCanceled, isTimerPaused, nextRecommendedUri, skipPaid]);
if (timerCanceled || !nextRecommendedUri) { if (timerCanceled || !nextRecommendedUri) {
return null; return null;
@ -105,19 +112,41 @@ function AutoplayCountdown(props: Props) {
)} )}
{!isTimerPaused && ( {!isTimerPaused && (
<div className="file-viewer__overlay-secondary autoplay-countdown__counter"> <div className="file-viewer__overlay-secondary autoplay-countdown__counter">
{__('Playing in %seconds_left% seconds...', { seconds_left: timer })}{' '} {__('Playing %message% in %seconds_left% seconds...', {
<Button label={__('Cancel')} button="link" onClick={() => setTimerCanceled(true)} /> message: skipPaid ? __('next free content') : __(''),
seconds_left: timer,
})}{' '}
<Button
label={__('Cancel')}
button="link"
onClick={() => {
setTimerCanceled(true);
if (onCanceled) onCanceled();
}}
/>
</div> </div>
)} )}
<Button {skipPaid && doPrevious && (
label={__('Replay?')} <div>
button="link" <Button
iconRight={ICONS.REPLAY} label={__('Play Previous')}
onClick={() => { button="link"
setTimerCanceled(true); icon={ICONS.PLAY_PREVIOUS}
doReplay(); onClick={() => doPrevious()}
}} />
/> </div>
)}
<div>
<Button
label={skipPaid ? __('Purchase?') : __('Replay?')}
button="link"
iconRight={skipPaid ? ICONS.WALLET : ICONS.REPLAY}
onClick={() => {
setTimerCanceled(true);
doReplay();
}}
/>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -31,14 +31,13 @@ import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscrip
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectListShuffle } from 'redux/selectors/content'; import { selectListShuffle } from 'redux/selectors/content';
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content'; import { doToggleLoopList, doToggleShuffleList } from 'redux/actions/content';
import ClaimPreview from './view'; import ClaimPreview from './view';
import fs from 'fs'; import fs from 'fs';
const select = (state, props) => { const select = (state, props) => {
const claim = makeSelectClaimForUri(props.uri, false)(state); const claim = makeSelectClaimForUri(props.uri, false)(state);
const collectionId = props.collectionId; const collectionId = props.collectionId;
const resolvedList = makeSelectUrlsForCollectionId(collectionId)(state);
const repostedClaim = claim && claim.reposted_claim; const repostedClaim = claim && claim.reposted_claim;
const contentClaim = repostedClaim || claim; const contentClaim = repostedClaim || claim;
const contentSigningChannel = contentClaim && contentClaim.signing_channel; const contentSigningChannel = contentClaim && contentClaim.signing_channel;
@ -73,7 +72,7 @@ const select = (state, props) => {
isMyCollection: makeSelectCollectionIsMine(collectionId)(state), isMyCollection: makeSelectCollectionIsMine(collectionId)(state),
editedCollection: makeSelectEditedCollectionForId(collectionId)(state), editedCollection: makeSelectEditedCollectionForId(collectionId)(state),
isAuthenticated: Boolean(selectUserVerifiedEmail(state)), isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
resolvedList, resolvedList: makeSelectUrlsForCollectionId(collectionId)(state),
playNextUri, playNextUri,
}; };
}; };
@ -102,8 +101,10 @@ const perform = (dispatch) => ({
doChannelUnsubscribe: (subscription) => dispatch(doChannelUnsubscribe(subscription)), doChannelUnsubscribe: (subscription) => dispatch(doChannelUnsubscribe(subscription)),
doCollectionEdit: (collection, props) => dispatch(doCollectionEdit(collection, props)), doCollectionEdit: (collection, props) => dispatch(doCollectionEdit(collection, props)),
fetchCollectionItems: (collectionId) => dispatch(doFetchItemsInCollection({ collectionId })), fetchCollectionItems: (collectionId) => dispatch(doFetchItemsInCollection({ collectionId })),
doSetPlayingUri: (uri) => dispatch(doSetPlayingUri({ uri })), doToggleShuffleList: (collectionId) => {
doToggleShuffleList: (collectionId) => dispatch(doToggleShuffleList(undefined, collectionId, true, true)), dispatch(doToggleLoopList(collectionId, false, true));
dispatch(doToggleShuffleList(undefined, collectionId, true, true));
},
}); });
export default connect(select, perform)(ClaimPreview); export default connect(select, perform)(ClaimPreview);

View file

@ -7,7 +7,13 @@ import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button'; import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import { generateShareUrl, generateRssUrl, generateLbryContentUrl, formatLbryUrlForWeb } from 'util/url'; import {
generateShareUrl,
generateRssUrl,
generateLbryContentUrl,
formatLbryUrlForWeb,
generateListSearchUrlParams,
} from 'util/url';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { buildURI, parseURI, COLLECTIONS_CONSTS } from 'lbry-redux'; import { buildURI, parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
@ -59,7 +65,6 @@ type Props = {
playNextUri: string, playNextUri: string,
resolvedList: boolean, resolvedList: boolean,
fetchCollectionItems: (string) => void, fetchCollectionItems: (string) => void,
doSetPlayingUri: (string) => void,
doToggleShuffleList: (string) => void, doToggleShuffleList: (string) => void,
}; };
@ -101,7 +106,6 @@ function ClaimMenuList(props: Props) {
playNextUri, playNextUri,
resolvedList, resolvedList,
fetchCollectionItems, fetchCollectionItems,
doSetPlayingUri,
doToggleShuffleList, doToggleShuffleList,
} = props; } = props;
const [doShuffle, setDoShuffle] = React.useState(false); const [doShuffle, setDoShuffle] = React.useState(false);
@ -129,15 +133,15 @@ function ClaimMenuList(props: Props) {
if (doShuffle && resolvedList) { if (doShuffle && resolvedList) {
doToggleShuffleList(collectionId); doToggleShuffleList(collectionId);
if (playNextUri) { if (playNextUri) {
const collectionParams = new URLSearchParams(); const navigateUrl = formatLbryUrlForWeb(playNextUri);
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId); push({
const navigateUrl = formatLbryUrlForWeb(playNextUri) + `?` + collectionParams.toString(); pathname: navigateUrl,
setDoShuffle(false); search: generateListSearchUrlParams(collectionId),
doSetPlayingUri(playNextUri); state: { collectionId, forceAutoplay: true },
push(navigateUrl); });
} }
} }
}, [collectionId, doSetPlayingUri, doShuffle, doToggleShuffleList, playNextUri, push, resolvedList]); }, [collectionId, doShuffle, doToggleShuffleList, playNextUri, push, resolvedList]);
if (!claim) { if (!claim) {
return null; return null;
@ -259,6 +263,7 @@ function ClaimMenuList(props: Props) {
push(`/$/${PAGES.REPORT_CONTENT}?claimId=${contentClaim && contentClaim.claim_id}`); push(`/$/${PAGES.REPORT_CONTENT}?claimId=${contentClaim && contentClaim.claim_id}`);
} }
const shouldShow = !IS_WEB || (IS_WEB && isAuthenticated);
return ( return (
<Menu> <Menu>
<MenuButton <MenuButton
@ -271,94 +276,93 @@ function ClaimMenuList(props: Props) {
<Icon size={20} icon={ICONS.MORE_VERTICAL} /> <Icon size={20} icon={ICONS.MORE_VERTICAL} />
</MenuButton> </MenuButton>
<MenuList className="menu__list"> <MenuList className="menu__list">
{(!IS_WEB || (IS_WEB && isAuthenticated)) && ( <>
<> {/* COLLECTION OPERATIONS */}
{collectionId && isCollectionClaim ? (
<> <>
{/* COLLECTION OPERATIONS */} <MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
{collectionId && isCollectionClaim ? ( <a className="menu__link" href={`/$/${PAGES.LIST}/${collectionId}`}>
<Icon aria-hidden icon={ICONS.VIEW} />
{__('View List')}
</a>
</MenuItem>
<MenuItem
className="comment__menu-option"
onSelect={() => {
if (!resolvedList) fetchItems();
setDoShuffle(true);
}}
>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.SHUFFLE} />
{__('Shuffle Play')}
</div>
</MenuItem>
{isMyCollection && (
<> <>
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}> <MenuItem
<a className="menu__link" href={`/$/${PAGES.LIST}/${collectionId}`}> className="comment__menu-option"
<Icon aria-hidden icon={ICONS.VIEW} /> onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}?view=edit`)}
{__('View List')} >
</a> <div className="menu__link">
<Icon aria-hidden iconColor={'red'} icon={ICONS.PUBLISH} />
{editedCollection ? __('Publish') : __('Edit List')}
</div>
</MenuItem> </MenuItem>
<MenuItem <MenuItem
className="comment__menu-option" className="comment__menu-option"
onSelect={() => { onSelect={() => openModal(MODALS.COLLECTION_DELETE, { collectionId })}
if (!resolvedList) fetchItems();
setDoShuffle(true);
}}
> >
<div className="menu__link"> <div className="menu__link">
<Icon aria-hidden icon={ICONS.SHUFFLE} /> <Icon aria-hidden icon={ICONS.DELETE} />
{__('Shuffle Play')} {__('Delete List')}
</div> </div>
</MenuItem> </MenuItem>
{isMyCollection && (
<>
<MenuItem
className="comment__menu-option"
onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}?view=edit`)}
>
<div className="menu__link">
<Icon aria-hidden iconColor={'red'} icon={ICONS.PUBLISH} />
{editedCollection ? __('Publish') : __('Edit List')}
</div>
</MenuItem>
<MenuItem
className="comment__menu-option"
onSelect={() => openModal(MODALS.COLLECTION_DELETE, { collectionId })}
>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.DELETE} />
{__('Delete List')}
</div>
</MenuItem>
</>
)}
</> </>
) : (
isPlayable && (
<>
{/* WATCH LATER */}
<MenuItem
className="comment__menu-option"
onSelect={() =>
handleAdd(hasClaimInWatchLater, __('Watch Later'), COLLECTIONS_CONSTS.WATCH_LATER_ID)
}
>
<div className="menu__link">
<Icon aria-hidden icon={hasClaimInWatchLater ? ICONS.DELETE : ICONS.TIME} />
{hasClaimInWatchLater ? __('In Watch Later') : __('Watch Later')}
</div>
</MenuItem>
{/* FAVORITES LIST */}
<MenuItem
className="comment__menu-option"
onSelect={() => handleAdd(hasClaimInFavorites, __('Favorites'), COLLECTIONS_CONSTS.FAVORITES_ID)}
>
<div className="menu__link">
<Icon aria-hidden icon={hasClaimInFavorites ? ICONS.DELETE : ICONS.STAR} />
{hasClaimInFavorites ? __('In Favorites') : __('Favorites')}
</div>
</MenuItem>
{/* CURRENTLY ONLY SUPPORT PLAYLISTS FOR PLAYABLE; LATER DIFFERENT TYPES */}
<MenuItem
className="comment__menu-option"
onSelect={() => openModal(MODALS.COLLECTION_ADD, { uri, type: 'playlist' })}
>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.STACK} />
{__('Add to Lists')}
</div>
</MenuItem>
<hr className="menu__separator" />
</>
)
)} )}
</> </>
) : (
shouldShow &&
isPlayable && (
<>
{/* WATCH LATER */}
<MenuItem
className="comment__menu-option"
onSelect={() => handleAdd(hasClaimInWatchLater, __('Watch Later'), COLLECTIONS_CONSTS.WATCH_LATER_ID)}
>
<div className="menu__link">
<Icon aria-hidden icon={hasClaimInWatchLater ? ICONS.DELETE : ICONS.TIME} />
{hasClaimInWatchLater ? __('In Watch Later') : __('Watch Later')}
</div>
</MenuItem>
{/* FAVORITES LIST */}
<MenuItem
className="comment__menu-option"
onSelect={() => handleAdd(hasClaimInFavorites, __('Favorites'), COLLECTIONS_CONSTS.FAVORITES_ID)}
>
<div className="menu__link">
<Icon aria-hidden icon={hasClaimInFavorites ? ICONS.DELETE : ICONS.STAR} />
{hasClaimInFavorites ? __('In Favorites') : __('Favorites')}
</div>
</MenuItem>
{/* CURRENTLY ONLY SUPPORT PLAYLISTS FOR PLAYABLE; LATER DIFFERENT TYPES */}
<MenuItem
className="comment__menu-option"
onSelect={() => openModal(MODALS.COLLECTION_ADD, { uri, type: 'playlist' })}
>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.STACK} />
{__('Add to Lists')}
</div>
</MenuItem>
<hr className="menu__separator" />
</>
)
)}
</>
{shouldShow && (
<>
{!isChannelPage && ( {!isChannelPage && (
<> <>
<MenuItem className="comment__menu-option" onSelect={handleSupport}> <MenuItem className="comment__menu-option" onSelect={handleSupport}>
@ -435,9 +439,9 @@ function ClaimMenuList(props: Props) {
)} )}
</> </>
)} )}
<hr className="menu__separator" />
</> </>
)} )}
<hr className="menu__separator" />
<MenuItem className="comment__menu-option" onSelect={handleCopyLink}> <MenuItem className="comment__menu-option" onSelect={handleCopyLink}>
<div className="menu__link"> <div className="menu__link">

View file

@ -10,9 +10,9 @@ import ChannelThumbnail from 'component/channelThumbnail';
import FileViewCountInline from 'component/fileViewCountInline'; import FileViewCountInline from 'component/fileViewCountInline';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import useGetThumbnail from 'effects/use-get-thumbnail'; import useGetThumbnail from 'effects/use-get-thumbnail';
import { formatLbryUrlForWeb } from 'util/url'; import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
import { formatClaimPreviewTitle } from 'util/formatAriaLabel'; import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
import { parseURI, COLLECTIONS_CONSTS, isURIEqual } from 'lbry-redux'; import { parseURI, isURIEqual } from 'lbry-redux';
import PreviewOverlayProperties from 'component/previewOverlayProperties'; import PreviewOverlayProperties from 'component/previewOverlayProperties';
import FileDownloadLink from 'component/fileDownloadLink'; import FileDownloadLink from 'component/fileDownloadLink';
import FileWatchLaterLink from 'component/fileWatchLaterLink'; import FileWatchLaterLink from 'component/fileWatchLaterLink';
@ -98,13 +98,9 @@ function ClaimPreviewTile(props: Props) {
const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, placeholder) || thumbnail; const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, placeholder) || thumbnail;
const canonicalUrl = claim && claim.canonical_url; const canonicalUrl = claim && claim.canonical_url;
const permanentUrl = claim && claim.permanent_url; const permanentUrl = claim && claim.permanent_url;
let navigateUrl = formatLbryUrlForWeb(canonicalUrl || uri || '/');
const listId = collectionId || collectionClaimId; const listId = collectionId || collectionClaimId;
if (listId) { const navigateUrl =
const collectionParams = new URLSearchParams(); formatLbryUrlForWeb(canonicalUrl || uri || '/') + (listId ? generateListSearchUrlParams(listId) : '');
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, listId);
navigateUrl = navigateUrl + `?` + collectionParams.toString();
}
const navLinkProps = { const navLinkProps = {
to: navigateUrl, to: navigateUrl,
onClick: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(),

View file

@ -1,17 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
makeSelectClaimIsMine,
makeSelectClaimForUri, makeSelectClaimForUri,
selectMyChannelClaims,
makeSelectClaimIsPending, makeSelectClaimIsPending,
makeSelectCollectionIsMine, makeSelectCollectionIsMine,
makeSelectEditedCollectionForId, makeSelectEditedCollectionForId,
} from 'lbry-redux'; } from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { doToast } from 'redux/actions/notifications';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import { selectListShuffle } from 'redux/selectors/content'; import { selectListShuffle } from 'redux/selectors/content';
import { doPlayUri, doSetPlayingUri, doToggleShuffleList, doToggleLoopList } from 'redux/actions/content'; import { doToggleShuffleList, doToggleLoopList } from 'redux/actions/content';
import CollectionActions from './view'; import CollectionActions from './view';
const select = (state, props) => { const select = (state, props) => {
@ -31,29 +27,23 @@ const select = (state, props) => {
const shuffleList = selectListShuffle(state); const shuffleList = selectListShuffle(state);
const shuffle = shuffleList && shuffleList.collectionId === collectionId && shuffleList.newUrls; const shuffle = shuffleList && shuffleList.collectionId === collectionId && shuffleList.newUrls;
const playNextUri = shuffle && shuffle[0]; const playNextUri = shuffle && shuffle[0];
const playNextClaim = makeSelectClaimForUri(playNextUri)(state);
return { return {
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
myChannels: selectMyChannelClaims(state),
claimIsPending: makeSelectClaimIsPending(props.uri)(state), claimIsPending: makeSelectClaimIsPending(props.uri)(state),
isMyCollection: makeSelectCollectionIsMine(collectionId)(state), isMyCollection: makeSelectCollectionIsMine(collectionId)(state),
collectionHasEdits: Boolean(makeSelectEditedCollectionForId(collectionId)(state)), collectionHasEdits: Boolean(makeSelectEditedCollectionForId(collectionId)(state)),
firstItem, firstItem,
playNextUri, playNextUri,
playNextClaim,
}; };
}; };
const perform = (dispatch) => ({ const perform = (dispatch) => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
doToast: (options) => dispatch(doToast(options)), doToggleShuffleList: (collectionId, shuffle) => {
doPlayUri: (uri) => dispatch(doPlayUri(uri)), dispatch(doToggleLoopList(collectionId, false, true));
doSetPlayingUri: (uri) => dispatch(doSetPlayingUri({ uri })), dispatch(doToggleShuffleList(undefined, collectionId, shuffle, true));
doToggleShuffleList: (collectionId, shuffle) => dispatch(doToggleShuffleList(undefined, collectionId, shuffle, true)), },
doToggleLoopList: (collectionId, loop) => dispatch(doToggleLoopList(collectionId, loop)),
}); });
export default connect(select, perform)(CollectionActions); export default connect(select, perform)(CollectionActions);

View file

@ -11,15 +11,12 @@ import { useHistory } from 'react-router';
import { EDIT_PAGE, PAGE_VIEW_QUERY } from 'page/collection/view'; import { EDIT_PAGE, PAGE_VIEW_QUERY } from 'page/collection/view';
import classnames from 'classnames'; import classnames from 'classnames';
import { ENABLE_FILE_REACTIONS } from 'config'; import { ENABLE_FILE_REACTIONS } from 'config';
import { COLLECTIONS_CONSTS } from 'lbry-redux'; import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
import { formatLbryUrlForWeb } from 'util/url';
type Props = { type Props = {
uri: string, uri: string,
claim: StreamClaim, claim: StreamClaim,
openModal: (id: string, { uri: string, claimIsMine?: boolean, isSupport?: boolean }) => void, openModal: (id: string, {}) => void,
myChannels: ?Array<ChannelClaim>,
doToast: ({ message: string }) => void,
claimIsPending: boolean, claimIsPending: boolean,
isMyCollection: boolean, isMyCollection: boolean,
collectionId: string, collectionId: string,
@ -28,11 +25,7 @@ type Props = {
collectionHasEdits: boolean, collectionHasEdits: boolean,
isBuiltin: boolean, isBuiltin: boolean,
doToggleShuffleList: (string, boolean) => void, doToggleShuffleList: (string, boolean) => void,
doToggleLoopList: (string, boolean) => void,
playNextUri: string, playNextUri: string,
playNextClaim: StreamClaim,
doPlayUri: (string) => void,
doSetPlayingUri: (string) => void,
firstItem: string, firstItem: string,
}; };
@ -49,11 +42,7 @@ function CollectionActions(props: Props) {
collectionHasEdits, collectionHasEdits,
isBuiltin, isBuiltin,
doToggleShuffleList, doToggleShuffleList,
doToggleLoopList,
playNextUri, playNextUri,
playNextClaim,
doPlayUri,
doSetPlayingUri,
firstItem, firstItem,
} = props; } = props;
const [doShuffle, setDoShuffle] = React.useState(false); const [doShuffle, setDoShuffle] = React.useState(false);
@ -63,23 +52,23 @@ function CollectionActions(props: Props) {
const webShareable = true; // collections have cost? const webShareable = true; // collections have cost?
const doPlay = React.useCallback( const doPlay = React.useCallback(
(uri) => { (playUri) => {
const collectionParams = new URLSearchParams(); const navigateUrl = formatLbryUrlForWeb(playUri);
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId); push({
const navigateUrl = formatLbryUrlForWeb(uri) + `?` + collectionParams.toString(); pathname: navigateUrl,
push(navigateUrl); search: generateListSearchUrlParams(collectionId),
doSetPlayingUri(uri); state: { forceAutoplay: true },
doPlayUri(uri); });
}, },
[collectionId, push, doSetPlayingUri, doPlayUri] [collectionId, push]
); );
React.useEffect(() => { React.useEffect(() => {
if (playNextClaim && doShuffle) { if (playNextUri && doShuffle) {
setDoShuffle(false); setDoShuffle(false);
doPlay(playNextUri); doPlay(playNextUri);
} }
}, [doPlay, doShuffle, playNextClaim, playNextUri]); }, [doPlay, doShuffle, playNextUri]);
const lhsSection = ( const lhsSection = (
<> <>
@ -90,7 +79,6 @@ function CollectionActions(props: Props) {
title={__('Play')} title={__('Play')}
onClick={() => { onClick={() => {
doToggleShuffleList(collectionId, false); doToggleShuffleList(collectionId, false);
doToggleLoopList(collectionId, false);
doPlay(firstItem); doPlay(firstItem);
}} }}
/> />

View file

@ -1,8 +1,8 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doCollectionEdit, makeSelectNameForCollectionId, doCollectionDelete } from 'lbry-redux'; import { makeSelectNameForCollectionId } from 'lbry-redux';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import { selectListShuffle } from 'redux/selectors/content'; import { selectListShuffle } from 'redux/selectors/content';
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content'; import { doToggleLoopList, doToggleShuffleList } from 'redux/actions/content';
import CollectionMenuList from './view'; import CollectionMenuList from './view';
const select = (state, props) => { const select = (state, props) => {
@ -17,10 +17,12 @@ const select = (state, props) => {
}; };
}; };
export default connect(select, { const perform = (dispatch) => ({
doCollectionEdit, openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
doOpenModal, doToggleShuffleList: (collectionId) => {
doCollectionDelete, dispatch(doToggleLoopList(collectionId, false, true));
doSetPlayingUri, dispatch(doToggleShuffleList(undefined, collectionId, true, true));
doToggleShuffleList, },
})(CollectionMenuList); });
export default connect(select, perform)(CollectionMenuList);

View file

@ -7,8 +7,7 @@ import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { formatLbryUrlForWeb } from 'util/url'; import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
import { COLLECTIONS_CONSTS } from 'lbry-redux';
type Props = { type Props = {
inline?: boolean, inline?: boolean,
@ -16,34 +15,26 @@ type Props = {
collectionName?: string, collectionName?: string,
collectionId: string, collectionId: string,
playNextUri: string, playNextUri: string,
doSetPlayingUri: ({ uri: ?string }) => void, doToggleShuffleList: (string) => void,
doToggleShuffleList: (string, string, boolean, boolean) => void,
}; };
function CollectionMenuList(props: Props) { function CollectionMenuList(props: Props) {
const { const { inline = false, collectionId, collectionName, doOpenModal, playNextUri, doToggleShuffleList } = props;
inline = false,
collectionId,
collectionName,
doOpenModal,
playNextUri,
doSetPlayingUri,
doToggleShuffleList,
} = props;
const [doShuffle, setDoShuffle] = React.useState(false); const [doShuffle, setDoShuffle] = React.useState(false);
const { push } = useHistory(); const { push } = useHistory();
React.useEffect(() => { React.useEffect(() => {
if (playNextUri && doShuffle) { if (playNextUri && doShuffle) {
const collectionParams = new URLSearchParams();
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
const navigateUrl = formatLbryUrlForWeb(playNextUri) + `?` + collectionParams.toString();
setDoShuffle(false); setDoShuffle(false);
doSetPlayingUri({ uri: playNextUri }); const navigateUrl = formatLbryUrlForWeb(playNextUri);
push(navigateUrl); push({
pathname: navigateUrl,
search: generateListSearchUrlParams(collectionId),
state: { forceAutoplay: true },
});
} }
}, [push, doSetPlayingUri, collectionId, playNextUri, doShuffle]); }, [collectionId, doShuffle, playNextUri, push]);
return ( return (
<Menu> <Menu>
@ -68,7 +59,7 @@ function CollectionMenuList(props: Props) {
<MenuItem <MenuItem
className="comment__menu-option" className="comment__menu-option"
onSelect={() => { onSelect={() => {
doToggleShuffleList('', collectionId, true, true); doToggleShuffleList(collectionId);
setDoShuffle(true); setDoShuffle(true);
}} }}
> >

View file

@ -7,8 +7,7 @@ import TruncatedText from 'component/common/truncated-text';
import CollectionCount from './collectionCount'; import CollectionCount from './collectionCount';
import CollectionPrivate from './collectionPrivate'; import CollectionPrivate from './collectionPrivate';
import CollectionMenuList from 'component/collectionMenuList'; import CollectionMenuList from 'component/collectionMenuList';
import { formatLbryUrlForWeb } from 'util/url'; import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
import { COLLECTIONS_CONSTS } from 'lbry-redux';
import FileThumbnail from 'component/fileThumbnail'; import FileThumbnail from 'component/fileThumbnail';
type Props = { type Props = {
@ -62,16 +61,12 @@ function CollectionPreviewTile(props: Props) {
if (collectionId && hasClaim && resolveCollectionItems) { if (collectionId && hasClaim && resolveCollectionItems) {
resolveCollectionItems({ collectionId, page_size: 5 }); resolveCollectionItems({ collectionId, page_size: 5 });
} }
}, [collectionId, hasClaim]); }, [collectionId, hasClaim, resolveCollectionItems]);
// const signingChannel = claim && claim.signing_channel; // const signingChannel = claim && claim.signing_channel;
let navigateUrl = formatLbryUrlForWeb(collectionItemUrls[0] || '/'); const navigateUrl =
if (collectionId) { formatLbryUrlForWeb(collectionItemUrls[0] || '/') + (collectionId ? generateListSearchUrlParams(collectionId) : '');
const collectionParams = new URLSearchParams();
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
navigateUrl = navigateUrl + `?` + collectionParams.toString();
}
function handleClick(e) { function handleClick(e) {
if (navigateUrl) { if (navigateUrl) {

View file

@ -2440,6 +2440,12 @@ export const icons = {
</svg> </svg>
), ),
[ICONS.PLAY]: buildIcon(<polygon points="5 3 19 12 5 21 5 3" />), [ICONS.PLAY]: buildIcon(<polygon points="5 3 19 12 5 21 5 3" />),
[ICONS.PLAY_PREVIOUS]: buildIcon(
<g>
<polygon points="19 20 9 12 19 4 19 20" />
<line x1="5" y1="19" x2="5" y2="5" />
</g>
),
[ICONS.REPLAY]: buildIcon( [ICONS.REPLAY]: buildIcon(
<g> <g>
<polyline points="1 4 1 10 7 10" /> <polyline points="1 4 1 10 7 10" />

View file

@ -4,6 +4,9 @@ import {
makeSelectTitleForUri, makeSelectTitleForUri,
makeSelectStreamingUrlForUri, makeSelectStreamingUrlForUri,
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
makeSelectClaimWasPurchased,
makeSelectNextUrlForCollectionAndUrl,
makeSelectPreviousUrlForCollectionAndUrl,
SETTINGS, SETTINGS,
} from 'lbry-redux'; } from 'lbry-redux';
import { import {
@ -13,8 +16,10 @@ import {
makeSelectFileRenderModeForUri, makeSelectFileRenderModeForUri,
} from 'redux/selectors/content'; } from 'redux/selectors/content';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetPlayingUri } from 'redux/actions/content'; import { makeSelectCostInfoForUri } from 'lbryinc';
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
import { doFetchRecommendedContent } from 'redux/actions/search'; import { doFetchRecommendedContent } from 'redux/actions/search';
import { doAnaltyicsPurchaseEvent } from 'redux/actions/app';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import FileRenderFloating from './view'; import FileRenderFloating from './view';
@ -30,12 +35,16 @@ const select = (state, props) => {
playingUri, playingUri,
title: makeSelectTitleForUri(uri)(state), title: makeSelectTitleForUri(uri)(state),
fileInfo: makeSelectFileInfoForUri(uri)(state), fileInfo: makeSelectFileInfoForUri(uri)(state),
mature: makeSelectClaimIsNsfw(props.uri)(state), mature: makeSelectClaimIsNsfw(uri)(state),
isFloating: makeSelectIsPlayerFloating(props.location)(state), isFloating: makeSelectIsPlayerFloating(props.location)(state),
streamingUrl: makeSelectStreamingUrlForUri(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),
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state), videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state),
costInfo: makeSelectCostInfoForUri(uri)(state),
claimWasPurchased: makeSelectClaimWasPurchased(uri)(state),
nextListUri: collectionId && makeSelectNextUrlForCollectionAndUrl(collectionId, uri)(state),
previousListUri: collectionId && makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state),
collectionId, collectionId,
}; };
}; };
@ -43,6 +52,19 @@ const select = (state, props) => {
const perform = (dispatch) => ({ const perform = (dispatch) => ({
closeFloatingPlayer: () => dispatch(doSetPlayingUri({ uri: null })), closeFloatingPlayer: () => dispatch(doSetPlayingUri({ uri: null })),
doFetchRecommendedContent: (uri, mature) => dispatch(doFetchRecommendedContent(uri, mature)), doFetchRecommendedContent: (uri, mature) => dispatch(doFetchRecommendedContent(uri, mature)),
doPlayUri: (uri, collectionId, hideFailModal) =>
dispatch(
doPlayUri(
uri,
false,
false,
(fileInfo) => {
dispatch(doAnaltyicsPurchaseEvent(fileInfo));
},
hideFailModal
),
dispatch(doSetPlayingUri({ uri, collectionId }))
),
}); });
export default withRouter(connect(select, perform)(FileRenderFloating)); export default withRouter(connect(select, perform)(FileRenderFloating));

View file

@ -11,10 +11,12 @@ import usePersistedState from 'effects/use-persisted-state';
import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view'; import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import { onFullscreenChange } from 'util/full-screen'; import { onFullscreenChange } from 'util/full-screen';
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
import { useIsMobile } from 'effects/use-screensize'; import { useIsMobile } from 'effects/use-screensize';
import debounce from 'util/debounce'; import debounce from 'util/debounce';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { isURIEqual, COLLECTIONS_CONSTS } from 'lbry-redux'; import { isURIEqual } from 'lbry-redux';
import AutoplayCountdown from 'component/autoplayCountdown';
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false; const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 60; const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 60;
@ -34,7 +36,12 @@ type Props = {
primaryUri: ?string, primaryUri: ?string,
videoTheaterMode: boolean, videoTheaterMode: boolean,
doFetchRecommendedContent: (string, boolean) => void, doFetchRecommendedContent: (string, boolean) => void,
doPlayUri: (string, string, boolean) => void,
collectionId: string, collectionId: string,
costInfo: any,
claimWasPurchased: boolean,
nextListUri: string,
previousListUri: string,
}; };
export default function FileRenderFloating(props: Props) { export default function FileRenderFloating(props: Props) {
@ -52,16 +59,23 @@ export default function FileRenderFloating(props: Props) {
primaryUri, primaryUri,
videoTheaterMode, videoTheaterMode,
doFetchRecommendedContent, doFetchRecommendedContent,
doPlayUri,
collectionId, collectionId,
costInfo,
claimWasPurchased,
nextListUri,
previousListUri,
} = props; } = props;
const { const { location, push } = useHistory();
location: { pathname }, const hideFloatingPlayer = location.state && location.state.hideFloatingPlayer;
} = useHistory();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const mainFilePlaying = playingUri && isURIEqual(playingUri.uri, primaryUri); const mainFilePlaying = playingUri && isURIEqual(playingUri.uri, primaryUri);
const [fileViewerRect, setFileViewerRect] = useState(); const [fileViewerRect, setFileViewerRect] = useState();
const [desktopPlayStartTime, setDesktopPlayStartTime] = useState(); const [desktopPlayStartTime, setDesktopPlayStartTime] = useState();
const [wasDragging, setWasDragging] = useState(false); const [wasDragging, setWasDragging] = useState(false);
const [doNavigate, setDoNavigate] = useState(false);
const [playNextUrl, setPlayNextUrl] = useState(true);
const [countdownCanceled, setCountdownCanceled] = useState(false);
const [position, setPosition] = usePersistedState('floating-file-viewer:position', { const [position, setPosition] = usePersistedState('floating-file-viewer:position', {
x: -25, x: -25,
y: window.innerHeight - 400, y: window.innerHeight - 400,
@ -71,13 +85,10 @@ export default function FileRenderFloating(props: Props) {
y: 0, y: 0,
}); });
let navigateUrl; const navigateUrl = uri + (collectionId ? generateListSearchUrlParams(collectionId) : '');
if (collectionId) {
const collectionParams = new URLSearchParams();
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
navigateUrl = uri + `?` + collectionParams.toString();
}
const isFree = costInfo && costInfo.cost === 0;
const canViewFile = isFree || claimWasPurchased;
const playingUriSource = playingUri && playingUri.source; const playingUriSource = playingUri && playingUri.source;
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode); const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed)); const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
@ -102,7 +113,7 @@ export default function FileRenderFloating(props: Props) {
} }
} }
function clampToScreen(pos) { const clampToScreen = React.useCallback((pos) => {
const ESTIMATED_SCROLL_BAR_PX = 50; const ESTIMATED_SCROLL_BAR_PX = 50;
const FLOATING_PLAYER_CLASS = 'content__viewer--floating'; const FLOATING_PLAYER_CLASS = 'content__viewer--floating';
const fpPlayerElem = document.querySelector(`.${FLOATING_PLAYER_CLASS}`); const fpPlayerElem = document.querySelector(`.${FLOATING_PLAYER_CLASS}`);
@ -115,7 +126,7 @@ export default function FileRenderFloating(props: Props) {
pos.y = getScreenHeight() - fpPlayerElem.getBoundingClientRect().height; pos.y = getScreenHeight() - fpPlayerElem.getBoundingClientRect().height;
} }
} }
} }, []);
// Updated 'relativePos' based on persisted 'position': // Updated 'relativePos' based on persisted 'position':
const stringifiedPosition = JSON.stringify(position); const stringifiedPosition = JSON.stringify(position);
@ -139,7 +150,7 @@ export default function FileRenderFloating(props: Props) {
setPosition({ x: pos.x, y: pos.y }); setPosition({ x: pos.x, y: pos.y });
} }
} }
}, [isFloating, stringifiedPosition]); }, [clampToScreen, isFloating, position.x, position.y, setPosition, stringifiedPosition]);
// Listen to main-window resizing and adjust the fp position accordingly: // Listen to main-window resizing and adjust the fp position accordingly:
useEffect(() => { useEffect(() => {
@ -157,9 +168,9 @@ export default function FileRenderFloating(props: Props) {
// 'relativePos' is needed in the dependency list to avoid stale closure. // 'relativePos' is needed in the dependency list to avoid stale closure.
// Otherwise, this could just be changed to a one-time effect. // Otherwise, this could just be changed to a one-time effect.
}, [relativePos]); }, [clampToScreen, relativePos.x, relativePos.y, setPosition]);
function handleResize() { const handleResize = React.useCallback(() => {
const element = mainFilePlaying const element = mainFilePlaying
? document.querySelector(`.${PRIMARY_PLAYER_WRAPPER_CLASS}`) ? document.querySelector(`.${PRIMARY_PLAYER_WRAPPER_CLASS}`)
: document.querySelector(`.${INLINE_PLAYER_WRAPPER_CLASS}`); : document.querySelector(`.${INLINE_PLAYER_WRAPPER_CLASS}`);
@ -184,13 +195,14 @@ export default function FileRenderFloating(props: Props) {
// $FlowFixMe // $FlowFixMe
setFileViewerRect({ ...objectRect, windowOffset: window.pageYOffset }); setFileViewerRect({ ...objectRect, windowOffset: window.pageYOffset });
} }, [mainFilePlaying]);
useEffect(() => { useEffect(() => {
if (streamingUrl) { if (streamingUrl) {
handleResize(); handleResize();
setCountdownCanceled(false);
} }
}, [streamingUrl, pathname, playingUriSource, isFloating, mainFilePlaying]); }, [handleResize, streamingUrl, videoTheaterMode]);
useEffect(() => { useEffect(() => {
handleResize(); handleResize();
@ -201,7 +213,7 @@ export default function FileRenderFloating(props: Props) {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
onFullscreenChange(window, 'remove', handleResize); onFullscreenChange(window, 'remove', handleResize);
}; };
}, [setFileViewerRect, isFloating, playingUriSource, mainFilePlaying, videoTheaterMode]); }, [handleResize]);
useEffect(() => { useEffect(() => {
// @if TARGET='app' // @if TARGET='app'
@ -219,9 +231,42 @@ export default function FileRenderFloating(props: Props) {
if (isFloating) { if (isFloating) {
doFetchRecommendedContent(uri, mature); doFetchRecommendedContent(uri, mature);
} }
}, [uri, mature, isFloating]); }, [doFetchRecommendedContent, isFloating, mature, uri]);
if (!isPlayable || !uri || (isFloating && (isMobile || !floatingPlayerEnabled))) { const doPlay = React.useCallback(
(playUri) => {
setDoNavigate(false);
if (!isFloating) {
const navigateUrl = formatLbryUrlForWeb(playUri);
push({
pathname: navigateUrl,
search: collectionId && generateListSearchUrlParams(collectionId),
state: { collectionId, forceAutoplay: true, hideFloatingPlayer: true },
});
} else {
doPlayUri(playUri, collectionId, true);
}
},
[collectionId, doPlayUri, isFloating, push]
);
useEffect(() => {
if (!doNavigate) return;
if (playNextUrl && nextListUri) {
doPlay(nextListUri);
} else if (previousListUri) {
doPlay(previousListUri);
}
setPlayNextUrl(true);
}, [doNavigate, doPlay, nextListUri, playNextUrl, previousListUri]);
if (
!isPlayable ||
!uri ||
(isFloating && (isMobile || !floatingPlayerEnabled || hideFloatingPlayer)) ||
(collectionId && !isFloating && ((!canViewFile && !nextListUri) || countdownCanceled))
) {
return null; return null;
} }
@ -307,17 +352,30 @@ export default function FileRenderFloating(props: Props) {
// @endif // @endif
/> />
) : ( ) : (
<LoadingScreen status={loadingMessage} /> <>
{collectionId && !canViewFile ? (
<div className="content__loading">
<AutoplayCountdown
nextRecommendedUri={nextListUri}
doNavigate={() => setDoNavigate(true)}
doReplay={() => doPlayUri(uri, collectionId, false)}
doPrevious={() => {
setPlayNextUrl(false);
setDoNavigate(true);
}}
onCanceled={() => setCountdownCanceled(true)}
skipPaid
/>
</div>
) : (
<LoadingScreen status={loadingMessage} />
)}
</>
)} )}
{isFloating && ( {isFloating && (
<div className="draggable content__info"> <div className="draggable content__info">
<div className="claim-preview__title" title={title || uri}> <div className="claim-preview__title" title={title || uri}>
<Button <Button label={title || uri} navigate={navigateUrl} button="link" className="content__floating-link" />
label={title || uri}
navigate={navigateUrl || uri}
button="link"
className="content__floating-link"
/>
</div> </div>
<UriIndicator link uri={uri} /> <UriIndicator link uri={uri} />
</div> </div>

View file

@ -37,7 +37,6 @@ const select = (state, props) => {
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state), insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state), streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY_MEDIA)(state), autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY_MEDIA)(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),

View file

@ -22,12 +22,11 @@ type Props = {
fileInfo: FileListItem, fileInfo: FileListItem,
uri: string, uri: string,
history: { push: (string) => void }, history: { push: (string) => void },
location: { search: ?string, pathname: string }, location: { search: ?string, pathname: string, href: string, state: { forceAutoplay: boolean } },
obscurePreview: boolean, obscurePreview: boolean,
insufficientCredits: boolean, insufficientCredits: boolean,
claimThumbnail?: string, claimThumbnail?: string,
autoplay: boolean, autoplay: boolean,
hasCostInfo: boolean,
costInfo: any, costInfo: any,
inline: boolean, inline: boolean,
renderMode: string, renderMode: string,
@ -50,7 +49,6 @@ export default function FileRenderInitiator(props: Props) {
location, location,
claimThumbnail, claimThumbnail,
renderMode, renderMode,
hasCostInfo,
costInfo, costInfo,
claimWasPurchased, claimWasPurchased,
authenticated, authenticated,
@ -58,24 +56,20 @@ export default function FileRenderInitiator(props: Props) {
collectionId, collectionId,
} = props; } = props;
// force autoplay if a timestamp is present // check if there is a time or autoplay parameter, if so force autoplay
let autoplay = props.autoplay; const urlTimeParam = location && location.href && location.href.indexOf('t=') > -1;
// get current url const forceAutoplayParam = location && location.state && location.state.forceAutoplay;
const url = window.location.href; const autoplay = forceAutoplayParam || urlTimeParam || props.autoplay;
// check if there is a time parameter, if so force autoplay
if (url.indexOf('t=') > -1) {
autoplay = true;
}
const cost = costInfo && costInfo.cost; const isFree = costInfo && costInfo.cost === 0;
const isFree = hasCostInfo && cost === 0; const canViewFile = isFree || claimWasPurchased;
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);
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode); const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder); const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
const containerRef = React.useRef<any>(); const containerRef = React.useRef<any>();
React.useEffect(() => { useEffect(() => {
if (claimThumbnail) { if (claimThumbnail) {
setTimeout(() => { setTimeout(() => {
let newThumbnail = claimThumbnail; let newThumbnail = claimThumbnail;
@ -96,7 +90,7 @@ export default function FileRenderInitiator(props: Props) {
} }
}, 200); }, 200);
} }
}, [claimThumbnail]); }, [claimThumbnail, thumbnail]);
function doAuthRedirect() { function doAuthRedirect() {
history.push(`/$/${PAGES.AUTH}?redirect=${encodeURIComponent(location.pathname)}`); history.push(`/$/${PAGES.AUTH}?redirect=${encodeURIComponent(location.pathname)}`);
@ -137,19 +131,20 @@ export default function FileRenderInitiator(props: Props) {
useEffect(() => { useEffect(() => {
const videoOnPage = document.querySelector('video'); const videoOnPage = document.querySelector('video');
if ( if (
(isFree || claimWasPurchased) && (canViewFile || forceAutoplayParam) &&
((autoplay && !videoOnPage && isPlayable) || RENDER_MODES.AUTO_RENDER_MODES.includes(renderMode)) ((autoplay && (!videoOnPage || forceAutoplayParam) && isPlayable) ||
RENDER_MODES.AUTO_RENDER_MODES.includes(renderMode))
) { ) {
viewFile(); viewFile();
} }
}, [autoplay, viewFile, isFree, renderMode, isPlayable, claimWasPurchased]); }, [autoplay, canViewFile, forceAutoplayParam, isPlayable, renderMode, viewFile]);
/* /*
once content is playing, let the appropriate <FileRender> take care of it... once content is playing, let the appropriate <FileRender> take care of it...
but for playables, always render so area can be used to fill with floating player but for playables, always render so area can be used to fill with floating player
*/ */
if (isPlaying && !isPlayable) { if (isPlaying && !isPlayable) {
if (isFree || claimWasPurchased) { if (canViewFile && !collectionId) {
return null; return null;
} }
} }

View file

@ -57,8 +57,8 @@ class UriIndicator extends React.PureComponent<Props> {
} }
const isChannelClaim = claim.value_type === 'channel'; const isChannelClaim = claim.value_type === 'channel';
const signingChannel = claim.signing_channel && claim.signing_channel.amount;
if (!claim.signing_channel && !isChannelClaim) { if (!signingChannel && !isChannelClaim) {
if (hideAnonymous) { if (hideAnonymous) {
return null; return null;
} }

View file

@ -8,7 +8,13 @@ import {
makeSelectNextUrlForCollectionAndUrl, makeSelectNextUrlForCollectionAndUrl,
makeSelectPreviousUrlForCollectionAndUrl, makeSelectPreviousUrlForCollectionAndUrl,
} from 'lbry-redux'; } from 'lbry-redux';
import { doChangeVolume, doChangeMute, doAnalyticsView, doAnalyticsBuffer } from 'redux/actions/app'; import {
doChangeVolume,
doChangeMute,
doAnalyticsView,
doAnalyticsBuffer,
doAnaltyicsPurchaseEvent,
} from 'redux/actions/app';
import { selectVolume, selectMute } from 'redux/selectors/app'; import { selectVolume, selectMute } from 'redux/selectors/app';
import { savePosition, clearPosition, doPlayUri, doSetPlayingUri } from 'redux/actions/content'; import { savePosition, clearPosition, doPlayUri, doSetPlayingUri } from 'redux/actions/content';
import { import {
@ -78,8 +84,19 @@ const perform = (dispatch) => ({
toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()), toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()),
toggleAutoplayNext: () => dispatch(toggleAutoplayNext()), toggleAutoplayNext: () => dispatch(toggleAutoplayNext()),
setVideoPlaybackRate: (rate) => dispatch(doSetClientSetting(SETTINGS.VIDEO_PLAYBACK_RATE, rate)), setVideoPlaybackRate: (rate) => dispatch(doSetClientSetting(SETTINGS.VIDEO_PLAYBACK_RATE, rate)),
doPlayUri: (uri) => dispatch(doPlayUri(uri)), doPlayUri: (uri, collectionId) =>
doSetPlayingUri: (uri, collectionId) => dispatch(doSetPlayingUri({ uri, collectionId })), dispatch(
doPlayUri(
uri,
false,
false,
(fileInfo) => {
dispatch(doAnaltyicsPurchaseEvent(fileInfo));
},
true
),
dispatch(doSetPlayingUri({ uri, collectionId }))
),
}); });
export default withRouter(connect(select, perform)(VideoViewer)); export default withRouter(connect(select, perform)(VideoViewer));

View file

@ -25,8 +25,7 @@ import I18nMessage from 'component/i18nMessage';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { getAllIds } from 'util/buildHomepage'; import { getAllIds } from 'util/buildHomepage';
import type { HomepageCat } from 'util/buildHomepage'; import type { HomepageCat } from 'util/buildHomepage';
import { formatLbryUrlForWeb } from 'util/url'; import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
import { COLLECTIONS_CONSTS } from 'lbry-redux';
const PLAY_TIMEOUT_ERROR = 'play_timeout_error'; const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
const PLAY_TIMEOUT_LIMIT = 2000; const PLAY_TIMEOUT_LIMIT = 2000;
@ -60,8 +59,7 @@ type Props = {
homepageData?: { [string]: HomepageCat }, homepageData?: { [string]: HomepageCat },
shareTelemetry: boolean, shareTelemetry: boolean,
isFloating: boolean, isFloating: boolean,
doPlayUri: (string) => void, doPlayUri: (string, string) => void,
doSetPlayingUri: (string, string) => void,
collectionId: string, collectionId: string,
nextRecommendedUri: string, nextRecommendedUri: string,
previousListUri: string, previousListUri: string,
@ -104,7 +102,6 @@ function VideoViewer(props: Props) {
shareTelemetry, shareTelemetry,
isFloating, isFloating,
doPlayUri, doPlayUri,
doSetPlayingUri,
collectionId, collectionId,
nextRecommendedUri, nextRecommendedUri,
previousListUri, previousListUri,
@ -183,28 +180,29 @@ function VideoViewer(props: Props) {
const doPlay = useCallback( const doPlay = useCallback(
(playUri) => { (playUri) => {
let navigateUrl = formatLbryUrlForWeb(playUri);
if (collectionId) {
const collectionParams = new URLSearchParams();
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
navigateUrl = navigateUrl + `?` + collectionParams.toString();
clearPosition(playUri);
}
if (!isFloating) {
push(navigateUrl);
}
doPlayUri(playUri);
doSetPlayingUri(playUri, collectionId);
setDoNavigate(false); setDoNavigate(false);
if (!isFloating) {
const navigateUrl = formatLbryUrlForWeb(playUri);
push({
pathname: navigateUrl,
search: collectionId && generateListSearchUrlParams(collectionId),
state: { collectionId, forceAutoplay: true, hideFloatingPlayer: true },
});
} else {
doPlayUri(playUri, collectionId);
}
}, },
[clearPosition, collectionId, doPlayUri, doSetPlayingUri, isFloating, push] [collectionId, doPlayUri, isFloating, push]
); );
useEffect(() => { useEffect(() => {
if (doNavigate) { if (doNavigate) {
if (playNextUrl) { if (playNextUrl) {
if (permanentUrl !== nextRecommendedUri) { if (permanentUrl !== nextRecommendedUri) {
if (nextRecommendedUri) doPlay(nextRecommendedUri); if (nextRecommendedUri) {
if (collectionId) clearPosition(permanentUrl);
doPlay(nextRecommendedUri);
}
} else { } else {
setReplay(true); setReplay(true);
} }
@ -224,7 +222,18 @@ function VideoViewer(props: Props) {
setEnded(false); setEnded(false);
setPlayNextUrl(true); setPlayNextUrl(true);
} }
}, [doNavigate, doPlay, ended, nextRecommendedUri, permanentUrl, playNextUrl, previousListUri, videoNode]); }, [
clearPosition,
collectionId,
doNavigate,
doPlay,
ended,
nextRecommendedUri,
permanentUrl,
playNextUrl,
previousListUri,
videoNode,
]);
React.useEffect(() => { React.useEffect(() => {
if (ended) { if (ended) {
@ -298,12 +307,12 @@ function VideoViewer(props: Props) {
const doPlayNext = () => { const doPlayNext = () => {
setPlayNextUrl(true); setPlayNextUrl(true);
setDoNavigate(true); setEnded(true);
}; };
const doPlayPrevious = () => { const doPlayPrevious = () => {
setPlayNextUrl(false); setPlayNextUrl(false);
setDoNavigate(true); setEnded(true);
}; };
const onPlayerReady = useCallback((player: Player, videoNode: any) => { const onPlayerReady = useCallback((player: Player, videoNode: any) => {
@ -397,7 +406,6 @@ function VideoViewer(props: Props) {
> >
{showAutoplayCountdown && ( {showAutoplayCountdown && (
<AutoplayCountdown <AutoplayCountdown
uri={uri}
nextRecommendedUri={nextRecommendedUri} nextRecommendedUri={nextRecommendedUri}
doNavigate={() => setDoNavigate(true)} doNavigate={() => setDoNavigate(true)}
doReplay={() => setReplay(true)} doReplay={() => setReplay(true)}

View file

@ -45,6 +45,7 @@ export const WEB = 'Globe';
export const SHARE = 'Share2'; export const SHARE = 'Share2';
export const EXTERNAL = 'ExternalLink'; export const EXTERNAL = 'ExternalLink';
export const PLAY = 'Play'; export const PLAY = 'Play';
export const PLAY_PREVIOUS = 'Play Previous';
export const FACEBOOK = 'Facebook'; export const FACEBOOK = 'Facebook';
export const TWITTER = 'Twitter'; export const TWITTER = 'Twitter';
export const TELEGRAM = 'Telegram'; export const TELEGRAM = 'Telegram';

View file

@ -148,7 +148,8 @@ export function doPlayUri(
uri: string, uri: string,
skipCostCheck: boolean = false, skipCostCheck: boolean = false,
saveFileOverride: boolean = false, saveFileOverride: boolean = false,
cb?: () => void cb?: () => void,
hideFailModal: boolean = false
) { ) {
return (dispatch: Dispatch, getState: () => any) => { return (dispatch: Dispatch, getState: () => any) => {
const state = getState(); const state = getState();
@ -183,7 +184,7 @@ export function doPlayUri(
!claimWasPurchased && !claimWasPurchased &&
(!instantPurchaseMax || !instantPurchaseEnabled || cost > instantPurchaseMax) (!instantPurchaseMax || !instantPurchaseEnabled || cost > instantPurchaseMax)
) { ) {
dispatch(doOpenModal(MODALS.AFFIRM_PURCHASE, { uri })); if (!hideFailModal) dispatch(doOpenModal(MODALS.AFFIRM_PURCHASE, { uri }));
} else { } else {
beginGetFile(); beginGetFile();
} }
@ -290,10 +291,10 @@ export function doToggleLoopList(collectionId: string, loop: boolean, hideToast:
type: ACTIONS.TOGGLE_LOOP_LIST, type: ACTIONS.TOGGLE_LOOP_LIST,
data: { collectionId, loop }, data: { collectionId, loop },
}); });
if (loop && !hideToast) { if (!hideToast) {
dispatch( dispatch(
doToast({ doToast({
message: __('Loop is on.'), message: loop ? __('Loop is on.') : __('Loop is off.'),
}) })
); );
} }
@ -323,18 +324,18 @@ export function doToggleShuffleList(currentUri: string, collectionId: string, sh
type: ACTIONS.TOGGLE_SHUFFLE_LIST, type: ACTIONS.TOGGLE_SHUFFLE_LIST,
data: { collectionId, newUrls }, data: { collectionId, newUrls },
}); });
if (!hideToast) {
dispatch(
doToast({
message: __('Shuffle is on.'),
})
);
}
} else { } else {
dispatch({ dispatch({
type: ACTIONS.TOGGLE_SHUFFLE_LIST, type: ACTIONS.TOGGLE_SHUFFLE_LIST,
data: { collectionId, newUrls: false }, data: { collectionId, newUrls: false },
}); });
} }
if (!hideToast) {
dispatch(
doToast({
message: shuffle ? __('Shuffle is on.') : __('Shuffle is off.'),
})
);
}
}; };
} }

View file

@ -432,5 +432,11 @@ export function toggleAutoplayNext() {
const autoplayNext = makeSelectClientSetting(SETTINGS.AUTOPLAY_NEXT)(state); const autoplayNext = makeSelectClientSetting(SETTINGS.AUTOPLAY_NEXT)(state);
dispatch(doSetClientSetting(SETTINGS.AUTOPLAY_NEXT, !autoplayNext, ready)); dispatch(doSetClientSetting(SETTINGS.AUTOPLAY_NEXT, !autoplayNext, ready));
dispatch(
doToast({
message: autoplayNext ? __('Autoplay Next is off.') : __('Autoplay Next is on.'),
})
);
}; };
} }

View file

@ -156,3 +156,9 @@ export const generateRssUrl = (domain, channelClaim) => {
const url = `${domain}/$/rss/${channelClaim.canonical_url.replace('lbry://', '').replace('#', ':')}`; const url = `${domain}/$/rss/${channelClaim.canonical_url.replace('lbry://', '').replace('#', ':')}`;
return url; return url;
}; };
export const generateListSearchUrlParams = (collectionId) => {
const urlParams = new URLSearchParams();
urlParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
return `?` + urlParams.toString();
};