[Fix] revert fileRenderFloating vs Mobile change (#937)

* Bump react-draggable

Old version was giving out console errors for outdated react functions

* Refactor fileRenderFloating

* Merge fileRenderMobile into fileRenderFloating

* Fixes from review

* Attempt fix failed to view live
This commit is contained in:
saltrafael 2022-02-23 18:13:22 -03:00 committed by GitHub
parent d6cd3caa77
commit 17e3fcc27c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 354 additions and 530 deletions

View file

@ -192,7 +192,7 @@
"react-awesome-lightbox": "^1.7.3",
"react-confetti": "^4.0.1",
"react-dom": "^16.8.2",
"react-draggable": "^3.3.0",
"react-draggable": "^4.4.4",
"react-google-recaptcha": "^2.0.1",
"react-hot-loader": "^4.11.1",
"react-modal": "^3.1.7",

View file

@ -15,7 +15,6 @@ import { openContextMenu } from 'util/context-menu';
import useKonamiListener from 'util/enhanced-layout';
import Yrbl from 'component/yrbl';
import FileRenderFloating from 'component/fileRenderFloating';
import FileRenderMobile from 'component/fileRenderMobile';
import { withRouter } from 'react-router';
import usePrevious from 'effects/use-previous';
import Nag from 'component/common/nag';
@ -524,7 +523,7 @@ function App(props: Props) {
<Router />
<ModalRouter />
<React.Suspense fallback={null}>{renderFiledrop && <FileDrop />}</React.Suspense>
{isMobile ? <FileRenderMobile /> : <FileRenderFloating />}
<FileRenderFloating />
<React.Suspense fallback={null}>
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}

View file

@ -0,0 +1,54 @@
// @flow
import { FLOATING_PLAYER_CLASS } from './view';
function getRootEl() {
return document && document.documentElement;
}
export function getScreenWidth() {
const mainEl = getRootEl();
return mainEl ? mainEl.clientWidth : window.innerWidth;
}
export function getScreenHeight() {
const mainEl = getRootEl();
return mainEl ? mainEl.clientHeight : window.innerHeight;
}
export function getFloatingPlayerRect() {
const elem = document.querySelector(`.${FLOATING_PLAYER_CLASS}`);
return elem ? elem.getBoundingClientRect() : null;
}
export function clampFloatingPlayerToScreen(x: number, y: number) {
const playerRect = getFloatingPlayerRect();
let newX = x;
let newY = y;
if (playerRect) {
const screenW = getScreenWidth();
const screenH = getScreenHeight();
if (x + playerRect.width > screenW) {
newX = screenW - playerRect.width;
} else if (x < 0) {
newX = 0;
}
if (y + playerRect.height > screenH) {
newY = screenH - playerRect.height;
} else if (y < 0) {
newY = 0;
}
}
return { x: newX, y: newY };
}
export function calculateRelativePos(x: number, y: number) {
return {
x: x / getScreenWidth(),
y: y / getScreenHeight(),
};
}

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { selectTitleForUri, selectClaimIsNsfwForUri, makeSelectClaimWasPurchased } from 'redux/selectors/claims';
import { makeSelectFileInfoForUri, makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
import { selectTitleForUri, makeSelectClaimWasPurchased, selectClaimForUri } from 'redux/selectors/claims';
import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
import {
makeSelectNextUrlForCollectionAndUrl,
makeSelectPreviousUrlForCollectionAndUrl,
@ -14,26 +14,29 @@ import {
} from 'redux/selectors/content';
import { selectClientSetting } from 'redux/selectors/settings';
import { selectCostInfoForUri } from 'lbryinc';
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
import { doUriInitiatePlay, doSetPlayingUri } from 'redux/actions/content';
import { doFetchRecommendedContent } from 'redux/actions/search';
import { doAnaltyicsPurchaseEvent } from 'redux/actions/app';
import { withRouter } from 'react-router';
import { getChannelIdFromClaim } from 'util/claim';
import { selectMobilePlayerDimensions } from 'redux/selectors/app';
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
import { doSetMobilePlayerDimensions } from 'redux/actions/app';
import FileRenderFloating from './view';
const select = (state, props) => {
const { location } = props;
const playingUri = selectPlayingUri(state);
const primaryUri = selectPrimaryUri(state);
const uri = playingUri && playingUri.uri;
const collectionId = playingUri && playingUri.collectionId;
const { uri, collectionId } = playingUri || {};
const claim = selectClaimForUri(state, uri);
return {
uri,
primaryUri,
playingUri,
primaryUri: selectPrimaryUri(state),
title: selectTitleForUri(state, uri),
fileInfo: makeSelectFileInfoForUri(uri)(state),
mature: selectClaimIsNsfwForUri(state, uri),
isFloating: makeSelectIsPlayerFloating(props.location)(state),
isFloating: makeSelectIsPlayerFloating(location)(state),
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
floatingPlayerEnabled: selectClientSetting(state, SETTINGS.FLOATING_PLAYER),
renderMode: makeSelectFileRenderModeForUri(uri)(state),
@ -43,26 +46,17 @@ const select = (state, props) => {
nextListUri: collectionId && makeSelectNextUrlForCollectionAndUrl(collectionId, uri)(state),
previousListUri: collectionId && makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state),
collectionId,
isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri),
channelClaimId: claim && getChannelIdFromClaim(claim),
mobilePlayerDimensions: selectMobilePlayerDimensions(state),
};
};
const perform = (dispatch) => ({
closeFloatingPlayer: () => dispatch(doSetPlayingUri({ uri: null })),
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 }))
),
clearSecondarySource: (uri) => dispatch(doSetPlayingUri({ uri })),
});
const perform = {
doFetchRecommendedContent,
doUriInitiatePlay,
doSetPlayingUri,
doSetMobilePlayerDimensions,
};
export default withRouter(connect(select, perform)(FileRenderFloating));

