Improve livestream claimLink embeds
- Remove embedPlayButton for fileRenderInitiator - getThumbnailFromClaim from utils function instead of redux - Improve playingUri
This commit is contained in:
parent
6dea79819d
commit
b096aad70e
35 changed files with 278 additions and 247 deletions
|
@ -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);
|
||||
|
|
7
flow-typed/content.js
vendored
7
flow-typed/content.js
vendored
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
<Router />
|
||||
<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" />}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 ? (
|
||||
<>
|
||||
<UriIndicator uri={uri} link showAtSign />
|
||||
<span>{fullUri.length > uri.length ? fullUri.substring(uri.length, fullUri.length) : ''}</span>
|
||||
</>
|
||||
) : allowPreview ? (
|
||||
<div className={classnames('claim-link')}>
|
||||
<div
|
||||
className={classnames({
|
||||
[INLINE_PLAYER_WRAPPER_CLASS]: isPlayingInline,
|
||||
})}
|
||||
>
|
||||
<EmbedPlayButton uri={uri} parentCommentId={parentCommentId} isMarkdownPost={isMarkdownPost} />
|
||||
if (isChannel) {
|
||||
return (
|
||||
<>
|
||||
<UriIndicator uri={uri} link showAtSign />
|
||||
<span>{fullUri.length > uri.length ? fullUri.substring(uri.length, fullUri.length) : ''}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (allowPreview) {
|
||||
return (
|
||||
<div className={classnames('claim-link')}>
|
||||
<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>
|
||||
<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}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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} />
|
||||
)}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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', {
|
||||
'content__cover--disabled': disabled,
|
||||
'content__cover--theater-mode': videoTheaterMode && !isMobile,
|
||||
'content__cover--text': isText,
|
||||
'card__media--nsfw': obscurePreview,
|
||||
})}
|
||||
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', {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))));
|
||||
};
|
||||
|
|
|
@ -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.`,
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue