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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,7 @@ type Props = {
claim: ?Claim, claim: ?Claim,
claimIsMine: boolean, claimIsMine: boolean,
activeChannelClaim: ?ChannelClaim, activeChannelClaim: ?ChannelClaim,
playingUri: ?PlayingUri, playingUri: PlayingUri,
moderationDelegatorsById: { [string]: { global: boolean, delegators: { name: string, claimId: string } } }, moderationDelegatorsById: { [string]: { global: boolean, delegators: { name: string, claimId: string } } },
// --- perform --- // --- perform ---
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
@ -87,7 +87,7 @@ function CommentMenuList(props: Props) {
Object.values(activeModeratorInfo.delegators).includes(contentChannelClaim.claim_id); Object.values(activeModeratorInfo.delegators).includes(contentChannelClaim.claim_id);
function handleDeleteComment() { function handleDeleteComment() {
if (playingUri && playingUri.source === 'comment') { if (playingUri.source === 'comment') {
clearPlayingUri(); 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 { 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 { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
import { import {
makeSelectNextUrlForCollectionAndUrl, makeSelectNextUrlForCollectionAndUrl,
@ -18,8 +18,10 @@ import { doUriInitiatePlay, doSetPlayingUri } from 'redux/actions/content';
import { doFetchRecommendedContent } from 'redux/actions/search'; import { doFetchRecommendedContent } from 'redux/actions/search';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { selectMobilePlayerDimensions } from 'redux/selectors/app'; 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 { doSetMobilePlayerDimensions } from 'redux/actions/app';
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
import { doSetSocketConnected } from 'redux/actions/livestream';
import FileRenderFloating from './view'; import FileRenderFloating from './view';
const select = (state, props) => { const select = (state, props) => {
@ -28,7 +30,13 @@ const select = (state, props) => {
const playingUri = selectPlayingUri(state); const playingUri = selectPlayingUri(state);
const { uri, collectionId } = playingUri || {}; const { uri, collectionId } = playingUri || {};
const claim = uri && selectClaimForUri(state, uri);
const { claim_id: claimId, signing_channel: channelClaim } = claim || {};
const { canonical_url: channelClaimUrl } = channelClaim || {};
return { return {
claimId,
channelClaimUrl,
uri, uri,
playingUri, playingUri,
primaryUri: selectPrimaryUri(state), primaryUri: selectPrimaryUri(state),
@ -45,6 +53,7 @@ const select = (state, props) => {
collectionId, collectionId,
isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri), isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri),
mobilePlayerDimensions: selectMobilePlayerDimensions(state), mobilePlayerDimensions: selectMobilePlayerDimensions(state),
socketConnected: selectCommentSocketConnected(state),
}; };
}; };
@ -53,6 +62,9 @@ const perform = {
doUriInitiatePlay, doUriInitiatePlay,
doSetPlayingUri, doSetPlayingUri,
doSetMobilePlayerDimensions, doSetMobilePlayerDimensions,
doCommentSocketConnect,
doCommentSocketDisconnect,
doSetSocketConnected,
}; };
export default withRouter(connect(select, perform)(FileRenderFloating)); 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 { 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 } from 'util/url'; import { generateListSearchUrlParams, formatLbryChannelName } 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';
@ -35,13 +35,15 @@ export const FLOATING_PLAYER_CLASS = 'content__viewer--floating';
// **************************************************************************** // ****************************************************************************
type Props = { type Props = {
claimId: ?string,
channelClaimUrl: ?string,
isFloating: boolean, isFloating: boolean,
uri: string, uri: string,
streamingUrl?: string, streamingUrl?: string,
title: ?string, title: ?string,
floatingPlayerEnabled: boolean, floatingPlayerEnabled: boolean,
renderMode: string, renderMode: string,
playingUri: ?PlayingUri, playingUri: PlayingUri,
primaryUri: ?string, primaryUri: ?string,
videoTheaterMode: boolean, videoTheaterMode: boolean,
collectionId: string, collectionId: string,
@ -50,15 +52,21 @@ type Props = {
nextListUri: string, nextListUri: string,
previousListUri: string, previousListUri: string,
doFetchRecommendedContent: (uri: string) => void, 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, doSetPlayingUri: ({ uri?: ?string }) => void,
isCurrentClaimLive?: boolean, isCurrentClaimLive?: boolean,
mobilePlayerDimensions?: any, mobilePlayerDimensions?: any,
socketConnected: boolean,
doSetMobilePlayerDimensions: ({ height?: ?number, width?: ?number }) => void, 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) { export default function FileRenderFloating(props: Props) {
const { const {
claimId,
channelClaimUrl,
uri, uri,
streamingUrl, streamingUrl,
title, title,
@ -73,12 +81,16 @@ export default function FileRenderFloating(props: Props) {
claimWasPurchased, claimWasPurchased,
nextListUri, nextListUri,
previousListUri, previousListUri,
socketConnected,
doFetchRecommendedContent, doFetchRecommendedContent,
doUriInitiatePlay, doUriInitiatePlay,
doSetPlayingUri, doSetPlayingUri,
isCurrentClaimLive, isCurrentClaimLive,
mobilePlayerDimensions, mobilePlayerDimensions,
doSetMobilePlayerDimensions, doSetMobilePlayerDimensions,
doCommentSocketConnect,
doCommentSocketDisconnect,
doSetSocketConnected,
} = props; } = props;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@ -88,7 +100,7 @@ export default function FileRenderFloating(props: Props) {
} = useHistory(); } = useHistory();
const hideFloatingPlayer = state && state.hideFloatingPlayer; const hideFloatingPlayer = state && state.hideFloatingPlayer;
const playingUriSource = playingUri && playingUri.source; const { uri: playingUrl, source: playingUriSource, primaryUri: playingPrimaryUri } = playingUri;
const isComment = playingUriSource === 'comment'; const isComment = playingUriSource === 'comment';
const mainFilePlaying = (!isFloating || !isMobile) && primaryUri && isURIEqual(uri, primaryUri); const mainFilePlaying = (!isFloating || !isMobile) && primaryUri && isURIEqual(uri, primaryUri);
const noFloatingPlayer = !isFloating || isMobile || !floatingPlayerEnabled || hideFloatingPlayer; 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 relativePosRef = React.useRef({ x: 0, y: 0 });
const navigateUrl = const navigateUrl =
playingUri && (playingPrimaryUri || playingUrl || '') + (collectionId ? generateListSearchUrlParams(collectionId) : '');
(playingUri.primaryUri || playingUri.uri) + (collectionId ? generateListSearchUrlParams(collectionId) : '');
const isFree = costInfo && costInfo.cost === 0; const isFree = costInfo && costInfo.cost === 0;
const canViewFile = isFree || claimWasPurchased; const canViewFile = isFree || claimWasPurchased;
@ -184,11 +195,37 @@ export default function FileRenderFloating(props: Props) {
resetState resetState
); );
// Establish web socket connection for viewer count.
React.useEffect(() => { 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();
} }
}, [handleResize, playingUri, videoTheaterMode]); }, [handleResize, playingPrimaryUri, videoTheaterMode, playingUrl]);
// Listen to main-window resizing and adjust the floating player position accordingly: // Listen to main-window resizing and adjust the floating player position accordingly:
React.useEffect(() => { React.useEffect(() => {
@ -336,7 +373,7 @@ export default function FileRenderFloating(props: Props) {
<AutoplayCountdown <AutoplayCountdown
nextRecommendedUri={nextListUri} nextRecommendedUri={nextListUri}
doNavigate={() => setDoNavigate(true)} doNavigate={() => setDoNavigate(true)}
doReplay={() => doUriInitiatePlay(uri, collectionId, false, isFloating)} doReplay={() => doUriInitiatePlay({ uri, collectionId }, false, isFloating)}
doPrevious={() => { doPrevious={() => {
setPlayNext(false); setPlayNext(false);
setDoNavigate(true); setDoNavigate(true);

View file

@ -1,10 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doUriInitiatePlay } from 'redux/actions/content'; import { doUriInitiatePlay } from 'redux/actions/content';
import { import { makeSelectClaimWasPurchased, selectClaimForUri } from 'redux/selectors/claims';
selectThumbnailForUri,
makeSelectClaimWasPurchased,
selectIsLivestreamClaimForUri,
} from 'redux/selectors/claims';
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { selectCostInfoForUri } from 'lbryinc'; import { selectCostInfoForUri } from 'lbryinc';
@ -19,12 +15,20 @@ import {
} from 'redux/selectors/content'; } from 'redux/selectors/content';
import FileRenderInitiator from './view'; import FileRenderInitiator from './view';
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream'; import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
import { getThumbnailFromClaim, isStreamPlaceholderClaim } from 'util/claim';
import { doFetchChannelLiveStatus } from 'redux/actions/livestream';
const select = (state, props) => { const select = (state, props) => {
const { uri } = props; const { uri } = props;
const claim = selectClaimForUri(state, uri);
const { claim_id: claimId, signing_channel: channelClaim } = claim || {};
const { claim_id: channelClaimId } = channelClaim || {};
return { return {
claimThumbnail: selectThumbnailForUri(state, uri), claimId,
channelClaimId,
claimThumbnail: getThumbnailFromClaim(claim),
fileInfo: makeSelectFileInfoForUri(uri)(state), fileInfo: makeSelectFileInfoForUri(uri)(state),
obscurePreview: selectShouldObscurePreviewForUri(state, uri), obscurePreview: selectShouldObscurePreviewForUri(state, uri),
isPlaying: makeSelectIsPlaying(uri)(state), isPlaying: makeSelectIsPlaying(uri)(state),
@ -35,12 +39,13 @@ const select = (state, props) => {
claimWasPurchased: makeSelectClaimWasPurchased(uri)(state), claimWasPurchased: makeSelectClaimWasPurchased(uri)(state),
authenticated: selectUserVerifiedEmail(state), authenticated: selectUserVerifiedEmail(state),
isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri), isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri),
isLivestreamClaim: selectIsLivestreamClaimForUri(state, uri), isLivestreamClaim: isStreamPlaceholderClaim(claim),
}; };
}; };
const perform = { const perform = {
doUriInitiatePlay, doUriInitiatePlay,
doFetchChannelLiveStatus,
}; };
export default withRouter(connect(select, perform)(FileRenderInitiator)); 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 FileRenderPlaceholder from 'static/img/fileRenderPlaceholder.png';
import * as COLLECTIONS_CONSTS from 'constants/collections'; import * as COLLECTIONS_CONSTS from 'constants/collections';
import { LayoutRenderContext } from 'page/livestream/view'; 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 = { type Props = {
channelClaimId: ?string,
isPlaying: boolean, isPlaying: boolean,
fileInfo: FileListItem, fileInfo: FileListItem,
uri: string, uri: string,
@ -33,13 +37,18 @@ type Props = {
authenticated: boolean, authenticated: boolean,
videoTheaterMode: boolean, videoTheaterMode: boolean,
isCurrentClaimLive?: boolean, isCurrentClaimLive?: boolean,
doUriInitiatePlay: (uri: string, collectionId: ?string, isPlayable: boolean) => void,
isLivestreamClaim: boolean, isLivestreamClaim: boolean,
customAction?: any, customAction?: any,
embedded?: boolean,
parentCommentId?: string,
isMarkdownPost?: boolean,
doUriInitiatePlay: (playingOptions: PlayingUri, isPlayable: boolean) => void,
doFetchChannelLiveStatus: (string) => void,
}; };
export default function FileRenderInitiator(props: Props) { export default function FileRenderInitiator(props: Props) {
const { const {
channelClaimId,
isPlaying, isPlaying,
fileInfo, fileInfo,
uri, uri,
@ -57,7 +66,11 @@ export default function FileRenderInitiator(props: Props) {
isCurrentClaimLive, isCurrentClaimLive,
isLivestreamClaim, isLivestreamClaim,
customAction, customAction,
embedded,
parentCommentId,
isMarkdownPost,
doUriInitiatePlay, doUriInitiatePlay,
doFetchChannelLiveStatus,
} = props; } = props;
const layountRendered = React.useContext(LayoutRenderContext); const layountRendered = React.useContext(LayoutRenderContext);
@ -67,14 +80,15 @@ export default function FileRenderInitiator(props: Props) {
const containerRef = React.useRef<any>(); const containerRef = React.useRef<any>();
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder); 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 urlParams = search && new URLSearchParams(search);
const collectionId = urlParams && urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID); const collectionId = urlParams && urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
// check if there is a time or autoplay parameter, if so force autoplay // check if there is a time or autoplay parameter, if so force autoplay
const urlTimeParam = href && href.indexOf('t=') > -1; const urlTimeParam = href && href.indexOf('t=') > -1;
const forceAutoplayParam = locationState && locationState.forceAutoplay; const forceAutoplayParam = locationState && locationState.forceAutoplay;
const shouldAutoplay = forceAutoplayParam || urlTimeParam || autoplay; const shouldAutoplay = !embedded && (forceAutoplayParam || urlTimeParam || autoplay);
const isFree = costInfo && costInfo.cost === 0; const isFree = costInfo && costInfo.cost === 0;
const canViewFile = isLivestreamClaim const canViewFile = isLivestreamClaim
? (layountRendered || isMobile) && isCurrentClaimLive ? (layountRendered || isMobile) && isCurrentClaimLive
@ -90,9 +104,20 @@ export default function FileRenderInitiator(props: Props) {
const shouldRedirect = !authenticated && !isFree; const shouldRedirect = !authenticated && !isFree;
function doAuthRedirect() { 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(() => { React.useEffect(() => {
if (!claimThumbnail) return; if (!claimThumbnail) return;
@ -114,11 +139,29 @@ export default function FileRenderInitiator(props: Props) {
}, 200); }, 200);
}, [claimThumbnail, thumbnail]); }, [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 // 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 // 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(() => { const viewFile = React.useCallback(() => {
doUriInitiatePlay(uri, collectionId, isPlayable); const playingOptions = { uri, collectionId, pathname, source: undefined, commentId: undefined };
}, [collectionId, doUriInitiatePlay, isPlayable, uri]);
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(() => { React.useEffect(() => {
const videoOnPage = document.querySelector('video'); const videoOnPage = document.querySelector('video');
@ -143,15 +186,21 @@ export default function FileRenderInitiator(props: Props) {
return ( return (
<div <div
ref={containerRef} ref={containerRef}
onClick={disabled ? undefined : shouldRedirect ? doAuthRedirect : viewFile} onClick={disabled ? undefined : shouldRedirect ? doAuthRedirect : handleClick}
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}} style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
className={classnames('content__cover', { className={
embedded
? 'embed__inline-button'
: classnames('content__cover', {
'content__cover--disabled': disabled, 'content__cover--disabled': disabled,
'content__cover--theater-mode': videoTheaterMode && !isMobile, 'content__cover--theater-mode': videoTheaterMode && !isMobile,
'content__cover--text': isText, 'content__cover--text': isText,
'card__media--nsfw': obscurePreview, 'card__media--nsfw': obscurePreview,
})} })
}
> >
{embedded && <FileViewerEmbeddedTitle uri={uri} isInApp />}
{renderUnsupported ? ( {renderUnsupported ? (
<Nag <Nag
type="helpful" type="helpful"
@ -173,10 +222,10 @@ export default function FileRenderInitiator(props: Props) {
) )
)} )}
{!disabled && ( {(!disabled || (embedded && isLivestreamClaim)) && (
<Button <Button
requiresAuth={shouldRedirect} requiresAuth={shouldRedirect}
onClick={viewFile} onClick={handleClick}
iconSize={30} iconSize={30}
title={isPlayable ? __('Play') : __('View')} title={isPlayable ? __('Play') : __('View')}
className={classnames('button--icon', { className={classnames('button--icon', {

View file

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

View file

@ -39,8 +39,8 @@ const select = (state, props) => {
const userId = selectUser(state) && selectUser(state).id; const userId = selectUser(state) && selectUser(state).id;
const internalFeature = selectUser(state) && selectUser(state).internal_feature; const internalFeature = selectUser(state) && selectUser(state).internal_feature;
const playingUri = selectPlayingUri(state); const playingUri = selectPlayingUri(state);
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID) || (playingUri && playingUri.collectionId); const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID) || playingUri.collectionId;
const isMarkdownOrComment = playingUri && (playingUri.source === 'markdown' || playingUri.source === 'comment'); const isMarkdownOrComment = playingUri.source === 'markdown' || playingUri.source === 'comment';
let nextRecommendedUri; let nextRecommendedUri;
let previousListUri; 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_STARTED = 'COMMENT_FETCH_BLOCKED_WORDS_STARTED';
export const COMMENT_FETCH_BLOCKED_WORDS_FAILED = 'COMMENT_FETCH_BLOCKED_WORDS_FAILED'; 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_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_RECEIVED = 'COMMENT_RECEIVED';
export const COMMENT_SUPER_CHAT_LIST_STARTED = 'COMMENT_SUPER_CHAT_LIST_STARTED'; export const COMMENT_SUPER_CHAT_LIST_STARTED = 'COMMENT_SUPER_CHAT_LIST_STARTED';
export const COMMENT_SUPER_CHAT_LIST_COMPLETED = 'COMMENT_SUPER_CHAT_LIST_COMPLETED'; 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 MAX_LIVESTREAM_COMMENTS = 50;
export const LIVESTREAM_STATUS_CHECK_INTERVAL = 30 * 1000;
export const LIVESTREAM_STARTS_SOON_BUFFER = 15; export const LIVESTREAM_STARTS_SOON_BUFFER = 15;
export const LIVESTREAM_STARTED_RECENTLY_BUFFER = 15; export const LIVESTREAM_STARTED_RECENTLY_BUFFER = 15;
export const LIVESTREAM_UPCOMING_BUFFER = 35; export const LIVESTREAM_UPCOMING_BUFFER = 35;

View file

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

View file

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

View file

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

View file

@ -4,7 +4,6 @@ import { withRouter } from 'react-router-dom';
import CollectionPage from './view'; import CollectionPage from './view';
import { import {
selectTitleForUri, selectTitleForUri,
getThumbnailFromClaim,
selectClaimIsMine, selectClaimIsMine,
makeSelectClaimIsPending, makeSelectClaimIsPending,
makeSelectClaimForClaimId, makeSelectClaimForClaimId,
@ -20,6 +19,7 @@ import {
makeSelectEditedCollectionForId, makeSelectEditedCollectionForId,
} from 'redux/selectors/collections'; } from 'redux/selectors/collections';
import { getThumbnailFromClaim } from 'util/claim';
import { doFetchItemsInCollection, doCollectionDelete, doCollectionEdit } from 'redux/actions/collections'; import { doFetchItemsInCollection, doCollectionDelete, doCollectionEdit } from 'redux/actions/collections';
import { selectUser } from 'redux/selectors/user'; 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 { DISABLE_COMMENTS_TAG } from 'constants/tags';
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket'; import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
import { getChannelIdFromClaim } from 'util/claim'; import { getChannelIdFromClaim } from 'util/claim';
import { selectActiveLivestreamForChannel, selectActiveLivestreamInitialized } from 'redux/selectors/livestream'; import {
import { doFetchChannelLiveStatus } from 'redux/actions/livestream'; selectActiveLivestreamForChannel,
selectActiveLivestreamInitialized,
selectCommentSocketConnected,
} from 'redux/selectors/livestream';
import { doFetchChannelLiveStatus, doSetSocketConnected } from 'redux/actions/livestream';
import LivestreamPage from './view'; import LivestreamPage from './view';
const select = (state, props) => { const select = (state, props) => {
@ -20,6 +24,7 @@ const select = (state, props) => {
chatDisabled: makeSelectTagInClaimOrChannelForUri(uri, DISABLE_COMMENTS_TAG)(state), chatDisabled: makeSelectTagInClaimOrChannelForUri(uri, DISABLE_COMMENTS_TAG)(state),
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelClaimId), activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelClaimId),
activeLivestreamInitialized: selectActiveLivestreamInitialized(state), activeLivestreamInitialized: selectActiveLivestreamInitialized(state),
socketConnected: selectCommentSocketConnected(state),
}; };
}; };
@ -29,6 +34,7 @@ const perform = {
doCommentSocketConnect, doCommentSocketConnect,
doCommentSocketDisconnect, doCommentSocketDisconnect,
doFetchChannelLiveStatus, doFetchChannelLiveStatus,
doSetSocketConnected,
}; };
export default connect(select, perform)(LivestreamPage); export default connect(select, perform)(LivestreamPage);

View file

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

View file

@ -119,7 +119,7 @@ export function doSetPlayingUri({
source?: string, source?: string,
commentId?: string, commentId?: string,
pathname?: string, pathname?: string,
collectionId?: string, collectionId?: ?string,
}) { }) {
return (dispatch: Dispatch) => { return (dispatch: Dispatch) => {
dispatch({ dispatch({
@ -149,16 +149,18 @@ export function doDownloadUri(uri: string) {
return (dispatch: Dispatch) => dispatch(doPlayUri(uri, false, true, () => dispatch(doAnalyticsView(uri)))); 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) => { return (dispatch: Dispatch, getState: () => any) => {
const { uri, source } = playingOptions;
if (!uri) return;
const state = getState(); const state = getState();
const isLive = selectIsActiveLivestreamForUri(state, uri); const isLive = selectIsActiveLivestreamForUri(state, uri);
if (!isFloating) dispatch(doSetPrimaryUri(uri)); if (!isFloating && !source) dispatch(doSetPrimaryUri(uri));
if (isPlayable) { if (isPlayable) dispatch(doSetPlayingUri(playingOptions));
dispatch(doSetPlayingUri({ uri, collectionId }));
}
if (!isLive) dispatch(doPlayUri(uri, false, true, (fileInfo) => dispatch(doAnaltyicsPurchaseEvent(fileInfo)))); 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'; import { shell } from 'electron';
// @endif // @endif
import Lbry from 'lbry'; import Lbry from 'lbry';
import { makeSelectClaimForUri } from 'redux/selectors/claims'; import { selectClaimForUri } from 'redux/selectors/claims';
import { doAbandonClaim } from 'redux/actions/claims'; import { doAbandonClaim } from 'redux/actions/claims';
import { batchActions } from 'util/batch-actions'; import { batchActions } from 'util/batch-actions';
@ -20,6 +20,7 @@ import {
selectDownloadingByOutpoint, selectDownloadingByOutpoint,
makeSelectStreamingUrlForUri, makeSelectStreamingUrlForUri,
} from 'redux/selectors/file_info'; } from 'redux/selectors/file_info';
import { isStreamPlaceholderClaim } from 'util/claim';
type Dispatch = (action: any) => any; type Dispatch = (action: any) => any;
type GetState = () => { claims: any, file: FileState, content: any, user: User }; type GetState = () => { claims: any, file: FileState, content: any, user: User };
@ -77,7 +78,7 @@ export function doDeleteFileAndMaybeGoBack(
const state = getState(); const state = getState();
const playingUri = selectPlayingUri(state); const playingUri = selectPlayingUri(state);
const { outpoint } = makeSelectFileInfoForUri(uri)(state) || ''; const { outpoint } = makeSelectFileInfoForUri(uri)(state) || '';
const { nout, txid } = makeSelectClaimForUri(uri)(state); const { nout, txid } = selectClaimForUri(state, uri);
const claimOutpoint = `${txid}:${nout}`; const claimOutpoint = `${txid}:${nout}`;
const actions = []; const actions = [];
@ -104,7 +105,7 @@ export function doDeleteFileAndMaybeGoBack(
) )
); );
if (playingUri && playingUri.uri === uri) { if (playingUri.uri === uri) {
actions.push(doSetPlayingUri({ uri: null })); actions.push(doSetPlayingUri({ uri: null }));
} }
// it would be nice to stay on the claim if you just want to delete it // 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) { export function doFileGet(uri: string, saveFile: boolean = true, onSuccess?: (GetResponse) => any) {
return (dispatch: Dispatch, getState: () => any) => { return (dispatch: Dispatch, getState: () => any) => {
const state = getState(); 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}`; const outpoint = `${txid}:${nout}`;
dispatch({ dispatch({
@ -169,6 +172,10 @@ export function doFileGet(uri: string, saveFile: boolean = true, onSuccess?: (Ge
data: { outpoint }, 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( dispatch(
doToast({ doToast({
message: `Failed to view ${uri}, please try again. If this problem persists, visit https://odysee.com/@OdyseeHelp:b?view=about for support.`, 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 }); 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 reducers = {};
const defaultState = { const defaultState = {
primaryUri: null, // Top level content uri triggered from the file page primaryUri: null, // Top level content uri triggered from the file page
playingUri: null, playingUri: {},
channelClaimCounts: {}, channelClaimCounts: {},
positions: {}, positions: {},
history: [], history: [],

View file

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

View file

@ -6,7 +6,13 @@ import { selectYoutubeChannels } from 'redux/selectors/user';
import { selectSupportsByOutpoint } from 'redux/selectors/wallet'; import { selectSupportsByOutpoint } from 'redux/selectors/wallet';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { createCachedSelector } from 're-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 CLAIM from 'constants/claim';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { INTERNAL_TAGS } from 'constants/tags'; import { INTERNAL_TAGS } from 'constants/tags';
@ -393,11 +399,6 @@ export const makeSelectContentTypeForUri = (uri: string) =>
return source ? source.media_type : undefined; 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) => { export const selectThumbnailForUri = createCachedSelector(selectClaimForUri, (claim) => {
return getThumbnailFromClaim(claim); return getThumbnailFromClaim(claim);
})((state, uri) => String(uri)); })((state, uri) => String(uri));

View file

@ -31,11 +31,11 @@ export const makeSelectIsPlaying = (uri: string) =>
createSelector(selectPrimaryUri, (primaryUri) => primaryUri === uri); createSelector(selectPrimaryUri, (primaryUri) => primaryUri === uri);
export const makeSelectIsUriCurrentlyPlaying = (uri: string) => export const makeSelectIsUriCurrentlyPlaying = (uri: string) =>
createSelector(selectPlayingUri, (playingUri) => playingUri && playingUri.uri === uri); createSelector(selectPlayingUri, (playingUri) => playingUri.uri === uri);
export const makeSelectIsPlayerFloating = (location: UrlLocation) => export const makeSelectIsPlayerFloating = (location: UrlLocation) =>
createSelector(selectPrimaryUri, selectPlayingUri, (primaryUri, playingUri) => { createSelector(selectPrimaryUri, selectPlayingUri, (primaryUri, playingUri) => {
if (!playingUri) return false; if (!playingUri.uri) return false;
const { pathname, search } = location; const { pathname, search } = location;
const hasSecondarySource = Boolean(playingUri.source); 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 selectActiveLivestreams = (state: State) => selectState(state).activeLivestreams;
export const selectFetchingActiveLivestreams = (state: State) => selectState(state).fetchingActiveLivestreams; export const selectFetchingActiveLivestreams = (state: State) => selectState(state).fetchingActiveLivestreams;
export const selectActiveLivestreamInitialized = (state: State) => selectState(state).activeLivestreamInitialized; 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 // select non-pending claims without sources for given channel
export const makeSelectLivestreamsForChannelId = (channelId: string) => export const makeSelectLivestreamsForChannelId = (channelId: string) =>

View file

@ -127,3 +127,8 @@ export function getClaimTitle(claim: ?Claim) {
export const isStreamPlaceholderClaim = (claim: ?StreamClaim) => { export const isStreamPlaceholderClaim = (claim: ?StreamClaim) => {
return claim ? Boolean(claim.value_type === 'stream' && !claim.value.source) : false; 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;
};