Improve livestream claimLink embeds

- Remove embedPlayButton for fileRenderInitiator
- getThumbnailFromClaim from utils function instead of redux
- Improve playingUri
This commit is contained in:
Rafael 2022-03-15 13:28:55 -03:00 committed by Thomas Zarebczan
parent 6dea79819d
commit b096aad70e
35 changed files with 278 additions and 247 deletions

View file

@ -197,7 +197,7 @@ const recsys = {
onPlayerDispose: function (claimId, isEmbedded) {
if (window && window.store) {
const state = window.store.getState();
const playingUri = selectPlayingUri(state);
const { uri: playingUri } = selectPlayingUri(state);
const primaryUri = selectPrimaryUri(state);
const onFilePage = playingUri === primaryUri;
if (!onFilePage || isEmbedded) {
@ -246,9 +246,8 @@ const recsys = {
onNavigate: function () {
if (window && window.store) {
const state = window.store.getState();
const playingUri = selectPlayingUri(state);
const actualPlayingUri = playingUri && playingUri.uri;
const claim = makeSelectClaimForUri(actualPlayingUri)(state);
const { uri: playingUri } = selectPlayingUri(state);
const claim = makeSelectClaimForUri(playingUri)(state);
const playingClaimId = claim ? claim.claim_id : null;
// const primaryUri = selectPrimaryUri(state);
const floatingPlayer = selectClientSetting(state, SETTINGS.FLOATING_PLAYER);

View file

@ -1,9 +1,10 @@
// @flow
declare type PlayingUri = {
uri: string,
primaryUri: string,
pathname: string,
uri?: ?string,
primaryUri?: string,
pathname?: string,
commentId?: string,
collectionId?: ?string,
source?: string,
};

View file

@ -94,6 +94,8 @@ type Props = {
fetchModAmIList: () => void,
};
export const SocketContext = React.createContext<any>();
function App(props: Props) {
const {
theme,
@ -137,6 +139,8 @@ function App(props: Props) {
const previousHasVerifiedEmail = usePrevious(hasVerifiedEmail);
const previousRewardApproved = usePrevious(isRewardApproved);
const [socketConnected, setSocketConnection] = React.useState(false);
const [gdprRequired, setGdprRequired] = usePersistedState('gdprRequired');
const [localeLangs, setLocaleLangs] = React.useState();
const [localeSwitchDismissed] = usePersistedState('locale-switch-dismissed', false);
@ -563,10 +567,14 @@ function App(props: Props) {
/>
) : (
<React.Fragment>
<SocketContext.Provider value={socketConnected}>
<Router />
</SocketContext.Provider>
<ModalRouter />
<React.Suspense fallback={null}>{renderFiledrop && <FileDrop />}</React.Suspense>
<FileRenderFloating />
<FileRenderFloating setSocketConnection={setSocketConnection} />
<React.Suspense fallback={null}>
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri, selectIsUriResolving } from 'redux/selectors/claims';
import { selectClaimForUri, selectIsUriResolving } from 'redux/selectors/claims';
import { doResolveUri } from 'redux/actions/claims';
import { doSetPlayingUri } from 'redux/actions/content';
import { punctuationMarks } from 'util/remark-lbry';
@ -17,7 +17,7 @@ const select = (state, props) => {
function getValidClaim(testUri) {
if (testUri.replace('lbry://', '').length <= 1) return;
claim = makeSelectClaimForUri(testUri)(state);
claim = selectClaimForUri(state, testUri);
if (claim === null && punctuationMarks.includes(testUri.charAt(testUri.length - 1))) {
getValidClaim(testUri.substring(0, testUri.length - 1));
} else {

View file

@ -1,10 +1,9 @@
// @flow
import { INLINE_PLAYER_WRAPPER_CLASS } from 'component/fileRenderFloating/view';
import { SIMPLE_SITE } from 'config';
import * as React from 'react';
import Button from 'component/button';
import classnames from 'classnames';
import EmbedPlayButton from 'component/embedPlayButton';
import FileRenderInitiator from 'component/fileRenderInitiator';
import UriIndicator from 'component/uriIndicator';
type Props = {
@ -15,7 +14,7 @@ type Props = {
description: ?string,
isResolvingUri: boolean,
doResolveUri: (string, boolean) => void,
playingUri: ?PlayingUri,
playingUri: PlayingUri,
parentCommentId?: string,
isMarkdownPost?: boolean,
allowPreview: boolean,
@ -61,7 +60,6 @@ class ClaimLink extends React.Component<Props> {
} = this.props;
const isUnresolved = (!isResolvingUri && !claim) || !claim;
const isPlayingInline =
playingUri &&
playingUri.uri === uri &&
((playingUri.source === 'comment' && parentCommentId === playingUri.commentId) ||
playingUri.source === 'markdown');
@ -73,26 +71,30 @@ class ClaimLink extends React.Component<Props> {
const { value_type: valueType } = claim;
const isChannel = valueType === 'channel';
return isChannel ? (
if (isChannel) {
return (
<>
<UriIndicator uri={uri} link showAtSign />
<span>{fullUri.length > uri.length ? fullUri.substring(uri.length, fullUri.length) : ''}</span>
</>
) : allowPreview ? (
);
}
if (allowPreview) {
return (
<div className={classnames('claim-link')}>
<div
className={classnames({
[INLINE_PLAYER_WRAPPER_CLASS]: isPlayingInline,
})}
>
<EmbedPlayButton uri={uri} parentCommentId={parentCommentId} isMarkdownPost={isMarkdownPost} />
<div className={isPlayingInline ? INLINE_PLAYER_WRAPPER_CLASS : undefined}>
<FileRenderInitiator uri={uri} parentCommentId={parentCommentId} isMarkdownPost={isMarkdownPost} embedded />
</div>
<Button button="link" className="preview-link__url" label={uri} navigate={uri} />
</div>
) : (
);
}
return (
<Button
button="link"
title={SIMPLE_SITE ? __("This channel isn't staking enough Credits for link previews.") : children}
title={__("This channel isn't staking enough Credits for link previews.")}
label={children}
className="button--external-link"
navigate={uri}

View file

@ -2,7 +2,6 @@ import { connect } from 'react-redux';
import {
makeSelectClaimForUri,
selectIsUriResolving,
getThumbnailFromClaim,
selectTitleForUri,
selectDateForUri,
} from 'redux/selectors/claims';
@ -11,7 +10,7 @@ import { doResolveUri } from 'redux/actions/claims';
import { selectViewCountForUri, selectBanStateForUri } from 'lbryinc';
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { isClaimNsfw, isStreamPlaceholderClaim } from 'util/claim';
import { isClaimNsfw, isStreamPlaceholderClaim, getThumbnailFromClaim } from 'util/claim';
import ClaimPreviewTile from './view';
import formatMediaDuration from 'util/formatMediaDuration';

View file

@ -12,10 +12,9 @@ import { doToggleLoopList, doToggleShuffleList } from 'redux/actions/content';
import { doCollectionEdit } from 'redux/actions/collections';
const select = (state, props) => {
const playingUri = selectPlayingUri(state);
const playingUrl = playingUri && playingUri.uri;
const claim = selectClaimForUri(state, playingUrl);
const url = claim && claim.permanent_url;
const { uri: playingUri } = selectPlayingUri(state);
const { permanent_url: url } = selectClaimForUri(state, playingUri) || {};
const loopList = selectListLoop(state);
const loop = loopList && loopList.collectionId === props.id && loopList.loop;
const shuffleList = selectListShuffle(state);

View file

@ -1,7 +1,6 @@
import { connect } from 'react-redux';
import {
selectIsUriResolving,
getThumbnailFromClaim,
selectTitleForUri,
makeSelectChannelForClaimUri,
selectClaimIsNsfwForUri,
@ -21,6 +20,7 @@ import { doResolveUri } from 'redux/actions/claims';
import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { getThumbnailFromClaim } from 'util/claim';
import CollectionPreviewTile from './view';
const select = (state, props) => {

View file

@ -60,7 +60,7 @@ type Props = {
},
commentIdentityChannel: any,
activeChannelClaim: ?ChannelClaim,
playingUri: ?PlayingUri,
playingUri: PlayingUri,
stakedLevel: number,
supportDisabled: boolean,
setQuickReply: (any) => void,
@ -186,7 +186,7 @@ function CommentView(props: Props) {
}
function handleEditComment() {
if (playingUri && playingUri.source === 'comment') {
if (playingUri.source === 'comment') {
clearPlayingUri();
}
setEditing(true);
@ -259,7 +259,13 @@ function CommentView(props: Props) {
>
<div className="comment__thumbnail-wrapper">
{authorUri ? (
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} xsmall className="comment__author-thumbnail" checkMembership={false} />
<ChannelThumbnail
uri={authorUri}
obscure={channelIsBlocked}
xsmall
className="comment__author-thumbnail"
checkMembership={false}
/>
) : (
<ChannelThumbnail xsmall className="comment__author-thumbnail" checkMembership={false} />
)}

View file

@ -26,7 +26,7 @@ type Props = {
claim: ?Claim,
claimIsMine: boolean,
activeChannelClaim: ?ChannelClaim,
playingUri: ?PlayingUri,
playingUri: PlayingUri,
moderationDelegatorsById: { [string]: { global: boolean, delegators: { name: string, claimId: string } } },
// --- perform ---
doToast: ({ message: string }) => void,
@ -87,7 +87,7 @@ function CommentMenuList(props: Props) {
Object.values(activeModeratorInfo.delegators).includes(contentChannelClaim.claim_id);
function handleDeleteComment() {
if (playingUri && playingUri.source === 'comment') {
if (playingUri.source === 'comment') {
clearPlayingUri();
}

View file

@ -1,27 +0,0 @@
import { connect } from 'react-redux';
import { selectThumbnailForUri, makeSelectClaimForUri } from 'redux/selectors/claims';
import { doResolveUri } from 'redux/actions/claims';
import * as SETTINGS from 'constants/settings';
import { doFetchCostInfoForUri, selectCostInfoForUri } from 'lbryinc';
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
import { doAnaltyicsPurchaseEvent } from 'redux/actions/app';
import { selectClientSetting } from 'redux/selectors/settings';
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
import ChannelThumbnail from './view';
const select = (state, props) => ({
thumbnail: selectThumbnailForUri(state, props.uri),
claim: makeSelectClaimForUri(props.uri)(state),
floatingPlayerEnabled: selectClientSetting(state, SETTINGS.FLOATING_PLAYER),
costInfo: selectCostInfoForUri(state, props.uri),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
});
export default connect(select, {
doResolveUri,
doFetchCostInfoForUri,
doPlayUri,
doSetPlayingUri,
doAnaltyicsPurchaseEvent,
})(ChannelThumbnail);

View file

@ -1,108 +0,0 @@
// @flow
import * as RENDER_MODES from 'constants/file_render_modes';
import React, { useEffect } from 'react';
import classnames from 'classnames';
import Button from 'component/button';
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
import { useHistory } from 'react-router-dom';
import { useIsMobile } from 'effects/use-screensize';
import { formatLbryUrlForWeb } from 'util/url';
type Props = {
uri: string,
thumbnail: string,
claim: ?Claim,
doResolveUri: (string) => void,
doFetchCostInfoForUri: (string) => void,
costInfo: ?{ cost: number },
floatingPlayerEnabled: boolean,
doPlayUri: (string, ?boolean, ?boolean, (GetResponse) => void) => void,
doAnaltyicsPurchaseEvent: (GetResponse) => void,
parentCommentId?: string,
isMarkdownPost: boolean,
doSetPlayingUri: ({}) => void,
renderMode: string,
};
export default function EmbedPlayButton(props: Props) {
const {
uri,
thumbnail = '',
claim,
doResolveUri,
doFetchCostInfoForUri,
floatingPlayerEnabled,
doPlayUri,
doSetPlayingUri,
doAnaltyicsPurchaseEvent,
costInfo,
parentCommentId,
isMarkdownPost,
renderMode,
} = props;
const {
push,
location: { pathname },
} = useHistory();
const isMobile = useIsMobile();
const hasResolvedUri = claim !== undefined;
const hasCostInfo = costInfo !== undefined;
const disabled = !hasResolvedUri || !costInfo;
const canPlayInline = [RENDER_MODES.AUDIO, RENDER_MODES.VIDEO].includes(renderMode);
useEffect(() => {
if (!hasResolvedUri) {
doResolveUri(uri);
}
if (!hasCostInfo) {
doFetchCostInfoForUri(uri);
}
}, [uri, doResolveUri, doFetchCostInfoForUri, hasCostInfo, hasResolvedUri]);
function handleClick() {
if (disabled) {
return;
}
if (isMobile || !floatingPlayerEnabled || !canPlayInline) {
const formattedUrl = formatLbryUrlForWeb(uri);
push(formattedUrl);
} else {
doPlayUri(uri, undefined, undefined, (fileInfo) => {
let playingOptions = { uri, pathname, source: undefined, commentId: undefined };
if (parentCommentId) {
playingOptions.source = 'comment';
playingOptions.commentId = parentCommentId;
} else if (isMarkdownPost) {
playingOptions.source = 'markdown';
}
doSetPlayingUri(playingOptions);
doAnaltyicsPurchaseEvent(fileInfo);
});
}
}
return (
<div
disabled={disabled}
role="button"
className="embed__inline-button"
onClick={handleClick}
style={{ backgroundImage: `url('${thumbnail.replace(/'/g, "\\'")}')` }}
>
<FileViewerEmbeddedTitle uri={uri} isInApp />
<Button
onClick={handleClick}
iconSize={30}
title={__('Play')}
className={classnames('button--icon', {
'button--play': canPlayInline,
'button--view': !canPlayInline,
})}
disabled={disabled}
/>
</div>
);
}

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { selectTitleForUri, makeSelectClaimWasPurchased } from 'redux/selectors/claims';
import { selectClaimForUri, selectTitleForUri, makeSelectClaimWasPurchased } from 'redux/selectors/claims';
import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
import {
makeSelectNextUrlForCollectionAndUrl,
@ -18,8 +18,10 @@ import { doUriInitiatePlay, doSetPlayingUri } from 'redux/actions/content';
import { doFetchRecommendedContent } from 'redux/actions/search';
import { withRouter } from 'react-router';
import { selectMobilePlayerDimensions } from 'redux/selectors/app';
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
import { selectIsActiveLivestreamForUri, selectCommentSocketConnected } from 'redux/selectors/livestream';
import { doSetMobilePlayerDimensions } from 'redux/actions/app';
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
import { doSetSocketConnected } from 'redux/actions/livestream';
import FileRenderFloating from './view';
const select = (state, props) => {
@ -28,7 +30,13 @@ const select = (state, props) => {
const playingUri = selectPlayingUri(state);
const { uri, collectionId } = playingUri || {};
const claim = uri && selectClaimForUri(state, uri);
const { claim_id: claimId, signing_channel: channelClaim } = claim || {};
const { canonical_url: channelClaimUrl } = channelClaim || {};
return {
claimId,
channelClaimUrl,
uri,
playingUri,
primaryUri: selectPrimaryUri(state),
@ -45,6 +53,7 @@ const select = (state, props) => {
collectionId,
isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri),
mobilePlayerDimensions: selectMobilePlayerDimensions(state),
socketConnected: selectCommentSocketConnected(state),
};
};
@ -53,6 +62,9 @@ const perform = {
doUriInitiatePlay,
doSetPlayingUri,
doSetMobilePlayerDimensions,
doCommentSocketConnect,
doCommentSocketDisconnect,
doSetSocketConnected,
};
export default withRouter(connect(select, perform)(FileRenderFloating));

View file

@ -11,7 +11,7 @@ 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 } from 'util/url';
import { generateListSearchUrlParams, formatLbryChannelName } from 'util/url';
import { useIsMobile } from 'effects/use-screensize';
import debounce from 'util/debounce';
import { useHistory } from 'react-router';
@ -35,13 +35,15 @@ export const FLOATING_PLAYER_CLASS = 'content__viewer--floating';
// ****************************************************************************
type Props = {
claimId: ?string,
channelClaimUrl: ?string,
isFloating: boolean,
uri: string,
streamingUrl?: string,
title: ?string,
floatingPlayerEnabled: boolean,
renderMode: string,
playingUri: ?PlayingUri,
playingUri: PlayingUri,
primaryUri: ?string,
videoTheaterMode: boolean,
collectionId: string,
@ -50,15 +52,21 @@ type Props = {
nextListUri: string,
previousListUri: string,
doFetchRecommendedContent: (uri: string) => void,
doUriInitiatePlay: (uri: string, collectionId: ?string, isPlayable: ?boolean, isFloating: ?boolean) => void,
doUriInitiatePlay: (playingOptions: PlayingUri, isPlayable: ?boolean, isFloating: ?boolean) => void,
doSetPlayingUri: ({ uri?: ?string }) => void,
isCurrentClaimLive?: boolean,
mobilePlayerDimensions?: any,
socketConnected: boolean,
doSetMobilePlayerDimensions: ({ height?: ?number, width?: ?number }) => void,
doCommentSocketConnect: (string, string, string) => void,
doCommentSocketDisconnect: (string, string) => void,
doSetSocketConnected: (connected: boolean) => void,
};
export default function FileRenderFloating(props: Props) {
const {
claimId,
channelClaimUrl,
uri,
streamingUrl,
title,
@ -73,12 +81,16 @@ export default function FileRenderFloating(props: Props) {
claimWasPurchased,
nextListUri,
previousListUri,
socketConnected,
doFetchRecommendedContent,
doUriInitiatePlay,
doSetPlayingUri,
isCurrentClaimLive,
mobilePlayerDimensions,
doSetMobilePlayerDimensions,
doCommentSocketConnect,
doCommentSocketDisconnect,
doSetSocketConnected,
} = props;
const isMobile = useIsMobile();
@ -88,7 +100,7 @@ export default function FileRenderFloating(props: Props) {
} = useHistory();
const hideFloatingPlayer = state && state.hideFloatingPlayer;
const playingUriSource = playingUri && playingUri.source;
const { uri: playingUrl, source: playingUriSource, primaryUri: playingPrimaryUri } = playingUri;
const isComment = playingUriSource === 'comment';
const mainFilePlaying = (!isFloating || !isMobile) && primaryUri && isURIEqual(uri, primaryUri);
const noFloatingPlayer = !isFloating || isMobile || !floatingPlayerEnabled || hideFloatingPlayer;
@ -105,8 +117,7 @@ export default function FileRenderFloating(props: Props) {
const relativePosRef = React.useRef({ x: 0, y: 0 });
const navigateUrl =
playingUri &&
(playingUri.primaryUri || playingUri.uri) + (collectionId ? generateListSearchUrlParams(collectionId) : '');
(playingPrimaryUri || playingUrl || '') + (collectionId ? generateListSearchUrlParams(collectionId) : '');
const isFree = costInfo && costInfo.cost === 0;
const canViewFile = isFree || claimWasPurchased;
@ -184,11 +195,37 @@ export default function FileRenderFloating(props: Props) {
resetState
);
// Establish web socket connection for viewer count.
React.useEffect(() => {
if (playingUri && (playingUri.primaryUri || playingUri.uri)) {
if (!claimId || !channelClaimUrl || !isCurrentClaimLive || socketConnected) return;
const channelName = formatLbryChannelName(channelClaimUrl);
doCommentSocketConnect(uri, channelName, claimId);
doSetSocketConnected(true);
return () => {
doCommentSocketDisconnect(claimId, channelName);
doSetSocketConnected(false);
};
// only listen to socketConnected on initial mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
channelClaimUrl,
claimId,
doCommentSocketConnect,
doCommentSocketDisconnect,
doSetSocketConnected,
isCurrentClaimLive,
uri,
]);
React.useEffect(() => {
if (playingPrimaryUri || playingUrl) {
handleResize();
}
}, [handleResize, playingUri, videoTheaterMode]);
}, [handleResize, playingPrimaryUri, videoTheaterMode, playingUrl]);
// Listen to main-window resizing and adjust the floating player position accordingly:
React.useEffect(() => {
@ -336,7 +373,7 @@ export default function FileRenderFloating(props: Props) {
<AutoplayCountdown
nextRecommendedUri={nextListUri}
doNavigate={() => setDoNavigate(true)}
doReplay={() => doUriInitiatePlay(uri, collectionId, false, isFloating)}
doReplay={() => doUriInitiatePlay({ uri, collectionId }, false, isFloating)}
doPrevious={() => {
setPlayNext(false);
setDoNavigate(true);

View file

@ -1,10 +1,6 @@
import { connect } from 'react-redux';
import { doUriInitiatePlay } from 'redux/actions/content';
import {
selectThumbnailForUri,
makeSelectClaimWasPurchased,
selectIsLivestreamClaimForUri,
} from 'redux/selectors/claims';
import { makeSelectClaimWasPurchased, selectClaimForUri } from 'redux/selectors/claims';
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
import * as SETTINGS from 'constants/settings';
import { selectCostInfoForUri } from 'lbryinc';
@ -19,12 +15,20 @@ import {
} from 'redux/selectors/content';
import FileRenderInitiator from './view';
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
import { getThumbnailFromClaim, isStreamPlaceholderClaim } from 'util/claim';
import { doFetchChannelLiveStatus } from 'redux/actions/livestream';
const select = (state, props) => {
const { uri } = props;
const claim = selectClaimForUri(state, uri);
const { claim_id: claimId, signing_channel: channelClaim } = claim || {};
const { claim_id: channelClaimId } = channelClaim || {};
return {
claimThumbnail: selectThumbnailForUri(state, uri),
claimId,
channelClaimId,
claimThumbnail: getThumbnailFromClaim(claim),
fileInfo: makeSelectFileInfoForUri(uri)(state),
obscurePreview: selectShouldObscurePreviewForUri(state, uri),
isPlaying: makeSelectIsPlaying(uri)(state),
@ -35,12 +39,13 @@ const select = (state, props) => {
claimWasPurchased: makeSelectClaimWasPurchased(uri)(state),
authenticated: selectUserVerifiedEmail(state),
isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri),
isLivestreamClaim: selectIsLivestreamClaimForUri(state, uri),
isLivestreamClaim: isStreamPlaceholderClaim(claim),
};
};
const perform = {
doUriInitiatePlay,
doFetchChannelLiveStatus,
};
export default withRouter(connect(select, perform)(FileRenderInitiator));

View file

@ -15,8 +15,12 @@ import Nag from 'component/common/nag';
import FileRenderPlaceholder from 'static/img/fileRenderPlaceholder.png';
import * as COLLECTIONS_CONSTS from 'constants/collections';
import { LayoutRenderContext } from 'page/livestream/view';
import { formatLbryUrlForWeb } from 'util/url';
import { LIVESTREAM_STATUS_CHECK_INTERVAL } from 'constants/livestream';
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
type Props = {
channelClaimId: ?string,
isPlaying: boolean,
fileInfo: FileListItem,
uri: string,
@ -33,13 +37,18 @@ type Props = {
authenticated: boolean,
videoTheaterMode: boolean,
isCurrentClaimLive?: boolean,
doUriInitiatePlay: (uri: string, collectionId: ?string, isPlayable: boolean) => void,
isLivestreamClaim: boolean,
customAction?: any,
embedded?: boolean,
parentCommentId?: string,
isMarkdownPost?: boolean,
doUriInitiatePlay: (playingOptions: PlayingUri, isPlayable: boolean) => void,
doFetchChannelLiveStatus: (string) => void,
};
export default function FileRenderInitiator(props: Props) {
const {
channelClaimId,
isPlaying,
fileInfo,
uri,
@ -57,7 +66,11 @@ export default function FileRenderInitiator(props: Props) {
isCurrentClaimLive,
isLivestreamClaim,
customAction,
embedded,
parentCommentId,
isMarkdownPost,
doUriInitiatePlay,
doFetchChannelLiveStatus,
} = props;
const layountRendered = React.useContext(LayoutRenderContext);
@ -67,14 +80,15 @@ export default function FileRenderInitiator(props: Props) {
const containerRef = React.useRef<any>();
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
const { search, href, state: locationState } = location;
const { search, href, state: locationState, pathname } = location;
const urlParams = search && new URLSearchParams(search);
const collectionId = urlParams && urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
// check if there is a time or autoplay parameter, if so force autoplay
const urlTimeParam = href && href.indexOf('t=') > -1;
const forceAutoplayParam = locationState && locationState.forceAutoplay;
const shouldAutoplay = forceAutoplayParam || urlTimeParam || autoplay;
const shouldAutoplay = !embedded && (forceAutoplayParam || urlTimeParam || autoplay);
const isFree = costInfo && costInfo.cost === 0;
const canViewFile = isLivestreamClaim
? (layountRendered || isMobile) && isCurrentClaimLive
@ -90,9 +104,20 @@ export default function FileRenderInitiator(props: Props) {
const shouldRedirect = !authenticated && !isFree;
function doAuthRedirect() {
history.push(`/$/${PAGES.AUTH}?redirect=${encodeURIComponent(location.pathname)}`);
history.push(`/$/${PAGES.AUTH}?redirect=${encodeURIComponent(pathname)}`);
}
// Find out current channels status + active live claim
React.useEffect(() => {
if (!channelClaimId || !isLivestreamClaim) return;
const fetch = () => doFetchChannelLiveStatus(channelClaimId);
const intervalId = setInterval(fetch, LIVESTREAM_STATUS_CHECK_INTERVAL);
return () => clearInterval(intervalId);
}, [channelClaimId, doFetchChannelLiveStatus, isLivestreamClaim]);
React.useEffect(() => {
if (!claimThumbnail) return;
@ -114,11 +139,29 @@ export default function FileRenderInitiator(props: Props) {
}, 200);
}, [claimThumbnail, thumbnail]);
function handleClick() {
if (embedded && !isPlayable) {
const formattedUrl = formatLbryUrlForWeb(uri);
history.push(formattedUrl);
} else {
viewFile();
}
}
// Wrap this in useCallback because we need to use it to the view effect
// If we don't a new instance will be created for every render and react will think the dependencies have changed, which will add/remove the listener for every render
const viewFile = React.useCallback(() => {
doUriInitiatePlay(uri, collectionId, isPlayable);
}, [collectionId, doUriInitiatePlay, isPlayable, uri]);
const playingOptions = { uri, collectionId, pathname, source: undefined, commentId: undefined };
if (parentCommentId) {
playingOptions.source = 'comment';
playingOptions.commentId = parentCommentId;
} else if (isMarkdownPost) {
playingOptions.source = 'markdown';
}
doUriInitiatePlay(playingOptions, isPlayable);
}, [collectionId, doUriInitiatePlay, isMarkdownPost, isPlayable, parentCommentId, pathname, uri]);
React.useEffect(() => {
const videoOnPage = document.querySelector('video');
@ -143,15 +186,21 @@ export default function FileRenderInitiator(props: Props) {
return (
<div
ref={containerRef}
onClick={disabled ? undefined : shouldRedirect ? doAuthRedirect : viewFile}
onClick={disabled ? undefined : shouldRedirect ? doAuthRedirect : handleClick}
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
className={classnames('content__cover', {
className={
embedded
? 'embed__inline-button'
: classnames('content__cover', {
'content__cover--disabled': disabled,
'content__cover--theater-mode': videoTheaterMode && !isMobile,
'content__cover--text': isText,
'card__media--nsfw': obscurePreview,
})}
})
}
>
{embedded && <FileViewerEmbeddedTitle uri={uri} isInApp />}
{renderUnsupported ? (
<Nag
type="helpful"
@ -173,10 +222,10 @@ export default function FileRenderInitiator(props: Props) {
)
)}
{!disabled && (
{(!disabled || (embedded && isLivestreamClaim)) && (
<Button
requiresAuth={shouldRedirect}
onClick={viewFile}
onClick={handleClick}
iconSize={30}
title={isPlayable ? __('Play') : __('View')}
className={classnames('button--icon', {

View file

@ -2,13 +2,13 @@ import { connect } from 'react-redux';
import {
selectClaimIsMine,
selectTitleForUri,
getThumbnailFromClaim,
selectClaimForUri,
selectIsUriResolving,
makeSelectMetadataItemForUri,
} from 'redux/selectors/claims';
import { doResolveUri } from 'redux/actions/claims';
import { selectBlackListedOutpoints } from 'lbryinc';
import { getThumbnailFromClaim } from 'util/claim';
import PreviewLink from './view';
const select = (state, props) => {

View file

@ -39,8 +39,8 @@ const select = (state, props) => {
const userId = selectUser(state) && selectUser(state).id;
const internalFeature = selectUser(state) && selectUser(state).internal_feature;
const playingUri = selectPlayingUri(state);
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID) || (playingUri && playingUri.collectionId);
const isMarkdownOrComment = playingUri && (playingUri.source === 'markdown' || playingUri.source === 'comment');
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID) || playingUri.collectionId;
const isMarkdownOrComment = playingUri.source === 'markdown' || playingUri.source === 'comment';
let nextRecommendedUri;
let previousListUri;

View file

@ -426,6 +426,7 @@ export const COMMENT_FETCH_SETTINGS_COMPLETED = 'COMMENT_FETCH_SETTINGS_COMPLETE
export const COMMENT_FETCH_BLOCKED_WORDS_STARTED = 'COMMENT_FETCH_BLOCKED_WORDS_STARTED';
export const COMMENT_FETCH_BLOCKED_WORDS_FAILED = 'COMMENT_FETCH_BLOCKED_WORDS_FAILED';
export const COMMENT_FETCH_BLOCKED_WORDS_COMPLETED = 'COMMENT_FETCH_BLOCKED_WORDS_COMPLETED';
export const COMMENT_SOCKET_CONNECTED = 'COMMENT_SOCKET_CONNECTED';
export const COMMENT_RECEIVED = 'COMMENT_RECEIVED';
export const COMMENT_SUPER_CHAT_LIST_STARTED = 'COMMENT_SUPER_CHAT_LIST_STARTED';
export const COMMENT_SUPER_CHAT_LIST_COMPLETED = 'COMMENT_SUPER_CHAT_LIST_COMPLETED';

View file

@ -15,6 +15,7 @@ export const LIVESTREAM_KILL = 'https://api.stream.odysee.com/stream/kill';
export const MAX_LIVESTREAM_COMMENTS = 50;
export const LIVESTREAM_STATUS_CHECK_INTERVAL = 30 * 1000;
export const LIVESTREAM_STARTS_SOON_BUFFER = 15;
export const LIVESTREAM_STARTED_RECENTLY_BUFFER = 15;
export const LIVESTREAM_UPCOMING_BUFFER = 35;

View file

@ -11,7 +11,7 @@ export default function usePlayNext(
nextListUri: ?string,
previousListUri: ?string,
doNavigate: boolean,
doUriInitiatePlay: (uri: string, collectionId: ?string, isPlayable: ?boolean, isFloating: ?boolean) => void,
doUriInitiatePlay: (playingOptions: PlayingUri, isPlayable: ?boolean, isFloating: ?boolean) => void,
resetState: () => void
) {
const { push } = useHistory();
@ -27,7 +27,7 @@ export default function usePlayNext(
state: { collectionId, forceAutoplay: true, hideFloatingPlayer: true },
});
} else {
doUriInitiatePlay(playUri, collectionId, true, isFloating);
doUriInitiatePlay({ uri: playUri, collectionId }, true, isFloating);
}
resetState();

View file

@ -18,7 +18,7 @@ type Props = {
cancelPurchase: () => void,
metadata: StreamMetadata,
analyticsPurchaseEvent: (GetResponse) => void,
playingUri: ?PlayingUri,
playingUri: PlayingUri,
setPlayingUri: (?string) => void,
};
@ -43,14 +43,14 @@ function ModalAffirmPurchase(props: Props) {
setSuccess(true);
analyticsPurchaseEvent(fileInfo);
if (!playingUri || playingUri.uri !== uri) {
if (playingUri.uri !== uri) {
setPlayingUri(uri);
}
});
}
function cancelPurchase() {
if (playingUri && isURIEqual(uri, playingUri.uri)) {
if (playingUri.uri && isURIEqual(uri, playingUri.uri)) {
setPlayingUri(null);
}

View file

@ -2,7 +2,6 @@ import { connect } from 'react-redux';
import {
selectClaimIsMine,
selectTitleForUri,
getThumbnailFromClaim,
makeSelectCoverForUri,
selectCurrentChannelPage,
selectClaimForUri,
@ -16,6 +15,7 @@ import { selectModerationBlockList } from 'redux/selectors/comments';
import { selectMutedChannels } from 'redux/selectors/blocked';
import { doOpenModal } from 'redux/actions/app';
import { selectLanguage } from 'redux/selectors/settings';
import { getThumbnailFromClaim } from 'util/claim';
import ChannelPage from './view';
const select = (state, props) => {

View file

@ -4,7 +4,6 @@ import { withRouter } from 'react-router-dom';
import CollectionPage from './view';
import {
selectTitleForUri,
getThumbnailFromClaim,
selectClaimIsMine,
makeSelectClaimIsPending,
makeSelectClaimForClaimId,
@ -20,6 +19,7 @@ import {
makeSelectEditedCollectionForId,
} from 'redux/selectors/collections';
import { getThumbnailFromClaim } from 'util/claim';
import { doFetchItemsInCollection, doCollectionDelete, doCollectionEdit } from 'redux/actions/collections';
import { selectUser } from 'redux/selectors/user';

View file

@ -6,8 +6,12 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
import { getChannelIdFromClaim } from 'util/claim';
import { selectActiveLivestreamForChannel, selectActiveLivestreamInitialized } from 'redux/selectors/livestream';
import { doFetchChannelLiveStatus } from 'redux/actions/livestream';
import {
selectActiveLivestreamForChannel,
selectActiveLivestreamInitialized,
selectCommentSocketConnected,
} from 'redux/selectors/livestream';
import { doFetchChannelLiveStatus, doSetSocketConnected } from 'redux/actions/livestream';
import LivestreamPage from './view';
const select = (state, props) => {
@ -20,6 +24,7 @@ const select = (state, props) => {
chatDisabled: makeSelectTagInClaimOrChannelForUri(uri, DISABLE_COMMENTS_TAG)(state),
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelClaimId),
activeLivestreamInitialized: selectActiveLivestreamInitialized(state),
socketConnected: selectCommentSocketConnected(state),
};
};
@ -29,6 +34,7 @@ const perform = {
doCommentSocketConnect,
doCommentSocketDisconnect,
doFetchChannelLiveStatus,
doSetSocketConnected,
};
export default connect(select, perform)(LivestreamPage);

View file

@ -1,7 +1,11 @@
// @flow
import { formatLbryChannelName } from 'util/url';
import { lazyImport } from 'util/lazyImport';
import { LIVESTREAM_STARTS_SOON_BUFFER, LIVESTREAM_STARTED_RECENTLY_BUFFER } from 'constants/livestream';
import {
LIVESTREAM_STATUS_CHECK_INTERVAL,
LIVESTREAM_STARTS_SOON_BUFFER,
LIVESTREAM_STARTED_RECENTLY_BUFFER,
} from 'constants/livestream';
import analytics from 'analytics';
import LivestreamLayout from 'component/livestreamLayout';
import moment from 'moment';
@ -9,7 +13,6 @@ import Page from 'component/page';
import React from 'react';
const LivestreamChatLayout = lazyImport(() => import('component/livestreamChatLayout' /* webpackChunkName: "chat" */));
const LIVESTREAM_STATUS_CHECK_INTERVAL = 30000;
type Props = {
activeLivestreamForChannel: any,
@ -19,11 +22,13 @@ type Props = {
claim: StreamClaim,
isAuthenticated: boolean,
uri: string,
socketConnected: boolean,
doSetPrimaryUri: (uri: ?string) => void,
doCommentSocketConnect: (string, string, string) => void,
doCommentSocketDisconnect: (string, string) => void,
doFetchChannelLiveStatus: (string) => void,
doUserSetReferrer: (string) => void,
doSetSocketConnected: (connected: boolean) => void,
};
export const LayoutRenderContext = React.createContext<any>();
@ -37,11 +42,13 @@ export default function LivestreamPage(props: Props) {
claim,
isAuthenticated,
uri,
socketConnected,
doSetPrimaryUri,
doCommentSocketConnect,
doCommentSocketDisconnect,
doFetchChannelLiveStatus,
doUserSetReferrer,
doSetSocketConnected,
} = props;
const [activeStreamUri, setActiveStreamUri] = React.useState(false);
@ -67,20 +74,25 @@ export default function LivestreamPage(props: Props) {
// Establish web socket connection for viewer count.
React.useEffect(() => {
if (!claim) return;
if (!claim || socketConnected) return;
const { claim_id: claimId, signing_channel: channelClaim } = claim;
const channelName = channelClaim && formatLbryChannelName(channelClaim.canonical_url);
if (claimId && channelName) {
doCommentSocketConnect(uri, channelName, claimId);
doSetSocketConnected(true);
}
return () => {
if (claimId && channelName) {
doCommentSocketDisconnect(claimId, channelName);
doSetSocketConnected(false);
}
};
// only listen to socketConnected on initial mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [claim, uri, doCommentSocketConnect, doCommentSocketDisconnect]);
// Find out current channels status + active live claim every 30 seconds
@ -149,10 +161,12 @@ export default function LivestreamPage(props: Props) {
}, [uri, stringifiedClaim, isAuthenticated, doUserSetReferrer]);
React.useEffect(() => {
if (!layountRendered) return;
doSetPrimaryUri(uri);
return () => doSetPrimaryUri(null);
}, [doSetPrimaryUri, uri]);
}, [doSetPrimaryUri, layountRendered, uri]);
return (
<Page

View file

@ -119,7 +119,7 @@ export function doSetPlayingUri({
source?: string,
commentId?: string,
pathname?: string,
collectionId?: string,
collectionId?: ?string,
}) {
return (dispatch: Dispatch) => {
dispatch({
@ -149,16 +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, isFloating?: boolean) {
export function doUriInitiatePlay(playingOptions: PlayingUri, isPlayable?: boolean, isFloating?: boolean) {
return (dispatch: Dispatch, getState: () => any) => {
const { uri, source } = playingOptions;
if (!uri) return;
const state = getState();
const isLive = selectIsActiveLivestreamForUri(state, uri);
if (!isFloating) dispatch(doSetPrimaryUri(uri));
if (!isFloating && !source) dispatch(doSetPrimaryUri(uri));
if (isPlayable) {
dispatch(doSetPlayingUri({ uri, collectionId }));
}
if (isPlayable) dispatch(doSetPlayingUri(playingOptions));
if (!isLive) dispatch(doPlayUri(uri, false, true, (fileInfo) => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
};

View file

@ -5,7 +5,7 @@ import * as ABANDON_STATES from 'constants/abandon_states';
import { shell } from 'electron';
// @endif
import Lbry from 'lbry';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
import { selectClaimForUri } from 'redux/selectors/claims';
import { doAbandonClaim } from 'redux/actions/claims';
import { batchActions } from 'util/batch-actions';
@ -20,6 +20,7 @@ import {
selectDownloadingByOutpoint,
makeSelectStreamingUrlForUri,
} from 'redux/selectors/file_info';
import { isStreamPlaceholderClaim } from 'util/claim';
type Dispatch = (action: any) => any;
type GetState = () => { claims: any, file: FileState, content: any, user: User };
@ -77,7 +78,7 @@ export function doDeleteFileAndMaybeGoBack(
const state = getState();
const playingUri = selectPlayingUri(state);
const { outpoint } = makeSelectFileInfoForUri(uri)(state) || '';
const { nout, txid } = makeSelectClaimForUri(uri)(state);
const { nout, txid } = selectClaimForUri(state, uri);
const claimOutpoint = `${txid}:${nout}`;
const actions = [];
@ -104,7 +105,7 @@ export function doDeleteFileAndMaybeGoBack(
)
);
if (playingUri && playingUri.uri === uri) {
if (playingUri.uri === uri) {
actions.push(doSetPlayingUri({ uri: null }));
}
// it would be nice to stay on the claim if you just want to delete it
@ -117,7 +118,9 @@ export function doDeleteFileAndMaybeGoBack(
export function doFileGet(uri: string, saveFile: boolean = true, onSuccess?: (GetResponse) => any) {
return (dispatch: Dispatch, getState: () => any) => {
const state = getState();
const { nout, txid } = makeSelectClaimForUri(uri)(state);
const claim = selectClaimForUri(state, uri);
const isLivestreamClaim = isStreamPlaceholderClaim(claim);
const { nout, txid } = claim;
const outpoint = `${txid}:${nout}`;
dispatch({
@ -169,6 +172,10 @@ export function doFileGet(uri: string, saveFile: boolean = true, onSuccess?: (Ge
data: { outpoint },
});
// TODO: probably a better way to address this
// supress no source error if it's a livestream
if (isLivestreamClaim && error.message === "stream doesn't have source data") return;
dispatch(
doToast({
message: `Failed to view ${uri}, please try again. If this problem persists, visit https://odysee.com/@OdyseeHelp:b?view=about for support.`,

View file

@ -189,3 +189,9 @@ export const doFetchActiveLivestreams = (
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED });
}
};
export const doSetSocketConnected = (connected: boolean) => (dispatch: Dispatch) =>
dispatch({
type: ACTIONS.COMMENT_SOCKET_CONNECTED,
data: { connected },
});

View file

@ -3,7 +3,7 @@ import * as ACTIONS from 'constants/action_types';
const reducers = {};
const defaultState = {
primaryUri: null, // Top level content uri triggered from the file page
playingUri: null,
playingUri: {},
channelClaimCounts: {},
positions: {},
history: [],

View file

@ -11,6 +11,7 @@ const defaultState: LivestreamState = {
activeLivestreamsLastFetchedDate: 0,
activeLivestreamsLastFetchedOptions: {},
activeLivestreamInitialized: false,
commentSocketConnected: false,
};
export default handleActions(
@ -67,6 +68,10 @@ export default handleActions(
if (activeLivestreams) delete activeLivestreams[action.data.channelId];
return { ...state, activeLivestreams: Object.assign({}, activeLivestreams), activeLivestreamInitialized: true };
},
[ACTIONS.COMMENT_SOCKET_CONNECTED]: (state: CommentsState, action: any) => ({
...state,
commentSocketConnected: action.data.connected,
}),
},
defaultState
);

View file

@ -6,7 +6,13 @@ import { selectYoutubeChannels } from 'redux/selectors/user';
import { selectSupportsByOutpoint } from 'redux/selectors/wallet';
import { createSelector } from 'reselect';
import { createCachedSelector } from 're-reselect';
import { isClaimNsfw, filterClaims, getChannelIdFromClaim, isStreamPlaceholderClaim } from 'util/claim';
import {
isClaimNsfw,
filterClaims,
getChannelIdFromClaim,
isStreamPlaceholderClaim,
getThumbnailFromClaim,
} from 'util/claim';
import * as CLAIM from 'constants/claim';
import * as SETTINGS from 'constants/settings';
import { INTERNAL_TAGS } from 'constants/tags';
@ -393,11 +399,6 @@ export const makeSelectContentTypeForUri = (uri: string) =>
return source ? source.media_type : undefined;
});
export const getThumbnailFromClaim = (claim: Claim) => {
const thumbnail = claim && claim.value && claim.value.thumbnail;
return thumbnail && thumbnail.url ? thumbnail.url.trim().replace(/^http:\/\//i, 'https://') : undefined;
};
export const selectThumbnailForUri = createCachedSelector(selectClaimForUri, (claim) => {
return getThumbnailFromClaim(claim);
})((state, uri) => String(uri));

View file

@ -31,11 +31,11 @@ export const makeSelectIsPlaying = (uri: string) =>
createSelector(selectPrimaryUri, (primaryUri) => primaryUri === uri);
export const makeSelectIsUriCurrentlyPlaying = (uri: string) =>
createSelector(selectPlayingUri, (playingUri) => playingUri && playingUri.uri === uri);
createSelector(selectPlayingUri, (playingUri) => playingUri.uri === uri);
export const makeSelectIsPlayerFloating = (location: UrlLocation) =>
createSelector(selectPrimaryUri, selectPlayingUri, (primaryUri, playingUri) => {
if (!playingUri) return false;
if (!playingUri.uri) return false;
const { pathname, search } = location;
const hasSecondarySource = Boolean(playingUri.source);

View file

@ -12,6 +12,7 @@ export const selectViewersById = (state: State) => selectState(state).viewersByI
export const selectActiveLivestreams = (state: State) => selectState(state).activeLivestreams;
export const selectFetchingActiveLivestreams = (state: State) => selectState(state).fetchingActiveLivestreams;
export const selectActiveLivestreamInitialized = (state: State) => selectState(state).activeLivestreamInitialized;
export const selectCommentSocketConnected = (state: State) => selectState(state).commentSocketConnected;
// select non-pending claims without sources for given channel
export const makeSelectLivestreamsForChannelId = (channelId: string) =>

View file

@ -127,3 +127,8 @@ export function getClaimTitle(claim: ?Claim) {
export const isStreamPlaceholderClaim = (claim: ?StreamClaim) => {
return claim ? Boolean(claim.value_type === 'stream' && !claim.value.source) : false;
};
export const getThumbnailFromClaim = (claim: ?Claim) => {
const thumbnail = claim && claim.value && claim.value.thumbnail;
return thumbnail && thumbnail.url ? thumbnail.url.trim().replace(/^http:\/\//i, 'https://') : undefined;
};