View file

@ -1,7 +1,7 @@
// @flow
import * as ICONS from 'constants/icons';
import * as RENDER_MODES from 'constants/file_render_modes';
import React, { useEffect, useState } from 'react';
import React from 'react';
import Button from 'component/button';
import classnames from 'classnames';
import LoadingScreen from 'component/common/loading-screen';
@ -11,135 +11,98 @@ import usePersistedState from 'effects/use-persisted-state';
import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
import Draggable from 'react-draggable';
import { onFullscreenChange } from 'util/full-screen';
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
import { generateListSearchUrlParams } from 'util/url';
import { useIsMobile } from 'effects/use-screensize';
import debounce from 'util/debounce';
import { useHistory } from 'react-router';
import { isURIEqual } from 'util/lbryURI';
import AutoplayCountdown from 'component/autoplayCountdown';
import LivestreamIframeRender from 'component/livestreamLayout/iframe-render';
import usePlayNext from 'effects/use-play-next';
import { getScreenWidth, getScreenHeight, clampFloatingPlayerToScreen, calculateRelativePos } from './helper-functions';
// scss/init/vars.scss
// --header-height
const HEADER_HEIGHT = 60;
const HEADER_HEIGHT_MOBILE = 60;
// --header-height-mobile
export const HEADER_HEIGHT_MOBILE = 56;
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 100;
export const INLINE_PLAYER_WRAPPER_CLASS = 'inline-player__wrapper';
function getScreenWidth() {
if (document && document.documentElement) {
return document.documentElement.clientWidth;
} else {
return window.innerWidth;
}
}
function getScreenHeight() {
if (document && document.documentElement) {
return document.documentElement.clientHeight;
} else {
return window.innerHeight;
}
}
function getFloatingPlayerRect() {
// TODO: use 'fileViewerRect'?
const FLOATING_PLAYER_CLASS = 'content__viewer--floating';
const elem = document.querySelector(`.${FLOATING_PLAYER_CLASS}`);
if (elem) {
return elem.getBoundingClientRect();
} else {
return null;
}
}
function clampRectToScreen(x, y, rect) {
if (rect) {
const ESTIMATED_SCROLL_BAR_PX = 50;
const screenW = getScreenWidth();
const screenH = getScreenHeight();
if (x + rect.width > screenW - ESTIMATED_SCROLL_BAR_PX) {
x = screenW - rect.width - ESTIMATED_SCROLL_BAR_PX;
}
if (y + rect.height > screenH) {
y = screenH - rect.height;
}
}
return { x, y };
}
function calculateRelativePos(x, y) {
return {
x: x / getScreenWidth(),
y: y / getScreenHeight(),
};
}
export const FLOATING_PLAYER_CLASS = 'content__viewer--floating';
// ****************************************************************************
// ****************************************************************************
type Props = {
isFloating: boolean,
fileInfo: FileListItem,
mature: boolean,
uri: string,
streamingUrl?: string,
title: ?string,
floatingPlayerEnabled: boolean,
closeFloatingPlayer: () => void,
clearSecondarySource: (string) => void,
renderMode: string,
playingUri: ?PlayingUri,
primaryUri: ?string,
videoTheaterMode: boolean,
doFetchRecommendedContent: (string, boolean) => void,
doPlayUri: (string, string, boolean) => void,
collectionId: string,
costInfo: any,
claimWasPurchased: boolean,
nextListUri: string,
previousListUri: string,
doFetchRecommendedContent: (uri: string) => void,
doUriInitiatePlay: (uri: string, collectionId: ?string, isPlayable: ?boolean, isFloating: ?boolean) => void,
doSetPlayingUri: ({ uri?: ?string }) => void,
// mobile only
isCurrentClaimLive?: boolean,
channelClaimId?: any,
mobilePlayerDimensions?: any,
doSetMobilePlayerDimensions: ({ height?: ?number, width?: ?number }) => void,
};
export default function FileRenderFloating(props: Props) {
const {
fileInfo,
mature,
uri,
streamingUrl,
title,
isFloating,
closeFloatingPlayer,
clearSecondarySource,
floatingPlayerEnabled,
renderMode,
playingUri,
primaryUri,
videoTheaterMode,
doFetchRecommendedContent,
doPlayUri,
collectionId,
costInfo,
claimWasPurchased,
nextListUri,
previousListUri,
doFetchRecommendedContent,
doUriInitiatePlay,
doSetPlayingUri,
// mobile only
isCurrentClaimLive,
channelClaimId,
mobilePlayerDimensions,
doSetMobilePlayerDimensions,
} = props;
const { location, push } = useHistory();
const hideFloatingPlayer = location.state && location.state.hideFloatingPlayer;
const isMobile = useIsMobile();
const {
location: { state },
} = useHistory();
const hideFloatingPlayer = state && state.hideFloatingPlayer;
const playingUriSource = playingUri && playingUri.source;
const isComment = playingUriSource === 'comment';
const isMobile = useIsMobile();
const mainFilePlaying = !isFloating && primaryUri && isURIEqual(uri, primaryUri);
const mainFilePlaying = (!isFloating || !isMobile) && primaryUri && isURIEqual(uri, primaryUri);
const noFloatingPlayer = !isFloating || isMobile || !floatingPlayerEnabled || hideFloatingPlayer;
const [fileViewerRect, setFileViewerRect] = useState();
const [wasDragging, setWasDragging] = useState(false);
const [doNavigate, setDoNavigate] = useState(false);
const [playNextUrl, setPlayNextUrl] = useState(true);
const [countdownCanceled, setCountdownCanceled] = useState(false);
const [fileViewerRect, setFileViewerRect] = React.useState();
const [wasDragging, setWasDragging] = React.useState(false);
const [doNavigate, setDoNavigate] = React.useState(false);
const [shouldPlayNext, setPlayNext] = React.useState(true);
const [countdownCanceled, setCountdownCanceled] = React.useState(false);
const [position, setPosition] = usePersistedState('floating-file-viewer:position', {
x: -25,
y: window.innerHeight - 400,
@ -152,56 +115,12 @@ export default function FileRenderFloating(props: Props) {
const isFree = costInfo && costInfo.cost === 0;
const canViewFile = isFree || claimWasPurchased;
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
const loadingMessage =
fileInfo && fileInfo.blobs_completed >= 1 && (!fileInfo.download_path || !fileInfo.written_bytes)
? __("It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds.")
: __('Loading');
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode) || isCurrentClaimLive;
const isReadyToPlay = isPlayable && streamingUrl;
function restoreToRelativePosition() {
const newX = Math.round(relativePosRef.current.x * getScreenWidth());
const newY = Math.round(relativePosRef.current.y * getScreenHeight());
setPosition(clampRectToScreen(newX, newY, getFloatingPlayerRect()));
}
const clampToScreenOnResize = React.useCallback(
debounce(() => {
restoreToRelativePosition();
}, DEBOUNCE_WINDOW_RESIZE_HANDLER_MS),
[]
);
// ???
useEffect(() => {
if (isFloating) {
// When the player begins floating, remove the comment source
// so that it doesn't try to resize again in case of going back
// to the origin's comment section and fail to position correctly
if (isComment && playingUri) clearSecondarySource(playingUri.uri);
}
}, [isFloating, isComment, clearSecondarySource, playingUri]);
// Initial update for relativePosRef:
useEffect(() => {
relativePosRef.current = calculateRelativePos(position.x, position.y);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Ensure player is within screen when 'isFloating' changes.
useEffect(() => {
if (isFloating) {
restoreToRelativePosition();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFloating]);
// Listen to main-window resizing and adjust the fp position accordingly:
useEffect(() => {
if (isFloating) {
window.addEventListener('resize', clampToScreenOnResize);
return () => window.removeEventListener('resize', clampToScreenOnResize);
}
}, [isFloating, clampToScreenOnResize]);
// ****************************************************************************
// FUNCTIONS
// ****************************************************************************
const handleResize = React.useCallback(() => {
const element = mainFilePlaying
@ -226,69 +145,121 @@ export default function FileRenderFloating(props: Props) {
// $FlowFixMe
setFileViewerRect({ ...objectRect, windowOffset: window.pageYOffset });
}, [mainFilePlaying]);
useEffect(() => {
if (!mobilePlayerDimensions || mobilePlayerDimensions.height !== rect.height) {
doSetMobilePlayerDimensions({ height: rect.height, width: getScreenWidth() });
}
}, [doSetMobilePlayerDimensions, mainFilePlaying, mobilePlayerDimensions]);
const restoreToRelativePosition = React.useCallback(() => {
const SCROLL_BAR_PX = 12; // root: --body-scrollbar-width
const screenW = getScreenWidth() - SCROLL_BAR_PX;
const screenH = getScreenHeight();
const newX = Math.round(relativePosRef.current.x * screenW);
const newY = Math.round(relativePosRef.current.y * screenH);
setPosition(clampFloatingPlayerToScreen(newX, newY));
}, [setPosition]);
const clampToScreenOnResize = React.useCallback(
debounce(restoreToRelativePosition, DEBOUNCE_WINDOW_RESIZE_HANDLER_MS),
[]
);
// For playlists when pressing next/previous etc and switching players
function resetState() {
setCountdownCanceled(false);
setDoNavigate(false);
setPlayNext(true);
}
// ****************************************************************************
// EFFECTS
// ****************************************************************************
usePlayNext(
isFloating,
collectionId,
shouldPlayNext,
nextListUri,
previousListUri,
doNavigate,
doUriInitiatePlay,
resetState
);
React.useEffect(() => {
if (playingUri && (playingUri.primaryUri || playingUri.uri)) {
handleResize();
setCountdownCanceled(false);
}
}, [handleResize, playingUri, videoTheaterMode]);
useEffect(() => {
handleResize();
window.addEventListener('resize', handleResize);
onFullscreenChange(window, 'add', handleResize);
// Listen to main-window resizing and adjust the floating player position accordingly:
React.useEffect(() => {
if (isFloating) {
// Ensure player is within screen when 'isFloating' changes.
restoreToRelativePosition();
} else {
handleResize();
}
function onWindowResize() {
return isFloating ? clampToScreenOnResize() : handleResize();
}
window.addEventListener('resize', onWindowResize);
if (!isFloating) onFullscreenChange(window, 'add', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
onFullscreenChange(window, 'remove', handleResize);
window.removeEventListener('resize', onWindowResize);
if (!isFloating) onFullscreenChange(window, 'remove', handleResize);
};
}, [handleResize]);
useEffect(() => {
if (isFloating) {
doFetchRecommendedContent(uri, mature);
// Only listen to these and avoid infinite loops
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [clampToScreenOnResize, handleResize, isFloating]);
React.useEffect(() => {
// Initial update for relativePosRef:
relativePosRef.current = calculateRelativePos(position.x, position.y);
// only on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (isFloating && isComment) {
// When the player begins floating, remove the comment source
// so that it doesn't try to resize again in case of going back
// to the origin's comment section and fail to position correctly
doSetPlayingUri({ ...playingUri, source: null });
}
}, [doFetchRecommendedContent, isFloating, mature, uri]);
}, [doSetPlayingUri, isComment, isFloating, playingUri]);
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]
);
React.useEffect(() => {
if (isFloating) doFetchRecommendedContent(uri);
}, [doFetchRecommendedContent, isFloating, uri]);
useEffect(() => {
if (!doNavigate) return;
if (playNextUrl && nextListUri) {
doPlay(nextListUri);
} else if (previousListUri) {
doPlay(previousListUri);
React.useEffect(() => {
if (isFloating && isMobile) {
doSetMobilePlayerDimensions({ height: null, width: null });
}
setPlayNextUrl(true);
}, [doNavigate, doPlay, nextListUri, playNextUrl, previousListUri]);
}, [doSetMobilePlayerDimensions, doSetPlayingUri, isFloating, isMobile]);
if (
!isPlayable ||
!uri ||
(isFloating && (isMobile || !floatingPlayerEnabled || hideFloatingPlayer)) ||
(isFloating && noFloatingPlayer) ||
(collectionId && !isFloating && ((!canViewFile && !nextListUri) || countdownCanceled))
) {
return null;
}
// ****************************************************************************
// RENDER
// ****************************************************************************
function handleDragStart() {
// Not really necessary, but reset just in case 'handleStop' didn't fire.
setWasDragging(false);
@ -306,13 +277,13 @@ export default function FileRenderFloating(props: Props) {
}
function handleDragStop(e, ui) {
if (wasDragging) {
setWasDragging(false);
}
if (wasDragging) setWasDragging(false);
const { x, y } = ui;
let newPos = { x, y };
let newPos = { x: ui.x, y: ui.y };
if (newPos.x !== position.x || newPos.y !== position.y) {
newPos = clampRectToScreen(newPos.x, newPos.y, getFloatingPlayerRect());
newPos = clampFloatingPlayerToScreen(newPos.x, newPos.y);
setPosition(newPos);
relativePosRef.current = calculateRelativePos(newPos.x, newPos.y);
}
@ -326,17 +297,18 @@ export default function FileRenderFloating(props: Props) {
defaultPosition={position}
position={isFloating ? position : { x: 0, y: 0 }}
bounds="parent"
disabled={!isFloating}
disabled={noFloatingPlayer}
handle=".draggable"
cancel=".button"
>
<div
className={classnames('content__viewer', {
'content__viewer--floating': isFloating,
[FLOATING_PLAYER_CLASS]: isFloating,
'content__viewer--inline': !isFloating,
'content__viewer--secondary': isComment,
'content__viewer--theater-mode': !isFloating && videoTheaterMode && playingUri?.uri === primaryUri,
'content__viewer--disable-click': wasDragging,
'content__viewer--mobile': isMobile,
})}
style={
!isFloating && fileViewerRect
@ -344,58 +316,52 @@ export default function FileRenderFloating(props: Props) {
width: fileViewerRect.width,
height: fileViewerRect.height,
left: fileViewerRect.x,
top:
fileViewerRect.windowOffset +
fileViewerRect.top -
(isMobile ? HEADER_HEIGHT_MOBILE : HEADER_HEIGHT) -
(IS_DESKTOP_MAC ? 24 : 0),
top: isMobile
? HEADER_HEIGHT_MOBILE
: fileViewerRect.windowOffset + fileViewerRect.top - HEADER_HEIGHT - (IS_DESKTOP_MAC ? 24 : 0),
}
: {}
}
>
<div
className={classnames('content__wrapper', {
'content__wrapper--floating': isFloating,
})}
>
<div className={classnames('content__wrapper', { 'content__wrapper--floating': isFloating })}>
{isFloating && (
<Button
title={__('Close')}
onClick={closeFloatingPlayer}
onClick={() => doSetPlayingUri({ uri: null })}
icon={ICONS.REMOVE}
button="primary"
className="content__floating-close"
/>
)}
{isReadyToPlay ? (
{isCurrentClaimLive && channelClaimId ? (
<LivestreamIframeRender channelClaimId={channelClaimId} showLivestream mobileVersion />
) : isReadyToPlay ? (
<FileRender className="draggable" uri={uri} />
) : collectionId && !canViewFile ? (
<div className="content__loading">
<AutoplayCountdown
nextRecommendedUri={nextListUri}
doNavigate={() => setDoNavigate(true)}
doReplay={() => doUriInitiatePlay(uri, collectionId, false, isFloating)}
doPrevious={() => {
setPlayNext(false);
setDoNavigate(true);
}}
onCanceled={() => setCountdownCanceled(true)}
skipPaid
/>
</div>
) : (
<>
{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} />
)}
</>
<LoadingScreen status={__('Loading')} />
)}
{isFloating && (
<div className="draggable content__info">
<div className="claim-preview__title" title={title || uri}>
<Button label={title || uri} navigate={navigateUrl} button="link" className="content__floating-link" />
</div>
<UriIndicator link uri={uri} />
</div>
)}

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { doUriInitiatePlay, doSetPlayingUri } from 'redux/actions/content';
import { selectThumbnailForUri, selectClaimForUri, makeSelectClaimWasPurchased } from 'redux/selectors/claims';
import { doUriInitiatePlay, doSetPlayingUri, doSetPrimaryUri } from 'redux/actions/content';
import { selectThumbnailForUri, makeSelectClaimWasPurchased } from 'redux/selectors/claims';
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
import * as SETTINGS from 'constants/settings';
import { selectCostInfoForUri } from 'lbryinc';
@ -14,16 +14,11 @@ import {
makeSelectFileRenderModeForUri,
} from 'redux/selectors/content';
import FileRenderInitiator from './view';
import { getChannelIdFromClaim } from 'util/claim';
import { selectActiveLivestreamForChannel } from 'redux/selectors/livestream';
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
const select = (state, props) => {
const { uri } = props;
const claim = selectClaimForUri(state, uri);
const claimId = claim && claim.claim_id;
const channelClaimId = claim && getChannelIdFromClaim(claim);
return {
claimThumbnail: selectThumbnailForUri(state, uri),
fileInfo: makeSelectFileInfoForUri(uri)(state),
@ -35,14 +30,14 @@ const select = (state, props) => {
renderMode: makeSelectFileRenderModeForUri(uri)(state),
claimWasPurchased: makeSelectClaimWasPurchased(uri)(state),
authenticated: selectUserVerifiedEmail(state),
activeLivestreamForChannel: channelClaimId && selectActiveLivestreamForChannel(state, channelClaimId),
claimId,
isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri),
};
};
const perform = {
doUriInitiatePlay,
doSetPlayingUri,
doSetPrimaryUri,
};
export default withRouter(connect(select, perform)(FileRenderInitiator));

View file

@ -31,10 +31,10 @@ type Props = {
claimWasPurchased: boolean,
authenticated: boolean,
videoTheaterMode: boolean,
activeLivestreamForChannel?: any,
claimId?: string,
isCurrentClaimLive?: boolean,
doUriInitiatePlay: (uri: string, collectionId: ?string, isPlayable: boolean) => void,
doSetPlayingUri: ({ uri: ?string }) => void,
doSetPrimaryUri: (uri: ?string) => void,
};
export default function FileRenderInitiator(props: Props) {
@ -53,10 +53,10 @@ export default function FileRenderInitiator(props: Props) {
claimWasPurchased,
authenticated,
videoTheaterMode,
activeLivestreamForChannel,
claimId,
isCurrentClaimLive,
doUriInitiatePlay,
doSetPlayingUri,
doSetPrimaryUri,
} = props;
const containerRef = React.useRef<any>();
@ -76,9 +76,8 @@ export default function FileRenderInitiator(props: Props) {
const isFree = costInfo && costInfo.cost === 0;
const canViewFile = isFree || claimWasPurchased;
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode) || isCurrentClaimLive;
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
const isCurrentClaimLive = activeLivestreamForChannel && claimId && activeLivestreamForChannel.claimId === claimId;
const isMobileClaimLive = isMobile && isCurrentClaimLive;
const foundCover = thumbnail !== FileRenderPlaceholder;
@ -87,12 +86,13 @@ export default function FileRenderInitiator(props: Props) {
const shouldRedirect = !authenticated && !isFree;
React.useEffect(() => {
// Set livestream as playing uri so it can be rendered by <FileRenderMobile />
// Set livestream as playing uri so it can be rendered by <FileRenderFloating /> on mobile
// instead of showing an empty cover image. Needs cover to fill the space with the player.
if (isMobileClaimLive && foundCover) {
doSetPlayingUri({ uri });
doSetPrimaryUri(uri);
}
}, [doSetPlayingUri, foundCover, isMobileClaimLive, uri]);
}, [doSetPlayingUri, doSetPrimaryUri, foundCover, isMobileClaimLive, uri]);
function doAuthRedirect() {
history.push(`/$/${PAGES.AUTH}?redirect=${encodeURIComponent(location.pathname)}`);

View file

@ -1,51 +0,0 @@
import { connect } from 'react-redux';
import { makeSelectClaimWasPurchased, selectClaimForUri } from 'redux/selectors/claims';
import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
import {
makeSelectNextUrlForCollectionAndUrl,
makeSelectPreviousUrlForCollectionAndUrl,
} from 'redux/selectors/collections';
import { selectPlayingUri, makeSelectFileRenderModeForUri, selectPrimaryUri } from 'redux/selectors/content';
import { selectCostInfoForUri } from 'lbryinc';
import { doPlayUri } from 'redux/actions/content';
import { doSetMobilePlayerDimensions } from 'redux/actions/app';
import { withRouter } from 'react-router';
import { getChannelIdFromClaim } from 'util/claim';
import { selectActiveLivestreamForChannel } from 'redux/selectors/livestream';
import FileRenderMobile from './view';
import { selectMobilePlayerDimensions } from 'redux/selectors/app';
const select = (state, props) => {
const playingUri = selectPlayingUri(state);
const primaryUri = selectPrimaryUri(state);
const uri = playingUri && playingUri.uri;
const collectionId = playingUri && playingUri.collectionId;
const claim = selectClaimForUri(state, uri);
const claimId = claim && claim.claim_id;
const channelClaimId = claim && getChannelIdFromClaim(claim);
return {
uri,
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
renderMode: makeSelectFileRenderModeForUri(uri)(state),
costInfo: selectCostInfoForUri(state, uri),
claimWasPurchased: makeSelectClaimWasPurchased(uri)(state),
nextListUri: collectionId && makeSelectNextUrlForCollectionAndUrl(collectionId, uri)(state),
previousListUri: collectionId && makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state),
collectionId,
activeLivestreamForChannel: channelClaimId && selectActiveLivestreamForChannel(state, channelClaimId),
claimId,
channelClaimId,
mobilePlayerDimensions: selectMobilePlayerDimensions(state),
primaryUri,
playingUri,
};
};
const perform = {
doPlayUri,
doSetMobilePlayerDimensions,
};
export default withRouter(connect(select, perform)(FileRenderMobile));

View file

@ -1,194 +0,0 @@
// @flow
import * as RENDER_MODES from 'constants/file_render_modes';
import React, { useEffect, useState } from 'react';
import { onFullscreenChange } from 'util/full-screen';
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
import { useHistory } from 'react-router';
import LoadingScreen from 'component/common/loading-screen';
import FileRender from 'component/fileRender';
import AutoplayCountdown from 'component/autoplayCountdown';
import LivestreamIframeRender from 'component/livestreamLayout/iframe-render';
const PRIMARY_PLAYER_WRAPPER_CLASS = 'file-page__video-container';
export const INLINE_PLAYER_WRAPPER_CLASS = 'inline-player__wrapper';
export const HEADER_HEIGHT_MOBILE = 56;
// ****************************************************************************
// ****************************************************************************
type Props = {
claimId?: string,
uri: string,
streamingUrl?: string,
renderMode: string,
collectionId: string,
costInfo: any,
claimWasPurchased: boolean,
nextListUri: string,
previousListUri: string,
activeLivestreamForChannel?: any,
channelClaimId?: any,
playingUri?: PlayingUri,
primaryUri: ?string,
mobilePlayerDimensions?: any,
doPlayUri: (string) => void,
doSetMobilePlayerDimensions: (height: number, width: number) => void,
};
export default function FileRenderMobile(props: Props) {
const {
claimId,
uri,
streamingUrl,
renderMode,
collectionId,
costInfo,
claimWasPurchased,
nextListUri,
previousListUri,
activeLivestreamForChannel,
channelClaimId,
playingUri,
primaryUri,
mobilePlayerDimensions,
doPlayUri,
doSetMobilePlayerDimensions,
} = props;
const { push } = useHistory();
const [fileViewerRect, setFileViewerRect] = useState();
const [doNavigate, setDoNavigate] = useState(false);
const [playNextUrl, setPlayNextUrl] = useState(true);
const [countdownCanceled, setCountdownCanceled] = useState(false);
const isCurrentClaimLive = activeLivestreamForChannel && activeLivestreamForChannel.claimId === claimId;
const isFree = costInfo && costInfo.cost === 0;
const canViewFile = isFree || claimWasPurchased;
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode) || activeLivestreamForChannel;
const isReadyToPlay = isPlayable && streamingUrl;
const isCurrentMediaPlaying = playingUri && playingUri.uri === uri;
const handleResize = React.useCallback(() => {
const element = document.querySelector(`.${PRIMARY_PLAYER_WRAPPER_CLASS}`);
if (!element) return;
const rect = element.getBoundingClientRect();
// getBoundingClientRect returns a DomRect, not an object
const objectRect = {
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
width: rect.width,
height: rect.height,
// $FlowFixMe
x: rect.x,
};
// $FlowFixMe
setFileViewerRect({ ...objectRect });
if (doSetMobilePlayerDimensions && (!mobilePlayerDimensions || mobilePlayerDimensions.height !== rect.height)) {
doSetMobilePlayerDimensions(rect.height, rect.width);
}
}, [doSetMobilePlayerDimensions, mobilePlayerDimensions]);
// Initial resize, will place the player correctly above the cover when starts playing
// (remember the URI here is from playingUri). The cover then keeps on the page and kind of serves as a placeholder
// for the player size and gives the content layered behind the player a "max scroll height"
useEffect(() => {
if (uri) {
handleResize();
setCountdownCanceled(false);
}
}, [handleResize, uri]);
useEffect(() => {
handleResize();
window.addEventListener('resize', handleResize);
onFullscreenChange(window, 'add', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
onFullscreenChange(window, 'remove', handleResize);
};
}, [handleResize]);
const doPlay = React.useCallback(
(playUri) => {
setDoNavigate(false);
const navigateUrl = formatLbryUrlForWeb(playUri);
push({
pathname: navigateUrl,
search: collectionId && generateListSearchUrlParams(collectionId),
state: { collectionId, forceAutoplay: true, hideFloatingPlayer: true },
});
},
[collectionId, push]
);
React.useEffect(() => {
if (!doNavigate) return;
if (playNextUrl && nextListUri) {
doPlay(nextListUri);
} else if (previousListUri) {
doPlay(previousListUri);
}
setPlayNextUrl(true);
}, [doNavigate, doPlay, nextListUri, playNextUrl, previousListUri]);
if (
!isCurrentMediaPlaying ||
!isPlayable ||
!uri ||
countdownCanceled ||
(!isCurrentClaimLive && primaryUri !== playingUri?.uri) || // No floating player on mobile as of now
(collectionId && !canViewFile && !nextListUri)
) {
return null;
}
return (
<div
className="content__viewer content__viewer--inline content__viewer--mobile"
style={
fileViewerRect
? {
width: fileViewerRect.width,
height: fileViewerRect.height,
left: fileViewerRect.x,
}
: {}
}
>
<div className="content__wrapper">
{isCurrentClaimLive && channelClaimId ? (
<LivestreamIframeRender channelClaimId={channelClaimId} showLivestream mobileVersion />
) : isReadyToPlay ? (
<FileRender uri={uri} />
) : !canViewFile ? (
<div className="content__loading">
<AutoplayCountdown
nextRecommendedUri={nextListUri}
doNavigate={() => setDoNavigate(true)}
doReplay={() => doPlayUri(uri)}
doPrevious={() => {
setPlayNextUrl(false);
setDoNavigate(true);
}}
onCanceled={() => setCountdownCanceled(true)}
skipPaid
/>
</div>
) : (
<LoadingScreen status={__('Loading')} />
)}
</div>
</div>
);
}

View file

@ -6,7 +6,8 @@ import { Global } from '@emotion/react';
// $FlowFixMe
import { grey } from '@mui/material/colors';
import { HEADER_HEIGHT_MOBILE } from 'component/fileRenderMobile/view';
import { HEADER_HEIGHT_MOBILE } from 'component/fileRenderFloating/view';
import { PRIMARY_PLAYER_WRAPPER_CLASS, PRIMARY_IMAGE_WRAPPER_CLASS } from 'page/file/view';
import { SwipeableDrawer as MUIDrawer } from '@mui/material';
import * as ICONS from 'constants/icons';
import * as React from 'react';
@ -31,18 +32,29 @@ export default function SwipeableDrawer(props: Props) {
const [coverHeight, setCoverHeight] = React.useState();
const videoHeight = coverHeight || (mobilePlayerDimensions ? mobilePlayerDimensions.height : 0);
const videoHeight = (mobilePlayerDimensions && mobilePlayerDimensions.height) || coverHeight || 0;
const handleResize = React.useCallback(() => {
const element =
document.querySelector(`.${PRIMARY_IMAGE_WRAPPER_CLASS}`) ||
document.querySelector(`.${PRIMARY_PLAYER_WRAPPER_CLASS}`);
if (!element) return;
const rect = element.getBoundingClientRect();
setCoverHeight(rect.height);
}, []);
React.useEffect(() => {
if (open && !mobilePlayerDimensions) {
const element = document.querySelector(`.file-page__video-container`);
// Drawer will follow the cover image on resize, so it's always visible
if (open && (!mobilePlayerDimensions || !mobilePlayerDimensions.height)) {
handleResize();
if (element) {
const rect = element.getBoundingClientRect();
setCoverHeight(rect.height);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}
}, [coverHeight, mobilePlayerDimensions, open]);
}, [handleResize, mobilePlayerDimensions, open]);
// Reset scroll position when opening: avoid broken position where
// the drawer is lower than the video

View file

@ -0,0 +1,47 @@
// @flow
import React from 'react';
import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url';
import { useHistory } from 'react-router';
// Returns web blob from the streaming url
export default function usePlayNext(
isFloating: boolean,
collectionId: ?string,
shouldPlayNext: boolean,
nextListUri: ?string,
previousListUri: ?string,
doNavigate: boolean,
doUriInitiatePlay: (uri: string, collectionId: ?string, isPlayable: ?boolean, isFloating: ?boolean) => void,
resetState: () => void
) {
const { push } = useHistory();
const doPlay = React.useCallback(
(playUri) => {
if (!isFloating) {
const navigateUrl = formatLbryUrlForWeb(playUri);
push({
pathname: navigateUrl,
search: collectionId && generateListSearchUrlParams(collectionId),
state: { collectionId, forceAutoplay: true, hideFloatingPlayer: true },
});
} else {
doUriInitiatePlay(playUri, collectionId, true, isFloating);
}
resetState();
},
[collectionId, doUriInitiatePlay, isFloating, push, resetState]
);
React.useEffect(() => {
if (!doNavigate) return;
if (shouldPlayNext) {
if (nextListUri) doPlay(nextListUri);
} else {
if (previousListUri) doPlay(previousListUri);
}
}, [doNavigate, doPlay, nextListUri, shouldPlayNext, previousListUri]);
}

View file

@ -16,7 +16,6 @@ import { selectShowMatureContent, selectClientSetting } from 'redux/selectors/se
import { makeSelectFileRenderModeForUri, makeSelectContentPositionForUri } from 'redux/selectors/content';
import { makeSelectCommentsListTitleForUri, selectSettingsByChannelId } from 'redux/selectors/comments';
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
import { doSetMobilePlayerDimensions } from 'redux/actions/app';
import { getChannelIdFromClaim } from 'util/claim';
import FilePage from './view';
@ -53,7 +52,6 @@ const perform = {
doSetContentHistoryItem,
doSetPrimaryUri,
clearPosition,
doSetMobilePlayerDimensions,
};
export default withRouter(connect(select, perform)(FilePage));

View file

@ -23,6 +23,7 @@ const CommentsList = lazyImport(() => import('component/commentsList' /* webpack
const PostViewer = lazyImport(() => import('component/postViewer' /* webpackChunkName: "postViewer" */));
export const PRIMARY_PLAYER_WRAPPER_CLASS = 'file-page__video-container';
export const PRIMARY_IMAGE_WRAPPER_CLASS = 'file-render__img-container';
type Props = {
costInfo: ?{ includesData: boolean, cost: number },
@ -164,7 +165,7 @@ export default function FilePage(props: Props) {
if (renderMode === RENDER_MODES.IMAGE) {
return (
<>
<div className="file-render--img-container">
<div className={PRIMARY_IMAGE_WRAPPER_CLASS}>
<FileRenderInitiator uri={uri} />
<FileRenderInline uri={uri} />
</div>

View file

@ -737,7 +737,7 @@ export function doSetIncognito(incognitoEnabled) {
};
}
export const doSetMobilePlayerDimensions = (height, width) => ({
export const doSetMobilePlayerDimensions = ({ height, width }) => ({
type: ACTIONS.SET_MOBILE_PLAYER_DIMENSIONS,
data: { heightWidth: { height, width } },
});

View file

@ -19,6 +19,7 @@ import Lbry from 'lbry';
import * as SETTINGS from 'constants/settings';
import { selectCostInfoForUri, Lbryio } from 'lbryinc';
import { selectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
const DOWNLOAD_POLL_INTERVAL = 1000;
@ -148,15 +149,18 @@ export function doDownloadUri(uri: string) {
return (dispatch: Dispatch) => dispatch(doPlayUri(uri, false, true, () => dispatch(doAnalyticsView(uri))));
}
export function doUriInitiatePlay(uri: string, collectionId?: string, isPlayable?: boolean) {
return (dispatch: Dispatch) => {
dispatch(doSetPrimaryUri(uri));
export function doUriInitiatePlay(uri: string, collectionId?: string, isPlayable?: boolean, isFloating?: boolean) {
return (dispatch: Dispatch, getState: () => any) => {
const state = getState();
const isLive = selectIsActiveLivestreamForUri(state, uri);
if (!isFloating) dispatch(doSetPrimaryUri(uri));
if (isPlayable) {
dispatch(doSetPlayingUri({ uri, collectionId }));
}
dispatch(doPlayUri(uri, false, true, (fileInfo) => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
if (!isLive) dispatch(doPlayUri(uri, false, true, (fileInfo) => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
};
}

View file

@ -7,6 +7,12 @@ type State = { livestream: any };
const selectState = (state: State) => state.livestream || {};
export const selectFetchingLivestreams = (state: State) => selectState(state).fetchingById;
export const selectViewersById = (state: State) => selectState(state).viewersById;
export const selectActiveLivestreams = (state: State) => selectState(state).activeLivestreams;
export const selectFetchingActiveLivestreams = (state: State) => selectState(state).fetchingActiveLivestreams;
export const selectActiveLivestreamInitialized = (state: State) => selectState(state).activeLivestreamInitialized;
// select non-pending claims without sources for given channel
export const makeSelectLivestreamsForChannelId = (channelId: string) =>
createSelector(selectState, selectMyClaims, (livestreamState, myClaims = []) => {
@ -23,9 +29,6 @@ export const makeSelectLivestreamsForChannelId = (channelId: string) =>
.sort((a, b) => b.timestamp - a.timestamp); // newest first
});
export const selectFetchingLivestreams = (state: State) => selectState(state).fetchingById;
export const selectViewersById = (state: State) => selectState(state).viewersById;
export const makeSelectIsFetchingLivestreams = (channelId: string) =>
createSelector(selectFetchingLivestreams, (fetchingLivestreams) => Boolean(fetchingLivestreams[channelId]));
@ -46,8 +49,6 @@ export const makeSelectPendingLivestreamsForChannelId = (channelId: string) =>
);
});
export const selectActiveLivestreams = (state: State) => selectState(state).activeLivestreams;
export const selectIsActiveLivestreamForUri = createCachedSelector(
(state, uri) => uri,
selectActiveLivestreams,
@ -86,7 +87,3 @@ export const selectActiveLivestreamForChannel = createCachedSelector(
return activeLivestreams[channelId] || null;
}
)((state, channelId) => String(channelId));
export const selectFetchingActiveLivestreams = (state: State) => selectState(state).fetchingActiveLivestreams;
export const selectActiveLivestreamInitialized = (state: State) => selectState(state).activeLivestreamInitialized;

View file

@ -39,7 +39,7 @@
margin-bottom: 0;
height: calc(var(--floating-viewer-height) + var(--floating-viewer-info-height));
overflow: hidden;
left: calc(var(--spacing-l) + var(--spacing-s));
top: 0;
z-index: 9999;
border: 2px solid black;
@ -53,6 +53,7 @@
.video-js--tap-to-unmute {
max-width: calc(var(--floating-viewer-width) - (var(--spacing-xs) * 3) - 42px);
}
.content__floating-close {
visibility: visible;
z-index: 9999;

View file

@ -237,7 +237,7 @@
max-height: none;
}
.file-render--img-container {
.file-render__img-container {
width: 100%;
aspect-ratio: 16 / 9;
}

View file

@ -13921,11 +13921,12 @@ react-dom@^16.8.2:
prop-types "^15.6.2"
scheduler "^0.19.0"
react-draggable@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.3.2.tgz#966ef1d90f2387af3c2d8bd3516f601ea42ca359"
react-draggable@^4.4.4:
version "4.4.4"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.4.tgz#5b26d9996be63d32d285a426f41055de87e59b2f"
integrity sha512-6e0WdcNLwpBx/YIDpoyd2Xb04PB0elrDrulKUgdrIlwuYvxh5Ok9M+F8cljm8kPXXs43PmMzek9RrB1b7mLMqA==
dependencies:
classnames "^2.2.5"
clsx "^1.1.1"
prop-types "^15.6.0"
react-error-overlay@^1.0.9: